R / Richie全部文章 ↑

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

参考