“销售明细”只记了姓名和销售额,”员工档案”只记了姓名和部门——信息各记一半,老板却要”按部门看销售”。Excel里你用VLOOKUP一列一列往回搬,搬错一列全表错位。
merge一行对上号拼成宽表,concat一行摞起来,”表和表的合体”从此不是DBA的专利
一、老板的”第四轮追问”
前几篇的剧情走到这里:第31篇问总量,第32篇问”哪几行”,第33篇问”分组对比”。老板满意了三天,今天把两张表拍在你桌上:
“这是销售明细,这是员工档案。合到一起,我要按部门看销售。”
你低头一看,两张表长这样:
表A「销售明细」(第33篇的老朋友):
姓名 销售额 日期
0 小李 8000 2026-01-03
1 小王 12000 2026-01-05
2 小张 15300 2026-01-07
3 小赵 9500 2026-01-08
4 小王 11000 2026-01-09
表B「员工档案」:
姓名 部门 城市
0 小王 华东 上海
1 小李 华南 广州
2 小张 华东 杭州
3 小赵 华北 北京
4 小周 西南 成都
问题来了:销售额在表A,部门在表B,中间只有”姓名”这一座桥。 老板要的”按部门看销售”,得先把每个人的部门”搬”到销售表旁边,变成一张全信息的宽表:
姓名 销售额 部门 城市
0 小李 8000 华南 广州
1 小王 12000 华东 上海
...
Excel里这活儿叫VLOOKUP——把表B的”部门”列按姓名查好、一列列往表A上贴。贴一列要配一堆参数(第几列、精确匹配还是模糊匹配),贴错一列全表错位,你还得逐行肉眼验收。更要命的是贴完它是死的,档案一更新,重贴。
pandas的答案叫merge(合并,念”默尔支”),一行对上号:
print(pd.merge(销售, 员工, on="姓名"))
一行,出宽表:
姓名 销售额 日期 部门 城市
0 小李 8000 2026-01-03 华南 广州
1 小王 12000 2026-01-05 华东 上海
2 小张 15300 2026-01-07 华东 杭州
3 小赵 9500 2026-01-08 华北 北京
4 小王 11000 2026-01-09 华东 上海
老板要的”按部门看销售”?这就是第33篇的groupby,接着上就行:pd.merge(...).groupby("部门")["销售额"].sum()。今天学完合并,”多表数据”的世界就打通了。
今天的唯一一个新概念是:把两张表拼起来——同结构的上下摞(concat),有共同列的对上号拼宽表(merge)。它是Excel里VLOOKUP+复制粘贴的代码版,也是”读进程序→看清长相→按需提问”三部曲升级为”多表作战”的第一课。
二、concat:把表”摞起来”
先想清楚:它到底干了什么
先学简单的。concat(concatenate的缩写,念”康开特”,意思是”连成一串”)干的事跟”把两张纸摞成一沓”一模一样:
- 上下摞(
axis=0,默认):两张列一样的表,头接尾叠成一张长表; - 左右拼(
axis=1):两张行一样的表,肩并肩拼成一张宽表——但行得严格对齐,日常少用,今天不展开。
最常见的场景:同一个东西分了两个文件。比如1月的销售表、2月的销售表,列一模一样,老板要”全年一张表”:
import pandas as pd
# 两张"列一样"的表:1月、2月的销售明细
yiyue = pd.DataFrame({
"姓名": ["小李", "小王", "小张"],
"销售额": [8000, 12000, 15300],
"月份": ["1月", "1月", "1月"],
})
eryue = pd.DataFrame({
"姓名": ["小赵", "小王", "小周"],
"销售额": [9500, 11000, 7200],
"月份": ["2月", "2月", "2月"],
})
quannian = pd.concat([yiyue, eryue])
print(quannian)
输出:
姓名 销售额 月份
0 小李 8000 1月
1 小王 12000 1月
2 小张 15300 1月
0 小赵 9500 2月
1 小王 11000 2月
2 小周 7200 2月
看清楚两件事:
- 接头的地方,行号重复了——0、1、2、0、1、2。因为
concat只是把两张表的行摞起来,各自的行号原样保留。加ignore_index=True(”别管旧编号,重新编”)就顺了:pd.concat([yiyue, eryue], ignore_index=True),行号变0到5; - 列名得对得上。两张表都是”姓名、销售额、月份”才能干净地摞。如果2月表的列叫”金额”而不是”销售额”,摞出来就是两列各带一半空值——
concat不猜你的心思,列名不同就是不同列。
pd.concat([表1, 表2, 表3, ...])的参数就是一个装表的列表,几张都行——12个月的表,写个循环把12个文件读进来装列表,一次摞完,第30篇批量处理的功夫正好用上。
什么时候用concat,什么时候用merge
一句话分清:
- concat:两张表是同一种东西(1月销售+2月销售),摞成一沓;
- merge(下面学):两张表是不同的东西(销售明细+员工档案),按共同列对上号拼宽表。
摞错了会怎样?拿员工档案去跟销售明细上下摞——姓名列对不齐、销售额和城市挤成一张表的上下两半,出来一坨谁也看不懂的东西。拼之前先问一句:这两张表是”同类摞一沓”还是”异类对号拼”?
三、merge:按”共同列”对上号
先想清楚:它到底干了什么
merge的动作,跟点名一模一样。想象班主任拿着两张纸:一张是”成绩单”(姓名+分数),一张是”花名册”(姓名+班级)。她想得到”分数+班级”的总表,干的事是:
- 找桥:两张表都有的那一列——”姓名”。这叫键(key),
merge靠它对号; - 对号:拿成绩单上的每个姓名,去花名册里找同名的人;
- 拼:找到了,把花名册那一行的信息接到成绩单旁边,变成一行全信息。
写成代码:
import pandas as pd
销售 = pd.DataFrame({
"姓名": ["小李", "小王", "小张", "小赵", "小王"],
"销售额": [8000, 12000, 15300, 9500, 11000],
})
员工 = pd.DataFrame({
"姓名": ["小王", "小李", "小张", "小赵", "小周"],
"部门": ["华东", "华南", "华东", "华北", "西南"],
"城市": ["上海", "广州", "杭州", "北京", "成都"],
})
kuanbiao = pd.merge(销售, 员工, on="姓名")
print(kuanbiao)
输出:
姓名 销售额 部门 城市
0 小李 8000 华南 广州
1 小王 12000 华东 上海
2 小张 15300 华东 杭州
3 小赵 9500 华北 北京
4 小王 11000 华东 上海
读法记好:pd.merge(表A, 表B, on="共同列"),念作”按某列,把两张表对上号拼起来“。逐行看它干了什么:
- 小李(销售额8000)→ 花名册里有小李 → 拼上”华南/广州”;
- 小王出现两次(12000、11000两单)→ 花名册里小王只有一条 → 两单都拼上小王的”华东/上海”;
- 小周呢?花名册里有,但销售表里没他 → 默认不出现在结果里。
反过来,如果有人只在销售表、不在花名册(比如来了个新人小孙还没建档),默认也是悄悄消失。这个”默认消失”的行为是今天最大的坑,下面第五节专治。
关键参数:on——对号的”桥”
on="姓名"就是”按姓名这一列对”。注意几点:
- 桥必须两张表都有,而且内容得是同一类东西(都是姓名,不能一边是姓名一边是工号)。两张表列名不同但其实是同一类信息(表A叫”姓名”、表B叫”员工名”)?用
left_on/right_on分别指定:pd.merge(A, B, left_on="姓名", right_on="员工名"); - 两列都留着:拼完的宽表里,”姓名”只有一列(同名列自动合并);但用
left_on/right_on时两个名字都在,出来两列重复信息,看着乱但数据没错; - 多列当桥:
on=["姓名", "日期"]——两列同时对上才算同一条。什么时候用?比如两张表都有”姓名”,光按姓名对会把小王的两单跟档案里的小王全拼上(这通常正是你要的);但如果表B是”每人每天的考勤”,就得姓名+日期双保险。
怎么”对不上”处理:how参数
how(怎么对)控制”对不上的行”的去留,就四个值,背下来值钱:
# inner(默认):两张表都有的,才拼。"只留对上号的"
pd.merge(销售, 员工, on="姓名", how="inner")
# left:左边(销售表)的行全留,右边对不上的补空值。"以销售表为准"
pd.merge(销售, 员工, on="姓名", how="left")
# right:右边(员工表)的行全留,左边对不上的补空值。"以员工表为准"
pd.merge(销售, 员工, on="姓名", how="right")
# outer:两张表的行全留,对不上的各补空值。"大团圆"
pd.merge(销售, 员工, on="姓名", how="outer")
拿上面两张表跑一遍,四种结果的差别一句话概括:
inner:5行(销售的5行都对上了);left:5行(同上——因为销售表的每个人档案里都有);right:5行(小周进来了,但他的”销售额”是空的——他没卖过东西);outer:6行(小王两单+小李小张小赵+小周的空销售额,全员到齐)。
日常九成用left:以你手里的主表为准,把另一张表的信息”查过来”,查不到的留空值——这正是VLOOKUP的行为,也是”不丢任何一行主数据”的稳妥选择。什么时候用inner?你只要”两边都有”的干净数据(统计时不想处理空值)。什么时候用outer?核对两张表的差异——谁只在A、谁只在B,一眼看穿。
四、实战:老板要的”按部门看销售”
把今天的活儿一次干完——合并、提问、出报表:
import pandas as pd
# 1. 读两张表(实际工作中换成 read_csv / read_excel)
销售 = pd.DataFrame({
"姓名": ["小李", "小王", "小张", "小赵", "小王"],
"销售额": [8000, 12000, 15300, 9500, 11000],
"日期": ["2026-01-03", "2026-01-05", "2026-01-07", "2026-01-08", "2026-01-09"],
})
员工 = pd.DataFrame({
"姓名": ["小王", "小李", "小张", "小赵", "小周"],
"部门": ["华东", "华南", "华东", "华北", "西南"],
"城市": ["上海", "广州", "杭州", "北京", "成都"],
})
# 2. 合并成宽表(left:销售表一行不丢,档案查过来)
宽表 = pd.merge(销售, 员工, on="姓名", how="left")
print("合并结果:")
print(宽表)
# 3. 老板要的:按部门看销售(第33篇的groupby直接接着上)
报表 = 宽表.groupby("部门")["销售额"].agg(["sum", "count", "mean"])
报表.columns = ["总销售额", "订单数", "平均单值"]
print("n按部门看销售:")
print(报表.sort_values("总销售额", ascending=False))
输出:
合并结果:
姓名 销售额 日期 部门 城市
0 小李 8000 2026-01-03 华南 广州
1 小王 12000 2026-01-05 华东 上海
2 小张 15300 2026-01-07 华东 杭州
3 小赵 9500 2026-01-08 华北 北京
4 小王 11000 2026-01-09 华东 上海
按部门看销售:
总销售额 订单数 平均单值
部门
华东 38300 2 19150.0
华南 8000 1 8000.0
华北 9500 1 9500.0
看懂这个流程的价值:多表数据的套路就是”先合并、再提问”。合并一次变成宽表,之后的总量(第31篇)、筛选(第32篇)、分组(第33篇)全部照常使用——今天的merge只是给三部曲前面加了一步”合体”。
追问一句”按城市看呢”?改一行:宽表.groupby("城市")["销售额"].sum()。追问”新人小周怎么没影儿”?他没卖过货,销售表里根本没他的行——想知道他部门在不重要,想知道谁没开单才是正经问题:
# 员工表里有、销售表里没有的人 = 零单选手
对账 = pd.merge(销售, 员工, on="姓名", how="right", indicator=True)
print(对账[对账["_merge"] == "right_only"][["姓名", "部门"]])
输出:
姓名 部门
4 小周 西南
indicator=True(”加一列标记”)是merge的隐藏好手:拼完多一列_merge,标明每一行是”两边都有”(both)、”只在左表”(left_only)还是”只在右表”(right_only)。对账、查差异的场景全靠它。
五、四个必踩的坑(先打疫苗)
坑1:默认inner悄悄吃掉”对不上”的行,人数对不上账。 merge默认只留两边都有的行,销售表里来了新人小孙(档案还没建),他的订单会无声消失——报表总额比实际少,程序不报错、不吭声。疫苗:主表在手就写how="left",一行不丢,查不到的显示NaN再按第29篇的清洗处理。合并后永远多问一句:print(len(宽表))——行数比主表多了还是少了?少了=丢了行,多了=一对多炸开了(见坑2)。
坑2:桥列有重复,行数”爆炸”。 如果员工档案里小王有两条记录(比如他在两个部门都有工号),merge会把小王的每一单跟两条档案各拼一次——5行销售变6行、7行……销售额跟着翻倍,报表数字虚高。疫苗:合并前先查桥列干不干净:print(员工["姓名"].duplicated().sum())(有几条重名),有重复就想清楚”一对多是不是我要的”(一人多部门时按天拆分可能正是你要的),不是就先去重员工.drop_duplicates("姓名")。
坑3:桥列的类型不一致,明明同一个人却对不上号。 一边”姓名”是文本、另一边被Excel存成了数字工号,或者一边”001″一边”1″——merge照着”字符串严格相等”对号,对不上就悄悄丢(回到坑1)。疫苗:合并后立刻验人:print(宽表["部门"].isna().sum())(有几个没查到部门)。查不到的不为0,就把两边的桥列都print出来肉眼对比,八成是空格、类型或写法不一致——df["姓名"] = df["姓名"].str.strip()先去空格是万能第一步。
坑4:两张表有同名不同义的列,拼完被”吃掉”或傻傻重复。 销售表和档案表都有”城市”列(一个是出差城市、一个是常驻城市),merge默认给同名列加后缀_x、_y(城市_x、城市_y)——数据没丢,但列名变成看不懂的暗号。疫苗:拼之前想清楚哪些列是桥、哪些列会撞名,用suffixes=("_销售", "_档案")把后缀改成人话:pd.merge(A, B, on="姓名", suffixes=("_销售", "_档案"))。
六、跟AI协作的正确姿势(合并特供)
- 用”对账语言”描述需求。说”把销售表和员工表按姓名合并,以销售表为主(left),对不上的部门留空,输出行数和空值数让我核对“——四要素(两张表、桥列、how、验收指标)说全。AI最常犯的错是默认
inner吃行和不验行数,拿到代码先查这两处 - 让AI”先合体再加工”。追加一句:”先只merge并print行数、列名、前5行让我确认拼对了,再做分组统计“。合并的错误比统计隐蔽得多(数字都挺像样,就是少了几行),先验拼接、再做分析,一步一个脚印
- 问”行数怎么变了”时,把两边的重复情况告诉AI。”主表5行,合并后8行”——让它查桥列重复(坑2);”主表5行,合并后3行”——让它查类型/空格不一致(坑3)+默认inner(坑1)。行数变化是合并问题的体温计,报问题先报行数
高频句式:”表A有这些列(贴df.dtypes),表B有这些列,按’某列’用left方式合并,加indicator=True对账,输出行数、对不上的行数、合并后前5行。注意桥列重复和类型一致性“。
七、今日小练习
练习1:三表摞一沓
写三张同结构的小表(1月、2月、3月的销售明细,各3行),用concat摞成全年表,要求行号重新编排,然后按”月份”分组算每月总销售额。(提示:pd.concat([一月, 二月, 三月], ignore_index=True)是今天的主菜;三张表手动构造太啰嗦,用循环for 月 in ["1月", "2月", "3月"]:造表存进列表再摞——第6篇循环的活儿。分组就是第33篇的groupby("月份")["销售额"].sum()。)
练习2:找出零单选手
拿今天的销售表和员工表,用merge找出档案里有、但一单没卖过的人,打印姓名和部门。(提示:how="right", indicator=True是本文第四节的现成招——但注意坑2,right合并后行数等于员工表行数(5行)才算对。想不加indicator也行:how="right"后筛宽表["销售额"].isna()——没卖过货的人销售额是空的,第32篇的筛选直接用。)
练习3:合并体检程序
写个”合并体检”函数:接收两张表和桥列名,合并后自动打印①主表行数②合并后行数③桥列在右表的重复数④对不上的行数,并给出一句健康判断(比如”行数变多,右表桥列有重复,请去重”)。(提示:函数是第7篇的活儿,参数是表A, 表B, 桥列。四个数字分别是len(表A)、len(宽表)、表B[桥列].duplicated().sum()、对账后_merge非both的行数。健康判断用第5篇的if/elif:行数变多→查重复,行数变少→查inner吃行,没变化→健康。这个函数是你以后所有合并的”随身医生”。)
写在后面:从”单表”到”多表”
盘点今天:concat是摞一沓——同结构的表上下叠,ignore_index=True重排行号;merge是对上号——on="共同列"找桥、how定去留(日常主表用left),indicator=True对账查差异。加上前四篇的读表、总量、筛选、分组,你的数据分析工具箱已经从”一张表打天下”升级到”多表联合作战”。
更值钱的还是思路:真实世界的数据从来不在一张表里——销售在销售表、人事在HR表、库存在仓库表,信息天然分布式存储,”合并”就是把它们对上号还原成完整真相。Excel的VLOOKUP是”贴上去的”,贴完就死;merge是”写下来的”,表一更新重跑就出新结果。当”表和表的合体”变成一行代码,你就再也不会被”数据分散在好几个文件里”吓住了。
下一篇,数据要”交货”了:分析结果怎么变成老板能打开的Excel和CSV——to_excel/to_csv,加上格式美化,你的报表终于能出门见人。
下篇预告
第三十五篇:导出报表——to_excel/to_csv登场:分析结果存成Excel,加列宽、加粗表头、多个sheet分页装订。老板说”把这个发我邮箱”,你交出一份有模有样的xlsx,而不是一坨裸数据。
发表回复