【Python数据科学实战之路】第4章 | Pandas高级:数据处理的艺术
·
Python版本:Python 3.12+
开发工具:PyCharm 或 VS Code
操作系统:Windows / macOS / Linux (通用)
本章难度:中级到高级
预计学习时间:4-5小时
学习目标
完成本章学习后,你将能够:
- 深入理解merge/join底层原理,掌握不同连接类型的适用场景
- 掌握groupby内部执行机制,编写高效的分组聚合代码
- 运用窗口函数进行高级时间序列分析
- 掌握性能优化技巧和内存管理策略
- 完成复杂实战项目,解决真实业务问题
1. 数据合并与连接:深入理解merge/join原理
1.1 连接类型图解
理解merge的关键在于理解SQL风格的连接操作。以下是四种基本连接类型的图解:
内连接 (Inner Join) 左连接 (Left Join)
A B A B
/ \ / \ / \ /
1---2----2---3 1---2----2
| | | | | | |
*---*----*---* *---*----*
保留交集 保留A全部,B无匹配填NaN
右连接 (Right Join) 外连接 (Outer Join)
A B A B
\ / \ / \ / \
2----2---3 1---2----2---3
| | | | | | |
*----*---* *---*----*---*
保留B全部,A无匹配填NaN 保留全部,无匹配填NaN
1.2 merge底层原理
merge操作的核心是**哈希连接(Hash Join)**算法:
步骤1: 构建哈希表
右表(dept_id) 哈希表
┌──────────┐ ┌─────────┬─────────┐
│dept_id │ -> │Hash Key │ Rows │
│ 1:技术部 │ │ 1 │ [技术部] │
│ 2:销售部 │ │ 2 │ [销售部] │
│ 3:财务部 │ │ 3 │ [财务部] │
└──────────┘ └─────────┴─────────┘
步骤2: 探测匹配
左表(emp_id, dept_id) 匹配结果
┌─────────┬─────────┐ ┌─────────┬─────────┬─────────┐
│emp_id │dept_id │ -> │emp_id │dept_id │dept_name│
│ 101 │ 1 │ │ 101 │ 1 │ 技术部 │
│ 102 │ 2 │ │ 102 │ 2 │ 销售部 │
│ 103 │ 1 │ │ 103 │ 1 │ 技术部 │
└─────────┴─────────┘ └─────────┴─────────┴─────────┘
时间复杂度分析:
- 构建哈希表:O(n),n为右表行数
- 探测匹配:O(m),m为左表行数
- 总复杂度:O(n + m)
1.3 merge核心参数详解
| 参数 | 说明 | 默认值 | 使用场景 |
|---|---|---|---|
| how | 连接方式 | ‘inner’ | 控制结果保留哪些行 |
| on | 连接键 | None | 两边列名相同时使用 |
| left_on/right_on | 左右连接键 | None | 列名不同时使用 |
| left_index/right_index | 使用索引连接 | False | 基于索引合并 |
| suffixes | 重名列后缀 | (‘_x’, ‘_y’) | 处理列名冲突 |
| validate | 关系验证 | None | 检查数据一致性 |
1.4 代码示例:merge实战
import pandas as pd
import numpy as np
# 创建示例数据
employees = pd.DataFrame({
'emp_id': [101, 102, 103, 104, 105],
'name': ['张三', '李四', '王五', '赵六', '孙七'],
'dept_id': [1, 2, 1, 3, 2],
'salary': [8000, 12000, 9500, 15000, 11000]
})
departments = pd.DataFrame({
'dept_id': [1, 2, 3, 4],
'dept_name': ['技术部', '销售部', '财务部', '人事部'],
'location': ['北京', '上海', '广州', '深圳']
})
# 1. 内连接:只保留匹配的数据
inner_result = pd.merge(
employees,
departments,
on='dept_id',
how='inner'
)
print("内连接结果:")
print(inner_result)
# 2. 左连接:保留左表全部数据
left_result = pd.merge(
employees,
departments,
on='dept_id',
how='left'
)
print("\n左连接结果:")
print(left_result)
# 3. 使用validate检查数据关系
try:
# one_to_many: 左表dept_id唯一,右表可重复
validated = pd.merge(
employees,
departments,
on='dept_id',
how='left',
validate='many_to_one' # employees多对departments一
)
print("\n验证通过!")
except Exception as e:
print(f"\n验证失败:{e}")
1.5 join方法:基于索引的高效合并
join是merge的特例,专用于索引对齐场景,性能更优:
# 设置索引后使用join
emp_idx = employees.set_index('dept_id')
dept_idx = departments.set_index('dept_id')
# join默认是左连接
joined = emp_idx.join(dept_idx, how='left')
print("join结果:")
print(joined)
# 多表链式join
performance = pd.DataFrame({
'score': [88, 92, 85, 95, 90]
}, index=[101, 102, 103, 104, 105])
# 链式连接
full_data = emp_idx.join(dept_idx).join(performance, on='emp_id')
print("\n链式join结果:")
print(full_data)
merge vs join 选择指南:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 基于列连接 | merge | 灵活,支持多键 |
| 基于索引连接 | join | 语法简洁,性能略优 |
| 多表连接 | merge | 更清晰的可读性 |
| 链式操作 | join | 代码更流畅 |
1.6 concat方法:轴向堆叠
# 纵向堆叠(增加行)
df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [5, 6], 'B': [7, 8]})
vertical = pd.concat([df1, df2], axis=0, ignore_index=True)
print("纵向堆叠:")
print(vertical)
# 横向堆叠(增加列)
df3 = pd.DataFrame({'C': [9, 10]}, index=[0, 1])
horizontal = pd.concat([df1, df3], axis=1)
print("\n横向堆叠:")
print(horizontal)
# 使用keys创建层次化索引
hierarchical = pd.concat([df1, df2], keys=['第一季度', '第二季度'])
print("\n层次化索引:")
print(hierarchical)
2. 分组聚合深度解析:groupby内部机制
2.1 groupby执行流程图解
groupby采用**“拆分-应用-合并”(Split-Apply-Combine)**模式:
原始数据 拆分(Split) 应用(Apply)
┌────┬────────┬───────┐ ┌───────────┐ ┌───────────┐
│地区│ 产品 │ 销售额 │ │ 华北组 │ │ sum: 3000 │
├────┼────────┼───────┤ │ ┌───────┐ │ │ mean: 1000│
│华北│ 手机 │ 1000 │ │ │华北 │ │ │ max: 1500 │
│华北│ 电脑 │ 2000 │ -> │ │华东 │ │ -> agg ->├───────────┤
│华东│ 手机 │ 1500 │ │ │华南 │ │ │ sum: 2700 │
│华东│ 电脑 │ 1200 │ │ └───────┘ │ │ mean: 1350│
│华南│ 手机 │ 1200 │ └───────────┘ │ max: 1500 │
└────┴────────┴───────┘ └───────────┘
按地区分组 合并结果
2.2 groupby内部数据结构
groupby对象的核心是Grouper和分组字典:
sales_data = pd.DataFrame({
'region': ['华北', '华北', '华东', '华东', '华南'],
'product': ['手机', '电脑', '手机', '电脑', '手机'],
'sales': [1000, 2000, 1500, 1200, 1200]
})
# 创建groupby对象
grouped = sales_data.groupby('region')
# 查看分组信息
print("分组键:", list(grouped.groups.keys()))
print("\n分组字典(索引映射):")
for key, idx in grouped.groups.items():
print(f" {key}: {list(idx)}")
# 迭代分组
print("\n迭代分组对象:")
for name, group in grouped:
print(f"\n{name}:")
print(group)
2.3 聚合函数执行机制
| 聚合类型 | 方法 | 返回值形状 | 适用场景 |
|---|---|---|---|
| 聚合 | agg/aggregate | 每个组一个值 | 统计汇总 |
| 转换 | transform | 与原数据同形状 | 组内标准化、填充 |
| 过滤 | filter | 筛选后的子集 | 按组条件筛选 |
| 应用 | apply | 灵活 | 自定义复杂操作 |
# agg:多维度聚合
multi_agg = sales_data.groupby('region').agg({
'sales': ['sum', 'mean', 'std', 'count'],
'product': 'nunique'
})
print("多维度聚合:")
print(multi_agg)
# 命名聚合(推荐,结果列名更清晰)
named_agg = sales_data.groupby('region').agg(
total_sales=('sales', 'sum'),
avg_sales=('sales', 'mean'),
product_count=('product', 'nunique')
)
print("\n命名聚合:")
print(named_agg)
# transform:保持原数据结构
sales_data['region_avg'] = sales_data.groupby('region')['sales'].transform('mean')
sales_data['region_ratio'] = sales_data['sales'] / sales_data['region_avg']
print("\ntransform结果:")
print(sales_data)
# filter:基于组特征筛选
high_sales = sales_data.groupby('region').filter(
lambda x: x['sales'].sum() > 2500
)
print("\nfilter结果(销售额>2500的地区):")
print(high_sales)
2.4 自定义聚合函数
# 定义自定义聚合函数
def range_calc(x):
"""计算极差"""
return x.max() - x.min()
def cv(x):
"""计算变异系数"""
return x.std() / x.mean()
def percentile_90(x):
"""计算90分位数"""
return x.quantile(0.9)
# 应用自定义函数
custom_result = sales_data.groupby('region')['sales'].agg([
('总和', 'sum'),
('均值', 'mean'),
('极差', range_calc),
('变异系数', cv),
('P90', percentile_90)
])
print("自定义聚合:")
print(custom_result)
3. 透视表与交叉表
3.1 pivot_table详解
# 创建示例数据
np.random.seed(42)
sales_detail = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=100, freq='D'),
'region': np.random.choice(['华北', '华东', '华南'], 100),
'product': np.random.choice(['手机', '电脑', '平板'], 100),
'sales': np.random.randint(500, 3000, 100),
'profit': np.random.randint(50, 500, 100)
})
# 基础透视表
pivot_basic = pd.pivot_table(
sales_detail,
values='sales',
index='region',
columns='product',
aggfunc='sum',
fill_value=0,
margins=True,
margins_name='总计'
)
print("基础透视表:")
print(pivot_basic)
# 多级透视表
sales_detail['month'] = sales_detail['date'].dt.to_period('M')
pivot_multi = pd.pivot_table(
sales_detail,
values=['sales', 'profit'],
index=['region', 'month'],
columns='product',
aggfunc={'sales': 'sum', 'profit': 'mean'},
fill_value=0
)
print("\n多级透视表:")
print(pivot_multi)
3.2 crosstab交叉表
# 创建类别数据
category_data = pd.DataFrame({
'gender': np.random.choice(['男', '女'], 200),
'age_group': np.random.choice(['青年', '中年', '老年'], 200),
'preference': np.random.choice(['线上', '线下'], 200)
})
# 基础交叉表
cross_basic = pd.crosstab(
category_data['gender'],
category_data['age_group']
)
print("基础交叉表:")
print(cross_basic)
# 归一化交叉表
cross_norm = pd.crosstab(
category_data['gender'],
category_data['age_group'],
normalize='index' # 行归一化
).round(3)
print("\n归一化交叉表(行百分比):")
print(cross_norm)
# 三维交叉表
cross_3d = pd.crosstab(
[category_data['gender'], category_data['age_group']],
category_data['preference']
)
print("\n三维交叉表:")
print(cross_3d)
4. 时间序列处理与窗口函数
4.1 时间序列基础
# 创建时间序列数据
dates = pd.date_range('2024-01-01', periods=100, freq='D')
ts_data = pd.DataFrame({
'value': np.random.randn(100).cumsum() + 100
}, index=dates)
# 时间特征提取
ts_data['year'] = ts_data.index.year
ts_data['month'] = ts_data.index.month
ts_data['day'] = ts_data.index.day
ts_data['weekday'] = ts_data.index.weekday
ts_data['quarter'] = ts_data.index.quarter
print("时间特征:")
print(ts_data.head())
# 时间筛选
print("\n2024年1月的数据:")
print(ts_data['2024-01'])
print("\n2024-01-15到2024-01-20的数据:")
print(ts_data['2024-01-15':'2024-01-20'])
4.2 重采样(Resample)
# 创建小时级数据
hours = pd.date_range('2024-01-01', periods=168, freq='H') # 一周数据
hourly_data = pd.DataFrame({
'value': np.random.randn(168).cumsum() + 100
}, index=hours)
# 降采样:小时 -> 日
daily = hourly_data.resample('D').agg({
'value': ['mean', 'max', 'min', 'std']
})
print("日级聚合:")
print(daily.head())
# 常用频率别名
# D: 日历日, W: 周, M: 月末, MS: 月初
# Q: 季末, A: 年末, H: 小时, T: 分钟
# 上采样(增加频率)并插值
upsampled = hourly_data.resample('30T').interpolate(method='linear')
print("\n上采样并插值(前5行):")
print(upsampled.head())
4.3 窗口函数详解
窗口函数对比:
| 窗口类型 | 特点 | 适用场景 |
|---|---|---|
| rolling | 固定大小滑动窗口 | 移动平均线、波动率 |
| expanding | 逐渐扩大的窗口 | 累计统计、历史均值 |
| ewm | 指数加权,近期权重高 | 趋势跟踪、风险指标 |
# 创建股票价格数据
np.random.seed(42)
days = pd.date_range('2024-01-01', periods=60, freq='D')
stock = pd.DataFrame({
'price': 100 + np.random.randn(60).cumsum()
}, index=days)
# 1. Rolling滑动窗口
stock['ma_5'] = stock['price'].rolling(window=5).mean()
stock['ma_10'] = stock['price'].rolling(window=10).mean()
stock['volatility'] = stock['price'].rolling(window=10).std()
# 2. Expanding扩展窗口
stock['expanding_mean'] = stock['price'].expanding().mean()
stock['expanding_max'] = stock['price'].expanding().max()
# 3. EWM指数加权移动平均
stock['ewm_5'] = stock['price'].ewm(span=5).mean()
stock['ewm_10'] = stock['price'].ewm(span=10).mean()
print("窗口函数结果(前15行):")
print(stock.head(15))
# 自定义窗口函数
stock['range_10'] = stock['price'].rolling(window=10).apply(
lambda x: x.max() - x.min(),
raw=True # 使用numpy数组,性能更好
)
print("\n移动极差(前15行):")
print(stock[['price', 'range_10']].head(15))
5. 性能优化与内存管理
5.1 数据类型优化
# 创建大数据集测试
np.random.seed(42)
large_df = pd.DataFrame({
'int_col': np.random.randint(0, 100, 1000000),
'float_col': np.random.randn(1000000),
'category_col': np.random.choice(['A', 'B', 'C', 'D'], 1000000),
'bool_col': np.random.choice([True, False], 1000000),
})
print("原始内存使用:")
print(large_df.memory_usage(deep=True))
print(f"总内存: {large_df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
# 优化数据类型
optimized_df = large_df.copy()
# 1. 整数类型优化
optimized_df['int_col'] = optimized_df['int_col'].astype('int8') # -128~127
# 2. 浮点数类型优化
optimized_df['float_col'] = optimized_df['float_col'].astype('float32')
# 3. 类别类型优化
optimized_df['category_col'] = optimized_df['category_col'].astype('category')
# 4. 布尔类型已经是bool,无需优化
print("\n优化后内存使用:")
print(optimized_df.memory_usage(deep=True))
print(f"总内存: {optimized_df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
5.2 类型优化建议表
| 原始类型 | 优化策略 | 适用条件 |
|---|---|---|
| int64 | int8/int16/int32 | 数值范围确定时 |
| float64 | float32 | 精度要求不高时 |
| object(字符串) | category | 重复值较多时 |
| bool | bool | 已经是最佳 |
| datetime64[ns] | datetime64[s] | 不需要纳秒精度 |
5.3 向量化操作 vs 循环
import time
df = pd.DataFrame({
'A': np.random.randn(100000),
'B': np.random.randn(100000)
})
# 方法1:循环(慢)
start = time.time()
result_loop = []
for i in range(len(df)):
result_loop.append(df.iloc[i]['A'] + df.iloc[i]['B'])
result_loop = pd.Series(result_loop)
time_loop = time.time() - start
# 方法2:iterrows(较慢)
start = time.time()
result_iterrows = []
for _, row in df.iterrows():
result_iterrows.append(row['A'] + row['B'])
result_iterrows = pd.Series(result_iterrows)
time_iterrows = time.time() - start
# 方法3:itertuples(较快)
start = time.time()
result_itertuples = [row.A + row.B for row in df.itertuples()]
result_itertuples = pd.Series(result_itertuples)
time_itertuples = time.time() - start
# 方法4:向量化(最快)
start = time.time()
result_vectorized = df['A'] + df['B']
time_vectorized = time.time() - start
print(f"循环耗时: {time_loop:.4f}s")
print(f"iterrows耗时: {time_iterrows:.4f}s")
print(f"itertuples耗时: {time_itertuples:.4f}s")
print(f"向量化耗时: {time_vectorized:.4f}s")
print(f"\n向量化比循环快 {time_loop/time_vectorized:.0f} 倍")
5.4 分块处理大文件
# 模拟大文件处理
# 实际使用时:pd.read_csv('large_file.csv', chunksize=10000)
chunksize = 10000
chunks = []
# 模拟分块读取
for i in range(10):
chunk = pd.DataFrame({
'A': np.random.randn(chunksize),
'B': np.random.randint(0, 100, chunksize)
})
# 对每个块进行处理
chunk['C'] = chunk['A'] * chunk['B']
chunks.append(chunk)
# 合并结果
result = pd.concat(chunks, ignore_index=True)
print(f"处理完成,总行数: {len(result)}")
# 使用query进行筛选(比布尔索引快)
filtered = result.query('A > 0 and B < 50')
print(f"筛选后行数: {len(filtered)}")
5.5 性能优化检查清单
- 使用合适的数据类型(category、int32、float32)
- 避免循环,使用向量化操作
- 使用eval/query处理复杂条件
- 分块处理超大文件
- 使用inplace参数减少内存拷贝
- 及时删除不再需要的变量(del)
- 使用numba/Cython加速关键函数
6. 高级索引技巧
6.1 MultiIndex多级索引
# 创建多级索引
arrays = [
['华北', '华北', '华东', '华东', '华南', '华南'],
['北京', '天津', '上海', '杭州', '广州', '深圳']
]
index = pd.MultiIndex.from_arrays(arrays, names=['region', 'city'])
df_multi = pd.DataFrame({
'sales': [1000, 800, 1200, 900, 1100, 950],
'population': [2000, 1500, 2400, 1200, 1800, 1300]
}, index=index)
print("多级索引DataFrame:")
print(df_multi)
# 多级索引选择
print("\n选择华北地区:")
print(df_multi.loc['华北'])
print("\n选择华北-北京:")
print(df_multi.loc[('华北', '北京')])
print("\n使用xs选择所有北京:")
print(df_multi.xs('北京', level='city'))
6.2 索引对齐机制
# 索引对齐示例
df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['a', 'b', 'c'])
df2 = pd.DataFrame({'B': [10, 20, 30]}, index=['b', 'c', 'd'])
print("DataFrame 1:")
print(df1)
print("\nDataFrame 2:")
print(df2)
# 自动对齐
result = df1['A'] + df2['B']
print("\n加法结果(自动对齐):")
print(result)
# 使用fill_value填充
result_fill = df1['A'].add(df2['B'], fill_value=0)
print("\n加法结果(填充0):")
print(result_fill)
7. 避坑小贴士
7.1 merge常见陷阱
# 陷阱1:未指定how参数,默认inner导致数据丢失
# 错误:pd.merge(df1, df2, on='key') # 可能丢失数据
# 正确:pd.merge(df1, df2, on='key', how='outer') # 保留所有数据
# 陷阱2:连接键数据类型不一致
df1 = pd.DataFrame({'key': [1, 2, 3], 'val': ['a', 'b', 'c']})
df2 = pd.DataFrame({'key': ['1', '2', '3'], 'val2': ['x', 'y', 'z']})
print("数据类型不一致:")
print(f"df1['key']: {df1['key'].dtype}")
print(f"df2['key']: {df2['key'].dtype}")
# 解决:统一数据类型
df2['key'] = df2['key'].astype(int)
merged = pd.merge(df1, df2, on='key')
print("\n合并结果:")
print(merged)
7.2 groupby常见陷阱
# 陷阱:apply中使用循环导致性能问题
# 错误:grouped.apply(lambda x: sum([i**2 for i in x['col']]))
# 正确:grouped['col'].apply(lambda x: (x**2).sum())
# 陷阱:忽略缺失值处理
df_na = pd.DataFrame({
'group': ['A', 'A', 'B', 'B', None],
'value': [1, 2, 3, 4, 5]
})
# 默认dropna=True,缺失值被排除
print("默认(排除NA):")
print(df_na.groupby('group')['value'].sum())
# 保留NA作为独立组
print("\n保留NA:")
print(df_na.groupby('group', dropna=False)['value'].sum())
8. 复杂实战案例:电商销售数据分析
8.1 项目背景
分析某电商平台一年的销售数据,完成以下任务:
- 数据整合与清洗
- 多维度销售分析
- 时间序列趋势分析
- RFM用户价值分析
- 库存预警模型
8.2 数据准备
np.random.seed(2024)
# 1. 用户数据(10万用户)
users = pd.DataFrame({
'user_id': range(1, 100001),
'gender': np.random.choice(['男', '女'], 100000),
'age_group': np.random.choice(['18-25', '26-35', '36-45', '46+'], 100000),
'city': np.random.choice(['北京', '上海', '广州', '深圳', '杭州', '成都'], 100000),
'register_date': pd.date_range('2022-01-01', periods=100000, freq='H')
})
# 2. 商品数据(500个SKU)
products = pd.DataFrame({
'product_id': range(1, 501),
'category': np.random.choice(['电子产品', '服装', '食品', '家居', '美妆'], 500),
'sub_category': np.random.choice(['手机', '电脑', '上衣', '裤子', '零食', '饮料'], 500),
'price': np.random.randint(50, 5000, 500),
'cost': np.random.randint(30, 3500, 500),
'stock': np.random.randint(0, 1000, 500)
})
products['profit_margin'] = (products['price'] - products['cost']) / products['price']
# 3. 订单数据(100万条)
order_dates = pd.date_range('2023-01-01', '2023-12-31', freq='H')
orders = pd.DataFrame({
'order_id': range(1, 1000001),
'user_id': np.random.choice(users['user_id'], 1000000),
'product_id': np.random.choice(products['product_id'], 1000000),
'quantity': np.random.randint(1, 10, 1000000),
'order_date': np.random.choice(order_dates, 1000000)
})
print("数据集创建完成!")
print(f"用户数:{len(users):,}")
print(f"商品数:{len(products):,}")
print(f"订单数:{len(orders):,}")
8.3 数据整合
# 合并数据
print("开始数据整合...")
# Step 1: 合并用户数据
orders_user = pd.merge(
orders,
users,
on='user_id',
how='left'
)
# Step 2: 合并商品数据
full_data = pd.merge(
orders_user,
products,
on='product_id',
how='left'
)
# 计算业务指标
full_data['order_amount'] = full_data['quantity'] * full_data['price']
full_data['order_profit'] = full_data['quantity'] * (full_data['price'] - full_data['cost'])
# 提取时间特征
full_data['month'] = full_data['order_date'].dt.to_period('M')
full_data['week'] = full_data['order_date'].dt.to_period('W')
full_data['hour'] = full_data['order_date'].dt.hour
full_data['weekday'] = full_data['order_date'].dt.weekday
full_data['is_weekend'] = full_data['weekday'].isin([5, 6])
print(f"整合完成,数据形状:{full_data.shape}")
print(f"\n内存使用:{full_data.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
8.4 多维度销售分析
# 分析1:品类销售分析
category_analysis = full_data.groupby('category').agg(
total_sales=('order_amount', 'sum'),
total_profit=('order_profit', 'sum'),
order_count=('order_id', 'count'),
unique_users=('user_id', 'nunique'),
avg_order_value=('order_amount', 'mean')
).round(2)
category_analysis['profit_rate'] = (
category_analysis['total_profit'] / category_analysis['total_sales']
).round(4)
category_analysis = category_analysis.sort_values('total_sales', ascending=False)
print("品类销售分析:")
print(category_analysis)
# 分析2:城市销售趋势(透视表)
city_monthly = pd.pivot_table(
full_data,
values='order_amount',
index='city',
columns='month',
aggfunc='sum',
fill_value=0
)
print("\n城市月度销售趋势:")
print(city_monthly)
# 分析3:时段分析
hourly_analysis = full_data.groupby('hour').agg(
sales=('order_amount', 'sum'),
orders=('order_id', 'count')
)
print("\n时段销售分析:")
print(hourly_analysis.head(10))
8.5 时间序列分析
# 设置时间索引
ts_data = full_data.set_index('order_date')
# 日销售额趋势
daily_sales = ts_data.resample('D')['order_amount'].sum()
# 计算移动平均线
daily_sales_df = daily_sales.to_frame()
daily_sales_df['ma_7'] = daily_sales_df['order_amount'].rolling(window=7).mean()
daily_sales_df['ma_30'] = daily_sales_df['order_amount'].rolling(window=30).mean()
# 计算同比增长(需要两年数据,这里用环比)
daily_sales_df['sales_prev'] = daily_sales_df['order_amount'].shift(1)
daily_sales_df['growth_rate'] = (
(daily_sales_df['order_amount'] - daily_sales_df['sales_prev'])
/ daily_sales_df['sales_prev'] * 100
).round(2)
print("日销售趋势(前20天):")
print(daily_sales_df.head(20))
# 周同比分析
weekly_sales = ts_data.resample('W')['order_amount'].sum()
weekly_sales_df = weekly_sales.to_frame()
weekly_sales_df['yoy'] = weekly_sales_df['order_amount'].shift(52) # 52周前
weekly_sales_df['yoy_growth'] = (
(weekly_sales_df['order_amount'] - weekly_sales_df['yoy'])
/ weekly_sales_df['yoy'] * 100
).round(2)
print("\n周销售趋势:")
print(weekly_sales_df.tail(10))
8.6 RFM用户价值分析
# RFM分析
analysis_date = full_data['order_date'].max()
rfm = full_data.groupby('user_id').agg(
recency=('order_date', lambda x: (analysis_date - x.max()).days),
frequency=('order_id', 'count'),
monetary=('order_amount', 'sum')
)
# RFM评分(使用分位数)
rfm['r_score'] = pd.qcut(rfm['recency'], 5, labels=[5, 4, 3, 2, 1]) # 越小越好
rfm['f_score'] = pd.qcut(rfm['frequency'].rank(method='first'), 5, labels=[1, 2, 3, 4, 5])
rfm['m_score'] = pd.qcut(rfm['monetary'], 5, labels=[1, 2, 3, 4, 5])
# 组合RFM得分
rfm['rfm_score'] = rfm['r_score'].astype(str) + rfm['f_score'].astype(str) + rfm['m_score'].astype(str)
# 用户分群
def segment_user(row):
if row['rfm_score'] in ['555', '554', '544', '545', '454', '455', '445']:
return '重要价值客户'
elif row['rfm_score'] in ['543', '444', '435', '355', '354', '345', '344', '335']:
return '重要保持客户'
elif row['rfm_score'] in ['553', '551', '552', '541', '542', '533', '532', '531', '452', '451']:
return '重要发展客户'
elif row['rfm_score'] in ['512', '511', '422', '421', '412', '411', '311']:
return '重要挽留客户'
elif row['rfm_score'] in ['155', '154', '144', '214', '215', '115', '114']:
return '一般价值客户'
else:
return '低价值客户'
rfm['segment'] = rfm.apply(segment_user, axis=1)
# 分群统计
segment_summary = rfm.groupby('segment').agg(
user_count=('recency', 'count'),
avg_recency=('recency', 'mean'),
avg_frequency=('frequency', 'mean'),
avg_monetary=('monetary', 'mean')
).round(2)
print("RFM用户分群统计:")
print(segment_summary)
8.7 库存预警模型
# 计算每个商品的日均销量
product_daily_sales = full_data.groupby(['product_id', 'date']).agg(
daily_sales=('quantity', 'sum')
).reset_index()
product_stats = product_daily_sales.groupby('product_id').agg(
avg_daily_sales=('daily_sales', 'mean'),
std_daily_sales=('daily_sales', 'std'),
max_daily_sales=('daily_sales', 'max')
).reset_index()
# 合并库存信息
inventory = products[['product_id', 'stock', 'category']].merge(
product_stats,
on='product_id',
how='left'
)
# 计算库存天数(安全库存 = 7天平均销量)
inventory['stock_days'] = (inventory['stock'] / inventory['avg_daily_sales']).round(1)
inventory['safety_stock'] = inventory['avg_daily_sales'] * 7
inventory['reorder_point'] = inventory['avg_daily_sales'] * 14 # 14天补货点
# 库存预警
def stock_status(row):
if row['stock'] == 0:
return '缺货'
elif row['stock'] <= row['safety_stock']:
return '库存不足'
elif row['stock'] <= row['reorder_point']:
return '需要补货'
else:
return '库存充足'
inventory['status'] = inventory.apply(stock_status, axis=1)
# 预警统计
warning_summary = inventory.groupby('status').agg(
product_count=('product_id', 'count'),
total_stock=('stock', 'sum')
)
print("库存预警统计:")
print(warning_summary)
print("\n缺货商品列表(前10个):")
out_of_stock = inventory[inventory['status'] == '缺货'].head(10)
print(out_of_stock[['product_id', 'category', 'stock', 'avg_daily_sales']])
8.8 性能优化实践
# 优化前:内存占用大
print("优化前内存使用:")
print(f"full_data: {full_data.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
# 优化数据类型
optimized_data = full_data.copy()
# 1. 类别型数据优化
optimized_data['gender'] = optimized_data['gender'].astype('category')
optimized_data['age_group'] = optimized_data['age_group'].astype('category')
optimized_data['city'] = optimized_data['city'].astype('category')
optimized_data['category'] = optimized_data['category'].astype('category')
optimized_data['sub_category'] = optimized_data['sub_category'].astype('category')
# 2. 数值型数据优化
optimized_data['quantity'] = optimized_data['quantity'].astype('int8')
optimized_data['hour'] = optimized_data['hour'].astype('int8')
optimized_data['weekday'] = optimized_data['weekday'].astype('int8')
# 3. 布尔型
optimized_data['is_weekend'] = optimized_data['is_weekend'].astype('bool')
print("\n优化后内存使用:")
print(f"optimized_data: {optimized_data.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
print(f"节省内存: {(1 - optimized_data.memory_usage(deep=True).sum() / full_data.memory_usage(deep=True).sum()) * 100:.1f}%")
# 使用query进行高效筛选
high_value_orders = optimized_data.query(
'order_amount > 1000 and category == "电子产品" and is_weekend'
)
print(f"\n高价值订单数: {len(high_value_orders):,}")
9. 本章小结
核心知识点回顾
| 主题 | 关键概念 | 最佳实践 |
|---|---|---|
| 数据合并 | merge/join/concat | 根据场景选择,注意连接键类型 |
| 分组聚合 | split-apply-combine | 使用向量化操作,避免循环 |
| 透视表 | pivot_table/crosstab | 合理使用margins和fill_value |
| 时间序列 | resample/rolling/ewm | 注意数据排序,选择合适窗口 |
| 性能优化 | 数据类型/向量化 | 先测量后优化,分块处理大文件 |
进阶学习路径
- 深入学习:阅读Pandas官方文档的Categorical和Internals部分
- 性能进阶:学习Dask、Polars等大数据处理工具
- 实战项目:参与Kaggle竞赛,处理真实数据集
- 源码阅读:阅读Pandas源码,理解底层实现
参考资源
- Pandas官方文档:https://pandas.pydata.org/docs/
- Python for Data Analysis(Wes McKinney著)
- Pandas Cookbook(Theodore Petrou著)
- Stack Overflow Pandas标签:https://stackoverflow.com/questions/tagged/pandas
学习建议:本章内容较多,建议分多次学习。每学完一个小节,都动手运行示例代码,并尝试修改参数观察结果变化。实战练习部分建议独立完成,再对照参考答案。遇到问题时,善用Pandas官方文档和搜索引擎,培养解决问题的能力。
本教程持续更新中,如有疑问或建议,欢迎在评论区留言交流。
更多推荐


所有评论(0)