数据合并的三种方式概览
- pd.concat() 沿着轴方向(行或列)拼接多个 DataFrame/Series。像把多块积木堆在一起,不做任何键匹配。
- pd.merge() 基于一个或多个键列的 SQL 风格连接,对应 SQL 的 JOIN。两个表必须有共同的键列,是最灵活的合并方式。
- df.join() 基于索引的连接,是 merge 的简化版。当连接键是行索引时更方便。
pd.concat:沿轴堆叠
import pandas as pd
import numpy as np
df1 = pd.DataFrame({'A': [1,2], 'B': [3,4]})
df2 = pd.DataFrame({'A': [5,6], 'B': [7,8]})
df3 = pd.DataFrame({'A': [9], 'C': [10]}) # 有不同的列
# ── 按行堆叠(axis=0,默认)──
pd.concat([df1, df2]) # 行索引重复
pd.concat([df1, df2], ignore_index=True) # 重置行索引
# 堆叠列不匹配的 DataFrame
pd.concat([df1, df3]) # join='outer'(默认):缺列填 NaN
pd.concat([df1, df3], join='inner') # 只保留共同列('A')
# 添加层级标识(方便区分来源)
combined = pd.concat(
[df1, df2],
keys=['2023', '2024'], # 最外层索引标识来源
ignore_index=False,
)
# 行索引变为 MultiIndex: ('2023', 0), ('2023', 1), ('2024', 0), ...
# ── 按列堆叠(axis=1)──
df_left = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [25, 30]})
df_right = pd.DataFrame({'salary': [60000, 80000], 'dept': ['eng', 'mkt']})
pd.concat([df_left, df_right], axis=1) # 按列拼接(按行索引对齐)
# 常见用法:将多个月的数据文件合并
import glob
files = glob.glob('data/2024-*.csv')
df_all = pd.concat(
[pd.read_csv(f) for f in files],
ignore_index=True
)
append() 已废弃
Pandas 2.0 移除了
df.append() 方法。替换方式:用 pd.concat([df, new_row_df])。在循环中频繁追加行时,先收集到 Python 列表,最后一次性 concat,避免每次都创建新 DataFrame。
pd.merge:SQL 风格的键连接
merge 对应 SQL 的 JOIN 操作,是最灵活的表合并方式。理解四种连接类型是关键:
| 连接类型 | SQL 等价 | 保留行为 |
|---|---|---|
how='inner' | INNER JOIN | 只保留两表键值都存在的行(默认) |
how='left' | LEFT JOIN | 保留左表所有行,右表无匹配填 NaN |
how='right' | RIGHT JOIN | 保留右表所有行,左表无匹配填 NaN |
how='outer' | FULL OUTER JOIN | 保留两表所有行,无匹配填 NaN |
import pandas as pd
employees = pd.DataFrame({
'emp_id': [1, 2, 3, 4],
'name': ['Alice', 'Bob', 'Carol', 'Dave'],
'dept_id':[10, 20, 10, 30],
})
departments = pd.DataFrame({
'dept_id': [10, 20, 40], # 注意:dept_id=30 不存在,40 是新的
'dept_name': ['工程部', '市场部', '行政部'],
})
# inner join:只有 dept_id 在两表都存在的员工
pd.merge(employees, departments, on='dept_id')
# Dave (dept_id=30) 被丢弃,行政部 (dept_id=40) 也被丢弃
# left join:保留所有员工,找不到部门的填 NaN
pd.merge(employees, departments, on='dept_id', how='left')
# Dave 的 dept_name 为 NaN
# 左右键名不同时
pd.merge(employees, departments, left_on='dept_id', right_on='dept_id')
# 本例两表键名相同,这只是演示参数用法
# 按索引连接
pd.merge(employees, departments, left_index=True, right_index=True)
# 多键连接
pd.merge(df1, df2, on=['year', 'month']) # 按年+月连接
# 处理重复列名(两表有相同列名但不作为连接键)
pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))
# 重复的非键列会被自动加上后缀
# validate 参数:验证连接类型的正确性
pd.merge(employees, departments, on='dept_id', validate='m:1')
# 'm:1':左表多行对应右表一行(最常见的员工-部门关系)
# '1:1':一对一
# '1:m':一对多
# 如果实际数据违反了指定的关系类型,会抛出异常
merge 的 indicator 参数
# indicator 参数:添加 _merge 列,标识每行的来源
result = pd.merge(employees, departments, on='dept_id', how='outer', indicator=True)
print(result['_merge'].value_counts())
# 'both' :两表都有
# 'left_only' :只在左表
# 'right_only' :只在右表
# 用于"反连接":找出只在左表出现的行
left_only = result[result['_merge'] == 'left_only'].drop(columns='_merge')
df.join:基于索引的连接
# join 默认基于索引连接
df_main = df1.set_index('emp_id')
df_extra = df2.set_index('emp_id')
df_main.join(df_extra) # 默认 left join
df_main.join(df_extra, how='inner') # inner join
# 同时 join 多个 DataFrame
df_main.join([df_extra1, df_extra2])
pivot_table:数据透视表
透视表是将"长格式"数据重塑为"宽格式"摘要的工具,类似 Excel 的数据透视表功能。它是 groupby + unstack 的组合简化写法。
import pandas as pd
sales = pd.DataFrame({
'year': [2023, 2023, 2024, 2024, 2023, 2024],
'region': ['华北', '华南', '华北', '华南', '华东', '华东'],
'product':['A', 'A', 'A', 'A', 'B', 'B'],
'amount': [100, 200, 150, 250, 80, 120],
'qty': [10, 20, 15, 25, 8, 12],
})
# 基础透视表:每个地区每年的总销售额
pivot = pd.pivot_table(
sales,
values='amount', # 汇总的值列
index='region', # 行维度
columns='year', # 列维度
aggfunc='sum', # 聚合方式
fill_value=0, # 缺失组合填0
margins=True, # 添加行列总计
margins_name='合计' # 总计行/列的标签
)
# 多值聚合
pivot2 = pd.pivot_table(
sales,
values=['amount', 'qty'], # 多个值列
index=['region', 'product'], # 多行维度
columns='year',
aggfunc={'amount': 'sum', 'qty': 'mean'} # 不同列不同聚合
)
# pivot(非聚合版):要求行列组合唯一
df_pivot = sales.pivot(index='region', columns='year', values='amount')
# 如果 (region, year) 有重复,会报错(需用 pivot_table)
melt:宽格式转长格式
数据整洁原则(Tidy Data)要求每个变量占一列,每次观测占一行。当数据以"宽格式"存在时(多个时间点的值分散在不同列),需要用 melt() 转换为"长格式"。
import pandas as pd
# 宽格式数据:每个月的销售额是一列
wide_df = pd.DataFrame({
'region': ['华北', '华南', '华东'],
'2024-01': [100, 200, 150],
'2024-02': [120, 210, 160],
'2024-03': [130, 190, 170],
})
# melt:将多列转为一列(长格式)
long_df = pd.melt(
wide_df,
id_vars=['region'], # 保持不变的列(标识符)
value_vars=['2024-01', '2024-02', '2024-03'], # 要熔化的列
var_name='month', # 熔化后"列名"存储在哪个新列
value_name='sales', # 熔化后"值"存储在哪个新列
)
# 结果:region, month, sales 三列,原来3列×3行变成了9行×3列
# 自动熔化所有非 id_vars 列
long_df2 = wide_df.melt(id_vars=['region'], var_name='month', value_name='sales')
stack 和 unstack:MultiIndex 与宽长转换
- stack() 将列标签"压入"行索引,变成 MultiIndex 的内层。相当于将宽格式转为长格式(列→行),与 melt 类似但操作对象是列名层级。
- unstack() 将行索引的某一层"弹出"变为列标签。相当于将长格式转为宽格式(行→列),与 pivot 类似。
import pandas as pd
# 基础 stack/unstack 示例
df = pd.DataFrame({
'math': [85, 90, 78],
'english': [72, 88, 95],
}, index=['Alice', 'Bob', 'Carol'])
stacked = df.stack() # 列变为行,返回 MultiIndex Series
# Alice math 85
# english 72
# Bob math 90
# english 88
df_back = stacked.unstack() # 还原为原始 DataFrame
# 指定 unstack 的层级
stacked.unstack(level=0) # 将第0层(姓名)展开为列
stacked.unstack(level=1) # 将第1层(科目)展开为列(默认)
stacked.unstack(level='name') # 按层级名称指定
# groupby + unstack 的组合(等价于 pivot_table)
result = (
sales
.groupby(['region', 'year'])['amount']
.sum()
.unstack(level='year') # 将 year 展开为列
.fillna(0)
)
explode:展开列表值
当某列的值是列表或其他可迭代对象时,explode() 可以将每个列表元素变成独立的行。
import pandas as pd
df = pd.DataFrame({
'product': ['A', 'B', 'C'],
'tags': [['电子', '数码'], ['家居'], ['电子', '家居', '数码']],
'price': [999, 199, 1299],
})
exploded = df.explode('tags')
# 每个 tag 占一行,product 和 price 列重复
# product tags price
# A 电子 999
# A 数码 999
# B 家居 199
# C 电子 1299
# C 家居 1299
# C 数码 1299
# 应用:统计每个 tag 出现的次数
tag_counts = (
df.explode('tags')['tags']
.value_counts()
)
综合案例:销售数据的多维分析
import pandas as pd
import numpy as np
# 生成模拟销售数据
np.random.seed(42)
n = 1000
orders = pd.DataFrame({
'order_id': range(1, n+1),
'customer_id': np.random.randint(1, 201, n),
'product': np.random.choice(['laptop', 'phone', 'tablet'], n),
'region': np.random.choice(['华北', '华南', '华东', '华西'], n),
'amount': np.random.randint(500, 15000, n),
'date': pd.date_range('2024-01-01', periods=n, freq='D'),
})
customers = pd.DataFrame({
'customer_id': range(1, 201),
'tier': np.random.choice(['gold', 'silver', 'bronze'], 200),
'city': np.random.choice(['北京', '上海', '广州', '成都'], 200),
})
# Step 1: 连接订单与客户数据
df = pd.merge(orders, customers, on='customer_id', how='left')
# Step 2: 添加时间维度
df['month'] = df['date'].dt.strftime('%Y-%m')
df['quarter'] = df['date'].dt.quarter
# Step 3: 透视表——产品 × 季度的销售额
product_quarter = pd.pivot_table(
df,
values='amount',
index='product',
columns='quarter',
aggfunc='sum',
fill_value=0,
margins=True,
margins_name='总计',
)
print(product_quarter)
# Step 4: 地区 × 客户等级的客单价
tier_region = pd.pivot_table(
df,
values='amount',
index='region',
columns='tier',
aggfunc='mean',
)
print(tier_region.round(0))
小结
数据合并与重塑是将多个数据源整合为分析就绪格式的关键技能:
- concat:简单拼接(行/列方向),不做键匹配
- merge:SQL 风格的键连接,使用
validate参数验证连接类型 - pivot_table:分组聚合的宽格式呈现,类似 Excel 透视表
- melt:宽格式→长格式,是 pivot_table 的逆操作
- stack/unstack:MultiIndex 层级与列之间的转换