AI编程(三十四):数据合并 — 两张表”对上号”拼成一张

作者:

在

“销售明细”只记了姓名和销售额,”员工档案”只记了姓名和部门——信息各记一半,老板却要”按部门看销售”。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月

看清楚两件事:

  1. 接头的地方,行号重复了——0、1、2、0、1、2。因为concat只是把两张表的行摞起来,各自的行号原样保留。加ignore_index=True(”别管旧编号,重新编”)就顺了:pd.concat([yiyue, eryue], ignore_index=True),行号变0到5;
  2. 列名得对得上。两张表都是”姓名、销售额、月份”才能干净地摞。如果2月表的列叫”金额”而不是”销售额”,摞出来就是两列各带一半空值——concat不猜你的心思,列名不同就是不同列。

pd.concat([表1, 表2, 表3, ...])的参数就是一个装表的列表,几张都行——12个月的表,写个循环把12个文件读进来装列表,一次摞完,第30篇批量处理的功夫正好用上。

什么时候用concat,什么时候用merge

一句话分清:

  • concat:两张表是同一种东西(1月销售+2月销售),摞成一沓;
  • merge(下面学):两张表是不同的东西(销售明细+员工档案),按共同列对上号拼宽表。

摞错了会怎样?拿员工档案去跟销售明细上下摞——姓名列对不齐、销售额和城市挤成一张表的上下两半,出来一坨谁也看不懂的东西。拼之前先问一句:这两张表是”同类摞一沓”还是”异类对号拼”?

三、merge:按”共同列”对上号

先想清楚:它到底干了什么

merge的动作,跟点名一模一样。想象班主任拿着两张纸:一张是”成绩单”(姓名+分数),一张是”花名册”(姓名+班级)。她想得到”分数+班级”的总表,干的事是:

  1. 找桥:两张表都有的那一列——”姓名”。这叫键(key),merge靠它对号;
  2. 对号:拿成绩单上的每个姓名,去花名册里找同名的人;
  3. 拼:找到了,把花名册那一行的信息接到成绩单旁边,变成一行全信息。

写成代码:

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协作的正确姿势(合并特供)

  1. 用”对账语言”描述需求。说”把销售表和员工表按姓名合并,以销售表为主(left),对不上的部门留空,输出行数和空值数让我核对“——四要素(两张表、桥列、how、验收指标)说全。AI最常犯的错是默认inner吃行和不验行数,拿到代码先查这两处
  2. 让AI”先合体再加工”。追加一句:”先只merge并print行数、列名、前5行让我确认拼对了,再做分组统计“。合并的错误比统计隐蔽得多(数字都挺像样,就是少了几行),先验拼接、再做分析,一步一个脚印
  3. 问”行数怎么变了”时,把两边的重复情况告诉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,而不是一坨裸数据。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注