返回首页

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

第 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"左表全部,右边匹配不上就填 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
print(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: int64

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

map():作用在单个元素上#

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: object

applymap():老接口,新版已移除#

老版本 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 分析流程的完整主干:看表 → 取数 → 洗数 → 分组 → 合并重塑。 接下来就是靠真实项目继续练。建议拿一份自己工作中最常处理的数据,把这五章的代码从头到尾敲一遍。

阅读 --