Machine Learning: 综合练习 — 入门项目 — SalesPredict数据分析与基线模型

学了5课工具,现在是时候把它们串起来,完成一个真实的ML项目了。

1. 你将学到


2. 一个ML项目新人的真实故事

(1) 痛点:学了一堆工具但不会串起来

Bob学完了NumPy、Pandas、Seaborn、Sklearn,但当面对真实的SalesPredict数据时,他不知道该从哪步开始——先清洗还是先可视化?用哪个模型?怎么判断模型好不好?工具会了,但项目流程不清。

(2) 端到端流程的解法

ML项目遵循固定流程:加载 → 探索 → 清洗 → 特征 → 基线 → 评估 → 迭代。基线模型先跑通,再逐步优化。

PYTHON
# End-to-end ML workflow template
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, r2_score

# Step 1: Load & Explore
df = pd.read_csv("sales_data.csv")
print(df.describe())

# Step 2: Clean & Prepare features
features = ["ad_spend", "traffic", "last_month_sales"]
target = "monthly_revenue"
df = df.dropna(subset=features + [target])

# Step 3: Train baseline
X_train, X_test, y_train, y_test = train_test_split(df[features], df[target], test_size=0.2)
model = LinearRegression().fit(X_train, y_train)
pred = model.predict(X_test)

# Step 4: Evaluate
print(f"R²: {r2_score(y_test, pred):.3f}, MAE: {mean_absolute_error(y_test, pred):.0f} USD")

(3) 收益:30分钟跑通第一个端到端模型

Bob按照标准流程,30分钟就跑通了第一个基线模型——虽然R²只有0.72,但有了基线,后续每一步改进都有参照。


3. 端到端ML工作流

(1) 项目流程全景

100%
graph LR
    A[1. Load Data] --> B[2. EDA]
    B --> C[3. Clean Data]
    C --> D[4. Feature Selection]
    D --> E[5. Train Baseline]
    E --> F[6. Evaluate]
    F --> G{Good Enough?}
    G -->|No| H[7. Iterate<br/>Better Features / Model]
    H --> D
    G -->|Yes| I[8. Deploy]

▶ 示例:生成SalesPredict模拟数据

PYTHON
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
n = 1000

df = pd.DataFrame({
    "date": pd.date_range("2023-01-01", periods=n, freq="D"),
    "ad_spend_k": rng.uniform(5, 100, n),
    "traffic_k": rng.uniform(10, 500, n),
    "category": rng.choice(["Electronics", "Clothing", "Food", "Books", "Home"], n),
    "is_promotion": rng.choice([0, 1], n, p=[0.8, 0.2]),
    "customer_count": rng.integers(50, 500, n),
})

# Revenue formula with realistic relationships
df["revenue_k"] = (
    20
    + 0.6 * df["ad_spend_k"]
    + 0.08 * df["traffic_k"]
    + 0.5 * df["customer_count"]
    + 50 * df["is_promotion"]
    + rng.normal(0, 15, n)
)
df["revenue_k"] = df["revenue_k"].clip(lower=0)

# Inject some missing values and outliers
missing_idx = rng.choice(n, size=50, replace=False)
df.loc[missing_idx[:25], "ad_spend_k"] = np.nan
df.loc[missing_idx[25:], "traffic_k"] = np.nan

outlier_idx = rng.choice(n, size=10, replace=False)
df.loc[outlier_idx, "revenue_k"] *= 5

print(f"Dataset shape: {df.shape}")
print(f"Missing values:\n{df.isnull().sum()}")
df.to_csv("salespredict_data.csv", index=False)

输出:

TEXT 📖 仅展示
# 执行成功

4. EDA探索性数据分析

(1) 数据概况

▶ 示例:全面EDA分析

PYTHON
import pandas as pd
import numpy as np

df = pd.read_csv("salespredict_data.csv", parse_dates=["date"])

# Basic overview
print("=" * 50)
print("DATA OVERVIEW")
print("=" * 50)
print(f"Shape: {df.shape}")
print(f"\nDtypes:\n{df.dtypes}")
print(f"\nBasic Stats:\n{df.describe().round(2)}")
print(f"\nMissing Values:\n{df.isnull().sum()}")
print(f"\nCategory Distribution:\n{df['category'].value_counts()}")

