数据清洗实用指南:将混乱数据转化为价值资产的10大方法

数据清洗往往不是最令人愉快的任务!然而,如果你问任何一位资深数据专业人士他们最有价值的技能是什么,许多人会告诉你:是他们将杂乱无章的数据整理成有用信息的能力。

本文涵盖了所有成功数据项目中必不可少的清洗技术。我还提供了实用的代码示例和一个样本数据集,方便你跟着操作,将这些方法应用到自己的工作中。

让我们开始吧!

10 Essential Data Cleaning Techniques Explained in 12 Minutes

注意:你可以使用这份样本混乱数据集跟随本文进行操作。


1. 处理缺失值

缺失数据或许是现实世界数据中最常见的问题。它可能导致分析结果偏差,使结论不可靠。因此,识别和处理缺失值是数据清洗的第一步。

识别缺失值

首先,需要量化并了解缺失数据的分布情况:

import pandas as pd
import numpy as np

# 载入样本数据集
df = pd.read_csv('messy_data.csv')

# 统计每列缺失值的数量和百分比
missing_count = df.isnull().sum()
missing_percentage = (df.isnull().sum() / len(df)) * 100

# 显示有缺失值的列,并按缺失数量降序排列
missing_data = pd.concat([missing_count, missing_percentage], axis=1, 
                         keys=['Count', 'Percentage'])
print(missing_data[missing_data['Count'] > 0].sort_values('Count', ascending=False))

这段代码会加载数据集,统计每列缺失值数量及其占比,并仅显示含有缺失值的列,按缺失条目数排序。

删除策略

有时,当别无选择时,你可能需要删除含有缺失值的行或列。

  • 删除完全为空的行:

    df_clean = df.dropna(how='all')
    
  • 删除关键列(如 name、email、age)存在缺失的行:

    df_clean = df.dropna(subset=['name', 'email', 'age'])
    
  • 删除缺失值超过50%的整列:

    threshold = len(df) * 0.5
    df_clean = df.dropna(axis=1, thresh=threshold)
    
填补缺失值(插补)

插补是用有意义的估计值替换缺失项。针对不同数据类型,插补策略也不同:

  • 对于数值型数据(如 age),用中位数(受异常值影响较小);

  • 对于收入(income),用平均值;

  • 对于评价(customer_rating),用众数(最常见值);

  • 对于类别型数据(如 gender),用众数或合理默认值。

df['age'] = df['age'].fillna(df['age'].median())
df['income'] = df['income'].fillna(df['income'].mean())
df['customer_rating'] = df['customer_rating'].fillna(df['customer_rating'].mode()[0])

df['gender'] = df['gender'].fillna(df['gender'].mode()[0])
df['comments'] = df['comments'].fillna('No comment provided')

更高级的方法是 KNN 插补,根据相似记录推测缺失值:

from sklearn.impute import KNNImputer
numeric_cols = ['age', 'income', 'customer_rating', 'purchase_amount']
imputer = KNNImputer(n_neighbors=3)
df[numeric_cols] = pd.DataFrame(
    imputer.fit_transform(df[numeric_cols]), 
    columns=numeric_cols,
    index=df.index
)

继续上文,以下是数据清洗指南的第2-4部分翻译:


2. 删除重复值

重复记录会导致统计结果失真,甚至得出错误结论。

首先,我们检查所有列的完全重复值:

# 查找完全重复的行
exact_duplicates = df.duplicated().sum()
print(f"Number of exact duplicate rows: {exact_duplicates}")

# 删除完全重复行
df_unique = df.drop_duplicates()

接着,寻找基于关键标识(如 name 和 email)的“功能性”重复:

# 查找可能的功能性重复(基于关键字段)
potential_duplicates = df.duplicated(subset=['name', 'email'], keep=False)
print(f"Number of potential functional duplicates: {potential_duplicates.sum()}")
print("Potential duplicate records:")
print(df[potential_duplicates].sort_values('name'))

对于重复项,我们保留信息最完整(缺失值最少)的记录:

# 先按非空字段数量排序,再去重
df['completeness'] = df.notna().sum(axis=1)
df_sorted = df.sort_values('completeness', ascending=False)
df_clean = df_sorted.drop_duplicates(subset=['name', 'email'])
df_clean = df_clean.drop(columns=['completeness'])

3. 标准化文本数据

文本不一致会制造不必要的“多样性”,增加分析难度。标准化文本字段能确保一致性。

首先,统一文本字段的大小写:姓名用标题格式,国家用大写,职位用小写。

