Skip to content

Pandas 清洗分析

Pandas 是 Python 处理表格数据最常用的库。它适合读取 CSV、Excel、SQL 查询结果,把数据变成 DataFrame,再进行字段清洗、类型转换、缺失处理、去重、过滤、分组聚合、透视和导出。

零基础先记住:

Pandas 不是 Excel 的替代皮肤,而是把表格处理过程变成可重复执行、可审计、可测试的代码。

如果你只会 read_csv()groupby(),但不知道为什么要检查类型、为什么不能无脑填 0、为什么要按业务主键去重、为什么 SettingWithCopyWarning 重要,那么报表一复杂就很容易算错。

学习目标

学完本页,你应该能回答:

  1. DataFrameSeries 是什么。
  2. Pandas 读取 CSV/Excel 后为什么要先看 shapeheadinfo
  3. 字段类型为什么会影响统计结果。
  4. 缺失值、重复值、异常值分别怎么处理。
  5. loc、布尔索引、链式赋值和 SettingWithCopyWarning 怎么理解。
  6. groupbypivot_table 的原理和适用场景。
  7. 为什么大文件不能无脑一次性读进内存。
  8. 能写一个完整商业清洗 Demo。
  9. 面试能说清 Pandas 清洗流程和生产排查方法。

为什么需要 Pandas

如果用原生 listdict 处理表格,很多逻辑都要手写循环:

python
total = {}
for row in rows:
    if row["status"] == "paid":
        city = row["city"]
        total[city] = total.get(city, 0) + row["amount"]

这对小数据可以,但业务一复杂,缺失值、类型转换、去重、异常值、导出、分组都会让代码越来越乱。

Pandas 把常见表格操作抽象成列式处理:

python
summary = (
    df[df["status"] == "paid"]
    .groupby("city", as_index=False)["amount"]
    .sum()
)

它的价值不是少写几行,而是:

能力价值
自动读取多种数据源CSV、Excel、SQL、JSON 都能接入
列式处理更贴近“按字段清洗和计算”的业务思维
缺失值和类型体系能系统处理脏数据
分组聚合快速得到报表指标
可脚本化可重复、可审计、可自动化

DataFrame 和 Series 是什么

DataFrame 可以理解成一张带行索引和列名的表。Series 可以理解成一列带索引的数据。

mermaid
flowchart TD
    A["DataFrame 一张表"] --> B["index 行索引"]
    A --> C["columns 列名"]
    A --> D["多列 Series"]
    D --> E["Series: city"]
    D --> F["Series: amount"]
    D --> G["Series: status"]

Demo:

python
import pandas as pd


df = pd.DataFrame([
    {"order_id": "1001", "city": "北京", "amount": 99.5},
    {"order_id": "1002", "city": "上海", "amount": 120.0},
])

print(type(df))
print(type(df["amount"]))
print(df.index)
print(df.columns)

关键点:

对象含义常见操作
DataFrame一张二维表读取、筛选、分组、合并、导出
Series一列或一维数据类型转换、缺失处理、统计
Index行标签对齐、选择、合并
columns列名字段选择和业务含义

Pandas 清洗流程

mermaid
flowchart TD
    A["读取数据"] --> B["检查结构"]
    B --> C["确认字段口径"]
    C --> D["类型转换"]
    D --> E["缺失值处理"]
    E --> F["重复值处理"]
    F --> G["异常值处理"]
    G --> H["业务过滤"]
    H --> I["分组聚合"]
    I --> J["结果校验"]
    J --> K["导出和日志"]

这条链路不能省。省掉哪一步,就可能在那里出错。

步骤常用 API目的
读取read_csvread_excelread_sql获取原始表格
检查shapeheadinfodescribe确认结构和类型
类型转换to_numericto_datetimeastype让字段可计算
缺失处理isnafillnadropna控制空值影响
去重duplicateddrop_duplicates防止重复计算
过滤布尔索引、query按业务口径取数
聚合groupbyaggpivot_table统计指标
导出to_csvto_excel交付结果

读取数据

CSV:

python
import pandas as pd


df = pd.read_csv("orders.csv", encoding="utf-8")

Excel:

python
df = pd.read_excel("orders.xlsx", sheet_name="订单")

SQL:

python
import sqlite3
import pandas as pd


conn = sqlite3.connect("demo.db")
df = pd.read_sql("select * from orders", conn)

读取时常见参数:

