电商销售预测因算算错一个数亏掉十万元——Python数据分析进阶课程:从pandas到sklearn的完整实战路径
那个让我彻底惊醒的夜晚
2023年双十一直播间里,我盯着屏幕上的销售额数字,手心全是汗。作为一家中型电商企业的运营负责人,我们刚结束了一场大规模促销。正常情况下,这种级别的直播应该带来千万级别的GMV。但当晚的数据报表出来后,我们实际销售额只完成了预期的六成。
第二天早上,财务部门把一份详细的损失报告拍在了我桌上:因为预测模型中的一个参数写错了小数点位置,导致备货量严重不足,损失超过十万——这还只是直接损失,后续的流量成本浪费、用户信任流失这些隐性亏损,根本没法用数字衡量。
那一刻我才真正意识到,数据分析这件事,不是玩玩儿那么简单。一个小数点的错误,真能把人从财务自由直接打回解放前。
这件事之后,我开始系统性地重新学习Python数据分析。不是那种速成班的套路,而是真的要把pandas、numpy、matplotlib、sklearn这些工具彻底吃透,做到能应对真实业务场景的程度。
今天就把我的学习路径和实战经验完整分享出来,希望能帮到有同样困惑的你。
先说pandas:这是你数据分析的”工具箱”
pandas是Python里做数据处理最核心的库,没有之一。你可以把它想象成一个超级加强版的Excel,但它不会累、不会报错、不会自动保存前一个状态让你以为数据没变。
读取数据,这是第一步也是最重要的一步
很多新手上来就急着写各种复杂操作,但数据处理的第一步其实是——理解你手里的数据长什么样。我见过太多人拿到数据就直接开始df.head()看前几行,然后就开始信誓旦旦地下结论。
import pandas as pd
# 读取电商销售数据
df = pd.read_excel('sales_data.xlsx', sheet_name='2023_Q3')
# 先看数据的"五脏六腑"——结构、类型、缺失情况
print(f"数据维度: {df.shape}")
print(f"列名: {df.columns.tolist()}")
print(f"数据类型:\n{df.dtypes}")
print(f"缺失值统计:\n{df.isnull().sum()}")
print(f"基础统计:\n{df.describe()}")
这里有个真实案例:我之前处理的一个销售数据,里面有一列订单金额,看起来是数值型。但实际上,这列数据里混入了不少字符串,比如”免单”、”售后退款”、”待确认”这些标记。如果直接拿去算平均值,pandas会报TypeError,而更糟糕的情况是——数据里有隐藏的小数点错位问题,直接算平均值得出一个看起来很合理但实际完全错误的结果。
所以每次处理数据之前,我的习惯是先做这些检查:
# 检查异常值
def check_data_quality(df):
"""数据质量检查函数——这是我踩了无数坑后总结出来的"""
issues = []
# 检查缺失值比例
missing_pct = df.isnull().sum() / len(df) * 100
high_missing = missing_pct[missing_pct > 10]
if len(high_missing) > 0:
issues.append(f"高缺失列: {high_missing.to_dict()}")
# 检查数值列的异常值(超过3倍标准差)
numeric_cols = df.select_dtypes(include='number').columns
for col in numeric_cols:
mean_val = df[col].mean()
std_val = df[col].std()
outliers = df[(df[col] > mean_val + 3*std_val) | (df[col] < mean_val - 3*std_val)]
if len(outliers) > 0:
issues.append(f"列{col}发现{len(outliers)}个异常值")
# 检查重复行
duplicates = df.duplicated().sum()
if duplicates > 0:
issues.append(f"存在{duplicates}条重复数据")
return issues if issues else ["数据质量良好,未发现明显问题"]
issues = check_data_quality(df)
for issue in issues:
print(f"⚠️ {issue}")
这段代码看起来简单,但它帮我避免了至少三次重大决策失误。其中一次是因为同一笔订单因为系统bug被重复导入了三次,导致销售额虚高了200%,差点让我做出错误的营销判断。
数据清洗:90%的时间花在这里
真正做数据分析的时候,我大概有70%-80%的时间都在做数据清洗。这不是因为我喜欢折腾,而是因为真实世界的数据本来就很脏。
# 处理订单金额列的异常值——基于业务逻辑而非简单统计
def clean_order_amount(series):
"""
清洗订单金额:
1. 非数值字符全部转NaN
2. 负数金额需要单独处理(可能是退款)
3. 超过正常范围的金额需要人工核查
"""
# 转换为数值,无法转换的变成NaN
cleaned = pd.to_numeric(series, errors='coerce')
# 分离出负数(退款/售后)
refunds = cleaned[cleaned < 0]
print(f"检测到{len(refunds)}笔退款/售后订单")
# 只保留正数金额用于分析
valid_orders = cleaned[cleaned > 0]
# 用IQR方法识别异常值
Q1 = valid_orders.quantile(0.25)
Q3 = valid_orders.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 超过上界的需要人工核查
need_review = valid_orders[valid_orders > upper_bound]
if len(need_review) > 0:
print(f"⚠️ 发现{len(need_review)}笔大额订单需要人工核查:")
print(need_review.head(10))
# 截断异常值(也可以用其他方式处理)
cleaned_cleaned = cleaned.clip(lower=lower_bound, upper=upper_bound)
return cleaned_cleaned, refunds
df['订单金额_清洗'], refund_orders = clean_order_amount(df['订单金额'])
这段代码里最重要的是业务逻辑的判断。比如那个upper_bound的上限,不是简单地用3倍标准差,而是用IQR方法,这样对偏态分布更友好。更重要的是,超过上限的数据我不会直接删掉——因为那可能是真实的VIP客户大单,删掉会严重低估销售额。我会把它单独标记出来,让业务人员去核实。
上次我就是因为把这个逻辑搞错了,直接删掉了所有超过5000元的订单,结果把我们一个月15%的实际销售额给”洗”没了。那个教训,花了五万块学费。
numpy:数值计算的底层引擎
numpy是pandas的底层依赖,但直接学习numpy非常重要。因为很多复杂的数据处理逻辑,用numpy实现不仅效率更高,而且更能让你理解数据在计算机里到底是什么形态。
理解数组的”灵魂”:shape、dtype和broadcasting
import numpy as np
# 创建模拟的销售数据——2023年1-9月,12个品类,每天的销售量
np.random.seed(42) # 固定随机种子,保证结果可复现
daily_sales = np.random.randint(50, 500, size=(273, 12)) # 273天 × 12个品类
print(f"数组形状: {daily_sales.shape}") # (273, 12)
print(f"数据类型: {daily_sales.dtype}") # int64
print(f"维度数: {daily_sales.ndim}") # 2
print(f"总元素数: {daily_sales.size}") # 3276
# 计算每个品类的总销售额
category_totals = np.sum(daily_sales, axis=0)
print(f"\n各品类总销售额:\n{category_totals}")
# 计算每天的销售总额
daily_totals = np.sum(daily_sales, axis=1)
print(f"\n每日销售总额统计: 均值{np.mean(daily_totals):.2f}, 标准差{np.std(daily_totals):.2f}")
这里有个关键概念要讲清楚:numpy的axis参数。很多新手看到axis=0和axis=1就懵了。我的理解方法是——想象你的数组是一个表格,axis=0就是沿着行方向操作(把每一列的数据合并),axis=1就是沿着列方向操作(把每一行的数据合并)。
比如上面的代码,axis=0表示把273天的数据加起来,得到每个品类的总销量;axis=1表示把12个品类的数据加起来,得到每天的总销量。
广播机制:numpy的”魔法”
broadcasting是numpy最强大也最容易让人困惑的特性之一。它的核心思想是:当两个数组进行运算时,如果它们的shape不完全一致,numpy会自动”广播”较小的数组,使其shape与较大的数组匹配。
# 场景:计算每个品类每天的销售完成率
# 假设我们有目标销售额,形状是(12,),表示12个品类的目标值
daily_sales = np.random.randint(50, 500, size=(273, 12))
target_sales = np.array([8000, 6000, 10000, 5000, 7000, 9000, 4000, 11000, 6000, 8000, 7500, 5500])
# 计算完成率——不需要循环,一行代码搞定
completion_rate = daily_sales / target_sales
print(f"完成率矩阵形状: {completion_rate.shape}") # (273, 12)
# numpy会自动把target_sales从(12,)广播到(273, 12),每一行都除以相同的target值
理解了这个机制,你就不会再写那种低效的嵌套循环了。我之前看过一个同事写的代码,用双重循环来计算完成率,273天×12品类,硬生生跑了四分钟。而我用numpy广播,只需要0.02秒。
matplotlib:让你的数据”说话”
数据再准,如果不会表达,也发挥不出价值。matplotlib是Python里最基础的可视化工具,虽然不如seaborn那么美观,但灵活性极高,而且掌握它是理解更高级可视化库的基础。
时间序列可视化:销售趋势分析
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
# 设置中文字体(根据操作系统选择)
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 生成日期序列
start_date = datetime(2023, 1, 1)
dates = [start_date + timedelta(days=i) for i in range(273)]
# 模拟数据
np.random.seed(42)
daily_sales = np.cumsum(np.random.randint(500, 1500, size=273)) # 累计销售额
monthly_target = np.array([50000, 55000, 60000, 65000, 70000, 75000, 80000, 85000, 90000])
cumulative_target = np.repeat(monthly_target, 30)[:273] # 模拟月度目标
# 创建图表
fig, ax = plt.subplots(figsize=(14, 6))
# 绘制实际销售额
ax.plot(dates, daily_sales, color='#2E86AB', linewidth=2, label='实际销售额')
# 绘制目标线
ax.plot(dates, cumulative_target, color='#E94F37', linewidth=2, linestyle='--', label='累计目标')
# 标记关键节点
ax.axvline(x=datetime(2023, 6, 1), color='gray', linestyle=':', alpha=0.5)
ax.annotate('年中大促', xy=(datetime(2023, 6, 1), 200000),
xytext=(datetime(2023, 6, 15), 220000),
arrowprops=dict(arrowstyle='->', color='gray'))
ax.axvline(x=datetime(2023, 10, 1), color='gray', linestyle=':', alpha=0.5)
ax.annotate('双十一直播', xy=(datetime(2023, 10, 1), 450000),
xytext=(datetime(2023, 10, 10), 480000),
arrowprops=dict(arrowstyle='->', color='gray'))
# 设置x轴格式
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax.xaxis.set_major_locator(mdates.MonthLocator())
plt.xticks(rotation=45)
ax.set_title('2023年1-9月销售趋势分析', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('日期', fontsize=12)
ax.set_ylabel('累计销售额(元)', fontsize=12)
ax.legend(loc='lower right', fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('sales_trend.png', dpi=150, bbox_inches='tight')
plt.show()
这张图看起来简单,但里面包含了几个关键的信息:趋势走向、目标对比、关键事件标记。在实际工作中,我经常会把这样的图放进给老板的汇报PPT里。老板可能看不懂pandas代码,但他能看懂这条线和那条线之间的关系,以及几个关键时间点的标注。
用户画像可视化:更丰富的表达
# 模拟用户数据
np.random.seed(24)
user_age = np.random.normal(32, 10, 1000).clip(18, 60).astype(int)
user_monthly_spend = np.random.exponential(800, 1000)
user_purchase_freq = np.random.poisson(3, 1000)
user_gender = np.random.choice(['男', '女'], 1000, p=[0.45, 0.55])
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 年龄分布
axes[0, 0].hist(user_age, bins=30, color='#780000', alpha=0.7, edgecolor='white')
axes[0, 0].set_title('用户年龄分布', fontsize=14, fontweight='bold')
axes[0, 0].set_xlabel('年龄')
axes[0, 0].set_ylabel('用户数')
axes[0, 0].axvline(np.mean(user_age), color='red', linestyle='--', label=f'平均年龄: {np.mean(user_age):.1f}')
axes[0, 0].legend()
# 月消费金额分布(对数刻度)
axes[0, 1].hist(user_monthly_spend, bins=50, color='#06A77D', alpha=0.7, edgecolor='white')
axes[0, 1].set_title('月消费金额分布(对数刻度)', fontsize=14, fontweight='bold')
axes[0, 1].set_xlabel('月消费金额(元)')
axes[0, 1].set_ylabel('用户数')
axes[0, 1].set_xscale('log')
# 购买频次分布
axes[1, 0].bar(range(0, 15), [np.sum(user_purchase_freq == i) for i in range(0, 15)],
color='#F38630', alpha=0.8, edgecolor='white')
axes[1, 0].set_title('月购买频次分布', fontsize=14, fontweight='bold')
axes[1, 0].set_xlabel('购买频次(次/月)')
axes[1, 0].set_ylabel('用户数')
axes[1, 0].axvline(np.mean(user_purchase_freq), color='red', linestyle='--',
label=f'平均频次: {np.mean(user_purchase_freq):.1f}')
axes[1, 0].legend()
# 性别分布饼图
gender_counts = np.array([np.sum(user_gender == '男'), np.sum(user_gender == '女')])
axes[1, 1].pie(gender_counts, labels=['男', '女'], autopct='%1.1f%%',
colors=['#3B5998', '#E91E63'], startangle=90)
axes[1, 1].set_title('用户性别分布', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('user_profile.png', dpi=150, bbox_inches='tight')
plt.show()
这套可视化方案我已经在多个项目中用过。它的逻辑是:先展示整体分布,再细分维度。老板一眼就能看出用户群体的核心特征——年龄集中在25-40岁、月消费多在200-2000元之间、平均每月购买3次左右。这些洞察可以直接指导后续的运营策略。
sklearn:让机器学习真正落地
sklearn是Python里最流行的机器学习库,它的设计哲学是一致、简洁、高效。对于电商场景,我们最常用到的几个模型是:线性回归(预测销售额)、决策树/随机森林(用户分类)、KMeans聚类(用户分群)。
销售预测:从线性回归开始
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# 模拟特征数据:广告投入、促销活动天数、节假日标记、季节指数
np.random.seed(42)
n_samples = 365
features = np.column_stack([
np.random.exponential(5000, n_samples), # 广告投入
np.random.poisson(2, n_samples), # 促销活动天数
np.random.randint(0, 2, n_samples), # 是否节假日
np.sin(np.linspace(0, 4*np.pi, n_samples)) ** 2 # 季节指数(周期性)
])
# 模拟销售额(基于特征的真实关系 + 噪声)
sales = (
features[:, 0] * 0.8 + # 广告投入的影响
features[:, 1] * 1500 + # 促销活动的影响
features[:, 2] * 3000 + # 节假日的影响
features[:, 3] * 5000 + # 季节影响
np.random.normal(0, 500, n_samples) # 随机噪声
)
# 划分训练集和测试集(按时间顺序,不要随机打乱!)
split_idx = int(len(features) * 0.8)
X_train, X_test = features[:split_idx], features[split_idx:]
y_train, y_test = sales[:split_idx], sales[split_idx:]
# 训练线性回归模型
model = LinearRegression()
model.fit(X_train, y_train)
# 预测
y_pred = model.predict(X_test)
# 评估
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
print(f"模型评估结果:")
print(f" RMSE: {rmse:.2f} 元")
print(f" R²: {r2:.4f}")
print(f"\n特征系数:")
feature_names = ['广告投入', '促销活动天数', '节假日', '季节指数']
for name, coef in zip(feature_names, model.coef_):
print(f" {name}: {coef:.4f}")
print(f" 截距: {model.intercept_:.2f}")
这里有一个很多人容易忽略的细节:时间序列数据不能随机划分训练集和测试集。上面的代码我特意按时间顺序划分,前80%作为训练集,后20%作为测试集。如果随机打乱,就会出现”用未来的数据预测过去”的逻辑错误,评估结果会虚高,上线后实际效果会大打折扣。
用户分群:KMeans聚类实战
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# 用户特征数据:年龄、月消费额、购买频次、最近一次购买距今天数
np.random.seed(24)
user_data = np.column_stack([
np.random.normal(32, 10, 5000).clip(18, 60), # 年龄
np.random.exponential(800, 5000), # 月消费额
np.random.poisson(3, 5000), # 购买频次
np.random.exponential(30, 5000) # 最近购买距今(天)
])
# 标准化(KMeans对量纲敏感,必须标准化!)
scaler = StandardScaler()
user_data_scaled = scaler.fit_transform(user_data)
# 用肘部法则确定最佳聚类数
inertias = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(user_data_scaled)
inertias.append(kmeans.inertia_)
# 找到"肘部"对应的k值
elbow_idx = np.argmin(np.diff(np.diff(inertias))) + 2
best_k = elbow_idx
print(f"肘部法则推荐的最佳聚类数: {best_k}")
# 最终聚类
kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
user_data['聚类标签'] = kmeans.fit_predict(user_data_scaled)
# 分析每个簇的特征
cluster_summary = user_data.groupby('聚类标签').agg({
0: ['mean', 'std'],
1: ['mean', 'std'],
2: ['mean', 'std'],
3: ['mean', 'std'],
'聚类标签': 'count'
}).round(2)
print("\n各用户群特征分析:")
print(cluster_summary)
# 为每个簇命名(基于业务理解)
cluster_names = {
0: '高价值活跃用户',
1: '潜在高价值用户',
2: '低活跃度用户',
3: '流失风险用户'
}
for i in range(best_k):
cluster_data = user_data[user_data['聚类标签'] == i]
name = cluster_names.get(i, f'簇{i}')
print(f"\n{name}({len(cluster_data)}人):")
print(f" 平均年龄: {cluster_data[0].mean():.1f}")
print(f" 月均消费: {cluster_data[1].mean():.0f}元")
print(f" 月均购买: {cluster_data[2].mean():.1f}次")
print(f" 距最近购买: {cluster_data[3].mean():.1f}天")
这个用户分群结果,可以直接用来指导运营策略。比如”高价值活跃用户”可以做VIP专属活动,”流失风险用户”需要激活召回。我曾用这套方法帮一个客户识别出了23%的流失风险用户,通过针对性的优惠券策略,两个月内挽回了其中60%的用户,直接带来数十万的增量收入。
完整实战案例:从零搭建销售预测系统
下面我把前面讲的所有知识点串联起来,做一个完整的实战案例。这个案例模拟了一个真实的电商销售预测场景。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')
# 设置图表样式
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
class EcommerceSalesPredictor:
"""电商销售预测器——整合pandas+numpy+matplotlib+sklearn"""
def __init__(self, data_path=None):
"""初始化预测器"""
self.data = None
self.model = None
self.scaler = StandardScaler()
if data_path:
self.load_data(data_path)
def load_data(self, data_path):
"""加载并初步清洗数据"""
print(f"📂 正在加载数据: {data_path}")
self.data = pd.read_excel(data_path)
# 数据质量检查
print(f"📊 数据维度: {self.data.shape}")
print(f"🔍 缺失值:\n{self.data.isnull().sum()[self.data.isnull().sum() > 0]}")
print(f"✅ 数据加载完成")
return self
def feature_engineering(self):
"""特征工程——这是预测准确性的关键"""
print("\n🔧 开始特征工程...")
df = self.data.copy()
# 1. 时间特征提取
df['日期'] = pd.to_datetime(df['日期'])
df['月份'] = df['日期'].dt.month
df['季度'] = df['日期'].dt.quarter
df['星期'] = df['日期'].dt.dayofweek
df['是否周末'] = (df['星期'] >= 5).astype(int)
df['是否节假日'] = df['是否节假日'].astype(int)
# 2. 周期性特征(用正弦余弦编码处理月份和星期)
df['月份_sin'] = np.sin(2 * np.pi * df['月份'] / 12)
df['月份_cos'] = np.cos(2 * np.pi * df['月份'] / 12)
df['星期_sin'] = np.sin(2 * np.pi * df['星期'] / 7)
df['星期_cos'] = np.cos(2 * np.pi * df['星期'] / 7)
# 3. 滞后特征(用过去7天、14天、30天的销售额作为特征)
for lag in [7, 14, 30]:
df[f'销售额_滞后{lag}天'] = df['销售额'].shift(lag)
# 4. 滚动统计特征
df['销售额_滚动7天均值'] = df['销售额'].rolling(window=7).mean()
df['销售额_滚动14天均值'] = df['销售额'].rolling(window=14).mean()
df['销售额_滚动30天均值'] = df['销售额'].rolling(window=30).mean()
# 5. 滑动窗口标准差(衡量波动性)
df['销售额_滚动7天标准差'] = df['销售额'].rolling(window=7).std()
# 6. 环比变化率
df['销售额_环比变化'] = df['销售额'].pct_change() * 100
# 删除含NaN的行(滞后和滚动特征会产生)
df = df.dropna().reset_index(drop=True)
print(f"📈 特征工程完成,剩余{len(df)}条有效数据")
# 定义特征列和目标列
feature_cols = [col for col in df.columns if col not in ['日期', '销售额']]
target_col = '销售额'
self.features = feature_cols
self.target = target_col
self.df_processed = df
print(f"🎯 特征数量: {len(feature_cols)}")
print(f"📋 特征列表: {feature_cols}")
return self
def train_model(self):
"""训练预测模型"""
print("\n🤖 开始训练模型...")
X = self.df_processed[self.features]
y = self.df_processed[self.target]
# 按时间划分训练集和测试集(不要随机打乱!)
split_idx = int(len(X) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
# 标准化特征
X_train_scaled = self.scaler.fit_transform(X_train)
X_test_scaled = self.scaler.transform(X_test)
# 训练随机森林模型
self.model = RandomForestRegressor(
n_estimators=100,
max_depth=15,
min_samples_split=5,
min_samples_leaf=2,
random_state=42,
n_jobs=-1
)
self.model.fit(X_train_scaled, y_train)
# 预测和评估
y_pred = self.model.predict(X_test_scaled)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
mae = np.mean(np.abs(y_test - y_pred))
self.metrics = {
'RMSE': rmse,
'MAE': mae,
'R²': r2
}
print(f"✅ 模型训练完成!")
print(f"📊 评估结果:")
print(f" RMSE: {rmse:.2f} 元")
print(f" MAE: {mae:.2f} 元")
print(f" R²: {r2:.4f}")
# 特征重要性
importance_df = pd.DataFrame({
'特征': self.features,
'重要性': self.model.feature_importances_
}).sort_values('重要性', ascending=False)
print(f"\n🔑 最重要的5个特征:")
for idx, row in importance_df.head(5).iterrows():
print(f" {row['特征']}: {row['重要性']:.4f}")
self.importance_df = importance_df
return self
def visualize_results(self):
"""可视化预测结果"""
print("\n📊 生成可视化图表...")
X = self.df_processed[self.features]
y = self.df_processed[self.target]
split_idx = int(len(X) * 0.8)
dates = self.df_processed['日期'].values
# 重新预测以获取完整的预测序列
X_all_scaled = self.scaler.transform(X)
y_pred_all = self.model.predict(X_all_scaled)
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# 图1:实际值vs预测值对比
axes[0, 0].plot(dates[split_idx:], y.values[split_idx:],
label='实际值', color='#E94F37', linewidth=2)
axes[0, 0].plot(dates[split_idx:], y_pred_all[split_idx:],
label='预测值', color='#2E86AB', linewidth=2, alpha=0.8)
axes[0, 0].set_title('销售预测对比:实际值 vs 预测值', fontsize=14, fontweight='bold')
axes[0, 0].set_xlabel('日期')
axes[0, 0].set_ylabel('销售额(元)')
axes[0, 0].legend()
axes[0, 0].tick_params(axis='x', rotation=45)
# 图2:预测误差分布
errors = y.values[split_idx:] - y_pred_all[split_idx:]
axes[0, 1].hist(errors, bins=30, color='#780000', alpha=0.7, edgecolor='white')
axes[0, 1].axvline(0, color='black', linestyle='--', linewidth=2)
axes[0, 1].axvline(np.mean(errors), color='red', linestyle=':',
label=f'平均误差: {np.mean(errors):.0f}')
axes[0, 1].set_title('预测误差分布', fontsize=14, fontweight='bold')
axes[0, 1].set_xlabel('误差(元)')
axes[0, 1].set_ylabel('频次')
axes[0, 1].legend()
# 图3:特征重要性
top_features = self.importance_df.head(10)
bars = axes[1, 0].barh(range(len(top_features)), top_features['重要性'].values,
color='#06A77D')
axes[1, 0].set_yticks(range(len(top_features)))
axes[1, 0].set_yticklabels(top_features['特征'].values, fontsize=9)
axes[1, 0].set_title('Top 10 特征重要性', fontsize=14, fontweight='bold')
axes[1, 0].invert_yaxis()
# 图4:模型性能指标
metrics = self.metrics
axes[1, 1].bar(['RMSE(百元)', 'MAE(百元)', 'R²'],
[metrics['RMSE']/100, metrics['MAE']/100, metrics['R²']],
color=['#E94F37', '#F38630', '#2E86AB'])
axes[1, 1].set_title('模型性能指标', fontsize=14, fontweight='bold')
axes[1, 1].set_ylabel('数值')
for i, (k, v) in enumerate(metrics.items()):
if k == 'R²':
axes[1, 1].text(i, v + 0.02, f'{v:.4f}', ha='center', fontweight='bold')
else:
axes[1, 1].text(i, v/100 + 0.5, f'{v:.0f}元', ha='center', fontweight='bold')
plt.tight_layout()
plt.savefig('sales_prediction_results.png', dpi=150, bbox_inches='tight')
print("💾 图表已保存: sales_prediction_results.png")
plt.show()
return self
def predict_future(self, future_features, days=30):
"""预测未来销售"""
if self.model is None:
raise ValueError("请先训练模型")
future_features_scaled = self.scaler.transform(future_features)
predictions = self.model.predict(future_features_scaled)
print(f"\n📈 未来{days}天销售预测:")
for i, pred in enumerate(predictions[:days]):
print(f" 第{i+1}天: ¥{pred:,.2f}")
return predictions
def run_full_pipeline(self, data_path=None):
"""运行完整预测流程"""
print("=" * 60)
print("🚀 电商销售预测系统启动")
print("=" * 60)
if data_path:
self.load_data(data_path)
self.feature_engineering()
self.train_model()
self.visualize_results()
print("\n✅ 完整流程执行完毕!")
return self
# 使用示例
# predictor = EcommerceSalesPredictor()
# predictor.run_full_pipeline('sales_data.xlsx')
这个完整的预测系统,把前面讲的所有知识点都串联起来了。从数据加载、清洗、特征工程,到模型训练、评估、可视化,形成了一个完整的闭环。
几个让我刻骨铭心的教训
教训一:数据格式转换的”坑”
有一次我处理一个销售数据,有一列日期格式是2023/10/1,另一列是2023-10-01。我当时直接pd.to_datetime()就用了,结果两列的日期类型不一致,合并时出现大量NaN。后来查了三天才找到原因——不同格式的日期字符串,to_datetime()处理后的dtype可能不同。
教训二:模型评估不能只看一个指标
我见过太多人用R²来评价模型,但R²高不代表模型准确。有时候R²能达到0.9,但RMSE可能高达几万——这意味着模型的预测值和真实值之间有一个固定的系统性偏差。我现在的习惯是同时看RMSE、MAE、R²三个指标,并且要结合业务场景来判断是否可接受。
教训三:特征工程比模型选择更重要
随机森林、XGBoost、LightGBM……模型怎么选?我的经验是:在电商销售预测这个场景下,模型之间的差距远小于特征工程带来的差距。花80%的时间做特征工程,20%的时间调模型,这个投入产出比是最划算的。
给你的学习建议
如果你也想系统掌握这套技能,我的建议是:
第一,不要追求”学完再动手”。 很多教程喜欢先把所有知识点讲完再给案例,但这样学完你就忘了。我的建议是边学边做,遇到不懂的概念马上在真实数据上验证。
第二,一定要用真实数据练习。 网上公开的Titanic数据集固然经典,但它解决不了你的实际问题。尽量找一些真实的业务数据来练手,哪怕数据很脏、很乱——因为这才是真实世界的数据。
第三,建立自己的”检查清单”。 每次拿到数据,先过一遍数据质量检查清单(缺失值、异常值、重复值、格式问题),然后再做分析。这个习惯能帮你避免90%的低级错误。
第四,学会解释你的模型。 老板和业务方不会关心你的模型用了什么算法,他们只关心”这个预测准不准”和”为什么会这样”。学会用业务语言解释模型结果,比会写代码更重要。
那个让我亏掉十万的夜晚,现在回想起来,反而成了我职业生涯的转折点。它让我真正理解了数据的力量——它既能帮你发现机会,也能因为你的疏忽而把你拖入深渊。
希望这些经验能帮你少走一些弯路。数据分析这条路,慢慢走,比较快。
