项目背景

电商销售数据是数据分析入门最经典的真实场景之一。每一笔订单背后都藏着用户行为、市场趋势和运营机会。在这个实战项目中,我们将扮演一家电商公司的数据分析师,从原始订单数据出发,完成一个完整的数据分析闭环。

业务问题定义

分析开始之前,我们要先明确业务方关心的问题:

  1. 销售额的整体趋势如何?有没有季节性规律?
  2. 哪些产品和品类贡献了主要收入?
  3. 客户的地域分布是怎样的?
  4. 哪些客户是高价值用户?如何区分不同价值的客户群体?
  5. 未来一个月销售额大概会是多少?

这些问题将指导我们后续的每一步分析。

数据加载与初始探索

我们先加载数据并了解基本情况。

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 可以实现自动化分群
  • 时间序列预测需要考虑趋势和季节性成分
  • 分析的最终产出是业务决策建议,而不仅仅是数字

在下一篇文章中,我们将聚焦用户行为分析,深入探索用户的完整行为路径。