Python数据清洗入门:用 pandas 告别脏数据

引言:为什么数据清洗如此重要?

在数据科学和数据分析的工作流程中,数据清洗是至关重要的一步。真实世界的数据很少是完美无瑕的——它们常常包含缺失值、重复记录、格式不一致、异常值等各种问题。这些“脏数据”会严重影响分析结果的准确性和可靠性。

数据清洗是指识别并纠正(或删除)数据集中的错误、不一致和不准确之处的过程。据统计,数据科学家花费大约60-80%的时间在数据清洗和准备上。没有经过适当清洗的数据就像用脏水煮饭,无论你的烹饪技术多么高超,最终结果都难以令人满意。

通过本教程,你将学习使用Python的pandas库进行数据清洗的核心技能,让你的数据分析工作建立在干净、可靠的数据基础之上。

环境准备:安装必要的工具

在开始之前,我们需要安装两个核心的Python库:pandas和numpy。pandas是Python数据分析的瑞士军刀,而numpy提供了高效的数值计算功能。

打开你的终端或命令提示符,运行以下命令:

pip install pandas numpy

如果你使用的是Jupyter Notebook或Google Colab,这些库通常已经预装好了。现在,让我们导入必要的库开始学习:

# 导入必要的库
import pandas as pd
import numpy as np
from datetime import datetime

print("pandas版本:", pd.__version__)
print("numpy版本:", np.__version__)

核心技能:掌握数据清洗的六大法宝

1. 创建包含各种问题的示例数据集

在学习数据清洗之前,我们先创建一个包含各种常见问题的数据集,这样我们就能在实际操作中学习如何解决它们。

# 创建一个包含各种数据问题的示例数据集
data = {
    '员工ID': [101, 102, 103, 104, 105, 106, 107, 108, 109, 110],
    '姓名': ['张三', '李四', '王五', '赵六', '张三', '孙七', '周八', '吴九', '郑十', '王五'],
    '年龄': [25, 30, np.nan, 35, 25, 28, 32, np.nan, 40, 45],
    '入职日期': ['2020-01-15', '2019-03-20', '2021-05-10', '2018-07-05', 
               '2020-01-15', '2022-02-28', '2017-11-11', '2019-09-09',
               '2023-01-01', '2016-12-25'],
    '薪资': [5000, 6000, 5500, 7000, 5000, 6500, 8000, 7500, 9000, 10000],
    '部门': ['技术部', '市场部', '技术部', '人事部', '技术部', 
            '市场部 ', '财务部', ' 技术部', '市场部', '技术部'],
    '邮箱': ['zhangsan@company.com', 'lisi@company.com', 'wangwu@company.com',
            'zhaoliu@company.com', 'zhangsan@company.com', 'sunqi@company.com',
            'zhouba@company.com', 'wujiu@company.com', 'zhengshi@company.com',
            'WANGWU@COMPANY.COM']
}

# 创建DataFrame
df = pd.DataFrame(data)

print("原始数据集(包含各种问题):")
print("=" * 60)
print(df)
print("\n数据集信息:")
print(df.info())
print("\n数据形状(行数, 列数):", df.shape)

这个数据集包含了我们将要处理的各种问题:

  • 重复记录(张三和王五)
  • 缺失值(年龄列)
  • 格式不一致(部门名称有空格,邮箱大小写不一致)
  • 日期格式需要转换

2. 检测和处理缺失值

缺失值是数据清洗中最常见的问题之一。pandas提供了多种方法来检测和处理缺失值。

# 检测缺失值
print("缺失值检测:")
print("=" * 60)

# 检查每列的缺失值数量
missing_values = df.isnull().sum()
print("各列缺失值数量:")
print(missing_values)

# 计算缺失值比例
missing_percentage = (df.isnull().sum() / len(df)) * 100
print("\n各列缺失值比例:")
print(missing_percentage)

# 可视化缺失值
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
missing_percentage.plot(kind='bar', color='skyblue')
plt.title('各列缺失值比例')
plt.ylabel('缺失值比例 (%)')
plt.xlabel('列名')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

处理缺失值有多种策略,我们需要根据具体情况选择合适的方法:

