{"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":"none","dataSources":[{"sourceId":96164,"databundleVersionId":11418275,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Credit to the following authors and notebooks/discussions:\n* https://www.kaggle.com/code/bakuer30/drw-remix-vi - For tuning and improving XGBoost parameters and features\n* https://www.kaggle.com/competitions/drw-crypto-market-prediction/discussion/581193 - For the idea of temporal weights / time slices\n* https://www.kaggle.com/competitions/drw-crypto-market-prediction/discussion/584475 - For poiting out that the data at the very beginning may be more valuable too","metadata":{}},{"cell_type":"code","source":"# Complete Working Code with 60% Similarity Slice\n\n# Imports\nimport sys\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import KFold\nfrom xgboost import XGBRegressor\nfrom lightgbm import LGBMRegressor\nfrom scipy.stats import pearsonr\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.linear_model import Ridge\nfrom sklearn.ensemble import RandomForestRegressor\nfrom sklearn.metrics.pairwise import cosine_similarity\nfrom sklearn.neighbors import KNeighborsRegressor\n\n# Feature Engineering\ndef feature_engineering(df):\n    df['volume_weighted_sell'] = df['sell_qty'] * df['volume']\n    df['buy_sell_ratio'] = df['buy_qty'] / (df['sell_qty'] + 1e-8)\n    df['selling_pressure'] = df['sell_qty'] / (df['volume'] + 1e-8)\n    df['effective_spread_proxy'] = np.abs(df['buy_qty'] - df['sell_qty']) / (df['volume'] + 1e-8)\n    \n    df = df.replace([np.inf, -np.inf], np.nan)\n    df = df.fillna(0)\n    return df \n\n# Configuration\nclass Config:\n    TRAIN_PATH = \"/kaggle/input/drw-crypto-market-prediction/train.parquet\"\n    TEST_PATH = \"/kaggle/input/drw-crypto-market-prediction/test.parquet\"\n    SUBMISSION_PATH = \"/kaggle/input/drw-crypto-market-prediction/sample_submission.csv\"\n\n    FEATURES = [\n        \"X863\", \"X856\", \"X598\", \"X862\", \"X385\", \"X852\", \"X603\", \"X860\", \"X674\",\n        \"X415\", \"X345\", \"X855\", \"X174\", \"X302\", \"X178\", \"X168\", \"X612\",\n        \"buy_qty\", \"sell_qty\", \"volume\", \"X888\", \"X421\", \"X333\", \"X292\",\n    ]\n\n    LABEL_COLUMN = \"label\"\n    N_FOLDS = 3\n    RANDOM_STATE = 42\n    \n    # Similarity configuration\n    SIMILARITY_PCT = 0.60  # 60% of training data\n\nXGB_PARAMS = {\n    'tree_method': 'hist', \n    'device': 'gpu',\n    'n_jobs': -1,\n    'colsample_bytree': 0.4111224922845363, \n    'colsample_bynode': 0.28869302181383194,\n    'gamma': 1.4665430311056709, \n    'learning_rate': 0.014053505540364681, \n    'max_depth': 7, \n    'max_leaves': 40, \n    'n_estimators': 500,\n    'reg_alpha': 27.791606770656145, \n    'reg_lambda': 84.90603428439086,\n    'subsample': 0.06567,\n    'verbosity': 0,\n    'random_state': Config.RANDOM_STATE\n}\n\nLEARNERS = [\n    {\"name\": \"xgb\", \"Estimator\": XGBRegressor, \"params\": XGB_PARAMS},\n]\n\n# Loading Data\ndef create_time_decay_weights(n: int, decay: float = 0.9, reverse: bool = False) -> np.ndarray:\n    \"\"\"Create time decay weights. If reverse=True, older data gets higher weight.\"\"\"\n    positions = np.arange(n)\n    if reverse:\n        normalized = 1.0 - (positions / (n - 1))\n    else:\n        normalized = positions / (n - 1)\n    weights = decay ** (1.0 - normalized)\n    return weights * n / weights.sum()\n\ndef load_data():\n    train_df = pd.read_parquet(Config.TRAIN_PATH, columns=Config.FEATURES + [Config.LABEL_COLUMN])\n    test_df = pd.read_parquet(Config.TEST_PATH, columns=Config.FEATURES)\n    submission_df = pd.read_csv(Config.SUBMISSION_PATH)\n\n    train_df = feature_engineering(train_df)\n    test_df = feature_engineering(test_df)\n    \n    print(f\"Loaded data - Train: {train_df.shape}, Test: {test_df.shape}, Submission: {submission_df.shape}\")\n    return train_df.reset_index(drop=True), test_df.reset_index(drop=True), submission_df\n\nConfig.FEATURES += [\"bid_qty\", \"ask_qty\", \"buy_qty\", \"sell_qty\", \"volume\"]\nConfig.FEATURES = list(set(Config.FEATURES))  # remove duplicates\n\n# Similarity Finding Functions\ndef find_similar_records_combined(train_df, features, label_col, early_pct=0.30, recent_pct=0.55, similarity_pct=0.60):\n    \"\"\"\n    Find records that behave similarly to both early and recent periods.\n    This combines multiple methods for robust selection of 60% of data.\n    \"\"\"\n    n_samples = len(train_df)\n    n_select = int(similarity_pct * n_samples)\n    \n    early_cutoff = int(early_pct * n_samples)\n    recent_cutoff = int((1 - recent_pct) * n_samples)\n    \n    print(f\"\\nFinding {n_select} similar records ({similarity_pct*100:.0f}% of {n_samples} total records)\")\n    print(f\"Early period: first {early_cutoff} records\")\n    print(f\"Recent period: last {n_samples - recent_cutoff} records\")\n    \n    # Standardize features\n    scaler = StandardScaler()\n    X_scaled = scaler.fit_transform(train_df[features])\n    y_scaled = StandardScaler().fit_transform(train_df[[label_col]]).ravel()\n    \n    # Initialize scores array\n    similarity_scores = np.zeros(n_samples)\n    \n    # Method 1: Prediction Agreement\n    print(\"\\nMethod 1: Prediction Agreement...\")\n    \n    # Train models on early and recent data\n    early_model = XGBRegressor(n_estimators=100, max_depth=5, learning_rate=0.1, \n                              random_state=42, verbosity=0)\n    early_model.fit(train_df.iloc[:early_cutoff][features], \n                   train_df.iloc[:early_cutoff][label_col])\n    \n    recent_model = XGBRegressor(n_estimators=100, max_depth=5, learning_rate=0.1,\n                               random_state=42, verbosity=0)\n    recent_model.fit(train_df.iloc[recent_cutoff:][features], \n                    train_df.iloc[recent_cutoff:][label_col])\n    \n    # Get predictions\n    early_preds = early_model.predict(train_df[features])\n    recent_preds = recent_model.predict(train_df[features])\n    actual_values = train_df[label_col].values\n    \n    # Calculate agreement\n    pred_diff = np.abs(early_preds - recent_preds)\n    pred_agreement = 1.0 - (pred_diff / (np.std(actual_values) + 1e-8))\n    \n    # Calculate error similarity\n    early_errors = np.abs(actual_values - early_preds)\n    recent_errors = np.abs(actual_values - recent_preds)\n    error_similarity = 1.0 - np.abs(early_errors - recent_errors) / (np.std(actual_values) + 1e-8)\n    \n    # Both models should be reasonably accurate\n    accuracy_score = np.exp(-(early_errors + recent_errors) / (2 * np.std(actual_values)))\n    \n    # Combine scores\n    method1_scores = (pred_agreement * 0.4 + error_similarity * 0.3 + accuracy_score * 0.3)\n    similarity_scores += method1_scores / 4  # Will average across 4 methods\n    \n    # Method 2: Feature-Target Correlation Similarity\n    print(\"Method 2: Feature-Target Correlation Similarity...\")\n    \n    # Calculate correlations in sliding windows\n    window_size = max(100, int(0.02 * n_samples))\n    correlation_scores = np.zeros(n_samples)\n    \n    # Get reference correlations\n    early_corrs = train_df.iloc[:early_cutoff][features].corrwith(\n        train_df.iloc[:early_cutoff][label_col])\n    recent_corrs = train_df.iloc[recent_cutoff:][features].corrwith(\n        train_df.iloc[recent_cutoff:][label_col])\n    \n    for i in range(n_samples):\n        start_idx = max(0, i - window_size // 2)\n        end_idx = min(n_samples, i + window_size // 2)\n        \n        if end_idx - start_idx > 50:\n            local_corrs = train_df.iloc[start_idx:end_idx][features].corrwith(\n                train_df.iloc[start_idx:end_idx][label_col])\n            \n            # Compare to both early and recent correlations\n            early_corr_sim = 1.0 - np.mean(np.abs(local_corrs - early_corrs))\n            recent_corr_sim = 1.0 - np.mean(np.abs(local_corrs - recent_corrs))\n            \n            correlation_scores[i] = np.sqrt(early_corr_sim * recent_corr_sim)\n    \n    similarity_scores += correlation_scores / 4\n    \n    # Method 3: Statistical Distribution Similarity\n    print(\"Method 3: Statistical Distribution Similarity...\")\n    \n    # Calculate distribution characteristics\n    early_mean = np.mean(X_scaled[:early_cutoff], axis=0)\n    early_std = np.std(X_scaled[:early_cutoff], axis=0)\n    recent_mean = np.mean(X_scaled[recent_cutoff:], axis=0)\n    recent_std = np.std(X_scaled[recent_cutoff:], axis=0)\n    \n    distribution_scores = np.zeros(n_samples)\n    \n    for i in range(n_samples):\n        # Distance to early distribution\n        early_dist = np.sqrt(np.mean(((X_scaled[i] - early_mean) / (early_std + 1e-8))**2))\n        # Distance to recent distribution  \n        recent_dist = np.sqrt(np.mean(((X_scaled[i] - recent_mean) / (recent_std + 1e-8))**2))\n        \n        # We want records close to both distributions\n        distribution_scores[i] = 1.0 / (1.0 + early_dist * recent_dist)\n    \n    similarity_scores += distribution_scores / 4\n    \n    # Method 4: K-Nearest Neighbors Consistency\n    print(\"Method 4: K-Nearest Neighbors Consistency...\")\n    \n    k = 30\n    early_knn = KNeighborsRegressor(n_neighbors=k, weights='distance')\n    early_knn.fit(train_df.iloc[:early_cutoff][features], \n                 train_df.iloc[:early_cutoff][label_col])\n    \n    recent_knn = KNeighborsRegressor(n_neighbors=k, weights='distance')\n    recent_knn.fit(train_df.iloc[recent_cutoff:][features], \n                  train_df.iloc[recent_cutoff:][label_col])\n    \n    # Get predictions\n    early_knn_preds = early_knn.predict(train_df[features])\n    recent_knn_preds = recent_knn.predict(train_df[features])\n    \n    # Similarity based on prediction consistency\n    knn_pred_diff = np.abs(early_knn_preds - recent_knn_preds)\n    knn_scores = 1.0 - (knn_pred_diff / (np.std(actual_values) + 1e-8))\n    \n    similarity_scores += knn_scores / 4\n    \n    # Apply smoothing to avoid selecting isolated points\n    print(\"\\nApplying spatial smoothing...\")\n    from scipy.ndimage import gaussian_filter1d\n    similarity_scores = gaussian_filter1d(similarity_scores, sigma=10)\n    \n    # Select top 60% records\n    selected_indices = np.argsort(similarity_scores)[-n_select:]\n    selected_indices = np.sort(selected_indices)\n    \n    # Print selection statistics\n    early_selected = np.sum(selected_indices < early_cutoff)\n    middle_selected = np.sum((selected_indices >= early_cutoff) & (selected_indices < recent_cutoff))\n    recent_selected = np.sum(selected_indices >= recent_cutoff)\n    \n    print(f\"\\nSelection distribution:\")\n    print(f\"  From early period: {early_selected} ({early_selected/n_select*100:.1f}%)\")\n    print(f\"  From middle period: {middle_selected} ({middle_selected/n_select*100:.1f}%)\")\n    print(f\"  From recent period: {recent_selected} ({recent_selected/n_select*100:.1f}%)\")\n    \n    # Analyze quality of selection\n    selected_scores = similarity_scores[selected_indices]\n    print(f\"\\nSimilarity scores:\")\n    print(f\"  Mean score of selected: {np.mean(selected_scores):.4f}\")\n    print(f\"  Min score of selected: {np.min(selected_scores):.4f}\")\n    print(f\"  Mean score of all: {np.mean(similarity_scores):.4f}\")\n    \n    return selected_indices\n\n# Training and Evaluation\ndef get_model_slices(n_samples: int, similar_indices: np.ndarray):\n    return [\n        {\"name\": \"full_data\", \"type\": \"full\", \"cutoff\": 0, \"indices\": None},\n        {\"name\": \"last_75pct\", \"type\": \"recent\", \"cutoff\": int(0.25 * n_samples), \"indices\": None},\n        {\"name\": \"last_50pct\", \"type\": \"recent\", \"cutoff\": int(0.50 * n_samples), \"indices\": None},\n        {\"name\": \"first_35pct\", \"type\": \"early\", \"cutoff\": int(0.35 * n_samples), \"indices\": None},\n        {\"name\": \"similar_60pct\", \"type\": \"similar\", \"cutoff\": 0, \"indices\": similar_indices},\n    ]\n\ndef train_and_evaluate(train_df, test_df):\n    n_samples = len(train_df)\n    \n    # Find similar records (60% of data)\n    similar_indices = find_similar_records_combined(\n        train_df, Config.FEATURES, Config.LABEL_COLUMN, \n        early_pct=0.30, recent_pct=0.55, similarity_pct=Config.SIMILARITY_PCT\n    )\n    \n    model_slices = get_model_slices(n_samples, similar_indices)\n\n    oof_preds = {\n        learner[\"name\"]: {s[\"name\"]: np.zeros(n_samples) for s in model_slices}\n        for learner in LEARNERS\n    }\n    test_preds = {\n        learner[\"name\"]: {s[\"name\"]: np.zeros(len(test_df)) for s in model_slices}\n        for learner in LEARNERS\n    }\n\n    full_weights = create_time_decay_weights(n_samples)\n    kf = KFold(n_splits=Config.N_FOLDS, shuffle=False)\n\n    for fold, (train_idx, valid_idx) in enumerate(kf.split(train_df), start=1):\n        print(f\"\\n--- Fold {fold}/{Config.N_FOLDS} ---\")\n        X_valid = train_df.iloc[valid_idx][Config.FEATURES]\n        y_valid = train_df.iloc[valid_idx][Config.LABEL_COLUMN]\n\n        for s in model_slices:\n            cutoff = s[\"cutoff\"]\n            slice_name = s[\"name\"]\n            slice_type = s[\"type\"]\n            \n            if slice_type == \"full\":\n                # Use all data\n                subset = train_df.reset_index(drop=True)\n                rel_idx = train_idx\n                sw = full_weights[train_idx]\n                \n            elif slice_type == \"recent\":\n                # Use data from cutoff to end (recent data)\n                subset = train_df.iloc[cutoff:].reset_index(drop=True)\n                rel_idx = train_idx[train_idx >= cutoff] - cutoff\n                if cutoff > 0:\n                    sw = create_time_decay_weights(len(subset))[rel_idx]\n                else:\n                    sw = full_weights[train_idx]\n                    \n            elif slice_type == \"early\":\n                # Use data from start to cutoff (early data)\n                subset = train_df.iloc[:cutoff].reset_index(drop=True)\n                rel_idx = train_idx[train_idx < cutoff]\n                if len(rel_idx) > 0:\n                    sw = create_time_decay_weights(len(subset))[rel_idx]\n                else:\n                    sw = np.array([])\n                    \n            elif slice_type == \"similar\":\n                # Use similar records (non-continuous)\n                similar_indices = s[\"indices\"]\n                # Find which training indices are in the similar set\n                train_in_similar = np.isin(train_idx, similar_indices)\n                if np.any(train_in_similar):\n                    # Get the actual indices that are both in train and similar\n                    actual_train_idx = train_idx[train_in_similar]\n                    subset = train_df.iloc[similar_indices].reset_index(drop=True)\n                    # Map actual_train_idx to positions in subset\n                    idx_map = {orig_idx: new_idx for new_idx, orig_idx in enumerate(similar_indices)}\n                    rel_idx = np.array([idx_map[idx] for idx in actual_train_idx if idx in idx_map])\n                    # Use time decay weights based on original positions\n                    original_positions = np.array([idx for idx in actual_train_idx if idx in idx_map])\n                    sw = create_time_decay_weights(n_samples)[original_positions]\n                else:\n                    rel_idx = np.array([])\n                    sw = np.array([])\n\n            # Skip if no training data available for this slice\n            if len(rel_idx) == 0:\n                print(f\"  Skipping slice: {slice_name} (no training data in fold)\")\n                continue\n\n            X_train = subset.iloc[rel_idx][Config.FEATURES]\n            y_train = subset.iloc[rel_idx][Config.LABEL_COLUMN]\n            \n            X_train_np = X_train.values\n            y_train_np = y_train.values\n            X_valid_np = X_valid.values\n            y_valid_np = y_valid.values\n            \n            print(f\"  Training slice: {slice_name}, samples: {len(X_train)}\")\n\n            for learner in LEARNERS:\n                model = learner[\"Estimator\"](**learner[\"params\"])\n                model.fit(X_train_np, y_train_np, sample_weight=sw, \n                          eval_set=[(X_valid_np, y_valid_np)], verbose=False)\n                \n                # Handle predictions based on slice type\n                if slice_type == \"early\":\n                    # For early slice, only predict on validation samples that were in the training range\n                    mask = valid_idx < cutoff\n                    if mask.any():\n                        idxs = valid_idx[mask]\n                        oof_preds[learner[\"name\"]][slice_name][idxs] = model.predict(train_df.iloc[idxs][Config.FEATURES])\n                    # For validation samples outside the early training range, use full_data predictions\n                    if (~mask).any():\n                        oof_preds[learner[\"name\"]][slice_name][valid_idx[~mask]] = oof_preds[learner[\"name\"]][\"full_data\"][valid_idx[~mask]]\n                        \n                elif slice_type == \"similar\":\n                    # For similar slice, predict on all validation samples\n                    all_preds = model.predict(train_df.iloc[valid_idx][Config.FEATURES])\n                    oof_preds[learner[\"name\"]][slice_name][valid_idx] = all_preds\n                    \n                else:\n                    # For recent slices and full data\n                    mask = valid_idx >= cutoff if slice_type == \"recent\" else np.ones(len(valid_idx), dtype=bool)\n                    if mask.any():\n                        idxs = valid_idx[mask]\n                        oof_preds[learner[\"name\"]][slice_name][idxs] = model.predict(train_df.iloc[idxs][Config.FEATURES])\n                    if slice_type == \"recent\" and cutoff > 0 and (~mask).any():\n                        oof_preds[learner[\"name\"]][slice_name][valid_idx[~mask]] = oof_preds[learner[\"name\"]][\"full_data\"][valid_idx[~mask]]\n\n                # Test predictions (always use the model regardless of slice type)\n                test_preds[learner[\"name\"]][slice_name] += model.predict(test_df[Config.FEATURES])\n\n    # Normalize test predictions\n    for learner_name in test_preds:\n        for slice_name in test_preds[learner_name]:\n            test_preds[learner_name][slice_name] /= Config.N_FOLDS\n\n    return oof_preds, test_preds, model_slices\n\n# Submission\ndef ensemble_and_submit(train_df, oof_preds, test_preds, submission_df):\n    learner_ensembles = {}\n    \n    print(\"\\nIndividual Slice Scores:\")\n    for learner_name in oof_preds:\n        scores = {}\n        for s in oof_preds[learner_name]:\n            # Calculate score only on samples where the model made actual predictions\n            score = pearsonr(train_df[Config.LABEL_COLUMN], oof_preds[learner_name][s])[0]\n            scores[s] = score\n            print(f\"  {learner_name} - {s}: {score:.4f}\")\n        \n        total_score = sum(scores.values())\n\n        # Simple average ensemble\n        oof_simple = np.mean(list(oof_preds[learner_name].values()), axis=0)\n        test_simple = np.mean(list(test_preds[learner_name].values()), axis=0)\n        score_simple = pearsonr(train_df[Config.LABEL_COLUMN], oof_simple)[0]\n\n        # Weighted ensemble based on OOF scores\n        oof_weighted = sum(scores[s] / total_score * oof_preds[learner_name][s] for s in scores)\n        test_weighted = sum(scores[s] / total_score * test_preds[learner_name][s] for s in scores)\n        score_weighted = pearsonr(train_df[Config.LABEL_COLUMN], oof_weighted)[0]\n        \n        # Custom ensemble - adjust weights based on slice performance\n        custom_weights = {}\n        for s in scores:\n            if s == 'similar_60pct':\n                # Give similarity slice weight based on its relative performance\n                avg_other_scores = np.mean([v for k, v in scores.items() if k != 'similar_60pct'])\n                if scores[s] > avg_other_scores:\n                    custom_weights[s] = scores[s] * 1.2  # 20% bonus if above average\n                else:\n                    custom_weights[s] = scores[s] * 0.8  # 20% penalty if below average\n            else:\n                custom_weights[s] = scores[s]\n        \n        total_custom = sum(custom_weights.values())\n        oof_custom = sum(custom_weights[s] / total_custom * oof_preds[learner_name][s] for s in scores)\n        test_custom = sum(custom_weights[s] / total_custom * test_preds[learner_name][s] for s in scores)\n        score_custom = pearsonr(train_df[Config.LABEL_COLUMN], oof_custom)[0]\n\n        print(f\"\\n{learner_name.upper()} Simple Ensemble Pearson:   {score_simple:.4f}\")\n        print(f\"{learner_name.upper()} Weighted Ensemble Pearson: {score_weighted:.4f}\")\n        print(f\"{learner_name.upper()} Custom Ensemble Pearson:   {score_custom:.4f}\")\n\n        # Store the best performing ensemble\n        best_score = max(score_simple, score_weighted, score_custom)\n        if best_score == score_custom:\n            learner_ensembles[learner_name] = {\n                \"oof\": oof_custom,\n                \"test\": test_custom,\n                \"type\": \"custom\"\n            }\n        elif best_score == score_weighted:\n            learner_ensembles[learner_name] = {\n                \"oof\": oof_weighted,\n                \"test\": test_weighted,\n                \"type\": \"weighted\"\n            }\n        else:\n            learner_ensembles[learner_name] = {\n                \"oof\": oof_simple,\n                \"test\": test_simple,\n                \"type\": \"simple\"\n            }\n\n    # Final ensemble across all learners\n    final_oof = np.mean([le[\"oof\"] for le in learner_ensembles.values()], axis=0)\n    final_test = np.mean([le[\"test\"] for le in learner_ensembles.values()], axis=0)\n    final_score = pearsonr(train_df[Config.LABEL_COLUMN], final_oof)[0]\n\n    print(f\"\\nFINAL ensemble across learners Pearson: {final_score:.4f}\")\n    print(f\"Ensemble types used: {[le['type'] for le in learner_ensembles.values()]}\")\n\n    submission_df[\"prediction\"] = final_test\n    submission_df.to_csv(\"submission_with_60pct_similarity.csv\", index=False)\n    print(\"\\nSaved: submission_with_60pct_similarity.csv\")\n    \n    # Also save individual slice predictions for analysis\n    print(\"\\nSaving individual slice predictions...\")\n    for learner_name in test_preds:\n        for slice_name in test_preds[learner_name]:\n            slice_submission = submission_df.copy()\n            slice_submission[\"prediction\"] = test_preds[learner_name][slice_name]\n            filename = f\"submission_{learner_name}_{slice_name}.csv\"\n            slice_submission.to_csv(filename, index=False)\n            print(f\"  Saved: {filename}\")\n\n# Main\nif __name__ == \"__main__\":\n    print(\"=\"*80)\n    print(\"CRYPTO PREDICTION WITH 60% SIMILARITY SLICE\")\n    print(\"=\"*80)\n    \n    # Load data\n    train_df, test_df, submission_df = load_data()\n    \n    # Train and evaluate\n    oof_preds, test_preds, model_slices = train_and_evaluate(train_df, test_df)\n    \n    # Create submissions\n    ensemble_and_submit(train_df, oof_preds, test_preds, submission_df)\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"PROCESSING COMPLETE\")\n    print(\"=\"*80)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}