第 5.3 节 常见问题#
这一节收集了初学者在「拼表」环节最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。
怎么用这一节 不用通读。拼表时遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。
准备一下数据,下面每题都会用到:
import pandas as pd
import numpy as np
orders = pd.DataFrame({
"订单号": [1001, 1002, 1003, 1004],
"城市": ["北京", "上海", "北京", "广州"],
"商品": ["手机", "电脑", "耳机", "手机"],
"金额": [7998.0, 7999.0, 1797.0, 7998.0],
})
clients = pd.DataFrame({
"订单号": [1001, 1002, 1005],
"客户名": ["张三", "李四", "王五"],
"会员等级": ["金卡", "银卡", "铜卡"],
})Q1:merge() 和 concat() 有什么区别?#
答: 一个按条件匹配,一个按位置堆叠。
merge() | concat() | |
|---|---|---|
| 对齐依据 | 你指定的共同字段 | 索引(axis=1 时)或直接摞(axis=0 时) |
| 行数 | 取决于 how | axis=0 时相加,axis=1 时取并集 |
| 像什么 | SQL 的 JOIN | 复制粘贴 |
| 什么时候用 | 「给表补信息」 | 「把多段记录接起来」 |
# 补信息:订单表加客户信息
orders.merge(clients, on="订单号", how="left")
# 接记录:这个月的表 + 上个月的表
pd.concat([jan_df, feb_df], ignore_index=True)一句话判断
- 结果里行数变多了,每行还是原来的那笔记录 → 用
concat();- 结果里行数不变或变少,多了几列 → 用
merge()。另一个判断角度:
merge需要「共同的字段」,concat不需要。
Q2:merge() 中的 inner、left、right 和 outer 分别表示什么?#
答: how 的名字就是「以哪张表为准」,另一张表匹配不上就填 NaN。
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 NaN
left是最常用的。 「订单表是主数据,客户表是补充信息」——补充信息缺了不影响订单本身,所以用left保住全部订单。outer常见于数据对账:把两边所有行都拉出来,看看差异在哪。
Q3:如何根据多个字段合并两个 DataFrame?#
答: on 传一个列表。
x = pd.DataFrame({"城市": ["北京", "上海"], "商品": ["手机", "电脑"], "金额": [1, 2]})
y = pd.DataFrame({"城市": ["北京", "上海", "广州"], "商品": ["手机", "电脑", "手机"], "金额": [10, 20, 30]})
print(x.merge(y, on=["城市", "商品"], suffixes=("_左", "_右"))) 城市 商品 金额_左 金额_右
0 北京 手机 1 10
1 上海 电脑 2 20「广州 + 手机」没匹配上,所以没出现。
多字段合并的「粒度」要想清楚。 上面那行结果的含义是:「同一个城市、卖同一种商品的两行才配对」。 如果你想的是「同一个城市就行」,那
on就只写["城市"]——但这会笛卡尔积:广州有 1 行、上海有 1 行还好,北京如果有多行就会炸出多行。 多键合并前先确认:你的业务里,是「这两个字段组合起来」才唯一吗?
Q4:如何处理合并后出现的重复列名?#
答: 用 suffixes 指定后缀。
a = pd.DataFrame({"订单号": [1, 2], "金额": [10, 20]})
b = pd.DataFrame({"订单号": [1, 3], "金额": [100, 300]})
print(a.merge(b, on="订单号", suffixes=("_左", "_右"))) 订单号 金额_左 金额_右
0 1 10 100默认行为:Pandas 会自动加 _x 和 _y:
a.merge(b, on="订单号").columns.tolist()['订单号', '金额_x', '金额_y']更推荐的做法:合并前先把重名列改掉。
a = a.rename(columns={"金额": "订单金额"}) b = b.rename(columns={"金额": "客户金额"})
金额_x/金额_y这种名字在后面写代码时很容易看错,改成有业务含义的名字会舒服很多。
Q5:如何使用 indicator=True 检查数据的匹配情况?#
答: 加上 indicator=True,结果会多一列 _merge。
m = orders.merge(clients, on="订单号", how="outer", indicator=True)
print(m) 订单号 城市 商品 金额 客户名 会员等级 _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 | 只有右表有 | 同上 |
拿它做筛选:
print(m[m["_merge"] == "left_only"][["订单号", "城市"]]) 订单号 城市
2 1003 北京
3 1004 广州统计各情况有几行:
print(m["_merge"].value_counts())left_only 2
both 2
right_only 1
dtype: int64
indicator=True是数据对账的标准动作。 「两边表合并后行数对不上」——不要猜,直接加indicator=True看一眼_merge的分布,情况立刻清楚。 排查完不想要这列的话,最后drop(columns=["_merge"])删掉。
Q6:concat() 的 axis=0 和 axis=1 有什么区别?#
答: axis=0(默认)上下摞,增加行;axis=1 左右拼,增加列。
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.0axis=1 则是按索引标签对齐地横向拼:
o = orders.set_index("订单号")
c = clients.set_index("订单号")
print(pd.concat([o, c], axis=1)) 城市 商品 金额 客户名 会员等级
订单号
1001 北京 手机 7998.0 张三 金卡
1002 上海 电脑 7999.0 李四 银卡
1003 北京 耳机 1797.0 NaN NaN
1004 广州 手机 7998.0 NaN NaN
1005 NaN NaN NaN 王五 铜卡
axis=1是按索引对齐的,不是按位置。 这和merge()有点像,但机制完全不同:merge按你指定的字段匹配,concat(axis=1)按索引标签。 所以按列拼之前,一定要先把连接字段设成索引,否则就是按「第几行」硬凑,很容易配错。
Q7:concat() 时如何处理不同 DataFrame 的列名和索引不一致的问题?#
答: 分三种情况处理。
情况一:列名不一致(axis=0 拼行)——不报错,缺的列填 NaN:
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这是静默的行为,不会警告你。所以拼完后一定要检查:
result = pd.concat([p1, p2])
print(result.isna().sum()) # 看看有多少格子是空的情况二:索引不一致(axis=1 拼列)——按索引对齐,缺的填 NaN:
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情况三:想让两边对齐——axis=1 时用 join 参数:
pd.concat([u, v], axis=1, join="inner") # 只保留两边都有的索引
pd.concat([u, v], axis=1, join="outer") # 默认,保留所有索引axis=0 也有 join 参数,作用是决定「以哪边的列为准」:
pd.concat([p1, p2], join="outer") # 默认,保留所有列
pd.concat([p1, p2], join="inner") # 只保留共有的列一个实用技巧:拼之前先对齐索引
df = df.reset_index(drop=True) # 索引重排成连续的 df2 = df2.reset_index(drop=True) result = pd.concat([df, df2], ignore_index=True)三招(
reset_index+ignore_index)能把 90% 的「拼表后索引乱掉」问题解决掉。
Q8:melt() 如何将宽表转换为长表?id_vars 和 value_vars 有什么作用?#
答: melt() 把「多个指标列」压成「多行」。
wide = pd.DataFrame({
"订单号": [1, 2],
"一季度": [100, 200],
"二季度": [150, 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 | 哪些列保持不动,作为每一行的标识 |
value_vars | 只把哪些列转成行(不写就是全部指标列) |
var_name | 原来那些列名(「一季度」「二季度」)放到哪一列 |
value_name | 原来格子里的值放到哪一列 |
只转指定的列:
print(pd.melt(wide, id_vars=["订单号"], value_vars=["二季度"],
var_name="季度", value_name="销售额")) 订单号 季度 销售额
0 1 二季度 150
1 2 二季度 250不写 id_vars 会怎样?
print(pd.melt(wide, var_name="季度", value_name="销售额")) 季度 销售额
0 订单号 1
1 订单号 2
2 一季度 100
3 一季度 200
4 二季度 150
5 二季度 250「订单号」也被当成指标转成了行——这通常不是你想要的。所以 id_vars 一定要写清楚。
什么时候需要 melt? 当你要按「季度」这个维度做分析时:
- 画折线图(绘图库要求一列是分组值);
- 把宽表和另一张「以季度为键」的表合并;
- 对每个季度算平均、占比。
如果只是要各季度合计,直接
wide[["一季度","二季度"]].sum()就行,不用绕这一圈。
Q9:stack()、unstack() 和 pivot() 分别适用于什么场景?#
答: 三个都是「长 ↔ 宽」转换,区别在于起点不同。
| 方法 | 起点 | 干什么 |
|---|---|---|
pivot() | 普通表 | 指定哪列做行、哪列做列、哪列取值,一步转成宽表 |
unstack() | 多级索引 | 把索引的最后一层摊开成列 |
stack() | 宽表 | 把列挤回行(unstack 的反方向) |
pivot() —— 最常用:
long = pd.DataFrame({
"订单号": [1, 1, 2],
"季度": ["Q1", "Q2", "Q1"],
"销售额": [100, 150, 200],
})
print(long.pivot(index="订单号", columns="季度", values="销售额"))季度 Q1 Q2
订单号
1 100.0 150.0
2 200.0 NaNunstack() —— 数据已经是多级索引时:
s = long.set_index(["订单号", "季度"])["销售额"]
print(s.unstack())季度 Q1 Q2
订单号
1 100.0 150.0
2 200.0 NaN结果和 pivot() 完全一样,但 pivot() 少一步 set_index(),所以优先用 pivot()。
stack() —— 反方向:
print(s.unstack().stack())订单号 季度
1 Q1 100.0
Q2 150.0
2 Q1 200.0
dtype: float64
pivot()要求「行 + 列」的组合唯一。dup = pd.DataFrame({"订单号":[1,1], "季度":["Q1","Q1"], "销售额":[100,200]}) dup.pivot(index="订单号", columns="季度", values="销售额") # ValueError: Index contains duplicate entries, cannot reshape遇到重复组合时,用
pivot_table()(第 4 章讲过),它会自动用聚合函数把重复的合并掉:dup.pivot_table(index="订单号", columns="季度", values="销售额", aggfunc="sum")
Q10:apply()、map() 和 applymap()(旧版接口)有什么区别?在新版 Pandas 中应如何选择合适的替代方法?#
答: 三个都是「调用自定义函数」,区别在于函数收到的粒度不同。
| 方法 | 作用对象 | 函数收到什么 | 返回 |
|---|---|---|---|
apply()(默认 axis=0) | 每一列 | 一整列 Series | 每列一个值 |
apply(axis=1) | 每一行 | 一整行 Series | 每行一个值 |
map() | 每个单元格 | 单个值 | 每个值一个结果 |
applymap()(旧版) | 每个单元格 | 单个值 | 每个值一个结果 |
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
# apply 按列
print(df.apply(lambda col: col.sum()))a 6
b 15
dtype: int64# apply 按行
print(df.apply(lambda row: row["a"] + row["b"], axis=1))0 5
1 7
2 9
dtype: int64# map 作用在单列的每个单元格
print(df["a"].map(lambda x: x * 10))0 10
1 20
2 30
dtype: int64旧版的 applymap():
# Pandas 2.x 里还能用
print(df.applymap(lambda x: x * 2)) a b
0 2 8
1 4 10
2 6 12但在 Pandas 3.0 里 applymap() 已经被移除:
df.applymap(lambda x: x * 2)
# AttributeError: 'DataFrame' object has no attribute 'applymap'替代方案是 DataFrame.map():
print(df.map(lambda x: x * 2)) a b
0 2 8
1 4 10
2 6 12新版 Pandas 的选择规则
你想做的事 用什么 Pandas 3.0 可用 对每一列做整体计算 df.apply(func)✅ 对每一行做计算 df.apply(func, axis=1)✅ 对单列的每个值做替换 df["列"].map(func)✅ 对整张表的每个格子做替换 df.map(func)✅ 对整张表的每个格子做替换(旧写法) df.applymap(func)❌ 已移除 要同时兼容新旧版本,可以这样写:
if hasattr(pd.DataFrame, "map"): result = df.map(func) else: result = df.applymap(func)
最后一条忠告:能用向量化的就别用
apply。df["金额"] = df["单价"] * df["数量"] # ✅ 快 df["金额"] = df.apply(lambda r: r["单价"] * r["数量"], axis=1) # ⚠️ 慢
apply会逐行调用 Python 函数,数据量大时明显变慢。它真正的用武之地是「没法写成公式」的逻辑,比如条件分支、字符串处理、多步计算。
小结#
这一节的 10 个问题,归纳成四组:
- 选工具——
merge按字段匹配,concat按位置堆叠;pivot_table/crosstab做二维报表,melt/unstack/pivot做形态转换; - 合并怎么连——
inner/left/right/outer四选一,suffixes处理重名列,indicator=True查匹配情况; - 拼接要注意——拼行加
ignore_index=True,拼列先set_index(),列名不一致会静默填 NaN; - 自定义变换——
apply按列或按行,map按单元格,applymap在 Pandas 3.0 已移除,改用DataFrame.map()。
课程真正结束了 五章的核心能力你已经全部具备。接下来最好的学习方式,就是拿一份真实数据,把「读进来 → 洗干净 → 分组汇总 → 拼其它表 → 输出报表」这条完整链路跑通一遍。 跑通一次,你就是会 Pandas 的了。