2024年12月15日 星期日

Python 學習筆記 : 簡單好用的 Web app 套件 gradio (六)

經過前幾篇測試我覺得 Gradio 真是一個非常好的 Web app 工具, 可以讓資料分析工程師專注於資料本身, 毋須花大把時間在前端技術上, 這種功能以前使用 Flask 可是要花不少功夫呢. 本篇繼續來測試 Gradio 的繪圖功能.


使用 Gradio 繪製統計圖是以 Pandas 的 DataFrame 做為資料來源 (value 參數), 以下測試主要是在 Gradio Playground 平台上進行, 並將 Web app 佈署到 Hugging Face Spaces 執行空間, 這兩個平台都預載了 Pandas, 可直接 import 使用. 


九. 用 Gradio 繪製統計圖 : 

Gradio 提供了如下之繪圖元件 :


 Gradio 繪圖元件 常用參數
 LinePlot (折線圖) title,x_title, y_title, x, y, value, color, interactive
 ScatterPlot (散佈圖) title,x_title, y_title, x, y, size, color, interactive
 BarPlot (長條圖) title,x_title, y_title, x, y, value, color, grouped, stacked, interactive
 Histogram (直方圖) title,x_title, y_title, x, y, bins, color, interactive
 AreaPlot (區域圖) title,x_title, y_title, x, y, stacked, color, interactive
 Heatmap (熱點圖) title,x_title, y_title, x, y, x, value, color, interactive
 TimeSeries (時間序列圖) title,x_title, y_title, x, y, value, color, interactive


注意, Gradio 的所有繪圖元件均具可透過設定 interactive=True 開啟互動功能 (預設 False). 


1. 用 LinePlot() 繪製折線圖 : 

折線圖用來顯示一組或多組資料的變化與關係, LinePlot() 常用參數如下 :
  • value : 要顯示的資料, 可以是 Pandas DataFrame 或 JSON 格式. 
  • x : 指定要做為 x 軸的 DataFrame 欄索引名稱 
  • y : 指定要做為 y 軸的 DataFrame 欄索引名稱
  • color : 指定要做為分組標籤的 DataFrame 欄索引名稱
  • title : 圖表的標題 (字串)
  • x_title : x 軸標籤
  • y_title : y 軸標籤
  • interactive : 圖表是否具有互動性 True/False (預設)
資料參數 value 通常是傳入一個 Pandas 的 DataFrame 物件, 這比較簡單且直觀, 如果是用 JSON 格式的字典串列的話處理起來較麻煩. 

範例程式碼 :

import gradio as gr
import pandas as pd

data=pd.DataFrame({
    'x': ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'],
    'y': [23, 20, 21, 17, 15, 14, 12]
    })

plot=gr.LinePlot(
    value=data, 
    x='x', 
    y='y', 
    title='本周溫度變化', 
    x_title='星期', 
    y_title='溫度', 
    interactive=True
    )

iface=gr.Interface(
    fn=lambda: data
    inputs=[], 
    outputs=plot,
    title='折線圖 LinePlot() 測試',
    flagging_mode='never',
    )

iface.launch()

本例顯示一周溫度變化之折線圖, 因為不須用指名函式處理資料, 所以 fn 參數使用 lambda 匿名函式傳入一個 DataFrame, 它會被傳遞給 LinePlot 物件繪製圖形. 其次, 由於不需要輸入, 故 inputs參數要傳入空串列 (不要傳入 None, 雖然也不會出現錯誤). 結果如下 :




當滑鼠移動到圖表上時會顯示資料點的 X, Y 軸值, 這就是互動圖表的功能. 

此 Web App 網址如下 :


如果要繪製兩組以上的折線圖, 那就要在 data 中添加另一個 Y 軸資料, 但需要用到 Pandas 的 melt() 方法將資料擴展, 且要用 color 參數指定分組標籤, 例如下面程式是比較本周與前一周的溫度變化 :

import gradio as gr
import pandas as pd

data=pd.DataFrame({
    'x': ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'],
    '本周': [23, 20, 21, 17, 15, 14, 12],       # 第一組資料
    '前一周': [26, 25, 24, 27, 25, 24, 23]    # 第二組資料
    })

data_long=data.melt(id_vars='x', var_name='label', value_name='y')

plot=gr.LinePlot(
    value=data_long, 
    x='x', 
    y='y',
    color='label',   
    title='本周溫度變化', 
    x_title='星期', 
    y_title='溫度', 
    interactive=True
    )

iface=gr.Interface(
    fn=lambda: data, 
    inputs=[], 
    outputs=plot,
    title='折線圖 LinePlot() 測試',
    flagging_mode='never',
    )

iface.launch()