# 处理缺失值的方法
print("\n处理缺失值:")
print("=" * 60)

# 方法1:删除包含缺失值的行(当缺失值很少时适用)
df_dropna = df.dropna()
print("方法1 - 删除缺失值后的数据形状:", df_dropna.shape)
print("删除了", df.shape[0] - df_dropna.shape[0], "行")

# 方法2:填充缺失值
# 对于数值列,可以用均值、中位数或众数填充
age_mean = df['年龄'].mean()
age_median = df['年龄'].median()

print(f"\n年龄列的均值: {age_mean:.2f}")
print(f"年龄列的中位数: {age_median:.2f}")

# 用中位数填充年龄列的缺失值(中位数对异常值不敏感)
df_filled = df.copy()
df_filled['年龄'] = df_filled['年龄'].fillna(age_median)

print("\n方法2 - 用中位数填充后的年龄列:")
print(df_filled['年龄'])

# 方法3:向前填充或向后填充(适用于时间序列数据)
df_ffill = df.copy()
df_ffill['年龄'] = df_ffill['年龄'].fillna(method='ffill')  # 向前填充

print("\n方法3 - 向前填充后的年龄列:")
print(df_ffill['年龄'])

3. 检测和删除重复值

重复值会扭曲分析结果,特别是在计算统计量时。

# 检测和删除重复值
print("重复值处理:")
print("=" * 60)

# 检测完全重复的行
duplicate_rows = df[df.duplicated()]
print("完全重复的行数:", len(duplicate_rows))
print("完全重复的行:")
print(duplicate_rows)

# 检测基于特定列的重复值
duplicate_names = df[df.duplicated(subset=['姓名'], keep=False)]
print("\n基于姓名的重复记录:")
print(duplicate_names)

# 删除重复值
# 删除完全重复的行
df_no_duplicates = df.drop_duplicates()
print(f"\n删除完全重复行后: {df.shape[0]} -> {df_no_duplicates.shape[0]} 行")

# 删除基于特定列的重复值,保留第一次出现的记录
df_unique_names = df.drop_duplicates(subset=['姓名'], keep='first')
print(f"删除姓名重复后: {df.shape[0]} -> {df_unique_names.shape[0]} 行")

print("\n删除姓名重复后的数据集:")
print(df_unique_names)

4. 字符串格式标准化

不一致的字符串格式是常见的数据质量问题,特别是在文本数据中。

# 字符串格式标准化
print("字符串格式标准化:")
print("=" * 60)

# 创建数据副本以避免修改原始数据
df_clean = df.copy()

# 1. 去除字符串两端的空格
df_clean['部门'] = df_clean['部门'].str.strip()
print("去除部门列空格后的唯一值:")
print(df_clean['部门'].unique())

# 2. 统一大小写(邮箱地址通常应该小写)
df_clean['邮箱'] = df_clean['邮箱'].str.lower()
print("\n邮箱统一为小写后的前5行:")
print(df_clean['邮箱'].head())

# 3. 替换特定字符或字符串
# 假设我们发现所有部门名称中的"部"字应该统一
df_clean['部门'] = df_clean['部门'].str.replace('部', '部门')
print("\n部门名称标准化后的唯一值:")
print(df_clean['部门'].unique())

# 4. 提取字符串中的特定部分
# 从邮箱中提取用户名
df_clean['用户名'] = df_clean['邮箱'].str.split('@').str[0]
print("\n从邮箱提取的用户名:")
print(df_clean[['姓名', '邮箱', '用户名']].head())

5. 日期格式统一

日期和时间数据常常以各种格式出现,统一格式对于时间序列分析至关重要。

# 日期格式处理
print("日期格式处理:")
print("=" * 60)

# 查看当前的日期格式
print("原始日期列的数据类型:", df['入职日期'].dtype)
print("\n原始日期列的前5个值:")
print(df['入职日期'].head())

# 将字符串转换为datetime类型
df['入职日期'] = pd.to_datetime(df['入职日期'])
print("\n转换后的日期列数据类型:", df['入职日期'].dtype)
print("\n转换后的日期列前5个值:")
print(df['入职日期'].head())

