数据探索与理解:先别急着清洗,看看数据长什么样

拿到一份新数据,我个人的习惯是——先别急着写清洗代码。你想想看,连数据长什么样都不清楚,上来就咔咔一顿删空值、去重,很容易把有用的信息也干掉了。

数据探索这一步,说白了就是跟数据打个照面。看看它有多少行、多少列,每个字段是什么类型,数值分布是否合理。我见过太多人跳过这一步,结果清洗到一半才发现数据结构和自己想的完全不一样,又得回头重来。

嗯,咱们这一章就把数据探索的四个核心动作拆开揉碎了讲清楚。

数据探索与理解 · 知识体系 数据探索与理解 数据概览 描述性统计 数据类型识别 数据分布分析 shape / info describe() dtypes / astype 直方图 / 箱线图 核心原则:先概览全局,再深入细节;先识别类型,再分析分布。 每一步都为后续的数据清洗和特征工程提供决策依据。

2.1 数据概览:先摸清家底

数据概览就两个核心动作:看形状、看字段。我一般用 df.shape 和 df.info() 打头阵。

import pandas as pd

# 读取数据
df = pd.read_csv('sales_data.csv')

# 看行列数
print(f"数据集形状: {df.shape}")
print(f"行数: {df.shape[0]}, 列数: {df.shape[1]}")

# 看字段信息
df.info()

df.info() 会告诉你每个字段的名称、非空数量、数据类型。这一步能快速发现两个问题:

  • 空值情况:哪个字段有缺失,缺了多少
  • 类型异常:比如本该是日期的字段显示为 object,本该是数值的字段显示为 object

我的经验:有一次拿到一个用户行为日志,df.info() 一看,时间戳字段全是 object 类型。再一查,里面混了两种日期格式——"2024-01-15" 和 "01/15/2024"。这种问题不先发现,后面解析日期时就会报错。

2.2 描述性统计:让数字说话

概览看完了,接下来要看数值型字段的统计特征。Pandas 的 describe() 方法就是干这个的。

# 默认只统计数值型字段
df.describe()

# 如果想看所有字段(包括类别型)
df.describe(include='all')

# 只看某个字段
df['price'].describe()

describe() 会输出以下统计量:

统计量 含义 用途
count 非空值数量 判断缺失程度
mean 均值 数据中心趋势
std 标准差 数据离散程度
min / max 最小值 / 最大值 数据范围
25% / 50% / 75% 四分位数 数据分布形态

一个小技巧:看均值和中位数(50%)的差距。如果两者相差很大,说明数据有偏态分布或者存在异常值。比如用户收入字段,均值 5 万但中位数只有 3 万,那大概率是少数高收入人群把均值拉高了。

2.3 数据类型识别:别让 Pandas 猜错了

Pandas 在读取数据时会自动推断字段类型,但说实话,它经常猜错。我遇到过最典型的情况:

  • 身份证号被识别成 int64(其实应该是字符串)
  • 金额字段带逗号或货币符号,被识别成 object
  • 日期字段被识别成 object

所以,手动检查和修正数据类型是必须的。

# 查看每个字段的类型
print(df.dtypes)

# 手动转换类型
df['user_id'] = df['user_id'].astype(str)
df['price'] = pd.to_numeric(df['price'], errors='coerce')
df['date'] = pd.to_datetime(df['date'], errors='coerce')

# 类别型字段用 category 类型节省内存
df['city'] = df['city'].astype('category')

我曾经踩过的坑:有一次处理一个 500 万行的用户表,有个 "gender" 字段取值只有 "男"、"女"、"未知" 三种。我忘了转成 category 类型,结果这个字段占了 80MB 内存。转成 category 后直接降到 3MB。数据量大的时候,类型转换能省下大量内存。

2.4 数据分布分析:发现异常和规律

描述性统计只能告诉你数字,但数据长什么样,还得靠可视化。我一般用直方图和箱线图来快速判断分布情况。

import matplotlib.pyplot as plt

# 直方图:看数值分布
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
df['age'].hist(bins=30, edgecolor='black')
plt.title('年龄分布直方图')
plt.xlabel('年龄')
plt.ylabel('频数')

# 箱线图:看异常值
plt.subplot(1, 2, 2)
df.boxplot(column='age')
plt.title('年龄箱线图')
plt.ylabel('年龄')

plt.tight_layout()
plt.show()

通过分布分析,你能发现:

  1. 偏态分布:数据集中在一边,比如收入数据通常右偏
  2. 多峰分布:数据有多个聚集中心,可能代表不同群体
  3. 异常值:箱线图中超出上下须的点,需要重点关注
  4. 数据范围异常:比如年龄出现 200 岁,明显是脏数据

实际案例:我在做电商订单数据清洗时,通过直方图发现 "订单金额" 字段在 0 附近有一个异常高峰。查了一下,原来是测试订单和退款订单混进来了。这些数据如果不剔除,后续的客单价分析就会严重失真。

2.5 综合实战:一个完整的探索流程

把上面四个步骤串起来,就是一个标准的数据探索流程。我一般会写一个函数,每次拿到新数据先跑一遍。

def explore_data(df, sample_size=5):
    """
    数据探索综合函数
    """
    print("=" * 50)
    print("1. 数据概览")
    print("=" * 50)
    print(f"形状: {df.shape}")
    print(f"字段列表: {list(df.columns)}")
    print()
    
    print("=" * 50)
    print("2. 数据类型")
    print("=" * 50)
    print(df.dtypes)
    print()
    
    print("=" * 50)
    print("3. 缺失值统计")
    print("=" * 50)
    missing = df.isnull().sum()
    missing_pct = (missing / len(df)) * 100
    missing_df = pd.DataFrame({
        '缺失数量': missing,
        '缺失比例(%)': missing_pct
    })
    print(missing_df[missing_df['缺失数量'] > 0])
    print()
    
    print("=" * 50)
    print("4. 数值型字段描述性统计")
    print("=" * 50)
    print(df.describe())
    print()
    
    print("=" * 50)
    print(f"5. 前 {sample_size} 行数据预览")
    print("=" * 50)
    print(df.head(sample_size))
    
    return None

# 使用
explore_data(df)

我的习惯:这个函数跑完后,我会把结果截图保存到项目文档里。后续做特征工程或者写报告时,随时可以回头参考。数据探索不是一次性工作,随着对业务理解的加深,你可能需要反复回来查看。

数据探索这一步,说白了就是给数据做个体检。不体检就开刀,风险太大了。你想想看,如果连数据的基本情况都不清楚,后面做的清洗、转换、建模,很可能都是在错误的基础上堆砌。

嗯,这一章的内容就到这里。记住四个核心动作:概览、统计、类型、分布。每一步都有它的价值,别跳过。