返回首页

Roadmap
更新于 2026年10月24日 全文 1888 字未阅读

第 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 时)
行数取决于 howaxis=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"左表全部,右边匹配不上填 NaN4
"right"右表全部,左边匹配不上填 NaN3
"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.0

axis=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    NaN

unstack() —— 数据已经是多级索引时:

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 个问题,归纳成四组:

  1. 选工具——merge 按字段匹配,concat 按位置堆叠;pivot_table/crosstab 做二维报表,melt/unstack/pivot 做形态转换;
  2. 合并怎么连——inner/left/right/outer 四选一,suffixes 处理重名列,indicator=True 查匹配情况;
  3. 拼接要注意——拼行加 ignore_index=True,拼列先 set_index(),列名不一致会静默填 NaN;
  4. 自定义变换——apply 按列或按行,map 按单元格,applymap 在 Pandas 3.0 已移除,改用 DataFrame.map()。

课程真正结束了 五章的核心能力你已经全部具备。接下来最好的学习方式,就是拿一份真实数据,把「读进来 → 洗干净 → 分组汇总 → 拼其它表 → 输出报表」这条完整链路跑通一遍。 跑通一次,你就是会 Pandas 的了。

阅读 --