Pandas 数据组合与缺失值处理最新版本
Pandas 数据组合与缺失值处理实战(含完整示例与注释)
本文基于代码与笔记,总结 Pandas 中数据组合(concat/merge/join)与缺失值处理(判断、加载、可视化、填充、插值)的系统用法。文末附一个从 SQLite 多表关联到聚合分析的完整案例,可直接参考在实际业务中落地。
目录
- 数据组合
- concat:按行/列拼接、重置索引、与 Series 拼接、追加行列
- merge:一对一/多对一、连接方式、聚合与时间单位转换
- join:索引对齐、设置索引、与 concat/merge 的对照
- 缺失值处理
- 缺失值的概念与判断 API 对比
- 加载缺失值:keep_default_na 与 na_values
- 缺失值可视化(missingno)
- 非时序与时序填充:fillna/ffill/bfill/interpolate
- 实战案例:多表关联统计平均消费
- 知识点速查表与最佳实践
一、数据组合
1.1 concat:按行/按列拼接 DataFrame
- 要点
- 行拼接参考列名;列拼接参考行索引
ignore_index=True
可重置拼接维度上的索引/列名- 和 Series 拼接时,Series 没有行索引会新增列,空位填 NaN
# 2:加载数据(读取文件数据)
df1 = pd.read_csv('data/concat_1.csv')
df2 = pd.read_csv('data/concat_2.csv')
df3 = pd.read_csv('data/concat_3.csv')print("----------DataFrame与DataFrame拼接-----------------------")
# 3:concat()函数,将df对象链接组合起来,默认是:按照行拼接
# 3-1:默认按照行拼接
r1 = pd.concat([df1, df2, df3]) # 默认按照行拼接
r2 = pd.concat([df1, df2, df3], axis="rows")
r3 = pd.concat([df1, df2, df3], axis=0)
print(r3)print("---------------------------------")
# 3-2:按照列拼接
r4 = pd.concat([df1, df2, df3], axis="columns")
r5 = pd.concat([df1, df2, df3], axis=1)
print(r5)
- Series 拼接与重置索引
print("----------DataFrame与Series拼接(作为列添加)-----------------------")
# Series 是列数据;concat 默认按行拼,Series 无行索引 → 会新增一列,缺失值用 NaN 填充
s1 = pd.Series(['n1', 'n2', 'n3'])
r6 = pd.concat([df1, s1])
print(r6)print("----------DataFrame与DataFrame拼接(将['n1','n2','n3','n4']作为行连接到df1后,可以创建DataFrame并指定列名)-------")
df5 = pd.DataFrame([['n1', 'n2', 'n3', 'n4']], columns=['A', 'B', 'C', 'D'])
print(df5)print("---------------------------------")
r7 = pd.concat([df1, df5], ignore_index=True) # 忽略索引,重新编号
print(r7)
- 追加行/列(append 已弃用、直接赋值/concat 更推荐)
print("----------使用append函数,追加DataFrame(一次只能追加一个df对象)-----------------------------------------")
dict1 = {'A': 'n1', 'B': 'n2', 'C': 'n3', 'D': 'n4'}
# 注意版本问题,使用 _append 或 append(新版本建议用 concat/assign 等替代)
r8 = df1._append(dict1, ignore_index=True)
print(r8)print("----------增加新列----------------------------------")
# 赋值追加新列(也可用 concat 追加新列)
df1['E'] = ['n1', 'n2', 'n3', 'n4']
print(df1)# Series 作为新列追加到 df
df1['F'] = pd.Series(['n1', 'n2', 'n3', 'n4'])
print(df1)
1.2 merge:像 SQL 一样关联表并聚合分析
- 要点
- 支持
inner/left/right/outer
四种连接 - 连接键相同用
on
,不同用left_on/right_on
- 多对一后
groupby().agg()
很顺手 - 时间单位可用
pd.to_timedelta(..., unit="ms").dt.floor("s")
处理到秒
- 支持
print("---------------数据组合-merge函数----------------------------------")
# 准备数据
conn = sqlite3.connect('data/chinook.db')
tracks_df = pd.read_sql_query("select * from tracks", conn)
genres_df = pd.read_sql_query("select * from genres", conn)# 为了演示,构造 tracks 子集
tracks_subset = tracks_df.loc[[0,62,76,98,110,193,204,281,322,359],]# 各种连接方式
df1 = genres_df.merge(tracks_subset[["TrackId", "GenreId", "Milliseconds"]], on="GenreId", how="inner")
df2 = genres_df.merge(tracks_subset[["TrackId", "GenreId", "Milliseconds"]], on="GenreId", how="left")
df3 = genres_df.merge(tracks_subset[["TrackId", "GenreId", "Milliseconds"]], on="GenreId", how="right")
df4 = genres_df.merge(tracks_subset[["TrackId", "GenreId", "Milliseconds"]], on="GenreId", how="outer")
- 多对一 + 分组统计 + 时间单位转换
# 一对多:一个 GenreId 对应多首歌
genres_track_df = genres_df.merge(tracks_df[["TrackId", "GenreId", "Milliseconds"]], on="GenreId", how="left")
# 分组统计每个类型的平均时长(毫秒)
avg_series = genres_track_df.groupby(["GenreId", "Name"])["Milliseconds"].mean()# 将毫秒转为时间差类型,截断到秒,并降序排序
rs1 = pd.to_timedelta(avg_series, unit="ms").dt.floor("s").sort_values(ascending=False)
print(rs1.head())
1.3 join:按索引对齐的“表连接”
- 要点
- 默认按行索引对齐,支持设置
on
指定列 - 可配合
set_index
实现与merge
类似的结果 - 同名列需用
lsuffix/rsuffix
区分
- 默认按行索引对齐,支持设置
# 读取三年股票数据
stocks_2016 = pd.read_csv('data/stocks_2016.csv')
stocks_2017 = pd.read_csv('data/stocks_2017.csv')
stocks_2018 = pd.read_csv('data/stocks_2018.csv')# 场景1:按行索引 join(默认左外连接)
df1 = stocks_2016.join(stocks_2017, lsuffix="_2016", rsuffix="_2017")
df2 = stocks_2016.join(stocks_2017, lsuffix="_2016", rsuffix="_2017", how="outer")# 场景2:两侧都设置 Symbol 为行索引
df3 = stocks_2016.set_index("Symbol").join(stocks_2017.set_index("Symbol"), lsuffix="_2016", rsuffix="_2017")# 场景3:一侧设置索引,另一侧用 on 匹配
df4 = stocks_2016.join(stocks_2017.set_index("Symbol"), on="Symbol", lsuffix="_2016", rsuffix="_2017")# 对照:concat / merge 也能达到类似效果
df5 = pd.concat([stocks_2016, stocks_2017], axis="columns")
df6 = stocks_2016.merge(stocks_2017, on="Symbol")
二、缺失值处理
2.1 缺失值的概念与判断
- 核心事实
- 在 Pandas/NumPy 中,缺失值有
NaN/NAN/nan
多种写法,本质相同 - 缺失值“不等于任何东西”,包括
''/0/True/False
等 - 判断建议用
pd.isnull/isna
和pd.notnull/notna
(更通用),np.isnan
仅适用于数值型且不能处理None
- 在 Pandas/NumPy 中,缺失值有
"""
pandas中,缺失值使用NaN,或者nan,NAN,这三个表示,都是一样的...
"""
import numpy as np
import pandas as pdprint(np.NaN == True) # False
print(np.NaN == False) # False
print(np.NaN == 0) # False
print(np.NaN == '') # False# 不能用 == 判断 NaN
print(np.NaN == np.NAN) # False
print(np.NaN == np.nan) # False# 推荐使用 isnull/isna/notnull/notna
print(pd.isnull(np.NAN)) # True
print(pd.isna(np.NaN)) # True
print(pd.notnull(np.NaN))# False
2.2 加载缺失值:控制 keep_default_na 与 na_values
- 要点
keep_default_na=False
关闭内置缺失值识别(小心真的“空”就进来了)na_values=[...]
明确哪些值按缺失处理(如空串)
# 加载数据
pd1 = pd.read_csv('data/survey_visited.csv')# 关闭默认缺失值识别
pd2 = pd.read_csv('data/survey_visited.csv', keep_default_na=False)# 自定义哪些值视为缺失(例如空字符串)
pd3 = pd.read_csv('data/survey_visited.csv', na_values=[""], keep_default_na=False)print(pd1.head(10))
print(pd2.head(10))
print(pd3.head(10))
2.3 缺失值可视化(missingno)
- 建议用
missingno
快速扫描数据的缺失分布与相关性 - 安装:
pip install missingno
- 示例:
import pandas as pd
import missingno as msnotrain = pd.read_csv('data/titanic_train.csv')
# 每列非空数量分布
msno.bar(train)
# 缺失值之间的相关性热力图
msno.heatmap(train)
2.4 缺失值处理:删除/常量填充/时间序列插补
- 非时间序列:
dropna
谨慎用(数据损失);常量或统计量填充优先 - 时间序列:
ffill/bfill
或interpolate
(线性插值,支持方向)
print("---缺失值处理方式2-非时间序列数据---------------------")
train = pd.read_csv('data/titanic_train.csv')
print(train.isnull().sum())# 示例:用 0 填充 Age 缺失
train['Age'].fillna(0, inplace=True)
print(train.isnull().sum())
print("---缺失值处理方式3-时间序列数据---------------------")
city_day = pd.read_csv('data/city_day.csv', parse_dates=['Date'], index_col='Date')
city_day1 = city_day.copy()
print(city_day1['Xylene'][50:68])# bfill:用下一个非缺失值填充
city_day1['Xylene'].fillna(method='bfill', inplace=True)
print(city_day1['Xylene'][50:68])print("---缺失值处理方式4:线性插值方法---------------------------------")
# both/forward/backward 可控插值方向
city_day1.interpolate(limit_direction="both", inplace=True)
print(city_day1['Xylene'][50:68])
三、实战案例:多表关联统计每名用户的平均消费
- 数据源:
chinook.db
(SQLite) - 表关系:
customers → invoices → invoice_items
- 思路:三表
merge
→ 计算每笔TotalPrice
→ groupby 聚合
# 步骤1:从三张表中获取数据
user_df = pd.read_sql_query("select CustomerId,FirstName,LastName from customers", conn)
invoice_df = pd.read_sql_query("select InvoiceId,CustomerId from invoices", conn)
invoice_items_df = pd.read_sql_query("select InvoiceId,UnitPrice,Quantity from invoice_items", conn)# 步骤2: 关联三张表
user_invoice_df = user_df.merge(invoice_df, on="CustomerId").merge(invoice_items_df, on="InvoiceId")# 步骤3:计算每笔消费金额
user_invoice_df["TotalPrice"] = user_invoice_df["UnitPrice"] * user_invoice_df["Quantity"]# 步骤4:统计每名用户的总消费与平均消费,并按平均值降序
print(user_invoice_df.groupby(["CustomerId", "FirstName", "LastName"])["TotalPrice"].agg(["sum", "mean"]).sort_values(by="mean", ascending=False).head())
四、知识点速查表(节选)
数据的合并pd.concat()轴向拼接:axis=0 行(默认)/1 列;join='inner'/'outer';ignore_index 控制索引重置pd.merge()on/left_on/right_on 设定键;how='inner'/'left'/'right'/'outer';suffixes 冲突后缀pd.join()索引对齐的合并;可 on 指定列;默认 left outer缺失值的处理判断:np.isnan(数值型)、pd.isnull/isna、pd.notnull/notna(推荐)删除:dropna(axis, how='any'/'all', subset, inplace)填充:fillna(value/method='ffill'/'bfill', axis, inplace)插值:interpolate(method='linear', limit_direction='forward/backward/both' ...)
五、常见坑与最佳实践
- 数据组合
- 优先用
concat/merge/join
,弃用老的append
- 行拼接看“列名”、列拼接看“行索引”;跨源拼接注意索引一致性
- 多表
merge
后合理选择连接方式,明确 on/left_on/right_on
- 优先用
- 缺失值
- 不要用
==
判断 NaN;使用pd.isnull/isna
- 加载 CSV 时明确
na_values
与keep_default_na
- 非时序用统计量或默认值填充;时序优先
ffill/bfill
或interpolate
- 删除缺失值会丢信息,仅在缺失占比极低时考虑
- 不要用
六、可直接使用的环境依赖建议
- pandas
- numpy
- matplotlib
- missingno
- sqlite3(标准库)
最后更新日期:2025年8月21日,后续更新会及时同步。