# 提取日期的各个部分
df['入职年份'] = df['入职日期'].dt.year
df['入职月份'] = df['入职日期'].dt.month
df['入职日'] = df['入职日期'].dt.day
df['入职季度'] = df['入职日期'].dt.quarter
df['星期几'] = df['入职日期'].dt.day_name()

print("\n提取日期部分后的数据集:")
print(df[['姓名', '入职日期', '入职年份', '入职月份', '入职季度', '星期几']].head())

# 计算工作年限(假设当前日期为2024年1月1日)
current_date = pd.to_datetime('2024-01-01')
df['工作年限'] = (current_date - df['入职日期']).dt.days / 365.25
df['工作年限'] = df['工作年限'].round(1)

print("\n计算工作年限后的数据集:")
print(df[['姓名', '入职日期', '工作年限']].sort_values('工作年限', ascending=False))

6. 数据类型转换

正确的数据类型不仅能节省内存,还能确保计算和操作的准确性。

# 数据类型转换
print("数据类型转换:")
print("=" * 60)

# 查看当前的数据类型
print("原始数据类型:")
print(df.dtypes)

# 创建数据副本
df_types = df.copy()

# 1. 将浮点数转换为整数(年龄列)
# 首先填充缺失值,然后转换类型
df_types['年龄'] = df_types['年龄'].fillna(df_types['年龄'].median()).astype(int)
print("\n年龄列转换为整数类型:")
print(df_types['年龄'].head())

# 2. 将数值列转换为分类数据(部门列)
df_types['部门'] = df_types['部门'].astype('category')
print("\n部门列转换为分类类型:")
print(df_types['部门'].dtype)
print("分类的类别:", df_types['部门'].cat.categories)

# 3. 创建布尔列(是否资深员工:工作年限超过3年)
df_types['是否资深员工'] = df_types['工作年限'] > 3
print("\n创建布尔列 - 是否资深员工:")
print(df_types[['姓名', '工作年限', '是否资深员工']].head())

# 查看转换后的数据类型
print("\n转换后的数据类型:")
print(df_types.dtypes)

# 比较内存使用情况
print("\n内存使用情况比较:")
print(f"原始数据内存使用: {df.memory_usage(deep=True).sum() / 1024:.2f} KB")
print(f"优化后内存使用: {df_types.memory_usage(deep=True).sum() / 1024:.2f} KB")

完整实战案例:整合所有技能的完整流程

现在,让我们将所有学到的技能整合到一个完整的数据清洗流程中。假设我们是一家公司的人力资源分析师,需要清洗员工数据以便进行年度分析。

# 完整实战案例:员工数据清洗流程
print("完整实战案例:员工数据清洗流程")
print("=" * 60)

# 步骤1:加载数据(这里使用我们创建的示例数据)
print("步骤1: 加载原始数据")
print(f"原始数据形状: {df.shape}")
print(f"原始数据列名: {list(df.columns)}")

# 步骤2:初步探索数据
print("\n步骤2: 数据探索")
print("前5行数据:")
print(df.head())
print("\n数据基本信息:")
print(df.info())
print("\n描述性统计:")
print(df.describe())

# 步骤3:处理缺失值
print("\n步骤3: 处理缺失值")
# 检查缺失值
missing_summary = pd.DataFrame({
    '缺失数量': df.isnull().sum(),
    '缺失比例': (df.isnull().sum() / len(df)) * 100
})
print("缺失值统计:")
print(missing_summary)

# 用中位数填充年龄缺失值
df_cleaned = df.copy()
df_cleaned['年龄'] = df_cleaned['年龄'].fillna(df_cleaned['年龄'].median())

# 步骤4:处理重复值
print("\n步骤4: 处理重复值")
# 删除完全重复的行
df_cleaned = df_cleaned.drop_duplicates()
print(f"删除完全重复行后: {df.shape[0]} -> {df_cleaned.shape[0]} 行")

# 删除姓名重复的记录,保留薪资较高的
df_cleaned = df_cleaned.sort_values('薪资', ascending=False).drop_duplicates(subset=['姓名'], keep='first')
print(f"删除姓名重复后: {df.shape[0]} -> {df_cleaned.shape[0]} 行")

