2  Chapter 2: 描述統計學 (descriptive statistics)

把資料整理到它願意好好說話

2.1 本章學習目標

上一章我們拿到統計地圖,知道資料、樣本、母群體與研究設計大概坐在哪裡。這一章要做的事更務實:把一份醫療資料整理成清楚、有意義、可溝通的摘要。描述統計學 (descriptive statistics) 就像臨床病歷摘要,不是把每個細節都塞進去,而是把最重要的資訊用最合適的方式呈現。

讀完本章後,你應該能夠:

  1. 區分中心位置 (central tendency) 與離散程度 (variability) 的統計量。
  2. 計算平均值 (mean)、中位數 (median)、眾數 (mode)、變異數 (variance)、標準差 (standard deviation)、四分位距 (interquartile range, IQR) 與範圍 (range)。
  3. 使用百分位數 (percentile) 與四分位數 (quartile) 描述資料分布。
  4. 為連續資料與類別資料選擇合適的表格與圖形。
  5. 使用 Python 3.14、pandasseaborn 產生臨床資料摘要與圖表。

2.2 描述統計是什麼?

描述統計學 (descriptive statistics) 是用數值、表格與圖形整理資料的方法。它回答的是:「這份資料長什麼樣子?」例如:平均年齡是多少?住院天數是否右偏?急診來源病人比例是多少?CRP 高的人住院天數看起來是否較長?

請注意,描述統計不直接回答「治療是否有效」或「暴露是否造成疾病」。那些屬於推論統計 (inferential statistics) 與研究設計的領域。描述統計比較像研究的第一輪身體檢查:先量血壓、聽心肺、看起來有沒有明顯異常,再決定後續檢查。

好的描述統計有三個功能:

  • 壓縮資訊:把一大堆觀察值整理成少數重點。
  • 暴露問題:發現離群值、遺漏值、單位錯誤或不合理資料。
  • 協助溝通:讓臨床團隊、研究者與讀者快速掌握樣本特徵。

2.3 範例資料:一份住院病人資料

本章使用一份假想但貼近臨床的住院病人資料。每位病人有病房來源、性別、年齡、住院天數、CRP 與 30 天內是否再住院。CRP 是 C 反應蛋白 (C-reactive protein, CRP),常用於評估發炎程度。

patients = pd.DataFrame(
    {
        "id": range(1, 31),
        "ward": [
            "內科", "內科", "外科", "內科", "急診", "外科", "內科", "急診", "外科", "內科",
            "急診", "外科", "內科", "內科", "急診", "外科", "內科", "急診", "外科", "內科",
            "內科", "外科", "急診", "內科", "外科", "急診", "內科", "外科", "急診", "內科",
        ],
        "sex": [
            "女", "男", "男", "女", "男", "女", "女", "男", "女", "男",
            "女", "男", "男", "女", "男", "女", "女", "男", "男", "女",
            "男", "女", "男", "女", "男", "女", "女", "男", "女", "男",
        ],
        "age": [72, 65, 58, 80, 44, 69, 76, 51, 62, 70, 39, 73, 68, 85, 47, 60, 78, 55, 66, 74, 71, 64, 42, 83, 59, 49, 77, 61, 53, 69],
        "los": [6, 5, 4, 9, 2, 7, 8, 3, 5, 6, 1, 10, 6, 14, 2, 5, 9, 4, 7, 8, 6, 5, 2, 13, 4, 3, 9, 6, 3, 7],
        "crp": [4.1, 3.8, 2.0, 7.5, 1.2, 5.4, 6.1, 1.8, 2.9, 4.8, 0.9, 8.6, 4.5, 12.7, 1.5, 3.1, 7.2, 2.3, 4.0, 5.9, 4.7, 3.3, 1.1, 11.8, 2.6, 1.7, 6.8, 3.6, 2.2, 5.1],
        "readmitted_30d": [
            "否", "否", "否", "是", "否", "否", "是", "否", "否", "否",
            "否", "是", "否", "是", "否", "否", "是", "否", "否", "否",
            "否", "否", "否", "是", "否", "否", "是", "否", "否", "否",
        ],
    }
)

patients.head()
id ward sex age los crp readmitted_30d
0 1 內科 72 6 4.1
1 2 內科 65 5 3.8
2 3 外科 58 4 2.0
3 4 內科 80 9 7.5
4 5 急診 44 2 1.2

