💨

Claude Sonnet 3.7が出たのでo3-mini-highと分析力戦わせてみた

2025/02/26に公開

戦わせる内容

どちらもコーディング力には自信があるらしいので、同じ課題に対してどの程度理解と表現力があるのかを試してみた。

周りはフロントの驚き屋ばかりなのでデータ分析でトライ

架空のフードデリバリーサービスの日々の注文数を予測させます。
しかし実務的には予測があたればいいわけではなく、解釈性を伴うのも重要です。
そうしないとアクション性が欠けてしまうので、そこをモデルが気持ちを汲みとってやってくれるのかを挑戦します。

適度にたまに話題に上がる欠損値を入れてどのように対処するのかも見ます。

データの生成は以下

データ生成コード
import pandas as pd
import numpy as np

# 再現性確保のために乱数シードを固定
np.random.seed(42)

# ------------------------------
# 1) 日付の生成
# ------------------------------
# 2024年はうるう年なので366日
dates = pd.date_range(start="2024-01-01", end="2024-12-31", freq="D")

# ------------------------------
# 2) 曜日を取得 (0=月曜日, 6=日曜日)
# ------------------------------
day_of_week = dates.dayofweek  # 月曜日が0, 日曜日が6

# ------------------------------
# 3) 基本の注文数(orders)を曜日で分けて作成
# ------------------------------
orders = []
for dow in day_of_week:
    if dow < 5:
        # 平日
        base = 300
    else:
        # 土日
        base = 350
    # ±30程度のランダムを足す
    val = base + np.random.randint(-30, 31)
    orders.append(val)

orders = np.array(orders, dtype=float)  # floatにしておく

# ------------------------------
# 4) 突発的な異常値の挿入 (キャンペーンなどを想定)
#    ランダムに5日選んで、1.5~3倍にする
# ------------------------------
num_outliers = 5
outlier_indices = np.random.choice(len(orders), num_outliers, replace=False)
for idx in outlier_indices:
    factor = np.random.uniform(1.5, 3.0)
    orders[idx] = orders[idx] * factor

# ------------------------------
# 5) 注文数の欠損値を挿入
#    ランダムに7日選んでNaNに
# ------------------------------
num_missing_orders = 7
missing_orders_indices = np.random.choice(len(orders), num_missing_orders, replace=False)
orders[missing_orders_indices] = np.nan

# ------------------------------
# 6) マーケティング費用 (marketing_spend) の列を作成
#    100~1000の範囲でランダムに設定
# ------------------------------
marketing_spend = np.random.uniform(100, 1000, size=len(dates))

# マーケティング費用にも一部欠損を入れる
num_missing_marketing = 5
missing_marketing_indices = np.random.choice(len(marketing_spend), num_missing_marketing, replace=False)
marketing_spend[missing_marketing_indices] = np.nan

# ------------------------------
# 7) 降水量 (rainfall_mm) の列を作成
#    0~20mmの範囲でランダムに設定
# ------------------------------
rainfall = np.random.uniform(0, 20, size=len(dates))

# ------------------------------
# 8) 気温 (temperature_C) の列を作成
#    0~35℃の範囲でランダムに設定
# ------------------------------
temperature = np.random.uniform(0, 35, size=len(dates))

# ------------------------------
# 9) 休日フラグ (holiday) の列を作成(ランダムに12日を休日としてTrueに)
# ------------------------------
holiday = np.array([False]*len(dates))
holiday_indices = np.random.choice(len(dates), 12, replace=False)
holiday[holiday_indices] = True

# ------------------------------
# 10) デリバリー可能店舗数 (available_stores)
#     50~200の範囲で乱数生成し、一部を欠損(NaN)にする
# ------------------------------
available_stores = np.random.randint(50, 201, size=len(dates)).astype(float)

num_missing_stores = 5
missing_stores_indices = np.random.choice(len(available_stores), num_missing_stores, replace=False)
available_stores[missing_stores_indices] = np.nan

# ------------------------------
# 11) 日別稼働可能デリバリースタッフ数 (available_staff)
#     10~100の範囲で乱数生成し、一部を欠損(NaN)にする
# ------------------------------
available_staff = np.random.randint(10, 101, size=len(dates)).astype(float)

