RSS
菜单
全部文章快讯开发科技深度热点

第 16 章 性能优化与实战案例

内容摘要

这是系列的最后一篇。前半部分讲 pandas 的性能优化技巧(向量化、内存优化、大数据处理),后半部分用一个完整的**实战项目**把前面 15 章的知识串起来:从读数据、清洗、分析到可视化出报告。

这是系列的最后一篇。前半部分讲 pandas 的性能优化技巧(向量化、内存优化、大数据处理),后半部分用一个完整的实战项目把前面 15 章的知识串起来:从读数据、清洗、分析到可视化出报告。

目录

1. 性能优化总原则

2. 向量化优先

3. 内存优化

4. 使用 category 与高效数据类型

5. 并行与大数据方案

6. 常见慢代码排查清单

7. 实战项目:电商销售数据分析报告

8. 写在最后:进阶学习路线


1. 性能优化总原则

pandas 性能优化的核心是减少 Python 层面的循环,尽量让运算发生在底层(C/NumPy 向量化)。

速度:向量化运算 > map/内置方法 > apply > 显式 for 循环

| 手段 | 相对速度 | 说明 |

|------|----------|------|

| 向量化(+ - * / >、内置方法) | ⭐⭐⭐⭐⭐ | 首选,没有之一 |

| 字典 map / replace | ⭐⭐⭐⭐ | 映射首选 |

| apply(逐列) | ⭐⭐⭐ | 复杂逻辑兜底 |

| apply(逐行 axis=1) | ⭐⭐ | 尽量避免 |

| for 循环 + loc 逐行 | ⭐ | 千万行数据会慢到无法接受 |


2. 向量化优先

2.1 用向量化代替循环

import pandas as pd
import numpy as np
import time

n = 1_000_000
df = pd.DataFrame({"a": np.random.rand(n), "b": np.random.rand(n)})

# ❌ 慢:循环逐行
def slow():
    result = []
    for i in range(n):
        if df.loc[i, "a"] > 0.5:
            result.append(df.loc[i, "a"] * df.loc[i, "b"])
        else:
            result.append(0)
    df["c_slow"] = result

# ✅ 快:向量化 + where/np.where
def fast():
    df["c_fast"] = np.where(df["a"] > 0.5, df["a"] * df["b"], 0)

t0 = time.time(); fast()
print(f"向量化: {time.time() - t0:.3f}s")

2.2 条件运算的向量化写法

# np.where(三目运算)
df["c"] = np.where(df["a"] > 0.5, "高", "低")

# 多条件
df["grade"] = np.select(
    [df["a"] > 0.8, df["a"] > 0.5, df["a"] > 0.2],   # 条件列表
    ["A", "B", "C"],                                   # 对应值
    default="D",
)

# 直接布尔运算(无需循环)
df["高且b大"] = (df["a"] > 0.5) & (df["b"] > 0.5)

2.3 用内置方法代替自定义函数

# ❌ 慢:apply 自定义
# df["x"] = df["col"].apply(lambda v: v * 2)

# ✅ 快:向量化
df["x"] = df["col"] * 2

# ❌ 慢:apply 算排名
# df["rank"] = df["col"].apply(lambda v: (df["col"] > v).sum())

# ✅ 快:内置 rank
df["rank"] = df["col"].rank()

# 用 str 方法代替 apply + re
# ❌ df["email"] = df["text"].apply(lambda s: re.findall(...))
# ✅ df["email"] = df["text"].str.extract(r"([\w.]+@[\w.]+)")

3. 内存优化

3.1 降低数值类型宽度

df = pd.DataFrame({"x": np.random.randint(0, 100, 1_000_000)})

# 查看内存
print(df.memory_usage(deep=True))
# x    8000000

# int64 -> int32 -> int16 逐级压缩
df["x"] = df["x"].astype("int16")
print(df.memory_usage(deep=True))
# x    2000000

一个自动优化函数:

def optimize_numeric(df):
    """把数值列压到最小合适类型"""
    for col in df.select_dtypes(include=["int64", "float64"]).columns:
        c_min, c_max = df[col].min(), df[col].max()
        if str(df[col].dtype) == "int64":
            if c_min >= -2**7 and c_max <= 2**7 - 1:
                df[col] = df[col].astype("int8")
            elif c_min >= -2**15 and c_max <= 2**15 - 1:
                df[col] = df[col].astype("int16")
            elif c_min >= -2**31 and c_max <= 2**31 - 1:
                df[col] = df[col].astype("int32")
        else:
            df[col] = df[col].astype("float32")
    return df

3.2 只读需要的列

# 读取时只拿需要的列(省内存 + 省时间)
df = pd.read_csv("big.csv", usecols=["id", "金额"])

3.3 及时释放内存

import gc

del df               # 删除引用
gc.collect()         # 触发垃圾回收

# 或者用 copy-on-write 语义下重新赋值
df = df[df["a"] > 0]      # 旧对象自然被回收

