Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 336 additions & 0 deletions docs/brain_lever_competition_improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
# 改善案(脳活動からレバー位置予測)

以下は、提示されたベースライン実装を前提にした改善アイデアのまとめです。実装方針を具体化しやすいように、追加の特徴量設計やクロスバリデーション、LightGBMの学習設定改善などを整理しています。

## 1. 特徴量エンジニアリングの拡張

### 1-1. 時系列の統計量
現在はラグと差分、セッション内Zスコアのみですが、**一定ウィンドウの統計量**を追加すると安定しやすいです。

- 直近 `k` ステップの **平均・標準偏差・最小/最大**
- **指数移動平均 (EMA)**
- **絶対値差分**(速度のような指標)

### 1-2. 周波数領域・帯域情報(簡易)
短いウィンドウで **移動平均を2種**(例: 3ステップ平均・10ステップ平均)入れるだけでも、低周波/高周波の成分を分けた特徴量として役立ちます。

### 1-3. セッション単位の集約特徴
各 `sample_id` 内での
- **平均/分散**
- **最大-最小**
- **トレンド傾向(時間 vs 平均値の回帰係数)**
などを、セッション全体特徴として行全体に付与する方法もあります。

## 2. モデルと学習設定の改善

### 2-1. LightGBMのearly stopping
現在は `early_stopping(100)` を使っていますが、
`min_data_in_leaf` や `max_depth` を調整すると過学習が減ることがあります。

### 2-2. レギュラライズ
LightGBMの `reg_alpha` や `reg_lambda` を追加して
安定化させるのが有効です。

### 2-3. Ridge/LGBMのアンサンブル
Ridgeの線形予測とLGBMの非線形予測を **平均アンサンブル**すると、
MSEが安定しやすくなります。

## 3. クロスバリデーション設計
GroupKFoldは妥当ですが、`sample_id` が「実験セッション」であるなら
**GroupKFold固定**は必須。
さらに、**最後の一部時間を各セッション内の検証区間とする時系列CV**も試す価値があります。

## 4. 改善例コード(抜粋)

以下は、現行コードの構造を保ったまま追加できる改善例です。

```python
def add_rolling_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", window=5):
df = df.sort_values([group_col, "time"]).copy()
rolled = (
df.groupby(group_col)[numeric_cols]
.rolling(window=window, min_periods=1)
.agg(["mean", "std", "min", "max"])
.reset_index(level=0, drop=True)
)
rolled.columns = [f"{c}_roll{window}_{stat}" for c, stat in rolled.columns]
return rolled

def add_ema_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", span=10):
df = df.sort_values([group_col, "time"]).copy()
ema = df.groupby(group_col)[numeric_cols].apply(lambda g: g.ewm(span=span).mean())
ema = ema.reset_index(level=0, drop=True)
ema.columns = [f"{c}_ema{span}" for c in numeric_cols]
return ema

def prepare_features(train: pd.DataFrame, test: pd.DataFrame):
y = train["lever"].astype(float)

base_train = train.drop(columns=["lever", "id"])
base_test = test.drop(columns=["id"])

numeric_cols = base_train.select_dtypes(include="number").columns.tolist()
cat_cols = [c for c in base_train.columns if c not in numeric_cols and c != "sample_id"]

train_feat = add_time_series_features(base_train, numeric_cols)
test_feat = add_time_series_features(base_test, numeric_cols)

train_z = add_session_zscore(base_train, numeric_cols)
test_z = add_session_zscore(base_test, numeric_cols)

train_diff = add_diff_features(base_train, numeric_cols)
test_diff = add_diff_features(base_test, numeric_cols)

# 追加: rolling & EMA
train_roll = add_rolling_features(base_train, numeric_cols, window=5)
test_roll = add_rolling_features(base_test, numeric_cols, window=5)

train_ema = add_ema_features(base_train, numeric_cols, span=10)
test_ema = add_ema_features(base_test, numeric_cols, span=10)

train_feat = pd.concat([train_feat, train_z, train_diff, train_roll, train_ema], axis=1)
test_feat = pd.concat([test_feat, test_z, test_diff, test_roll, test_ema], axis=1)

X_train = train_feat.drop(columns=["sample_id"])
X_test = test_feat.drop(columns=["sample_id"])

if cat_cols:
X_train = pd.get_dummies(X_train, columns=cat_cols, dummy_na=False)
X_test = pd.get_dummies(X_test, columns=cat_cols, dummy_na=False)

X_train, X_test = X_train.align(X_test, join="left", axis=1, fill_value=0)
X_train = X_train.fillna(0)
X_test = X_test.fillna(0)

return X_train, y, X_test
```

## 5. 追加のチューニング候補
- `num_leaves`, `min_data_in_leaf`, `max_depth` を探索
- `learning_rate` を小さめにして `n_estimators` を増やす(高精度狙い)
- `subsample` / `colsample_bytree` で正則化を強める