参数作用场景
encoding指定编码中文乱码
sep指定分隔符非逗号 CSV
dtype指定列类型订单号、手机号保留字符串
parse_dates读取时解析日期日期格式稳定
usecols只读部分列大文件减少内存
chunksize分块读取文件很大

订单号建议指定字符串:

python
df = pd.read_csv(
    "orders.csv",
    encoding="utf-8",
    dtype={"order_id": "string"},
)

为什么?订单号不是拿来加减乘除的数字,前导 0、长度和格式都可能有业务意义。

读取后先做三板斧

python
print(df.shape)
print(df.head())
print(df.info())

含义:

方法看什么如果异常说明什么
shape行数和列数文件没读全、表头错、分隔符错
head前几行样例列错位、乱码、表头行不对
info类型和非空数量金额是 object、日期没解析、缺失严重

再看数值分布:

python
print(df.describe())

缺失分布:

python
print(df.isna().sum())

重复主键:

python
print(df.duplicated(subset=["order_id"]).sum())

类型转换

真实数据最常见的问题是:看起来像数字,实际上是字符串。

python
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
df["created_at"] = pd.to_datetime(df["created_at"], errors="coerce")

errors="coerce" 表示转换失败时变成缺失值。它适合批量清洗,但转换后必须检查:

python
bad_amount = df[df["amount"].isna()]
bad_date = df[df["created_at"].isna()]

金额带单位怎么处理

python
df["amount"] = (
    df["amount"]
    .astype("string")
    .str.replace("元", "", regex=False)
    .str.replace(",", "", regex=False)
    .str.strip()
)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")

状态字段怎么处理

python
df["status"] = df["status"].astype("string").str.strip().str.lower()

然后校验枚举:

python
allowed_status = {"paid", "refund", "cancelled"}
invalid_status = df[~df["status"].isin(allowed_status)]

缺失值处理

缺失值不是技术细节,而是业务语义。

mermaid
flowchart TD
    A["发现缺失值"] --> B{"是否核心字段"}
    B -- "是" --> C["进入异常文件"]
    B -- "否" --> D{"是否有安全默认值"}
    D -- "有" --> E["fillna 默认值"]
    D -- "没有" --> F["保留为空或标记 unknown"]

常见处理:

字段建议原因
order_id 缺失异常无法追溯
amount 缺失异常或人工核对0 和未知不同
remark 缺失填空字符串不影响核心统计
age 缺失中位数或 unknown看分析目的
created_at 缺失时间报表中剔除无法归属日期

示例:

python
df["remark"] = df["remark"].fillna("")
invalid = df[df[["order_id", "amount", "created_at"]].isna().any(axis=1)]
valid = df.dropna(subset=["order_id", "amount", "created_at"]).copy()

重复值处理

完全重复:

python
df.duplicated().sum()
df = df.drop_duplicates()

业务主键重复:

python
duplicates = df[df.duplicated(subset=["order_id"], keep=False)]

主键重复更重要。比如同一个订单出现两次,即使其他列不完全相同,也会造成销售额重复。

去重策略要由业务决定:

策略适合
保留第一条后续是重复导入
保留最后一条后续代表最新状态
updated_at 保留最新有更新时间
不自动去重,输出异常金额或状态冲突

示例:

python
df = (
    df.sort_values("created_at")
    .drop_duplicates(subset=["order_id"], keep="last")
    .copy()
)

异常值处理

异常值要分两类:

  1. 技术异常:无法转换、格式错误、字段缺失。
  2. 业务异常:金额为负、日期未来、状态非法、数量过大。
python
negative_paid = df[(df["status"] == "paid") & (df["amount"] < 0)]
future_orders = df[df["created_at"] > pd.Timestamp.today()]
too_large = df[df["amount"] > 100_000]

处理方式:

异常处理
状态非法输出异常文件
已支付金额为负输出异常文件
退款金额为负可能合理,看口径
金额过大抽样核对或单独标记
日期无法转换输出异常文件

不要静默删除异常数据。生产处理应该保留异常数据,用来反馈数据源。

布尔索引和 loc

筛选已支付订单:

python
paid = df[df["status"] == "paid"]

多条件:

python
paid_large = df[(df["status"] == "paid") & (df["amount"] >= 100)]

注意:Pandas 条件要用 &|,每个条件加括号,不要用 andor

推荐用 .loc 做赋值:

python
df.loc[df["amount"] < 0, "amount_flag"] = "invalid"

