Pandas数据炼金术

Pandas 数据分析:80% 的时间在清洗,这篇帮你省掉一半

做数据分析的人都知道一句话:“垃圾进,垃圾出”。不管你的图表画得多漂亮、模型多复杂,数据本身有问题,结论就是错的。

数据科学家 80% 的时间花在数据清洗和准备上。不是算法难,是数据脏——缺失值、重复行、格式混乱、异常值、字段名不一致……

Pandas 就是干这个的。它是 Python 数据分析的基石,能帮你把脏数据变成干净、可分析的表格。

Pandas 的两个核心结构:Series 和 DataFrame

结构 维度 类似
Series 1维,一列数据 Excel 里的一列
DataFrame 2维,表格 Excel 里的整张表
import pandas as pd
import numpy as np

# DataFrame 就像一张表
df = pd.DataFrame({
    '姓名': ['张三', '李四', '王五'],
    '年龄': [25, 30, 35],
    '城市': ['北京', '上海', '广州']
})

日常 99% 的工作都在跟 DataFrame 打交道——加载、清洗、筛选、聚合、输出。

拿到数据第一件事:先”体检”

假设你拿到一份销售订单数据 sales.csv,不要急着分析,先体检。

看一眼数据长什么样:

df = pd.read_csv('sales.csv')

df.head()      # 前5行,看列名和样例数据
df.tail()      # 后5行,看数据末尾有没有异常
df.sample(5)   # 随机5行,避免排序带来的偏见

看整体概况:多少行、多少列、每列是什么类型、有没有空值

df.info()

输出里重点关注:

  • Non-Null Count:小于总行数说明有缺失值
  • Dtype:object 表示字符串,需要确认是否需要转成日期或数字

看数值列的统计摘要:

df.describe()

输出里关注:

  • count:非空数量,进一步确认缺失
  • min/max:最小值最大值,有没有明显异常(比如年龄出现 999)
  • mean/std:均值和标准差,判断数据分布是否合理

专门看一下缺失值:

df.isnull().sum()                     # 每列缺了多少
df.isnull().sum() / len(df) * 100     # 缺失比例

拿到”体检报告”之后,你才知道数据有哪些问题:哪些列有缺失、哪些列类型不对、哪些值明显异常。先诊断再动手,别上来就瞎洗。

数据清洗:处理重复值、缺失值、异常值

重复值:先看有没有,有就删

df.duplicated().sum()          # 重复行数量
df = df.drop_duplicates()      # 删除重复行

# 如果某几列组合应该唯一,按这几列去重
df = df.drop_duplicates(subset=['订单号', '客户ID'])

缺失值:删还是填?

先判断严重程度:

  • 某列缺失超过 50% → 考虑直接删掉这列
  • 某列缺失较少 → 填充

删除:

# 删掉包含任何空值的行(谨慎用,丢数据太多)
df = df.dropna()

# 删掉"客户ID"或"订单金额"为空的行(关键字段不能丢)
df = df.dropna(subset=['客户ID', '订单金额'])

# 删掉缺失超过一半的列
threshold = len(df) * 0.5
df = df.dropna(axis=1, thresh=threshold)

填充(更常用):

# 数字列:用中位数填充(比均值更稳健,不易受异常值影响)
df['年龄'] = df['年龄'].fillna(df['年龄'].median())

# 分类列:用众数填充
df['城市'] = df['城市'].fillna(df['城市'].mode()[0])

# 时间序列:用前一个值填充(前向填充)
df['价格'] = df['价格'].fillna(method='ffill')

# 时间序列:用插值填充
df['销售额'] = df['销售额'].interpolate()

异常值:找出来,处理掉

方法一:IQR(四分位距)法——最常用
def detect_outliers(df, col):
    Q1 = df[col].quantile(0.25)
    Q3 = df[col].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return lower, upper

lower, upper = detect_outliers(df, '订单金额')

# 剔除异常值
df = df[(df['订单金额'] >= lower) & (df['订单金额'] <= upper)]

# 或把异常值"盖"到边界(盖帽法)
df['订单金额'] = df['订单金额'].clip(lower=lower, upper=upper)
方法二:业务规则过滤
# 年龄不能为负,不能超过120
df = df[df['年龄'] >= 0]
df = df[df['年龄'] <= 120]

# 单价不能为负
df = df[df['单价'] >= 0]

数据类型转换

