Skip to content

第4章 数据清洗与预处理

4.1 处理缺失值:isna、dropna、fillna

在数据分析的江湖里,数据缺失就像武侠小说里的"断臂"情节——看似残缺,实则暗藏玄机。但咱们可不能像杨过那样潇洒,得老老实实地把这些"断臂"接上或者干脆砍掉。

pandas提供了三把利器来对付缺失值:isna()用来侦查,dropna()用来斩草除根,fillna()用来妙手回春。

缺失值处理方法表

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
检测缺失值df.isna()返回布尔型DataFrame,True表示缺失值。也可用isnull(),两者完全等价
删除含缺失值的行df.dropna()默认删除任何包含NaN的行。axis=0(行)/1(列),how='any'(任一缺失)/'all'(全部缺失)
填充缺失值df.fillna(value)value可以是标量、字典、Series或DataFrame。method参数支持'ffill'(前向填充)/'bfill'(后向填充)

来看个实战例子:

python
import pandas as pd
import numpy as np

# 创建一个包含缺失值的DataFrame
data = {
    '姓名': ['张三', '李四', '王五', '赵六'],
    '年龄': [25, np.nan, 30, 35],
    '城市': ['北京', '上海', np.nan, '深圳'],
    '薪资': [8000, 9000, np.nan, 12000]
}
df = pd.DataFrame(data)

# 检测缺失值 - 返回每个位置是否为缺失值
print("缺失值检测结果:")
print(df.isna())

# 删除包含缺失值的行 - 默认删除任何有缺失值的行
df_dropped = df.dropna()
print("\n删除缺失值后的数据:")
print(df_dropped)

# 用不同策略填充缺失值
# 策略1: 用固定值填充所有缺失值
df_filled1 = df.fillna('未知')
print("\n用'未知'填充后的数据:")
print(df_filled1)

# 策略2: 用不同列的不同值填充
fill_values = {'年龄': df['年龄'].mean(), '城市': '未知', '薪资': df['薪资'].median()}
df_filled2 = df.fillna(fill_values)
print("\n按列分别填充后的数据:")
print(df_filled2)

# 策略3: 前向填充(用前面的值填充后面的缺失值)
df_filled3 = df.fillna(method='ffill')
print("\n前向填充后的数据:")
print(df_filled3)

注意事项:

  • dropna()默认会创建新DataFrame,如果想在原地修改,需要设置inplace=True
  • fillna()中的method参数在pandas 2.1.0版本后已被弃用,建议使用df.ffill()df.bfill()替代
  • 数值列填充均值、中位数,分类列填充众数或"未知"是比较常见的做法
  • 处理缺失值前最好先分析缺失的原因,有时候缺失本身就是一种信息

这节我们学会了如何识别、删除和填充缺失值,这是数据清洗中最基础也是最重要的技能之一。掌握了这些方法,就能让我们的数据变得更加完整可靠。

4.2 删除重复数据与异常值识别

数据世界里不仅有缺失值这个"幽灵",还有重复数据这个"双胞胎"和异常值这个"怪胎"。今天咱们就来收拾这两个捣蛋鬼。

重复数据与异常值处理方法表

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
检测重复行df.duplicated()返回布尔Series,标记重复行。keep参数控制保留哪个重复项('first','last',False)
删除重复行df.drop_duplicates()基于全部列或指定列删除重复行。inplace参数控制是否原地修改
基于IQR识别异常值手动计算IQR = Q3 - Q1,异常值范围:[Q1 - 1.5IQR, Q3 + 1.5IQR]
基于Z-score识别异常值手动计算Z-score = (x - mean) / std,通常

先看重复数据的处理:

python
import pandas as pd

# 创建包含重复数据的DataFrame
data = {
    'ID': [1, 2, 3, 2, 4, 1],
    '姓名': ['张三', '李四', '王五', '李四', '赵六', '张三'],
    '年龄': [25, 30, 35, 30, 40, 25],
    '城市': ['北京', '上海', '广州', '上海', '深圳', '北京']
}
df = pd.DataFrame(data)

# 检测重复行 - 默认基于所有列判断
duplicates = df.duplicated()
print("重复行检测结果:")
print(duplicates)

# 查看具体的重复行
print("\n重复的数据行:")
print(df[duplicates])

# 删除重复行 - 保留第一次出现的
df_cleaned = df.drop_duplicates()
print("\n删除重复行后的数据:")
print(df_cleaned)

# 如果只想基于特定列判断重复(比如只看ID和姓名)
df_cleaned_subset = df.drop_duplicates(subset=['ID', '姓名'])
print("\n基于ID和姓名去重后的数据:")
print(df_cleaned_subset)

再来看异常值识别:

python
import pandas as pd
import numpy as np

# 创建包含异常值的数据
np.random.seed(42)  # 设置随机种子保证结果可重现
ages = np.random.normal(30, 5, 100).tolist()  # 正常年龄分布
ages.extend([120, -5, 150])  # 添加明显的异常值
salaries = np.random.normal(8000, 2000, 100).tolist()
salaries.extend([50000, -1000, 80000])  # 添加薪资异常值

