Python · 3 分钟阅读
Pandas 数据处理与转换
2026 年视角:Pandas 2.2+ 配合
pyarrow后端、链式 API(assign/pipe/query)、方法链做端到端数据清洗,是数据分析的"瑞士军刀"。Polars 在内存 / 性能上更胜一筹,但 Pandas 的生态优势仍在。
链式思维
assign / pipe / query 让你把数据流写成一串方法链,而不是一连串 df = ... 赋值:
result = (
df
.query("country == 'CN'")
.assign(revenue=lambda d: d["qty"] * d["price"])
.pipe(lower_cols)
.groupby("city", as_index=False)["revenue"].sum()
.sort_values("revenue", ascending=False)
)
链式的好处:读起来就是"数据流的图",新增/删除一步不影响其他。
1. 选 / 滤 / 改
# 选列
df[["name", "age"]]
# 过滤
df.query("age > 30 and city == 'BJ'")
df[df["age"].between(20, 40)] # 闭区间
df[df["city"].isin(["BJ", "SH"])]
df[df["email"].str.contains("@gmail")]
# 改值
df = df.assign(age=lambda d: d["age"].fillna(d["age"].median()))
df.loc[df["age"] < 0, "age"] = 0 # 范围外强制夹紧
2. 新增衍生列
df = df.assign(
is_adult=lambda d: d["age"] >= 18,
full_name=lambda d: d["first"] + " " + d["last"],
age_bucket=lambda d: pd.cut(d["age"], bins=[0, 18, 35, 60, 100],
labels=["kid", "young", "adult", "senior"]),
)
pd.cut 把连续值分桶,比手写 if-else 干净。
3. 类型转换
# 转可空整数 / 字符串(Pandas 2.x 推荐)
df["age"] = df["age"].astype("Int64")
df["name"] = df["name"].astype("string")
# 转日期
df["created"] = pd.to_datetime(df["created"], utc=True, errors="coerce")
# 转 category(节省内存 + 加速 groupby)
df["city"] = df["city"].astype("category")
Int64大写 I = 可空;int64小写 = 不允许 NaN。
4. 缺失值
df.isna().sum() # 每列缺失数
df.dropna(subset=["order_id"]) # 仅丢指定列缺失的行
df.fillna({"age": df["age"].median(),
"city": "UNKNOWN"})
df.ffill() # 沿轴向前填充
df.bfill() # 沿轴向后填充
5. 去重
df.drop_duplicates() # 完全重复
df.drop_duplicates(subset=["user_id"]) # 按业务键
df.drop_duplicates(subset=["user_id"], keep="last") # 保留最后一条
6. 字符串处理
df["email"] = df["email"].str.strip().str.lower()
df["domain"] = df["email"].str.split("@").str[1]
df["is_gmail"] = df["email"].str.endswith("@gmail.com")
.str 访问器是 vectorized 的,比 apply(lambda x: x.split(...)) 快。
7. apply / map / replace
# 字典映射
df["city_code"] = df["city"].map({"BJ": "010", "SH": "021", "GZ": "020"})
# 替换值
df["status"] = df["status"].replace({"Y": True, "N": False})
# apply:自定义函数(Python 级循环,慢)
df["age2"] = df["age"].apply(lambda x: x * 2)
apply能用 numpy 矢量化替代就别用。
8. 排序与排名
df.sort_values("age", ascending=False)
df.sort_values(["city", "age"], ascending=[True, False])
df["rank"] = df["score"].rank(method="dense", ascending=False)
rank(method="dense") 同分同名次,1, 2, 2, 3;"min" 是 1, 2, 2, 4。
9. 聚合
# 单聚合
df.groupby("city")["amount"].sum()
# 多聚合
df.groupby("city").agg(
total=("amount", "sum"),
n=("amount", "count"),
avg=("amount", "mean"),
)
# 多键聚合
df.groupby(["city", "category"])["amount"].sum().reset_index()
# 透视
pivot = df.pivot_table(
index="city", columns="month", values="amount", aggfunc="sum"
).fillna(0)
10. 合并
# SQL 风格
merged = pd.merge(
orders, users,
on="user_id", how="left",
suffixes=("_o", "_u"),
)
# 一对多
result = orders.merge(users, on="user_id")
# 按位置(不推荐)
pd.concat([df1, df2], ignore_index=True)
11. 时间序列
# 设为索引
df = df.set_index("created").sort_index()
# 重采样
df.resample("D")["amount"].sum() # 按天
df.resample("H")["amount"].mean() # 按小时
# 滚动窗口
df["amount_7d"] = df["amount"].rolling("7D").mean()
重采样前索引必须是单调递增的时间类型。
12. 实战:端到端清洗
import pandas as pd
raw = pd.read_csv("orders.csv", dtype_backend="pyarrow")
clean = (
raw
.query("status != 'CANCELLED' and amount >= 0")
.assign(
created=lambda d: pd.to_datetime(d["created"], utc=True, errors="coerce"),
city=lambda d: d["city"].astype("category"),
user_id=lambda d: d["user_id"].astype("Int64"),
)
.dropna(subset=["order_id", "user_id"])
.drop_duplicates(subset=["order_id"])
.pipe(lambda d: d.merge(users[["user_id", "segment"]], on="user_id", how="left"))
.groupby(["city", "segment"], as_index=False)["amount"].sum()
.sort_values("amount", ascending=False)
)
clean.to_parquet("clean_orders.parquet", engine="pyarrow")
13. 性能 / 内存
# 看每列内存占用
df.memory_usage(deep=True)
# 大文件分块
chunks = pd.read_csv("big.csv", chunksize=100_000)
out = []
for c in chunks:
out.append(filter_rows(c))
result = pd.concat(out, ignore_index=True)
大数据 + 性能敏感:考虑 Polars,API 接近但快 5–10 倍。Polars 的
lazy模式是杀手锏。
14. 易错点
- 链式赋值:
df[df.x > 0]["y"] = 1几乎一定不工作。改用df.loc[cond, "y"] = 1。 - inplace:2.x 已弃用。直接重新赋值。
- 行顺序:groupby / merge 不保证输出顺序,要
sort_values显式声明。 - 类型转换:Excel 读回的时间列通常是
object,记得pd.to_datetime。 - SettingWithCopyWarning:链式切片后赋值 = 写到了 view。改
df = df.copy()或用loc。
一些经验
- 链式 + 命名函数 > 一长串
df = df... - 大数据用 pyarrow 后端 + category dtype
- 性能敏感:Polars / DuckDB
- 业务报告:先 Parquet 落盘,最后一步才 Excel
- 复杂时间逻辑用
pyarrow.compute而不是apply