2025年6月15日 星期日

Python 學習筆記 : 用 Altair 繪製互動式圖表 (三) 長條圖

本篇旨在測試 Altair 套件的長條圖繪製方法 chart.make_bar(). 

本系列之前一篇文章 :


摘要說明 Altair 繪製圖表的六個步驟 : 
  1. 建立資料 : DataFrame 
  2. 建立圖表物件 : 呼叫建構式 Chart() 並傳入 DataFrame
  3. 指定圖表類型 : 例如折線圖 mark_line()
  4. 指定欄位如何呈現 : 呼叫 encode() 方法
  5. 設定外觀屬性 : 呼叫 properties() 方法
  6. 設定互動性 : 呼叫 interactive() 方法
從第二步建立 Chart 物件後, 每一個方法都會傳回更新 JSON 設定後的 Chart 物件, 故這些方法可使用鏈式呼叫. 

繪製長條圖第三步驟要呼叫 Chart 物件的 mark_bar() 方法, 其常用參數如下表所示 :


mark_bar() 參數 說明
color 設定長條的顏色,可為 CSS 色碼(如 'red' 或 '#ff0000')或欄位名稱
opacity 長條透明度,介於 0(完全透明)到 1(不透明)之間
size 長條寬度(水平長條為高度),預設依欄位自動計算 (也可指定 px)
cornerRadius 設定長條的四角圓弧程度(單一值套用四角,半徑 px)
cornerRadiusTopLeft
cornerRadiusTopRight
cornerRadiusBottomLeft
cornerRadiusBottomRight
分別設定長條四個角的圓弧大小 (半徑 px)
stroke 長條邊框顏色
strokeWidth 長條邊框寬度(像素)
strokeOpacity 長條邊框透明度


例如 : 


測試 1 : 預設的長條圖 [看原始碼] 

# altair-bar-test-1.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': ['1月', '2月', '3月', '4月', '5月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar().encode(
    x='x',
    y='y',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-1.html')

結果如下 : 




但是如果將 X 軸改為全中文, 排序就會因為 Unicode 問題而亂掉, 例如 : 


測試 2 : X 軸類別資料的 Unicode 排序問題 [看原始碼] 

# altair-bar-test-2.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar().encode(
    x='x',
    y='y',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-2.html')

此例將 X 軸內容改為全中文, 結果 X 軸順序並非預期的照一月, 二月, ... 排列, 三月在二月前面, 五月在四月前面 :




因為 Altair 是基於 Vega-Lite 的宣告式視覺化系統, 它會將名目 (Nominal) 資料視為無序, 然後根據內部排序 Unicode 字典排序機制來排序, 於是 X 軸順序變成 : 

'一' (U+4E00)  → '三' (U+4E09) → '二'  (U+4E8C) → '五' (U+4E94) → '四' (U+56DB)

解決辦法是在 encode() 中用 X 軸的軸物件 alt.X() 用 sort 參數列舉正確順序, 例如 : 


測試 3 : 用軸物件 alt.X() 的 sort 參數列舉名目資料的順序 [看原始碼] 

# altair-bar-test-3.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar().encode(
    x=alt.X('x', sort=['一月', '二月', '三月', '四月', '五月']),   # 列舉 X 軸順序
    y='y',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-3.html')

結果如下 : 




這樣 X 軸順序就正確了. 另一個方法是用 Pandas 的 pd.Categorical() 函式將 X 軸由名目型 (Nominal) 資料改為類別型資料 (Categorical), 這樣 Altair 就會遵循該類型之順序, 如果 X 軸資料很多不勝列舉, 這方法最方便, 例如 :


測試 4 : 將 X 軸由名目型資料改為類別型資料 [看原始碼] 