4. 使用 category 与高效数据类型

n = 1_000_000
df = pd.DataFrame({"city": np.random.choice(["北京", "上海", "广州", "深圳"], n)})

# object 内存
print(df.memory_usage(deep=True))
# city    8000064

# category 内存(少量类别时大幅压缩)
df["city"] = df["city"].astype("category")
print(df.memory_usage(deep=True))
# city    1000438   (约 1/8)

适合转 category 的场景:

  • 分类数远小于行数(如性别、城市、状态码);
  • 需要按分类做 groupby 或 value_counts。

注意事项:分类数接近行数(如 ID 列)时转 category 反而更占内存,不要盲目转。

# string 类型对文本更友好
df["code"] = df["code"].astype("string")

5. 并行与大数据方案

当单机 pandas 内存不够时,按数据量从小到大依次考虑:

| 方案 | 适合 | 工具 |

|------|------|------|

| 分块读取 + 聚合 | 单文件几 GB | pd.read_csv(chunksize=) |

| 列式存储 | 反复读取同一数据 | to_parquet |

| Polars | 想要更快 | pip install polars(API 兼容) |

| Dask | 超过内存 | dask.dataframe |

| 数据库 | 稳定查询 | SQLite/PostgreSQL + read_sql |

# 分块聚合示例
chunks = pd.read_csv("big.csv", chunksize=100_000)
total = 0
for chunk in chunks:
    total += chunk["金额"].sum()
print(total)

# 分块 + groupby 聚合
from collections import defaultdict
agg = defaultdict(float)
for chunk in pd.read_csv("big.csv", chunksize=100_000):
    for k, v in chunk.groupby("城市")["金额"].sum().items():
        agg[k] += v
print(dict(agg))

6. 常见慢代码排查清单

| 症状 | 常见原因 | 修复 |

|------|----------|------|

| 数据加载慢 | 全列读取、未指定 dtype | usecols、dtype、parse_dates |

| 逐行处理慢 | for + loc | 向量化、np.where、apply |

| apply 极慢 | axis=1 大表逐行 | 尽量向量化或分组处理 |

| groupby 慢 | 未用内置 agg | agg("sum") 代替 apply(lambda) |

| 字符串处理慢 | 正则逐条 | .str 向量化方法 |

| 内存爆 | object 列多、int64 宽 | category + 降位 + 删列 |

| 拼接慢 | 循环里 concat 小表 | 先收集列表,最后一次性 concat |

经典错误示范与修复:

# ❌ 循环里拼接(极慢)
# for f in files:
#     df = pd.concat([df, pd.read_csv(f)])

# ✅ 先收集再一次性拼接(快)
frames = [pd.read_csv(f) for f in files]
df = pd.concat(frames, ignore_index=True)
# ❌ groupby 用 apply lambda
# df.groupby("g")["v"].apply(lambda x: x.sum())

# ✅ 内置 agg
df.groupby("g")["v"].agg("sum")

7. 实战项目:电商销售数据分析报告

把前 15 章的知识全部用上,完成一个端到端的数据分析项目。

7.1 数据准备(模拟)

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
plt.rcParams["axes.unicode_minus"] = False
np.random.seed(2025)

# 模拟 5000 条订单
n = 5000
orders = pd.DataFrame({
    "订单号": [f"O{i:06d}" for i in range(1, n + 1)],
    "日期": np.random.choice(pd.date_range("2025-01-01", "2025-06-30"), n),
    "区域": np.random.choice(["华东", "华南", "华北", "西南", "东北"], n, p=[0.3, 0.25, 0.2, 0.15, 0.1]),
    "品类": np.random.choice(["数码", "服装", "食品", "家居", "美妆"], n),
    "数量": np.random.randint(1, 10, n),
    "单价": np.random.choice([99, 199, 299, 499, 799, 1299], n),
    "客户评分": np.random.randint(1, 6, n),
})
orders.loc[np.random.choice(n, 50), "客户评分"] = np.nan   # 制造缺失
orders = orders.sort_values("日期").reset_index(drop=True)

7.2 数据清洗与探索

print("=== 基本信息 ===")
print(orders.shape)
print(orders.info())

print("\n=== 缺失值 ===")
print(orders.isna().sum())

# 处理缺失:客户评分用中位数填充
orders["客户评分"] = orders["客户评分"].fillna(orders["客户评分"].median())

# 新增金额列
orders["金额"] = orders["数量"] * orders["单价"]

# 提取月份、周几
orders["月份"] = orders["日期"].dt.month
orders["周几"] = orders["日期"].dt.day_name()

7.3 业务分析

# 1. 月度销售趋势
monthly = orders.groupby("月份")["金额"].sum()
print("=== 月度销售 ===")
print(monthly)

# 2. 区域销售占比
area = orders.groupby("区域")["金额"].sum().sort_values(ascending=False)
print("\n=== 区域销售 ===")
print(area)

