{"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":"import 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\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\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\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\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_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_24pct\", \"cutoff\": int(0.24 * 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            subset = train_df.iloc[cutoff:].reset_index(drop=True)\n            rel_idx = train_idx[train_idx >= cutoff] - cutoff\n\n            X_train = subset.iloc[rel_idx][Config.SELECTED_FEATURES]\n            y_train = subset.iloc[rel_idx][Config.LABEL_COLUMN]\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                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.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\nmanual_weights = {\n    \"full_data\": 1,\n    \"last_90pct\": 1,\n    \"last_85pct\": 1,\n    \"last_80pct\": 1,\n    \"last_50pct\": 1,\n    \"oldest_24pct\": 1,\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\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}]}