## 6. 改善を適用した“最終コード”例
「実際に置き換えるとどうなるか」を明確にするため、上記の改善(rolling/EMA/正則化・early stoppingなど)を反映した**全体例**を掲載します。パスやカラム名は手元のデータに合わせてください。

```python
import pandas as pd
from pathlib import Path
from sklearn.model_selection import GroupKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
import lightgbm as lgb


def load_data(base: Path):
train = pd.read_csv(base / "train.csv")
test = pd.read_csv(base / "test.csv")
sample_sub = pd.read_csv(base / "sample_submission.csv")
return train, test, sample_sub


def add_time_series_features(df: pd.DataFrame, numeric_cols, group_col="sample_id"):
df = df.sort_values([group_col, "time"]).copy()
for lag in (1, 2, 3):
lagged = df.groupby(group_col)[numeric_cols].shift(lag)
lagged.columns = [f"{c}_lag{lag}" for c in numeric_cols]
df = pd.concat([df, lagged], axis=1)
return df


def add_session_zscore(df: pd.DataFrame, numeric_cols, group_col="sample_id"):
grouped = df.groupby(group_col)[numeric_cols]
mean = grouped.transform("mean")
std = grouped.transform("std").replace(0, 1)
z = (df[numeric_cols] - mean) / std
z.columns = [f"{c}_z" for c in numeric_cols]
return z


def add_diff_features(df: pd.DataFrame, numeric_cols, group_col="sample_id"):
df = df.sort_values([group_col, "time"]).copy()
diff = df.groupby(group_col)[numeric_cols].diff(1)
diff.columns = [f"{c}_diff1" for c in numeric_cols]
return diff


def add_rolling_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", window=5):
df = df.sort_values([group_col, "time"]).copy()
rolled = (
df.groupby(group_col)[numeric_cols]
.rolling(window=window, min_periods=1)
.agg(["mean", "std", "min", "max"])
.reset_index(level=0, drop=True)
)
rolled.columns = [f"{c}_roll{window}_{stat}" for c, stat in rolled.columns]
return rolled


def add_ema_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", span=10):
df = df.sort_values([group_col, "time"]).copy()
ema = df.groupby(group_col)[numeric_cols].apply(lambda g: g.ewm(span=span).mean())
ema = ema.reset_index(level=0, drop=True)
ema.columns = [f"{c}_ema{span}" for c in numeric_cols]
return ema


def prepare_features(train: pd.DataFrame, test: pd.DataFrame):
y = train["lever"].astype(float)

base_train = train.drop(columns=["lever", "id"])
base_test = test.drop(columns=["id"])

numeric_cols = base_train.select_dtypes(include="number").columns.tolist()
cat_cols = [c for c in base_train.columns if c not in numeric_cols and c != "sample_id"]

train_feat = add_time_series_features(base_train, numeric_cols)
test_feat = add_time_series_features(base_test, numeric_cols)

train_z = add_session_zscore(base_train, numeric_cols)
test_z = add_session_zscore(base_test, numeric_cols)

train_diff = add_diff_features(base_train, numeric_cols)
test_diff = add_diff_features(base_test, numeric_cols)

train_roll = add_rolling_features(base_train, numeric_cols, window=5)
test_roll = add_rolling_features(base_test, numeric_cols, window=5)

train_ema = add_ema_features(base_train, numeric_cols, span=10)
test_ema = add_ema_features(base_test, numeric_cols, span=10)

train_feat = pd.concat([train_feat, train_z, train_diff, train_roll, train_ema], axis=1)
test_feat = pd.concat([test_feat, test_z, test_diff, test_roll, test_ema], axis=1)

X_train = train_feat.drop(columns=["sample_id"])
X_test = test_feat.drop(columns=["sample_id"])

if cat_cols:
X_train = pd.get_dummies(X_train, columns=cat_cols, dummy_na=False)
X_test = pd.get_dummies(X_test, columns=cat_cols, dummy_na=False)

X_train, X_test = X_train.align(X_test, join="left", axis=1, fill_value=0)
X_train = X_train.fillna(0)
X_test = X_test.fillna(0)
return X_train, y, X_test


def cv_score(X, y, groups, model_type="lgbm"):
cv = GroupKFold(n_splits=5)
scores = []

for fold, (tr_idx, va_idx) in enumerate(cv.split(X, y, groups=groups), start=1):
X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx]
y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx]

if model_type == "ridge":
model = make_pipeline(StandardScaler(with_mean=False), Ridge(alpha=1.0))
model.fit(X_tr, y_tr)
pred = model.predict(X_va)
else:
model = lgb.LGBMRegressor(
n_estimators=2000,
learning_rate=0.03,
num_leaves=63,
max_depth=-1,
min_data_in_leaf=80,
subsample=0.8,
colsample_bytree=0.8,
reg_alpha=0.1,
reg_lambda=0.1,
random_state=42,
n_jobs=-1,
)
model.fit(
X_tr,
y_tr,
eval_set=[(X_va, y_va)],
eval_metric="mse",
callbacks=[lgb.early_stopping(200, verbose=False)],
)
pred = model.predict(X_va, num_iteration=model.best_iteration_)

mse = mean_squared_error(y_va, pred)
scores.append(mse)
print(f"fold {fold} mse: {mse:.6f}")

print(f"mean mse: {sum(scores)/len(scores):.6f}")


def train_and_predict(X, y, X_test, model_type="lgbm"):
if model_type == "ridge":
model = make_pipeline(StandardScaler(with_mean=False), Ridge(alpha=1.0))
model.fit(X, y)
return model.predict(X_test)

model = lgb.LGBMRegressor(
n_estimators=2000,
learning_rate=0.03,
num_leaves=63,
max_depth=-1,
min_data_in_leaf=80,
subsample=0.8,
colsample_bytree=0.8,
reg_alpha=0.1,
reg_lambda=0.1,
random_state=42,
n_jobs=-1,
)
model.fit(X, y)
return model.predict(X_test)


def main():
base = Path(r"c:\Users\hashi\Downloads\JOAI")
train, test, sample_sub = load_data(base)
X, y, X_test = prepare_features(train, test)

groups = train["sample_id"]
model_type = "lgbm"
cv_score(X, y, groups, model_type=model_type)

test_pred = train_and_predict(X, y, X_test, model_type=model_type)
submission = sample_sub.copy()
submission["lever"] = test_pred
submission.to_csv(base / "submission_baseline.csv", index=False)
print("saved: submission_baseline.csv")


if __name__ == "__main__":
main()
```

