数据组合与合并:Pandas 数据整合全指南 +缺失值处理
数据组合与合并:Pandas 数据整合全指南
在进行数据分析之前,数据清洗与整合是关键步骤。
遵循“整洁数据”(Tidy Data)原则:
- 每个观测值占一行
- 每个变量占一列
- 每种观测单元构成一张独立的表格
整理好数据后,常常需要将多个数据集组合起来,才能回答复杂问题。
一、连接数据(Concatenation)
当多个数据集结构相似(列名一致或相近),可以使用 pd.concat()
将它们按行或列方向拼接。
1. 添加行:DataFrame 连接
import pandas as pd# 加载多个结构相似的 CSV 文件
df1 = pd.read_csv('data/demo1.csv')
df2 = pd.read_csv('data/demo2.csv')
df3 = pd.read_csv('data/demo3.csv')# 沿行方向(垂直)连接
row_concat = pd.concat([df1, df2, df3], ignore_index=True) # 重置索引
print(row_concat)
参数说明:
ignore_index=True
:重新生成从 0 开始的整数索引,避免重复行标签。- 默认
axis=0
表示按行连接(上下拼接)。
2. 添加行:DataFrame 与 Series 连接
df1 = pd.read_csv('data/demo1.csv')
new_series = pd.Series(['n1', 'n2', 'n3', 'n4'], index=['A', 'B', 'C', 'D']) # 推荐指定索引# 错误方式(不推荐):
# pd.concat([df1, new_series]) # Series 被当作新列添加,造成 NaN 填充# 正确方式:将 Series 转为 DataFrame 再连接
new_row_df = pd.DataFrame([new_series], index=['new_row']) # 包装成一行的 DataFrame
result = pd.concat([df1, new_row_df], ignore_index=False)
关键点:
- 直接
concat([df, series])
会尝试按列对齐,导致Series
成为新列,原数据缺失处填NaN
。 - 应先将
Series
转换为单行DataFrame
,并确保其index
与目标DataFrame
的列名匹配。
3. 添加行:使用 append()
方法(已弃用,建议用 concat)
注意:
DataFrame.append()
方法在 Pandas 1.4+ 中已被 弃用(deprecated),官方推荐使用pd.concat()
替代。
旧写法(不推荐):
# 已弃用
new_row = {'A': 'n1', 'B': 'n2', 'C': 'n3', 'D': 'n4'}
df1 = df1.append(new_row, ignore_index=True)
新写法(推荐):
new_row_df = pd.DataFrame([{'A': 'n1', 'B': 'n2', 'C': 'n3', 'D': 'n4'}])
df1 = pd.concat([df1, new_row_df], ignore_index=True)
4. 添加行:重置索引
当多次连接后,行索引可能出现重复或无序:
result = pd.concat([df1, df2, df3])
result = result.reset_index(drop=True) # 丢弃原索引,生成新整数索引
建议:在垂直连接时始终使用
ignore_index=True
或后续调用reset_index(drop=True)
。
5. 添加列:使用 concat()
水平连接(axis=1
)可实现列拼接:
col_concat = pd.concat([df1, df2], axis=1) # 按列拼接(左右)
注意:
- 默认按行索引对齐,若索引不一致会导致
NaN
。 - 若两个 DataFrame 行数不同但索引相同,也能对齐。
6. 添加列:直接赋值(最常用)
# 添加标量(广播)
df1['new_col'] = 0# 添加列表或数组(长度需匹配)
df1['new_col'] = [1, 2, 3, 4]# 添加 Series(按索引自动对齐)
s = pd.Series([10, 20, 30, 40], index=df1.index)
df1['new_col'] = s
这是最简洁高效的方式,适用于大多数场景。
7. 添加列:重置索引的影响
如果 Series
的索引与 DataFrame
不一致,直接赋值可能导致 NaN
:
s = pd.Series([10, 20, 30, 40], index=[0, 1, 2, 3])
df1['new_col'] = s # 若 df1 索引为 [0,1,2,3],则正常;否则部分为 NaN
解决方案:
- 使用
.values
强制忽略索引:df1['new_col'] = s.values # 忽略索引,按顺序赋值
- 或先对齐索引:
s.reindex(df1.index)
二、合并多个数据集(Merge & Join)
当数据集之间有共同键(key),但结构不同(如主表+属性表),应使用 merge
或 join
。
方法 | 类型 | 用途 | 对齐方式 | 默认连接类型 |
---|---|---|---|---|
pd.concat() | 函数 | 拼接多个对象(行/列) | 按索引对齐 | 外连接(outer) |
DataFrame.join() | 方法 | 水平合并多个 DataFrame | 左表列/行索引 vs 右表行索引 | 左连接(left) |
pd.merge() | 函数 | 灵活合并两个 DataFrame | 基于列或索引 | 内连接(inner) |
1. pd.merge()
:最灵活的合并方式
left = pd.DataFrame({'key': ['K0', 'K1', 'K2'],'A': ['A0', 'A1', 'A2']
})right = pd.DataFrame({'key': ['K0', 'K1', 'K3'],'B': ['B0', 'B1', 'B3']
})# 基于列 'key' 合并
merged = pd.merge(left, right, on='key', how='inner')
参数详解:
on
:指定连接键(列名)how
:连接方式'inner'
:交集(默认)'outer'
:并集'left'
:保留左表所有行'right'
:保留右表所有行
left_on
,right_on
:左右表键名不同时使用left_index=True
,right_index=True
:基于索引合并
示例:不同列名合并
pd.merge(left, right, left_on='key', right_on='key_r', how='outer')
2. DataFrame.join()
:基于索引的便捷合并
result = left.join(right.set_index('key'), on='key', how='left')
# 或者 right 的索引已经是 'key'
# result = left.join(right, on='key')
特点:
- 默认以左表为基准(
how='left'
) - 通常用于:主表 + 多个属性表(如公司信息 + 股价、行业等)
- 支持多表连接:
df.join([df2, df3, df4])
注意:
join()
默认是左连接,而merge()
默认是内连接!
3. concat()
vs merge()
vs join()
对比总结
功能 | concat() | merge() | join() |
---|---|---|---|
是否支持多对象 | 支持多个 | 仅两个 | 支持多个 |
连接方向 | 行或列 | 仅列(水平) | 仅列(水平) |
对齐依据 | 索引 | 列或索引 | 左表列/索引 vs 右表索引 |
默认连接方式 | outer | inner | left |
典型用途 | 日志文件合并、时间序列拼接 | 主键关联表(如订单+用户) | 属性扩展(如 ID + 特征) |
三、常见实践建议
推荐流程:
- 清理单个数据集 → 遵循整洁数据原则
- 统一列名与数据类型
- 根据关系选择合并方式:
- 结构相同 →
pd.concat()
- 有公共键 →
pd.merge()
或df.join()
- 结构相同 →
- 合并后检查:
- 行数是否合理?
- 是否出现意外的
NaN
? - 索引是否需要重置?
常见错误避免:
- 忘记
ignore_index=True
导致索引重复 - 直接
concat(df, series)
而未包装成 DataFrame - 使用已弃用的
.append()
merge
时未指定on
,导致笛卡尔积- 忽视
how
参数,丢失数据(如默认 inner 丢掉不匹配行)
四、总结
数据整合三剑客
1. pd.concat()
- 用途:拼接多个对象(行/列)
- axis=0: 上下拼(常用)
- axis=1: 左右拼
- ignore_index=True 重置索引
2. pd.merge()
- 用途:基于列或索引合并两个表
- on=‘key’: 指定连接列
- how: inner, outer, left, right
- 最灵活,推荐用于主键关联
3. df.join()
- 用途:基于索引合并多个表
- 默认左连接
- 常用于主表 + 多个属性表
Tip: 优先使用
concat
和merge
,join
适合索引对齐场景。
五、补充
1. 合并时处理重复列名(suffixes 参数)
当两个 DataFrame 有相同列名但不是连接键时,merge
或 join
会自动添加后缀避免冲突。
left = pd.DataFrame({'key': ['K0', 'K1'], 'value': [1, 2]})
right = pd.DataFrame({'key': ['K0', 'K1'], 'value': [3, 4]})merged = pd.merge(left, right, on='key', suffixes=('_left', '_right'))
print(merged)
# 输出:
# key value_left value_right
# 0 K0 1 3
# 1 K1 2 4
用途:比较同一指标在不同时间/来源的数据。
建议:始终显式设置
suffixes
,避免默认的_x
,_y
造成混淆。
2. 基于多列合并(复合键合并)
有时需要多个字段共同作为“主键”来合并。
pd.merge(df1, df2, on=['date', 'city', 'product_id'], how='inner')
典型场景:
- 按“日期+地区”合并天气与销售数据
- 按“用户ID+商品ID”合并评分与评论
注意:确保多列组合后能唯一标识一条记录,否则可能产生笛卡尔积(行数暴增)。
3. 使用索引进行合并(left_index / right_index)
当数据以索引为唯一标识时(如时间序列、ID),可以直接用索引合并。
# 基于索引合并
result = pd.merge(df1, df2, left_index=True, right_index=True, how='outer')# 等价于:
result = df1.join(df2, how='outer')
优势:避免创建冗余的 ID 列;适合时间对齐。
示例:对齐不同频率的股票数据(日频 + 周频)
4. concat 的 join 参数:inner vs outer
pd.concat(..., axis=1)
默认是 outer
(外连接),但可通过 join='inner'
只保留共有的行。
# 只保留所有表都存在的行
pd.concat([df1, df2, df3], axis=1, join='inner')# 保留所有行(默认)
pd.concat([df1, df2, df3], axis=1, join='outer')
用途:
join='inner'
:严格对齐,避免 NaNjoin='outer'
:保留全部信息,后续填充缺失值
5. 合并性能优化建议
场景 | 建议 |
---|---|
大数据集合并 | 确保连接键是 category 或 int 类型(比 object 快) |
多次合并 | 先合并小表,再与大表连接 |
时间序列拼接 | 使用 pd.concat() + ignore_index=False 保持时间索引 |
内存不足 | 考虑使用 dask 或 polars 替代 |
小技巧:合并前检查数据类型:
print(df1['key'].dtype)
print(df2['key'].dtype)
# 确保类型一致,否则可能导致匹配失败!
6. 合并后的质量检查清单(QA)
每次合并后建议检查以下几点:
def check_merge_result(left, right, merged, how):print(f"左表行数: {len(left)}")print(f"右表行数: {len(right)}")print(f"合并后行数: {len(merged)}")if how == 'inner':assert len(merged) <= min(len(left), len(right))elif how == 'left':assert len(merged) >= len(left)elif how == 'right':assert len(merged) >= len(right)elif how == 'outer':assert len(merged) >= max(len(left), len(right))print(f"新增 NaN 数量: {merged.isna().sum().sum()}")
关键问题:
- 行数是否合理?
- 是否出现大量
NaN
?是否预期? - 索引是否重复或混乱?
- 数据类型是否被意外转换?
缺失值处理
为什么会出现缺失值?
在数据整合过程中,以下操作极易引入
NaN
:
pd.concat(..., axis=1)
拼接列时,行索引不完全对齐pd.merge(..., how='left')
左连接时,右表无匹配记录join
扩展属性时,某些 ID 没有对应信息- 不同来源数据字段覆盖不全
Pandas 使用 NaN
(Not a Number)表示浮点型缺失值,None
表示对象型缺失值,两者在大部分操作中被视为等价。
一、识别缺失值
1. 检查缺失情况
# 查看每个字段缺失数量
df.isnull().sum()# 查看整体缺失比例
df.isnull().mean() * 100# 查看是否有任意缺失
df.isnull().any().any()# 可视化缺失模式(需 seaborn)
import seaborn as sns
sns.heatmap(df.isnull(), cbar=True, yticklabels=False, cmap='viridis')
建议:在每次合并后立即检查缺失情况。
二、缺失值的产生场景与应对策略
整合操作 | 缺失原因 | 建议处理方式 |
---|---|---|
concat(axis=0) | 不同文件字段不一致 | 统一列名 / 补充默认值 |
concat(axis=1) | 行索引不对齐 | 对齐索引 / 使用 join='inner' |
merge(how='left') | 右表无匹配键 | 检查数据完整性 / 改用 outer |
join() | 某些 ID 无扩展信息 | 补充默认属性 / 标记为“未知” |
三、处理缺失值的常用方法
1. 删除缺失值(dropna
)
适用于:缺失严重且无法填补,或样本足够多。
# 删除任意含缺失的行
df.dropna(axis=0, how='any')# 删除所有值都缺失的列
df.dropna(axis=1, how='all')# 只在关键列缺失时删除
df.dropna(subset=['user_id', 'order_date'])
风险:可能导致样本偏差或信息丢失。
2. 填充缺失值(fillna
)
(1)填充固定值
df['category'] = df['category'].fillna('Unknown')
df['price'] = df['price'].fillna(0)
(2)前向/后向填充(适合时间序列)
df['value'] = df['value'].fillna(method='ffill') # 用前一个值填充
df['value'] = df['value'].fillna(method='bfill') # 用后一个值填充
method
参数已弃用,推荐使用ffill
/bfill
方法:df['value'] = df['value'].ffill()
(3)用统计量填充
# 数值型:均值、中位数
df['age'] = df['age'].fillna(df['age'].mean())
df['income'] = df['income'].fillna(df['income'].median())# 分类型:众数
mode_value = df['gender'].mode()[0] # 取第一个众数
df['gender'] = df['gender'].fillna(mode_value)
(4)用模型预测填充(高级)
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df[['age', 'income']] = imputer.fit_transform(df[['age', 'income']])
3. 插值法填充(interpolate
)
适用于:时间序列或有序数据。
# 线性插值
df['value'] = df['value'].interpolate(method='linear')# 时间序列插值(考虑时间间隔)
df['value'] = df.set_index('date')['value'].interpolate(method='time').values# 多项式插值(更平滑)
df['value'] = df['value'].interpolate(method='polynomial', order=2)
4. 标记缺失(创建指示变量)
有时“是否缺失”本身就是一个重要特征。
# 创建缺失标志列
df['age_missing'] = df['age'].isnull().astype(int)
df['price_missing'] = df['price'].isnull().astype(int)
用途:在机器学习中作为额外特征,帮助模型理解数据质量。
四、结合数据整合的完整流程示例
# 示例:合并用户信息与订单数据
users = pd.read_csv('users.csv') # 包含 user_id, age, city
orders = pd.read_csv('orders.csv') # 包含 order_id, user_id, amount# 步骤1:合并
merged = pd.merge(orders, users, on='user_id', how='left')# 步骤2:检查缺失
print("合并后缺失情况:")
print(merged.isnull().sum())# 步骤3:处理缺失
merged['city'] = merged['city'].fillna('Unknown')
merged['age'] = merged['age'].fillna(merged['age'].median())# 步骤4:标记缺失(可选)
merged['age_was_missing'] = (merged['age'] == merged['age'].median()).astype(int)# 步骤5:验证
assert merged.isnull().sum().sum() == 0 # 确保无缺失
五、缺失值处理策略选择指南
场景 | 推荐方法 |
---|---|
缺失率 < 5% | 可考虑删除行 |
数值型变量 | 均值/中位数填充,或插值 |
分类型变量 | 众数填充,或新增“Unknown”类别 |
时间序列 | 前向填充、时间插值 |
关键字段无匹配 | 检查数据源,避免错误合并 |
机器学习建模 | 结合 fillna + 缺失标志列 |
六、最佳实践建议
黄金法则:
“宁可标记,也不要随意删除或填充。”
推荐流程
- 合并前:统一字段、数据类型、索引
- 合并后:立即检查缺失分布
- 分析缺失机制:
- MCAR(完全随机缺失)
- MAR(随机缺失)
- MNAR(非随机缺失)
- 选择合适策略:根据业务逻辑决定如何处理
- 记录处理过程:便于复现和审计
七、缺失值处理速查表
检查
df.isnull().sum()
→ 查看各列缺失数df.isnull().mean()
→ 查看缺失比例
删除
df.dropna()
→ 删除含缺失的行df.dropna(subset=['col'])
→ 指定列删除
填充
df['col'].fillna(0)
→ 填固定值df['col'].fillna(df['col'].mean())
→ 填均值df['col'].ffill()
→ 前向填充df['col'].interpolate()
→ 插值
标记
df['col_missing'] = df['col'].isnull().astype(int)