在這份資料中,ageloscrp 是連續或近似連續的數值資料;wardsexreadmitted_30d 是類別資料。los 是住院天數 (length of stay, LOS),在醫療品質、資源使用與照護轉銜研究中非常常見。

2.4 中心位置:資料的典型值

中心位置 (central tendency) 用來描述資料「大概落在哪裡」。最常用的三個指標是平均值 (mean)、中位數 (median) 與眾數 (mode)。

平均值是所有觀察值加總後除以觀察數。它很好懂,也常用於常態或近似對稱的資料。不過平均值對極端值很敏感。例如大多數病人住院 3 到 8 天,但有一位住院 60 天,平均值就可能被拉高,像被很有存在感的住院天數抓著走。

中位數是資料排序後位於中間的值。它對極端值較不敏感,適合偏態分布 (skewed distribution) 的資料。醫療資料常常偏態,例如住院天數、醫療費用、等待時間與發炎指標。

眾數是出現次數最多的值。它對類別資料尤其有用,例如最常見的血型、最常見的病房來源。

center_table = pd.DataFrame(
    {
        "平均值": patients[["age", "los", "crp"]].mean(),
        "中位數": patients[["age", "los", "crp"]].median(),
        "眾數": patients[["age", "los", "crp"]].mode().iloc[0],
    }
).round(2)

center_table
平均值 中位數 眾數
age 64.00 65.5 69.0
los 5.97 6.0 6.0
crp 4.44 3.9 0.9

從表中可以看到,住院天數的平均值略高於中位數,表示資料可能有右偏現象。右偏 (right skewness) 意味著少數較大的觀察值把尾巴往右拉;在住院天數資料裡,這通常代表少數病人住院較久。

2.5 離散程度:資料散得多開?

如果只知道中心位置,還不夠。兩個病房的平均住院天數都可能是 6 天,但其中一個病房的病人幾乎都住 5 到 7 天,另一個病房可能有人住 1 天、也有人住 20 天。對臨床管理來說,這兩個世界差很多。

離散程度 (variability) 用來描述資料的分散程度。常見指標包括範圍、變異數、標準差與四分位距。

範圍 (range) 是最大值減最小值。它很直觀,但只看兩個極端值,因此也很容易受離群值 (outlier) 影響。

變異數 (variance) 是每個觀察值與平均值差距平方的平均概念。樣本變異數常用分母 n - 1,這是為了修正用樣本估計母群體變異時的偏差。標準差是變異數的平方根,單位會回到原始資料單位,因此比變異數更容易解讀。

四分位距 (interquartile range, IQR) 是第三四分位數減第一四分位數,也就是中間 50% 資料的範圍。它對極端值較穩健,很適合偏態資料。

variability_table = pd.DataFrame(
    {
        "最小值": patients[["age", "los", "crp"]].min(),
        "最大值": patients[["age", "los", "crp"]].max(),
        "範圍": patients[["age", "los", "crp"]].max() - patients[["age", "los", "crp"]].min(),
        "變異數": patients[["age", "los", "crp"]].var(),
        "標準差": patients[["age", "los", "crp"]].std(),
        "四分位距": patients[["age", "los", "crp"]].quantile(0.75) - patients[["age", "los", "crp"]].quantile(0.25),
    }
).round(2)

variability_table
最小值 最大值 範圍 變異數 標準差 四分位距
age 39.0 85.0 46.0 153.52 12.39 17.00
los 1.0 14.0 13.0 9.76 3.12 3.75
crp 0.9 12.7 11.8 8.68 2.95 3.55

實務上,連續資料常用「平均值 ± 標準差」或「中位數 [四分位距]」呈現。若資料接近對稱,平均值與標準差常很有代表性;若資料明顯偏態,通常中位數與四分位距比較誠實。統計的誠實很重要,資料不喜歡被硬穿不合身的衣服。

2.6 百分位數與五數摘要

百分位數 (percentile) 表示資料中有多少比例低於或等於某個值。例如第 90 百分位數代表約 90% 的觀察值不超過該值。四分位數 (quartile) 是特別常用的百分位數:第一四分位數 (first quartile, Q1) 是第 25 百分位數,中位數是第 50 百分位數,第三四分位數 (third quartile, Q3) 是第 75 百分位數。

五數摘要 (five-number summary) 包含最小值、Q1、中位數、Q3 與最大值。箱型圖 (box plot) 正是五數摘要的圖形版本之一。