# 统一类别字段的大小写
df['name'] = df['name'].str.title()
df['country'] = df['country'].str.upper()
df['job_title'] = df['job_title'].str.lower()

利用映射字典标准化国家名称和性别:

# 标准化国家名称
country_mapping = {
    'US': 'USA',
    'U.S.A.': 'USA',
    'United States': 'USA',
    'united states': 'USA',
    'United states': 'USA'
}
df['country'] = df['country'].replace(country_mapping)

# 标准化性别取值
gender_mapping = {
    'M': 'Male',
    'm': 'Male',
    'Male': 'Male',
    'male': 'Male',
    'F': 'Female',
    'f': 'Female',
    'Female': 'Female',
    'female': 'Female'
}
df['gender'] = df['gender'].replace(gender_mapping)

最后,定义自定义函数,结合关键词标准化教育背景:

# 标准化学历信息
def standardize_education(edu_str):
    if pd.isna(edu_str):
        return np.nan

    edu_str = str(edu_str).lower().strip()

    if 'bachelor' in edu_str:
        return "Bachelor's Degree"
    elif 'master' in edu_str or 'mba' in edu_str or 'msc' in edu_str:
        return "Master's Degree"
    elif 'phd' in edu_str or 'doctor' in edu_str:
        return "Doctorate"
    else:
        return "Other"

df['education'] = df['education'].apply(standardize_education)

4. 处理异常值

异常值可能极大影响统计与建模效果,正确识别和处理异常值至关重要。

首先,确保数值型字段为数字格式:

df['income'] = pd.to_numeric(df['income'], errors='coerce')
df['age'] = pd.to_numeric(df['age'], errors='coerce')

然后,采用两种方法检测异常值:Z-score(正态分布假设)和 IQR(对非正态数据更鲁棒)。

# 用 Z-score 方法检测异常值
from scipy import stats
z_scores = stats.zscore(df['income'].dropna())
outliers_z = (abs(z_scores) > 3)
print(f"Z-score method identified {outliers_z.sum()} outliers in income")

