5 minutes
电商销售分析实战
项目背景
电商销售数据是数据分析入门最经典的真实场景之一。每一笔订单背后都藏着用户行为、市场趋势和运营机会。在这个实战项目中,我们将扮演一家电商公司的数据分析师,从原始订单数据出发,完成一个完整的数据分析闭环。
业务问题定义
分析开始之前,我们要先明确业务方关心的问题:
- 销售额的整体趋势如何?有没有季节性规律?
- 哪些产品和品类贡献了主要收入?
- 客户的地域分布是怎样的?
- 哪些客户是高价值用户?如何区分不同价值的客户群体?
- 未来一个月销售额大概会是多少?
这些问题将指导我们后续的每一步分析。
数据加载与初始探索
我们先加载数据并了解基本情况。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
df = pd.read_csv('ecommerce_orders.csv')
print(f'数据集大小: {df.shape}')
print(f'列名: {df.columns.tolist()}')
df.head()
假设我们的数据包含以下字段:
| 字段 | 含义 | 类型 |
|---|---|---|
| order_id | 订单编号 | 字符串 |
| customer_id | 客户编号 | 字符串 |
| product_id | 产品编号 | 字符串 |
| category | 产品类别 | 字符串 |
| price | 单价 | 浮点数 |
| quantity | 数量 | 整数 |
| order_date | 订单日期 | 日期 |
| shipping_address | 收货地址 | 字符串 |
| payment_method | 支付方式 | 字符串 |
| status | 订单状态 | 字符串 |
df.info()
df.describe()
数据清洗
现实数据很少是完美的。我们需要处理几个典型问题。
处理退货记录
退货记录通常表现为数量为负或状态为"returned"。
# 查看订单状态分布
print(df['status'].value_counts())
# 分离正常订单和退货
returns = df[df['status'] == 'returned']
normal_orders = df[df['status'] != 'returned']
print(f'正常订单: {len(normal_orders)}')
print(f'退货订单: {len(returns)}')
# 计算净销售额时剔除退货
df['net_amount'] = df.apply(
lambda row: row['price'] * row['quantity'] if row['status'] != 'returned' else 0,
axis=1
)
处理缺失价格
missing_price = df[df['price'].isna()]
print(f'缺失价格的记录数: {len(missing_price)}')
# 用同类产品的中位数填充
category_median_price = df.groupby('category')['price'].transform('median')
df['price'].fillna(category_median_price, inplace=True)
# 验证填充结果
print(f'填充后缺失值: {df["price"].isna().sum()}')
处理无效日期
# 尝试转换日期,无效日期转为 NaT
df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce')
# 删除无效日期的记录
invalid_dates = df[df['order_date'].isna()]
df = df.dropna(subset=['order_date'])
print(f'删除无效日期记录: {len(invalid_dates)}')
计算销售额
df['amount'] = df['price'] * df['quantity']
df['order_month'] = df['order_date'].dt.to_period('M')
df['order_week'] = df['order_date'].dt.isocalendar().week.astype(int)
df['order_dow'] = df['order_date'].dt.dayofweek # 周一=0
探索性分析
销售趋势分析
# 日销售额趋势
daily_sales = df.groupby(df['order_date'].dt.date)['amount'].sum()
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
daily_sales.plot(ax=axes[0], title='每日销售额趋势', color='#2E86AB')
axes[0].set_ylabel('销售额')
# 周销售额
weekly_sales = df.groupby('order_week')['amount'].sum()
weekly_sales.plot(ax=axes[1], title='每周销售额趋势', color='#A23B72')
axes[1].set_ylabel('销售额')
# 月销售额
monthly_sales = df.groupby('order_month')['amount'].sum()
monthly_sales.plot(ax=axes[2], title='每月销售额趋势', color='#F18F01')
axes[2].set_ylabel('销售额')
plt.tight_layout()
plt.show()
观察趋势图时,我们要关注几个关键信号:
- 上升或下降趋势:业务在增长还是萎缩?
- 周期性波动:是否存在周末效应或月末效应?
- 异常峰值或低谷:是否对应促销活动或突发事件?
星期几的销售模式
dow_sales = df.groupby('order_dow')['amount'].agg(['sum', 'count'])
dow_sales.index = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
fig, ax = plt.subplots(figsize=(10, 5))
dow_sales['sum'].plot(kind='bar', ax=ax, color=['#4C72B0', '#55A868', '#C44E52',
'#8172B2', '#CCB974', '#64B5CD', '#E5A23C'])
ax.set_title('各星期销售额分布')
ax.set_ylabel('销售额')
for i, v in enumerate(dow_sales['sum']):
ax.text(i, v + 500, f'{v/10000:.1f}万', ha='center')
plt.show()
产品品类表现
# 各品类销售额
category_sales = df.groupby('category')['amount'].agg(['sum', 'count', 'mean'])
category_sales = category_sales.sort_values('sum', ascending=False)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
colors = plt.cm.Set3(np.linspace(0, 1, len(category_sales)))
axes[0].pie(category_sales['sum'], labels=category_sales.index,
autopct='%1.1f%%', colors=colors, startangle=90)
axes[0].set_title('各品类销售额占比')
# 各品类平均客单价
category_sales['mean'].plot(kind='bar', ax=axes[1], color='#2E86AB')
axes[1].set_title('各品类平均订单金额')
axes[1].set_ylabel('平均金额')
plt.tight_layout()
plt.show()
print(category_sales)
地域分布
# 简单的地域分析 - 假设地址包含省份信息
df['province'] = df['shipping_address'].str.extract(r'(北京|上海|天津|重庆|河北|山西|辽宁|吉林|黑龙江|江苏|浙江|安徽|福建|江西|山东|河南|湖北|湖南|广东|广西|海南|四川|贵州|云南|西藏|陕西|甘肃|青海|宁夏|新疆|内蒙古)')
province_sales = df.groupby('province')['amount'].sum().sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(12, 6))
province_sales.head(15).plot(kind='bar', ax=ax, color='#2E86AB')
ax.set_title('销售额 Top 15 省份')
ax.set_ylabel('销售额')
plt.xticks(rotation=45)
plt.show()
客户购买频率
# 每个客户的购买次数
customer_freq = df.groupby('customer_id')['order_id'].nunique()
print(customer_freq.describe())
# 购买次数分布
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
customer_freq.plot(kind='hist', bins=30, ax=axes[0], color='#A23B72', edgecolor='white')
axes[0].set_title('客户购买次数分布')
axes[0].set_xlabel('购买次数')
# 累计百分比
freq_pct = customer_freq.value_counts().sort_index()
freq_cumsum = freq_pct.cumsum() / freq_pct.sum() * 100
freq_pct.plot(kind='bar', ax=axes[1], color='#55A868', alpha=0.7)
axes[1].set_title('购买次数分布(柱状图+累计百分比)')
axes[1].set_xlabel('购买次数')
plt.tight_layout()
plt.show()
RFM 客户分群
RFM 分析是客户价值分析的核心方法。它从三个维度衡量客户价值:
- Recency(最近一次购买):距离现在多久没买了?值越小越好
- Frequency(购买频率):买了多少次?值越大越好
- Monetary(消费金额):总共花了多少钱?值越大越好
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# 计算 RFM 指标
reference_date = df['order_date'].max()
rfm = df.groupby('customer_id').agg({
'order_date': lambda x: (reference_date - x.max()).days, # R
'order_id': 'nunique', # F
'amount': 'sum' # M
}).rename(columns={
'order_date': 'Recency',
'order_id': 'Frequency',
'amount': 'Monetary'
})
print(rfm.describe())
# 处理异常值(截尾处理)
for col in ['Recency', 'Frequency', 'Monetary']:
upper = rfm[col].quantile(0.99)
rfm[col] = rfm[col].clip(upper=upper)
# 标准化
scaler = StandardScaler()
rfm_scaled = scaler.fit_transform(rfm[['Recency', 'Frequency', 'Monetary']])
确定最佳聚类数
# 肘部法则
inertias = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(rfm_scaled)
inertias.append(kmeans.inertia_)
# 轮廓系数
from sklearn.metrics import silhouette_score
silhouettes = []
for k in range(2, 11):
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(rfm_scaled)
sil = silhouette_score(rfm_scaled, labels)
silhouettes.append(sil)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(K_range, inertias, 'bo-')
axes[0].set_title('肘部法则')
axes[0].set_xlabel('K')
axes[0].set_ylabel('惯性')
axes[1].plot(range(2, 11), silhouettes, 'ro-')
axes[1].set_title('轮廓系数')
axes[1].set_xlabel('K')
axes[1].set_ylabel('轮廓系数')
plt.tight_layout()
plt.show()
# 选择 K=4
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
rfm['Cluster'] = kmeans.fit_predict(rfm_scaled)
分群解读
# 分析每个聚类的特征
cluster_profile = rfm.groupby('Cluster').agg({
'Recency': 'mean',
'Frequency': 'mean',
'Monetary': 'mean',
'customer_id': 'count'
}).rename(columns={'customer_id': 'Count'})
print(cluster_profile)
# 雷达图展示
from math import pi
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
categories = ['Recency (逆)', 'Frequency', 'Monetary']
N = len(categories)
# 标准化到 0-1 方便对比
rfm_norm = (rfm[['Recency', 'Frequency', 'Monetary']] - rfm[['Recency', 'Frequency', 'Monetary']].min()) / \
(rfm[['Recency', 'Frequency', 'Monetary']].max() - rfm[['Recency', 'Frequency', 'Monetary']].min())
rfm_norm['Recency'] = 1 - rfm_norm['Recency'] # 越近越好,所以反转
for cluster in range(4):
values = rfm_norm[rfm['Cluster'] == cluster][['Recency', 'Frequency', 'Monetary']].mean().values.tolist()
values += values[:1]
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
ax.plot(angles, values, 'o-', label=f'Cluster {cluster}')
ax.fill(angles, values, alpha=0.1)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories)
ax.set_title('各客户分群特征对比')
ax.legend(loc='upper right')
plt.show()
客户分群解读与运营策略
| 分群 | R | F | M | 标签 | 运营策略 |
|---|---|---|---|---|---|
| 0 | 高 | 高 | 高 | 重要价值客户 | VIP 维护,专属优惠 |
| 1 | 低 | 高 | 高 | 重要保持客户 | 定期唤醒,防止流失 |
| 2 | 中 | 中 | 中 | 一般发展客户 | 提升购买频次 |
| 3 | 高 | 低 | 低 | 低价值客户 | 自动化营销,低成本维护 |
# 给客户打标签
def label_cluster(row):
if row['Cluster'] == 0:
return '重要价值客户'
elif row['Cluster'] == 1:
return '重要保持客户'
elif row['Cluster'] == 2:
return '一般发展客户'
else:
return '低价值客户'
rfm['Segment'] = rfm.apply(label_cluster, axis=1)
# 各分群的销售额贡献
segment_revenue = rfm.groupby('Segment')['Monetary'].sum().sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(8, 5))
colors_dict = {'重要价值客户': '#C44E52', '重要保持客户': '#55A868',
'一般发展客户': '#4C72B0', '低价值客户': '#8172B2'}
segment_revenue.plot(kind='bar', ax=ax, color=[colors_dict[s] for s in segment_revenue.index])
ax.set_title('各客户分群销售额贡献')
ax.set_ylabel('总销售额')
for i, v in enumerate(segment_revenue):
ax.text(i, v + 500, f'{v/10000:.1f}万', ha='center')
plt.show()
销售额预测
使用时间序列方法预测未来一个月的销售额。
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
# 准备每日销售时间序列
daily = df.groupby(df['order_date'].dt.date)['amount'].sum().reset_index()
daily.columns = ['ds', 'y']
daily['ds'] = pd.to_datetime(daily['ds'])
daily = daily.set_index('ds').asfreq('D').fillna(0)
# 划分训练集和测试集
train = daily[:-14]
test = daily[-14:]
# Holt-Winters 季节性模型
model = ExponentialSmoothing(
train['y'],
seasonal_periods=7,
trend='add',
seasonal='add'
)
fitted = model.fit()
# 预测
forecast = fitted.forecast(30) # 预测未来30天
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(train.index, train['y'], label='训练集', color='#4C72B0')
ax.plot(test.index, test['y'], label='测试集', color='#55A868')
ax.plot(forecast.index, forecast, label='预测值', color='#C44E52', linestyle='--')
ax.set_title('销售额预测(Holt-Winters 模型)')
ax.set_ylabel('日销售额')
ax.legend()
plt.show()
# 评估
test_forecast = fitted.forecast(14)
mae = mean_absolute_error(test['y'], test_forecast)
mape = mean_absolute_percentage_error(test['y'], test_forecast)
print(f'MAE: {mae:.2f}')
print(f'MAPE: {mape:.2%}')
业务建议
基于以上分析,我们给业务团队提出以下建议:
1. 品类策略
- 核心品类:销售额占比最高的品类应作为库存和营销重点
- 高客单价品类:虽然销量可能不高,但利润率高,适合做精准推荐
- 潜力品类:增长率高的品类值得加大投入
2. 客户运营
- 重要价值客户(VIP):提供专属客服、会员日特权、新品优先体验
- 重要保持客户:针对长时间未购买的客户发送定向优惠券,设置流失预警
- 一般发展客户:通过交叉销售和向上销售提升客单价
3. 营销节奏
- 季节性备货:根据历史销售周期提前调整库存
- 促销时机:结合星期效应和月度效应安排促销活动
- 地域差异:针对高增长区域加大市场投放
4. 数据建议
- 统一数据采集标准,确保字段完整性
- 建立客户画像体系,收集更多行为数据
- 搭建自动化报表系统,实时监控核心指标
小结
在这个实战项目中,我们完整走了一遍电商数据分析流程:从业务问题定义,到数据清洗和探索性分析,再到 RFM 客户分群和时间序列预测,最后产出可执行的业务建议。
关键知识点回顾:
- 数据清洗是数据分析最耗时但最重要的环节
- 可视化是发现模式和沟通结论的有力工具
- RFM 模型是客户分群的经典方法,结合 K-Means 可以实现自动化分群
- 时间序列预测需要考虑趋势和季节性成分
- 分析的最终产出是业务决策建议,而不仅仅是数字
在下一篇文章中,我们将聚焦用户行为分析,深入探索用户的完整行为路径。