一次 melt,决定你能不能一句话画出图
卷 III 要把列接到眼睛上。但在碰任何画图函数之前,有一步必须先做完——因为它决定了后面四章你是在写代码还是在描述数据。同样 12 个数字,摆成 4 × 4 的表你只能一根一根手画,摆成 12 × 3 的表一行就能画出带分面、带配色的图。这一步没有任何视觉美感可言,它纯粹是形状问题。
一张四个月、三个城市的气温表,共 12 个数。它现在是 4 行 × 4 列(第一列是月份,后三列是三个城市)。
现在业务上加了第四个城市。
问:宽表和长表,各自要改几处?
两张表,同样 12 个数
左边这张叫宽表(wide form)。它读起来很舒服,一眼能看完,Excel 里绝大多数表都长这样。它的问题在于:「城市」这个变量,藏在列名里。
右边这张叫长表(long form),或者按 Hadley Wickham 那篇论文的说法叫整洁数据(tidy data)。它的规则只有两条:
一行一个观测,一列一个变量。
「北京三月的气温是 25 度」是一个观测——它涉及三个变量:月份、城市、气温。所以长表里它就是一行三列。
宽表违反了第二条:「北京」不是一个变量,它是「城市」这个变量的一个取值。把取值提升成列名,这个变量就消失了——你没法对一个不存在的列做任何事。
转换只要一行:
long = wide.melt(id_vars="月份", var_name="城市", value_name="温度") # (4, 4) → (12, 3) back = long.pivot(index="月份", columns="城市", values="温度") # (12, 3) → (4, 3) ← 转得回去,一个数不差
melt 和 pivot 是一对逆运算。数据量一点没变(12 个数还是 12 个数),变的只是「哪些东西是列」。
顺手记一个细节:pivot 转回去的时候,行和列都会被重新排序——按标签的字典序,不是按原来的顺序。本机上四个月份变成了这个顺序:
原来的月份顺序 一月 二月 三月 四月 pivot 之后 一月 三月 二月 四月 ← 按 Unicode 码位排的 pivot 之后的列 上海 北京 广州 ← 城市也重排了
数一个不差,顺序全变了。所以「转回去和原表一模一样」这句话要加个前提:先 reindex 回原来的顺序才成立。这条在处理月份、星期、评分档位这类有内在顺序但字典序不对的标签时会反复咬人——正规解法是把它们声明成有序分类:pd.Categorical(月份, categories=[...], ordered=True)。
为什么这一步值得单独占一章
回到开头那个问题。加一个城市:
| 宽表 | 长表 | |
|---|---|---|
| 数据本身 | 加一列(5 列) | 加四行(16 行) |
| 画图代码 | 要改:循环里的城市列表、图例、颜色 | 一个字都不用改 |
| groupby 汇总 | 要改:得手写四列的名字 | groupby("城市"),不变 |
| 加筛选条件 | 没法写「只看南方城市」 | df[df.城市.isin(...)] |
| 列的名字 | 随数据变 | 永远是 月份/城市/温度 |
关键在最后一行:长表的列名是稳定的,宽表的列名跟着数据走。而代码是写给列名的。列名会变,代码就得跟着变;列名不变,代码就能一直用。
这不是画图专属的道理。往前看,第 9 章的 groupby("城市") 需要「城市」是一列;往后看,第 14 章的 hue="城市" 也需要「城市」是一列,第 17 章 sklearn 的 OneHotEncoder 同样需要。整个下游生态,都建立在「一个变量是一列」这个前提上。
那宽表就没用了吗
不是。两种形状各有各的场合,判据很清楚:
| 用宽表 | 用长表 |
|---|---|
| 给人看:报表、周报、屏幕上的表格 | 给代码看:画图、分组、建模 |
| 做矩阵运算:相关矩阵、透视表 | 存储:加一个类别不用改表结构 |
| 行列都是同一种东西(比如混淆矩阵) | 每列的含义不同(月份、城市、温度) |
所以典型的流程是:长表存、长表算、宽表看。数据库里存长表(加一个城市不用 ALTER TABLE),分析时用长表,最后一步 pivot 成宽表给人看。
顺带认一下 pandas 里这一族的四个方法,它们经常被搞混:
melt 宽 → 长 指定 id_vars,其余列被「融化」成两列 pivot 长 → 宽 要求 (index, columns) 组合唯一,否则报错 pivot_table 长 → 宽 允许重复,用 aggfunc 汇总(Excel 数据透视表) stack / unstack 宽 ↔ 长 在 index 和列名之间搬东西(多层 index 时用)
pivot 和 pivot_table 的差别值得记:pivot 在遇到重复时报错,pivot_table 默认取平均。如果你不确定组合是否唯一,pivot 的报错反而是好事——它逼你先想清楚「重复了该怎么办」。
整洁数据(tidy data)来自 Hadley Wickham 2014 年的同名论文。原文给了三条:每个变量一列、每个观测一行、每类观测单位一张表。第三条常被忽略,但它就是数据库范式里的「一张表只讲一件事」。
注意这个词很容易被误解成「干净的数据」。它跟数据质量无关——一张全是错误值的表也可以是 tidy 的,一张完全正确的宽表也可以是不 tidy 的。它说的纯粹是形状。
另外,「宽」和「长」在不同工具里叫法不一:R 的 tidyr 叫 pivot_longer/pivot_wider,SQL 里叫 UNPIVOT/PIVOT,Excel 叫「逆透视」/「透视」。四个生态,同一个操作,四套名字。
整个转换六行:
import pandas as pd
wide = pd.DataFrame({"月份": ["一月", "二月", "三月", "四月"],
"北京": [12, 18, 25, 31],
"上海": [15, 20, 24, 28],
"广州": [22, 26, 29, 32]})
long = wide.melt(id_vars="月份", var_name="城市", value_name="温度")
print(wide.shape, "→", long.shape) # (4, 4) → (12, 3)
print(long.head(3))
back = long.pivot(index="月份", columns="城市", values="温度")
print(back) # 转得回去,一个数不差
然后亲手做一次「加一个城市」,感受那个差别:
wide2 = wide.copy()
wide2["深圳"] = [24, 27, 30, 33]
long2 = wide2.melt(id_vars="月份", var_name="城市", value_name="温度")
print(len(wide2.columns), len(long2)) # 5 列 / 16 行
print(list(long.columns) == list(long2.columns)) # True ← 列名没变
# 长表上这些全都不用改:
long2.groupby("城市")["温度"].mean()
long2[long2["城市"].isin(["广州", "深圳"])]
python3 -c "import pandas as pd;w=pd.DataFrame({'m':[1,2],'a':[1,2],'b':[3,4]});print(w.shape,'->',w.melt(id_vars='m').shape)"
画图部分从下一章开始。这一章不需要 matplotlib,只要 pandas。
- 数据库设计里的「加一列还是加一行」。用户属性表设计成宽表(每个属性一列),加一个属性要
ALTER TABLE;设计成长表(user_id, key, value,也就是 EAV 模型),加属性只是插一行。取舍和这一章完全一样:宽表查询快、类型明确,长表灵活但每次查询都要 pivot 一次。 - 时序数据库天生是长表。Prometheus、InfluxDB 存的都是
(时间, 指标名, 标签, 值)——完美的长表。新增一个监控指标不需要改任何 schema,这正是长表灵活性的价值。 - Excel 的「逆透视」按钮。Power Query 里那个按钮做的就是
melt。会用它的人和不会用的人,在 Excel 里的生产力差一个量级——因为数据透视表要求的输入正是长表。 - 为什么日志要写成结构化的键值对。
level=error service=auth latency=230是长表;一行自由文本是没有形状的数据。日志系统的全部查询能力,都建立在「每个字段是一列」这个前提上。
「数据整理是个人习惯问题,怎么摆都行,反正数是一样的。」
数确实一样,但能力不一样。宽表里「城市」不是一列,所以你没法:按城市分组、按城市配色、按城市分面、按城市过滤、把城市喂给 OneHotEncoder。这些不是「写起来麻烦一点」,是「根本没有这个东西可以指」。
代价是可以量的:加一个城市,长表要改 0 行代码,宽表要改画图循环、图例、颜色映射,还有每一处硬编码了城市名的地方。而且这类改动不会报错——你新加的城市只是不出现在图上而已。
判据一句话:看着你的列名,问「这些列名里有没有藏着一个变量的取值」。如果三列叫「北京/上海/广州」,那「城市」就藏在里面。如果三列叫「月份/城市/温度」,那才是三个变量。
正确答案是 B:宽表要改画图代码,长表不用改任何代码。
A 「都是一处,差不多」——只看数据的话确实差不多(加一列 vs 加四行)。差别全在代码那边:长表的列名是 月份/城市/温度,加多少个城市都不变;宽表的列名就是城市名,数据一变,所有引用列名的代码都得跟着变。 C 「长表更麻烦,要多加四行」——行数确实多了,但行是数据,列是接口。多几行数据没有任何维护成本;多一列意味着所有下游代码的接口变了。这正是「一行一个观测」这条规则的价值:让变化落在行上,不落在列上。 D 「都要改」——长表真的不用改。这一点值得亲手验一次:上面那段代码里,long2.groupby("城市") 和 sns.relplot(hue="城市") 在加了深圳之后一个字都不用动,新城市自动出现在结果里。
x=/hue=/col= 参数收的是列名,宽表里那个变量不是列,所以你根本填不进去。这不是报错,是「没有东西可填」——第 14 章会看到这道关卡的样子。
这一章的一句话
整理数据不是审美问题,是形状问题:只有当一个变量真的是一列时,下游那些「按它分组、按它配色、按它分面」的能力才存在——而宽表把变量藏进了列名,那个东西就不存在了。
下一章开始碰画图本身,第一件事是解释一个让所有人困惑过的现象:为什么 plt.plot() 有时候会画到另一张图上。本机实测,连着 plt.figure() 两次再 plt.plot(),那条线画在了第二张图上,第一张是空的——而这不是 bug,是一个 1990 年代的设计决定留下的影子。