quantile_table = patients[["age", "los", "crp"]].quantile([0, 0.25, 0.5, 0.75, 1]).round(2)
quantile_table.index = ["最小值", "Q1", "中位數", "Q3", "最大值"]

quantile_table
age los crp
最小值 39.00 1.00 0.90
Q1 55.75 4.00 2.22
中位數 65.50 6.00 3.90
Q3 72.75 7.75 5.78
最大值 85.00 14.00 12.70

如果你在論文的 Table 1 看到「LOS, median (IQR): 6 (4-8) days」,意思就是住院天數的中位數是 6 天,中間 50% 的病人大約落在 4 到 8 天之間。這比只說平均值更能呈現偏態資料的實際樣貌。

2.7 範例 1:住院天數的分布

先畫直方圖看看住院天數的分布。直方圖 (histogram) 適合呈現連續或離散但取值較多的數值資料。

plt.figure(figsize=(7, 4.5))
sns.histplot(data=patients, x="los", bins=8, kde=True, color="#2a9d8f")
plt.axvline(
    patients["los"].mean(),
    color="#d62828",
    linestyle="--",
    label=f"平均值 {patients['los'].mean():.1f}",
)
plt.axvline(
    patients["los"].median(),
    color="#264653",
    linestyle="-.",
    label=f"中位數 {patients['los'].median():.1f}",
)
plt.xlabel("住院天數")
plt.ylabel("人數")
plt.title("住院天數的分布")
plt.legend()
plt.tight_layout()
plt.savefig(FIG_DIR / "ch02_los_distribution.png", dpi=300)
plt.show()
Figure 2.1: 住院天數的分布。虛線為平均值,點線為中位數。

圖中可見住院天數多集中在 3 到 9 天,但也有少數 10 天以上的病人。平均值比中位數更容易被較長住院天數拉高,因此報告住院天數時,中位數與 IQR 通常很值得一起呈現。

2.8 箱型圖與離群值

箱型圖 (box plot) 用盒子呈現 Q1 到 Q3 的範圍,中間線通常代表中位數。鬚線 (whisker) 延伸到合理範圍內的較小與較大觀察值,超出鬚線的點常被標示為可能離群值。

離群值不是「錯誤值」的同義詞。離群值可能是輸入錯誤,例如年齡 770 歲;也可能是真實但少見的臨床狀況,例如複雜感染導致住院 60 天。統計上看到離群值時,第一步不是刪除,而是查證。

2.9 範例 2:不同病房來源的住院天數

plt.figure(figsize=(7, 4.5))
sns.boxplot(data=patients, x="ward", y="los", hue="ward", palette="Set2", legend=False)
sns.stripplot(data=patients, x="ward", y="los", color="#333333", alpha=0.65, jitter=0.15)
plt.xlabel("病房來源")
plt.ylabel("住院天數")
plt.title("不同病房來源的住院天數")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch02_los_boxplot_by_ward.png", dpi=300)
plt.show()
Figure 2.2: 不同病房來源的住院天數箱型圖。每個黑點代表一位病人。

這張圖讓我們同時看到每組的中位數、分散程度與個別病人的位置。若只看平均值,很多細節會消失;箱型圖則提醒我們每個病房內部仍有差異。臨床資料不是罐頭濃湯,不會攪一攪就均質。

2.10 類別資料的摘要

類別資料 (categorical data) 通常用次數 (frequency) 與百分比 (percentage) 摘要。例如病房來源、性別、是否再住院都適合用人數與百分比呈現。

ward_table = (
    patients["ward"]
    .value_counts()
    .rename_axis("病房來源")
    .reset_index(name="人數")
)
ward_table["百分比"] = (ward_table["人數"] / len(patients) * 100).round(1)

ward_table
病房來源 人數 百分比
0 內科 13 43.3
1 外科 9 30.0
2 急診 8 26.7

百分比很好用,但請永遠記得看分母。50% 可以是 1/2,也可以是 500/1000,兩者的穩定度完全不同。醫學文獻中看到百分比時,若沒有樣本數,就像看到體溫卻不知道量的是人還是熱水。

2.11 範例 3:病房來源分布

長條圖 (bar chart) 適合呈現類別資料各組的次數或百分比。和直方圖不同,長條圖的 x 軸是分類;直方圖的 x 軸是連續數值的區間。