# altair-bar-test-4.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
df['x']=pd.Categorical(df['x'], categories=df['x'].tolist(), ordered=True)  # 改為類別型資料
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar().encode(
    x='x', # 將資料集中的欄位 'x' 對應到 X 軸的視覺通道
    y='y',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-4.html')

此例呼叫 pd.Categorical() 先將 X 軸由名目型資料改為類別型資料, 結果與上例一樣 X 軸排序正確. 




軸標籤預設是資料欄位名稱 x, y, 下列範例透過直接更改欄位名稱來改變軸標籤, 同時測試直條的 color, size, opacity, 以及邊框的三個參數 :


測試 5 : 修改欄位名稱與測試直條的 color, size, opacity, 與邊框的三個參數 [看原始碼] 

# altair-bar-test-5.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    color='orange',  # 設定長條顏色
    size=30,   # 設定長條寬度 px
    opacity=0.5,   # 設定長條透明度
    stroke='blue',   # 設定邊框顏色
    strokeWidth=2,   # 設定邊框寬度 px 
    strokeOpacity=0.5   # 設定邊框透明度
    ).encode(
    x=alt.X('月份', sort=['一月', '二月', '三月', '四月', '五月']),   # 列舉 X 軸順序
    y='營收',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-5.html')

此例修改了 DataFrame 的欄位名稱, 由 'x' 與 'y' 改為 '月份' 與 '營收', 軸標籤預設就是欄位名稱, 另外也設定了長寬寬度為 30 px, 以及邊框的樣式, 結果如下 :




長條的四個邊角可以用 cornerRadius 參數一次設定四個邊角圓弧半徑, 例如 : 


測試 6 : 用 cornerRadius 參數一次設定四個邊角圓弧半徑 [看原始碼] 

# altair-bar-test-6.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    color='rgba(0, 255, 255, 0.5)',  # 設定長條顏色
    size=50,   # 設定長條寬度 px
    stroke='navy',   # 設定邊框顏色
    cornerRadius=10  # 設定長條四個邊角圓弧半徑 px
    ).encode(
    x=alt.X('月份', sort=['一月', '二月', '三月', '四月', '五月']),   # 列舉 X 軸順序
    y='營收',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-6.html')

結果如下 : 




也可以用 cornerRadiusTopLeft, cornerRadiusTopRight, cornerRadiusBottomLeft, 與  cornerRadiusBottomRight 這四個參數分別設定四個邊角的圓弧半徑, 例如 : 


測試 7 : 分別設定四個邊角圓弧半徑 [看原始碼] 

# altair-bar-test-7.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    color='cyan',  # 設定長條顏色
    size=50,   # 設定長條寬度 px
    cornerRadiusTopLeft=10,
    cornerRadiusTopRight=10,
    cornerRadiusBottomLeft=5,
    cornerRadiusBottomRight=5
    ).encode(
    x=alt.X('月份', sort=['一月', '二月', '三月', '四月', '五月']),   # 列舉 X 軸順序
    y='營收',
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-7.html')

結果如下 :




前面的三個範例我們直接修改 DataFrame 欄位名稱來設定軸標籤為 '月份' 與 '營收', 下面範例則是要在呼叫 encode() 時利用 Axis 軸物件的 title 屬性來設定 : 


測試 8 : 用軸物件 Axis 設定 X, Y 軸標籤 [看原始碼] 

# altair-bar-test-8.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    size=50,  # 設定直條寬度
    color='purple'
    ).encode(
    x=alt.X(
        'x',
        sort=['一月', '二月', '三月', '四月', '五月'], # 列舉 X 軸順序
        axis=alt.Axis(title='月份')  # 設定 X 軸標籤
        ),  
    y=alt.X(
        'y',
        axis=alt.Axis(title='營收(元)')   # 設定 Y 軸標籤
        )
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-8.html')

此例 DataFrame 使用 'x' 與 'y' 為欄位名稱, 在 encode() 中用 alt.X() 與 alt.Y() 對應 x, y 軸到視覺通道時傳入 axis 參數, 其值為呼叫 alt.Axis() 建立之軸物件, 只要傳入 title 參數即可設定軸標籤, 結果如下 : 




除了 title 外, alt.Axis() 建構式還有其他參數可設定軸標籤樣式, 如下表所示 :


參數名稱 說明
title 軸的標籤文字(預設為欄位名)
titleFontSize 標籤文字的字型大小
titleColor 標籤文字顏色
labelAngle 軸刻度文字旋轉角度(如 -45)
labelFontSize 軸刻度文字的字型大小
labelColor 軸刻度文字顏色
grid 是否顯示格線(布林值)
format 指定數值格式(如 ".2f" 表示小數兩位)
orient 軸的位置,如 "bottom"、"top"、"left"、"right"
ticks 顯示的刻度數量
zindex 控制圖層順序(數字愈大圖層愈上方)


例如 : 


測試 9 : 用 Axis 軸物件的參數設定軸物件樣式 [看原始碼] 

# altair-bar-test-9.py
import altair as alt
import pandas as pd

# 建立資料
df=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
# 建立繪製折線圖的 Chart 物件
chart=alt.Chart(df).mark_bar(
    size=50,  # 設定直條寬度
    color='purple'
    ).encode(
    x=alt.X(
        'x',
        sort=['一月', '二月', '三月', '四月', '五月'], # 列舉 X 軸順序
        axis=alt.Axis(
            title='月份',
            titleFontSize=16,   # 設定軸標籤文字字型大小
            titleColor='blue',  # 設定軸標籤文字顏色
            labelAngle=-45,     # 設定軸刻度文字旋轉角度
            labelFontSize=12    # 設定軸刻度字型大小
            )  # 設定 X 軸標籤
        ),  
    y=alt.X(
        'y',
        axis=alt.Axis(
            title='營收(元)',
            titleFontSize=16,   # 設定軸標籤文字字型大小
            titleColor='blue',  # 設定軸標籤文字顏色
            labelAngle=-45,     # 設定軸刻度文字旋轉角度
            labelFontSize=12    # 設定軸刻度字型大小
            )   # 設定 Y 軸標籤
        )
    ).properties(
    width=400,   # 圖片寬 (px)
    height=300,  # 圖片高 (px)
    title='Altair 長條圖'  # 圖片標題
    )
chart.save('altair-bar-test-9.html')

結果如下 :




最後來測試在 Altair 要如何繪製兩組 Y 軸資料的群組化 (group) 長條圖, 這須滿足兩個條件 : 
  • DataFrame 資料須從寬格式 (兩組分欄必列) 合併為長格式 (X 軸資料重複兩份, Y 軸兩欄串接合併為一欄), 並且增加一個分群欄位標示 Y 軸資料屬於哪一群. 
  • 呼叫 encode() 時傳入 xOffset='分群欄位名稱'
以下面的原始資料為例 :

x=['一月', '二月', '三月', '四月', '五月']
y1=[120000, 135000, 99000, 150000, 170000]
y2=[100000, 125000, 87000, 140000, 160000]

其寬格式的 DataFrame 如下 :

df=pd.DataFrame({
    '月份': x,
    '今年營收': y1,
    '去年營收': y2
    })

結果如下 : 




其長格式的 DataFrame 如下 :

df=pd.DataFrame({
    '月份': x * 2,
    '營收': y1 + y2,
    '類別': ['今年'] * len(x) + ['去年'] * len(x)
    })

結果如下 :




可見轉成長格式後月份變成兩倍, 營收今年串接去年, 用類別欄區分今年還是去年.


測試 10 : 繪製兩組 Y 軸資料的群組式長條圖 (垂直) [看原始碼] 

# altair-bar-test-10.py
import altair as alt
import pandas as pd

# 原始資料
x=['一月', '二月', '三月', '四月', '五月']
y1=[120000, 135000, 99000, 150000, 170000]
y2=[100000, 125000, 87000, 140000, 160000]

# 將原始資料轉成長格式的 DataFrame
df=pd.DataFrame({
    '月份': x * 2,
    '營收': y1 + y2,
    '類別': ['今年'] * len(x) + ['去年'] * len(x)
    })

# 建立群組長條圖
chart=alt.Chart(df).mark_bar().encode(
    x=alt.X('月份:N', title='月份', sort=['一月', '二月', '三月', '四月', '五月']),
    y=alt.Y('營收:Q', title='營收金額 (元)'),
    color=alt.Color('類別:N', title='類別'),
    xOffset='類別:N',  # 依照指定欄位群組化顯示長條
    tooltip=['月份', '類別', '營收']
    ).properties(
        width=400,
        height=300,
        title='今年與去年每月營收比較'
    )   
chart.save('altair-bar-test-10.html')

群組化的關鍵就是在 encode() 中用 xOffset 參數指定兩組 Y 軸資料的分群欄位名稱, 結果如下 :




可見兩組以上資料 Altair 會自動出現圖例 (來自 xOffset 指定之分群欄位), 毋須設定. 

如果原始資料本來就是寬格式的 DataFrame 例如 : 

df=pd.DataFrame({
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '今年營收': [120000, 135000, 99000, 150000, 170000],
    '去年營收': [100000, 125000, 87000, 140000, 160000]
    })

則可用 Pandas 的 melt() 函式將今年與去年營收這兩個欄位融化 (串接) 變成長格式, 例如 :

df_long=df.melt(
    id_vars=['月份'],                # 保持不變的欄位 (其他欄位要被融化)
    value_name='營收',           # 融化後新的數值欄位名稱
    var_name='類別'                # 要新增的分類欄位名稱
    )

融化 (串接) 後的值會放進 value_name 指定的新增欄位 '營收' 裡, 而被融化的兩個欄位名稱則會被當作分類名稱填入新增的 '類別' 欄位裡, 結果如下 :




用欄位的 str 屬性值呼叫 replace() 即可去除類別欄中的 '營收' : 

df_long['類別']=df_long['類別'].str.replace('營收', '')

結果如下 :




其實原始 DataFrame 的營收欄位名稱改用 '今年' 與 '去年' 就不需要這個修正指令了. 

如果要繪製水平的群組式長條圖只要將 X 與 Y 軸對調, 並且在 encode() 中改用 yOffset 參數設定分群欄位名稱即可, 例如 : 


測試 11 : 繪製兩組 Y 軸資料的群組式長條圖 (水平) [看原始碼] 

# altair-bar-test-11.py
import altair as alt
import pandas as pd

# 原始資料
x=['一月', '二月', '三月', '四月', '五月']
y1=[120000, 135000, 99000, 150000, 170000]
y2=[100000, 125000, 87000, 140000, 160000]

# 將原始資料轉成長格式的 DataFrame
df=pd.DataFrame({
    '月份': x * 2,
    '營收': y1 + y2,
    '類別': ['今年'] * len(x) + ['去年'] * len(x)
    })

# 建立群組長條圖
chart=alt.Chart(df).mark_bar().encode(
    x=alt.X('營收:Q', title='營收金額 (元)'),
    y=alt.Y('月份:N', title='月份', sort=['一月', '二月', '三月', '四月', '五月']),
    color=alt.Color('類別:N', title='類別'),
    yOffset='類別:N',  # 依照指定欄位群組化顯示長條
    tooltip=['月份', '類別', '營收']
    ).properties(
        width=400,
        height=300,
        title='今年與去年每月營收比較'
    )   
chart.save('altair-bar-test-11.html')

結果如下 :




值得一提的是, 長條圖會以水平或垂直呈現, Altair 預設是以所謂的 Smart defaults 繪圖邏輯規則依據 channel type 與資料型別自動決定的, 此規則摘要如下表 :


資料型別組合 顯示圖形
x: 數值, y: 類別 水平長條圖 ✅
x: 類別, y: 數值 垂直長條圖 ✅


上面範例的原始資料都是 x: 類別, y: 數值之組合, 因此預設以垂直長條圖呈現, 如果想讓 x, y 不受推論自動作用影響, 就要用 alt.X('欄位:型別') 明確指定型別與軸, 亦即將資料欄位對應到視覺通道. 

沒有留言 :