此例第一組資料為本周溫度, 在 data 中添加前一周溫度為第一組資料, 這時在 LinePlot() 要傳入 color 參數指定分組標籤 (這會決定 legend 圖例), 然後在呼叫 DataFrame 物件的 melt() 方法將寬格式資料展開為長格式資料時要指定 var_name 為此分組標籤 (此處為 'label'), 結果如下 :




可見指定 color 參數使兩個 Y 軸欄索引變成圖例 (legend) 了.  

此 Web App 網址如下 :



2. 用 ScatterPlot() 繪製散佈圖 : 

散佈圖用來顯示兩組資料之間的關係 (可觀察分佈模式), ScatterPlot() 函式支持分組, 標籤和互動等功能, 透過顏色分組可對比不同類別的資料特徵. 

常用參數與上面折線圖相同 : 
  • value : 要顯示的資料, 可以是 Pandas DataFrame 或 JSON 格式. 
  • x : 指定要做為 x 軸的 DataFrame 欄索引名稱 
  • y : 指定要做為 y 軸的 DataFrame 欄索引名稱
  • color : 指定要做為分組標籤的 DataFrame 欄索引名稱
  • title : 圖表的標題 (字串)
  • x_title : x 軸標籤
  • y_title : y 軸標籤
  • interactive : 圖表是否具有互動性 True/False (預設)
範例程式碼 (1) :

import gradio as gr
import pandas as pd

data=pd.DataFrame({
    'x': [1, 2, 3, 4, 5, 6, 7, 8],
    'y': [2.5, 3.1, 4.7, 3.8, 5.5, 6.8, 7.9, 8.2], 
    })

plot=gr.ScatterPlot(
    value=data, 
    x='x', 
    y='y', 
    title='練習次數與平均成績的關係', 
    x_title='練習次數', 
    y_title='平均成績(0~10)', 
    interactive=True
    )

iface=gr.Interface(
    fn=lambda: data, 
    inputs=[], 
    outputs=plot,
    title='分佈圖 ScatterPlot() 測試',
    flagging_mode='never',
    )

iface.launch()

此例資料紀錄選手賽前練習次數與平均成績之關係, 結果如下 :




從分布圖可知練習次數與平均成績呈正相關關係. 

此 Web App 網址如下 :


不過 Gradio 的 ScatterPlot() 與 Matplotlib 的 scatter() 不同的是, ScatterPlot() 的繪圖時 x, y 軸總是從原點開始, 缺點是若 x, y 值都很小或很大, 那麼分布圖會繪製在離原點很遠的小角落, 導致無法看出資料間的關係; 而 Matplotlib 的 scatter() 則會自動將原點移到比 x, y 軸最小值還低一些的地方, 這樣就能將視野聚焦在資料分布的區域. 

範例程式碼 (2) :

import gradio as gr
import pandas as pd

data=pd.DataFrame({
    'x': [49, 65, 53, 45, 56, 47, 52, 61],
    'y': [159, 177, 156, 163, 164, 158, 166, 171] 
    })

plot=gr.ScatterPlot(
    value=data, 
    x='x', 
    y='y', 
    title='身高與體重關係', 
    x_title='體重(Kg)', 
    y_title='身高(Cm)', 
    interactive=True
    )

iface=gr.Interface(
    fn=lambda: data, 
    inputs=[], 
    outputs=plot,
    title='分佈圖 ScatterPlot() 測試',
    flagging_mode='never',
    )

iface.launch()

此例最低體重 45Kg, 最低身高 158Cm, 都離原點甚遠, 其分布點聚集在右上角, 由於尺度被壓縮了, 使得 x, y 的關係無法清楚地看出來 :




此 Web App 網址如下 :


資料取自之前測試 Matplotlib 時所用之體重與身高關聯性資料, 參考 :


如果使用 Matplotlib 的 scatter() 函式畫分布圖, 它會自動移動畫布原點讓畫布能呈現整個資料分布區域的全貌, Gradio 目前無法做到這樣的功能, 但能透過 Matplotlib 繪圖後將結果交給 Gradio 的 Plot() 函式輸出了, 如下面範例所示 :

範例程式碼 (3) :

import gradio as gr
import pandas as pd
import matplotlib.pyplot as plt

def plot_scatter_chart():
    plt.figure(figsize=(8, 5))
    x=[49, 65, 53, 45, 56, 47, 52, 61]
    y=[159, 177, 156, 163, 164, 158, 166, 171]
    plt.scatter(x, y, color='blue', alpha=0.75)
    plt.title('Relationship between Height and Weight')  # 圖表標題
    plt.xlabel('Weight (Kg)')  # X 軸標籤
    plt.ylabel('Height (Cm)')  # Y 軸標籤
    plt.grid(True, linestyle='--', alpha=0.5)  # 添加網格
    plt.tight_layout()  # 自動調整布局
    return plt.gcf()  # 傳回 Figure 物件