输出:

TEXT 📖 仅展示
=
DATA OVERVIEW
=

(2) 分布与趋势分析

▶ 示例:可视化EDA

PYTHON
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv("salespredict_data.csv", parse_dates=["date"])
sns.set_theme(style="whitegrid")

fig, axes = plt.subplots(2, 3, figsize=(18, 10))

# (1) Revenue distribution
sns.histplot(df["revenue_k"], bins=40, kde=True, ax=axes[0, 0], color="#2196F3")
axes[0, 0].set_title("Revenue Distribution (thousand USD)")

# (2) Revenue by category
sns.boxplot(data=df, x="category", y="revenue_k", ax=axes[0, 1], palette="Set2")
axes[0, 1].set_title("Revenue by Category")

# (3) Promotion effect
sns.barplot(data=df, x="is_promotion", y="revenue_k", ax=axes[0, 2], palette="Pastel1")
axes[0, 2].set_title("Promotion Effect on Revenue")

# (4) Ad spend vs Revenue scatter
axes[1, 0].scatter(df["ad_spend_k"], df["revenue_k"], alpha=0.3, s=10)
axes[1, 0].set_xlabel("Ad Spend (thousand USD)")
axes[1, 0].set_ylabel("Revenue (thousand USD)")
axes[1, 0].set_title("Ad Spend vs Revenue")

# (5) Traffic vs Revenue scatter
axes[1, 1].scatter(df["traffic_k"], df["revenue_k"], alpha=0.3, s=10, color="#4CAF50")
axes[1, 1].set_xlabel("Traffic (thousand)")
axes[1, 1].set_ylabel("Revenue (thousand USD)")
axes[1, 1].set_title("Traffic vs Revenue")

# (6) Correlation heatmap
num_cols = ["ad_spend_k", "traffic_k", "customer_count", "is_promotion", "revenue_k"]
corr = df[num_cols].corr()
sns.heatmap(corr, annot=True, fmt=".2f", cmap="RdBu_r", ax=axes[1, 2], vmin=-1, vmax=1)
axes[1, 2].set_title("Feature Correlations")

plt.tight_layout()
plt.savefig("eda_overview.png", dpi=150)

输出:

TEXT 📖 仅展示
# 执行成功

(3) EDA关键发现模板

发现编号 发现内容 业务影响 后续行动
1 促销日Revenue平均高出50k USD 促销效果显著 保留is_promotion特征
2 ad_spend与revenue相关系数0.72 广告驱动销售 核心特征
3 10个异常高值点 可能是数据录入错误 需要清洗
4 5%数据有缺失值 影响模型训练 填充或删除

5. 数据清洗与特征准备

▶ 示例:数据清洗流水线

PYTHON
import pandas as pd
import numpy as np

df = pd.read_csv("salespredict_data.csv", parse_dates=["date"])

# Step 1: Handle missing values
print(f"Before cleaning: {len(df)} rows")
df = df.dropna(subset=["revenue_k"])  # Drop if target is missing
df["ad_spend_k"] = df["ad_spend_k"].fillna(df["ad_spend_k"].median())
df["traffic_k"] = df["traffic_k"].fillna(df["traffic_k"].median())

# Step 2: Remove outliers using IQR
def remove_outliers(df, col, factor=1.5):
    q1, q3 = df[col].quantile([0.25, 0.75])
    iqr = q3 - q1
    return df[(df[col] >= q1 - factor * iqr) & (df[col] <= q3 + factor * iqr)]

df = remove_outliers(df, "revenue_k")
print(f"After cleaning: {len(df)} rows")

# Step 3: Feature engineering
df["month"] = df["date"].dt.month
df["day_of_week"] = df["date"].dt.dayofweek
df["is_weekend"] = (df["day_of_week"] >= 5).astype(int)

# Step 4: Encode categorical
df_encoded = pd.get_dummies(df, columns=["category"], drop_first=True)

# Step 5: Select features
feature_cols = [c for c in df_encoded.columns if c not in ["date", "revenue_k"]]
X = df_encoded[feature_cols]
y = df_encoded["revenue_k"]

print(f"Features: {feature_cols}")
print(f"X shape: {X.shape}, y shape: {y.shape}")

