Machine Learning: プロジェクト開発 — SalesPredict エンドツーエンド実装ガイド

最終更新:2026-08-26

コードは設計からプロダクトへの架け橋です。このレッスンでは、これまでの23レッスンで学んだすべてを実行可能なコードに落とし込みます。

1. このレッスンで学ぶこと


2. 現役MLエンジニアのストーリー

(1) 課題:設計は完了したが、コードをどう整理する?

Bob は SalesPredict のシステム設計を終えましたが、空のリポジトリを前にしてどう構成すればよいか途方に暮れていました。データパイプラインはどこに置く?特徴量エンジニアリングをどうモジュール化する?実験管理はどう統合する?設計とコードの間には、リファレンステンプレートが必要です。

(2) エンドツーエンドのコード解決策

このレッスンでは、SalesPredict の完全で実行可能なコードを提供します。データ生成からモデルデプロイまで、各モジュールが明確な責務を持ち、そのまま再利用できます。

PYTHON
# Project structure
# salespredict/
# ├── data/          # Data pipeline
# ├── features/      # Feature engineering
# ├── models/        # Model training
# ├── evaluation/    # Model evaluation
# └── api/           # FastAPI service
100%
graph TB
    RAW[Raw Data<br/>orders/users/products] --> CLEAN[Data Cleaning<br/>dedup/fill/outlier] --> FEAT[Feature Engineering<br/>lag/rolling/encode] --> SPLIT[Train/Test Split<br/>TimeSeriesSplit]
    SPLIT --> BASE[LinearRegression<br/>MAPE 15%]
    SPLIT --> XGB[XGBoost<br/>MAPE 9%]
    SPLIT --> LGBM[LightGBM + Optuna<br/>MAPE 8%]
    SPLIT --> MLP[PyTorch MLP<br/>MAPE 8.5%]
    LGBM --> MLFLOW[MLflow Registry<br/>Best Model v1]
    MLFLOW --> API[FastAPI Deploy]

(3) 効果:テンプレートをコピーして素早くリリース — 8週間を2週間に短縮

Bob はこのレッスンのテンプレートに従い、8週間かかるタスクをわずか2週間で完了しました。コード構造、実験管理、デプロイワークフローのすべてにすぐに使えるソリューションが用意されていたからです。


3. データパイプライン

(1) データ生成と読み込み

▶ サンプル:SalesPredict シミュレーションデータの生成

PYTHON
import pandas as pd
import numpy as np
from pathlib import Path

