HOWTO · Matplotlib
Matplotlib 密度圖
為了使用 Python 生成密度圖,我們首先使用 `scipy.stats` 模組中的 `gaussian_kde()` 方法從給定的資料中估計密度函式,然後繪製密度函式來生成密度圖。
本頁內容
密度圖(density plot)呈現一維樣本資料分布情形的平滑估計。在 Python 中,常見的做法是核心密度估計(kernel density estimation,KDE):使用 scipy.stats 模組的 gaussian_kde() 從觀測值估計密度函式,在一組 x 值的網格上求值,再以 Matplotlib 畫出估計結果的曲線。想以更少的程式碼得到相同結果,可以使用 seaborn 套件的 kdeplot(),或在 pandas.DataFrame.plot() 中設定 kind="density"。以下範例使用 NumPy、SciPy 與 Matplotlib;Seaborn 與 pandas 的寫法還需另外安裝對應套件,而 pandas 的密度圖會在內部呼叫 gaussian_kde,因此需要安裝 SciPy。
使用 scipy.stats.gaussian_kde 建立密度圖
gaussian_kde 建立估計的方式,是在每個觀測值上疊加一個核心(kernel)——一個形狀類似高斯分布的小突起——再將這些突起取平均。結果是一個可呼叫的密度函式:傳入一組 x 位置的網格,它就會回傳每個位置的估計密度,而這正是 Matplotlib 所繪製的線。
首先,在樣本上擬合估計,並在網格上求值:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import numpy as np
from scipy.stats import gaussian_kde
data = [2, 3, 3, 4, 2, 1, 5, 6, 4, 3, 3, 3, 6, 4, 5, 4, 3, 2]
density = gaussian_kde(data)
x = np.linspace(-2, 10, 300)
y = density(x)
print(f"Peak estimated density: {y.max():.4f} at x = {x[np.argmax(y)]:.2f}")
Peak estimated density: 0.2635 at x = 3.14
這個程式在 18 個樣本值上擬合 gaussian_kde,並在 -2 到 10 之間 300 個等距位置上求值;估計在 x = 3 附近達到峰值,因為大多數觀測值都聚集在那一帶。請依據自己資料的範圍選擇網格,例如從略低於最小值到略高於最大值;這裡的 -2 到 10 區間僅適用於這組樣本。
下方的繪圖腳本固定使用 Matplotlib 的非互動式 Agg 後端,因此無論是否連接顯示器都能以相同方式執行;它也將函式庫的日誌訊息靜音,讓首次執行時的字型訊息不會出現在標準錯誤中。執行以下程式碼可以查看選用了哪個後端:
import logging
import matplotlib
matplotlib.use("Agg")
logging.getLogger("matplotlib").setLevel(logging.ERROR)
import matplotlib.pyplot as plt
print("Backend:", matplotlib.get_backend())
Backend: Agg
接著用完整腳本繪製求值後的曲線:
import logging
import matplotlib
matplotlib.use("Agg")
logging.getLogger("matplotlib").setLevel(logging.ERROR)
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import gaussian_kde
data = [2, 3, 3, 4, 2, 1, 5, 6, 4, 3, 3, 3, 6, 4, 5, 4, 3, 2]
density = gaussian_kde(data)
x = np.linspace(-2, 10, 300)
y = density(x)
plt.plot(x, y)
plt.title("Density Plot of the data")
plt.xlabel("Value")
plt.ylabel("Estimated density")
print("Plotted", len(plt.gca().get_lines()), "density line")
Plotted 1 density line
plt.plot() 會將估計密度繪製為目前座標軸上的一條線,而 plt.gca().get_lines() 會回報它所加入的這一條線。在互動式工作階段中,你可以省略 matplotlib.use("Agg") 呼叫,並在繪圖呼叫之後加上 plt.show() 來開啟圖形視窗。
繪製出來的曲線是估計密度:觀測值集中之處較高,稀疏之處較低。由於它是一種估計,請將其形狀視為對這組樣本的一種呈現,而非對真實分布的度量。
調整 KDE 頻寬
頻寬(bandwidth)控制估計被平滑的程度。較大的頻寬會加寬每個核心,產生更平坦、更平滑的曲線,但可能掩蓋真實結構;較小的頻寬會讓核心變窄,使曲線緊貼樣本,但也可能出現僅屬於這份資料集雜訊的起伏。gaussian_kde 預設以 Scott 法則選擇頻寬;你也可以明確傳入 "scott" 或 "silverman",或透過 bw_method 引數提供一個純量,它會相對於資料的離散程度設定核心寬度。之後也可透過公開的 set_bandwidth() 方法進行同樣的調整,因此不需要呼叫私有輔助函式。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import gaussian_kde
data = [2, 3, 3, 4, 2, 1, 5, 6, 4, 3, 3, 3, 6, 4, 5, 4, 3, 2]
density = gaussian_kde(data, bw_method=0.25)
x = np.linspace(-2, 10, 300)
y = density(x)
plt.plot(x, y)
plt.title("Density Plot with a smaller bandwidth")
plt.xlabel("Value")
plt.ylabel("Estimated density")
print(f"Peak estimated density: {y.max():.4f}")
Peak estimated density: 0.3913
使用 bw_method=0.25 時,每個核心都比預設值更窄,因此曲線在峰值處升得更高,也更緊密地追蹤樣本的聚集情形。調大該值可增加平滑程度,調小則減少平滑;請比較不同頻寬下的曲線形狀,而不要將任何單一頻寬視為普遍正確。
使用 Seaborn 建立密度圖
seaborn 將同樣的概念包裝成一次呼叫:kdeplot() 會在內部估計密度,並將其繪製到 Matplotlib 座標軸上。透過 x 傳入樣本,並用 bw_adjust 縮放自動選擇的頻寬——大於 1 的值更平滑,小於 1 的值則減少平滑。較舊的 distplot() 函式已被棄用;只畫曲線請使用 kdeplot(),需要圖形層級(figure-level)版本則使用 displot(kind="kde")。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import seaborn as sns
data = [2, 3, 3, 4, 2, 1, 5, 6, 4, 3, 3, 3, 6, 4, 5, 4, 3, 2]
ax = sns.kdeplot(x=data, bw_adjust=0.5)
ax.set_title("Density Plot of the data")
ax.set_xlabel("Value")
print("kdeplot drew", len(ax.get_lines()), "density line")
kdeplot drew 1 density line
kdeplot 會回傳它繪製所用的 Matplotlib Axes,因此標題與標籤可用一般的座標軸方法設定;加上 plt.show() 即可在互動模式下顯示圖形視窗。
使用 pandas 建立密度圖
當樣本已經在 DataFrame 或 Series 中時,pandas.DataFrame.plot 可以直接產生曲線:傳入 kind="density"(別名 kind="kde" 同樣有效)。pandas 會以 gaussian_kde 計算估計,因此這種圖必須安裝 SciPy。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
data = [2, 3, 3, 4, 2, 1, 5, 6, 4, 3, 3, 3, 6, 4, 5, 4, 3, 2]
df = pd.DataFrame(data, columns=["value"])
ax = df.plot(kind="density")
ax.set_title("Density Plot of the data")
ax.set_xlabel("Value")
print("pandas labeled the axis:", ax.get_ylabel())
pandas labeled the axis: Density
對 DataFrame 呼叫此方法會為每個欄位繪製一條曲線,並自動將 y 軸標示為 Density;對單一 Series 呼叫則對應單一具名變數。與 SciPy 的做法相同,平滑程度取決於估計器的頻寬,而 pandas 會從資料推導頻寬。