# IQR 方法检测异常值
Q1 = df['income'].quantile(0.25)
Q3 = df['income'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_iqr = ((df['income'] < lower_bound) | (df['income'] > upper_bound))
print(f"IQR method identified {outliers_iqr.sum()} outliers in income")

# 可视化异常值
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.boxplot(df['income'].dropna())
plt.title('Income Distribution with Outliers')
plt.ylabel('Income')
plt.show()

最后,修正明显错误值(如负收入),并用分位数法(如 winsorization)削弱极端值的影响:

# 修正不应为负的数值
df.loc[df['income'] < 0, 'income'] = np.nan  # 替换为缺失值,后续处理

# 分位数法对极端值进行截断
def cap_outliers(series, lower_percentile=0.05, upper_percentile=0.95):
    lower_limit = series.quantile(lower_percentile)
    upper_limit = series.quantile(upper_percentile)
    return series.clip(lower=lower_limit, upper=upper_limit)

df['income_capped'] = cap_outliers(df['income'], 0.01, 0.99)

继续上文,以下是数据清洗指南的第5-7部分翻译:


5. 数据类型转换

确保数据类型正确能提升性能,并保证各列能够进行合适的操作。

我们先检查当前的数据类型,然后将字符串格式的数值型数据转换为真正的数值类型,对于无法转换的内容以 NaN 处理:

# 显示当前数据类型
print(df.dtypes)

# 将字符串转换为数值型
df['age'] = pd.to_numeric(df['age'], errors='coerce')
df['income'] = pd.to_numeric(df['income'], errors='coerce')
df['customer_rating'] = pd.to_numeric(df['customer_rating'], errors='coerce')
df['purchase_amount'] = pd.to_numeric(df['purchase_amount'], errors='coerce')

对于日期字段,由于数据格式可能不一致,我们自定义函数尝试多种格式解析,解析成功后还可提取年份、月份等信息,并计算在职天数:

# 处理日期格式不一致
def parse_date(date_str):
    if pd.isna(date_str):
        return np.nan
    for fmt in ['%Y-%m-%d', '%Y/%m/%d', '%m/%d/%Y']:
        try:
            return pd.to_datetime(date_str, format=fmt)
        except:
            continue
    return pd.NaT 

df['start_date'] = df['start_date'].apply(parse_date)

# 提取有用时间特征
df['start_year'] = df['start_date'].dt.year
df['start_month'] = df['start_date'].dt.month
df['tenure_days'] = (pd.Timestamp('now') - df['start_date']).dt.days

最后,统一手机号格式,先去除所有非数字字符,然后按统一格式输出:

# 统一手机号格式
def standardize_phone(phone):
    if pd.isna(phone):
        return np.nan
    digits_only = ''.join(c for c in str(phone) if c.isdigit())
    if len(digits_only) == 10:
        return f"{digits_only[:3]}-{digits_only[3:6]}-{digits_only[6:]}"
    else:
        return digits_only  # 非10位时原样返回

df['phone_number'] = df['phone_number'].apply(standardize_phone)

6. 处理分类变量的不一致

分类变量常常存在拼写不一致、大小写混乱、类别过多等问题。

首先,查看职位名称的分布,然后通过映射将相似职位统一:

# 查看职位名称的唯一值分布
print(f"Original job title count: {df['job_title'].nunique()}")
print(df['job_title'].value_counts())

# 标准化职位名称
job_mapping = {
    'sr. developer': 'senior developer',
    'senior developer': 'senior developer',
    'ux designer': 'designer',
    'regional manager': 'manager',
    'project manager': 'manager',
    'product manager': 'manager',
    'lead engineer': 'senior developer',
    'bi analyst': 'data analyst',
    'data scientist': 'data analyst',
    'hr specialist': 'specialist',
    'marketing specialist': 'specialist'
}
df['standardized_job'] = df['job_title'].str.lower().replace(job_mapping)

接着,将部门字段归并到更宽泛的类别中:

# 部门归类
dept_categories = {
    'IT': 'Technology',
    'Engineering': 'Technology',
    'Analytics': 'Technology',
    'Design': 'Creative',
    'Marketing': 'Business',
    'Product': 'Business',
    'Executive': 'Management',
    'Human Resources': 'Operations',
    'Management': 'Management'
}
df['dept_category'] = df['department'].replace(dept_categories)

最后,将出现频率较低的职位归为“Other”:

# 处理稀有类别,归入“Other”
value_counts = df['standardized_job'].value_counts()
threshold = 2  # 低于该次数归为Other
frequent_jobs = value_counts[value_counts >= threshold].index
df['job_grouped'] = df['standardized_job'].apply(
    lambda x: x if x in frequent_jobs else 'Other'
)

7. 特征缩放与归一化

如果各特征取值范围差异很大,许多机器学习算法会受到影响,因此特征缩放很重要。

首先选出需要缩放的数值型字段,并填补缺失值:

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

# 选取数值字段并填充缺失值
numeric_cols = ['age', 'income', 'customer_rating', 'purchase_amount']
numeric_df = df[numeric_cols].copy()
numeric_df = numeric_df.fillna(numeric_df.median())

然后分别用三种方法缩放:

  • 标准化(StandardScaler):均值为0,标准差为1

    scaler = StandardScaler()
    scaled_data = scaler.fit_transform(numeric_df)
    df_scaled = pd.DataFrame(scaled_data, 
                              columns=[f"{col}_scaled" for col in numeric_cols],
                              index=df.index)
    
  • 最小-最大缩放(MinMaxScaler):全部映射到[0,1]区间

    min_max = MinMaxScaler()
    minmax_data = min_max.fit_transform(numeric_df)
    df_minmax = pd.DataFrame(minmax_data, 
                             columns=[f"{col}_minmax" for col in numeric_cols],
                             index=df.index)
    
  • 鲁棒缩放(RobustScaler):基于中位数和四分位数,对异常值不敏感

    robust = RobustScaler()
    robust_data = robust.fit_transform(numeric_df)
    df_robust = pd.DataFrame(robust_data, 
                             columns=[f"{col}_robust" for col in numeric_cols],
                             index=df.index)
    

将所有缩放结果与原始数据合并,并对比不同缩放效果:

df_with_scaled = pd.concat([df, df_scaled, df_minmax, df_robust], axis=1)
print(df_with_scaled[['income', 'income_scaled', 'income_minmax', 'income_robust']].head())

这样可以直观比较如“收入”(千位数)与“评分”(1-5分)等不同尺度特征在归一化后的表现。


继续上文,以下是数据清洗指南的第8-10部分翻译:


8. 字符串清洗与正则表达式

正则表达式对于清洗文本数据和从中提取信息非常有用。对于样本数据集(以及任何数据集),我们可以实现多种字符串清洗技巧。

首先,标准化姓名的大小写,并根据姓名提取“名”和“姓”:

import re

# 清理姓名:去除多余空格、标准化大小写
df['name_clean'] = df['name'].str.strip().str.title()

# 提取名和姓
def extract_names(full_name):
    if pd.isna(full_name):
        return pd.Series([np.nan, np.nan])
    parts = full_name.strip().split()
    if len(parts) >= 2:
        return pd.Series([parts[0], parts[-1]])
    else:
        return pd.Series([parts[0] if parts else np.nan, np.nan])

name_parts = df['name_clean'].apply(extract_names)
df['first_name'] = name_parts[0]
df['last_name'] = name_parts[1]

对邮箱进行正则校验,并尝试修复常见问题:

# 校验和清洗邮箱地址
def clean_email(email):
    if pd.isna(email):
        return np.nan
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    if re.match(pattern, email):
        return email.lower()
    else:
        # 修复常见问题
        if '@' not in email:
            return np.nan
        if not re.search(r'\.[a-zA-Z]{2,}$', email):
            return email.lower() + '.com'
        return email.lower()

df['email_clean'] = df['email'].apply(clean_email)

提取邮箱的域名信息:

# 提取邮箱域名
df['email_domain'] = df['email_clean'].str.extract(r'@([^@]+)$')

将不同格式的电话号码统一为一致的格式:

# 标准化/提取电话号码
def extract_phone(phone):
    if pd.isna(phone):
        return np.nan
    digits = re.sub(r'\D', '', str(phone))
    if len(digits) == 10:
        return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
    elif len(digits) > 10:  # 包含国家码
        return f"({digits[-10:-7]}) {digits[-7:-4]}-{digits[-4:]}"
    else:
        return np.nan

df['phone_standardized'] = df['phone_number'].apply(extract_phone)

这些技术让文本数据更一致,也能从非结构化或半结构化字段中提取结构化信息。


9. 脏数据中的特征工程

有时候,数据中的混乱本身也蕴含信息,你可以据此创造新的特征。

缺失值指示器:为关键字段创建“是否缺失”的哑变量

# 为关键字段创建缺失值指示器
missing_indicators = ['age', 'income', 'email', 'phone_number', 'comments']
for col in missing_indicators:
    df[f'{col}_is_missing'] = df[col].isnull().astype(int)

数据质量评分:计算每条记录的完整性并分类

# 数据质量评分
df['quality_score'] = df.notna().sum(axis=1) / len(df.columns) * 10
df['quality_category'] = pd.cut(
    df['quality_score'], 
    bins=[0, 6, 8, 10], 
    labels=['Poor', 'Average', 'Good']
)

标记可疑值,如整数倍收入、年龄不合理等:

# 检测潜在的数据录入错误
df['income_suspiciously_round'] = (df['income'] % 10000 == 0).astype(int)
df['age_out_of_range'] = ((df['age'] < 18) | (df['age'] > 80)).astype(int)
df['rating_out_of_range'] = ((df['customer_rating'] < 1) | (df['customer_rating'] > 5)).astype(int)

不仅要清洗脏数据,还应从中提取有用的信息。


10. 处理格式问题

不一致的格式会导致数据分析或建模时出现问题。

日期格式统一

# 统一日期格式
df['start_date_clean'] = pd.to_datetime(df['start_date'], errors='coerce')
df['start_date_formatted'] = df['start_date_clean'].dt.strftime('%Y-%m-%d')

去除货币符号,方便数值转换

# 处理货币格式问题
def clean_currency(amount):
    if pd.isna(amount):
        return np.nan
    if isinstance(amount, (int, float)):
        return amount
    amount_str = str(amount)
    amount_str = re.sub(r'[$,]', '', amount_str)
    try:
        return float(amount_str)
    except:
        return np.nan

df['purchase_amount_clean'] = df['purchase_amount'].apply(clean_currency)

统一数值格式,便于展示或报表

# 统一数值显示格式
df['income_formatted'] = df['income'].apply(
    lambda x: f"${x:,.2f}" if not pd.isna(x) else ""
)

对于要展示给最终用户或用于报表的数据,一致的格式尤为重要。

应用了多种清洗方法后,我们就得到了可以分析的最终干净数据集。


总结

我们已经覆盖了实际数据集中最常见数据质量问题的实用应对技巧。这些方法能帮助你将问题数据转化为可靠的数据资产。

数据清洗是一个迭代过程——每一步都能让你对数据的结构和质量有新的认识。上述模式可根据你的具体需求调整,并融入自动化工作流之中。

祝你数据清洗愉快!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值