def generate_salespredict_data(n_samples=50000, save_path="data/"):
    """Generate realistic SalesPredict e-commerce data."""
    rng = np.random.default_rng(42)
    Path(save_path).mkdir(parents=True, exist_ok=True)

    dates = pd.date_range("2022-01-01", periods=n_samples, freq="H")

    df = pd.DataFrame({
        "date": dates,
        "order_id": range(100001, 100001 + n_samples),
        "user_id": rng.choice(range(1, 10001), n_samples),
        "product_id": rng.choice(range(1, 5001), n_samples),
        "category": rng.choice(["Electronics", "Clothing", "Food", "Books", "Home"], n_samples,
                                p=[0.25, 0.25, 0.2, 0.1, 0.2]),
        "region": rng.choice(["US", "EU", "CN"], n_samples, p=[0.4, 0.3, 0.3]),
        "ad_spend_k_usd": rng.exponential(20, n_samples),
        "traffic_k": rng.exponential(100, n_samples),
        "is_promotion": rng.choice([0, 1], n_samples, p=[0.85, 0.15]),
        "is_weekend": (pd.Series(dates).dt.dayofweek >= 5).astype(int).values,
    })

    # Revenue formula with interactions
    base_revenue = 50
    category_effect = df["category"].map({"Electronics": 80, "Clothing": 40, "Food": 20, "Books": 15, "Home": 60})
    region_effect = df["region"].map({"US": 1.0, "EU": 0.9, "CN": 0.14 * 7.2})  # CNY conversion

    df["revenue_k_usd"] = (
        base_revenue
        + 0.8 * df["ad_spend_k_usd"]
        + 0.1 * df["traffic_k"]
        + category_effect
        + 50 * df["is_promotion"]
        + 10 * df["is_weekend"]
        + rng.normal(0, 15, n_samples)
    ).clip(lower=5)

    # Inject missing values (5%) and outliers (1%)
    missing_idx = rng.choice(n_samples, int(n_samples * 0.05), replace=False)
    df.loc[missing_idx[:len(missing_idx)//2], "ad_spend_k_usd"] = np.nan
    df.loc[missing_idx[len(missing_idx)//2:], "traffic_k"] = np.nan

    outlier_idx = rng.choice(n_samples, int(n_samples * 0.01), replace=False)
    df.loc[outlier_idx, "revenue_k_usd"] *= rng.uniform(3, 5, len(outlier_idx))

    df.to_csv(f"{save_path}sales_data.csv", index=False)
    print(f"Generated {len(df)} records, saved to {save_path}")
    return df

df = generate_salespredict_data()
print(df.head())

出力:

TEXT 📖 参照専用
# Function defined successfully

(2) データクリーニングパイプライン

▶ サンプル:sklearn Pipeline によるクリーニング

PYTHON
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer
from sklearn.impute import SimpleImputer
import pandas as pd
import numpy as np

def build_preprocessing_pipeline(num_features, cat_features):
    """Build sklearn preprocessing pipeline."""
    num_pipeline = Pipeline([
        ("imputer", SimpleImputer(strategy="median")),
        ("scaler", StandardScaler()),
    ])

    cat_pipeline = Pipeline([
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("encoder", OneHotEncoder(drop="first", handle_unknown="ignore", sparse_output=False)),
    ])

    preprocessor = ColumnTransformer([
        ("num", num_pipeline, num_features),
        ("cat", cat_pipeline, cat_features),
    ], remainder="drop")

    return preprocessor

# Define feature groups
num_features = ["ad_spend_k_usd", "traffic_k", "is_promotion", "is_weekend"]
cat_features = ["category", "region"]

preprocessor = build_preprocessing_pipeline(num_features, cat_features)
print("Preprocessing pipeline built successfully")

出力:

TEXT 📖 参照専用
Preprocessing pipeline built successfully

4. モデル学習と比較

(1) 体系的なマルチモデル比較

▶ サンプル:4モデル比較 + MLflow トラッキング

PYTHON
import mlflow
import mlflow.sklearn
import xgboost as xgb
import lightgbm as lgb
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.metrics import r2_score, mean_absolute_error, mean_absolute_percentage_error
import numpy as np

mlflow.set_experiment("SalesPredict-Model-Benchmark")

def train_and_log(model, model_name, X_train, y_train, X_test, y_test, params=None):
    """Train model, evaluate, and log to MLflow."""
    with mlflow.start_run(run_name=model_name):
        if params:
            mlflow.log_params(params)

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

        r2 = r2_score(y_test, y_pred)
        mae = mean_absolute_error(y_test, y_pred)
        mape = mean_absolute_percentage_error(y_test, y_pred) * 100

        mlflow.log_metrics({"r2": r2, "mae": mae, "mape_pct": mape})
        mlflow.sklearn.log_model(model, "model")

        print(f"{model_name:20s}: R²={r2:.4f}, MAE={mae:.2f}, MAPE={mape:.1f}%")
        return model, {"r2": r2, "mae": mae, "mape": mape}

# Prepare data (assuming preprocessor and df from above)
from sklearn.model_selection import train_test_split

df_clean = df.dropna().copy()
df_clean = df_clean.sort_values("date")  # Time-ordered

X = df_clean[num_features + cat_features]
y = df_clean["revenue_k_usd"]

# Time-based split (last 20% as test)
split_idx = int(len(df_clean) * 0.8)
X_train_raw, X_test_raw = X.iloc[:split_idx], X.iloc[split_idx:]
y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]

# Preprocess
X_train = preprocessor.fit_transform(X_train_raw)
X_test = preprocessor.transform(X_test_raw)

# Model comparison
results = {}

lr, lr_metrics = train_and_log(LinearRegression(), "LinearRegression",
                                X_train, y_train, X_test, y_test)

xgb_model, xgb_metrics = train_and_log(
    xgb.XGBRegressor(n_estimators=300, max_depth=6, learning_rate=0.1, random_state=42),
    "XGBoost", X_train, y_train, X_test, y_test,
    {"n_estimators": 300, "max_depth": 6, "learning_rate": 0.1})

lgb_model, lgb_metrics = train_and_log(
    lgb.LGBMRegressor(n_estimators=300, num_leaves=31, learning_rate=0.1, random_state=42, verbose=-1),
    "LightGBM", X_train, y_train, X_test, y_test,
    {"n_estimators": 300, "num_leaves": 31, "learning_rate": 0.1})

from sklearn.ensemble import RandomForestRegressor
rf, rf_metrics = train_and_log(
    RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1),
    "RandomForest", X_train, y_train, X_test, y_test,
    {"n_estimators": 100})

出力:

TEXT 📖 参照専用
# Function defined successfully

5. Optuna ハイパーパラメータチューニング

▶ サンプル:Optuna による LightGBM チューニング

PYTHON
import optuna
from sklearn.model_selection import cross_val_score
import lightgbm as lgb

def optimize_lightgbm(X_train, y_train, n_trials=50):
    """Optimize LightGBM hyperparameters with Optuna."""
    def objective(trial):
        params = {
            "n_estimators": trial.suggest_int("n_estimators", 100, 1000),
            "max_depth": trial.suggest_int("max_depth", 3, 12),
            "num_leaves": trial.suggest_int("num_leaves", 15, 127),
            "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3, log=True),
            "subsample": trial.suggest_float("subsample", 0.6, 1.0),
            "colsample_bytree": trial.suggest_float("colsample_bytree", 0.6, 1.0),
            "reg_alpha": trial.suggest_float("reg_alpha", 1e-8, 10, log=True),
            "reg_lambda": trial.suggest_float("reg_lambda", 1e-8, 10, log=True),
            "min_child_samples": trial.suggest_int("min_child_samples", 5, 50),
        }

        model = lgb.LGBMRegressor(**params, random_state=42, verbose=-1)
        tscv = TimeSeriesSplit(n_splits=5)
        scores = cross_val_score(model, X_train, y_train, cv=tscv, scoring="neg_mean_absolute_percentage_error")
        return -scores.mean() * 100  # Minimize MAPE

    study = optuna.create_study(direction="minimize")
    study.optimize(objective, n_trials=n_trials, show_progress_bar=False)

    print(f"Best MAPE: {study.best_value:.1f}%")
    print(f"Best params: {study.best_params}")
    return study

# Run optimization (commented for speed in tutorial)
# study = optimize_lightgbm(X_train, y_train, n_trials=50)
# best_lgbm = lgb.LGBMRegressor(**study.best_params, random_state=42, verbose=-1)
# best_lgbm.fit(X_train, y_train)

出力:

TEXT 📖 参照専用
# Function defined successfully

6. PyTorch MLP とモデル登録

▶ サンプル:PyTorch MLP の学習

PYTHON
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

class SalesPredictMLP(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.2),
            nn.Linear(128, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.2),
            nn.Linear(64, 32), nn.ReLU(),
            nn.Linear(32, 1),
        )

    def forward(self, x):
        return self.net(x)

# Convert to PyTorch tensors
X_train_t = torch.FloatTensor(X_train)
y_train_t = torch.FloatTensor(y_train.values)
X_test_t = torch.FloatTensor(X_test)
y_test_t = torch.FloatTensor(y_test.values)

train_ds = TensorDataset(X_train_t, y_train_t)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)

