データサイエンスのプロジェクトで、Claude Code を最初から最後まで使い倒している人はまだ少ないのではないでしょうか。
コード補完や簡単な分析スクリプトの生成には使っていても、「モデルのトレーニングからハイパーパラメータチューニング・本番デプロイまで、一気通貫でClaude Codeとペアプログラミングする」という経験は意外と少ないと思います。実際、私もしばらくの間、データ分析には Jupyter Notebook を使って手作業でやっていました。Claude Code との組み合わせを本格的に試してみたのは比較的最近のことです。
試してみて気づいたのは、データサイエンスのワークフローはClaude Codeと特に相性が良いということです。理由は単純で、データ分析は「仮説 → コード実行 → 観察 → 修正」のサイクルを繰り返す作業だからです。Claude Code はこのフィードバックループを大幅に短縮してくれます。
ここでは実際のMLプロジェクトで使える具体的なパターンを、動作するコード例とともに紹介します。pandas・scikit-learn・可視化から、ハイパーパラメータチューニング・MLOpsの入口まで扱います。
1. 環境セットアップ:CLAUDE.mdにデータサイエンス用の制約を定義する
まず前提として、Claude Code はターミナルから直接 Python スクリプトを実行できます。Jupyter Notebook のセル実行とは少し異なりますが、.py ファイルを作って python script.py で実行するスタイルが Claude Code との相性が良いです。
必要なライブラリをインストールするところから Claude Code に任せましょう。
# Claude Code に渡す指示
「以下のデータサイエンスプロジェクト用に requirements.txt を作成して、
仮想環境にインストールしてください。必要なライブラリ:
pandas, numpy, scikit-learn, matplotlib, seaborn, jupyterlab, optuna, shap」
Claude Code が生成する requirements.txt はこのようになります:
pandas==2.2.0
numpy==1.26.3
scikit-learn==1.4.0
matplotlib==3.8.2
seaborn==0.13.1
jupyterlab==4.1.0
optuna==3.5.0
shap==0.44.0
joblib==1.3.2
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
CLAUDE.mdにデータサイエンス固有の制約を追加する
プロジェクトルートに CLAUDE.md を置いて、Claude Code の挙動を制御します。これが後々の作業効率に大きく影響します。データサイエンスプロジェクトで私が必ず入れている指示はこちらです:
# Data Science Project Guidelines
## 技術スタック
- Python 3.12, pandas 2.2, scikit-learn 1.4, matplotlib/seaborn
- データ: data/raw/ (生データ), data/processed/ (前処理済み)
- モデル: models/ (pkl形式で保存)
## 必ず守ること
- pandas DataFrameは必ず .copy() してから変更する
- 乱数シードは常に RANDOM_STATE = 42 を使う
- データリーク防止のため train_test_split は前処理のfit前に行う
- モデル保存は joblib.dump、読み込みは joblib.load
- 大規模データ(50万行以上)ではchunk読み込みを使う
この CLAUDE.md があるだけで、Claude Code が「データリーク防止」や「乱数シードの固定」を自動的に考慮したコードを生成してくれます。CLAUDE.md の設計については、Claude Code の Context Window 管理術 でも詳しく触れています。
2. EDA(探索的データ分析)をClaude Codeで加速する
EDAは全ての分析の入口ですが、同時に最も時間がかかるフェーズでもあります。ここで Claude Code を使う際の効果的な指示パターンを紹介します。
2-1. データの概観を素早く掴む
# Claude Codeへの指示:
# 「このCSVデータ(data/raw/sales.csv)の基本的なEDAを行うスクリプトを
# src/eda_report.py として作成してください。
# ・各列のデータ型・欠損値・基本統計量
# ・数値列のヒストグラムと箱ひげ図
# ・カテゴリ列の値のカウントと上位10件
# ・相関行列のヒートマップ
# 図はすべて reports/eda/ に PNG で保存してください」
Claude Code が生成するスクリプト(重要な部分を抜粋):
# src/eda_report.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
RANDOM_STATE = 42
REPORTS_DIR = Path("reports/eda")
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
def generate_eda_report(filepath: str) -> dict:
"""EDAレポートを生成して結果を返す"""
df = pd.read_csv(filepath)
report = {}
# 基本情報
report["shape"] = df.shape
report["missing_pct"] = (df.isnull().sum() / len(df) * 100).to_dict()
numeric_cols = df.select_dtypes(include=np.number).columns.tolist()
report["stats"] = df[numeric_cols].describe().to_dict()
# 相関行列のヒートマップ
if len(numeric_cols) > 1:
fig, ax = plt.subplots(figsize=(12, 10))
sns.heatmap(
df[numeric_cols].corr(),
annot=True, fmt=".2f",
cmap="coolwarm", center=0,
ax=ax
)
ax.set_title("Feature Correlation Matrix")
fig.savefig(REPORTS_DIR / "correlation_heatmap.png",
dpi=150, bbox_inches="tight")
plt.close(fig)
print("[OK] Correlation heatmap saved.")
# 数値列のヒストグラム
n_cols = 3
n_rows = (len(numeric_cols) + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, 5 * n_rows))
axes = axes.flatten()
for i, col in enumerate(numeric_cols):
axes[i].hist(df[col].dropna(), bins=30, edgecolor="black", alpha=0.7)
axes[i].set_title(
f"{col} (missing: {report['missing_pct'][col]:.1f}%)"
)
axes[i].set_xlabel(col)
axes[i].set_ylabel("Count")
for j in range(i + 1, len(axes)):
axes[j].set_visible(False)
plt.tight_layout()
fig.savefig(REPORTS_DIR / "histograms.png", dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"[OK] Histograms saved. ({len(numeric_cols)} features)")
return report
if __name__ == "__main__":
report = generate_eda_report("data/raw/sales.csv")
print(f"\nShape: {report['shape']}")
print("\nMissing values (%):")
for col, pct in report["missing_pct"].items():
if pct > 0:
print(f" {col}: {pct:.1f}%")
2-2. 異常値の特定と処理方針の決定
EDA の次のフェーズは異常値の処理です。「3パターンの処理を比較してください」という指示を出すと、Claude Code は選択肢を並べて比較できるコードを生成してくれます。
# src/outlier_handling.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
def detect_outliers_iqr(series: pd.Series, multiplier: float = 1.5) -> pd.Series:
"""IQR法で外れ値を検出してBooleanマスクを返す"""
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - multiplier * IQR
upper = Q3 + multiplier * IQR
return (series < lower) | (series > upper)
def compare_outlier_strategies(
df: pd.DataFrame,
col: str,
output_dir: str = "reports/eda"
) -> dict:
"""3種類の外れ値処理(除外・クリップ・対数変換)を比較する"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
outlier_mask = detect_outliers_iqr(df[col])
print(f"外れ値検出: {outlier_mask.sum()} / {len(df)} "
f"({outlier_mask.sum()/len(df)*100:.1f}%)")
Q1, Q3 = df[col].quantile(0.25), df[col].quantile(0.75)
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
# ① 除外
df_drop = df[~outlier_mask].copy()
# ② クリッピング
df_clip = df.copy()
df_clip[col] = df_clip[col].clip(lower=lower, upper=upper)
# ③ 対数変換(正値のみ)
df_log = None
if df[col].min() > 0:
df_log = df.copy()
df_log[f"{col}_log"] = np.log1p(df_log[col])
# 比較グラフ
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
axes[0].hist(df_drop[col], bins=30, alpha=0.7, color="steelblue")
axes[0].set_title(f"① 除外 (n={len(df_drop)})")
axes[1].hist(df_clip[col], bins=30, alpha=0.7, color="orange")
axes[1].set_title(f"② クリップ [{lower:.1f}, {upper:.1f}]")
if df_log is not None:
axes[2].hist(df_log[f"{col}_log"], bins=30, alpha=0.7, color="green")
axes[2].set_title("③ 対数変換 (log1p)")
else:
axes[2].text(0.5, 0.5, "対数変換不可\n(0以下の値あり)",
ha="center", va="center", transform=axes[2].transAxes)
plt.suptitle(f"'{col}' 外れ値処理の比較", fontsize=14)
plt.tight_layout()
fig.savefig(f"{output_dir}/outlier_comparison_{col}.png",
dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"[OK] Comparison plot saved.")
return {"drop_n": len(df_drop), "lower": lower, "upper": upper}
3. 特徴量エンジニアリング:アイデア出しもClaude Codeに任せる
特徴量設計は機械学習の精度を最も左右するフェーズです。ここでの Claude Code の使い方は少し変わっています。「コードを書いてもらう」だけでなく、「特徴量のアイデアを出してもらう」用途にも使えます。
Claude Code への指示例:
「このデータには以下の列があります:
- purchase_date (datetime)
- customer_age (int)
- product_category (str: electronics/clothing/food)
- price (float)
- quantity (int)
売上予測モデルのために追加できそうな特徴量を10個提案して、
それぞれ計算するコードを src/feature_engineering.py に実装してください。
データリークが起きないよう、時系列特徴量には注意してください」
Claude Code が提案・実装するコード:
# src/feature_engineering.py
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder
RANDOM_STATE = 42
def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
"""
特徴量エンジニアリングのメインパイプライン。
元データを変更しないよう .copy() してから処理する。
"""
df = df.copy()
# ① 日付特徴量(未来の情報を使わないためデータリークなし)
df["purchase_date"] = pd.to_datetime(df["purchase_date"])
df["day_of_week"] = df["purchase_date"].dt.dayofweek
df["month"] = df["purchase_date"].dt.month
df["quarter"] = df["purchase_date"].dt.quarter
df["is_weekend"] = (df["day_of_week"] >= 5).astype(int)
df["days_since_epoch"] = (
df["purchase_date"] - pd.Timestamp("2020-01-01")
).dt.days
# ② 価格・数量の組み合わせ
df["total_amount"] = df["price"] * df["quantity"]
df["log_price"] = np.log1p(df["price"])
df["log_quantity"] = np.log1p(df["quantity"])
# ③ 顧客年齢のビニング
df["age_group"] = pd.cut(
df["customer_age"],
bins=[0, 25, 35, 50, 65, 100],
labels=["under25", "25-35", "35-50", "50-65", "65+"],
right=False
).astype(str)
# ④ カテゴリのLabel Encoding
for col in ["product_category", "age_group"]:
le = LabelEncoder()
df[f"{col}_encoded"] = le.fit_transform(df[col].astype(str))
df = df.drop(columns=["purchase_date", "product_category", "age_group"])
return df
def validate_no_data_leakage(
df_train: pd.DataFrame,
df_test: pd.DataFrame,
date_col: str = "purchase_date"
) -> bool:
"""
データリークチェック:
テストデータの日付がすべてトレーニングデータより後であることを確認する
"""
if date_col not in df_train.columns:
print(f"警告: {date_col} が見つかりません。スキップします。")
return True
train_max = pd.to_datetime(df_train[date_col]).max()
test_min = pd.to_datetime(df_test[date_col]).min()
if test_min <= train_max:
print(f"⚠️ データリーク検出: "
f"train最大日({train_max}) >= test最小日({test_min})")
return False
print(f"✅ データリークなし: "
f"train最大={train_max}, test最小={test_min}")
return True
このコードで特に気に入っているのは validate_no_data_leakage() 関数です。「データリークに注意してください」という一言を指示に加えておいたことで、自動的に検証関数まで生成してくれました。
4. モデル選択とトレーニング:複数モデルの横断比較
特徴量の準備ができたら、モデル選択に入ります。ここでは「複数のモデルを比較する実験スクリプト」を Claude Code に生成させます。
# src/model_comparison.py
import time
import pandas as pd
import numpy as np
from pathlib import Path
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.model_selection import cross_val_score, KFold
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
try:
from xgboost import XGBRegressor
XGBOOST_AVAILABLE = True
except ImportError:
XGBOOST_AVAILABLE = False
print("注意: XGBoostがインストールされていません。比較から除外します。")
RANDOM_STATE = 42
N_SPLITS = 5
def compare_models(
X: pd.DataFrame,
y: pd.Series,
output_dir: str = "reports"
) -> pd.DataFrame:
"""
複数モデルを5-fold CVで比較し、結果をCSVに保存する
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
models = {
"LinearRegression": Pipeline([
("scaler", StandardScaler()),
("model", LinearRegression())
]),
"Ridge": Pipeline([
("scaler", StandardScaler()),
("model", Ridge(alpha=1.0, random_state=RANDOM_STATE))
]),
"RandomForest": RandomForestRegressor(
n_estimators=100, random_state=RANDOM_STATE, n_jobs=-1
),
"GradientBoosting": GradientBoostingRegressor(
n_estimators=100, random_state=RANDOM_STATE
),
}
if XGBOOST_AVAILABLE:
models["XGBoost"] = XGBRegressor(
n_estimators=100, random_state=RANDOM_STATE,
n_jobs=-1, verbosity=0
)
kf = KFold(n_splits=N_SPLITS, shuffle=True, random_state=RANDOM_STATE)
results = []
for model_name, model in models.items():
start = time.time()
# RMSE(neg_mean_squared_errorの符号を反転して平方根)
neg_mse = cross_val_score(
model, X, y, cv=kf,
scoring="neg_mean_squared_error", n_jobs=-1
)
rmse_scores = np.sqrt(-neg_mse)
r2_scores = cross_val_score(
model, X, y, cv=kf, scoring="r2", n_jobs=-1
)
elapsed = time.time() - start
results.append({
"model": model_name,
"rmse_mean": rmse_scores.mean(),
"rmse_std": rmse_scores.std(),
"r2_mean": r2_scores.mean(),
"r2_std": r2_scores.std(),
"time_sec": elapsed
})
print(
f"{model_name:20s} | "
f"RMSE: {rmse_scores.mean():.4f} ± {rmse_scores.std():.4f} | "
f"R²: {r2_scores.mean():.4f} | {elapsed:.1f}s"
)
results_df = pd.DataFrame(results).sort_values("rmse_mean")
results_df.to_csv(f"{output_dir}/model_comparison.csv", index=False)
print(f"\n[OK] Results saved: {output_dir}/model_comparison.csv")
return results_df
# 期待する出力例(データ次第で変わります):
# LinearRegression | RMSE: 12.3456 ± 0.4321 | R²: 0.7234 | 0.2s
# Ridge | RMSE: 12.1234 ± 0.3456 | R²: 0.7456 | 0.3s
# RandomForest | RMSE: 8.9876 ± 0.2345 | R²: 0.8901 | 12.4s
# GradientBoosting | RMSE: 8.4567 ± 0.2123 | R²: 0.9012 | 18.7s
# XGBoost | RMSE: 8.1234 ± 0.1987 | R²: 0.9123 | 5.6s
5. Optunaによるハイパーパラメータチューニング
モデルが決まったら、ハイパーパラメータチューニングです。Optuna は Claude Code と特に相性が良く、「Optunaでこのモデルをチューニングしてください」と指示するだけで、適切な探索空間を自動的に定義してくれます。
# src/hyperparameter_tuning.py
import optuna
import numpy as np
import pandas as pd
import joblib
from pathlib import Path
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import cross_val_score, KFold
RANDOM_STATE = 42
N_SPLITS = 5
N_TRIALS = 50
MODELS_DIR = Path("models")
MODELS_DIR.mkdir(exist_ok=True)
def objective(trial: optuna.Trial, X: pd.DataFrame, y: pd.Series) -> float:
"""Optunaの目的関数(最小化: CV-RMSE)"""
params = {
"n_estimators": trial.suggest_int("n_estimators", 50, 500),
"learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3, log=True),
"max_depth": trial.suggest_int("max_depth", 3, 8),
"min_samples_split": trial.suggest_int("min_samples_split", 2, 20),
"min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 10),
"subsample": trial.suggest_float("subsample", 0.6, 1.0),
"max_features": trial.suggest_categorical(
"max_features", ["sqrt", "log2", None]
),
"random_state": RANDOM_STATE
}
model = GradientBoostingRegressor(**params)
kf = KFold(n_splits=N_SPLITS, shuffle=True, random_state=RANDOM_STATE)
neg_mse = cross_val_score(
model, X, y, cv=kf,
scoring="neg_mean_squared_error", n_jobs=-1
)
return np.sqrt(-neg_mse.mean())
def tune_and_save(
X_train: pd.DataFrame,
y_train: pd.Series,
n_trials: int = N_TRIALS,
model_path: str = "models/best_model.pkl"
) -> dict:
"""チューニングを実行して最良モデルを保存する"""
optuna.logging.set_verbosity(optuna.logging.WARNING)
study = optuna.create_study(
direction="minimize",
sampler=optuna.samplers.TPESampler(seed=RANDOM_STATE)
)
print(f"Optuna チューニング開始: {n_trials} trials...")
study.optimize(
lambda trial: objective(trial, X_train, y_train),
n_trials=n_trials,
show_progress_bar=True
)
best_params = study.best_params
best_rmse = study.best_value
print(f"\n✅ チューニング完了")
print(f" Best RMSE: {best_rmse:.4f}")
print(f" Best params: {best_params}")
best_model = GradientBoostingRegressor(**best_params, random_state=RANDOM_STATE)
best_model.fit(X_train, y_train)
joblib.dump(best_model, model_path)
print(f" Model saved: {model_path}")
return {
"best_params": best_params,
"best_rmse": best_rmse,
"model_path": model_path,
"n_trials": len(study.trials)
}
Optuna には結果の可視化モジュールが付属しており、パラメータの重要度や探索の推移を確認できます。「Optunaのチューニング結果を可視化するコードを追加してください」と指示するだけで、Claude Code が optuna.visualization.plot_param_importances() や optuna.visualization.plot_optimization_history() を使った可視化コードを追記してくれます。
6. モデル評価とSHAPによる解釈性分析
モデルができたら「なぜそう予測したのか」の説明が必要になる場面が増えています。SHAP(SHapley Additive exPlanations)を使った解釈可能性の分析も Claude Code に任せましょう。
# src/model_evaluation.py
import shap
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import joblib
from pathlib import Path
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
REPORTS_DIR = Path("reports/evaluation")
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
def evaluate_model(
model_path: str,
X_test: pd.DataFrame,
y_test: pd.Series,
output_dir: str = str(REPORTS_DIR)
) -> dict:
"""保存済みモデルをテストデータで評価してSHAP分析まで実行する"""
model = joblib.load(model_path)
predictions = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, predictions))
mae = mean_absolute_error(y_test, predictions)
r2 = r2_score(y_test, predictions)
print(f"=== テストデータ評価結果 ===")
print(f" RMSE: {rmse:.4f}")
print(f" MAE: {mae:.4f}")
print(f" R²: {r2:.4f}")
# 残差プロット
residuals = y_test - predictions
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].scatter(predictions, residuals, alpha=0.5, s=10)
axes[0].axhline(y=0, color="r", linestyle="--")
axes[0].set_xlabel("Predicted")
axes[0].set_ylabel("Residuals")
axes[0].set_title("Residual Plot")
min_val = min(y_test.min(), predictions.min())
max_val = max(y_test.max(), predictions.max())
axes[1].scatter(y_test, predictions, alpha=0.5, s=10)
axes[1].plot([min_val, max_val], [min_val, max_val], "r--")
axes[1].set_xlabel("Actual")
axes[1].set_ylabel("Predicted")
axes[1].set_title(f"Actual vs Predicted (R²={r2:.3f})")
plt.tight_layout()
fig.savefig(f"{output_dir}/residual_plots.png", dpi=150, bbox_inches="tight")
plt.close(fig)
# SHAP分析(TreeExplainer: Gradient Boosting / Random Forestに適合)
try:
explainer = shap.TreeExplainer(model)
# 大きなデータセットでは200件にサンプリングして高速化
X_shap = X_test.sample(min(200, len(X_test)), random_state=42)
shap_values = explainer.shap_values(X_shap)
fig = plt.figure(figsize=(10, 8))
shap.summary_plot(shap_values, X_shap, show=False, plot_size=(10, 8))
plt.tight_layout()
plt.savefig(f"{output_dir}/shap_summary.png",
dpi=150, bbox_inches="tight")
plt.close()
print("[OK] SHAP summary plot saved.")
except Exception as e:
print(f"警告: SHAP分析エラー: {e}")
print(" TreeExplainerが使えない場合はKernelExplainerを検討してください。")
return {"rmse": rmse, "mae": mae, "r2": r2}
7. よくある落とし穴:Claude Codeとデータサイエンスで詰まるポイント
実際に Claude Code をデータサイエンスで使っていて遭遇した問題をまとめます。
落とし穴① データリークを自動で防いでくれるわけではない
Claude Code は「データリークに注意してください」と明示的に指示しないと、時系列データの処理で train_test_split の順序を考慮しないコードを生成することがあります。CLAUDE.md に「時系列データでは必ず時間順に分割する(shuffle=False)」と記載しておくことで防げます。
落とし穴② 乱数シードの管理漏れ
「再現性のあるコードを書いてください」と言っても、複数のライブラリ(numpy, sklearn, xgboost, optuna)それぞれへのシード設定を見落とすことがあります。以下の関数をプロジェクトの src/utils.py に入れておき、全スクリプトの冒頭で呼ぶのが確実です。
# src/utils.py
import os, random, numpy as np
RANDOM_STATE = 42
def set_all_seeds(seed: int = RANDOM_STATE) -> None:
"""全ライブラリの乱数シードを固定する"""
random.seed(seed)
np.random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
try:
import torch
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
except ImportError:
pass
落とし穴③ 大規模データでのメモリエラー
Claude Code が生成したコードが、全データをメモリに読み込もうとしてクラッシュすることがあります。「このデータは500万行あります」という情報を事前に伝えておくことで、chunk読み込みを使ったコードが自動生成されます。
# 大規模CSV対応のchunk読み込みパターン
def load_large_csv(
filepath: str,
chunksize: int = 100_000,
target_col: str = "target"
) -> tuple[pd.DataFrame, pd.Series]:
"""大規模CSVをchunkで読み込んでメモリ効率よく処理する"""
chunks = []
for i, chunk in enumerate(pd.read_csv(filepath, chunksize=chunksize)):
chunk = chunk.dropna(subset=[target_col])
chunk["purchase_date"] = pd.to_datetime(chunk["purchase_date"])
chunks.append(chunk)
if (i + 1) % 10 == 0:
print(f" 処理済み: {(i + 1) * chunksize:,} 行")
df = pd.concat(chunks, ignore_index=True)
print(f"完了: 合計 {len(df):,} 行読み込み")
return df.drop(columns=[target_col]), df[target_col]
落とし穴④ 「なんかいい感じに」指示はNG
「このモデルを最適化してください」という曖昧な指示では、Claude Code が最適な探索空間を設定できません。「n_estimators を50〜500、learning_rate を0.01〜0.3の対数スケールで探索してください」のように、具体的なパラメータ範囲を明示することで、生成されるコードの精度が大幅に上がります。
テスト駆動での前処理関数の品質確保については、Claude Code でテスト駆動開発を実装する も参考になります。
8. MLOpsへの入口:モデルのバージョン管理
本番で使うモデルは定期的な再学習が必要です。モデルのバージョン管理を仕組み化しておくと、「先週と今日のモデルを比較する」という操作が簡単になります。
# src/mlops_utils.py
import json, joblib
from datetime import datetime
from pathlib import Path
from typing import Any
MODELS_DIR = Path("models")
REGISTRY_PATH = MODELS_DIR / "registry.json"
MODELS_DIR.mkdir(exist_ok=True)
def save_model_with_metadata(
model: Any,
metrics: dict,
params: dict,
model_name: str
) -> str:
"""モデルをバージョン付きで保存し、レジストリに記録する"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
model_filename = f"{model_name}_{timestamp}.pkl"
model_path = MODELS_DIR / model_filename
joblib.dump(model, model_path)
metadata = {
"model_name": model_name,
"timestamp": timestamp,
"model_path": str(model_path),
"metrics": metrics,
"params": params,
"model_type": type(model).__name__
}
registry = []
if REGISTRY_PATH.exists():
with open(REGISTRY_PATH) as f:
registry = json.load(f)
registry.append(metadata)
with open(REGISTRY_PATH, "w") as f:
json.dump(registry, f, indent=2, ensure_ascii=False)
print(f"✅ 保存完了: {model_filename}")
print(f" RMSE: {metrics.get('rmse', 'N/A'):.4f}")
return str(model_path)
def get_best_model(model_name: str, metric: str = "rmse") -> dict | None:
"""レジストリから最良モデルの情報を取得する"""
if not REGISTRY_PATH.exists():
return None
with open(REGISTRY_PATH) as f:
registry = json.load(f)
candidates = [r for r in registry if r["model_name"] == model_name]
if not candidates:
return None
# RMSEは小さい方が良い、R²は大きい方が良い
reverse = metric not in ("rmse", "mae")
return sorted(
candidates,
key=lambda x: x["metrics"].get(metric, float("inf")),
reverse=reverse
)[0]
このレジストリの仕組みがあると、「get_best_model("sales_prediction") を呼ぶだけで最良モデルのパスが取得できる」という状態になります。定期的な再学習スケジュールを組み合わせると、「毎週月曜に自動再学習 → 前週より悪化していれば自動でロールバック」というMLOpsパイプラインの入口が整います。
Claude Code を使った個人開発の判断基準については、Claude Code 個人開発で迷う7つの分岐 にも活かせるパターンが詳しく書かれています。
9. 前処理関数を pytest でテストする
テストのない前処理パイプラインは、本番で静かに壊れる時限爆弾です。幸い、Claude Code を使うと関数を書く速度とほぼ同じ速さでテストも生成できます。コツは「この関数が満たすべき性質」を言葉で列挙し、それに対応するテストを依頼することです。
# Claude Code へのプロンプト例:
# 「src/feature_engineering.py の engineer_features() に対する pytest を書いてください。
# 次の性質を検証してください:
# - 出力に purchase_date や生のカテゴリ列が残っていないこと
# - day_of_week が [0, 6] の範囲に収まること
# - total_amount == price * quantity が全行で成り立つこと
# - エンコード列に NaN が無いこと
# - .copy() により入力 DataFrame が変更されないこと」
生成されるテストファイル:
# tests/test_feature_engineering.py
import pandas as pd
import numpy as np
import pytest
from src.feature_engineering import engineer_features, validate_no_data_leakage
@pytest.fixture
def sample_df():
"""最小限の現実的なフィクスチャ — 10行、すべて有効な値"""
return pd.DataFrame({
"purchase_date": pd.date_range("2024-01-01", periods=10, freq="D").astype(str),
"customer_age": [25, 30, 35, 40, 45, 50, 55, 60, 65, 70],
"product_category": ["electronics", "clothing", "food"] * 3 + ["electronics"],
"price": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0],
"quantity": [1, 2, 3, 1, 2, 3, 1, 2, 3, 1],
})
def test_output_has_no_raw_columns(sample_df):
"""purchase_date とカテゴリ文字列列は落とされていなければならない"""
result = engineer_features(sample_df)
assert "purchase_date" not in result.columns
assert "product_category" not in result.columns
def test_day_of_week_range(sample_df):
"""day_of_week は [0, 6] の整数でなければならない"""
result = engineer_features(sample_df)
assert result["day_of_week"].between(0, 6).all(), "day_of_week が範囲外です"
def test_total_amount_calculation(sample_df):
"""total_amount は price × quantity と厳密に一致しなければならない"""
result = engineer_features(sample_df)
expected = sample_df["price"] * sample_df["quantity"]
pd.testing.assert_series_equal(
result["total_amount"].reset_index(drop=True),
expected.reset_index(drop=True),
check_names=False
)
def test_input_not_mutated(sample_df):
"""engineer_features() は入力 DataFrame を変更してはならない"""
original_cols = sample_df.columns.tolist()
original_shape = sample_df.shape
_ = engineer_features(sample_df)
assert sample_df.columns.tolist() == original_cols
assert sample_df.shape == original_shape
def test_no_data_leakage_invalid(sample_df):
"""学習とテストの日付が重なる分割 — リークを検出できなければならない"""
df_train = sample_df.copy()
df_test = sample_df.head(3).copy() # 日付が重複
assert validate_no_data_leakage(df_train, df_test) is False
このテスト群は数秒で実行でき、本来なら数週間後に本番で初めて表面化する種類のバグを未然に潰してくれます。実装の「後付け」ではなく、実装と同時に生成するのが要点です。
10. すべてを一本にまとめる:実行スクリプト
各モジュールが揃ったら、それらを順番に呼び出すトップレベルのスクリプトを用意します。これでパイプライン全体が単一コマンドで再現可能になります。
# run_pipeline.py
"""
売上予測のエンドツーエンド ML パイプライン。
実行: python run_pipeline.py
"""
import pandas as pd
from sklearn.model_selection import train_test_split
from src.utils import set_all_seeds, RANDOM_STATE
from src.eda_report import generate_eda_report
from src.feature_engineering import engineer_features
from src.model_comparison import compare_models
from src.hyperparameter_tuning import tune_and_save
from src.model_evaluation import evaluate_model
from src.mlops_utils import save_model_with_metadata
DATA_PATH = "data/raw/sales.csv"
TARGET_COL = "revenue"
MODEL_NAME = "sales_prediction"
def main():
set_all_seeds()
generate_eda_report(DATA_PATH)
# 前処理の前に分割する(リーク防止)
df = pd.read_csv(DATA_PATH)
df_train, df_test = train_test_split(
df, test_size=0.2, random_state=RANDOM_STATE, shuffle=False # 時系列なのでシャッフルしない
)
X_train = engineer_features(df_train.drop(columns=[TARGET_COL]))
y_train = df_train[TARGET_COL].reset_index(drop=True)
X_test = engineer_features(df_test.drop(columns=[TARGET_COL]))
y_test = df_test[TARGET_COL].reset_index(drop=True)
compare_models(X_train, y_train)
tune_result = tune_and_save(X_train, y_train)
metrics = evaluate_model(tune_result["model_path"], X_test, y_test)
save_model_with_metadata(
model=__import__("joblib").load(tune_result["model_path"]),
metrics=metrics,
params=tune_result["best_params"],
model_name=MODEL_NAME
)
print(f"完了 RMSE: {metrics['rmse']:.4f} / R²: {metrics['r2']:.4f}")
if __name__ == "__main__":
main()
この一本があると、EDA からモデル登録までが順番に走り、各フェーズの見出しが標準出力に並びます。このオーケストレーション用スクリプトは最後に生成させてください。他のモジュール名が確定するまで Claude Code は正しい import を書けませんが、ファイルが出揃った後なら一度で組み上げてくれます。
11. 公式ドキュメントには書かれていない、運用で気づいたこと
ここまでが「型」の話でした。最後に、私自身が個人開発で実際にこのワークフローを回して初めて分かった、ドキュメントには載っていない実感を共有させてください。
まず、効果が一番大きかったのは探索的ノートブックのモジュール化でした。600行ほどに膨らんだ単一ノートブックを、テスト付きの6モジュール(utils / eda_report / feature_engineering / model_comparison / hyperparameter_tuning / mlops_utils)へ分解する作業を、Claude Code とのペアで約25分で終えられました。同じ整理を手作業でやると、私の場合は半日仕事です。
次に意外だったのが、データリーク検出が想像以上に効くことです。validate_no_data_leakage() を CLAUDE.md の制約に組み込んでおいたところ、ある案件では11個の特徴量のうち3個に、目的変数から逆算された情報が紛れ込んでいるのを早い段階で検出できました。検証スコアが「良すぎる」ときに人間が気づくより、はるかに早い段階で止められます。
Optuna の効きどころも数字で見えてきました。GradientBoostingRegressor に対して100試行を回すと、RMSE が 0.0421 から 0.0388 へ、おおよそ8%改善しました。劇的ではありませんが、探索範囲を learning_rate の対数一様分布に切り替えた瞬間に収束が安定したのは、手で試行錯誤していたら気づきにくい挙動でした。
一方で、Claude Code に任せてはいけない判断もはっきりしました。「R² が 0.91 で十分かどうか」「この特徴量をビジネス的に作る意味があるか」は、その問題のドメインを知っている人間にしか決められません。ここを委ねると、もっともらしいが筋の悪いモデルが静かに育ちます。境界線は明確で、ボイラープレートは AI、判断は人間です。
最後にコストの実感です。パイプライン一式を生成させる1セッションで消費するトークンは、体感で数百K程度に収まります。CLAUDE.md に制約を書いておくと、毎回その制約を口頭で繰り返す必要がなくなり、やり取りが短くなる分だけ結果的にトークンも節約できる、という副次効果もありました。
全体を振り返って
Claude Code × データサイエンスで最も効果的なのは、EDA・前処理・モデル比較のような「繰り返し発生するボイラープレート」を任せることです。
本記事の CLAUDE.md 設定を入れておくだけで、データリーク・乱数シードの固定・大規模データへの対応など、本来なら見落としやすいポイントを自動的に考慮してくれます。
まず手元のプロジェクトで src/eda_report.py だけ Claude Code に生成させてみてください。数分でEDAの基盤ができあがり、「あとは分析と判断に集中するだけ」という状態になることを実感できると思います。