Python 数据处理总览
Python 做数据处理,最常用的组合是 NumPy 和 Pandas。NumPy 负责高效数组计算,Pandas 负责表格数据读取、清洗、分析和导出。
零基础先记住一句话:
数据处理不是“会写 Pandas API”,而是把来源不稳定、格式不统一、质量不可信的原始数据,变成可统计、可追溯、可重复执行的可信数据。
如果只会 read_csv()、groupby(),但不知道字段类型、缺失值、重复数据、异常值、编码、业务主键和口径校验,最后很容易算出一份“看起来很精确、实际上完全错误”的报表。
学习目标
学完本页,你应该能回答:
- NumPy 和 Pandas 分别解决什么问题。
- 数据处理为什么必须先检查数据质量。
- 一份 CSV/Excel 从读取到导出完整经历哪些步骤。
- 缺失值、重复值、异常值、类型错误分别怎么处理。
- 为什么金额、日期、业务状态不能直接相信原始字段。
- 为什么脚本必须可重复、可审计,而不是在 Excel 里手动改。
- 能写一个可运行的商业数据清洗 Demo。
- 面试时能说清 Pandas 数据处理流程、常见坑和排查方法。
NumPy 和 Pandas 怎么分工
flowchart TD
A["原始业务数据"] --> B["Pandas 读取 CSV/Excel/SQL"]
B --> C["DataFrame 表格清洗"]
C --> D["Series 列计算"]
D --> E["NumPy 数组和向量化"]
C --> F["分组、聚合、透视、导出"]| 工具 | 主要对象 | 适合做什么 | 不适合做什么 |
|---|---|---|---|
| NumPy | ndarray | 大量数值计算、矩阵、向量化、科学计算 | 直接处理复杂业务表格和字段语义 |
| Pandas | DataFrame / Series | CSV/Excel/SQL 表格清洗、聚合、透视、导出 | 超大数据量分布式计算 |
| 原生 Python | list / dict / set | 小规模逻辑、校验、业务规则、脚本控制流 | 大规模列式计算 |
可以这样理解:
- NumPy 更靠近“计算引擎”。
- Pandas 更靠近“业务表格处理”。
- 商业数据清洗通常以 Pandas 为主,必要时借助 NumPy 做数值计算。
数据处理为什么不是简单 API 调用
假设你拿到一份订单 CSV:
order_id,city,amount,status,created_at
1001,北京,99.5,paid,2026-07-01
1002,上海,120元,paid,2026/07/01
1002,上海,120,paid,2026/07/01
1003,北京,,refund,unknown
1004,深圳,-80,paid,2026-07-02直接求和会有很多问题:
| 问题 | 如果不处理会怎样 |
|---|---|
120元 是字符串 | 金额列无法正确求和,或被当成 object |
order_id=1002 重复 | 销售额重复计算 |
amount 为空 | 直接填 0 可能掩盖脏数据 |
created_at=unknown | 日期聚合失败 |
amount=-80 | 订单金额为负可能是异常,也可能是退款口径 |
refund 和 paid 混在一起 | 不区分状态会把退款算进销售 |
所以数据处理要先问业务问题:
- 这份数据的主键是什么。
- 哪些字段必填。
- 哪些状态应该参与统计。
- 金额为空代表 0,还是代表数据缺失。
- 负数金额是异常还是业务含义。
- 重复订单保留第一条、最后一条,还是按更新时间判断。
标准处理流程
flowchart TD
A["拿到原始数据"] --> B["确认字段含义和业务口径"]
B --> C["读取文件并指定编码"]
C --> D["查看 shape、head、info"]
D --> E["字段类型转换"]
E --> F["缺失值分析"]
F --> G["重复值处理"]
G --> H["异常值处理"]
H --> I["业务规则过滤"]
I --> J["分组聚合和校验"]
J --> K["导出结果"]
K --> L["保存脚本、日志和处理报告"]每一步都不是形式主义。
| 步骤 | 为什么要做 | 不做会怎样 |
|---|---|---|
| 字段口径确认 | 知道每列代表什么 | 技术上算对,业务上算错 |
| 指定编码 | 防止中文乱码 | 城市、姓名、机构名乱码 |
| 查看结构 | 确认行数、列数、样例 | 不知道读入是否完整 |
| 类型转换 | 数字和日期才能正确计算 | 字符串求和、日期排序错误 |
| 缺失分析 | 判断缺失是正常还是脏数据 | 无脑填充导致结果失真 |
| 去重 | 防止重复计算 | 金额、数量、人数被放大 |
| 异常处理 | 排除或标记明显不合理数据 | 极端值拉歪统计结果 |
| 业务过滤 | 只统计符合口径的数据 | 把退款、取消、测试数据算进去 |
| 校验 | 对比总数、金额、样例 | 错误进入报表还不知道 |
| 保存脚本 | 可复现、可审计 | 下次数据来了只能手工重做 |
第一步:读取前先确认数据来源
商业数据通常来自:
| 来源 | 常见问题 |
|---|---|
| CSV | 编码、分隔符、引号、换行、字段类型 |
| Excel | 多 sheet、合并单元格、表头不在第一行 |
| 数据库 | SQL 口径、分页、时间范围、权限 |
| API | 超时、分页、重复拉取、字段缺失 |
| 日志 | 格式不稳定、字段嵌套、时间格式多样 |
读取前至少确认:
- 文件是否完整。
- 表头在哪一行。
- 编码是
utf-8、utf-8-sig还是gbk。 - 日期和金额字段格式。
- 是否包含测试数据。
- 是否有业务主键。
第二步:读取并做结构检查
import pandas as pd
df = pd.read_csv("orders_raw.csv", encoding="utf-8")
print("行列数:", df.shape)
print("前 5 行:")
print(df.head())
print("字段类型和非空数量:")
print(df.info())shape、head()、info() 是初学者必须养成的习惯。
| 方法 | 看什么 | 例子 |
|---|---|---|
df.shape | 行数和列数 | 是否少读、多读 |
df.head() | 样例数据 | 表头是否正确、字段是否错位 |
df.info() | 类型和非空数量 | 金额是否 object、日期是否 object |
df.describe() | 数值分布 | 最大值、最小值是否异常 |
df.isna().sum() | 缺失数量 | 哪些字段缺失严重 |
如果读取后发现所有列都挤在一列,可能是分隔符不对:
df = pd.read_csv("orders_raw.csv", sep=";", encoding="utf-8")如果中文乱码,尝试:
df = pd.read_csv("orders_raw.csv", encoding="gbk")第三步:字段类型转换
Pandas 读取 CSV 时不一定能猜对类型。金额、日期、ID、手机号尤其要谨慎。
金额转换
df["amount"] = (
df["amount"]
.astype(str)
.str.replace("元", "", regex=False)
.str.strip()
)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")errors="coerce" 表示转换失败变成缺失值。这样不会让整个脚本直接崩,但后面必须检查转换失败的数据。
bad_amount = df[df["amount"].isna()]
print("金额无法转换的行数:", len(bad_amount))
print(bad_amount[["order_id", "amount"]].head())日期转换
df["created_at"] = pd.to_datetime(df["created_at"], errors="coerce")日期转完后也要检查:
bad_date = df[df["created_at"].isna()]
print("日期无法转换的行数:", len(bad_date))ID 字段不要随便转数字
订单号、身份证号、手机号、资产编码这类字段即使看起来是数字,也经常应该保留字符串。
| 字段 | 为什么不建议转数字 |
|---|---|
| 手机号 | 可能有前导 0 或国际区号 |
| 身份证号 | 可能包含 X,且不需要数学计算 |
| 资产编码 | 编码不是数值,前导 0 有意义 |
| 订单号 | 太长时可能被科学计数法影响 |
第四步:缺失值处理
缺失值处理不是“看到空就填 0”。要先判断业务含义。
flowchart TD
A["发现缺失值"] --> B{"字段是否必填"}
B -- "必填" --> C["标记错误或剔除"]
B -- "非必填" --> D{"缺失是否有业务默认值"}
D -- "有" --> E["填默认值"]
D -- "没有" --> F["保留为空或标记 unknown"]
C --> G["输出错误报告"]
E --> H["继续处理"]
F --> H常见策略:
| 字段 | 处理建议 | 原因 |
|---|---|---|
| 业务主键缺失 | 剔除或进入错误文件 | 无法追溯和去重 |
| 金额缺失 | 通常不直接填 0 | 0 和未知不是一回事 |
| 备注缺失 | 可以填空字符串 | 不影响核心统计 |
| 年龄缺失 | 可填中位数或 unknown | 看分析目的 |
| 日期缺失 | 时间分析中通常剔除 | 无法归属周期 |
示例:
required_columns = ["order_id", "amount", "created_at"]
invalid_required = df[df[required_columns].isna().any(axis=1)]
valid_df = df.dropna(subset=required_columns).copy()第五步:重复值处理
重复不一定是完全重复。商业项目更常见的是“业务主键重复”。
duplicate_orders = df[df.duplicated(subset=["order_id"], keep=False)]
print(duplicate_orders.sort_values("order_id"))去重前要明确规则:
| 规则 | 适合场景 |
|---|---|
keep="first" | 第一条是原始记录,后续重复是误导入 |
keep="last" | 后一条代表更新后的状态 |
| 按更新时间排序后保留最新 | 有 updated_at 字段 |
| 不自动去重,输出错误报告 | 金额、状态冲突,需要人工核对 |
示例:按订单号保留最后一条。
df = df.sort_values("created_at")
df = df.drop_duplicates(subset=["order_id"], keep="last")如果重复行金额或状态不一致,不要静默去重,应输出错误报告。
第六步:异常值处理
异常值要结合业务判断。
negative_paid = df[(df["status"] == "paid") & (df["amount"] < 0)]
too_large = df[df["amount"] > 100_000]| 异常 | 可能含义 | 处理 |
|---|---|---|
| 已支付订单金额为负 | 脏数据或状态错误 | 输出错误报告 |
| 退款金额为负 | 可能合理 | 看退款口径 |
| 金额特别大 | 大客户订单或录入错误 | 抽样核对 |
| 日期在未来 | 预约订单或错误日期 | 看业务定义 |
| 状态不在枚举内 | 脏数据 | 标记错误 |
异常值不是一定删除。正确流程是:识别、标记、解释、按口径处理。
第七步:业务过滤和口径确认
比如统计“已支付销售额”,不能把所有订单都算进去。
paid_df = df[df["status"] == "paid"].copy()如果业务要求排除测试城市、测试用户、内部订单,也要明确写入脚本:
paid_df = paid_df[~paid_df["city"].isin(["测试城市"])]口径必须写清楚:
本报表销售额 = status 为 paid 的订单 amount 求和;
不包含 refund、cancelled、test 数据;
重复订单按 order_id 保留最后一条。否则不同人写出来的报表会对不上。
第八步:分组聚合
按城市统计订单数和销售额:
summary = (
paid_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)
)as_index=False 的好处是导出后城市仍然是普通列。
常用聚合:
| 需求 | 写法 |
|---|---|
| 计数 | count |
| 去重计数 | nunique |
| 求和 | sum |
| 平均值 | mean |
| 最大值 | max |
| 中位数 | median |
第九步:结果校验
导出之前必须校验。不要相信第一版结果。
raw_paid_total = paid_df["amount"].sum()
summary_total = summary["total_amount"].sum()
if abs(raw_paid_total - summary_total) > 0.0001:
raise ValueError("聚合前后金额不一致")还可以做:
| 校验 | 目的 |
|---|---|
| 聚合前后总金额一致 | 防止漏行或重复 |
| 分组数量和业务预期接近 | 防止城市字段异常 |
| 抽样核对明细 | 防止口径理解错 |
| 输出错误文件 | 让脏数据可追溯 |
| 对比历史报表 | 发现异常波动 |
第十步:导出和交付
summary.to_csv("city_amount_summary.csv", index=False, encoding="utf-8-sig")
invalid_required.to_csv("invalid_rows.csv", index=False, encoding="utf-8-sig")为什么导出用 utf-8-sig:
| 编码 | 说明 |
|---|---|
utf-8 | 通用编码,很多程序都支持 |
utf-8-sig | Excel 打开中文 CSV 更不容易乱码 |
gbk | 一些中文 Windows 老系统常见 |
交付时建议同时给:
- 汇总结果文件。
- 错误数据文件。
- 处理脚本。
- 口径说明。
- 执行日志。
商业 Demo:订单数据清洗和城市销售汇总
下面 Demo 使用内存字符串模拟 CSV,可以直接运行。真实项目只需要把 StringIO 换成文件路径。
from io import StringIO
import pandas as pd
RAW_CSV = """order_id,city,amount,status,created_at
1001,北京,99.5,paid,2026-07-01
1002,上海,120元,paid,2026/07/01
1002,上海,120,paid,2026/07/01
1003,北京,,refund,unknown
1004,深圳,-80,paid,2026-07-02
1005,广州,300,paid,2026-07-02
"""
def load_orders() -> pd.DataFrame:
return pd.read_csv(StringIO(RAW_CSV), dtype={"order_id": "string"})
def normalize_types(df: pd.DataFrame) -> pd.DataFrame:
result = df.copy()
result["amount_raw"] = result["amount"]
result["amount"] = (
result["amount"]
.astype(str)
.str.replace("元", "", regex=False)
.str.strip()
)
result["amount"] = pd.to_numeric(result["amount"], errors="coerce")
result["created_at"] = pd.to_datetime(result["created_at"], errors="coerce")
return result
def split_invalid_rows(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
required = ["order_id", "city", "amount", "created_at", "status"]
invalid_mask = (
df[required].isna().any(axis=1)
| ~df["status"].isin(["paid", "refund", "cancelled"])
| ((df["status"] == "paid") & (df["amount"] < 0))
)
invalid = df[invalid_mask].copy()
valid = df[~invalid_mask].copy()
return valid, invalid
def deduplicate_orders(df: pd.DataFrame) -> pd.DataFrame:
return (
df.sort_values("created_at")
.drop_duplicates(subset=["order_id"], keep="last")
.copy()
)
def summarize_city_sales(df: pd.DataFrame) -> pd.DataFrame:
paid_df = df[df["status"] == "paid"].copy()
return (
paid_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)
)
def main() -> None:
raw = load_orders()
typed = normalize_types(raw)
deduped = deduplicate_orders(typed)
valid, invalid = split_invalid_rows(deduped)
summary = summarize_city_sales(valid)
print("有效数据:")
print(valid)
print("\n异常数据:")
print(invalid)
print("\n城市汇总:")
print(summary)
if __name__ == "__main__":
main()这个 Demo 要看懂几件事:
| 代码 | 意义 |
|---|---|
dtype={"order_id": "string"} | 订单号按字符串处理,避免编码类字段被当数字 |
amount_raw | 保留原始金额,方便追溯 |
pd.to_numeric(..., errors="coerce") | 无法转换的金额变成缺失,再进入错误处理 |
pd.to_datetime(..., errors="coerce") | 无法转换的日期变成缺失 |
drop_duplicates | 按业务主键去重 |
split_invalid_rows | 不让脏数据直接进入汇总 |
groupby(...).agg(...) | 按城市统计多个指标 |
数据处理脚本怎么工程化
脚本不是写完一次就扔。商业项目建议至少有这些结构:
data_job/
input/
orders_raw.csv
output/
city_summary.csv
invalid_rows.csv
src/
clean_orders.py
tests/
test_clean_orders.py
README.md工程化要求:
| 要求 | 原因 |
|---|---|
| 参数化输入输出路径 | 不要把文件名写死 |
| 输出错误数据 | 脏数据要能反馈给业务 |
| 日志记录行数和耗时 | 排查任务是否完整执行 |
| 核心清洗函数可测试 | 口径变更时有回归保护 |
| README 写口径 | 别人知道你怎么算的 |
| 保留原始数据 | 方便复核,不污染源文件 |
数据处理常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
不看 info() 直接算 | 字符串金额、日期错误都不知道 | 先检查类型 |
| 空值全部填 0 | 把未知伪装成 0 | 按字段含义处理 |
| 完全重复去重 | 业务主键重复仍存在 | 按业务主键去重 |
| 静默删除异常行 | 业务不知道数据有问题 | 输出错误文件 |
| 只保存最终报表 | 无法审计处理过程 | 保存脚本、日志、口径 |
| 手工改 Excel | 不可复现、不可回滚 | 用脚本处理 |
| 大文件一次读入 | 内存爆掉 | 分块读取或换数据库/分布式工具 |
| 金额用 float 做精确财务 | 精度风险 | 财务结算用 Decimal 或数据库 decimal |
生产排查流程
数据报表对不上时,不要先改代码。按链路排查:
flowchart TD
A["报表结果不对"] --> B{"原始数据是否完整"}
B -- "否" --> C["检查导出 SQL、时间范围、分页"]
B -- "是" --> D{"读取行数是否正确"}
D -- "否" --> E["检查编码、分隔符、表头"]
D -- "是" --> F{"字段类型是否正确"}
F -- "否" --> G["检查金额、日期、ID 转换"]
F -- "是" --> H{"去重和过滤口径是否正确"}
H -- "否" --> I["检查主键、状态、测试数据"]
H -- "是" --> J{"聚合逻辑是否正确"}
J -- "否" --> K["检查 groupby、口径、单位"]
J -- "是" --> L["抽样对账和历史对比"]排查证据:
| 证据 | 用途 |
|---|---|
| 原始文件行数 | 确认数据是否完整 |
读取后 df.shape | 确认读取是否正确 |
| 缺失值统计 | 找到转换失败字段 |
| 重复主键数量 | 判断是否重复计算 |
| 异常数据文件 | 反馈数据质量问题 |
| 聚合前后总金额 | 校验汇总是否漏数据 |
| 样例明细 | 和业务人工核对 |
面试标准回答
Pandas 数据清洗的完整流程是什么?
可以这样答:
我会先确认字段含义和业务口径,再读取数据并检查 shape、head、info、缺失值和重复值。然后对金额、日期、状态等关键字段做类型转换和合法性校验,按业务主键去重,处理缺失值和异常值,再按明确口径过滤数据,最后 groupby 或 pivot 聚合并导出结果。导出前会做总数、总金额、样例抽查等校验,异常数据会单独输出,保证清洗过程可复现、可审计。NumPy 和 Pandas 区别是什么?
NumPy 核心是 ndarray,适合高效数值计算、矩阵和向量化;Pandas 核心是 Series 和 DataFrame,适合带行列标签的表格数据清洗、筛选、分组、聚合和导出。Pandas 很多底层计算依赖 NumPy,但它提供了更贴近业务表格的 API。
为什么数据处理不能只在 Excel 里手动改?
因为手工操作不可复现、不可审计、容易漏步骤,也无法自动处理新数据。Python 脚本可以把读取、清洗、校验、聚合、导出固化下来,后续数据来了可以重复执行,出错也能回看脚本和日志。
数据清洗中最容易出错的点是什么?
最常见的是字段类型错误、缺失值误填、重复数据没有按业务主键处理、异常值没有结合业务判断、过滤口径不清、聚合前后没有校验。生产数据处理的重点不是 API 背得多,而是能保证结果可信、过程可追溯。
关联知识点
| 知识点 | 作用 |
|---|---|
| NumPy 数组计算 | 理解数组、向量化、广播、dtype |
| Pandas 清洗分析 | 深入学习 DataFrame、缺失、重复、聚合 |
| 端到端实践 | 从原始 CSV 到报表输出 |
| Python 异常与文件 | 理解文件读取、编码和异常处理 |
| Python 日志与调试 | 让数据处理脚本可排查 |
| Python 测试 | 给清洗函数写回归测试 |
| Python 面试题 | 查看标准回答和追问 |
本章小结
Python 数据处理的核心不是“会 Pandas”,而是建立一条可信数据流水线:理解口径、读取数据、检查质量、转换类型、处理缺失和重复、识别异常、按业务规则过滤、聚合统计、校验结果、导出并保留处理过程。只有这条链路说得清、跑得通、能复现,数据结果才值得被业务使用。