SettingWithCopyWarning 是什么

错误倾向写法:

python
paid = df[df["status"] == "paid"]
paid["amount"] = paid["amount"].fillna(0)

Pandas 可能警告 SettingWithCopyWarning。它想提醒你:paid 可能是原表的视图,也可能是拷贝,你的赋值到底会不会影响原表不够明确。

mermaid
flowchart TD
    A["原 DataFrame"] --> B["过滤得到 paid"]
    B --> C{"paid 是视图还是拷贝不明确"}
    C --> D["赋值可能没按预期生效"]

正确做法:

python
paid = df[df["status"] == "paid"].copy()
paid.loc[:, "amount"] = paid["amount"].fillna(0)

或者直接在原表上明确赋值:

python
df.loc[df["status"] == "paid", "amount"] = (
    df.loc[df["status"] == "paid", "amount"].fillna(0)
)

商业项目里不要忽略这个警告。它可能导致脚本看起来跑完了,但某一步清洗没有真正生效。

groupby 原理和用法

groupby 可以理解成三步:拆分、应用、合并。

mermaid
flowchart TD
    A["DataFrame"] --> B["按 city 拆分成多个组"]
    B --> C["每组分别 sum、mean、count"]
    C --> D["合并成汇总表"]

示例:

python
summary = (
    df.groupby("city", as_index=False)
    .agg(
        order_count=("order_id", "nunique"),
        total_amount=("amount", "sum"),
        avg_amount=("amount", "mean"),
    )
    .sort_values("total_amount", ascending=False)
)

常见聚合:

API含义
count非空数量
size行数,包括空值
nunique去重数量
sum求和
mean平均
median中位数
max / min最大/最小

countsize 区别很重要:

python
df.groupby("city").agg(
    row_count=("order_id", "size"),
    non_null_amount=("amount", "count"),
)

如果金额有空值,count 不会算空值,size 会算行数。

pivot_table 透视表

透视表适合多维交叉统计。

python
pivot = pd.pivot_table(
    df,
    values="amount",
    index="city",
    columns="status",
    aggfunc="sum",
    fill_value=0,
)

含义:

参数作用
values被统计的值
index行维度
columns列维度
aggfunc聚合方法
fill_value缺失组合填充值

业务例子:

城市paidrefundcancelled
北京10001000
上海20008050

透视表适合报表展示,但如果后续还要继续做程序处理,普通 groupby 结果通常更简单。

merge 合并表

商业数据经常需要把订单表和城市维度表、用户表、资产表合并。

python
orders = pd.DataFrame([
    {"order_id": "1001", "city_code": "BJ", "amount": 100},
    {"order_id": "1002", "city_code": "SH", "amount": 200},
])

city_dim = pd.DataFrame([
    {"city_code": "BJ", "city_name": "北京"},
    {"city_code": "SH", "city_name": "上海"},
])

result = orders.merge(city_dim, on="city_code", how="left")
print(result)

how 怎么选:

how含义场景
left保留左表全部订单补维度
inner只保留两边匹配只分析有维度的数据
outer两边都保留对账
right保留右表全部少用

合并后要检查是否匹配失败:

python
missing_city = result[result["city_name"].isna()]

如果维度没匹配上,后续分组会出现空城市或统计丢失。

排序、TopN 和排名

python
top_city = summary.sort_values("total_amount", ascending=False).head(10)

组内 TopN:

python
df["rank_in_city"] = (
    df.groupby("city")["amount"]
    .rank(method="first", ascending=False)
)

top_orders = df[df["rank_in_city"] <= 3]

排名常见用途:

场景用法
每城市销售 Top10groupby + rank
每部门资产访问 TopNgroupby + rank
找异常高金额订单排序后抽查

apply 什么时候用

apply 很灵活,但不是首选。能用内置向量化就不要用 apply

优先:

python
df["amount_level"] = "normal"
df.loc[df["amount"] >= 1000, "amount_level"] = "high"

复杂逻辑再用:

python
def classify(row: pd.Series) -> str:
    if row["status"] != "paid":
        return "not_paid"
    if row["amount"] >= 1000:
        return "high"
    return "normal"


df["amount_level"] = df.apply(classify, axis=1)

apply(axis=1) 是逐行调用 Python 函数,数据大时会慢。商业项目里要先保证正确,再考虑性能;性能瓶颈出现时,优先改成向量化、SQL 或分布式处理。