## 7. 参考: ユーザー提示の最適化スクリプトに対する改善ポイント
以下は、提示された「最適化版」スクリプトに対する改善点の整理です。コード全体の方向性は良いものの、**再現性・汎化性・計算コスト**の観点で修正すると精度が安定しやすくなります。

### 7-1. データリーク/評価設計の見直し
- `time_holdout_split` は **未来のデータが学習に混ざらない**ようにする点では良いですが、`day_n` と `time` の順序だけで切ると **マウスIDやセッションの分布が偏る**可能性があります。
- `GroupKFold(sample_id)` を基本にしつつ、**最後の時間区間を検証**にする「時系列+グループ」の折衷設計を検討してください。
- `group_mouse` は **マウスIDで分割**できる点は良いですが、分布が大きく変わるためスコアが不安定になりやすいです。
- 指標のブレが大きい場合は `group_sample` を優先し、補助的に `group_mouse` を使うのが安全です。

### 7-2. 特徴量の過多と冗長性
- 現状は「平均・標準偏差・最大/最小」「左右差」「機能領域平均」「学習進捗」「時系列短期平均」などが大量に入り、**冗長な特徴量が多い**です。
- LightGBMなど木系は冗長でも耐えますが、**不要特徴が過学習と計算コストを増やす**ため、重要度で削減すると良いです。
- `SSp_ll_l_enhanced` などの「2倍強調」は **データドリブンでなく経験則**なので、まずは通常特徴として扱い、必要なら後で重み付けを検討しましょう。

### 7-3. 時系列特徴の扱い
- `rolling(window=3)` などの単純移動平均だけでなく、**差分・EMA・ラグ**の追加は有効です。
- ただし `sample_id` 内の時系列特徴を増やしすぎると **リークに近い情報**を取りやすくなるため、
- `lag1/2/3`, `diff1`, `EMA(10)` 程度から始めるのが安全です。

### 7-4. モデルの重複と計算コスト
- XGBoost / LightGBM / RandomForest の **3モデルを常に回す設計**は重いです。
- まず LightGBM を主軸にし、スコアが詰まったら追加モデルを試す方が効率的です。
- アンサンブル平均は安定性が増す一方で、**CV設計が不十分だと評価が過大に見える**ため注意が必要です。

### 7-5. 数値安定性・前処理
- `inf` の置換は良いですが、**NaN補完は変換前に統一**した方が安定します。
- `mouse_id_encoded` を数値特徴として扱うのは有効ですが、**カテゴリ特徴として扱う**(One-Hot / Target Encoding)も有効です。

### 7-6. 改善の方向性(推奨)
- **特徴量削減**: 重要度で上位 200〜500 程度に絞る。
- **シンプルな時系列特徴**: `lag + diff + EMA` 程度に抑える。
- **モデルは LGBM 中心**: まず `num_leaves` と `min_data_in_leaf` を調整。
- **CV安定化**: `GroupKFold(sample_id)` を基準に、補助で `time_holdout`。