library(tidyr)
library(dplyr)
# df_long 是資料式:縣市、年齡層、性別、人數
df_wide <- df_long |>
pivot_wider(
names_from = c(性別, 年齡層), # 這兩欄變成新的欄名
values_from = 人數
)報表式 vs 資料式:同一份 Excel 的兩種存法,決定後面每一步好不好走
打開一份從主計總處抓下來的 Excel,常會看到兩種完全不同長相的表。一種表頭好幾層、儲存格合併來合併去,看起來像是設計來列印給主管翻的;另一種只有一列表頭,欄名就是「縣市、年齡層、性別、人數」,看起來很無聊,但每一列就是一筆觀察值。
這篇要講的就是這兩種存法的差別 —— 「報表式」(cross-tab / 交叉表,給人看的)和「資料式」(tidy / 乾淨資料,給程式看的)—— 以及為什麼你如果要拿去做分析、丟給 AI、跑樞紐,兩者的處理成本會差到讓人有感。
兩種表在講同一件事,但長相完全不同
先用一個簡化的例子。假設我們手上有「三個縣市 × 兩個年齡層 × 兩性人口」的資料。
報表式大概長這樣(真的用 Excel 做出來截下來的 —— 表頭有兩層,「男 / 女」跨欄合併、下面再切「0–14 / 15+」,最後多一列「總計」):

另一種常見的合併是用 rowspan(跨列合併儲存格)做區域分組 —— 同一個「區域」名字只寫在第一列,下面幾列的儲存格被合併起來共用:

眼睛看很清楚:「新北市」跟「桃園市」也是北部。但程式讀進來只有第一列有「北部」,下面兩列的「區域」欄位都會變成 NaN(遺失值 —— pandas 的 read_excel 預設會把合併儲存格底下的位置填成這個),要自己再 forward fill(向下填充:把上一列的值抄下來)一次才對。
資料式是同一份資料,但攤平成一列一筆:

看起來資料式好像變得很長、很囉唆,但這種寫法有一個核心優點 —— 每一格的意義完全固定:這一列的第 4 欄,就是「人數」,不會因為你在第幾欄而改變意思。
先澄清一件事:資料式(tidy)不等於「長表」
看到上面這個攤平的樣子,很容易誤會「愈長愈瘦才叫資料式」。其實不是。資料式的定義是「一列一個觀察值、一欄一個變數」,跟表格寬窄沒有直接關係。
同一份資料,下面這樣也算資料式:

它是「寬」的,但每一欄的意思都固定:「男人數」就是這一列這個縣市這個年齡層的男性人口數。沒有把兩個變數塞在同一個欄名裡,也沒有多層表頭、沒有小計混進來 —— 程式讀進來一樣直覺。
換句話說,報表式真正的問題不在「寬」,而在下面這幾件事同時發生:
- 欄名同時裝了多個變數(「男 0–14」= 性別 + 年齡層)
- 表頭跨兩層以上,需要合併儲存格才顯示得出來
- 小計、總計混在資料列裡
- 用空白列或空白欄來分區
把這幾件事拿掉,剩下的表就算是資料式。至於要不要更進一步把「男人數」、「女人數」再攤成一欄「性別」加一欄「人數」,是後續分析方便性的問題(例如畫 ggplot 時 long 通常比較好用),不是資料式的必要條件。
為什麼報表式在程式眼裡很難讀?
把上面那張報表式的表倒過來想一次,你會發現它塞了很多隱藏資訊:
- 「男 0–14」這個欄名,其實同時裝了「性別=男」和「年齡層=0–14」兩個變數
- 表頭是兩層合併的,第一列寫「男」、第二列寫「0–14」,程式讀進來只會看到一堆錯位的欄名
- 最後那一列「總計」不是資料,是統計結果,混在一起會讓平均、加總都算錯
- 如果原表還有「空白列分區」(例如北部縣市一區、中部一區、中間插一列空的),程式讀進來會出現一堆 NA(遺失值,R 的講法;Python 則叫 NaN,同一件事)
這些東西人眼看一眼就懂,但 AI 或程式讀進來會遇到的問題:
- 要花很多 token(AI 模型讀寫文字的計算單位,愈長愈貴)猜表頭:你把整張 Excel 貼給 ChatGPT,它得先讀懂「這一欄的『男』跟上面那一欄的『男』是不是同一件事」,然後才能回答你的問題。猜錯的機率也不低。
- 每個小計都要跳過:不然算平均會把「總計那一列」也算進去,數字全歪
- 合併儲存格會變空白:pandas 讀進來,只有第一格有值,下面幾格都是 NaN(遺失值),要自己 forward fill(向下填充)
資料式就沒有這些問題。你把它丟給 AI:「幫我算各縣市男女比例」,它一看就知道要 groupby(縣市, 性別)(依「縣市 + 性別」分組再加總)操作,寫兩行就完成。
兩種格式各自的位置
不是說報表式就沒用。它有它的場景:
| 情境 | 報表式 | 資料式 |
|---|---|---|
| 印出來給主管看一眼 | 適合,一頁就看完 | 太長,翻不完 |
| 給 Python / R 讀進來 groupby | 要先攤平,多一步 | 直接讀,直接算 |
| Excel 樞紐分析的「資料來源」 | 樞紐吃不下多層表頭 | 樞紐的標準輸入 |
| 貼給 AI 問問題 | 要花 token 解釋表頭 | prompt 可以很短 |
| 畫圖(ggplot / matplotlib) | 幾乎都要先攤平 | 直接對應 x/y/color |
所以兩者不是誰取代誰,而是用途分工。差別在於:如果你手上只能留一份「原始檔」,留哪一種對後續每一步都比較省力?
一個實務上的順序建議
一個常見的做法是這樣:
- 原始蒐集、儲存階段用資料式 —— 每次新增資料就 append(追加到最後)一列,欄位固定,不用改結構
- 要給人看的時候再從資料式 pivot 出報表式 —— Excel 樞紐、R 的
pivot_wider()、Python 的df.pivot()都是一行的事 - 從別人那邊拿到報表式(例如政府開放資料常常是報表式)—— 花一點時間攤平成資料式,之後不管做幾次分析、換幾次工具,都不用再拆一次表頭
反過來的順序 —— 只留報表式、每次要分析就重新拆 —— 表頭一改就要重寫一次拆表邏輯,換一種分析角度又要拆一次。
Excel 樞紐分析:從資料式一鍵切成報表式
如果你手上是資料式(四欄:縣市、年齡層、性別、人數),要生上面那張報表:
- 選整個範圍 → 插入 → 樞紐分析表
- 「列」放「縣市」
- 「欄」放「性別」和「年齡層」
- 「值」放「人數」(加總)
樞紐就會幫你切出多層表頭的報表式,順便附上小計、總計。但這張樞紐報表只當「輸出」,不要拿去當下一步分析的「輸入」,不然又走一次剛才講的老路。
R 的 pivot_longer / pivot_wider
R 的 tidyverse 有一組專門重塑表格形狀的函數:tidyr::pivot_longer() 把寬表變長、pivot_wider() 把長表變寬。它們處理的是「同一份資料式的不同排列」,不是把資料式變成報表式 —— 報表式的多層表頭、小計、合併儲存格,這兩個函數處理不了,要先手動清掉。
從寬的資料式撐成報表式的欄位結構(或是要給樞紐用):
從已經是資料式、但欄名包了兩個變數的寬表,攤成一列一觀察的長表(假設欄名是「男_0–14」、「男_15+」、「女_0–14」、「女_15+」):
df_long <- df_wide |>
pivot_longer(
cols = -縣市, # 縣市這欄不動,其他都攤
names_to = c("性別", "年齡層"),
names_sep = "_",
values_to = "人數"
)Python pandas 對應的是 df.melt() 和 df.pivot(),邏輯一模一樣。
給 AI 資料時,格式差異會直接反映在正確率
這是最近很有感的一點。同一個問題「算一下各縣市女性 15 歲以上的人口比例」:
- 貼報表式 Excel 給 ChatGPT → 它要先花一段回覆解讀表頭、確認哪一欄是女性 15+,中間還可能問你「總計那一列要不要算」,來回三四輪才會給答案。而且合併儲存格轉成文字後常常整個亂掉。
- 貼資料式 → 一句「請幫我算
性別=='女' & 年齡層=='15+'的人數佔各縣市總人口的比例」就結束,AI 直接寫 code 給你,不用猜。
你的 prompt(提示語,就是你打給 AI 的那段話)可以短、AI 的答案可以準,很大一部分不是 prompt 工程的差別,是資料格式的差別。
小結
同一份資料,存成報表式還是資料式,是兩種完全不同的世界觀:報表式服務「這一頁要好讀」,資料式服務「這一列要好算」。原始蒐集階段留資料式,需要給人看的時候再 pivot,之後不管換到 Excel、R、Python,還是丟給 AI,都能省下一大截「先攤平」的時間。
拿到別人給的報表式檔案時,可以先問一句:「這份原始資料式的版本可以給我嗎?」很多時候對方手上就有,只是預設把好看那份寄出來。
本文用到的 Excel 範例檔(含四張工作表:報表式-多層表頭、報表式-區域分組、資料式-寬、資料式-長):report-vs-tidy-example.xlsx