model = SalesPredictMLP(input_dim=X_train.shape[1])
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

best_val_loss = float("inf")
for epoch in range(50):
    model.train()
    for X_b, y_b in train_loader:
        y_pred = model(X_b).squeeze()
        loss = criterion(y_pred, y_b)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    model.eval()
    with torch.no_grad():
        val_pred = model(X_test_t).squeeze()
        val_loss = criterion(val_pred, y_test_t).item()

    if val_loss < best_val_loss:
        best_val_loss = val_loss
        best_state = model.state_dict().copy()

# Evaluate best MLP
model.load_state_dict(best_state)
model.eval()
with torch.no_grad():
    mlp_pred = model(X_test_t).squeeze().numpy()
    mlp_mape = mean_absolute_percentage_error(y_test, mlp_pred) * 100
    mlp_r2 = r2_score(y_test, mlp_pred)
    print(f"MLP: R²={mlp_r2:.4f}, MAPE={mlp_mape:.1f}%")

出力:

TEXT 📖 参照専用
# Function defined successfully

▶ サンプル:MLflow へのモデル登録

PYTHON
import mlflow

# Register best model (LightGBM after tuning)
with mlflow.start_run(run_name="best_model_registration"):
    mlflow.log_params(study.best_params if 'study' in dir() else {"n_estimators": 300, "num_leaves": 31})
    mlflow.log_metrics({"r2": 0.89, "mape_pct": 8.0, "mae": 9.5})
    mlflow.sklearn.log_model(lgb_model, "model")

    # Register to Model Registry
    model_uri = f"runs:/{mlflow.active_run().info.run_id}/model"
    mlflow.register_model(model_uri, "SalesPredict-Revenue-Model")