num_missing_staff = 5
missing_staff_indices = np.random.choice(len(available_staff), num_missing_staff, replace=False)
available_staff[missing_staff_indices] = np.nan

# ------------------------------
# 12) アプリのDAU (dau)
#     1,000~10,000の範囲で乱数生成し、
#     キャンペーン日(ordersが異常に高い日)は1.5~2倍に増加
# ------------------------------
dau = np.random.randint(1000, 10001, size=len(dates)).astype(float)

# ordersで異常値を入れた日を目印に、dauを増やす
for idx in outlier_indices:
    increase_factor = np.random.uniform(1.5, 2.0)
    dau[idx] = dau[idx] * increase_factor

# ------------------------------
# DataFrameの作成
# ------------------------------
df = pd.DataFrame({
    "date": dates,
    "day_of_week": day_of_week,
    "orders": orders,
    "marketing_spend": marketing_spend,
    "rainfall_mm": rainfall,
    "temperature_C": temperature,
    "holiday": holiday,
    "available_stores": available_stores,
    "available_staff": available_staff,
    "dau": dau
})

# # 確認したい場合は適宜コメントアウトして表示
# print(df.head(15))
# print(df.info())

df.to_csv('架空のデリバリフードサービスのdaily data.csv')

生成したデータは保存してそれをまずは基本的な情報をまとめます。
データのおおよその概要は以下です

データの定義