plt.figure(figsize=(7, 4.5))
sns.barplot(data=ward_table, x="病房來源", y="百分比", color="#8ab17d")
plt.ylim(0, 60)
plt.xlabel("病房來源")
plt.ylabel("百分比 (%)")
plt.title("病房來源分布")
for index, row in ward_table.iterrows():
    plt.text(index, row["百分比"] + 1, f"{row['百分比']}%", ha="center")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch02_ward_distribution.png", dpi=300)
plt.show()
Figure 2.3: 病房來源的百分比分布。

這張圖讓讀者快速看出樣本以內科病人最多。若研究問題與病房來源相關,這個不平衡就可能影響後續分析與解釋。

2.12 雙變項描述:兩個變項一起看

單一變項摘要稱為單變項描述 (univariate description)。當我們同時描述兩個變項的關係,就進入雙變項描述 (bivariate description)。例如:CRP 較高的病人住院是否較久?不同病房來源的再住院比例是否不同?

雙變項描述不等於因果推論。散布圖看起來有趨勢,只能說「資料中有關聯的線索」,不能直接說 CRP 造成住院變久。臨床上,CRP 可能只是疾病嚴重度的指標,而真正影響住院天數的是感染位置、共病、治療反應與出院資源。

plt.figure(figsize=(7, 4.5))
sns.scatterplot(
    data=patients,
    x="crp",
    y="los",
    hue="readmitted_30d",
    s=90,
    palette=["#457b9d", "#e76f51"],
)
plt.xlabel("CRP (mg/dL)")
plt.ylabel("住院天數")
plt.title("CRP 與住院天數")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch02_crp_los_scatter.png", dpi=300)
plt.show()
Figure 2.4: CRP 與住院天數的散布圖。顏色代表 30 天內是否再住院。

圖中 CRP 較高者住院天數似乎較長,且有些再住院病人集中在較高 CRP 與較長住院天數區域。這是很好的探索性發現,但還需要更正式的分析,例如相關分析、迴歸模型與混雜因子調整。

2.13 Table 1:醫學論文的第一張表

醫學論文常見的 Table 1 通常用來描述研究樣本的基準特徵 (baseline characteristics)。它不是裝飾品,而是讓讀者判斷研究樣本是否合理、組間是否平衡、結果是否可推廣的重要工具。

下面建立一張簡化版 Table 1:

table_one = pd.DataFrame(
    {
        "變項": [
            "年齡, mean (SD)",
            "住院天數, median [IQR]",
            "CRP, median [IQR]",
            "女性, n (%)",
            "30 天內再住院, n (%)",
        ],
        "摘要": [
            f"{patients['age'].mean():.1f} ({patients['age'].std():.1f})",
            f"{patients['los'].median():.1f} [{patients['los'].quantile(0.25):.1f}, {patients['los'].quantile(0.75):.1f}]",
            f"{patients['crp'].median():.1f} [{patients['crp'].quantile(0.25):.1f}, {patients['crp'].quantile(0.75):.1f}]",
            f"{(patients['sex'] == '女').sum()} ({(patients['sex'] == '女').mean() * 100:.1f}%)",
            f"{(patients['readmitted_30d'] == '是').sum()} ({(patients['readmitted_30d'] == '是').mean() * 100:.1f}%)",
        ],
    }
)

table_one
變項 摘要
0 年齡, mean (SD) 64.0 (12.4)
1 住院天數, median [IQR] 6.0 [4.0, 7.8]
2 CRP, median [IQR] 3.9 [2.2, 5.8]
3 女性, n (%) 15 (50.0%)
4 30 天內再住院, n (%) 7 (23.3%)

這張表混合了連續資料與類別資料的摘要方式。年齡若近似對稱,可用 mean (SD);住院天數與 CRP 較可能偏態,因此使用 median [IQR] 更合適。這不是死規則,而是根據資料分布與臨床解釋做出的選擇。

2.14 常見陷阱

描述統計看似溫和,但仍有幾個常見陷阱:

  1. 只報平均值,不看分布:平均值可能被少數極端值影響。
  2. 百分比沒有分母:沒有樣本數的百分比很難判斷穩定性。
  3. 把標準差當標準誤:標準差描述個體資料分散;標準誤 (standard error) 描述估計值的不確定性,兩者用途不同。
  4. 圖形比例誤導:截斷 y 軸或過度裝飾可能放大差異。
  5. Table 1 過度檢定:基準特徵表的重點是描述樣本,不是把每個變項都變成顯著性競賽。

描述統計的精神是清楚與誠實。漂亮很加分,但清楚更重要;圖表是給人看的,不是給軟體炫技的。