输出:

TEXT 📖 仅展示
# 函数定义成功

6. 基线模型构建与评估

▶ 示例:训练基线LinearRegression

PYTHON
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Baseline pipeline
baseline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LinearRegression()),
])

baseline.fit(X_train, y_train)
y_pred = baseline.predict(X_test)

# Comprehensive evaluation
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100

print("=" * 50)
print("BASELINE MODEL EVALUATION")
print("=" * 50)
print(f"MAE:  {mae:.2f} thousand USD")
print(f"RMSE: {rmse:.2f} thousand USD")
print(f"R²:   {r2:.4f}")
print(f"MAPE: {mape:.1f}%")

输出:

TEXT 📖 仅展示
=
BASELINE MODEL EVALUATION
=

▶ 示例:预测结果可视化

PYTHON
import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# (1) Actual vs Predicted scatter
axes[0].scatter(y_test, y_pred, alpha=0.5, s=20)
axes[0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], "r--", lw=2)
axes[0].set_xlabel("Actual Revenue (thousand USD)")
axes[0].set_ylabel("Predicted Revenue (thousand USD)")
axes[0].set_title("Actual vs Predicted")

# (2) Residual distribution
residuals = y_test - y_pred
axes[1].hist(residuals, bins=30, edgecolor="white", color="#2196F3")
axes[1].axvline(x=0, color="red", linestyle="--")
axes[1].set_xlabel("Residual (thousand USD)")
axes[1].set_title("Residual Distribution")

plt.tight_layout()
plt.savefig("baseline_evaluation.png", dpi=150)

输出:

TEXT 📖 仅展示
# 执行成功

(1) 业务指标解读

指标 基线值 业务含义 目标
MAE ~12k USD 平均预测偏差1.2万USD < 10k
RMSE ~15k USD 大误差被放大 < 12k
~0.72 解释72%的方差 > 0.85
MAPE ~15% 平均百分比误差 < 10%
📌 重点: MAPE 15%意味着Bob预测100万USD的月度销售额时,误差可能高达15万USD——这直接导致库存积压或断货。后续Phase 2-3的进阶模型将把这个误差降到8%以下。


❓ 常见问题

Q 为什么基线模型要用LinearRegression而不是更复杂的模型?
A 基线模型的目的是建立性能下限,了解数据的基本可预测性。复杂模型可能过拟合,掩盖真实信号。先用简单模型建立基准,再逐步提升。
Q MAE和RMSE该看哪个?
A 都看。MAE对异常值不敏感,反映平均表现;RMSE对大误差惩罚更重,反映最坏情况。如果RMSE远大于MAE,说明存在个别大误差。
Q MAPE 15%算好还是差?
A 取决于业务场景。电商销售预测MAPE < 10%是生产级水平,15%是基线级。但金融时序预测MAPE < 2%才算好。关键是与业务成本关联。
Q EDA应该花多少时间?
A 对于新数据集,EDA应该占项目总时间的20-30%。不要跳过EDA直接建模——你可能漏掉关键特征或数据问题。
Q 缺失值填充用中位数还是均值?
A 有异常值时用中位数(robust),近似正态分布时用均值。还可以用更高级的方法如KNNImputer或IterativeImputer。
Q 异常值该不该删除?
A 区分"真异常"和"假异常"。数据录入错误→删除;真实极端事件(如双十一)→保留并用特殊特征标记。盲目删除会丢失信息。

📖 小节


📝 作业

  1. 基础题(难度⭐):用自己的数据(或sklearn内置数据集如boston/housing),完成加载→EDA→基线模型→评估的完整流程。提示:参考第4-6节的代码模板。
  2. 进阶题(难度⭐⭐):在基线模型基础上,添加2个新特征(如ad_spend * is_promotion交互项),对比R²和MAPE是否改善。提示:用df.assign()添加新列。
  3. 挑战题(难度⭐⭐⭐):实现一个简单的模型对比框架——同时训练LinearRegression、DecisionTreeRegressor、RandomForestRegressor,输出每个模型的MAE/RMSE/R²/MAPE对比表格。提示:用字典循环多个模型,结果收集到DataFrame中。

← 上一课:Scikit-learn入门 | 下一课:线性回归 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