各カラムの概要

  1. date

    • 日付(datetime型)
    • 例: 2024-01-01, 2024-01-02 …
    • 期間は 2024-01-01 から 2024-12-31 までの 366 日(うるう年)
  2. day_of_week

    • 曜日を数値で示したもの(int
    • 0 = 月曜日 / 1 = 火曜日 / … / 6 = 日曜日
  3. orders

    • 1 日あたりのフードデリバリー注文数(float
    • 平日をベースに 300 件前後、土日を 350 件前後に設定し、ランダムで ±30 件ほど変動
    • キャンペーン等を想定して一部日付では 1.5~3.0 倍の異常値(急増)を挿入
    • 一部日付では欠損値(NaN)が含まれる
  4. marketing_spend

    • マーケティング費用(float
    • 1 日あたり 100~1000 の範囲で乱数生成
    • 一部日付では欠損値(NaN)が含まれる
  5. rainfall_mm

    • 1 日あたりの降水量 (mm)(float
    • 0~20 mm の範囲で乱数生成
  6. temperature_C

    • 1 日あたりの平均気温 (℃)(float
    • 0~35 ℃ の範囲で乱数生成
  7. holiday

    • 休日フラグ(bool
    • ランダムに 12 日間を True(祝日等)としている
    • True: 祝日 / False: 平日
  8. available_stores

    • デリバリー可能な店舗数(float
    • 1 日あたり 50~200 の範囲で乱数生成
    • 一部日付では欠損値(NaN)が含まれる
  9. available_staff

    • 日別に稼働可能なデリバリースタッフ数(float
    • 1 日あたり 10~100 の範囲で乱数生成
    • 一部日付では欠損値(NaN)が含まれる
  10. dau

    • アプリのデイリーアクティブユーザー数(float
    • 1 日あたり 1,000~10,000 の範囲で乱数生成
    • キャンペーン日(orders に異常値を入れた日)は、1.5~2.0 倍ほど増やして設定

=== Missing Values (欠損数) ===
Unnamed: 0 0
date 0
day_of_week 0
orders 7
marketing_spend 5
rainfall_mm 0
temperature_C 0
holiday 0
available_stores 5
available_staff 5
dau 0
dtype: int64

欠損を入れてます。これをどのように処理するのか。平均値で埋めたりすると統計家からは文句が出るでしょうし、今回は予測が目的とはいえ予測には解釈性もつきものなので、欠損を欠損のまま扱う機械学習的な方法を取ることにもリスクはあります。

注文数の日別グラフはこんな感じ
キャンペーンで注文数が突然跳ね上がるのが見えますね。

相関係数は以下のような感じです。

ランダム生成なだけあって、相関がほぼない感じに仕上がってしまいます。
しかし度々話題に上がる多重共線性には気をつけなくてもよさそうですね。

変数の選択も基本的にはAI任せで最終仕上げていく感じにしましょう。

Claude Sonnet 3.7 vs. o3-mini-high

まずはo3-mini-highから行きます。返答は圧倒的に早いです。
データを投げて指示するだけなので楽ですね。

指示のプロンプトはこれをまずは試します。

このデータを使ってあなたの判断で変数を選択し注文数を予測するモデルを作って。
その際に重要なことは予測精度と解釈性です。
解釈性が悪いモデルになると説明ができずアクションが起こせないので、精度を保ちつつステークホルダーに説明ができるような予測モデルにしてください。
あなたの判断でどのようなモデルを選択してもいいですが、必ずモデル選択の目的、変数選択の目的、解釈などを含めるように。

学習に2024-01~2024-08-31までを使い 2024-09-01~を予測するモデルにせよ

o3-mini-highの簡潔な返答以下に

LASSO(Least Absolute Shrinkage and Selection Operator)回帰 を採用します

LASSOで行くらしいです。(不安)

返答以下

回答コード

ライブラリのインポート

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

from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LassoCV
from sklearn.metrics import mean_squared_error, mean_absolute_error
import joblib

1. データの読み込みと日付のパース

data = pd.read_csv('架空のデリバリフードサービスのdaily data.csv')

仮に日付カラムの名前を 'date' とする。実際のカラム名に合わせて変更してください。

data['date'] = pd.to_datetime(data['date'])

2. 学習期間(2024-01-01 ~ 2024-08-31)と予測期間(2024-09-01以降)でデータを分割

train_data = data[(data['date'] >= '2024-01-01') & (data['date'] <= '2024-08-31')].copy()
predict_data = data[data['date'] >= '2024-09-01'].copy()

ターゲット変数の名前(例:'order_count')

target = 'orders'

ここで、訓練データのターゲット変数に欠損があれば削除

if target not in train_data.columns:
raise KeyError(f"ターゲット変数 '{target}' が訓練データに存在しません。")
else:
missing_target_count = train_data[target].isnull().sum()
if missing_target_count > 0:
print(f"訓練データにおけるターゲット変数 '{target}' の欠損数: {missing_target_count}. 該当行を削除します。")
train_data = train_data.dropna(subset=[target])

3. 前処理

対象となる特徴量は日付とターゲット以外の全カラムとする

feature_cols = [col for col in data.columns if col not in ['date', target]]

数値型とカテゴリ型の列を抽出(学習データに基づく)

num_cols = train_data[feature_cols].select_dtypes(include=np.number).columns.tolist()
cat_cols = train_data[feature_cols].select_dtypes(include='object').columns.tolist()

3-1. 数値型変数の欠損値補完(中央値)

imputer_num = SimpleImputer(strategy='median')
train_data[num_cols] = imputer_num.fit_transform(train_data[num_cols])
predict_data[num_cols] = imputer_num.transform(predict_data[num_cols])

3-2. カテゴリ型変数の欠損値補完(最頻値)

if cat_cols:
imputer_cat = SimpleImputer(strategy='most_frequent')
train_data[cat_cols] = imputer_cat.fit_transform(train_data[cat_cols])
predict_data[cat_cols] = imputer_cat.transform(predict_data[cat_cols])

3-3. カテゴリ変数のダミー変数化(ワンホットエンコーディング)

train_data = pd.get_dummies(train_data, columns=cat_cols, drop_first=True)
predict_data = pd.get_dummies(predict_data, columns=cat_cols, drop_first=True)

3-4. 学習データと予測データで同じ特徴量の整合性を確保する

予測データに学習データと同じカラムが存在するように reindex

predict_data = predict_data.reindex(columns=train_data.columns, fill_value=0)

3-5. 最終的な特徴量の定義

学習データからは 'date' とターゲット変数を除く

X_train = train_data.drop(columns=['date', target])
y_train = train_data[target]

予測データからは 'date' とターゲット変数(存在する場合)を除く

X_predict = predict_data.drop(columns=['date', target], errors='ignore')

4. 探索的データ解析(学習データのみ)

plt.figure(figsize=(8,6))
sns.histplot(y_train, kde=True)
plt.title('Distribution of Order Count (Training Data)')
plt.xlabel('Order Count')
plt.ylabel('Frequency')
plt.show()

plt.figure(figsize=(12,10))
sns.heatmap(train_data.drop(columns=['date']).corr(), annot=True, fmt='.2f', cmap='coolwarm')
plt.title('Correlation Heatmap (Training Data)')
plt.show()

5. 特徴量のスケーリング(標準化)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_predict_scaled = scaler.transform(X_predict)

6. LASSO回帰モデルの構築と交差検証による最適な正則化パラメータ(α)の決定

lasso_cv = LassoCV(cv=5, random_state=42)
lasso_cv.fit(X_train_scaled, y_train)

print("\n【最適な正則化パラメータ(α)】")
print(lasso_cv.alpha_)

7. 学習データでのモデル評価

y_pred_train = lasso_cv.predict(X_train_scaled)
rmse_train = np.sqrt(mean_squared_error(y_train, y_pred_train))
mae_train = mean_absolute_error(y_train, y_pred_train)
print("\n【学習データでの評価】")
print("Train RMSE: {:.2f}, MAE: {:.2f}".format(rmse_train, mae_train))

8. 2024-09-01以降の未来データで注文数を予測

y_pred_future = lasso_cv.predict(X_predict_scaled)

予測結果と日付を併せたDataFrame作成

predictions = predict_data[['date']].copy()
predictions['predicted_order_count'] = y_pred_future

print("\n【未来の予測結果(一部表示)】")
print(predictions.head())

9. モデル解釈:LASSOで選択された変数とその係数

coef = pd.Series(lasso_cv.coef_, index=X_train.columns)
important_features = coef[coef != 0].sort_values(ascending=False)
print("\n【重要な特徴量(非ゼロの係数)】")
print(important_features)

plt.figure(figsize=(10,6))
important_features.plot(kind='bar')
plt.title("Important Features and Their Coefficients")
plt.ylabel("Coefficient Value")
plt.show()

10. モデルとスケーラーの保存(必要に応じて)

joblib.dump(lasso_cv, 'lasso_model.pkl')
joblib.dump(scaler, 'scaler.pkl')
print("\nモデルとスケーラーを 'lasso_model.pkl' と 'scaler.pkl' として保存しました。")

欠損は平均値補完ですね(あわあわ)

予測結果はLASSOに従うと変数は
day_of_week 9.910772
dau 3.564235

Train RMSE: 51.85, MAE: 25.75
だけになり、

結果はこれ。使い物にならない。。。
これは誘導の仕方にもよりますが、選択された変数もモデル選択理由も意味不明ですね。。
そもそものステークホルダーへの説明をするという命題が達成できなさそうです。
まぁこれではただコーディングしただけで、予測にも使えないし説明にも使えないですね。

個人的にo1系は解釈性を求める予測モデルにはLASSOやRidge系を選択しがちな気がします(もちろんプロンプト次第ですが、基本変数を減らして説明性を上げようとしている気もする)

Claude Sonnet 3.7 Extendedの簡潔な返答以下に

o3-mini-highと同じことをさせようとすると一生思考中で返答を返してくれないので(おそらく回答のトークンが超過しているかも)、工夫します。

回答のトークン数超過を防ぐためにStep by Stepで行います。
前処理
モデル作成
解釈説明
の順です。

線形回帰:最も解釈性が高く、各変数の影響を係数として把握可能
Ridge回帰:正則化により多重共線性の影響を軽減した線形モデル
Lasso回帰:変数選択効果も持つ正則化線形モデル
決定木:非線形関係を捉え、ルールベースで解釈可能
ランダムフォレスト:複数の決定木による堅牢な非線形モデル
勾配ブースティング:高い予測精度と適度な解釈性のバランスが取れたモデル

モデルを複数個比較してくれるらしいです。
回答長いので以下でまとめちゃいます。

回答

特徴量選択のプロセスを開始...
高い相関関係にある特徴量ペア:
is_weekend - day_of_week: 0.789
quarter - month: 0.944
cumulative_days - month: 0.992
cumulative_days - quarter: 0.937
season_2 - month: 0.845
season_2 - quarter: 0.788
season_2 - cumulative_days: 0.839

VIF (多重共線性) 分析:
feature VIF
12 cumulative_days 52421.828662
9 month 51095.006728
10 day 820.095290
11 quarter 9.634318
6 day_of_week 2.762288
7 is_weekend 2.740223
14 season_2 2.256062
13 season_1 1.784456
2 temperature_C 1.088193
15 holiday 1.072733
0 marketing_spend 1.066977
4 available_staff 1.057652
1 rainfall_mm 1.054356
5 dau 1.043470
3 available_stores 1.034167
8 year NaN

多重共線性が高い特徴量 (VIF > 10): ['cumulative_days', 'month', 'day']

ランダムフォレストによる特徴量重要度:
feature importance
5 dau 0.408503
6 day_of_week 0.133380
7 is_weekend 0.126477
10 day 0.084826
3 available_stores 0.069079
2 temperature_C 0.033760
1 rainfall_mm 0.032500
0 marketing_spend 0.030851
4 available_staff 0.026800
12 cumulative_days 0.023495
13 season_1 0.017512
11 quarter 0.005967
9 month 0.005756
14 season_2 0.000676
15 holiday 0.000419
8 year 0.000000

重要度が低い特徴量: ['quarter', 'month', 'season_2', 'holiday', 'year']

RFEによる特徴量選択:
RFEで選択された特徴量: ['dau', 'day_of_week', 'is_weekend', 'month', 'day', 'quarter', 'cumulative_days', 'season_1', 'season_2', 'holiday']

最終的な特徴量選択の提案:
除外推奨特徴量: ['month']
推奨される特徴量セット: ['marketing_spend', 'rainfall_mm', 'temperature_C', 'available_stores', 'available_staff', 'dau', 'day_of_week', 'is_weekend', 'year', 'day', 'quarter', 'cumulative_days', 'season_1', 'season_2', 'holiday']
モデリングに使用する特徴量: ['marketing_spend', 'rainfall_mm', 'temperature_C', 'available_stores', 'available_staff', 'dau', 'day_of_week', 'is_weekend', 'year', 'day', 'quarter', 'cumulative_days', 'season_1', 'season_2', 'holiday']
特徴量の次元: 15

Linear Regression モデルの評価:
モデル評価指標:
学習データ - RMSE: 46.29, MAE: 21.16, R²: 0.2551
テストデータ - RMSE: 50.94, MAE: 25.36, R²: 0.2334

Ridge Regression モデルの評価:
モデル評価指標:
学習データ - RMSE: 46.29, MAE: 21.11, R²: 0.2550
テストデータ - RMSE: 50.85, MAE: 25.05, R²: 0.2361

Lasso Regression モデルの評価:
モデル評価指標:
学習データ - RMSE: 46.30, MAE: 21.04, R²: 0.2547
テストデータ - RMSE: 50.88, MAE: 24.76, R²: 0.2352

Decision Tree モデルの評価:
モデル評価指標:
学習データ - RMSE: 11.39, MAE: 8.19, R²: 0.9549
テストデータ - RMSE: 57.54, MAE: 25.71, R²: 0.0218

Random Forest モデルの評価:
モデル評価指標:
学習データ - RMSE: 20.58, MAE: 9.05, R²: 0.8527
テストデータ - RMSE: 41.56, MAE: 23.11, R²: 0.4896

Gradient Boosting モデルの評価:
モデル評価指標:
学習データ - RMSE: 10.99, MAE: 8.57, R²: 0.9580
テストデータ - RMSE: 44.95, MAE: 24.26, R²: 0.4032

ベストモデル: Random Forest
テストR²: 0.4896

Random Forest モデルの解釈:
特徴量の重要度:
Feature Importance
5 dau 0.413156
6 day_of_week 0.144584
7 is_weekend 0.127584
9 day 0.085133
3 available_stores 0.059231
2 temperature_C 0.044232
0 marketing_spend 0.028690
4 available_staff 0.027046
11 cumulative_days 0.024373
1 rainfall_mm 0.021278
12 season_1 0.018432
10 quarter 0.005096
13 season_2 0.000685
14 holiday 0.000480
8 year 0.000000

最も重要な特徴量: dau (重要度: 0.4132)
2番目に重要な特徴量: day_of_week (重要度: 0.1446)

順列重要度の計算:
Feature Importance
5 dau 0.489860
7 is_weekend 0.071244
6 day_of_week 0.012986
1 rainfall_mm 0.001304
8 year 0.000000
10 quarter 0.000000
11 cumulative_days 0.000000
12 season_1 0.000000
13 season_2 0.000000
14 holiday -0.000137
4 available_staff -0.001377
3 available_stores -0.002295
0 marketing_spend -0.004564
2 temperature_C -0.007662
9 day -0.024140

===== モデル概要 =====
選択されたモデル: Random Forest
モデルの性能 (R²): 0.4896
モデルの性能 (RMSE): 41.56

===== 重要な変数 =====
最も重要な5つの特徴量: ['dau', 'is_weekend', 'day_of_week', 'rainfall_mm', 'year']

===== モデルの解釈 =====

  • dau: この変数はモデルの予測に41.32%の影響を与えています
  • day_of_week: この変数はモデルの予測に14.46%の影響を与えています
  • is_weekend: この変数はモデルの予測に12.76%の影響を与えています
  • day: この変数はモデルの予測に8.51%の影響を与えています
  • available_stores: この変数はモデルの予測に5.92%の影響を与えています

===== ビジネスへの示唆 =====
天候条件(雨量や気温)が注文数に影響を与えています。
提案: 天候予報に基づいた在庫管理やスタッフ配置の最適化を検討してください。
日次アクティブユーザー数が注文数に重要な影響を与えています。
提案: ユーザーエンゲージメントを高めるための施策を強化し、アクティブユーザー数を増やすことを検討してください。
曜日や週末かどうかが注文数に影響を与えています。
提案: 曜日ごとの需要パターンに合わせた人員配置や特別プロモーションを検討してください。

===== 示唆されるアクション =====

  1. 最も重要な変数に焦点を当てたビジネス戦略の策定
  2. 注文数の増加につながる特定の条件(季節、天候、曜日など)を特定し、それに合わせたマーケティング施策の実施
  3. データ収集の改善(より詳細な顧客データ、競合情報など)によるモデルの精度向上
  4. モデルを定期的に更新し、変化するパターンやトレンドを把握

===== モデルの限界 =====

  1. 外部要因(競合の動向、市場の変化など)はモデルに含まれていません
  2. 過去のデータに基づいているため、新たなトレンドや急激な変化を予測することは難しい場合があります
  3. 相関関係は因果関係を示すものではないため、解釈には注意が必要です

予測モデルの構築が完了しました。ベストモデル: Random Forest

あっているかは置いておいて、少ない情報量からここまでやり切る力は素直にすごい。。。
なぜそれを選択したのかも説明していて最終示唆まで出していて指示の理解力と実現力はありますね。

ただし、やはりアプローチは必ずしも良さそうではないです。

欠損は平均値で補完しているみたいなので、ここは議論があるところですね。
また、実際のデータを見るとすごい定常過程なのにそこは考慮に入れていない。
説明性を無理に入れようとしてランダムフォレストでやっているところ(定常過程ならもっとクラシックな統計モデルのARIMAXやSARIMAX、もしくはベイズでポアソン分布による線形モデルの方がシンプルな気もする、、、)

まとめ

もちろんそもそものデータが悪いという指摘はありますが、ここまでの指示でこれだけ理解してコードを生成するという点においては驚異的です。
まだまだ指示する側の理解力ありきでAIを活用しないといけない部分も垣間見えました。

そしてこれを説明し切るだけの力はAIよりデータ分析者によるところが大きいので、そう言った意味でもまだまだAIが仕事を奪い切ることはなさそうですね。

しかしながら曖昧な指示でここまでできるようになっているので、やはり人間が大枠の部分を考えてAIに表現してもらい、それをレビューしながら改善していくという共生的な働き方が今後進みそうです。

Discussion