df = pd.DataFrame({
    '年龄': ages,
    '薪资': salaries
})

# 方法1: 使用IQR方法识别异常值
def detect_outliers_iqr(data):
    """使用IQR方法检测异常值"""
    Q1 = data.quantile(0.25)
    Q3 = data.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return (data < lower_bound) | (data > upper_bound)

# 检测年龄异常值
age_outliers = detect_outliers_iqr(df['年龄'])
print(f"年龄异常值数量: {age_outliers.sum()}")
print("年龄异常值:")
print(df[age_outliers]['年龄'])

# 方法2: 使用Z-score方法识别异常值
def detect_outliers_zscore(data, threshold=3):
    """使用Z-score方法检测异常值"""
    z_scores = np.abs((data - data.mean()) / data.std())
    return z_scores > threshold

# 检测薪资异常值
salary_outliers = detect_outliers_zscore(df['薪资'])
print(f"\n薪资异常值数量: {salary_outliers.sum()}")
print("薪资异常值:")
print(df[salary_outliers]['薪资'])

# 删除异常值(以年龄为例)
df_no_outliers = df[~age_outliers]
print(f"\n删除年龄异常值后,数据量从{len(df)}变为{len(df_no_outliers)}")

注意事项:

  • duplicated()drop_duplicates()默认考虑所有列,可以通过subset参数指定特定列
  • 异常值不一定是错误数据,有时候异常值本身就包含重要信息(比如欺诈检测)
  • IQR方法对非正态分布数据更稳健,Z-score方法适合正态分布数据
  • 处理异常值时要谨慎,最好先可视化数据分布再决定处理策略

这节我们学会了如何处理重复数据和识别异常值。重复数据会扭曲统计结果,异常值会影响模型性能,掌握这些清理技巧能让我们的分析更加准确可靠。

4.3 数据类型转换:astype 与 to_numeric

数据类型就像人的身份证,搞错了就会闹出大笑话。比如把字符串"123"当成数字123来计算,或者把日期当成普通文本处理。今天咱们就来学习如何给数据"验明正身"。

数据类型转换方法表

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
通用类型转换df.astype(dtype)dtype可以是numpy数据类型、pandas数据类型或Python内置类型
智能数值转换pd.to_numeric(series)errors参数控制错误处理('raise','coerce','ignore'),downcast参数优化存储
日期时间转换pd.to_datetime(series)format参数指定日期格式,errors参数处理错误
分类数据转换df.astype('category')适用于有限个唯一值的字符串列,节省内存

先看基本的类型转换:

python
import pandas as pd
import numpy as np

# 创建混合类型的数据
data = {
    'ID': ['1', '2', '3', '4'],  # 字符串形式的数字
    '姓名': ['张三', '李四', '王五', '赵六'],
    '年龄': ['25', '30', '35', '40'],  # 字符串形式的年龄
    '薪资': ['8000.5', '9000.0', '10000.75', '12000.25'],  # 字符串形式的薪资
    '入职日期': ['2020-01-15', '2019-03-20', '2021-07-10', '2018-11-05']
}
df = pd.DataFrame(data)

# 查看原始数据类型
print("原始数据类型:")
print(df.dtypes)
print("\n原始数据:")
print(df)

# 方法1: 使用astype进行基本类型转换
# 将ID转换为整数
df['ID'] = df['ID'].astype(int)
# 将年龄转换为整数
df['年龄'] = df['年龄'].astype(int)
# 将薪资转换为浮点数
df['薪资'] = df['薪资'].astype(float)

print("\n使用astype转换后的数据类型:")
print(df.dtypes)
print("\n转换后的数据:")
print(df)

再看更智能的to_numeric

python
import pandas as pd

# 创建包含非数字字符的数据
data = {
    '产品ID': ['P001', 'P002', 'P003', 'P004'],
    '销量': ['100', '200', 'abc', '400'],  # 包含非数字字符
    '价格': ['19.99', '29.99', '', '39.99']  # 包含空字符串
}
df = pd.DataFrame(data)

print("原始数据:")
print(df)
print("\n原始数据类型:")
print(df.dtypes)

# 使用to_numeric处理销量列
# errors='coerce'会将无法转换的值设为NaN
df['销量'] = pd.to_numeric(df['销量'], errors='coerce')
print("\n处理销量列后的数据:")
print(df['销量'])

# 使用to_numeric处理价格列
df['价格'] = pd.to_numeric(df['价格'], errors='coerce')
print("\n处理价格列后的数据:")
print(df['价格'])

# 查看最终结果
print("\n最终数据:")
print(df)
print("\n最终数据类型:")
print(df.dtypes)

# 如果想要优化存储空间,可以使用downcast参数
large_numbers = pd.Series([1000000, 2000000, 3000000])
print(f"\n原始大数Series内存使用: {large_numbers.memory_usage()} bytes")
optimized_numbers = pd.to_numeric(large_numbers, downcast='integer')
print(f"优化后内存使用: {optimized_numbers.memory_usage()} bytes")

