第4章:数据清洗实战——处理缺失值、重复值、异常值,数据类型转换

数据清洗这活儿,说白了就是给原始数据「搓澡」。

我在金融监管项目里见过太多「脏数据」了。有的缺失值直接空着,有的重复行偷偷藏着,还有的异常值能把你的图表拉成过山车。不洗?那报告就是废纸一张。

今天咱们就动手,把缺失值、重复值、异常值、数据类型转换这四个硬骨头啃下来。嗯,代码我都跑过,你直接拿去用就行。

核心原则:清洗不是删数据,而是让数据变得可用。每删一行,都要问自己「我确定吗?」

4.1 缺失值处理:别急着删

缺失值在金融数据里太常见了。比如某只股票某天停牌,交易量就是NaN。再比如财报里某个指标没披露,也是空值。

我习惯先看一眼缺失率。如果超过50%,这列基本可以扔了。如果只是零星缺失,那就补上。

常用三板斧:

  1. 删除法——缺失行很少时,直接dropna()。简单粗暴,但别滥用。
  2. 填充法——用均值、中位数、众数填充。金融数据我偏爱中位数,因为不怕极端值干扰。
  3. 插值法——时间序列数据用前向填充或线性插值,效果比均值好得多。
import pandas as pd
import numpy as np

# 模拟一份金融数据
df = pd.DataFrame({
    '日期': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
    '收盘价': [100, np.nan, 102, 103],
    '成交量': [10000, 12000, np.nan, 11000]
})

# 看一眼缺失情况
print(df.isnull().sum())

# 我习惯:时间序列用前向填充
df['收盘价'].fillna(method='ffill', inplace=True)

# 数值型用中位数填充
df['成交量'].fillna(df['成交量'].median(), inplace=True)

print(df)

我的小技巧:填充前先画个分布图。如果数据是正态分布,用均值;如果有偏态,用中位数。我在做债券收益率数据时吃过亏,均值填充把整个分布拉偏了,后来改用中位数才正常。

4.2 重复值处理:一个不留

重复值在金融数据里经常是「手滑」造成的。比如数据采集脚本跑了两遍,或者Excel里复制粘贴多了一行。

我建议:先查重复,再决定删不删。有些重复是合法的(比如同一只股票在不同交易所的数据),但大部分是垃圾。

# 检查重复行
print(df.duplicated().sum())

# 删除重复行,保留第一次出现的
df.drop_duplicates(inplace=True)

# 如果想按某列去重(比如按股票代码)
df.drop_duplicates(subset=['股票代码'], keep='first', inplace=True)

注意:千万别一上来就drop_duplicates()。我曾经在监管报告里删了「看似重复」的行,结果发现那是同一只股票在不同交易日的合法记录。嗯,从那以后我养成了先看subset的习惯。

4.3 异常值处理:别一刀切

异常值是最考验经验的。金融数据里,异常可能是错误,也可能是黑天鹅事件。

比如某天股价突然跌了90%,可能是数据录入错误,也可能是真的发生了熔断。你怎么判断?

我常用的方法:

  • 3σ原则——超出均值±3倍标准差的值,标记为异常。适合正态分布的数据。
  • IQR方法——超出Q1-1.5IQR或Q3+1.5IQR的值,标记为异常。更稳健,不怕极端值。
  • 业务规则——比如股价不能为负,成交量不能为0。这比任何统计方法都靠谱。
# IQR方法检测异常值
Q1 = df['收盘价'].quantile(0.25)
Q3 = df['收盘价'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常
df['异常标记'] = (df['收盘价'] < lower_bound) | (df['收盘价'] > upper_bound)

# 我习惯:先看异常值长什么样,再决定处理方式
print(df[df['异常标记']])

避坑指南:我曾经在分析某只银行股时,用3σ原则把「2008年金融危机期间的数据」全标成了异常。你想想看,那本来就是极端行情,删了反而失真。所以,异常值处理一定要结合业务背景。

4.4 数据类型转换:让数据「说人话」

数据类型不对,后面所有分析都是白搭。我见过最离谱的:日期列是字符串,金额列是对象类型,百分比列是带百分号的文本。

转换其实就三步:看类型、想目标、动手转。

# 查看数据类型
print(df.dtypes)

# 日期字符串转datetime
df['日期'] = pd.to_datetime(df['日期'])

# 金额字符串转浮点数(去掉逗号和货币符号)
df['金额'] = df['金额'].str.replace(',', '').str.replace('¥', '').astype(float)

# 百分比转小数
df['增长率'] = df['增长率'].str.replace('%', '').astype(float) / 100

核心心法:转换前先备份原始列。我每次都会加一列「_raw」保留原始值,万一转错了还能回滚。这个习惯救过我很多次。

4.5 实战流程:一条龙搞定

下面是我做监管报告时的标准清洗流程。你直接套用就行。

def clean_financial_data(df):
    """
    金融数据清洗流水线
    """
    # 1. 备份
    df_clean = df.copy()
    
    # 2. 删除全空列
    df_clean.dropna(axis=1, how='all', inplace=True)
    
    # 3. 删除重复行
    df_clean.drop_duplicates(inplace=True)
    
    # 4. 日期列转换
    for col in df_clean.select_dtypes(include=['object']).columns:
        if '日期' in col or '时间' in col:
            df_clean[col] = pd.to_datetime(df_clean[col], errors='coerce')
    
    # 5. 数值列转换(去掉逗号、百分号等)
    for col in df_clean.select_dtypes(include=['object']).columns:
        if df_clean[col].str.contains('%').any():
            df_clean[col] = df_clean[col].str.replace('%', '').astype(float) / 100
    
    # 6. 缺失值填充(数值列用中位数)
    num_cols = df_clean.select_dtypes(include=[np.number]).columns
    df_clean[num_cols] = df_clean[num_cols].fillna(df_clean[num_cols].median())
    
    # 7. 异常值处理(IQR方法,只标记不删除)
    for col in num_cols:
        Q1 = df_clean[col].quantile(0.25)
        Q3 = df_clean[col].quantile(0.75)
        IQR = Q3 - Q1
        lower = Q1 - 1.5 * IQR
        upper = Q3 + 1.5 * IQR
        df_clean[f'{col}_异常标记'] = (df_clean[col] < lower) | (df_clean[col] > upper)
    
    return df_clean

我的建议:把这个函数保存成.py文件,每次新项目直接import。我用了三年,迭代了十几个版本,现在基本不用改就能跑通大部分金融数据。

4.6 知识体系总览

下面这张图,是我自己梳理的数据清洗知识框架。你保存下来,每次清洗前看一眼,思路就清晰了。

数据清洗知识体系 原始数据 缺失值处理 重复值处理 异常值处理 数据类型转换 删除法 填充法(中位数) 插值法(ffill) 全行去重 按列去重 保留首/末行 3σ原则 IQR方法 业务规则 字符串→日期 字符串→数值 百分比→小数 ✅ 干净可用的数据

你看,整个流程其实不复杂。关键就四点:缺了补、重了删、异常标、类型转。把这四步走完,数据基本就能用了。

我个人习惯是每步都留个日志,记录删了多少行、补了多少值。这样写监管报告时,还能在附录里说明「数据清洗过程」,显得专业又严谨。

最后说一句:数据清洗没有银弹。每份数据都有自己的脾气,多跑、多看、多问业务方,才是正道。

专注资料整理