大文件怎么处理

Pandas 默认把数据读进内存。如果文件很大,可能内存爆掉。

分块读取:

python
chunks = []

for chunk in pd.read_csv("big_orders.csv", chunksize=100_000):
    chunk["amount"] = pd.to_numeric(chunk["amount"], errors="coerce")
    paid = chunk[chunk["status"] == "paid"].copy()
    part = paid.groupby("city", as_index=False)["amount"].sum()
    chunks.append(part)

merged = pd.concat(chunks, ignore_index=True)
final = merged.groupby("city", as_index=False)["amount"].sum()

分块难点:

问题说明
跨块去重同一订单可能分布在不同块
全局排序单块排序不等于全局排序
全局 TopN需要最终合并后再算
内存中间结果也可能越来越大

如果数据量持续增长,应考虑数据库、DuckDB、Polars、Spark,而不是所有事情都硬用 Pandas。

商业 Demo:医疗资产导入清洗

需求:业务上传一份资产 CSV,字段包括资产编码、资产名称、部门、负责人、访问次数、质量分、状态。我们要清洗数据并输出:

  1. 有效资产清单。
  2. 异常资产清单。
  3. 按部门统计资产数量、平均质量分、访问次数。

下面 Demo 可直接运行。

python
from io import StringIO
import pandas as pd


RAW_CSV = """asset_code,asset_name,department,owner,visit_count,quality_score,status
A001,门诊明细,信息科,张三,120,0.95,online
A002,检验报告,信息科,李四,80,0.88,online
A002,检验报告,信息科,李四,90,0.90,online
A003,药品库存,药剂科,王五,abc,0.76,online
,设备台账,设备科,赵六,30,0.99,offline
A005,收费明细,财务科,钱七,300,1.20,online
A006,病案首页,病案室,,60,0.82,unknown
"""


def load_assets() -> pd.DataFrame:
    return pd.read_csv(StringIO(RAW_CSV), dtype={"asset_code": "string"})


def normalize_assets(df: pd.DataFrame) -> pd.DataFrame:
    result = df.copy()
    text_columns = ["asset_code", "asset_name", "department", "owner", "status"]
    for column in text_columns:
        result[column] = result[column].astype("string").str.strip()

    result["visit_count"] = pd.to_numeric(result["visit_count"], errors="coerce")
    result["quality_score"] = pd.to_numeric(result["quality_score"], errors="coerce")
    return result


