{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# SetUp","metadata":{}},{"cell_type":"code","source":"# -*- coding: utf-8 -*-\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import StratifiedKFold, StratifiedShuffleSplit\nfrom sklearn.metrics import f1_score, classification_report\nimport lightgbm as lgb\nimport os\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score, f1_score\nimport joblib\nfrom functools import reduce\n\n# Import evaluation API\nimport kaggle_evaluation.cmi_inference_server\npd.set_option('display.max_rows', 500)\nrandom_seed = 42","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:38.244826Z","iopub.execute_input":"2025-08-16T06:32:38.245175Z","iopub.status.idle":"2025-08-16T06:32:38.251033Z","shell.execute_reply.started":"2025-08-16T06:32:38.245113Z","shell.execute_reply":"2025-08-16T06:32:38.250209Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# def of file paths\ndata_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/\"\n\n# load datasets\ntrain = pl.read_csv(os.path.join(data_path, \"train.csv\"))\ntest = pl.read_csv(os.path.join(data_path, \"test.csv\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:40.446975Z","iopub.execute_input":"2025-08-16T06:32:40.447598Z","iopub.status.idle":"2025-08-16T06:32:44.566888Z","shell.execute_reply.started":"2025-08-16T06:32:40.447576Z","shell.execute_reply":"2025-08-16T06:32:44.566311Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = train.drop([\"sequence_type\",\"orientation\",\"behavior\",\"phase\"])\n\n# check concat data\nprint(f\"Train shape: {train.shape}\")\nprint(f\"Test shape: {test.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:44.568058Z","iopub.execute_input":"2025-08-16T06:32:44.568273Z","iopub.status.idle":"2025-08-16T06:32:44.574467Z","shell.execute_reply.started":"2025-08-16T06:32:44.568257Z","shell.execute_reply":"2025-08-16T06:32:44.573754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train.head(1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:44.575279Z","iopub.execute_input":"2025-08-16T06:32:44.575493Z","iopub.status.idle":"2025-08-16T06:32:44.594536Z","shell.execute_reply.started":"2025-08-16T06:32:44.575471Z","shell.execute_reply":"2025-08-16T06:32:44.593867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test.head(1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:44.729109Z","iopub.execute_input":"2025-08-16T06:32:44.729836Z","iopub.status.idle":"2025-08-16T06:32:44.736063Z","shell.execute_reply.started":"2025-08-16T06:32:44.729791Z","shell.execute_reply":"2025-08-16T06:32:44.735501Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Feature Engineering","metadata":{}},{"cell_type":"markdown","source":"## acc & rot & thm","metadata":{}},{"cell_type":"code","source":"from sklearn.base import BaseEstimator, TransformerMixin\nimport polars as pl\nimport numpy as np\nfrom tqdm import tqdm\nfrom scipy.stats import kurtosis, entropy\n\nclass PolarsBasicStatsExtractor(BaseEstimator, TransformerMixin):\n    def __init__(self, sensor_cols, group_col=\"sequence_id\"):\n        self.sensor_cols = sensor_cols\n        self.group_col = group_col\n    \n    def fit(self, X, y=None):\n        return self\n    \n    def transform(self, X):\n        X = pl.DataFrame(X)\n        aggs = []\n        print(\"Building aggregation expressions...\")\n        for col in tqdm(self.sensor_cols, desc=\"Processing sensor columns\"):\n            aggs.extend([\n                pl.col(col).mean().alias(f\"{col}_mean\"),\n                pl.col(col).std().alias(f\"{col}_std\"),\n                pl.col(col).min().alias(f\"{col}_min\"),\n                pl.col(col).max().alias(f\"{col}_max\"),\n                (pl.col(col).max() - pl.col(col).min()).alias(f\"{col}_range\"),\n                pl.col(col).median().alias(f\"{col}_median\"),\n                (pl.col(col).quantile(0.75) - pl.col(col).quantile(0.25)).alias(f\"{col}_iqr\"),\n                pl.col(col).skew().alias(f\"{col}_skew\"),\n            ])\n        print(\"Computing basic statistics...\")\n        return X.group_by(self.group_col).agg(aggs)\n\n\nclass PolarsDiffRateExtractor(BaseEstimator, TransformerMixin):\n    def __init__(self, sensor_cols, group_col=\"sequence_id\"):\n        self.sensor_cols = sensor_cols\n        self.group_col = group_col\n    \n    def fit(self, X, y=None):\n        return self\n    \n    def transform(self, X):\n        print(\"Sorting data and computing differences...\")\n        X_sorted = pl.DataFrame(X).sort(by=[self.group_col])\n        results = []\n        groups = list(X_sorted.group_by(self.group_col, maintain_order=True))\n        for seq_id, group in tqdm(groups, desc=\"Computing diff/rate features\"):\n            if isinstance(seq_id, tuple):\n                seq_id = seq_id[0]\n            row = {self.group_col: seq_id}\n            for col in self.sensor_cols:\n                vals = group[col].to_numpy()\n                diffs = np.diff(vals)\n                if len(diffs) > 0:\n                    row[f\"{col}_diff_mean\"] = diffs.mean()\n                    row[f\"{col}_diff_std\"] = diffs.std()\n                    row[f\"{col}_diff_max\"] = diffs.max()\n                else:\n                    row[f\"{col}_diff_mean\"] = 0\n                    row[f\"{col}_diff_std\"] = 0\n                    row[f\"{col}_diff_max\"] = 0\n            results.append(row)\n        return pl.DataFrame(results)\n\nclass PolarsFFTFeatureExtractor(BaseEstimator, TransformerMixin):\n    def __init__(self, sensor_cols, group_col=\"sequence_id\"):\n        self.sensor_cols = sensor_cols\n        self.group_col = group_col\n    \n    def fit(self, X, y=None):\n        return self\n    \n    def transform(self, X):\n        features = []\n        print(\"Computing FFT features...\")\n        groups = list(pl.DataFrame(X).group_by(self.group_col, maintain_order=True))\n        for seq_id, group in tqdm(groups, desc=\"Processing sequences\"):\n            if isinstance(seq_id, tuple):\n                seq_id = seq_id[0]\n            row = {self.group_col: seq_id}\n            for col in self.sensor_cols:\n                signal = group[col].to_numpy()\n                fft_vals = np.fft.rfft(signal)\n                fft_abs = np.abs(fft_vals)\n                psd = fft_abs ** 2\n\n                # 基本\n                row[f\"{col}_fft_peak_freq\"] = int(np.argmax(fft_abs[1:]) + 1)\n                row[f\"{col}_fft_energy\"] = float(psd.sum())\n\n                # 平均周波数\n                freqs = np.fft.rfftfreq(len(signal))\n                row[f\"{col}_fft_mean_freq\"] = float(np.sum(freqs * psd) / np.sum(psd))\n\n                # スペクトルエントロピー\n                psd_norm = psd / np.sum(psd)\n                row[f\"{col}_fft_entropy\"] = float(entropy(psd_norm))\n\n                # 帯域パワー比（低周波 vs 高周波）\n                cutoff = len(freqs) // 4\n                low_power = psd[:cutoff].sum()\n                high_power = psd[cutoff:].sum()\n                row[f\"{col}_fft_low_high_ratio\"] = float(low_power / (high_power + 1e-8))\n            features.append(row)\n        return pl.DataFrame(features)\n\n\ndef fe_pipeline(df, sensor_cols):\n    print(\"Starting feature engineering pipeline...\")\n    df = pl.DataFrame(df)\n    \n    print(\"\\n=== Step 1/4: Basic Statistics ===\")\n    basic_stats = PolarsBasicStatsExtractor(sensor_cols).transform(df)\n    \n    print(\"\\n=== Step 2/4: Diff/Rate Features ===\")\n    diff_features = PolarsDiffRateExtractor(sensor_cols).transform(df)  # ✅ slope削除済\n    \n    print(\"\\n=== Step 3/4: FFT Features ===\")\n    fft_features = PolarsFFTFeatureExtractor(sensor_cols).transform(df)\n    \n    print(\"\\n=== Step 4/4: Segment Statistics ===\")\n    segment_stats = PolarsSegmentStatsExtractor(sensor_cols).transform(df)\n    \n    print(\"\\n=== Joining all features ===\")\n    result = (\n        basic_stats\n        .join(diff_features, on=\"sequence_id\", how=\"inner\")\n        .join(fft_features, on=\"sequence_id\", how=\"inner\")\n        .join(segment_stats, on=\"sequence_id\", how=\"inner\")\n    )\n    print(\"Feature engineering pipeline completed!\")\n    return result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:46.480266Z","iopub.execute_input":"2025-08-16T06:32:46.480556Z","iopub.status.idle":"2025-08-16T06:32:46.499229Z","shell.execute_reply.started":"2025-08-16T06:32:46.480537Z","shell.execute_reply":"2025-08-16T06:32:46.498432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sensor_cols = [\n    \"acc_x\",\n    \"acc_y\",\n    \"acc_z\",\n    \"rot_w\",\n    \"rot_x\",\n    \"rot_y\",\n    \"rot_z\",\n    'thm_1', 'thm_2', 'thm_3', 'thm_4', 'thm_5'\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:49.941372Z","iopub.execute_input":"2025-08-16T06:32:49.941636Z","iopub.status.idle":"2025-08-16T06:32:49.945334Z","shell.execute_reply.started":"2025-08-16T06:32:49.941617Z","shell.execute_reply":"2025-08-16T06:32:49.944643Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Processing training data...\")\ntrain_featureed = fe_pipeline(train, sensor_cols)\n\nprint(\"\\nProcessing test data...\")\ntest_featured = fe_pipeline(test, sensor_cols)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:32:51.565247Z","iopub.execute_input":"2025-08-16T06:32:51.565993Z","iopub.status.idle":"2025-08-16T06:33:46.362567Z","shell.execute_reply.started":"2025-08-16T06:32:51.565969Z","shell.execute_reply":"2025-08-16T06:33:46.361944Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_featureed","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:33:46.363593Z","iopub.execute_input":"2025-08-16T06:33:46.363797Z","iopub.status.idle":"2025-08-16T06:33:46.37654Z","shell.execute_reply.started":"2025-08-16T06:33:46.363781Z","shell.execute_reply":"2025-08-16T06:33:46.375947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_featured","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:33:46.377223Z","iopub.execute_input":"2025-08-16T06:33:46.377388Z","iopub.status.idle":"2025-08-16T06:33:46.397858Z","shell.execute_reply.started":"2025-08-16T06:33:46.377375Z","shell.execute_reply":"2025-08-16T06:33:46.39725Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_featureed = train_featureed.join(train[[\"sequence_id\", \"gesture\"]].unique(), on=\"sequence_id\", how=\"left\")\ntrain_featureed","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:33:46.399685Z","iopub.execute_input":"2025-08-16T06:33:46.400024Z","iopub.status.idle":"2025-08-16T06:33:46.465426Z","shell.execute_reply.started":"2025-08-16T06:33:46.400009Z","shell.execute_reply":"2025-08-16T06:33:46.464684Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modeling","metadata":{}},{"cell_type":"code","source":"target_col = \"gesture\"\npdf = train_featureed.to_pandas()  # keeps nullable dtypes\n\nle = LabelEncoder()\ny = le.fit_transform(pdf[target_col])\nX = pdf.drop(columns=[target_col, \"sequence_id\"])         # drop id + label\n\njoblib.dump(le, 'le.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:33:46.466266Z","iopub.execute_input":"2025-08-16T06:33:46.466515Z","iopub.status.idle":"2025-08-16T06:33:46.514716Z","shell.execute_reply.started":"2025-08-16T06:33:46.466499Z","shell.execute_reply":"2025-08-16T06:33:46.514114Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def competition_metric(y_true, y_pred, le_instance, all_original_gestures):\n    \"\"\"\n    Competition metric calculation\n    \"\"\"\n    bfrb_gestures = [g for g in all_original_gestures if g in le_instance.classes_]\n    \n    # Binary F1: All are Target in this filtered dataset\n    y_true_binary = np.ones_like(y_true, dtype=int)\n    y_pred_binary = np.ones_like(y_pred, dtype=int)\n    binary_f1 = f1_score(y_true_binary, y_pred_binary, average='binary', pos_label=1, zero_division=0)\n    \n    # Macro F1: specific gesture classification\n    macro_f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)\n    \n    final_score = (binary_f1 + macro_f1) / 2\n    return final_score, binary_f1, macro_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:33:46.515457Z","iopub.execute_input":"2025-08-16T06:33:46.515709Z","iopub.status.idle":"2025-08-16T06:33:46.5207Z","shell.execute_reply.started":"2025-08-16T06:33:46.51568Z","shell.execute_reply":"2025-08-16T06:33:46.519943Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ncv_scores = []\nmodels = []\n\nall_original_gestures_in_train = pdf['gesture'].unique()\n\ncallbacks = [lgb.early_stopping(stopping_rounds=100, verbose=100)]\n\n# LightGBM model with cross-validation\nprint(\"\\nTraining LightGBM models with cross-validation...\")\nfor fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):\n    print(f\"\\nFold {fold + 1}/5\")\n    \n    X_train_fold, X_val_fold = X.iloc[train_idx], X.iloc[val_idx]\n    y_train_fold, y_val_fold = y[train_idx], y[val_idx]\n\n    # LightGBM model with GPU acceleration\n    model = lgb.LGBMClassifier(\n        objective='multiclass',\n        n_estimators= 1000,\n        learning_rate= 0.08,\n        max_depth= 15,\n        reg_alpha= 0.8,\n        lambda_l2= 4.0,  \n        num_leaves=31, \n        min_child_samples= 32,\n        colsample_bytree= 0.85,\n        subsample= 0.5,\n        subsample_freq=0,\n        cat_smooth=20.0,\n        is_unbalance=True,\n        max_bin=127,\n        verbose=-1,  \n        metric='multi_logloss',   \n        device='gpu',  \n    )\n\n    # Train model with verbose output\n    model.fit(\n        X_train_fold, y_train_fold,\n        eval_set=[(X_val_fold, y_val_fold)],  \n        eval_metric='multi_logloss',  \n        callbacks=callbacks\n    )\n    \n    # Predict\n    y_pred_fold = model.predict(X_val_fold)\n\n    # Calculate score\n    score, binary_f1, macro_f1 = competition_metric(\n        y_true=y_val_fold,\n        y_pred=y_pred_fold,\n        all_original_gestures=all_original_gestures_in_train,\n        le_instance=le\n    )\n    \n    cv_scores.append(score)\n    models.append(model)\n    joblib.dump(model, f'model_lgb{fold}.joblib')\n    \n    print(f\"Fold {fold + 1} - Competition Score: {score:.4f} (Binary F1: {binary_f1:.4f}, Macro F1: {macro_f1:.4f})\")\n\n    # feature importances\n    feature_importances = model.feature_importances_    \n    feature_names = X.columns    \n    feature_importance_df = pd.DataFrame({\n        'Feature': feature_names,\n        'Importance': feature_importances\n    })    \n    feature_importance_df = feature_importance_df.sort_values(by='Importance', ascending=False)    \n    print(feature_importance_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:33:46.521386Z","iopub.execute_input":"2025-08-16T06:33:46.521671Z","iopub.status.idle":"2025-08-16T06:37:08.034347Z","shell.execute_reply.started":"2025-08-16T06:33:46.521612Z","shell.execute_reply":"2025-08-16T06:37:08.033399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"\\nCross-validation results:\")\nprint(f\"Mean CV Score: {np.mean(cv_scores):.4f} (+/- {np.std(cv_scores) * 2:.4f})\")\nprint(f\"Individual fold scores: {cv_scores}\")\n\n# Train final model on all data with GPU acceleration\nprint(\"\\nTraining final model on all training data...\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:37:08.035325Z","iopub.execute_input":"2025-08-16T06:37:08.035874Z","iopub.status.idle":"2025-08-16T06:37:08.041063Z","shell.execute_reply.started":"2025-08-16T06:37:08.035844Z","shell.execute_reply":"2025-08-16T06:37:08.0403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}