iface=gr.Interface(
    fn=plot_scatter_chart,  # 繪製函數
    inputs=[],              # 無需用戶輸入
    outputs=gr.Plot(),      # 輸出為 Gradio 的 Plot 物件
    title='Scatter Plot Test(with Matplotlib)',
    flagging_mode='never',
    )

iface.launch()

此例以 Gradio 的 Plot 物件做為輸出對象, 處理函式參數 fn 指定呼叫 Matplotlib 寫的繪圖函式來繪製散佈圖, 將繪圖結果的 Figure 畫布物件傳回給 Gradio 的 Plot 物件繪製即可, 結果如下 :




可見由於 Matplotlib 會自動移動圖表的原點以聚焦在資料分布之區域, 故能較清楚觀察資料間的似乎呈現一種正相關關係. 

此 Web App 網址如下 : 


注意, 由於 Hugging Face Spaces 執行空間並未預載 Matplotlib, 因此需編輯 requirements.txt 加入matplotlib 於佈署時安裝此套件才能執行繪圖 : 


3. 用 BarPlot() 繪製長條圖 : 

長條圖主要用來顯示類別資料的數量變化, 透過柱狀高度或長度來觀察不同類別之數量變化情形. BarPlot() 函式支持分組, 標籤和互動等功能, 透過 color 參數指定之分組欄可對比不同分組之類別資料差異. 注意, Gradio 目前僅支援垂直長條圖, 不支援水平長條圖.

常用參數如下 : 
  • value : 要顯示的資料, 可以是 Pandas DataFrame 或 JSON 格式. 
  • x : 指定要做為 x 軸的 DataFrame 欄索引名稱 
  • y : 指定要做為 y 軸的 DataFrame 欄索引名稱
  • color : 指定要做為分組標籤的 DataFrame 欄索引名稱
  • title : 圖表的標題 (字串)
  • x_title : x 軸標籤
  • y_title : y 軸標籤
  • interactive : 圖表是否具有互動性 True/False (預設)
  • grouped: True/False (預設), 用來是否多組長條圖並列顯示 
  • stacked: True (預設)/False, 用來是否多組長條圖堆疊顯示
注意, BarPlot() 的 y 參數只能指定一個欄, 不能傳入多欄串列, 也不能像 Matplotlib 那樣指定 y1, y2, ... 等多個 y 軸來顯示多組長條圖, 當有多組資料時需用 color 參數指定分組欄位, y 軸資料與指定之分組欄位均需對應擴展.

範例程式碼 (1) :

import gradio as gr
import pandas as pd

data=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月', '六月'],
    'y': [12000, 15000, 17000, 13000, 19000, 21000], 
    })

plot=gr.BarPlot(
    value=data, 
    x='x', 
    y='y', 
    title='上半年月銷售額變化', 
    x_title='月份', 
    y_title='銷售額(萬元)', 
    interactive=True
    )

iface=gr.Interface(
    fn=lambda: data, 
    inputs=[], 
    outputs=plot,
    title='長條圖 BarPlot() 測試',
    flagging_mode='never',
    )

iface.launch()

結果如下 :




此 Web App 網址如下 :


如果要顯示多組資料的長條圖要用到 color, grouped, 與 stacked 參數, 如上所述, 目前 Gradio 版本的 BarPlot() 只能指定一個 y 軸資料, 因此如果要顯示多組資料的長條圖, 必須將各組資料串成一個串列, 然後用分組欄位標示是哪一組資料, 例如上面顯示的是銷售一課的業績, 如果加入銷售二課業績 : 

'銷售額': [11000, 14000, 16000, 12000, 18000, 20000],  # 銷售二課

那麼要將其合併擴展為一個串列做為 y 軸資料 : 

'銷售額': [12000, 15000, 17000, 13000, 19000, 21000,  # 銷售一課
              11000, 14000, 16000, 12000, 18000, 20000],  # 銷售二課

然後增加一個分組欄位來對應每一筆資料所屬分組  :

'部門': ['銷售一課'] * 6 + ['銷售二課'] * 6  # 部門標籤

然後傳入 color 參數指定分組欄位為 '部門' 即可. 

範例程式碼 (2) :

import gradio as gr
import pandas as pd

data=pd.DataFrame({  # 將資料轉為長格式
    '月份': ['一月', '二月', '三月', '四月', '五月', '六月'] * 2,
    '銷售額': [12000, 15000, 17000, 13000, 19000, 21000,  # 銷售一課
              11000, 14000, 16000, 12000, 18000, 20000], # 銷售二課
    '部門': ['銷售一課'] * 6 + ['銷售二課'] * 6            # 分組標籤
    })