def split_invalid_assets(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
    required = ["asset_code", "asset_name", "department", "owner"]
    valid_status = {"online", "offline"}

    invalid_mask = (
        df[required].isna().any(axis=1)
        | (df[required] == "").any(axis=1)
        | df["visit_count"].isna()
        | df["quality_score"].isna()
        | (df["quality_score"] < 0)
        | (df["quality_score"] > 1)
        | ~df["status"].isin(valid_status)
    )

    invalid = df[invalid_mask].copy()
    valid = df[~invalid_mask].copy()
    return valid, invalid


def deduplicate_assets(df: pd.DataFrame) -> pd.DataFrame:
    return df.drop_duplicates(subset=["asset_code"], keep="last").copy()


def build_department_report(df: pd.DataFrame) -> pd.DataFrame:
    online = df[df["status"] == "online"].copy()
    return (
        online.groupby("department", as_index=False)
        .agg(
            asset_count=("asset_code", "nunique"),
            total_visit=("visit_count", "sum"),
            avg_quality=("quality_score", "mean"),
        )
        .sort_values("asset_count", ascending=False)
    )


def main() -> None:
    raw = load_assets()
    normalized = normalize_assets(raw)
    deduped = deduplicate_assets(normalized)
    valid, invalid = split_invalid_assets(deduped)
    report = build_department_report(valid)

    print("有效资产:")
    print(valid)
    print("\n异常资产:")
    print(invalid)
    print("\n部门报表:")
    print(report)


if __name__ == "__main__":
    main()

这个 Demo 体现了:

处理为什么
asset_code 指定 string资产编码不是数学数字
文本字段 strip去掉导入文件中的多余空格
数值字段 to_numeric把访问次数、质量分转成可统计字段
quality_score 范围校验分数必须在 0 到 1
status 枚举校验防止未知状态进入统计
asset_code 去重防止同一资产重复导入
异常数据单独输出反馈给业务修正
只统计 online报表口径明确

结果校验

清洗完不要直接交付,要做校验。

python
assert report["asset_count"].sum() <= valid["asset_code"].nunique()
assert valid["quality_score"].between(0, 1).all()
assert valid["visit_count"].ge(0).all()

也可以打印处理报告:

python
print("原始行数:", len(raw))
print("有效行数:", len(valid))
print("异常行数:", len(invalid))
print("重复资产数:", normalized.duplicated(subset=["asset_code"]).sum())

生产任务建议把这些信息写入日志,而不是只打印到控制台。

常见坑

后果正确做法
不指定编码中文乱码明确 encoding
订单号、资产编码转数字前导 0 丢失,科学计数法指定 string
不检查 info()类型错误不知道读取后三板斧
缺失值全填 0业务含义被篡改按字段含义处理
主键重复只做完全去重重复业务数据仍存在按业务主键去重
忽略 SettingWithCopyWarning修改可能没生效.copy().loc
apply(axis=1) 滥用大数据很慢优先向量化
分块后块内去重跨块重复仍存在最终阶段全局去重
导出不带异常文件数据问题无反馈输出异常数据和原因

生产排查流程

Pandas 报表结果不对时,按这条链路查:

mermaid
flowchart TD
    A["报表结果异常"] --> B["确认原始数据范围"]
    B --> C["检查读取行数和列名"]
    C --> D["检查 dtype 和缺失"]
    D --> E["检查主键重复"]
    E --> F["检查异常值和枚举"]
    F --> G["检查过滤条件"]
    G --> H["检查 groupby / pivot 口径"]
    H --> I["聚合前后总数对账"]
    I --> J["抽样明细核对"]

排查证据:

证据用途
原始文件行数判断源数据完整性
df.shape判断读取是否完整
df.dtypes判断类型是否正确
isna().sum()找缺失和转换失败
duplicated(subset=key)找业务重复
value_counts()检查状态、部门、城市分布
聚合前后总金额判断是否漏算或重复
异常文件反馈源数据质量

面试标准回答

Pandas 数据清洗完整流程是什么?

可以这样答:

text
我会先确认字段含义和统计口径,再读取数据并检查 shape、head、info、describe、缺失值和重复值。接着对金额、日期、状态等关键字段做类型转换和枚举校验,按业务主键去重,处理缺失值和异常值,再按业务口径过滤数据。最后使用 groupby 或 pivot_table 聚合,导出结果和异常数据,并做聚合前后总数、总金额和样例明细校验,保证结果可复现、可追溯。

DataFrame 和 Series 是什么?

DataFrame 是一张带行索引和列名的二维表,Series 是带索引的一维列数据。实际使用中,读取 CSV/Excel/SQL 后通常得到 DataFrame,选取某一列得到 Series。清洗和统计就是围绕这些列做类型转换、缺失处理、筛选和聚合。

SettingWithCopyWarning 是什么?

它表示你可能在一个不明确的视图或拷贝上赋值,修改结果不一定按预期生效。常见原因是先过滤出子 DataFrame,再直接赋值。正确做法是过滤后显式 .copy(),或者在原 DataFrame 上用 .loc[row_condition, column] 明确赋值。

groupby 的原理是什么?

groupby 可以理解成拆分、应用、合并三步。先按一个或多个字段把 DataFrame 拆成多个组,再对每组执行 summeancountnunique 等聚合函数,最后把结果合并成汇总表。面试时要注意 count 是非空数量,size 是行数。

apply 为什么不建议滥用?

apply(axis=1) 会逐行调用 Python 函数,灵活但性能通常不如 Pandas/NumPy 向量化。小数据或复杂业务规则可以用,数据量大时应优先考虑向量化、loc 条件赋值、SQL 聚合或其他更适合大数据的工具。

关联知识点

知识点作用
Python 数据处理总览建立完整数据处理流程
NumPy 数组计算理解 Pandas 底层数组和向量化
端到端实践完整跑通 CSV 清洗和导出
Python 异常与文件理解文件编码、异常和资源关闭
Python 日志与调试让清洗脚本可排查
Python 测试给清洗函数写测试
Python 面试题查看标准回答和追问

本章小结

Pandas 清洗分析的核心是“让表格数据可信”。你要先检查结构和类型,再按业务口径处理缺失、重复和异常,最后聚合、校验、导出。真正的商业数据处理不是跑出一个 CSV,而是保证结果可解释、过程可复现、错误可追溯。