Python数据分析(八):Pandas 数据清洗与预处理
目录
一、数据的导入与导出
数据清洗的第一步是 "取数" 与 "存数"。Pandas 提供了强大且一致的 I/O(输入/输出)API,常见的语法模式为:
-
读取数据:pd.read_<format>()
-
写出数据:df.to_<format>()
这一节我们将重点梳理最常见的 CSV、Excel 和 JSON 三种格式的操作
1. CSV 文件的导入与导出
CSV 是数据分析中最通用的文本表格格式,体积小且加载速度快。
(1) 读取 CSV 文件
import pandas as pd
# 1. 基础读取
df_csv = pd.read_csv('sales_data.csv')
# 2. 带有常用参数的高级读取
df_csv = pd.read_csv(
'sales_data.csv',
sep=',', # 分隔符
encoding='utf-8', # 文件编码格式
index_col='User_ID', # 指定哪一列作为 DataFrame 的行索引
usecols=['User_ID', 'Age', 'Amount'], # 仅读取指定的列,减少内存占用
nrows=1000 # 仅读取前 1000 行
)
(2) 保存到 CSV 文件
# 将数据保存至本地 CSV
df_csv.to_csv(
'sales_cleaned.csv',
index=False, # 通常设为 False,避免将自增数字索引保存为额外的一列
encoding='utf-8-sig'
)
2. Excel 文件的导入与导出
Excel(.xlsx / .xls)是办公中最普遍的数据载体。Pandas 依赖底层引擎来高效读写 Excel 文件
(1) 读取 Excel 文件
# 1. 基础读取
df_excel = pd.read_excel('financial_report.xlsx')
# 2. 读取指定 Sheet 与特定范围
df_excel = pd.read_excel(
'financial_report.xlsx',
sheet_name='2023_Q4', # 可传入 Sheet 名称,也可传入索引(如 0 代表第一个 Sheet)
header=0, # 指定第几行为列名(默认为 0)
skiprows=2 # 跳过前 2 行无用的标题或元数据行
)
(2) 保存到 Excel 文件
# 保存到 Excel 文件
df_excel.to_excel(
'financial_report_cleaned.xlsx',
sheet_name='Cleaned_Data',
index=False # 不导出行索引
)
如果需要将多个 DataFrame 分别写入同一个 Excel 文件的不同 Sheet 中,可以使用 pd.ExcelWriter() 上下文管理器:
with pd.ExcelWriter('multi_sheets.xlsx') as writer:
df1.to_excel(writer, sheet_name='Sheet1', index=False)
df2.to_excel(writer, sheet_name='Sheet2', index=False)
3. JSON 文件的导入与导出
JSON 是网络 API 接口交互与半结构化数据存储的通用格式
(1) 读取 JSON 文件
# 读取 JSON 文件或网络接口数据
df_json = pd.read_json('user_profiles.json')
# 处理嵌套或特定结构的 JSON (通过 orient 参数设置解析结构)
df_json = pd.read_json('user_profiles.json', orient='records')
(2) 保存到 JSON 文件
# 将 DataFrame 导出为 JSON 格式
df_json.to_json(
'user_profiles_out.json',
orient='records', # 常用格式:[{col1: val1, col2: val2}, ...]
force_ascii=False, # 确保中文字符不被编码为 \uXXXX 格式
indent=4 # 设置缩进,方便人类阅读
)
关键细节
-
中文乱码问题:
-
在 Windows 环境下读取本地文件时,如果提示 UnicodeDecodeError,可以将 encoding 参数切换为 'gbk' 或 'gb2312'
-
用 to_csv() 保存带有中文的数据并使用 Excel 打开时,强烈建议指定 encoding='utf-8-sig',Excel 才能正确识别 BOM 标记避免乱码
-
-
Unnamed: 0 冗余列问题:
-
如果在导出的文件没有设置 index=False,下次用 read_csv() 重新读入时,Pandas 会把上次保存的自增索引当作普通列读入,并命名为 Unnamed: 0。设置 index=False 即可解决这一常见痛点
-
二、缺失值处理
在真实业务数据集中,由于系统传输故障、用户拒填或传感器异常等原因,数据缺失是不可避免的。在 Pandas 中,缺失值通常用 NaN 或 Python 原生的 None 来表示
在统计计算前,我们必须先识别缺失值,再结合业务逻辑选择删除或填充
1. 缺失值处理方法
下表汇总了 Pandas 中处理缺失值(NaN)的最核心 API:
| 方法 / 操作 | 语法示例 | 描述 |
|---|---|---|
| 检测缺失值 | df.isna () 或 df.isnull () | 返回布尔矩阵,标记元素是否为缺失值(NaN 或 None) |
| 统计缺失值 | df.isna().sum() | 按列统计每列包含的缺失值总数 |
| 删除缺失值 | df.dropna() | 默认删除包含任意缺失值的行(axis=0) |
| 删除缺失值 | df.dropna(axis=1) | 删除包含任意缺失值的列 |
| 删除缺失值 | df.dropna(subset=['col1']) | 仅根据指定列的缺失情况来删除行 |
| 填充缺失值 | df.fillna(value) | 用固定值填充(如 df.fillna (0)) |
| 填充缺失值 | df.fillna(method='ffill') | 用前一个非缺失值填充(向前填充,新版推荐 df.ffill ()) |
| 填充缺失值 | df.fillna(method='bfill') | 用后一个非缺失值填充(向后填充,新版推荐 df.bfill ()) |
| 填充缺失值 | df.fillna(df.mean()) | 使用各数值列的均值(或中位数 / 众数)自动填充 |
2. 检测缺失值 (isna / isnull)
我们构建一份包含不同程度缺失的示例数据,演示检测、删除与填充的全过程:
import pandas as pd
import numpy as np
# 构建包含缺失值的示例 DataFrame
data = {
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'Age': [25, np.nan, 35, np.nan, 28],
'Score': [90, 85, np.nan, 95, 88],
'Dept': ['HR', 'IT', 'IT', None, 'HR']
}
df = pd.DataFrame(data)
-
isna() 与 isnull():两者功能完全相同,均会返回一个与原表同等形状的布尔值矩阵,缺失值为 True,正常值为 False
-
isna().sum():由于布尔值在 Python 中可以当作 1 和 0 计算,结合 .sum() 就能快速得到每列缺失值的数量
# 1. 检测缺失值矩阵
print("--- 缺失值布尔矩阵 ---")
print(df.isna())
# 2. 统计每列缺失值的数量
print("\n--- 每列缺失值统计 ---")
print(df.isna().sum())
输出结果:

