{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# CMI V13 Synthetic Data Training\n\n\u7269\u7406\u6cd5\u5247\u30d9\u30fc\u30b9\u5408\u6210\u30c7\u30fc\u30bf\u6226\u7565\uff08\u6539\u826f\u7248\uff09\n- V11\u306e\u5931\u6557\uff0830%\u7cbe\u5ea6\uff09\u3092\u8e0f\u307e\u3048\u305f\u7269\u7406\u7684\u306b\u81ea\u7136\u306a\u5408\u6210\u30c7\u30fc\u30bf\u751f\u6210\n- \u6bb5\u968e\u7684\u6bd4\u7387\u30c6\u30b9\u30c8\uff085%, 10%, 15%, 20%\uff09\n- \u7269\u7406\u7684\u5236\u7d04: \u30bb\u30f3\u30b5\u30fc\u76f8\u95a2\u4fdd\u6301\u3001\u30af\u30a9\u30fc\u30bf\u30cb\u30aa\u30f3\u6b63\u898f\u5316\u3001\u91cd\u529b\u5236\u7d04"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import numpy as np\nimport pandas as pd\nimport pickle\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import StandardScaler\nimport lightgbm as lgb\nimport xgboost as xgb\nfrom catboost import CatBoostClassifier\nimport warnings\nimport os\nimport joblib\n\nwarnings.filterwarnings(\"ignore\")\n\nprint(\"Starting CMI V13 Synthetic Data Training...\")\nprint(\"GPU available:\", os.system(\"nvidia-smi\") == 0)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "class PhysicsBasedSyntheticDataGenerator:\n    \"\"\"\u7269\u7406\u6cd5\u5247\u306b\u57fa\u3065\u304f\u81ea\u7136\u306a\u5408\u6210\u30c7\u30fc\u30bf\u751f\u6210\u5668\"\"\"\n    \n    def __init__(self, train_df):\n        self.train_df = train_df.copy()\n        self.sensor_correlations = self._calculate_sensor_correlations()\n        self.gesture_distributions = self._analyze_gesture_distributions()\n        \n    def _calculate_sensor_correlations(self):\n        \"\"\"\u30bb\u30f3\u30b5\u30fc\u9593\u306e\u7269\u7406\u7684\u76f8\u95a2\u3092\u8a08\u7b97\"\"\"\n        correlations = {}\n        \n        # \u52a0\u901f\u5ea6-\u30b8\u30e3\u30a4\u30ed\u76f8\u95a2\n        acc_cols = [c for c in self.train_df.columns if c.startswith(\"acc_\")]\n        gyro_cols = [c for c in self.train_df.columns if c.startswith(\"gyro_\")]\n        quat_cols = [c for c in self.train_df.columns if c.startswith(\"quat_\")]\n        \n        if acc_cols and gyro_cols:\n            correlations[\"acc_gyro\"] = self.train_df[acc_cols + gyro_cols].corr()\n        \n        if quat_cols:\n            correlations[\"quat\"] = self.train_df[quat_cols].corr()\n            \n        return correlations\n    \n    def _analyze_gesture_distributions(self):\n        \"\"\"\u30b8\u30a7\u30b9\u30c1\u30e3\u30fc\u3054\u3068\u306e\u30bb\u30f3\u30b5\u30fc\u30d1\u30bf\u30fc\u30f3\u5206\u6790\"\"\"\n        distributions = {}\n        \n        for gesture in self.train_df[\"gesture\"].unique():\n            gesture_data = self.train_df[self.train_df[\"gesture\"] == gesture]\n            \n            # \u5404\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u8a08\u7684\u7279\u6027\n            sensor_stats = {}\n            for col in [\"acc_x\", \"acc_y\", \"acc_z\", \"gyro_x\", \"gyro_y\", \"gyro_z\"]:\n                if col in gesture_data.columns:\n                    sensor_stats[col] = {\n                        \"mean\": gesture_data[col].mean(),\n                        \"std\": gesture_data[col].std(),\n                        \"min\": gesture_data[col].min(),\n                        \"max\": gesture_data[col].max(),\n                        \"q25\": gesture_data[col].quantile(0.25),\n                        \"q75\": gesture_data[col].quantile(0.75),\n                    }\n            \n            distributions[gesture] = sensor_stats\n            \n        return distributions\n    \n    def generate_realistic_sequence(self, base_sequence, gesture, noise_factor=0.08):\n        \"\"\"\u7269\u7406\u7684\u306b\u81ea\u7136\u306a\u5408\u6210\u30b7\u30fc\u30b1\u30f3\u30b9\u751f\u6210\"\"\"\n        synthetic_seq = base_sequence.copy()\n        \n        # \u30b8\u30a7\u30b9\u30c1\u30e3\u30fc\u56fa\u6709\u306e\u7d71\u8a08\u7684\u7279\u6027\u3092\u53d6\u5f97\n        if gesture in self.gesture_distributions:\n            gesture_stats = self.gesture_distributions[gesture]\n            \n            # \u5404\u30bb\u30f3\u30b5\u30fc\u306b\u7269\u7406\u7684\u5236\u7d04\u3092\u4fdd\u6301\u3057\u305f\u30ce\u30a4\u30ba\u3092\u8ffd\u52a0\n            for col in [\"acc_x\", \"acc_y\", \"acc_z\", \"gyro_x\", \"gyro_y\", \"gyro_z\"]:\n                if col in synthetic_seq.columns and col in gesture_stats:\n                    stats = gesture_stats[col]\n                    \n                    # \u5143\u30c7\u30fc\u30bf\u306e\u7bc4\u56f2\u5185\u3067\u81ea\u7136\u306a\u30ce\u30a4\u30ba\u3092\u8ffd\u52a0\n                    current_values = synthetic_seq[col].values\n                    noise_range = stats[\"std\"] * noise_factor\n                    noise = np.random.normal(0, noise_range, len(current_values))\n                    \n                    # \u7269\u7406\u7684\u5236\u9650\u5185\u306b\u5236\u7d04\n                    synthetic_seq[col] = np.clip(\n                        current_values + noise, stats[\"min\"], stats[\"max\"]\n                    )\n        \n        # \u30af\u30a9\u30fc\u30bf\u30cb\u30aa\u30f3\u306e\u6b63\u898f\u5316\uff08\u7269\u7406\u7684\u5236\u7d04\uff09\n        quat_cols = [\"quat_w\", \"quat_x\", \"quat_y\", \"quat_z\"]\n        if all(col in synthetic_seq.columns for col in quat_cols):\n            for i in range(len(synthetic_seq)):\n                quat = synthetic_seq.iloc[i][quat_cols].values\n                quat_norm = quat / np.linalg.norm(quat)\n                synthetic_seq.iloc[i, synthetic_seq.columns.get_indexer(quat_cols)] = quat_norm\n        \n        # \u52a0\u901f\u5ea6\u30de\u30b0\u30cb\u30c1\u30e5\u30fc\u30c9\u306e\u7269\u7406\u7684\u6574\u5408\u6027\n        if all(col in synthetic_seq.columns for col in [\"acc_x\", \"acc_y\", \"acc_z\"]):\n            acc_magnitude = np.sqrt(\n                synthetic_seq[\"acc_x\"] ** 2\n                + synthetic_seq[\"acc_y\"] ** 2\n                + synthetic_seq[\"acc_z\"] ** 2\n            )\n            # \u91cd\u529b\u4ed8\u8fd1\uff089.8m/s\u00b2\uff09\u306e\u7bc4\u56f2\u3067\u5236\u7d04\n            acc_magnitude = np.clip(acc_magnitude, 5.0, 15.0)\n            \n            # \u5143\u306e\u65b9\u5411\u3092\u4fdd\u6301\u3057\u3064\u3064\u30de\u30b0\u30cb\u30c1\u30e5\u30fc\u30c9\u3092\u8abf\u6574\n            current_magnitude = np.sqrt(\n                synthetic_seq[\"acc_x\"] ** 2\n                + synthetic_seq[\"acc_y\"] ** 2\n                + synthetic_seq[\"acc_z\"] ** 2\n            )\n            ratio = acc_magnitude / (current_magnitude + 1e-10)\n            \n            synthetic_seq[\"acc_x\"] *= ratio\n            synthetic_seq[\"acc_y\"] *= ratio\n            synthetic_seq[\"acc_z\"] *= ratio\n        \n        return synthetic_seq\n    \n    def generate_synthetic_dataset(self, target_ratio=0.15, max_per_gesture=50):\n        \"\"\"\u6307\u5b9a\u6bd4\u7387\u306e\u5408\u6210\u30c7\u30fc\u30bf\u30bb\u30c3\u30c8\u751f\u6210\"\"\"\n        print(f\"Generating synthetic data with {target_ratio * 100:.1f}% ratio...\")\n        \n        synthetic_data = []\n        real_count = len(self.train_df[\"sequence_id\"].unique())\n        target_synthetic_count = int(real_count * target_ratio)\n        \n        gestures = self.train_df[\"gesture\"].unique()\n        samples_per_gesture = max(1, target_synthetic_count // len(gestures))\n        samples_per_gesture = min(samples_per_gesture, max_per_gesture)\n        \n        print(f\"Target synthetic sequences: {target_synthetic_count}\")\n        print(f\"Samples per gesture: {samples_per_gesture}\")\n        \n        for gesture in gestures:\n            gesture_data = self.train_df[self.train_df[\"gesture\"] == gesture]\n            unique_sequences = gesture_data[\"sequence_id\"].unique()\n            \n            # \u30b8\u30a7\u30b9\u30c1\u30e3\u30fc\u3054\u3068\u306b\u6307\u5b9a\u6570\u306e\u5408\u6210\u30b7\u30fc\u30b1\u30f3\u30b9\u751f\u6210\n            for i in range(samples_per_gesture):\n                # \u30e9\u30f3\u30c0\u30e0\u306b\u5b9f\u30b7\u30fc\u30b1\u30f3\u30b9\u3092\u9078\u629e\n                base_seq_id = np.random.choice(unique_sequences)\n                base_sequence = gesture_data[\n                    gesture_data[\"sequence_id\"] == base_seq_id\n                ].copy()\n                \n                # \u7269\u7406\u7684\u306b\u81ea\u7136\u306a\u5408\u6210\u30b7\u30fc\u30b1\u30f3\u30b9\u751f\u6210\n                synthetic_sequence = self.generate_realistic_sequence(\n                    base_sequence, gesture, noise_factor=0.08\n                )\n                \n                # \u65b0\u3057\u3044sequence_id\u3092\u5272\u308a\u5f53\u3066\n                new_seq_id = f\"synthetic_{gesture}_{i:03d}\"\n                synthetic_sequence[\"sequence_id\"] = new_seq_id\n                \n                synthetic_data.append(synthetic_sequence)\n        \n        if synthetic_data:\n            synthetic_df = pd.concat(synthetic_data, ignore_index=True)\n            print(\n                f\"Generated {len(synthetic_df['sequence_id'].unique())} synthetic sequences\"\n            )\n            return synthetic_df\n        else:\n            return pd.DataFrame()\n\nprint(\"PhysicsBasedSyntheticDataGenerator defined.\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "def extract_advanced_features(df):\n    \"\"\"\u9ad8\u5ea6\u306a\u7279\u5fb4\u91cf\u62bd\u51fa\uff08V12\u6e96\u62e0\uff09\"\"\"\n    features = []\n    \n    for seq_id in df[\"sequence_id\"].unique():\n        seq_data = df[df[\"sequence_id\"] == seq_id].copy()\n        feat = {}\n        \n        # Basic info\n        feat[\"sequence_id\"] = seq_id\n        feat[\"subject_id\"] = (\n            seq_data[\"subject_id\"].iloc[0] if \"subject_id\" in seq_data else 0\n        )\n        feat[\"seq_length\"] = len(seq_data)\n        feat[\"seq_length_log\"] = np.log1p(len(seq_data))\n        \n        # Sensor features\n        sensor_cols = [\"acc_x\", \"acc_y\", \"acc_z\", \"gyro_x\", \"gyro_y\", \"gyro_z\"]\n        for col in sensor_cols:\n            if col in seq_data.columns:\n                values = seq_data[col].values\n                feat[f\"{col}_mean\"] = np.mean(values)\n                feat[f\"{col}_std\"] = np.std(values)\n                feat[f\"{col}_max\"] = np.max(values)\n                feat[f\"{col}_min\"] = np.min(values)\n                feat[f\"{col}_range\"] = np.max(values) - np.min(values)\n                feat[f\"{col}_skew\"] = pd.Series(values).skew()\n                feat[f\"{col}_kurt\"] = pd.Series(values).kurtosis()\n                feat[f\"{col}_q50\"] = np.median(values)\n                \n                # FFT features\n                if len(values) > 1:\n                    fft = np.abs(np.fft.fft(values))\n                    feat[f\"{col}_fft_max\"] = np.max(fft)\n                    feat[f\"{col}_fft_mean\"] = np.mean(fft)\n                    feat[f\"{col}_fft_std\"] = np.std(fft)\n                    feat[f\"{col}_fft_energy\"] = np.sum(fft**2)\n                    feat[f\"{col}_dominant_freq\"] = np.argmax(fft)\n                \n                # Jerk features\n                if len(values) > 1:\n                    jerk = np.diff(values)\n                    feat[f\"{col}_jerk_mean\"] = np.mean(jerk)\n                    feat[f\"{col}_jerk_std\"] = np.std(jerk)\n                    feat[f\"{col}_jerk_max\"] = np.max(np.abs(jerk))\n                \n                # Zero crossing\n                feat[f\"{col}_zero_cross\"] = np.sum(np.diff(np.sign(values)) != 0)\n        \n        # Magnitude features\n        if all(col in seq_data.columns for col in [\"acc_x\", \"acc_y\", \"acc_z\"]):\n            acc_mag = np.sqrt(\n                seq_data[\"acc_x\"] ** 2 + seq_data[\"acc_y\"] ** 2 + seq_data[\"acc_z\"] ** 2\n            )\n            feat[\"acc_magnitude_mean\"] = np.mean(acc_mag)\n            feat[\"acc_magnitude_std\"] = np.std(acc_mag)\n            feat[\"acc_magnitude_max\"] = np.max(acc_mag)\n            feat[\"acc_magnitude_energy\"] = np.sum(acc_mag**2)\n            \n            # Linear acceleration (gravity removed)\n            gravity = np.array(\n                [\n                    np.mean(seq_data[\"acc_x\"]),\n                    np.mean(seq_data[\"acc_y\"]),\n                    np.mean(seq_data[\"acc_z\"]),\n                ]\n            )\n            linear_acc = acc_mag - np.linalg.norm(gravity)\n            feat[\"linear_acc_mean\"] = np.mean(linear_acc)\n            feat[\"linear_acc_std\"] = np.std(linear_acc)\n            feat[\"linear_acc_max\"] = np.max(np.abs(linear_acc))\n        \n        # Target\n        feat[\"gesture\"] = seq_data[\"gesture\"].iloc[0]\n        \n        features.append(feat)\n    \n    return pd.DataFrame(features)\n\nprint(\"Feature extraction function defined.\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "def train_hybrid_model_v13(synthetic_ratio=0.15):\n    \"\"\"V13\u30cf\u30a4\u30d6\u30ea\u30c3\u30c9\u30e2\u30c7\u30eb\uff08\u5b9f\u30c7\u30fc\u30bf + \u7269\u7406\u30d9\u30fc\u30b9\u5408\u6210\u30c7\u30fc\u30bf\uff09\"\"\"\n    print(\n        f\"=== CMI V13 Hybrid Model Training (Synthetic: {synthetic_ratio * 100:.1f}%) ===\"\n    )\n    \n    # \u30c7\u30fc\u30bf\u30ed\u30fc\u30c9\n    print(\"Loading data...\")\n    train_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\n    print(f\"Original data shape: {train_df.shape}\")\n    print(f\"Unique sequences: {len(train_df['sequence_id'].unique())}\")\n    print(f\"Unique gestures: {len(train_df['gesture'].unique())}\")\n    \n    # \u5408\u6210\u30c7\u30fc\u30bf\u751f\u6210\n    generator = PhysicsBasedSyntheticDataGenerator(train_df)\n    synthetic_df = generator.generate_synthetic_dataset(target_ratio=synthetic_ratio)\n    \n    # \u30c7\u30fc\u30bf\u7d50\u5408\n    if len(synthetic_df) > 0:\n        combined_df = pd.concat([train_df, synthetic_df], ignore_index=True)\n        print(\n            f\"Combined dataset: {len(train_df)} real + {len(synthetic_df)} synthetic = {len(combined_df)} total\"\n        )\n        print(f\"Synthetic ratio achieved: {len(synthetic_df) / len(combined_df) * 100:.1f}%\")\n    else:\n        combined_df = train_df\n        print(\"No synthetic data generated, using real data only\")\n    \n    # \u7279\u5fb4\u91cf\u62bd\u51fa\n    print(\"Extracting features...\")\n    features_df = extract_advanced_features(combined_df)\n    \n    # \u7279\u5fb4\u91cf\u6e96\u5099\n    feature_cols = [\n        col for col in features_df.columns if col not in [\"sequence_id\", \"gesture\"]\n    ]\n    X = features_df[feature_cols].fillna(0)\n    y = features_df[\"gesture\"].values\n    \n    print(f\"Feature shape: {X.shape}\")\n    print(f\"Feature columns: {len(feature_cols)}\")\n    print(f\"Unique gestures: {len(np.unique(y))}\")\n    \n    # 5-fold GroupKFold validation\n    subject_ids = (\n        features_df[\"subject_id\"].values\n        if \"subject_id\" in features_df.columns\n        else np.arange(len(X))\n    )\n    gkf = GroupKFold(n_splits=5)\n    \n    val_scores = []\n    models = []\n    \n    for fold, (train_idx, val_idx) in enumerate(gkf.split(X, y, subject_ids)):\n        print(f\"\\n--- Fold {fold + 1}/5 ---\")\n        \n        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]\n        y_train, y_val = y[train_idx], y[val_idx]\n        \n        print(f\"Train size: {len(X_train)}, Val size: {len(X_val)}\")\n        \n        # Scaling\n        scaler = StandardScaler()\n        X_train_scaled = scaler.fit_transform(X_train)\n        X_val_scaled = scaler.transform(X_val)\n        \n        # LightGBM\n        lgb_model = lgb.LGBMClassifier(\n            objective=\"multiclass\",\n            num_class=18,\n            n_estimators=500,\n            learning_rate=0.05,\n            num_leaves=31,\n            random_state=42 + fold,\n            verbosity=-1,\n        )\n        lgb_model.fit(\n            X_train_scaled,\n            y_train,\n            eval_set=[(X_val_scaled, y_val)],\n            callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)],\n        )\n        \n        # XGBoost\n        xgb_model = xgb.XGBClassifier(\n            objective=\"multi:softprob\",\n            num_class=18,\n            n_estimators=500,\n            learning_rate=0.05,\n            max_depth=6,\n            random_state=42 + fold,\n            verbosity=0,\n        )\n        xgb_model.fit(\n            X_train_scaled,\n            y_train,\n            eval_set=[(X_val_scaled, y_val)],\n            early_stopping_rounds=50,\n            verbose=False,\n        )\n        \n        # CatBoost\n        cat_model = CatBoostClassifier(\n            iterations=500,\n            learning_rate=0.05,\n            depth=6,\n            random_state=42 + fold,\n            verbose=False,\n        )\n        cat_model.fit(\n            X_train_scaled,\n            y_train,\n            eval_set=(X_val_scaled, y_val),\n            early_stopping_rounds=50,\n            verbose=False,\n        )\n        \n        # Ensemble prediction\n        pred_lgb = lgb_model.predict_proba(X_val_scaled)\n        pred_xgb = xgb_model.predict_proba(X_val_scaled)\n        pred_cat = cat_model.predict_proba(X_val_scaled)\n        \n        ensemble_pred = 0.4 * pred_lgb + 0.3 * pred_xgb + 0.3 * pred_cat\n        val_pred = np.argmax(ensemble_pred, axis=1)\n        \n        # Validation accuracy\n        accuracy = np.mean(val_pred == y_val)\n        val_scores.append(accuracy)\n        \n        print(f\"Fold {fold + 1} Validation Accuracy: {accuracy:.4f}\")\n        \n        models.append(\n            {\"lgb\": lgb_model, \"xgb\": xgb_model, \"cat\": cat_model, \"scaler\": scaler}\n        )\n    \n    mean_cv_score = np.mean(val_scores)\n    std_cv_score = np.std(val_scores)\n    \n    print(f\"\\n=== Cross-Validation Results ===\")\n    print(f\"Mean CV Accuracy: {mean_cv_score:.4f} \u00b1 {std_cv_score:.4f}\")\n    print(f\"Individual fold scores: {[f'{score:.4f}' for score in val_scores]}\")\n    \n    # Return best performing fold model\n    best_fold = np.argmax(val_scores)\n    print(f\"Best fold: {best_fold + 1} (accuracy: {val_scores[best_fold]:.4f})\")\n    \n    return models[best_fold], feature_cols, mean_cv_score\n\nprint(\"Training function defined.\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Test different synthetic ratios\nbest_ratio = 0.10\nbest_score = 0.0\nbest_model = None\nbest_features = None\n\nfor ratio in [0.05, 0.10, 0.15, 0.20]:\n    print(f\"\\n{'=' * 60}\")\n    print(f\"Testing synthetic ratio: {ratio * 100:.1f}%\")\n    print(f\"{'=' * 60}\")\n    \n    try:\n        model, features, cv_score = train_hybrid_model_v13(synthetic_ratio=ratio)\n        print(f\"Final CV Score for {ratio * 100:.1f}% synthetic: {cv_score:.4f}\")\n        \n        # Track best model\n        if cv_score > best_score:\n            best_score = cv_score\n            best_ratio = ratio\n            best_model = model\n            best_features = features\n            print(f\"NEW BEST SCORE: {cv_score:.4f} at ratio {ratio * 100:.0f}%\")\n        \n        # Save model if performance is good\n        if cv_score > 0.55:  # Better than V12's 55-65% range\n            model_name = f\"hybrid_v13_synthetic_{ratio * 100:.0f}pct\"\n            print(f\"Saving model: {model_name}\")\n            \n            # Save models and metadata\n            os.makedirs(model_name, exist_ok=True)\n            \n            # Save each component\n            joblib.dump(model[\"scaler\"], f\"{model_name}/scaler.pkl\")\n            joblib.dump(features, f\"{model_name}/feature_names.pkl\")\n            \n            with open(f\"{model_name}/cv_score.txt\", \"w\") as f:\n                f.write(f\"{cv_score:.6f}\")\n            \n            print(f\"Model saved to {model_name}/\")\n    \n    except Exception as e:\n        print(f\"Error with ratio {ratio}: {e}\")\n        continue\n\nprint(f\"\\n{'=' * 60}\")\nprint(f\"FINAL RESULTS\")\nprint(f\"{'=' * 60}\")\nprint(f\"Best synthetic ratio: {best_ratio * 100:.1f}%\")\nprint(f\"Best CV score: {best_score:.4f}\")\nprint(f\"Previous best (V12): 0.55-0.65\")\nprint(f\"Improvement: {'+' if best_score > 0.60 else '-'}{abs(best_score - 0.60):.4f}\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Save best model with detailed info\nif best_model is not None:\n    final_model_name = f\"v13_best_synthetic_{best_ratio * 100:.0f}pct\"\n    print(f\"Saving final best model: {final_model_name}\")\n    \n    os.makedirs(final_model_name, exist_ok=True)\n    \n    # Save all components\n    joblib.dump(best_model[\"lgb\"], f\"{final_model_name}/lgb_model.pkl\")\n    joblib.dump(best_model[\"xgb\"], f\"{final_model_name}/xgb_model.pkl\")\n    joblib.dump(best_model[\"cat\"], f\"{final_model_name}/cat_model.pkl\")\n    joblib.dump(best_model[\"scaler\"], f\"{final_model_name}/scaler.pkl\")\n    joblib.dump(best_features, f\"{final_model_name}/feature_names.pkl\")\n    \n    # Save metadata\n    metadata = {\n        \"cv_score\": best_score,\n        \"synthetic_ratio\": best_ratio,\n        \"model_version\": \"V13\",\n        \"strategy\": \"Physics-based synthetic data\",\n        \"feature_count\": len(best_features),\n        \"ensemble_weights\": {\"lgb\": 0.4, \"xgb\": 0.3, \"cat\": 0.3}\n    }\n    \n    with open(f\"{final_model_name}/metadata.json\", \"w\") as f:\n        import json\n        json.dump(metadata, f, indent=2)\n    \n    print(f\"Final model saved to {final_model_name}/\")\n    print(f\"Files: lgb_model.pkl, xgb_model.pkl, cat_model.pkl, scaler.pkl, feature_names.pkl, metadata.json\")\nelse:\n    print(\"No best model found - all experiments failed\")\n\nprint(\"\\nV13 Synthetic Data Training Complete!\")"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5"}}, "nbformat": 4, "nbformat_minor": 4}