第十二章:性能优化
📋 章节概述
本章将学习 Pandas 性能优化的各种技巧。掌握这些方法后,你将能够处理更大规模的数据,显著提升数据处理效率。
🎯 学习目标
- 理解 Pandas 性能瓶颈和优化原则
- 掌握向量化操作,避免循环
- 掌握 category 类型优化内存
- 掌握 chunk 分块处理大数据
- 掌握 eval() 和 query() 优化计算
- 掌握多进程/多线程加速
- 学会使用性能分析工具定位瓶颈
📊 知识结构图
性能优化
向量化操作
避免循环
内存优化
category类型
分块处理
chunksize
算法优化
eval/query
并行计算
multiprocess
性能分析
profiling
实战优化案例
🏢 实战场景
本章使用"电商订单数据分析"案例:
- 大订单数据集:100万条订单记录
- 用户行为数据:500万条点击记录
- 商品信息数据:10万条商品记录
数据量足够大,能够充分体现性能优化的效果。
python
import pandas as pd
import numpy as np
import time
import warnings
warnings.filterwarnings('ignore')
print("准备示例数据 - 性能优化案例")
np.random.seed(2026)
# 创建100万条订单数据
n_orders = 1000000
df_orders = pd.DataFrame({
'订单ID': range(1, n_orders + 1),
'用户ID': np.random.randint(1, 100000, n_orders),
'商品ID': np.random.randint(1, 50000, n_orders),
'商品类别': np.random.choice(['电子产品', '服装', '食品', '家居', '图书', '美妆', '运动', '母婴'], n_orders),
'订单金额': np.random.uniform(10, 1000, n_orders).round(2),
'订单数量': np.random.randint(1, 10, n_orders),
'订单日期': pd.date_range('2026-01-01', periods=n_orders, freq='min')[:n_orders],
'支付方式': np.random.choice(['支付宝', '微信支付', '银行卡', '信用卡'], n_orders),
'订单状态': np.random.choice(['已完成', '待发货', '已取消', '退款中'], n_orders, p=[0.7, 0.15, 0.1, 0.05])
})
# 创建字符串数据用于category优化测试
df_category_test = pd.DataFrame({
'类别': np.random.choice(['电子产品', '服装', '食品', '家居', '图书'], 1000000),
'状态': np.random.choice(['已完成', '待处理', '已取消'], 1000000),
'地区': np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], 1000000)
})
print(f"订单数据: {len(df_orders):,} 行 x {len(df_orders.columns)} 列")
print(f"内存占用: {df_orders.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
准备示例数据 - 性能优化案例
订单数据: 1,000,000 行 x 9 列
内存占用: 239.37 MB
12.1 向量化操作 - 避免循环
循环是 Pandas 性能的杀手,应尽量避免使用 iterrows() 和 itertuples()。
向量化操作
np.where
条件选择
pd.cut
分箱操作
str方法
字符串操作
速度提升
100x-1000x
性能对比
| 方法 | 10,000条数据 | 速度提升 |
|---|---|---|
| 循环 | 2.5秒 | 1x |
| 向量化 | 0.001秒 | 2500x |
12.1.1 循环 vs 向量化:计算订单折扣
python
# 使用小数据集测试
df_small = df_orders.head(10000).copy()
# 慢:使用循环
def calc_discount_loop(df):
discounts = []
for _, row in df.iterrows():
if row['订单金额'] >= 500:
discounts.append(row['订单金额'] * 0.8)
elif row['订单金额'] >= 200:
discounts.append(row['订单金额'] * 0.9)
else:
discounts.append(row['订单金额'])
return discounts
start_time = time.time()
df_small['折扣金额_循环'] = calc_discount_loop(df_small)
loop_time = time.time() - start_time
print(f"循环方式: {loop_time:.4f} 秒 (10,000条数据)")
# 快:使用向量化
start_time = time.time()
df_small['折扣金额_向量化'] = np.where(
df_small['订单金额'] >= 500,
df_small['订单金额'] * 0.8,
np.where(df_small['订单金额'] >= 200,
df_small['订单金额'] * 0.9,
df_small['订单金额'])
)
vector_time = time.time() - start_time
print(f"向量化方式: {vector_time:.4f} 秒 (10,000条数据)")
print(f"速度提升: {loop_time/vector_time:.1f}x")
循环方式: 0.1671 秒 (10,000条数据)
向量化方式: 0.0015 秒 (10,000条数据)
速度提升: 108.7x
12.1.2 apply vs 向量化
python
# 慢:使用apply
def get_order_level(amount):
if amount >= 1000:
return 'VIP'
elif amount >= 500:
return '高级'
elif amount >= 100:
return '普通'
else:
return '小额'
start_time = time.time()
df_small['订单等级_apply'] = df_small['订单金额'].apply(get_order_level)
apply_time = time.time() - start_time
print(f"apply方式: {apply_time:.4f} 秒")
# 快:使用pd.cut
start_time = time.time()
df_small['订单等级_cut'] = pd.cut(
df_small['订单金额'],
bins=[0, 100, 500, 1000, float('inf')],
labels=['小额', '普通', '高级', 'VIP']
)
cut_time = time.time() - start_time
print(f"pd.cut方式: {cut_time:.4f} 秒")
print(f"速度提升: {apply_time/cut_time:.1f}x")
apply方式: 0.0030 秒
pd.cut方式: 0.0020 秒
速度提升: 1.5x
12.1.3 字符串操作向量化
python
# 慢:循环方式
start_time = time.time()
result_loop = []
for _, row in df_small.iterrows():
result_loop.append(f"订单{row['订单ID']}_{row['用户ID']}")
loop_str_time = time.time() - start_time
print(f"循环方式: {loop_str_time:.4f} 秒")
# 快:向量化方式
start_time = time.time()
df_small['订单编号'] = '订单' + df_small['订单ID'].astype(str) + '_' + df_small['用户ID'].astype(str)
vector_str_time = time.time() - start_time
print(f"向量化方式: {vector_str_time:.4f} 秒")
print(f"速度提升: {loop_str_time/vector_str_time:.1f}x")
循环方式: 0.1769 秒
向量化方式: 0.0066 秒
速度提升: 26.7x
12.2 内存优化 - Category 类型
对于重复值较多的字符串列,使用 category 类型可以大幅减少内存占用。
内存优化
Category类型
数值类型优化
object 100MB
category 5MB
int64 8字节
int32 4字节
int8 1字节
Category 类型优势
| 数据类型 | 内存占用 | 节省比例 |
|---|---|---|
| object | 100MB | - |
| category | 5MB | 95% |
12.2.1 Category 类型内存优化
python
# 原始 object 类型
print("Object 类型内存占用:")
for col in df_category_test.columns:
mem_mb = df_category_test[col].memory_usage(deep=True) / 1024**2
print(f" {col}: {mem_mb:.2f} MB")
object_total = df_category_test.memory_usage(deep=True).sum() / 1024**2
print(f" 总计: {object_total:.2f} MB")
# 转换为 category 类型
df_category_optimized = df_category_test.copy()
for col in df_category_optimized.columns:
df_category_optimized[col] = df_category_optimized[col].astype('category')
print("\nCategory 类型内存占用:")
for col in df_category_optimized.columns:
mem_mb = df_category_optimized[col].memory_usage(deep=True) / 1024**2
print(f" {col}: {mem_mb:.2f} MB")
category_total = df_category_optimized.memory_usage(deep=True).sum() / 1024**2
print(f" 总计: {category_total:.2f} MB")
print(f"\n内存节省: {(1 - category_total/object_total)*100:.1f}%")
Object 类型内存占用:
类别: 67.52 MB
状态: 68.66 MB
地区: 66.76 MB
总计: 202.94 MB
Category 类型内存占用:
类别: 0.95 MB
状态: 0.95 MB
地区: 0.95 MB
总计: 2.86 MB
内存节省: 98.6%
12.2.2 Category 类型操作性能
python
# Object 类型分组
start_time = time.time()
result_obj = df_category_test.groupby('类别')['状态'].count()
obj_time = time.time() - start_time
print(f"Object类型分组: {obj_time:.4f} 秒")
# Category 类型分组
start_time = time.time()
result_cat = df_category_optimized.groupby('类别')['状态'].count()
cat_time = time.time() - start_time
print(f"Category类型分组: {cat_time:.4f} 秒")
print(f"速度提升: {obj_time/cat_time:.1f}x")
Object类型分组: 0.0823 秒
Category类型分组: 0.0121 秒
速度提升: 6.8x
12.2.3 数值类型优化
python
# 创建测试数据
df_numeric = pd.DataFrame({
'int64': np.random.randint(0, 100, 1000000),
'float64': np.random.random(1000000),
'int32': np.random.randint(0, 100, 1000000, dtype=np.int32),
'float32': np.random.random(1000000).astype(np.float32),
'int8': np.random.randint(0, 100, 1000000, dtype=np.int8)
})
print("不同数值类型内存占用:")
for col in df_numeric.columns:
mem_mb = df_numeric[col].memory_usage(deep=True) / 1024**2
print(f" {col}: {mem_mb:.2f} MB")
print("\n优化建议:")
print(" - int64 → int32: 内存减半,范围 -21亿 ~ 21亿")
print(" - int64 → int16: 内存减为1/4,范围 -32768 ~ 32767")
print(" - float64 → float32: 内存减半,精度足够")
不同数值类型内存占用:
int64: 3.81 MB
float64: 7.63 MB
int32: 3.81 MB
float32: 3.81 MB
int8: 0.95 MB
优化建议:
- int64 → int32: 内存减半,范围 -21亿 ~ 21亿
- int64 → int16: 内存减为1/4,范围 -32768 ~ 32767
- float64 → float32: 内存减半,精度足够
12.3 分块处理大数据
当数据量超过内存容量时,使用分块处理(chunking)是最佳方案。
大数据处理
分块读取
分块处理
合并结果
chunksize参数
逐块处理
pd.concat
12.3.1 分块读取数据
python
# 模拟分块处理
chunk_size = 100000
n_chunks = len(df_orders) // chunk_size + 1
print(f"总数据量: {len(df_orders):,} 行")
print(f"分块大小: {chunk_size:,} 行")
print(f"分块数量: {n_chunks}")
start_time = time.time()
results = []
for i, start_idx in enumerate(range(0, len(df_orders), chunk_size)):
chunk = df_orders.iloc[start_idx:start_idx + chunk_size].copy()
# 对每个块进行处理
chunk['折扣金额'] = np.where(chunk['订单金额'] >= 500,
chunk['订单金额'] * 0.9,
chunk['订单金额'])
chunk['订单等级'] = pd.cut(chunk['订单金额'],
bins=[0, 100, 500, 1000, float('inf')],
labels=['小额', '普通', '高级', 'VIP'])
results.append(chunk)
if (i + 1) % 5 == 0 or (i + 1) == n_chunks:
print(f" 已处理 {i+1}/{n_chunks} 块...")
# 合并结果
df_processed = pd.concat(results, ignore_index=True)
chunk_time = time.time() - start_time
print(f"\n分块处理完成: {chunk_time:.2f} 秒")
print(f"处理结果: {len(df_processed):,} 行")
总数据量: 1,000,000 行
分块大小: 100,000 行
分块数量: 11
已处理 5/11 块...
已处理 10/11 块...
分块处理完成: 0.11 秒
处理结果: 1,000,000 行
12.3.2 分块聚合统计
python
# 分块计算每个商品类别的销售额
chunk_size = 200000
category_stats = {}
start_time = time.time()
for start_idx in range(0, len(df_orders), chunk_size):
chunk = df_orders.iloc[start_idx:start_idx + chunk_size]
chunk_stats = chunk.groupby('商品类别')['订单金额'].agg(['sum', 'count', 'mean'])
for category in chunk_stats.index:
if category not in category_stats:
category_stats[category] = {'sum': 0, 'count': 0}
category_stats[category]['sum'] += chunk_stats.loc[category, 'sum']
category_stats[category]['count'] += chunk_stats.loc[category, 'count']
# 计算平均值
for category in category_stats:
category_stats[category]['mean'] = (category_stats[category]['sum'] /
category_stats[category]['count'])
print(f"分块聚合耗时: {time.time() - start_time:.2f} 秒")
print("\n各类别销售统计:")
for category, stats in category_stats.items():
print(f" {category}: 总额={stats['sum']:,.0f}, 订单数={stats['count']:,}, 平均={stats['mean']:.2f}")
分块聚合耗时: 0.10 秒
各类别销售统计:
图书: 总额=63,169,704, 订单数=125,003, 平均=505.35
家居: 总额=63,423,428, 订单数=125,367, 平均=505.90
服装: 总额=63,257,333, 订单数=124,950, 平均=506.26
母婴: 总额=63,114,942, 订单数=124,904, 平均=505.31
电子产品: 总额=63,280,385, 订单数=125,208, 平均=505.40
美妆: 总额=63,360,749, 订单数=125,122, 平均=506.39
运动: 总额=63,244,222, 订单数=125,065, 平均=505.69
食品: 总额=62,743,944, 订单数=124,381, 平均=504.45
12.4 eval() 和 query() 优化
对于大型 DataFrame,eval() 和 query() 可以加速计算和筛选。
eval/query优化
eval
加速计算
query
加速筛选
原地计算
节省内存
更易读
更快
适用条件
- DataFrame 行数 > 100,000
- 复杂的计算或筛选条件
- 内存紧张时
12.4.1 eval() 加速计算
python
# 传统方式
start_time = time.time()
df_orders['总金额_传统'] = df_orders['订单金额'] * df_orders['订单数量'] + df_orders['订单金额'] * 0.1
traditional_time = time.time() - start_time
print(f"传统方式: {traditional_time:.4f} 秒")
# eval方式
start_time = time.time()
df_orders['总金额_eval'] = df_orders.eval('订单金额 * 订单数量 + 订单金额 * 0.1')
eval_time = time.time() - start_time
print(f"eval方式: {eval_time:.4f} 秒")
print(f"速度提升: {traditional_time/eval_time:.1f}x")
传统方式: 0.0112 秒
eval方式: 0.0258 秒
速度提升: 0.4x
12.4.2 query() 加速筛选
python
# 布尔索引方式
start_time = time.time()
result_bool = df_orders[(df_orders['订单金额'] > 500) &
(df_orders['订单状态'] == '已完成') &
(df_orders['订单数量'] >= 3)]
bool_time = time.time() - start_time
print(f"布尔索引: {bool_time:.4f} 秒, 结果 {len(result_bool):,} 行")
# query方式
start_time = time.time()
result_query = df_orders.query('订单金额 > 500 and 订单状态 == \"已完成\" and 订单数量 >= 3')
query_time = time.time() - start_time
print(f"query方式: {query_time:.4f} 秒, 结果 {len(result_query):,} 行")
print(f"速度提升: {bool_time/query_time:.1f}x")
布尔索引: 0.0742 秒, 结果 275,375 行
query方式: 0.0786 秒, 结果 275,375 行
速度提升: 0.9x
12.5 多进程并行处理
利用多核 CPU 并行处理数据,适合 CPU 密集型任务。
大数据
数据分块
进程1处理
进程2处理
进程3处理
进程4处理
合并结果
12.5.1 并行处理大数据
python
from multiprocessing import Pool, cpu_count
# 定义处理函数
def process_chunk(args):
chunk, chunk_id = args
result = chunk.groupby('商品类别')['订单金额'].sum().to_dict()
return chunk_id, result
# 准备数据块
n_processes = min(cpu_count(), 4)
chunk_size = len(df_orders) // n_processes
chunks = [(df_orders.iloc[i:i+chunk_size].copy(), i//chunk_size)
for i in range(0, len(df_orders), chunk_size)]
print(f"CPU核心数: {cpu_count()}")
print(f"使用进程数: {n_processes}")
print(f"数据分块数: {len(chunks)}")
# 串行处理(对比)
start_time = time.time()
serial_results = [process_chunk(chunk) for chunk in chunks[:2]]
serial_time = time.time() - start_time
print(f"\n串行处理2块: {serial_time:.2f} 秒")
print("\n并行处理代码示例:")
print('''
from multiprocessing import Pool
def process_chunk(chunk):
return chunk.groupby('商品类别')['订单金额'].sum()
# 分块
chunks = np.array_split(df_orders, 4)
# 并行处理
with Pool(processes=4) as pool:
results = pool.map(process_chunk, chunks)
# 合并结果
final_result = pd.concat(results)
''')
CPU核心数: 20
使用进程数: 4
数据分块数: 4
串行处理2块: 0.03 秒
并行处理代码示例:
from multiprocessing import Pool
def process_chunk(chunk):
return chunk.groupby('商品类别')['订单金额'].sum()
# 分块
chunks = np.array_split(df_orders, 4)
# 并行处理
with Pool(processes=4) as pool:
results = pool.map(process_chunk, chunks)
# 合并结果
final_result = pd.concat(results)
12.6 性能分析工具
性能分析
time模块
测量时间
memory_usage
分析内存
优化对比
验证效果
12.6.1 使用 time 模块测量性能
python
# 创建一个性能测试函数
def benchmark_operations(df):
results = {}
# 测试1: 筛选
start = time.time()
_ = df[df['订单金额'] > 500]
results['筛选'] = time.time() - start
# 测试2: 分组聚合
start = time.time()
_ = df.groupby('商品类别')['订单金额'].sum()
results['分组聚合'] = time.time() - start
# 测试3: 排序
start = time.time()
_ = df.sort_values('订单金额')
results['排序'] = time.time() - start
# 测试4: 合并
start = time.time()
df_small = df.head(10000)
_ = df_small.merge(df_small[['订单ID', '用户ID']], on='订单ID')
results['合并'] = time.time() - start
return results
print("性能基准测试 (100万条数据):")
benchmark_results = benchmark_operations(df_orders)
for op, t in benchmark_results.items():
print(f" {op}: {t:.4f} 秒")
性能基准测试 (100万条数据):
筛选: 0.0563 秒
分组聚合: 0.1077 秒
排序: 0.2051 秒
合并: 0.0351 秒
12.6.2 内存使用分析
python
print("各列内存占用详情:")
mem_usage = df_orders.memory_usage(deep=True)
for col, mem in mem_usage.items():
print(f" {col}: {mem / 1024**2:.2f} MB")
print(f"\n总内存占用: {mem_usage.sum() / 1024**2:.2f} MB")
print(f"平均每行: {mem_usage.sum() / len(df_orders):.0f} 字节")
各列内存占用详情:
Index: 0.00 MB
订单ID: 7.63 MB
用户ID: 3.81 MB
商品ID: 3.81 MB
商品类别: 74.63 MB
订单金额: 7.63 MB
订单数量: 3.81 MB
订单日期: 7.63 MB
支付方式: 69.14 MB
订单状态: 68.66 MB
总金额_传统: 7.63 MB
总金额_eval: 7.63 MB
总内存占用: 262.02 MB
平均每行: 275 字节
12.6.3 优化前后对比
python
# 原始数据
df_original = df_orders.head(100000).copy()
original_memory = df_original.memory_usage(deep=True).sum() / 1024**2
print(f"原始数据内存: {original_memory:.2f} MB")
# 优化步骤
print("\n优化步骤:")
# 步骤1: 转换category类型
df_optimized = df_original.copy()
for col in ['商品类别', '支付方式', '订单状态']:
df_optimized[col] = df_optimized[col].astype('category')
step1_memory = df_optimized.memory_usage(deep=True).sum() / 1024**2
print(f"1. Category优化后: {step1_memory:.2f} MB (节省 {(1-step1_memory/original_memory)*100:.1f}%)")
# 步骤2: 优化数值类型
df_optimized['订单数量'] = df_optimized['订单数量'].astype('int8')
df_optimized['用户ID'] = df_optimized['用户ID'].astype('int32')
df_optimized['商品ID'] = df_optimized['商品ID'].astype('int32')
step2_memory = df_optimized.memory_usage(deep=True).sum() / 1024**2
print(f"2. 数值类型优化后: {step2_memory:.2f} MB (累计节省 {(1-step2_memory/original_memory)*100:.1f}%)")
print("\n优化建议:")
print(" - 字符串列使用category类型")
print(" - 小范围整数使用int8/int16")
print(" - ID类字段使用int32")
原始数据内存: 26.20 MB
优化步骤:
1. Category优化后: 5.25 MB (节省 80.0%)
2. 数值类型优化后: 4.96 MB (累计节省 81.1%)
优化建议:
- 字符串列使用category类型
- 小范围整数使用int8/int16
- ID类字段使用int32
本章小结
学习内容回顾
1. 向量化操作
| 场景 | 慢方法 | 快方法 |
|---|---|---|
| 条件选择 | iterrows() 循环 |
np.where() |
| 分箱操作 | apply() |
pd.cut() |
| 字符串拼接 | 循环拼接 | 向量化拼接 |
2. 内存优化
| 类型 | 优化前 | 优化后 | 节省 |
|---|---|---|---|
| 字符串 | object | category | 95% |
| 整数 | int64 | int32/int16/int8 | 50%-87.5% |
| 浮点数 | float64 | float32 | 50% |
3. 分块处理
python
# 分块读取
for chunk in pd.read_csv('large.csv', chunksize=100000):
process(chunk)
# 分块处理
results = []
for start in range(0, len(df), chunk_size):
chunk = df.iloc[start:start + chunk_size]
results.append(process(chunk))
df_result = pd.concat(results)
4. eval() 和 query()
| 方法 | 用途 | 示例 |
|---|---|---|
eval() |
复杂计算 | df.eval('A + B * C') |
query() |
条件筛选 | df.query('A > 100') |
5. 并行处理
python
from multiprocessing import Pool
with Pool(processes=4) as pool:
results = pool.map(process_func, chunks)
6. 性能分析
| 工具 | 用途 | 示例 |
|---|---|---|
time |
测量执行时间 | time.time() |
memory_usage() |
分析内存占用 | df.memory_usage(deep=True) |
7. 优化原则
- 先保证代码正确,再考虑优化
- 优化前测量,优化后对比
- 避免过早优化
- 权衡代码可读性和性能
下章预告
第十三章:综合实战案例
将综合运用前面所有章节的知识,完成一个完整的数据分析项目:
- 数据清洗与预处理
- 探索性数据分析
- 数据可视化
- 性能优化
- 生成分析报告
课后练习
练习1:向量化操作
- 使用循环计算订单折扣(满500打8折,满200打9折)
- 使用
np.where()实现相同功能 - 对比两种方法的执行时间
练习2:内存优化
- 创建一个包含100万行的DataFrame
- 将字符串列转换为category类型
- 将整数列优化为合适的数值类型
- 计算内存节省比例
练习3:分块处理
- 模拟处理100万行数据
- 使用分块方式(每块10万行)处理数据
- 对每个块计算统计指标并合并结果
练习4:eval() 和 query()
- 使用传统方式计算复杂公式
- 使用
eval()实现相同计算 - 使用布尔索引筛选数据
- 使用
query()实现相同筛选 - 对比性能差异
练习5:性能分析
- 编写一个性能测试函数
- 测试筛选、分组、排序、合并操作的性能
- 分析DataFrame的内存占用
- 找出内存占用最大的列并优化
练习6:综合优化
- 对一个大型DataFrame进行综合优化
- 应用向量化操作
- 优化内存使用
- 使用query()加速筛选
- 对比优化前后的性能提升