第 5.1 节 核心知识点#
预计学习时长: 50 分钟
本节目标:
- 分清
merge()(按条件匹配)和concat()(按位置堆叠);- 会用
inner/left/right/outer四种连接方式;- 会用
suffixes处理重名列,用indicator=True检查匹配情况;- 会用
concat()按行拼和按列拼;- 会用
melt()把宽表变长表、unstack()把长表变宽表;- 会用
apply()和map()写自己的变换逻辑。
先说清楚一件事:merge 和 concat 的本质区别#
这一章所有的困惑,都来自没分清这两件事:
merge() | concat() | |
|---|---|---|
| 干什么 | 按条件匹配 | 按位置堆叠 |
| 怎么对齐 | 按你指定的「共同字段」 | 按索引对齐 |
| 行数会变吗 | 会(取决于 how) | axis=0 时相加,axis=1 时不变 |
| 类比 | SQL 的 JOIN | 复制粘贴 |
一个生活化的比喻:
merge()像是查字典——你拿着订单号去客户表里查,查到谁就补上谁的信息,查不到的就空着;concat()像是把两叠纸摞起来——不看内容,只看位置。
搞清这个区别,下面所有内容都好理解。
知识点 1:Merge DataFrames(合并 DataFrame)#
准备两张表:
import pandas as pd
orders = pd.DataFrame({
"订单号": [1001, 1002, 1003, 1004],
"城市": ["北京", "上海", "北京", "广州"],
"商品": ["手机", "电脑", "耳机", "手机"],
"金额": [7998.0, 7999.0, 1797.0, 7998.0],
})
clients = pd.DataFrame({
"订单号": [1001, 1002, 1005],
"客户名": ["张三", "李四", "王五"],
"会员等级": ["金卡", "银卡", "铜卡"],
})现在的问题是:订单表里没有客户名,怎么补上?
答案:merge()。
最基本的合并#
result = orders.merge(clients, on="订单号")
print(result)输出:
订单号 城市 商品 金额 客户名 会员等级
0 1001 北京 手机 7998.0 张三 金卡
1 1002 上海 电脑 7999.0 李四 银卡看结果:
- 只有 2 行,不是 4 行也不是 3 行;
- 订单 1003、1004 在客户表里找不到,被丢掉了;
- 订单 1005 在客户表里有,但订单表里没有,也没出现。
这就是默认的 how="inner"——只要两边都有的。
四种连接方式#
merge() 的 how 参数决定「以谁为准」:
how | 保留哪些行 | 结果行数 | 什么时候用 |
|---|---|---|---|
"inner"(默认) | 两边都有的 | 2 | 只关心完全匹配上的 |
"left" | 左表全部,右边匹配不上就填 NaN | 4 | 左边是主表,右边是补充信息 |
"right" | 右表全部,左边匹配不上就填 NaN | 3 | 右边是主表 |
"outer" | 两边全部 | 5 | 要做数据对账,找出所有差异 |
print(orders.merge(clients, on="订单号", how="left")) 订单号 城市 商品 金额 客户名 会员等级
0 1001 北京 手机 7998.0 张三 金卡
1 1002 上海 电脑 7999.0 李四 银卡
2 1003 北京 耳机 1797.0 NaN NaN
3 1004 广州 手机 7998.0 NaN NaNprint(orders.merge(clients, on="订单号", how="outer")) 订单号 城市 商品 金额 客户名 会员等级
0 1001 北京 手机 7998.0 张三 金卡
1 1002 上海 电脑 7999.0 李四 银卡
2 1003 北京 耳机 1797.0 NaN NaN
3 1004 广州 手机 7998.0 NaN NaN
4 1005 NaN NaN NaN 王五 铜卡
left是最常用的。 场景:「订单表是主数据,客户表是补充信息」——补充信息缺了不影响订单本身,所以要保留全部订单。 记住一句话:how的名字就是「以哪张表为准」,另一张表匹配不上就填 NaN。
用 indicator=True 检查匹配情况#
做数据对账时,你想知道「哪些行两边都有、哪些只有一边」。加 indicator=True:
print(orders.merge(clients, on="订单号", how="outer", indicator=True)) 订单号 城市 商品 金额 客户名 会员等级 _merge
0 1001 北京 手机 7998.0 张三 金卡 both
1 1002 上海 电脑 7999.0 李四 银卡 both
2 1003 北京 耳机 1797.0 NaN NaN left_only
3 1004 广州 手机 7998.0 NaN NaN left_only
4 1005 NaN NaN NaN 王五 铜卡 right_only_merge 列有三种取值:
| 值 | 含义 |
|---|---|
both | 两边都有 |
left_only | 只有左表有 |
right_only | 只有右表有 |
然后就能直接筛选:
m = orders.merge(clients, on="订单号", how="outer", indicator=True)
print(m[m["_merge"] == "left_only"][["订单号", "城市"]]) 订单号 城市
2 1003 北京
3 1004 广州这是排查数据问题的神器。 「订单表有 4 条,客户表有 3 条,合并后少了 2 条,去哪了?」——用
indicator=True一看就清楚。
合并时列名不一样怎么办#
如果两张表的连接字段名字不一样(比如订单表叫「订单号」,客户表叫「order_id」),用 left_on 和 right_on:
p = orders.rename(columns={"订单号": "order_id"})
print(orders.merge(p[["order_id", "城市"]], left_on="订单号", right_on="order_id").columns.tolist())输出:
['订单号', '城市_x', '商品', '金额', 'order_id', '城市_y']注意出现了「城市_x」和「城市_y」——因为「城市」这一列在两张表里都有,重名了。
重名列加后缀:suffixes#
刚才那个「城市_x / 城市_y」是 Pandas 自动加的。想自己指定后缀:
a = pd.DataFrame({"订单号": [1, 2], "金额": [10, 20]})
b = pd.DataFrame({"订单号": [1, 3], "金额": [100, 300]})
print(a.merge(b, on="订单号", suffixes=("_左", "_右"))) 订单号 金额_左 金额_右
0 1 10 100按多个字段合并#
连接键可以是一个列表:
x = pd.DataFrame({"城市": ["北京", "上海"], "商品": ["手机", "电脑"], "金额": [1, 2]})
y = pd.DataFrame({"城市": ["北京", "上海", "广州"], "商品": ["手机", "电脑", "手机"], "金额": [10, 20, 30]})
print(x.merge(y, on=["城市", "商品"], suffixes=("_左", "_右"))) 城市 商品 金额_左 金额_右
0 北京 手机 1 10
1 上海 电脑 2 20「广州 + 手机」没匹配上,所以没出现(默认 inner)。
知识点 2:Concatenate DataFrames(拼接 DataFrame)#
concat() 不做匹配,只做堆叠。
按行拼(axis=0,默认)#
jan = pd.DataFrame({"订单号": [1001, 1002], "金额": [7998.0, 7999.0]})
feb = pd.DataFrame({"订单号": [1003, 1004], "金额": [1797.0, 7998.0]})
print(pd.concat([jan, feb])) 订单号 金额
0 1001 7998.0
1 1002 7999.0
0 1003 1797.0
1 1004 7998.0注意索引重复了——两段都是 0, 1。这在后续操作时容易出问题,所以通常要加 ignore_index=True:
print(pd.concat([jan, feb], ignore_index=True)) 订单号 金额
0 1001 7998.0
1 1002 7999.0
2 1003 1797.0
3 1004 7998.0拼表时几乎总要加
ignore_index=True,让索引重新连续编号。 索引重复不会报错,但等你后面loc取值或者存 CSV 时就会出问题。
列不一样也能拼#
p1 = pd.DataFrame({"a": [1, 2]})
p2 = pd.DataFrame({"b": [3, 4]})
print(pd.concat([p1, p2])) a b
0 1.0 NaN
1 2.0 NaN
0 NaN 3.0
1 NaN 4.0缺的地方自动填 NaN,不会报错。
按列拼(axis=1)#
info = clients.set_index("订单号")
amount = orders.set_index("订单号")[["城市", "金额"]]
print(pd.concat([amount, info], axis=1)) 城市 金额 客户名 会员等级
订单号
1001 北京 7998.0 张三 金卡
1002 上海 7999.0 李四 银卡
1003 北京 1797.0 NaN NaN
1004 广州 7998.0 NaN NaN
1005 NaN NaN 王五 铜卡按列拼是按「索引」对齐的,不是按位置!这跟 merge() 有点像了,但机制完全不同:
merge()按你指定的字段匹配;concat(axis=1)按索引标签对齐。
索引不一致时会怎样?
u = pd.DataFrame({"金额": [1, 2]}, index=[0, 1])
v = pd.DataFrame({"客户名": ["A", "B"]}, index=[1, 2])
print(pd.concat([u, v], axis=1)) 金额 客户名
0 1.0 NaN
1 2.0 A
2 NaN B索引 0 只有左边有,索引 2 只有右边有,中间索引 1 的是两边的组合。
join 参数#
按列拼时还有个 join 参数:
pd.concat([u, v], axis=1, join="inner") # 只保留两边都有的索引
pd.concat([u, v], axis=1, join="outer") # 默认,保留所有索引知识点 3:Melt Wide to Long(宽格式转长格式)#
「宽表」是每个指标一列,「长表」是每个指标一行。
宽表(人眼看着直观,但不好画图):
wide = pd.DataFrame({
"订单号": [1, 2],
"一季度": [100, 200],
"二季度": [150, 250],
})
print(wide) 订单号 一季度 二季度
0 1 100 150
1 2 200 250长表(每行一个「订单 + 季度」):
long = pd.melt(wide, id_vars=["订单号"], var_name="季度", value_name="销售额")
print(long) 订单号 季度 销售额
0 1 一季度 100
1 2 一季度 200
2 1 二季度 150
3 2 二季度 250三个参数:
| 参数 | 作用 |
|---|---|
id_vars | 哪些列要保留,并作为每一行的标识 |
var_name | 原来那些列名(「一季度」「二季度」)放哪一列 |
value_name | 原来格子里的值放哪一列 |
value_vars:只转指定的列#
print(pd.melt(wide, id_vars=["订单号"], value_vars=["一季度"],
var_name="季度", value_name="销售额")) 订单号 季度 销售额
0 1 一季度 100
1 2 一季度 200只有「一季度」被转成了行,「二季度」那列消失了。
不指定 id_vars 会怎样?#
print(pd.melt(wide, var_name="季度", value_name="销售额")) 季度 销售额
0 订单号 1
1 订单号 2
2 一季度 100
3 一季度 200
4 二季度 150
5 二季度 250「订单号」这一列也被当成指标转成了行——这通常不是你要的。所以 id_vars 一定要写清楚。
为什么要转成长表? 因为画图工具(比如
seaborn的折线图、plotly)几乎都要求「一列是 X 轴、一列是 Y 轴、一列是颜色分组」。 宽表里「一季度、二季度」是两个列名,绘图库不认识;转成长表后,「季度」变成一列的值,绘图库就能按它分组画线了。
知识点 4:Unstack Long to Wide(长格式转宽格式)#
unstack() 是 melt() 的反向操作——把多级索引的行标签转成列。
long = pd.DataFrame({
"订单号": [1, 1, 2],
"季度": ["Q1", "Q2", "Q1"],
"销售额": [100, 150, 200],
})
s = long.set_index(["订单号", "季度"])["销售额"]
print(s.unstack())季度 Q1 Q2
订单号
1 100.0 150.0
2 200.0 NaN- 原来「订单号」和「季度」是两层索引;
unstack()把最后一层索引(季度)转成了列;- 没有对应值的位置填 NaN(订单 2 没有 Q2)。
stack() 是反过来的#
print(s.unstack().stack())订单号 季度
1 Q1 100.0
Q2 150.0
2 Q1 200.0
dtype: float64又变回长格式了。
pivot():更直观的写法#
unstack() 要求你先把别的列设成索引,步骤多。pivot() 可以一步到位:
print(long.pivot(index="订单号", columns="季度", values="销售额"))季度 Q1 Q2
订单号
1 100.0 150.0
2 200.0 NaN结果和 unstack() 完全一样,但写法直观得多。
三个怎么选?#
| 方法 | 适用场景 |
|---|---|
pivot() | 从普通表出发,指定哪列做行、哪列做列、哪列取值。最常用 |
unstack() | 数据已经是多级索引了,直接把某一层摊开成列 |
stack() | unstack() 的反方向,把列挤回行 |
pivot()要求「行 + 列」的组合唯一。 如果同一个「订单号 + 季度」出现两行,pivot()会直接报ValueError: Index contains duplicate entries, cannot reshape。 这时候要用pivot_table()(第 4 章讲过),它会自动用聚合函数把重复的合并掉。
知识点 5:Apply Custom Transforms(应用自定义变换)#
当内置方法不够用时,用 apply() 把自己的函数交给 Pandas。
apply() 作用在整列#
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
print(df.apply(lambda col: col.sum()))输出:
a 6
b 15
dtype: int64不写 axis 时,默认按列(axis=0)遍历,每列传进来,返回每列一个结果。
apply() 作用在整行#
print(df.apply(lambda row: row["a"] + row["b"], axis=1))输出:
0 5
1 7
2 9
dtype: int64axis=1 让 Pandas 逐行遍历,把每一行(一个 Series)传给你的函数。
一个更实用的例子#
df["总计"] = df.apply(lambda row: row["a"] + row["b"], axis=1)
print(df) a b 总计
0 1 4 5
1 2 5 7
2 3 6 9map():作用在单个元素上#
apply() 处理整列(传进来的是一整列),map() 处理单个元素:
print(df["a"].map(lambda x: x * 10))输出:
0 10
1 20
2 30
dtype: int64选型口诀
- 想对每一列做整体计算(求和、最大值)→
apply()(默认axis=0)- 想对每一行做计算 →
apply(axis=1)- 想对每个单元格做替换/映射 →
map()- 想对每个单元格做替换/映射,且要处理整张表 →
DataFrame.map()
条件判断也能用 apply#
print(df.apply(lambda row: "大" if row["a"] > 1 else "小", axis=1))输出:
0 小
1 大
2 大
dtype: objectapplymap():老接口,新版已移除#
老版本 Pandas 有一个 applymap(),对每个单元格做变换:
# Pandas 2.x 里还能用
df.applymap(lambda x: x * 2) a b
0 2 8
1 4 10
2 6 12但在 Pandas 3.0 里,applymap() 已经被移除了,会报 AttributeError:
df.applymap(lambda x: x * 2)
# AttributeError: 'DataFrame' object has no attribute 'applymap'替代方案是 DataFrame.map():
df.map(lambda x: x * 2) # Pandas 2.1+ 和 3.0 都可以版本兼容的写法
# 想同时兼容老版本和新版本 if hasattr(pd.DataFrame, "map"): result = df.map(func) else: result = df.applymap(func)不过多数情况下你只需要支持一个版本,直接写
df.map(func)就行——Pandas 2.1 起DataFrame.map就已经可用了。
本节小结#
merge()是按条件匹配,concat()是按位置堆叠——这是本章最核心的区别。merge()的四种how:inner(两边都有)、left(左表全留)、right(右表全留)、outer(两边全留)。indicator=True会加一列_merge,取值both/left_only/right_only,用来查匹配情况。- 重名列用
suffixes=("_左", "_右")指定后缀;连接字段名不同用left_on/right_on。 concat(axis=0)按行摞(默认),concat(axis=1)按列摞、按索引对齐;拼行时记得ignore_index=True。melt()宽转长:id_vars保留哪些列,value_vars只转哪些列。unstack()把多级索引的最后一层摊成列;pivot()是从普通表出发的更直观写法;stack()是反方向。apply()按列(默认)或按行(axis=1)处理整块数据;map()处理单个单元格;applymap()在 Pandas 3.0 已被移除,用DataFrame.map()替代。
课程到这里就结束了 五章走完,你已经掌握了 Pandas 分析流程的完整主干:看表 → 取数 → 洗数 → 分组 → 合并重塑。 接下来就是靠真实项目继续练。建议拿一份自己工作中最常处理的数据,把这五章的代码从头到尾敲一遍。