{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":96164,"databundleVersionId":11418275,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 📈 DRW Crypto Market Prediction | Time Series + Ensemble\n# Welcome to my solution for the DRW Crypto Market Prediction Kaggle competition!\n# If this helps you, consider giving it an upvote ❤️\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\n\n# Feature Engineering\ndef feature_engineering(df):\n   #10\n   df['exp_856P868P855P289'] = np.exp(df['X856'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_860P868P855P289'] = np.exp(df['X860'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_598P868P855P289'] = np.exp(df['X598'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_612P868P855P289'] = np.exp(df['X612'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_289P855P21'] = np.exp(df['X289'] + df['X855'] + df['X21'])\n   df['868xexp_289M125'] = df['X868'] * np.exp(df['X289'] - df['X125'])\n   #9\n   df['exp_603P868P855P289'] = np.exp(df['X603'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_174P868P855P289'] = np.exp(df['X174'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_465P868P855P289'] = np.exp(df['X465'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_125P862P289M125'] = np.exp(df['X125'] + df['X862'] + df['X289'] - df['X125'])\n   df['exp_168P868P855P289'] = np.exp(df['X168'] + df['X868'] + df['X855'] + df['X289'])\n   df['exp_855P289M125'] = np.exp(df['X855'] + df['X289'] - df['X125'])\n   df['exp_302P289M125'] = np.exp(df['X302'] + df['X289'] - df['X125'])\n   df['289xexp_289M125'] = df['X289'] * np.exp(df['X289'] - df['X125'])\n   #8\n   df['exp_862P868P855P289'] = np.exp(df['X862'] + df['X868'] + df['X855'] + df['X289'])\n   df['868x868x855x289'] = df['X868'] * df['X868'] * df['X855'] * df['X289']\n   df['385xexp_289M125'] = df['X385'] * np.exp(df['X289'] - df['X125'])\n   df['exp_862P289M125'] = np.exp(df['X862'] + df['X289'] - df['X125'])\n   df['exp_786P289M125'] = np.exp(df['X786'] + df['X289'] - df['X125'])\n   df['exp_856P289M125'] = np.exp(df['X856'] + df['X289'] - df['X125'])\n   df['852x868x855x289'] = df['X852'] * df['X868'] * df['X855'] * df['X289']\n   df['465x862x465']=df['X465']*df['X465']*df['X862']\n   df['540x881']=df['X540']*df['X881']\n   \n   df['bid_ask_interaction'] = df['bid_qty'] * df['ask_qty']\n   df['bid_buy_interaction'] = df['bid_qty'] * df['buy_qty']\n   df['bid_sell_interaction'] = df['bid_qty'] * df['sell_qty']\n   df['ask_buy_interaction'] = df['ask_qty'] * df['buy_qty']\n   df['ask_sell_interaction'] = df['ask_qty'] * df['sell_qty']\n\n   df['volume_weighted_sell'] = df['sell_qty'] * df['volume']\n   df['buy_sell_ratio'] = df['buy_qty'] / (df['sell_qty'] + 1e-10)\n   df['selling_pressure'] = df['sell_qty'] / (df['volume'] + 1e-10)\n   df['log_volume'] = np.log1p(df['volume'])\n\n   df['effective_spread_proxy'] = np.abs(df['buy_qty'] - df['sell_qty']) / (df['volume'] + 1e-10)\n   df['bid_ask_imbalance'] = (df['bid_qty'] - df['ask_qty']) / (df['bid_qty'] + df['ask_qty'] + 1e-10)\n   df['order_flow_imbalance'] = (df['buy_qty'] - df['sell_qty']) / (df['buy_qty'] + df['sell_qty'] + 1e-10)\n   df['liquidity_ratio'] = (df['bid_qty'] + df['ask_qty']) / (df['volume'] + 1e-10)\n   \n   df['ask_buy_interaction_x_X293']=df['X293']*df['ask_buy_interaction']\n   # Price Pressure Indicators\n   df['net_order_flow'] = df['buy_qty'] - df['sell_qty']\n   df['normalized_net_flow'] = df['net_order_flow'] / (df['volume'] + 1e-10)\n   df['buying_pressure'] = df['buy_qty'] / (df['volume'] + 1e-10)\n   df['volume_weighted_buy'] = df['buy_qty'] * df['volume']\n   \n   # Liquidity Depth Measures\n   df['total_depth'] = df['bid_qty'] + df['ask_qty']\n   df['depth_imbalance'] = (df['bid_qty'] - df['ask_qty']) / (df['total_depth'] + 1e-10)\n   df['relative_spread'] = np.abs(df['bid_qty'] - df['ask_qty']) / (df['total_depth'] + 1e-10)\n   df['log_depth'] = np.log1p(df['total_depth'])\n   \n   # Order Flow Toxicity Proxies\n   df['kyle_lambda'] = np.abs(df['net_order_flow']) / (df['volume'] + 1e-10)\n   df['flow_toxicity'] = np.abs(df['order_flow_imbalance']) * df['volume']\n   df['aggressive_flow_ratio'] = (df['buy_qty'] + df['sell_qty']) / (df['total_depth'] + 1e-10)\n   \n   # Market Activity Indicators\n   df['volume_depth_ratio'] = df['volume'] / (df['total_depth'] + 1e-10)\n   df['activity_intensity'] = (df['buy_qty'] + df['sell_qty']) / (df['volume'] + 1e-10)\n   df['log_buy_qty'] = np.log1p(df['buy_qty'])\n   df['log_sell_qty'] = np.log1p(df['sell_qty'])\n   df['log_bid_qty'] = np.log1p(df['bid_qty'])\n   df['log_ask_qty'] = np.log1p(df['ask_qty'])\n   \n   # Microstructure Volatility Proxies\n   df['realized_spread_proxy'] = 2 * np.abs(df['net_order_flow']) / (df['volume'] + 1e-10)\n   df['price_impact_proxy'] = df['net_order_flow'] / (df['total_depth'] + 1e-10)\n   df['quote_volatility_proxy'] = np.abs(df['depth_imbalance'])\n   \n   # Complex Interaction Terms\n   df['flow_depth_interaction'] = df['net_order_flow'] * df['total_depth']\n   df['imbalance_volume_interaction'] = df['order_flow_imbalance'] * df['volume']\n   df['depth_volume_interaction'] = df['total_depth'] * df['volume']\n   df['buy_sell_spread'] = np.abs(df['buy_qty'] - df['sell_qty'])\n   df['bid_ask_spread'] = np.abs(df['bid_qty'] - df['ask_qty'])\n   \n   # Information Asymmetry Measures\n   df['trade_informativeness'] = df['net_order_flow'] / (df['bid_qty'] + df['ask_qty'] + 1e-10)\n   df['execution_shortfall_proxy'] = df['buy_sell_spread'] / (df['volume'] + 1e-10)\n   df['adverse_selection_proxy'] = df['net_order_flow'] / (df['total_depth'] + 1e-10) * df['volume']\n   \n   # Market Efficiency Indicators\n   df['fill_probability'] = df['volume'] / (df['buy_qty'] + df['sell_qty'] + 1e-10)\n   df['execution_rate'] = (df['buy_qty'] + df['sell_qty']) / (df['total_depth'] + 1e-10)\n   df['market_efficiency'] = df['volume'] / (df['bid_ask_spread'] + 1e-10)\n   \n   # Non-linear Transformations\n   df['sqrt_volume'] = np.sqrt(df['volume'])\n   df['sqrt_depth'] = np.sqrt(df['total_depth'])\n   df['volume_squared'] = df['volume'] ** 2\n   df['imbalance_squared'] = df['order_flow_imbalance'] ** 2\n   \n   # Relative Measures\n   df['bid_ratio'] = df['bid_qty'] / (df['total_depth'] + 1e-10)\n   df['ask_ratio'] = df['ask_qty'] / (df['total_depth'] + 1e-10)\n   df['buy_ratio'] = df['buy_qty'] / (df['buy_qty'] + df['sell_qty'] + 1e-10)\n   df['sell_ratio'] = df['sell_qty'] / (df['buy_qty'] + df['sell_qty'] + 1e-10)\n   \n   # Market Stress Indicators\n   df['liquidity_consumption'] = (df['buy_qty'] + df['sell_qty']) / (df['total_depth'] + 1e-10)\n   df['market_stress'] = df['volume'] / (df['total_depth'] + 1e-10) * np.abs(df['order_flow_imbalance'])\n   df['depth_depletion'] = df['volume'] / (df['bid_qty'] + df['ask_qty'] + 1e-10)\n   \n   # Directional Indicators\n   df['net_buying_ratio'] = df['net_order_flow'] / (df['volume'] + 1e-10)\n   df['directional_volume'] = df['net_order_flow'] * np.log1p(df['volume'])\n   df['signed_volume'] = np.sign(df['net_order_flow']) * df['volume']\n\n   #etc\n   df['sqrt_volume_div_log_volume'] = df['sqrt_volume'] / (df['log_volume'] + 1e-6)\n   df['sqrt_volume_div_activity_intensity'] = df['sqrt_volume'] / (df['activity_intensity'] + 1e-6)\n   df['sqrt_volume_mul_fill_probability'] = df['sqrt_volume'] * df['fill_probability']\n   df['volume_div_sqrt_volume'] = df['volume'] / (df['sqrt_volume'] + 1e-6)\n   df['sqrt_volume_div_fill_probability'] = df['sqrt_volume'] / (df['fill_probability'] + 1e-6)\n   df['sqrt_volume_mul_activity_intensity'] = df['sqrt_volume'] * df['activity_intensity']\n   df['sqrt_volume_div_log_sell_qty'] = df['sqrt_volume'] / (df['log_sell_qty'] + 1e-6)\n   df['log_buy_qty_mul_sqrt_volume'] = df['log_buy_qty'] * df['sqrt_volume']\n   df['sqrt_volume_mul_log_buy_qty'] = df['sqrt_volume'] * df['log_buy_qty']\n   df['log_volume_mul_sqrt_volume'] = df['log_volume'] * df['sqrt_volume']\n   \n   df['log_sell_qty_mul_X598'] = df['log_sell_qty'] * df['X598']\n   df['log_buy_qty_mul_X598'] = df['log_buy_qty'] * df['X598']\n   df['log_volume_mul_X598'] = df['log_volume'] * df['X598']\n   \n   df['sqrt_volume_mul_X856'] = df['sqrt_volume'] * df['X856']\n   \n   df['log_sell_qty_mul_X302'] = df['log_sell_qty'] * df['X302']\n   df['log_volume_mul_X302'] = df['log_volume'] * df['X302']\n   df['log_buy_qty_mul_X302'] = df['log_buy_qty'] * df['X302']\n   \n   df['log_sell_qty_mul_X292'] = df['log_sell_qty'] * df['X292']\n   \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       \"bid_qty\", \"ask_qty\", \"buy_qty\", \"sell_qty\", \"volume\", \"X888\", \"X421\", \"X333\",\n       'X465','X153','X289','X125','X21',\"X868\", \"X786\" ,\"X293\",\"X873\",'X540','X493','X862',\n       'X881','X425','X858',\"X292\",\"X817\", \"X586\"\n       \n       \n   ]\n   SELECTED_FEATURES=[\n\n       \"X863\", \"X856\", \"X598\", \"X862\", \"X385\", \"X603\", \"X860\", \"X674\",\n       \"X415\", \"X345\", \"X855\", \"X174\", \"X302\", \"X178\", \"X168\", \"X612\",\n       \"buy_qty\", \"sell_qty\", \"volume\", \n       \"X888\", \"X421\", \"X333\",\"X292\",\"X817\", \n       \"X586\",\n       'ask_buy_interaction_x_X293',  '868xexp_289M125','exp_786P289M125','exp_856P289M125',\n       'exp_612P868P855P289','exp_598P868P855P289',\n       'exp_855P289M125',\n       '385xexp_289M125','465x862x465','540x881','exp_125P862P289M125','bid_ask_interaction', 'bid_buy_interaction', 'bid_sell_interaction', 'ask_buy_interaction',\n       'ask_sell_interaction', \"log_volume\", 'net_order_flow', 'normalized_net_flow',\n       'buying_pressure', 'volume_weighted_buy', 'total_depth', 'depth_imbalance',\n       'relative_spread', 'log_depth', 'kyle_lambda', 'flow_toxicity', 'aggressive_flow_ratio',\n       'volume_depth_ratio', 'activity_intensity', 'log_buy_qty', 'log_sell_qty',\n       'log_bid_qty', 'log_ask_qty', 'realized_spread_proxy', 'price_impact_proxy',\n       'quote_volatility_proxy', 'flow_depth_interaction', 'imbalance_volume_interaction',\n       'depth_volume_interaction',  'trade_informativeness',\n       'execution_shortfall_proxy', 'adverse_selection_proxy', 'fill_probability',\n       'execution_rate', 'market_efficiency', 'sqrt_volume', 'sqrt_depth', 'volume_squared',\n       'imbalance_squared', 'bid_ratio', 'ask_ratio', 'buy_ratio', 'sell_ratio',\n       'liquidity_consumption', 'market_stress', 'depth_depletion', 'net_buying_ratio',\n       'directional_volume', 'signed_volume',   \n       \n   \n       #\"sqrt_volume_div_activity_intensity\",\n       \"sqrt_volume_mul_fill_probability\",\n       \"volume_div_sqrt_volume\",\n       #\"sqrt_volume_div_fill_probability\",\n       #\"sqrt_volume_mul_activity_intensity\",\n       #\"sqrt_volume_div_log_sell_qty\",\n       \"log_buy_qty_mul_sqrt_volume\",\n       \"sqrt_volume_mul_log_buy_qty\",\n       \"log_volume_mul_sqrt_volume\",\n       \n       #\"log_sell_qty_mul_X598\",\n       #\"log_buy_qty_mul_X598\",\n       #\"log_volume_mul_X598\",\n       \"sqrt_volume_mul_X856\",\n       \"log_sell_qty_mul_X302\",\n       \"log_volume_mul_X302\",\n       \"log_buy_qty_mul_X302\",\n       \"log_sell_qty_mul_X292\"\n\n\n                     \n   \n   ]\n\n   LABEL_COLUMN = \"label\"\n   N_FOLDS = 3\n   RANDOM_STATE = 42\n\nXGB_PARAMS = {\n   \"tree_method\": \"hist\",\n   \"device\": \"gpu\",\n   \"colsample_bylevel\": 0.4778,\n   \"colsample_bynode\": 0.3628,\n   \"colsample_bytree\": 0.7107,\n   \"gamma\": 1.7095,\n   \"learning_rate\": 0.02213,\n   \"max_depth\": 20,\n   \"max_leaves\": 12,\n   \"min_child_weight\": 16,\n   \"n_estimators\": 1667,\n   \"subsample\": 0.06567,\n   \"reg_alpha\": 39.3524,\n   \"reg_lambda\": 75.4484,\n   \"verbosity\": 0,\n   \"random_state\": Config.RANDOM_STATE,\n   \"n_jobs\": -1\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) -> np.ndarray:\n   positions = np.arange(n)\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   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\n#Config.FEATURES += [\"bid_qty\", \"ask_qty\", \"buy_qty\", \"sell_qty\", \"volume\"]\nConfig.FEATURES = list(set(Config.FEATURES))  # remove duplicates\n\n# Training and Evaluation\ndef get_model_slices(n_samples: int):\n   base_slices = [\n       {\"name\": \"full_data\", \"cutoff\": 0, \"is_oldest\": False, \"outlier_adjusted\": False},\n       {\"name\": \"last_92pct\", \"cutoff\": int(0.08 * n_samples), \"is_oldest\": False, \"outlier_adjusted\": False},\n       {\"name\": \"last_90pct\", \"cutoff\": int(0.10 * n_samples), \"is_oldest\": False, \"outlier_adjusted\": False},\n       {\"name\": \"last_85pct\", \"cutoff\": int(0.15 * n_samples), \"is_oldest\": False, \"outlier_adjusted\": False},\n       {\"name\": \"last_80pct\", \"cutoff\": int(0.20 * n_samples), \"is_oldest\": False, \"outlier_adjusted\": False},\n       {\"name\": \"last_50pct\", \"cutoff\": int(0.50 * n_samples), \"is_oldest\": False, \"outlier_adjusted\": False},\n       {\"name\": \"oldest_30pct\", \"cutoff\": int(0.30 * n_samples), \"is_oldest\": True, \"outlier_adjusted\": False},\n       {\"name\": \"oldest_25pct\", \"cutoff\": int(0.25 * n_samples), \"is_oldest\": True, \"outlier_adjusted\": False},\n   ]\n   \n   # Duplicate slices with outlier adjustment\n   outlier_adjusted_slices = []\n   for slice_info in base_slices:\n       adjusted_slice = slice_info.copy()\n       adjusted_slice[\"name\"] = f\"{slice_info['name']}_outlier_adj\"\n       adjusted_slice[\"outlier_adjusted\"] = True\n       outlier_adjusted_slices.append(adjusted_slice)\n   \n   return base_slices + outlier_adjusted_slices\n\ndef train_and_evaluate(train_df, test_df):\n   n_samples = len(train_df)\n   model_slices = get_model_slices(n_samples)\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   # 모델 저장용 딕셔너리 추가 (예: learner_name -> slice_name -> list of models per fold)\n   trained_models = {\n       learner[\"name\"]: {s[\"name\"]: [] 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.SELECTED_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           \n           if s[\"is_oldest\"]:\n               subset = train_df.iloc[:cutoff].reset_index(drop=True)\n               rel_idx = train_idx[train_idx < cutoff]\n           else:\n               subset = train_df.iloc[cutoff:].reset_index(drop=True)\n               rel_idx = train_idx[train_idx >= cutoff] - cutoff\n\n           if len(rel_idx) == 0:\n               continue\n\n           X_train = subset.iloc[rel_idx][Config.SELECTED_FEATURES]\n           y_train = subset.iloc[rel_idx][Config.LABEL_COLUMN]\n           \n           if s[\"is_oldest\"]:\n               sw = create_time_decay_weights(len(subset))[rel_idx]\n           else:\n               sw = create_time_decay_weights(len(subset))[rel_idx] if cutoff > 0 else full_weights[train_idx]\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, y_train, sample_weight=sw, eval_set=[(X_valid, y_valid)], verbose=False)\n\n               # 학습된 모델 저장\n               trained_models[learner[\"name\"]][slice_name].append(model)\n\n               if s[\"is_oldest\"]:\n                   mask = valid_idx < cutoff\n               else:\n                   mask = valid_idx >= cutoff\n                   \n               if mask.any():\n                   idxs = valid_idx[mask]\n                   oof_preds[learner[\"name\"]][slice_name][idxs] = model.predict(train_df.iloc[idxs][Config.SELECTED_FEATURES])\n               if 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_preds[learner[\"name\"]][slice_name] += model.predict(test_df[Config.SELECTED_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, trained_models\n\n# Submission\nmanual_weights = {\n   \"full_data\": 1,\n   \"last_92pct\": 0.025,\n   \"last_90pct\": 1,\n   \"last_85pct\": 1,\n   \"last_80pct\": 1,\n   \"last_50pct\": 1,\n   \"oldest_30pct\": 0.025,\n   \"oldest_25pct\": 1,\n}\n\ndef ensemble_and_submit(train_df, oof_preds, test_preds, submission_df, manual_weights=None):\n   learner_ensembles = {}\n\n   # 슬라이스 weight 설정\n   weights = manual_weights if manual_weights is not None else {\n       s: 1.0 for s in next(iter(oof_preds.values())).keys()\n   }\n\n   total_weight = sum(weights.values())\n\n   for learner_name in oof_preds:\n       oof_weighted = sum(\n           weights[s] / total_weight * oof_preds[learner_name][s]\n           for s in weights if s in oof_preds[learner_name]\n       )\n       test_weighted = sum(\n           weights[s] / total_weight * test_preds[learner_name][s]\n           for s in weights if s in test_preds[learner_name]\n       )\n\n       score_weighted = pearsonr(train_df[Config.LABEL_COLUMN], oof_weighted)[0]\n\n       print(f\"{learner_name.upper()} Weighted Ensemble Pearson: {score_weighted:.4f}\")\n\n       learner_ensembles[learner_name] = {\n           \"oof_weighted\": oof_weighted,\n           \"test_weighted\": test_weighted\n       }\n\n   # 여러 learner 평균\n   final_oof = np.mean([le[\"oof_weighted\"] for le in learner_ensembles.values()], axis=0)\n   final_test = np.mean([le[\"test_weighted\"] 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 (weighted): {final_score:.4f}\")\n\n   submission_df[\"prediction\"] = final_test\n   submission_df.to_csv(\"submission.csv\", index=False)\n   print(\"Saved: submission.csv\")\n\n# Main\nif __name__ == \"__main__\":\n   train_df, test_df, submission_df = load_data()\n   oof_preds, test_preds, model_slices, trained_models = train_and_evaluate(train_df, test_df)\n   ensemble_and_submit(train_df, oof_preds, test_preds, submission_df, manual_weights)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}