2.15 本章重點整理

  • 描述統計用來摘要與呈現資料,是所有統計分析的起點。
  • 中心位置描述典型值,常見指標包括平均值、中位數與眾數。
  • 離散程度描述資料分散情形,常見指標包括範圍、變異數、標準差與四分位距。
  • 偏態資料常用中位數與 IQR;接近對稱的資料常用平均值與標準差。
  • 類別資料應用次數與百分比摘要,並清楚呈現分母。
  • 圖形能揭露表格看不到的分布、離群值與組間差異。
  • 描述統計能提出問題,但不應被誤解為因果證明。

2.16 小練習

  1. 計算 patients 中 CRP 的平均值、中位數與標準差。
  2. readmitted_30d 分組,計算住院天數的中位數與 IQR。
  3. 畫出年齡的直方圖,並判斷它是否看起來偏態。
  4. 建立性別的次數與百分比表。
  5. 想一想:若某病人的住院天數是 60 天,你會如何判斷它是錯誤值還是真實離群值?
crp_summary = patients["crp"].agg(["mean", "median", "std"]).round(2)

los_by_readmission = (
    patients.groupby("readmitted_30d")["los"]
    .agg(
        median="median",
        q1=lambda x: x.quantile(0.25),
        q3=lambda x: x.quantile(0.75),
    )
    .round(1)
)
los_by_readmission["IQR"] = (
    los_by_readmission["q1"].astype(str)
    + ", "
    + los_by_readmission["q3"].astype(str)
)

sex_table = (
    patients["sex"]
    .value_counts()
    .rename_axis("性別")
    .reset_index(name="人數")
)
sex_table["百分比"] = (sex_table["人數"] / len(patients) * 100).round(1)

crp_summary, los_by_readmission, sex_table
(mean      4.44
 median    3.90
 std       2.95
 Name: crp, dtype: float64,
                 median   q1    q3        IQR
 readmitted_30d                              
 否                  5.0  3.0   6.0   3.0, 6.0
 是                  9.0  9.0  11.5  9.0, 11.5,
   性別  人數   百分比
 0  女  15  50.0
 1  男  15  50.0)

2.17 Glossary

中文術語 English term 說明
描述統計學 descriptive statistics 用數值、表格與圖形摘要資料的方法。
推論統計 inferential statistics 從樣本資料推論母群體特性的統計方法。
C 反應蛋白 C-reactive protein, CRP 常用於評估發炎程度的血液檢驗指標。
住院天數 length of stay, LOS 病人從入院到出院的時間長度。
中心位置 central tendency 描述資料典型位置的概念。
平均值 mean 所有觀察值加總後除以觀察數。
中位數 median 資料排序後位於中間位置的值。
眾數 mode 出現次數最多的值。
偏態分布 skewed distribution 資料分布不對稱,尾端向一側延伸的情況。
右偏 right skewness 分布尾端往較大數值方向延伸。
離散程度 variability 描述資料分散程度的概念。
範圍 range 最大值與最小值之差。
變異數 variance 觀察值相對於平均值差距平方的摘要。
標準差 standard deviation 變異數的平方根,常用於描述資料分散程度。
四分位距 interquartile range, IQR Q3 與 Q1 的差,表示中間 50% 資料範圍。
離群值 outlier 與其他觀察值明顯不同的資料點。
百分位數 percentile 表示資料中有特定比例低於或等於該值的位置。
四分位數 quartile 將資料分成四等份的位置。
第一四分位數 first quartile, Q1 第 25 百分位數。
第三四分位數 third quartile, Q3 第 75 百分位數。
五數摘要 five-number summary 最小值、Q1、中位數、Q3 與最大值。
箱型圖 box plot 呈現中位數、四分位距與可能離群值的圖形。
鬚線 whisker 箱型圖中延伸至合理範圍內觀察值的線段。
類別資料 categorical data 以分類表示的資料。
次數 frequency 某類別或數值出現的次數。
百分比 percentage 某類別人數除以總人數後轉為百分率。
長條圖 bar chart 用長條高度呈現類別資料次數或百分比的圖形。
單變項描述 univariate description 一次描述一個變項的資料摘要。
雙變項描述 bivariate description 同時描述兩個變項之間關係的資料摘要。
基準特徵 baseline characteristics 研究開始時樣本或各組受試者的基本特徵。
標準誤 standard error 描述估計值抽樣不確定性的指標。