# 字符串转数字(转不了的话变成 NaN,再处理)
df['价格'] = pd.to_numeric(df['价格'], errors='coerce')

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

# 转为分类类型(节省内存)
df['城市'] = df['城市'].astype('category')

数据筛选:从数据里捞出你要的那部分

布尔索引(最常用):

# 单条件
electronics = df[df['品类'] == '电子产品']

# 多条件 AND(用 &)
filtered = df[(df['年龄'] > 25) & (df['城市'] == '北京')]

# 多条件 OR(用 |)
filtered = df[(df['品类'] == '电子产品') | (df['品类'] == '图书')]

# 简化 OR:用 isin()
filtered = df[df['品类'].isin(['电子产品', '图书'])]

# 取反(NOT)
filtered = df[~df['品类'].isin(['电子产品', '图书'])]

query() 写法更接近自然语言:

filtered = df.query("年龄 > 25 and 城市 == '北京'")
filtered = df.query("品类 in ['电子产品', '图书']")

选列:

# 选一列
names = df['姓名']

# 选多列
subset = df[['姓名', '年龄', '城市']]

# 删列
df = df.drop('无用列', axis=1)

特征工程:从已有数据里”造”出新信息

算术运算创建新列

df['总金额'] = df['单价'] * df['数量']

# 条件创建
df['价格等级'] = np.where(df['单价'] > 50, '高', '低')

# 多条件
conditions = [
    df['单价'] < 20,
    df['单价'] < 50,
    df['单价'] >= 50
]
choices = ['低档', '中档', '高档']
df['档次'] = np.select(conditions, choices)

从日期里提取信息

df['订单日期'] = pd.to_datetime(df['订单日期'])

df['年'] = df['订单日期'].dt.year
df['月'] = df['订单日期'].dt.month
df['季度'] = df['订单日期'].dt.quarter
df['星期几'] = df['订单日期'].dt.day_name()      # Monday, Tuesday...
df['是否周末'] = df['订单日期'].dt.dayofweek >= 5

分箱(把连续值切成几段)

# 等距分箱
bins = [0, 18, 30, 45, 60, 100]
labels = ['<18', '18-30', '30-45', '45-60', '60+']
df['年龄段'] = pd.cut(df['年龄'], bins=bins, labels=labels)

# 等频分箱(每段样本数差不多)
df['收入等级'] = pd.qcut(df['收入'], q=4, labels=['低', '中低', '中高', '高'])

apply 自定义函数

def calc_discount(row):
    if row['数量'] >= 10:
        return row['单价'] * 0.9
    return row['单价']

df['折后单价'] = df.apply(calc_discount, axis=1)

数据聚合:把明细变成报表

groupby 分组聚合——数据分析最核心的操作:

# 按品类分组,算总销售额
df.groupby('品类')['总金额'].sum()

# 按品类分组,算多个指标
df.groupby('品类')['总金额'].agg(['sum', 'mean', 'count', 'std'])

# 按多个维度分组
df.groupby(['品类', '城市'])['总金额'].sum()

# 不同列用不同聚合函数
df.groupby('品类').agg({
    '总金额': ['sum', 'mean'],
    '数量': 'sum',
    '单价': 'median'
})

# 命名聚合(推荐,结果更清晰)
df.groupby('品类').agg(
    总销售额=('总金额', 'sum'),
    平均单价=('单价', 'mean'),
    订单数=('订单号', 'count')
)

数据透视表:把分组结果展开成表格

# 行=品类,列=城市,值=总销售额
pivot = pd.pivot_table(
    df,
    values='总金额',
    index='品类',
    columns='城市',
    aggfunc='sum',
    fill_value=0
)

# 加总计行列
pivot = pd.pivot_table(
    df,
    values='总金额',
    index='品类',
    columns='城市',
    aggfunc='sum',
    margins=True,
    margins_name='总计'
)

数据输出:把结果保存下来

# 保存为 CSV
df.to_csv('清洗后数据.csv', index=False, encoding='utf-8-sig')

# 保存为 Excel
df.to_excel('报表.xlsx', index=False)

# 保存多个 Sheet
with pd.ExcelWriter('报表.xlsx') as writer:
    df1.to_excel(writer, sheet_name='销售明细', index=False)
    df2.to_excel(writer, sheet_name='汇总', index=False)

# 保存为 JSON
df.to_json('数据.json', orient='records', force_ascii=False)