[python] 基於PyWaffle庫繪製華夫餅圖

落痕的寒假發表於2024-04-30

華夫餅圖Waffle chart是一種獨特而直觀的圖表,用於表示分類資料。它採用網格狀排列的等大小方格或矩形,每個方格或矩形分配不同的顏色或陰影來表示不同的類別。這種視覺化方法有效地傳達了每個類別在整個資料集中的相對比例。本文介紹如何使用基於Python的PyWaffle庫繪製華夫餅圖。PyWaffle開源倉庫地址見:PyWaffle,PyWaffle官方文件見:PyWaffle-docs。本文程式碼下載地址:Python-Study-Notes

PyWaffle安裝方式如下:

pip install -U pywaffle

# PyWaffle庫依賴於matplotlib繪製圖片
import matplotlib.pyplot as plt
from pywaffle import Waffle

目錄
  • 1 使用簡介
    • 1.1 基本使用
      • 1.1.1 基礎繪圖
      • 1.1.2 類別比例設定
    • 1.2 樣式設定
      • 1.2.1 展示效果設定
      • 1.2.2 繪圖框架自定義
      • 1.2.3 方格樣式變換
  • 2 參考

1 使用簡介

1.1 基本使用

1.1.1 基礎繪圖

PyWaffle庫透過其Waffle類提供了一種便捷的方式來建立華夫餅圖。每個類別的方格佔比可以透過引數values進行設定,資料可以是列表、字典和Pandas.DataFrame結構。然後可以使用rows和columns引數來定製行數和列數。只需要指定其中一個引數,另一個引數可以根據values的總和推斷出來。

# 新建繪圖影像
fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    # columns=10,
    values=[30, 16, 4]
)
# 儲存結果
# fig.savefig("plot.png", bbox_inches="tight")
plt.show()

png

如果value引數輸入的是比例值,則必須設定rows和columns屬性來定義值的顯示方式。

fig = plt.figure(
    FigureClass=Waffle,
    rows=2,
    columns=5,
    values=[0.2, 0.5, 0.3]
)
plt.show()

png

如果values總和與rows*columns的結果不一致,Waffle類將values解釋為各部分的相對比例。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    columns=5,
    values=[30, 16, 4]
)
plt.show()

png

當字典傳遞給values時,鍵將用作標籤並顯示在圖例中,注意圖例會預設顯示。

plt.figure(
    FigureClass=Waffle,
    rows=5,
    values={'Cat': 30, 'Dog': 16, 'Bird': 4},
    legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1), 'frameon': False} # 修改圖例
)
plt.show()

png

不顯示圖例。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values={'Cat': 30, 'Dog': 16, 'Bird': 4},
    legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1), 'frameon': False} # 修改圖例
)

# 移除legend
fig.axes[0].get_legend().remove()

plt.show()

png

1.1.2 類別比例設定

如果values的總和與rows*columns的結果不一致,Waffle類將解釋values為各部分的相對比例。如下所示各個類別方塊數輸出為小數,小數位的取捨會導致不同的展示效果:

# nearest, ceil or floor

import numpy as np

target = {'Cat': 48, 'Dog': 46, 'Bird': 3, 'Fish': 9}

values = np.array(list(target.values()))

values_ratio = values /sum(values)
# 華夫餅圖結構
row = 2
col = 5
# 各個類別方塊數
values_ratio * row *col
array([4.52830189, 4.33962264, 0.28301887, 0.8490566 ])

為此,Waffle類透過rounding_rule引數來處理小數位取捨問題。rounding_rule可選值為nearest(預設), ceil或floor。具體如下:

nearest

nearest表示將方塊的數值四捨五入到最接近的整數。

# 各個類別方塊數
np.round(values_ratio * row *col, 0).astype(int)
array([5, 4, 0, 1])
np.round(values_ratio*10,0).astype(int)

plt.figure(
    FigureClass=Waffle,
    rows=2,
    columns=5,
    values=target,
    rounding_rule='nearest', # 預設值
    legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1), 'frameon': False} # 修改圖例
)
plt.show()