3. 删除缺失值 (dropna)
如果缺失数据占比较低且无法准确还原,或者某关键字段缺失会导致后续分析无效,可以直接采用删除策略:
# 1. 默认删除:只要行内有任意一个 NaN,就将该行整行删除
df_drop_default = df.dropna()
print("--- (df.dropna()) ---")
print(df_drop_default)
# 2. 按列删除:只要列内包含 NaN,就将整列删除
df_drop_col = df.dropna(axis=1)
print("\n--- df.dropna(axis=1) ---")
print(df_drop_col)
# 3. 仅根据特定列进行条件删除
# 场景:只有当 'Age' 列缺失时才删除该行
df_drop_subset = df.dropna(subset=['Age'])
print("\n--- df.dropna(subset=['Age']) ---")
print(df_drop_subset)
输出结果:

4. 填充缺失值 (fillna)
删除数据可能会丢失有价值的信息。大多数情况下,我们会根据业务逻辑填充缺失值:
(1) 使用固定值填充
# 1. 全表用固定值填充 (例如用 0)
df_zero = df.fillna(0)
# 2. 针对特定列用不同的固定值填充
df_custom = df.fillna({
'Age': 0,
'Score': 60,
'Dept': 'Unassigned'
})
print(df_custom)
输出结果:

(2) 向前 / 向后填充
-
ffill (forward fill):用缺失位置上一个非空值向上/向前覆盖填充
-
bfill (backward fill):用缺失位置下一个非空值向下/向后覆盖填充
# 用上一个非空值填充 (ffill)
df_ffill = df.ffill()
print("--- 前向填充 (ffill) ---")
print(df_ffill)
输出结果:

(3) 使用统计指标填充(均值、中位数、众数)
针对数值型数据,最常用的填补手段是用该列的均值或中位数:
# 复制副本
df_stat = df.copy()
# 用 Age 列的均值填充 Age 的缺失值
mean_age = df_stat['Age'].mean() # (25 + 35 + 28) / 3 = 29.33
df_stat['Age'] = df_stat['Age'].fillna(mean_age)
# 用 Dept 列的众数填充 Dept 的缺失值
mode_dept = df_stat['Dept'].mode()[0] # 'HR' 和 'IT' 中选出最高频元素
df_stat['Dept'] = df_stat['Dept'].fillna(mode_dept)
print(df_stat)
输出结果:

关键细节
-
isna() 与 isnull() 的关系:
-
在 Pandas 源码中,isnull() 仅仅是 isna() 的别名,二者完全等价。选用 isna() 命名更符合现代数据科学规范
-
-
伪缺失值问题:
-
有时导入的数据包含字符型的 "N/A"、"NULL"、"None" 或空白符 " ",Pandas 默认可能无法将其识别为 NaN。此时需要在读取数据时指定参数,或者先调用 replace 将其标准化为 NaN 后再统一处理
-
-
Pandas 2.0 语法变更:
-
在旧版本中常见的 df.fillna(method='ffill') 语法在最新版本中已被弃用,强烈建议直接改用 df.ffill() 和 df.bfill(),代码更简洁
-
三、重复值处理
在数据采集、多次系统接口对接或表格合并拼接的过程中,数据集中经常会出现重复录入的脏数据。重复数据不仅会虚增样本总量,还会严重偏离均值、总量等统计分析结果
在 Pandas 中,我们主要使用 duplicated() 进行重复识别,使用 drop_duplicates() 进行去重清洗
1. 重复值处理方法汇总
下表汇总了重复值检测与删除的核心 API 及常见组合用法:
| 语法示例 | 描述 |
|---|---|
| df.duplicated() | 返回布尔序列标记重复行(默认将首次出现的行标记为 False,后续重复出现的标记为 True) |
| df.drop_duplicates() | 保留首次出现的行,删除其余重复行(默认检查所有列) |
| df.drop_duplicates (subset=['col1']) |
仅根据指定列进行去重 |
| df.drop_duplicates(keep='last') | 保留最后一次出现的行,删除之前的重复行 |
| df.drop_duplicates(keep=False) | 删除所有存在重复的行(一次都不保留) |
2. 检测重复行 (duplicated)
为了清晰演示全列重复与局部列重复的区别,我们构建以下示例数据集:
import pandas as pd
data = {
'User_ID': [101, 102, 101, 103, 102],
'Name': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob'],
'Score': [90, 85, 90, 95, 88] # 注意:User 102 两次记录的 Score 不同
}
df = pd.DataFrame(data)
df.duplicated() 会返回一个与原 DataFrame 等长的布尔类型 Series:
-
默认机制:对于完全相同的行,第一次出现的记录返回 False(视作非重复),从第二次出现开始返回 True(标记为重复)
-
快速计数:结合 .sum() 可以直接算出整张表中有多少行重复记录
# 1. 检查全列完全重复的行
dup_series = df.duplicated()
print("--- 检测完全重复行 ---")
print(dup_series)
# 2. 统计重复行总数
dup_count = df.duplicated().sum()
print(f"\n全表完全重复的行数: {dup_count}")
# 3. 提取所有重复的记录进行查看
print("\n--- 提取重复的行内容 ---")
print(df[df.duplicated()])
输出结果:

3. 删除重复行 (drop_duplicates)
(1) 默认去重
默认情况下,df.drop_duplicates() 会检查所有列,只有当整行各个字段完全一致时才会删除:
# 默认去重 (保留第 0 行,删除第 2 行)
df_clean = df.drop_duplicates()
print(df_clean)
输出结果:

(2) 指定关键列去重 (subset)
在业务分析中,我们经常需要依据业务主键(如 User_ID)进行去重。即使其他字段存在差异,只要主键相同就视为重复:
# 仅根据 'User_ID' 列进行去重
df_sub_clean = df.drop_duplicates(subset=['User_ID'])
print(df_sub_clean)
输出结果:
说明:
索引 4 的 (102, Bob, 88) 虽然 Score 是 88(与索引 1 的 85 不同),但由于设置了 subset=['User_ID'],Pandas 识别到 User_ID=102 已经出现过,因此索引 4 被顺利去除
(3) 保留策略控制 (keep)
通过设置 keep 参数,我们可以精准控制去重时到底留下哪一条:
-
keep='first' (默认):保留首次出现的记录
-
keep='last':保留最后一次出现的记录
-
keep=False:只要有重复,一条都不留
# 1. 保留最后一次出现的记录
df_keep_last = df.drop_duplicates(subset=['User_ID'], keep='last')
# 2. 剔除所有包含重复的项
df_keep_none = df.drop_duplicates(subset=['User_ID'], keep=False)
print("--- keep='last' ---")
print(df_keep_last)
print("\n--- keep=False ---")
print(df_keep_none)
输出结果:

关键细节
-
keep=False 排查重复数据: 如果想把所有涉及重复的记录全挑出来对比分析,可以组合使用 df[df.duplicated(subset=['ID'], keep=False)],这样所有重复的行(包括第 1 次出现的原行)都会被完整筛选出来,方便人工比对审查
-
去重前排序: 如果你想按某个时间戳字段保留最新的一条数据,最稳妥的写法是:
# 先按时间升序排序,再保留最后一条 (或降序排序后保留第一条) df_latest = (df.sort_values('Update_Time'). drop_duplicates(subset=['User_ID'], keep='last')) -
字符串空格影响: 有时候看似一模一样的文本(如 ' Alice' 与 'Alice'),因为带有隐蔽的前后空格,duplicated() 会将其认定为不重复。在去重前对字符串列先执行 .str.strip() 往往能避免这种问题
四、数据类型
在将外部数据导入 Pandas 时,Pandas 会尝试自动推断每列的数据类型。然而,由于源文件中可能存在脏数据、空值或格式不统一,列类型经常会出现错配——例如:原本是数字的年龄被读成了字符串,原本是日期的字段被当作普通文本存储
如果数据类型不正确,不仅无法调用对应的特有方法,还会极大地浪费内存
1. 数据类型处理汇总
下表汇总了 Pandas 中查看、转换与格式化数据类型的最常用 API:
| 方法 / 操作 | 语法 | 描述 |
|---|---|---|
| 查看数据类型 | df.dtypes | 显示每列的数据类型 |
| 强制类型转换 | df['col'].astype('int') | 将列转换为指定类型 |
| 转换为日期时间 | pd.to_datetime(df['col']) | 将字符串或数值列转为 datetime 类型 |
| 转换为分类数据 | df['col'].astype('category') | 将列转为分类类型 |
| 数值格式化 | df['col'].round(2) | 保留指定小数位数(如保留 2 位小数) |
2. 基础数据准备
我们构建一份包含多种类型错配问题的数据集:
import pandas as pd
import numpy as np
# 构建存在类型错配的示例 DataFrame
data = {
'User_ID': ['1001', '1002', '1003', '1004'], # 应该是整数,但被读作了字符串
'Age': ['25', '30', '35', '40'], # 应该是数值,但被读作了字符串
'Salary': [8500.5678, 12000.1234, 9800.0, 15000.8888],# 小数位数过长,需格式化
'Gender': ['Male', 'Female', 'Male', 'Female'], # 取值有限,适合转为分类类型
'Join_Date': ['2023-01-15', '2023/02/20', '20230305', '2023-04-10'] # 格式不统一的日期文本
}
df = pd.DataFrame(data)
print("--- 原始数据类型 ---")
print(df.dtypes)
输出结果:

3. 强制类型转换 (astype)
使用 astype() 可以将列转换为指定的数据类型,如 int64, float64, str 等。也可以一次性通过字典批量转换多列:
# 1. 单列转换:将 Age 列从 object 转为 int
df['Age'] = df['Age'].astype(int)
# 2. 字典批量转换:将 User_ID 转为 int,Age 转为 float
df = df.astype({
'User_ID': int,
'Age': float
})
print("--- astype 转换后的类型 ---")
print(df.dtypes[['User_ID', 'Age']])
输出结果:
4. 转换为日期时间
普通文本类型的日期(如 '2023-01-15')无法进行时间加减、按季度统计等操作。通过 pd.to_datetime() 可以识别各种格式的日期并统一转为 Pandas 标准的 datetime64 类型:
# 将格式各异的日期文本统一转换为 datetime 类型
df['Join_Date'] = pd.to_datetime(df['Join_Date'], format='mixed')
print("--- 转换后的 Join_Date 列 ---")
print(df['Join_Date'])
print("\n类型:", df['Join_Date'].dtype)
# 转换后即可轻松提取年份、月份、星期等时间属性
df['Join_Year'] = df['Join_Date'].dt.year
df['Join_Month'] = df['Join_Date'].dt.month
输出结果:

5. 转换为分类数据
对于重复出现且唯一取值较少的字符串列(如:性别、省份、学历、订单状态等),将其转换为 category(分类) 类型不仅能大幅节省内存开销,还能加快后续分组汇总的运算性能:
# 转换前查看内存占用
mem_before = df['Gender'].memory_usage(deep=True)
# 将 Gender 转换为 category 分类类型
df['Gender'] = df['Gender'].astype('category')
# 转换后查看内存占用
mem_after = df['Gender'].memory_usage(deep=True)
print(f"转换前内存占用: {mem_before} bytes")
print(f"转换后内存占用: {mem_after} bytes")
print("Gender 列类型:", df['Gender'].dtype)
输出结果:

原理:转换为 category 后,Pandas 底层只会将独特的分类文本存储一次,列内部使用高效的整数编码代替重复的长字符串,在百万级数据量下效果极其显著
6. 数值格式化
对于浮点数列,如果小数位数过多,可以使用 .round(decimals) 精确保留指定的小数位数:
# 将 Salary 保留 2 位小数
df['Salary'] = df['Salary'].round(2)
print("--- Salary 格式化保留 2 位小数 ---")
print(df[['User_ID', 'Salary']])
输出结果:

关键细节
1. 含有 NaN 缺失值的列直接转 int 会报错:
-
在标准 NumPy/Pandas 中,原生 int 类型不支持存储 NaN。如果一列包含缺失值,执行 df['col'].astype(int) 会直接抛出 ValueError
-
解决方案 A:改用 Pandas 提供的可空整数类型 Int64(首字母大写):df['col'].astype('Int64')
-
解决方案 B:先填充缺失值 df['col'].fillna(0) 后再转 astype(int)
2. 包含非非法字符时使用 pd.to_numeric:
-
如果数值列里混入了字母或非法字符(如 '100', '200', 'Unknown'),直接用 astype(float) 会报错。此时可以使用安全转换 API:pd.to_numeric(df['col'], errors='coerce'),非法字符会被自动替换为 NaN 而不会中断程序
总结
本章学习了 Pandas 中常见的数据清洗与预处理方法,包括数据的导入与导出、缺失值处理、重复数据处理以及数据类型转换等内容,并结合实际案例掌握了常用清洗函数的使用方法。数据清洗是数据分析过程中不可或缺的一步,良好的数据质量能够为后续分析提供可靠保障
下一篇将继续学习 Pandas 数据变形与时间数据处理,包括数据重塑、文本处理、日期时间处理以及时间序列等内容,进一步提升数据整理与转换能力

更多推荐


所有评论(0)