plot=gr.BarPlot(
    value=data,
    x='月份',          # X 軸:月份
    y='銷售額',        # Y 軸:銷售額
    color='部門',      # 分組欄位
    title='上半年月銷售額變化',
    x_title='月份',
    y_title='銷售額(萬元)',
    interactive=True
    )

# 建立介面
iface=gr.Interface(
    fn=lambda: data,  
    inputs=[],        
    outputs=plot,
    title='分組長條圖 BarPlot() 測試',
    flagging_mode='never',
    )

iface.launch()

結果如下 : 




可見預設會顯示堆疊長條圖, 這是因為 stacked 參數預設是 True, 而 grouped 參數預設是 False 之故. 此例 Web app 網址如下 :


但是如果將 stacked 設為 False 與 grouped 設為 True, 並不會顯示並列的長條圖, 這可能是 Gradio 是高階的套件, 沒辦法像 Matplotlib 那樣支援低階繪圖功能所致. 如果要繪製並列的長條圖, 可以像上面分布圖做法, 利用 Matplotlib 畫好圖後將 Figure 物件傳給 Gradio 的 Plot() 顯示即可. 

範例程式碼 (3) :

import gradio as gr
import matplotlib.pyplot as plt
import numpy as np

def plot_grouped_barchart():
    plt.rcParams["font.family"]=["Microsoft JhengHei"]
    months=['一月', '二月', '三月', '四月', '五月', '六月']  # 月份
    x=np.arange(len(months))  # 數值型索引
    y1=[12000, 15000, 17000, 13000, 19000, 21000]  # 銷售一課
    y2=[11000, 14000, 16000, 12000, 18000, 20000]  # 銷售二課    
    width=0.35  # 長條寬度
    # 繪製長條圖
    plt.figure(figsize=(8, 5))
    plt.bar(x - width/2, y1, width, label='銷售一課', color='cyan')
    plt.bar(x + width/2, y2, width, label='銷售二課', color='orange')
    # 設定標籤與標題
    plt.xlabel('月份')
    plt.ylabel('銷售額(萬元)')
    plt.title('上半年月銷售額變化 (並排分組)')
    plt.xticks(x, months)  # 設定 x 軸刻度標籤
    plt.legend()  # 顯示圖例
    plt.tight_layout()  # 自動調整布局
    return plt.gcf()  # 傳回 Figure 物件

iface=gr.Interface(
    fn=plot_grouped_barchart,
    inputs=[],
    outputs=gr.Plot(),
    title='分組長條圖測試',
    flagging_mode='never'
    )

iface.launch()

此例先利用 X 軸刻度 (月份字串串列) 產生可計算之數值刻度, 這樣才能計算兩組資料的 X 軸偏移位置以繪製並立的長條. 繪圖完成後呼叫 plt.gcf() 取得 Figure 畫布物件並將其傳回給 gradio.Plot() 函式繪製. 由於 Gradio 沒有所需之中文微軟正黑體字型, 所以改在本機執行上面的程式, 結果如下 :




當然也可以用 Matplotlib 的 barh() 繪製水平的分組並立長條圖, 但要將 x, y 軸顛倒. 下面範例除了改用 barh() 繪製水平長條圖外, 還將中文改成英文, 這樣才能在 Gradio 與 Hugging Face Spaces 中呈現與佈署 : 

範例程式碼 (3) :

import gradio as gr
import matplotlib.pyplot as plt
import numpy as np

def plot_grouped_barchart():
    months=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']  # 月份
    y=np.arange(len(months))  # 數值型索引
    x1=[12000, 15000, 17000, 13000, 19000, 21000]  # 銷售一課
    x2=[11000, 14000, 16000, 12000, 18000, 20000]  # 銷售二課    
    height=0.35  # 長條寬度
    # 繪製長條圖
    plt.figure(figsize=(8, 5))
    plt.barh(y - height/2, x1, height, label='Sales Team 1', color='cyan')
    plt.barh(y + height/2, x2, height, label='Sales Team 2', color='orange')    # 設定標籤與標題
    plt.ylabel('Months')
    plt.xlabel('Sales (in ten thousand)')
    plt.title('Monthly Sales Performance (Grouped)')
    plt.yticks(y, months)  # 設定 y 軸刻度標籤
    plt.legend()  # 顯示圖例
    plt.tight_layout()  # 自動調整布局
    return plt.gcf()  # 返回圖表對象

iface=gr.Interface(
    fn=plot_grouped_barchart,
    inputs=[],
    outputs=gr.Plot(),
    title='Grouped Bar Chart Test(Horizontal)',
    flagging_mode='never'
    )

iface.launch()

結果如下 : 




此例 Web app 網址如下 :


沒有留言 :