注意事项:

  • astype()在遇到无法转换的数据时会直接报错,而to_numeric()可以通过errors参数灵活处理
  • errors='coerce'会将无法转换的值转为NaN,errors='ignore'会保持原值不变
  • 对于内存敏感的应用,downcast参数可以帮助减少内存占用
  • 转换类型前最好先检查数据质量,避免意外的转换结果

这节我们学会了如何正确转换数据类型。正确的数据类型不仅能节省内存,还能确保后续的计算和分析得到正确的结果。记住,给数据一个正确的"身份",是数据分析的重要第一步。

4.4 字符串操作与正则表达式清洗

数据清洗中最头疼的往往是那些"脏兮兮"的字符串数据——大小写混乱、多余空格、奇怪符号,简直就像没洗过的臭袜子。不过别担心,pandas的字符串处理功能就是我们的"洗衣粉",正则表达式就是我们的"消毒液"。

字符串操作方法表

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
基础字符串操作df['col'].str.method()method包括lower()/upper()/strip()/replace()等
字符串分割df['col'].str.split(pat)pat为分隔符,expand=True可返回DataFrame
字符串提取df['col'].str.extract(pattern)pattern为正则表达式,必须包含捕获组()
字符串匹配df['col'].str.contains(pattern)返回布尔Series,regex参数控制是否使用正则

先看基础的字符串清洗:

python
import pandas as pd

# 创建包含"脏"字符串的数据
data = {
    '姓名': [' 张三 ', 'LISI', ' wangwu ', 'Zhao Liu'],
    '邮箱': ['zhangsan@GMAIL.COM', ' lisi@qq.com ', 'WANGWU@163.COM', 'invalid-email'],
    '电话': [' 138-1234-5678 ', '(010) 87654321', '13912345678', 'not-a-phone']
}
df = pd.DataFrame(data)

print("原始数据:")
print(df)

# 清洗姓名列:去除空格并统一为小写
df['姓名_clean'] = df['姓名'].str.strip().str.lower()
print("\n清洗后的姓名:")
print(df['姓名_clean'])

# 清洗邮箱列:统一转为小写
df['邮箱_clean'] = df['邮箱'].str.strip().str.lower()
print("\n清洗后的邮箱:")
print(df['邮箱_clean'])

# 清洗电话列:只保留数字
df['电话_clean'] = df['电话'].str.replace(r'\D', '', regex=True)  # \D匹配非数字字符
print("\n清洗后的电话(仅数字):")
print(df['电话_clean'])

再看正则表达式的强大功能:

python
import pandas as pd

# 创建需要复杂清洗的数据
data = {
    '产品描述': [
        'iPhone 13 Pro Max 256GB 黑色',
        'Samsung Galaxy S21 Ultra 512GB 白色',
        'Huawei P40 Pro+ 256GB 银色',
        'Xiaomi Mi 11 Ultra 256GB 黑色'
    ],
    '价格字符串': [
        '价格:¥8999元',
        '售价$1299美元',
        '¥6488人民币',
        'Price: €999 EUR'
    ]
}
df = pd.DataFrame(data)

print("原始数据:")
print(df)

# 使用正则表达式提取手机品牌
# 假设品牌名都是英文单词开头
df['品牌'] = df['产品描述'].str.extract(r'^([A-Za-z]+)')
print("\n提取的品牌:")
print(df['品牌'])

# 提取存储容量
df['存储容量'] = df['产品描述'].str.extract(r'(\d+GB)')
print("\n提取的存储容量:")
print(df['存储容量'])

# 从价格字符串中提取纯数字价格
# 使用正则表达式匹配数字(包括小数)
df['价格数字'] = df['价格字符串'].str.extract(r'([\d.]+)').astype(float)
print("\n提取的价格数字:")
print(df['价格数字'])

# 验证邮箱格式(简单示例)
emails = pd.Series(['valid@email.com', 'invalid-email', 'another@domain.org', 'bad@'])
# 简单的邮箱正则:包含@符号,且前后都有字符
valid_emails = emails.str.contains(r'.+@.+', regex=True)
print("\n邮箱验证结果:")
for email, valid in zip(emails, valid_emails):
    print(f"{email}: {'有效' if valid else '无效'}")

注意事项:

  • 所有字符串操作都要通过.str访问器进行,直接对Series调用字符串方法会出错
  • 正则表达式中的捕获组()决定了extract()返回的内容
  • contains()方法默认使用正则表达式,如果要匹配字面量字符串,需要设置regex=False
  • 复杂的字符串清洗可能需要组合多个操作,建议分步骤进行并验证每步结果

这节我们学会了如何使用pandas的字符串操作和正则表达式来清洗文本数据。字符串清洗虽然繁琐,但却是数据预处理中不可或缺的一环。掌握了这些技巧,你就能把那些"脏乱差"的数据变成整洁可用的分析素材了。