png

ceil

ceil表示將方塊的數值向上舍入到最接近的整數。然而,在nearest和ceil模式下,方塊的總和可能會超過華夫餅圖的預設長度,超出預設長度的方塊將不會顯示。這也意味著,輸入類別位置越靠後,其方塊顯示的數值越可能不準確。

# 各個類別方塊數
np.ceil(values_ratio * row *col).astype(int)
array([5, 5, 1, 1])
np.round(values_ratio*10,0).astype(int)

plt.figure(
    FigureClass=Waffle,
    rows=2,
    columns=5,
    values=target,
    rounding_rule='ceil', # 預設值
    legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1), 'frameon': False} # 修改圖例
)
plt.show()

png

floor

floor表示將方塊的數值向下舍入到最接近的整數,由於這種舍入方式的特性,繪製的方塊數可少於預設的數量。

# 各個類別方塊數
np.floor(values_ratio * row *col).astype(int)
array([4, 4, 0, 0])
np.round(values_ratio*10,0).astype(int)

plt.figure(
    FigureClass=Waffle,
    rows=2,
    columns=5,
    values=target,
    rounding_rule='floor', # 預設值
    legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1), 'frameon': False} # 修改圖例
)
plt.show()

png

如果想要在華夫餅圖中完整顯示所有類別,但又不希望方塊數量過多,一種有效的方法是隻設定rows或columns中的一個引數,然後將每個類別的數值除以最小類別的數值,以確保最少類別的方塊數量為1,同時將其他類別的數值顯示為相對於最少類別的比例。

target_ = target.copy()
# 找到最小值
min_value = min(target_.values())

# 更新字典中每個鍵對應的值
target_ = {key: int(value / min_value) for key, value in target.items()}
print(target_)
{'Cat': 16, 'Dog': 15, 'Bird': 1, 'Fish': 3}
np.round(values_ratio*10,0).astype(int)

plt.figure(
    FigureClass=Waffle,
    rows=4,
    values=target_,
    legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1), 'frameon': False} # 修改圖例
)
plt.show()

png

1.2 樣式設定

1.2.1 展示效果設定

標題、標籤和圖例

data = {'Cat': 30, 'Dog': 16, 'Bird': 4}
fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    columns=10,
    values = data,
    title={
        'label': 'Example plot',
        'loc': 'center',
        'fontdict': {
            'fontsize': 20
        }
    },
    labels=[f"{k} ({int(v / sum(data.values()) * 100)}%)" for k, v in data.items()],
    legend={
        #'labels': [f"{k} ({v}%)" for k, v in data.items()],  # labels可以在legend中設定
        'loc': 'lower left',
        'bbox_to_anchor': (0, -0.2),
        'ncol': len(data),
        'framealpha': 0,
        'fontsize': 12
    },
    # 設定顏色條,支援Pastel1, Pastel2, Paired, Accent, Dark2, Set1, Set2, Set3, tab10, tab20, tab20b, tab20c
    cmap_name="Accent",
    facecolor='#DDDDDD',  # 繪圖背景色

    # 也可以單獨設定顏色
    # colors=["#232066", "#983D3D", "#DCB732", "F12F34"]

)

png

方塊寬高比設定

引數block_aspect_ratio透過更改塊的寬度與塊的高度之比來控制方塊的形狀。預設block_aspect_ratio為1,也就是每個方塊都是正方形。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    block_aspect_ratio=1.618
)

png

方塊間距控制

引數interval_ratio_x和interval_ratio_y分別負責調節塊之間的水平和垂直間距。具體來說,interval_ratio_x代表了塊之間的水平間距與塊寬度的比率,而interval_ratio_y則反映了塊之間的垂直間距與塊高度的比率。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    interval_ratio_x=1,
    interval_ratio_y=0.5
)

png

起始方塊繪圖位置

starting_location用於設定起始方塊的位置。這個引數可輸入包括 "NW(西北角、左上角)"、"SW(預設值,西南角,右下角)"、"NE(東北角,右上角)" 和 "SE(東南角,右下角)"。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    starting_location='SE'
)