# 步骤5:标准化字符串格式
print("\n步骤5: 标准化字符串格式")
# 去除部门列的空格
df_cleaned['部门'] = df_cleaned['部门'].str.strip()
# 统一邮箱为小写
df_cleaned['邮箱'] = df_cleaned['邮箱'].str.lower()

# 步骤6:处理日期格式
print("\n步骤6: 处理日期格式")
# 转换日期列
df_cleaned['入职日期'] = pd.to_datetime(df_cleaned['入职日期'])
# 计算工作年限
current_date = pd.to_datetime('2024-01-01')
df_cleaned['工作年限'] = ((current_date - df_cleaned['入职日期']).dt.days / 365.25).round(1)

# 步骤7:数据类型优化
print("\n步骤7: 数据类型优化")
# 年龄转换为整数
df_cleaned['年龄'] = df_cleaned['年龄'].astype(int)
# 部门转换为分类类型
df_cleaned['部门'] = df_cleaned['部门'].astype('category')

# 步骤8:创建衍生特征
print("\n步骤8: 创建衍生特征")
# 根据工作年限创建员工级别
def assign_level(years):
    if years < 1:
        return '新人'
    elif years < 3:
        return '中级'
    elif years < 5:
        return '高级'
    else:
        return '资深'

df_cleaned['员工级别'] = df_cleaned['工作年限'].apply(assign_level)
df_cleaned['员工级别'] = df_cleaned['员工级别'].astype('category')

# 步骤9:最终数据验证
print("\n步骤9: 最终数据验证")
print("清洗后的数据形状:", df_cleaned.shape)
print("\n清洗后的数据前5行:")
print(df_cleaned.head())
print("\n清洗后的数据信息:")
print(df_cleaned.info())
print("\n各列数据类型:")
print(df_cleaned.dtypes)

# 步骤10:保存清洗后的数据
print("\n步骤10: 保存清洗后的数据")
# 保存为CSV文件
df_cleaned.to_csv('cleaned_employee_data.csv', index=False, encoding='utf-8-sig')
print("数据已保存为 'cleaned_employee_data.csv'")

# 对比清洗前后的数据质量
print("\n" + "=" * 60)
print("数据清洗效果对比")
print("=" * 60)

print(f"原始数据行数: {df.shape[0]}")
print(f"清洗后数据行数: {df_cleaned.shape[0]}")
print(f"删除重复记录: {df.shape[0] - df_cleaned.shape[0]} 行")

print(f"\n原始数据缺失值: {df.isnull().sum().sum()} 个")
print(f"清洗后数据缺失值: {df_cleaned.isnull().sum().sum()} 个")

print(f"\n原始数据内存使用: {df.memory_usage(deep=True).sum() / 1024:.2f} KB")
print(f"清洗后数据内存使用: {df_cleaned.memory_usage(deep=True).sum() / 1024:.2f} KB")

print("\n清洗后的数据摘要:")
print(df_cleaned.describe(include='all'))

总结:关键要点回顾

通过本教程,我们学习了Python数据清洗的核心技能:

  1. 缺失值处理:学会检测缺失值,并根据情况选择删除或填充(均值、中位数、向前/向后填充)策略。

  2. 重复值处理:识别并删除完全重复或基于特定列的重复记录,确保数据的唯一性。

  3. 字符串标准化:使用.str方法进行大小写转换、去除空格、替换字符等操作,确保文本数据的一致性。

  4. 日期格式处理:将字符串转换为datetime类型,提取日期各部分,进行日期计算。

  5. 数据类型转换:优化数据类型以节省内存并确保计算准确性,包括数值转换、分类数据创建等。

  6. 完整工作流程:将各项技能整合到系统的数据清洗流程中,从数据探索到最终保存。

记住,数据清洗不是一次性的任务,而是一个迭代过程。在实际工作中,你可能需要多次重复这些步骤,直到数据达到分析要求。良好的数据清洗习惯不仅能提高分析结果的可靠性,还能节省后续处理的时间。

现在,你已经

更多推荐