pandas 多源数据合并:两种 merge 策略对比
做数据分析,很大一部分时间花在"把散落各处的数据拼到一起"上。这学期做了两个分析作业,都涉及多源数据合并,但用的策略完全不同——一个用 inner 一个用 left,一个用复合键一个用双列键。
这篇笔记把两种做法放在一起对比,顺便记下合并时踩到的坑。
为什么一定要合并
先说清楚动机。要分析"城市经济发展和空气质量的关系",需要两类数据:
- 经济指标:GDP、人均 GDP、第三产业占比、人口密度、汽车保有量……
- 环境指标:AQI、PM2.5、PM10、SO2、NO2、O3、CO、优良天数比例
这两类数据来自不同的来源——经济数据来自统计年鉴,空气质量数据来自环保部门的数据集。它们在不同的文件里,但分析时必须对齐到同一张表上,否则没法算"GDP 高的城市空气质量是不是更差"。
合并就是把两张表按某个共同的"键"拼起来。听起来简单,但键怎么选、用哪种合并方式,直接决定了结果对不对。
做法一:复合键 + inner join
城市项目里,两个 CSV 各有 20 行,合并的代码只有一行:
df_merged = df_economy.merge(df_air, on=['城市', '区域'], how='inner')三个参数值得逐个说。
键是 ['城市', '区域'] 而不是只有 '城市'
为什么用两列做键?乍看"城市"单独做主键就够了——北京、上海、广州,每组只有一个。
但用复合键有额外的好处:它能校验数据的一致性。如果经济表里"杭州"标的是东部、而空气表里"杭州"标成了中部,用单键合并不会报任何错误,合并后的行会同时带上两个矛盾的区域标签,后续按区域分组统计就全错了。
用复合键的话,这种情况会因为匹配不上而被排除——错误立刻暴露出来,而不是悄悄污染结果。
这是一个通用的思路:当你知道两个字段应该是一致的,就把它们都放进键里,让数据自己报错。
how='inner':只保留两边都有的
inner join 的意思是取交集——只有当两个表里都能找到匹配的行,才保留下来。
这个选择在本项目里是合理的:20 个城市两边都齐全,用 inner 和用 left 结果一样。但语义上 inner 更准确——"我要分析的是既有经济数据又有空气质量数据的城市"。如果某个城市只有污染数据没有经济数据,那它本来就参与不了这个分析,保留它反而要处理一堆 NaN。
合并完顺手算了两个派生指标
df_merged['人均汽车保有量_辆每千人'] = (
df_merged['汽车保有量_万辆'] * 10000 / df_merged['年末户籍人口_万人']
).round(0)
df_merged['经济密度_GDP每km2'] = (
df_merged['GDP_亿元'] / (df_merged['年末户籍人口_万人'] / df_merged['人口密度_人每km2'] * 100)
).round(2)第一行是把"汽车保有量(万辆)"换算成人均,单位统一到"辆/千人"。单位换算在数据分析里是高频操作——原始数据的单位往往不适合直接比较,北京 657 万辆和南昌 130 万辆放一起看着差距很大,但北京人口多得多,人均之后差距就完全不同了。
第二行更有意思:它用人口和人口密度反推出了城市面积。因为 人口密度 = 人口 / 面积,所以 面积 = 人口 / 人口密度。拿到面积之后再算 GDP 密度。
这是数据处理里常见的迂回:缺少某个指标时,看看能不能用已有的指标推导出来。
做法二:双列键 + 三次 left join
全球互联网项目的数据结构不一样——4 个 CSV,每个都是一个指标的多国多年面板数据:
| 文件 | 规模 | 年份范围 |
|---|---|---|
| 互联网使用率 | 7,119 行 / 261 国 | 1990–2019 |
| 宽带渗透率 | 4,175 行 / 255 国 | 1998–2019 |
| 移动订阅量 | 11,895 行 / 262 国 | 1960–2019 |
| 互联网用户数 | 4,536 行 / 193 国 | 1990–2017 |
合并方式是以使用率表为主表,连续做三次 left join:
df = df_share.merge(df_broadband[['Entity', 'Year', 'Broadband']],
on=['Entity', 'Year'], how='left')
df = df.merge(df_mobile[['Entity', 'Year', 'Mobile']],
on=['Entity', 'Year'], how='left')
df = df.merge(df_users[['Entity', 'Year', 'NetUsers']],
on=['Entity', 'Year'], how='left')这里的键是 ['Entity', 'Year']——国家和年份两列。因为这是面板数据,同一个国家有很多年的记录,光用国家做键会一对多,合并后行数会爆炸。
必须用"国家+年份"才能唯一定位到一个数据点。
为什么这次用 left 而不是 inner
和城市项目相反,这里用 left 保留左表(使用率表)的所有行,右表匹配不上就填 NaN。
原因是四个数据集的覆盖范围不一样:
- 国家数不同:使用率 261 国,用户数只有 193 国
- 年份范围不同:移动订阅量从 1960 年就有,互联网使用率从 1990 才开始
如果直接用 inner,会丢掉大量行——而且丢掉的往往是有价值的记录(比如某个国家有使用率数据但缺宽带数据)。
用 left 先把所有行保住,把"哪些数据缺失"这件事显式地变成 NaN,而不是静默地丢掉整行。然后在下游按需要处理:要么填值,要么在分析某个特定指标时再过滤。
这就是两种合并方式的核心区别:
| inner | left | |
|---|---|---|
| 保留什么 | 两边都有的行 | 左表全部的行 |
| 匹配不上 | 整行丢弃 | 填 NaN |
| 适用场景 | 键的完整性有保证,缺数据的行本来就不参与分析 | 各源覆盖范围不同,想保留缺失信息 |
只取需要的列
注意 df_broadband[['Entity', 'Year', 'Broadband']] 这个写法——合并前先切片,只保留要用的三列。
这不是可有可无的优化。原始表可能有很多列,全带进来一是浪费内存,二是列名冲突时 pandas 会自动加 _x/_y 后缀,后面引用起来很麻烦。先切干净,合并后的表就是清爽的。
合并之后的清理
拼完表还没完。全球项目的清洗流程里有两步专门处理合并带来的问题:
剔除聚合实体
# 用 Code(ISO 三字母码)过滤掉聚合实体
# 剔除 "World"、"Asia"、"Europe"、"High income" 等这是个很容易漏的坑。OWID 这类数据集的 Entity 列里,除了国家还有地区汇总和收入分组——"World"(全球)、"Asia"(亚洲)、"High income"(高收入国家)等等。
这些行如果不剔掉,会带来严重问题:算"平均互联网使用率"时,"World"那一行本身就是所有国家的平均,再和其他国家一起平均一次,等于重复计算。
识别方法是看 Code 列:真实的 ISO 三字母国家码(如 CHN、USA),聚合实体的 Code 字段是空的。
处理缺失值
# 剔除 4 指标任一缺失的行,最终 184 个国家 × 8 列,完整度 100%left join 产生的 NaN 在这里被清理掉——因为后续要做相关性分析,任何一个指标缺失都会让这一行无法参与计算。
清理后剩 184 个国家。这个数字值得注意:原始的使用率表有 261 个国家,剔除聚合实体再要求四个指标齐全之后只剩 184 个。数据量从 261 降到 184,是合并策略必须付出的代价——想要四个指标都齐,就得接受覆盖国家变少。
另一个选择是放宽要求,用两个指标做分析,能覆盖更多国家。这是分析目标决定的取舍,没有标准答案。
还有一点没想清楚
两个项目做下来,合并本身的操作已经很熟了——键怎么选、用 inner 还是 left、合并完怎么检查,这些都能总结成套路。
但有个问题我一直没找到好的答案:合并之后丢掉了多少数据,到什么程度算"可以接受"?
全球互联网项目里,原始使用率表有 261 个实体,要求四个指标齐全之后只剩 184 个国家,少了将近三成。我当时的处理是直接砍掉——因为后续要做四变量相关性分析,缺任何一个都算不了。但如果分析目标换成"只看宽带和使用率的关系",能保住的国家会多得多。
也就是说,"保留多少行"这件事是被分析目标反向决定的,不存在一个标准答案。先想清楚要回答什么问题,再决定能接受多大的数据损失。
这个顺序我一开始是反着来的——先把数据清洗干净,之后才发现剩下的样本对某些分析已经不够用了,只能回去重新跑一遍合并。下次做类似的事,我会先把要问的问题列出来,再倒推需要保留哪些字段和哪些行。
暂无评论