png

繪圖方向

預設情況下PyWaffle逐列繪製方塊,因此類別是按水平方向排列繪製的,如果按行繪製方塊,則設定vertical引數為True。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    vertical=True
)

png

類別方塊排列方式

block_arranging_style引數用於控圖中各類別方塊的排列方式,具體如下:

  • normal: 預設方式,方塊會按照常規的網格模式進行排列。
  • new-line: 每個類別的方塊都會從新的一行開始排列。這意味著,如果你有多個類別,每行只有一個類別的圖示。
  • snake: 方塊會按照“蛇形”或“之字形”的模式進行排列。
# new-line排列
fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[12, 22, 20, 4],
    vertical=True,
    block_arranging_style='new-line'
)

png

# snake排列
fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[12, 22, 20, 4],
    block_arranging_style='snake'
)

png

1.2.2 繪圖框架自定義

子圖繪製

PyWaffle可以透過新增子圖的方式,實現在同一張圖中顯示多個華夫餅圖。以下是示例:

import pandas as pd
# 建立一個產量表格
data = pd.DataFrame(
    {
        'labels': ['Car', 'Truck', 'Motorcycle'],
        'Factory A': [44183, 12354, 3246],
        'Factory B': [29198, 7678, 2556],
        'Factory C': [9013, 4079, 996],
    },
).set_index('labels')
fig = plt.figure(
    FigureClass=Waffle,
    plots={
        311: { # 311數字對應於 matplotlib中subplot 的位置編碼方式,311表示一個三行一列的佈局中的第一個子圖
            'values': data['Factory A'] / 1000,   # 將實際數字轉換為合理的塊數
            'labels': [f"{k} ({v})" for k, v in data['Factory A'].items()],
            'legend': {'loc': 'upper left', 'bbox_to_anchor': (1.05, 1), 'fontsize': 8},
            'title': {'label': 'Vehicle Production of Factory A', 'loc': 'left', 'fontsize': 12}
        },
        312: {
            'values': data['Factory B'] / 1000,
            'labels': [f"{k} ({v})" for k, v in data['Factory B'].items()],
            'legend': {'loc': 'upper left', 'bbox_to_anchor': (1.2, 1), 'fontsize': 8},
            'title': {'label': 'Vehicle Production of Factory B', 'loc': 'left', 'fontsize': 12}
        },
        313: {
            'values': data['Factory C'] / 1000,
            'labels': [f"{k} ({v})" for k, v in data['Factory C'].items()],
            'legend': {'loc': 'upper left', 'bbox_to_anchor': (1.3, 1), 'fontsize': 8},
            'title': {'label': 'Vehicle Production of Factory C', 'loc': 'left', 'fontsize': 12}
        },
    },
    rows=5,   # 應用於所有子圖的外部引數,與下面相同
    cmap_name="Paired", 
    rounding_rule='ceil', 
    figsize=(5, 5)
)

fig.suptitle('Vehicle Production by Vehicle Type', fontsize=14, fontweight='bold')
# 說明1個方塊代表多少車輛
fig.supxlabel('1 block = 1000 vehicles', fontsize=8, ha='right')
Text(0.5, 0.01, '1 block = 1000 vehicles')

png

現有軸繪圖

Pywaffle提供Waffle.make_waffle方法來在現有的matplotlib軸上繪圖,而不需要重新初始化Waffle例項。

fig = plt.figure()
ax = fig.add_subplot(111)


ax.set_title("Axis Title", loc= "right")
ax.set_aspect(aspect="equal")

Waffle.make_waffle(
    ax=ax,  # 利用現有繪圖物件
    rows=5, 
    columns=10, 
    values=[30, 16, 4], 
    title={"label": "Waffle Title", "loc": "left"}
)

png

此外可以利用make_waffle與已定義座標軸來顯示多個資料結果。

import matplotlib.patches as mpatches 

data = {  
    2021: [175, 139, 96],  
    2022: [232, 187, 126],  
    2023: [345, 278, 195],  
    2024: [456, 389, 267]  
}  
  