# 3. 品类销量与销售额
cat = orders.groupby("品类").agg(
    订单数=("订单号", "count"),
    总销量=("数量", "sum"),
    总金额=("金额", "sum"),
).sort_values("总金额", ascending=False)
print("\n=== 品类分析 ===")
print(cat)

# 4. 客单价(平均每单金额)
avg_order = orders["金额"].mean()
print(f"\n客单价: {avg_order:.2f}")

# 5. 客户满意度(平均评分)与金额关系
score_amount = orders.groupby("客户评分")["金额"].mean()
print("\n=== 评分与金额 ===")
print(score_amount)

7.4 可视化报告

fig, axes = plt.subplots(2, 2, figsize=(14, 9))

# 月度趋势
monthly.plot(kind="line", marker="o", ax=axes[0, 0], title="月度销售额趋势", color="#2E86AB")

# 区域占比
area.plot(kind="pie", autopct="%.1f%%", ax=axes[0, 1], title="区域销售占比")
axes[0, 1].set_ylabel("")

# 品类对比
cat["总金额"].plot(kind="bar", ax=axes[1, 0], title="品类销售额", color="steelblue")
axes[1, 0].set_xticklabels(axes[1, 0].get_xticklabels(), rotation=0)

# 周几表现
orders.groupby("周几")["金额"].sum().reindex(
    ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
).plot(kind="bar", ax=axes[1, 1], title="每周各天销售", color="coral")
axes[1, 1].set_xticklabels(axes[1, 1].get_xticklabels(), rotation=45)

plt.tight_layout()
plt.savefig("销售分析报告.png", dpi=150)
plt.show()

7.5 输出结论要点(示例)

=== 结论示例 ===
1. 销售整体呈现上升趋势,6 月达到峰值;
2. 华东区域贡献约 30% 销售额,是核心市场;
3. 数码品类销售额最高,其次为家居;
4. 客单价约 X 元,评分 4-5 分的订单金额显著更高,说明好评客户消费力强;
5. 周末(周六、周日)销售额明显高于工作日。

(上述数字请以实际运行结果为准)

7.6 导出结果

# 汇总表导出
with pd.ExcelWriter("销售分析结果.xlsx") as writer:
    monthly.to_frame("销售额").to_excel(writer, sheet_name="月度")
    area.to_frame("销售额").to_excel(writer, sheet_name="区域")
    cat.to_excel(writer, sheet_name="品类")
    orders.head(1000).to_excel(writer, sheet_name="明细", index=False)

8. 写在最后:进阶学习路线

到这里,你已经掌握了 pandas 的核心知识全貌。想继续进阶,建议按此路线:

8.1 掌握程度自测

| 模块 | 自测问题 |

|------|----------|

| 数据结构 | 能说清 Series 与 DataFrame 区别吗? |

| 索引 | loc/iloc/布尔索引随手就用吗? |

| 清洗 | 缺失值、重复值、类型转换熟练吗? |

| 变换 | map/apply/transform 何时用哪个? |

| 合并 | inner/left/outer 结果想清楚了吗? |

| 重塑 | melt/pivot 互转顺手吗? |

| 分组 | agg/transform/filter 组合玩得转吗? |

| 时间序列 | resample/rolling/shift 会了吗? |

| 性能 | 能避免 apply 滥用吗? |

8.2 推荐继续学习的方向

1. Polars:新一代高性能 DataFrame 库,API 与 pandas 高度兼容;

2. Dask / Modin:pandas 的分布式/并行替代;

3. Seaborn / Plotly / ECharts:更专业的可视化;

4. scikit-learn:把 DataFrame 接入机器学习;

5. 数据库:学会 SQL,与 pandas 双剑合璧;

6. 官方文档:pandas.pydata.org 的 User Guide 是最终权威。

8.3 学习建议

  • 每个例子都要亲手敲一遍,观察输出;
  • 用自己手头的数据做项目,比刷教程有效 10 倍;
  • 遇到报错先看最后一行错误信息,再查文档;
  • 多用 df.info()、type(obj) 确认对象类型;
  • 善用 help() 和 pd.__version__。

全系列目录

| 文章 | 主题 |

|------|------|

| 01 | pandas 入门与环境搭建 |

| 02 | Series 详解 |

| 03 | DataFrame 详解 |

| 04 | 数据读取与写入 |

| 05 | 数据查看与探索 |

| 06 | 数据选择与索引 |

| 07 | 缺失值与重复值处理 |

| 08 | 数据类型与转换 |

| 09 | 文本数据处理 |

| 10 | 数据变换:map / apply / applymap |

| 11 | 数据合并:concat / merge / join |

| 12 | 数据重塑与透视表 |

| 13 | 分组聚合:groupby |

| 14 | 时间序列分析 |

| 15 | 数据可视化 |

| 16 | 性能优化与实战案例 |

祝学习顺利!

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 16 章 性能优化与实战案例》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。