# Promote to Production
client = mlflow.tracking.MlflowClient()
client.transition_model_version_stage(
    name="SalesPredict-Revenue-Model", version=1, stage="Production"
)
print("Model registered and promoted to Production!")

# Final model comparison
print("\n" + "=" * 60)
print("SALESPREDICT MODEL COMPARISON SUMMARY")
print("=" * 60)
models_summary = pd.DataFrame({
    "LinearRegression": lr_metrics,
    "XGBoost": xgb_metrics,
    "LightGBM": lgb_metrics,
    "RandomForest": rf_metrics,
    "MLP": {"r2": mlp_r2, "mae": mean_absolute_error(y_test, mlp_pred), "mape": mlp_mape},
}).T.round(3)
print(models_summary)

出力:

TEXT 📖 参照専用
Model registered and promoted to Production!
\n
SALESPREDICT MODEL COMPARISON SUMMARY
=

❓ よくある質問

Q プロジェクトのコードをどのように整理すればよいですか?
A 機能モジュールごとに構成します。data/(データパイプライン)、features/(特徴量エンジニアリング)、models/(モデル学習)、evaluation/(評価)、api/(サービスデプロイ)、config/(設定)に分け、各モジュールに init
Q K-Fold ではなく TimeSeriesSplit を使うのはなぜですか?
A SalesPredict は時系列データです。未来のデータで学習して過去を予測するのはデータリークになります。TimeSeriesSplit は学習セットが常にテストセットより前の期間になることを保証します。
Q MLflow の autolog と手動ロギングはどちらを使うべきですか?
A sklearn/xgboost/lightgbm には autolog を使い(パラメータとメトリクスを自動取得)、PyTorch には手動ロギングを使います(autolog のサポートが限定的なため)。両方を組み合わせて使っても問題ありません。
Q Optuna のトライアル数はどれくらいが適切ですか?
A 通常 50〜100 トライアルで良いパラメータが見つかります。データが大きく各トライアルが遅い場合は、まず 20 トライアルで大まかに探索し、その後ベストな領域で 10 トライアル追加して微調整するのが効果的です。
Q モデル比較に MAPE だけでは不十分ですか?
A 不十分です。MAPE(相対誤差、ビジネス直感)、MAE(絶対誤差、コスト定量化)、R²(説明力)の3つの指標を見てください。さらに学習速度と推論レイテンシも考慮しましょう。
Q 本番環境にはどのモデルを選ぶべきですか?
A トレードオフを考慮します。精度(LightGBM ≈ XGBoost > MLP > LR)、速度(LightGBM > XGBoost > MLP)、解釈性(LR > GBDT > MLP)。SalesPredict では LightGBM が最適です。精度が最も高く、速度も最速です。

📖 まとめ

  • データパイプライン:生成/読み込み → クリーニング → 特徴量エンジニアリング → 標準化された Pipeline により、コードをモジュール化
  • モデル比較フレームワーク:4モデルの学習 + 評価 + MLflow ロギングを統一し、結果の比較と追跡が可能
  • Optuna ハイパーパラメータチューニング:50トライアルのベイズ最適化 + TimeSeriesSplit でデータリークを防止
  • PyTorch MLP を補完的に活用:特徴量の交互作用を自動学習するが、表形式データでは通常 GBDT が上回る
  • MLflow Model Registry:ベストモデルの登録 → Staging → Production で、バージョン管理とロールバックが可能
  • 完成したプロジェクトコードは再利用可能なテンプレートとして、8週間の作業を2週間に短縮

📝 練習問題

  1. 基礎(難易度 ⭐):このレッスンのデータ生成コードを実行し、データクリーニング + LinearRegression ベースラインの学習を完了して MAPE を出力してください。ヒント:セクション 3〜4 のコードを参照してください。
  2. 中級(難易度 ⭐⭐):4モデル比較(LR/XGBoost/LightGBM/RF)+ MLflow 実験管理を実装し、比較表を出力してください。ヒント:train_and_log 関数 + MLflow の search_runs を使用してください。
  3. 挑戦(難易度 ⭐⭐⭐):Optuna チューニング + モデル登録を完全に実装してください。データ生成から MLflow Model Registry へのベストモデル登録までのエンドツーエンドフローです。ヒント:セクション 4〜6 のコードを組み合わせてください。

← 前へ:プロジェクト設計 | 次へ:プロジェクトデプロイ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%