df = pd.DataFrame(data,index=['cat', 'dog', 'cow'])  
  
number_of_bars = len(df.columns)
colors = ["darkred", "red", "darkorange"]

# Init the whole figure and axes
fig, axs = plt.subplots(nrows=1,
                        ncols=number_of_bars,
                        figsize=(8,6),)

# Iterate over each bar and create it
for i,ax in enumerate(axs):
    
    col_name = df.columns[i]
    values = df[col_name]/1000
    Waffle.make_waffle(
        ax=ax, 
        rows=20,
        columns=5,
        values=values,
        title={"label": col_name, "loc": "left"},
        colors=colors,
        vertical=True,
        font_size=12
    )
    
fig.suptitle('Animal Type Data Display',
             fontsize=14, fontweight='bold')

legend_labels = df.index
legend_elements = [mpatches.Patch(color=colors[i],
                                  label=legend_labels[i]) for i in range(len(colors))]

fig.legend(handles=legend_elements,
           loc="upper right",
           title="Animal Types",
           bbox_to_anchor=(1.04, 0.5),
           framealpha = 0,
           fontsize=12)
plt.subplots_adjust(right=0.85)
plt.show()

png

新增其他matplotlib繪圖元件

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4]
)
fig.text(
    x=0.5,
    y=0.5,
    s="hello world!",
    ha="center",
    va="center",
    rotation=30,
    fontsize=40,
    color='black',
    alpha=0.3,
    bbox={
        'boxstyle': 'square', 
        'lw': 3, 
        'ec': 'gray', 
        'fc': (0.9, 0.9, 0.9, 0.5), 
        'alpha': 0.3
    }
)
Text(0.5, 0.5, 'hello world!')

png

1.2.3 方格樣式變換

Pywaffle支援字元和圖示來變換方格樣式。

字元

Pywaffe允許一個或多個Unicode字元來替換華夫餅圖中方格的樣式。可用的Unicode字元見:unicode-table

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    colors=["#4C8CB5", "#B7CBD7", "#C0C0C0"],
    characters='😊Ã', # 使用兩個Unicode字元來展示
    font_size=24
)

png

圖示

Pywaffe允許fontawesome中的圖示來替換華夫餅圖中方格的樣式。注意,如果是Windows系統,需要matplotlib 3.5.2及以下版本才能支援該功能。Linux系統對matplotlib版本沒有限制。

import matplotlib
# 檢視matplotlib版本
matplotlib.__version__
'3.8.3'

圖示設定方式如下:

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    colors=["#FD5C46", "#9DDF3D", "#AFFBC1"],  # 設定顏色
    icons='person', # 設定icon
    font_size= 30 # 圖表大小
)

png

為每個類別設定對應的圖示的方式如下所示:

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values={'Cat': 30, 'Dog': 16, 'Cow': 4},
    colors=["#000077", "#139900", "#771234"],
    icons=['cat', 'dog', 'cow'],
    font_size=20,
    icon_legend=True, # 替換legend的標籤為icon
    legend={
        'labels': ['CAT', 'DOG', 'COW'], 
        'loc': 'upper left', 
        'bbox_to_anchor': (1, 1),
        'frameon': False,
    }
)

png

進一步設定圖示的樣式,注意並非每個FontAwesome都擁有不同的圖示樣式,因此在編寫程式碼時務必進行檢查。

fig = plt.figure(
    FigureClass=Waffle,
    rows=5,
    values=[30, 16, 4],
    colors=["#FFA500", "#4384FF", "#C0C0C0"],
    icons=['sun', 'cloud-showers-heavy', 'font-awesome'],
    icon_size=20,
    icon_style=['regular', 'solid', 'brands'],
    icon_legend=False,
    legend={
        'labels': ['Sun', 'Shower', 'Flag'], 
        'loc': 'upper left', 
        'bbox_to_anchor': (1, 1)
    }
)

png

2 參考

  • PyWaffle
  • PyWaffle-docs
  • unicode-table
  • fontawesome

相關文章