数据分析(01):Pandas 与 NumPy 实战——DataFrame、清洗、聚合、向量化性能
更新时间:2026-09-02。本文是
data-ai/data-analysis/第 01 篇,在数据分析索引下。Pandas 建立在 NumPy 之上,是 Python 数据分析的事实标准。本文不讲 API 大全,而是沿着"数据怎么进来、怎么洗干净、怎么算得快"这条主线,把最常用、最容易踩坑的部分讲透。
本文要回答的问题
- NumPy 的 ndarray 和 Pandas 的 DataFrame 是什么关系?为什么快?
- 数据读进来后,缺失值、重复值、类型错乱怎么处理?
- groupby 聚合、merge 关联的本质是什么,为什么会慢/会炸内存?
- 为什么不要在 DataFrame 上写
for循环?向量化、apply、numba 怎么选?
一、数据模型:ndarray 与 DataFrame
1. NumPy ndarray:同构的连续内存数组
Python list: NumPy ndarray:
[1, 2, 3, ...] [1, 2, 3, ...] ← 一整块连续内存
↓ 每个元素都是 ↓ 元素是原始 C 类型(int64/float64)
PyObject 指针 无逐元素对象开销
类型可混杂 dtype 统一
循环慢(解释执行) 向量化操作在 C 层执行ndarray 的三个关键属性:
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)
a.shape # (2, 3) 形状
a.dtype # int32 元素类型(决定内存与精度)
a.strides # (12, 4) 走到下一"行/列"要跨多少字节为什么快:a * 2 不是 Python 层逐元素循环,而是在 C 层对连续内存批量运算,无逐元素 PyObject 装箱、无解释器循环开销。同样对 100 万个数做运算,NumPy 通常比纯 Python 循环快 50~500 倍。
向量化与广播(broadcasting):
a = np.arange(12).reshape(3, 4)
a + 1 # 标量广播到每个元素
a + np.array([10, 20, 30, 40]) # (3,4) + (4,) → 沿行广播
a.mean(axis=0) # 沿轴聚合:axis=0 按列,axis=1 按行2. Pandas DataFrame:带标签的列式表
DataFrame = 若干"带索引的 Series"按列拼起来
index name age score
+------+---------+------+-------+
| 0 | Alice | 30 | 88.5 | 每列是一个 Series
| 1 | Bob | 25 | NaN | 底层通常是 ndarray
| 2 | Carol | 35 | 92.0 |
+------+---------+------+-------+
^ ^ ^ ^
行索引 列标签 同列同 dtype(int/str/float)import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [30, 25, 35],
"score": [88.5, np.nan, 92.0],
})
df["age"] # 取列 -> Series
df[["name","age"]] # 取多列 -> DataFrame
df.dtypes # 各列类型;object 通常意味着字符串或混杂
df.shape # (行数, 列数)核心心智模型:
- DataFrame 是列式的。整列运算快,逐行运算慢。选列、按列变换是正道。
- 索引(index)是带标签的行号。
loc按标签、iloc按位置,二者混用是最常见 bug。 - 同列同 dtype。一列里混了数字和字符串,dtype 会退化成
object,后续数值运算全部变慢甚至报错。
二、数据读写
# 读 CSV(实际工作里 90% 的入口)
df = pd.read_csv("orders.csv",
parse_dates=["order_time"], # 直接解析日期
dtype={"user_id": "int64", "city": "category"})
# 读 Parquet(列式压缩,大数据首选,比 CSV 快 10~100 倍)
df = pd.read_parquet("orders.parquet")
df.to_parquet("out.parquet", index=False)实战要点:
- 大数据优先 Parquet:列式存储 + 压缩 + 保留 dtype,读特定列只需
columns=[...],I/O 和内存都大幅下降。 read_csv用dtype=指定category(低基数字符串如城市、状态)能省一个数量级内存。chunksize=100_000可分块读超大 CSV,逐块处理后pd.concat。
三、数据清洗
清洗通常占数据分析 70% 的时间。三类高频问题:缺失、重复、类型。
1. 缺失值
df.isna().sum() # 每列缺失个数
df["score"] = df["score"].fillna(df["score"].mean()) # 数值用均值填
df = df.dropna(subset=["user_id"]) # 关键字段缺失直接丢
df["price"] = df["price"].ffill() # 时间序列用前值填注意:NaN 参与运算会"传染"(NaN + 1 = NaN),聚合时默认跳过(mean 忽略 NaN)。
2. 重复值
df.duplicated(subset=["order_id"]).sum()
df = df.drop_duplicates(subset=["order_id"], keep="last")
## 四、分组聚合 groupby
split-apply-combine 模型: 分组(split) 应用(apply) 合并(combine)
+--------------+
| 按 city 切分 | 每组算 mean/sum/ 拼回成
df ->| 成若干子表 |-> count/自定义函数 -> 汇总表 | group A/B/C | +--------------+
```python
# 单聚合
df.groupby("city")["score"].mean()
# 多列多聚合(agg 是最常用形态)
df.groupby("city").agg(
avg_score=("score", "mean"),
n=("user_id", "count"),
max_age=("age", "max"),
).reset_index()
# 多列分组
df.groupby(["city", "channel"])["amount"].sum().sort_values(ascending=False)
# transform:聚合结果广播回每一行(不压缩行数)
df["city_avg"] = df.groupby("city")["score"].transform("mean")性能要点:
groupby的分组键若是高基数字符串(如 user_id),先转category或用observed=True,避免笛卡尔展开炸内存。- 能用内置聚合(
mean/sum/count)就别用apply(lambda):内置走 C 路径,apply 走 Python 回调,差距可达数十倍。
五、merge / join
orders = pd.DataFrame({"order_id": [1,2,3], "user_id": [10,10,20]})
users = pd.DataFrame({"user_id": [10,20], "name": ["Alice","Bob"]})
m = orders.merge(users, on="user_id", how="left") # 左连接
# how: inner / left / right / outer本质是数据库 join。踩坑点:
- 键类型必须一致:一列 int、一列 str(
"10")会导致关联全空,且不报错。 - 右表键不唯一时行数会膨胀。先
users["user_id"].duplicated().any()检查。 - 大表 merge 前把关联键转成排序好的同 dtype,必要时
pd.merge(..., validate="many_to_one")让 Pandas 校验。
六、性能:为什么不能写 for 循环
1. 几种写法的速度阶梯
# 1) 最慢:iterrows 逐行 Python 循环(百万行可能几十秒~分钟)
for i, row in df.iterrows():
df.loc[i, "x2"] = row["x"] * 2
# 2) 中等:apply(仍是 Python 回调)
df["x2"] = df["x"].apply(lambda v: v * 2)
# 3) 快:向量化(C 层,推荐)
df["x2"] = df["x"] * 2
# 4) 快:numpy 数组级运算
df["x2"] = df["x"].to_numpy() * 2经验法则:能用整列向量化运算就不要 apply,能不写循环就不写。字符串用 .str.、日期用 .dt. 访问器,都是向量化的。
2. 条件赋值
import numpy as np
# 快:np.where / np.select
df["level"] = np.where(df["score"] >= 90, "A", "B")
df["grade"] = np.select(
[df["score"]>=90, df["score"]>=80, df["score"]>=60],
["优", "良", "中"], default="差")3. 内存与进一步提速
df["city"] = df["city"].astype("category") # 低基数串省内存
df["age"] = pd.to_numeric(df["age"], downcast="integer")- numba:对无法向量化的数值循环加
@numba.jit,编译成机器码,接近 C 速度。 - 分块:
chunksize流式处理,控制内存峰值。 - 换引擎:数据超内存量级,考虑 Polars(多线程、惰性查询)或 DuckDB(直接在 Parquet 上跑 SQL)。
七、常见坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
loc/iloc 混用 | 取错行还不报错 | loc 按标签、iloc 按位置 |
| 列 dtype 退化为 object | 数值运算慢/报错 | pd.to_numeric(errors="coerce") |
| merge 键类型不一致 | 关联结果全空 | 统一 dtype 后再 join |
inplace=True + 链式索引 | SettingWithCopyWarning、改了没生效 | 用 df = df.xxx() 显式赋值 |
| NaN 传染 | 结果莫名 NaN | 先 isna().sum() 体检 |
| 高基数字符串 groupby | 内存暴涨 | 转 category 或 observed=True |
小结
NumPy 用连续内存 + 同构 dtype + C 层向量化换来数量级提速;Pandas 在其上提供带标签的列式 DataFrame。高效数据分析的心法是:按列思考、向量化运算、用内置聚合、严控 dtype。数据进来先体检(dtypes / isna / duplicated),清洗用 coerce 统一转缺失,聚合用 groupby+agg,关联用 merge 并保证键一致;性能问题的第一解药永远是"删掉 for 循环、改成整列运算"。
下一篇:机器学习入门——把干净数据喂给模型,讲特征工程、经典模型、训练/验证/测试与偏差方差权衡。
### 3. 类型与字符串
```python
df["age"] = pd.to_numeric(df["age"], errors="coerce") # 转不了变 NaN
df["order_time"] = pd.to_datetime(df["order_time"], errors="coerce")
df["city"] = df["city"].str.strip().str.lower() # 字符串向量化
df = df[df["age"] >= 0] # 过滤非法值errors="coerce" 是关键:遇到脏数据不抛异常,而是转成缺失,后续统一处理。