From bdf937086f175dab2daab623e76585797b9a4528 Mon Sep 17 00:00:00 2001 From: liaozhaorun <1300336796@qq.com> Date: Sat, 14 Mar 2026 02:41:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(training):=20=E7=AE=80=E5=8C=96=20Ligh?= =?UTF-8?q?tGBM=20=E6=A8=A1=E5=9E=8B=E5=8F=82=E6=95=B0=E5=A4=84=E7=90=86?= =?UTF-8?q?=20-=20=E9=87=8D=E6=9E=84=20LightGBM=20=E5=92=8C=20LambdaRank?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=8F=90=E5=8F=96=E9=80=BB=E8=BE=91=20-=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=B1=BB=E5=8F=AA=E4=BF=9D=E7=95=99=20params=20?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=EF=BC=8C=E7=AC=A6=E5=90=88=20LightGBM=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/experiment/learn_to_rank.ipynb | 661 +----------------- src/experiment/learn_to_rank.py | 8 +- src/training/components/models/lightgbm.py | 71 +- .../components/models/lightgbm_lambdarank.py | 229 ++---- 4 files changed, 105 insertions(+), 864 deletions(-) diff --git a/src/experiment/learn_to_rank.ipynb b/src/experiment/learn_to_rank.ipynb index cc29291..a7c7616 100644 --- a/src/experiment/learn_to_rank.ipynb +++ b/src/experiment/learn_to_rank.ipynb @@ -22,12 +22,7 @@ "source": "## 1. 导入依赖" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:00.178020Z", - "start_time": "2026-03-13T18:09:58.791253Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "import os\n", @@ -56,7 +51,7 @@ "\n" ], "outputs": [], - "execution_count": 1 + "execution_count": null }, { "metadata": {}, @@ -64,12 +59,7 @@ "source": "## 2. 辅助函数" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:00.209449Z", - "start_time": "2026-03-13T18:10:00.197996Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "def register_factors(\n", @@ -273,7 +263,7 @@ "\n" ], "outputs": [], - "execution_count": 2 + "execution_count": null }, { "metadata": {}, @@ -285,12 +275,7 @@ ] }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:00.241454Z", - "start_time": "2026-03-13T18:10:00.237620Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "# 特征因子定义字典(复用 regression.ipynb 的因子定义)\n", @@ -341,6 +326,7 @@ " \"net_profit_yoy\",\n", " \"revenue_yoy\",\n", " \"healthy_expansion_velocity\",\n", + " \"ebit_rank\",\n", " # ================= 6. 基本面估值与截面动量共振 =================\n", " \"EP\",\n", " \"BP\",\n", @@ -352,7 +338,6 @@ " \"pe_expansion_trend\",\n", " \"value_price_divergence\",\n", " \"active_market_cap\",\n", - " \"ebit_rank\",\n", "]\n", "\n", "# 因子定义字典(完整因子库)\n", @@ -366,7 +351,7 @@ "}" ], "outputs": [], - "execution_count": 3 + "execution_count": null }, { "metadata": {}, @@ -374,12 +359,7 @@ "source": "### 3.2 训练参数配置" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:00.257506Z", - "start_time": "2026-03-13T18:10:00.253934Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "# 日期范围配置(正确的 train/val/test 三分法)\n", @@ -404,7 +384,7 @@ " \"max_depth\": 4,\n", " \"min_data_in_leaf\": 20,\n", " \"n_estimators\": 2000,\n", - " \"early_stopping_round\": 300,\n", + " \"early_stopping_round\": 100,\n", " \"subsample\": 0.8,\n", " \"colsample_bytree\": 0.8,\n", " \"reg_alpha\": 0.1,\n", @@ -435,7 +415,7 @@ " )\n", "\n", " valid_df = df.filter(code_filter)\n", - " n = min(1000, len(valid_df))\n", + " n = min(500, len(valid_df))\n", " small_cap_codes = valid_df.sort(\"total_mv\").head(n)[\"ts_code\"]\n", "\n", " return df[\"ts_code\"].is_in(small_cap_codes)\n", @@ -452,7 +432,7 @@ "TOP_N = 5 # 可调整为 10, 20 等" ], "outputs": [], - "execution_count": 4 + "execution_count": null }, { "metadata": {}, @@ -460,12 +440,7 @@ "source": "## 4. 训练流程" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:11.844363Z", - "start_time": "2026-03-13T18:10:00.265162Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -548,224 +523,8 @@ " persist_model=PERSIST_MODEL,\n", ")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "LightGBM LambdaRank 排序学习训练\n", - "================================================================================\n", - "\n", - "[1] 创建 FactorEngine\n", - "\n", - "[2] 定义因子(从 metadata 注册)\n", - "================================================================================\n", - "注册因子\n", - "================================================================================\n", - "\n", - "注册特征因子(从 metadata):\n", - " - ma_5\n", - " - ma_20\n", - " - ma_ratio_5_20\n", - " - bias_10\n", - " - high_low_ratio\n", - " - bbi_ratio\n", - " - return_5\n", - " - return_20\n", - " - kaufman_ER_20\n", - " - mom_acceleration_10_20\n", - " - drawdown_from_high_60\n", - " - up_days_ratio_20\n", - " - volatility_5\n", - " - volatility_20\n", - " - volatility_ratio\n", - " - std_return_20\n", - " - sharpe_ratio_20\n", - " - min_ret_20\n", - " - volatility_squeeze_5_60\n", - " - overnight_intraday_diff\n", - " - upper_shadow_ratio\n", - " - capital_retention_20\n", - " - max_ret_20\n", - " - volume_ratio_5_20\n", - " - turnover_rate_mean_5\n", - " - turnover_deviation\n", - " - amihud_illiq_20\n", - " - turnover_cv_20\n", - " - pv_corr_20\n", - " - close_vwap_deviation\n", - " - roe\n", - " - roa\n", - " - profit_margin\n", - " - debt_to_equity\n", - " - current_ratio\n", - " - net_profit_yoy\n", - " - revenue_yoy\n", - " - healthy_expansion_velocity\n", - " - EP\n", - " - BP\n", - " - CP\n", - " - market_cap_rank\n", - " - turnover_rank\n", - " - return_5_rank\n", - " - EP_rank\n", - " - pe_expansion_trend\n", - " - value_price_divergence\n", - " - active_market_cap\n", - " - ebit_rank\n", - "\n", - "注册特征因子(表达式):\n", - " - turnover_rate_volatility: ts_std(log(turnover_rate), 20)\n", - "\n", - "注册 Label 因子(表达式):\n", - " - future_return_5_rank: (ts_delay(close, -5) / ts_delay(open, -1)) - 1\n", - "\n", - "特征因子数: 50\n", - " - 来自 metadata: 49\n", - " - 来自表达式: 1\n", - "Label: future_return_5_rank\n", - "已注册因子总数: 64\n", - "\n", - "[3] 准备数据\n", - "\n", - "================================================================================\n", - "准备数据\n", - "================================================================================\n", - "\n", - "计算因子: 20200101 - 20251231\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "D:\\PyProject\\ProStock\\src\\data\\financial_loader.py:148: UserWarning: Sortedness of columns cannot be checked when 'by' groups provided\n", - " merged = df_price.join_asof(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "数据形状: (7044952, 71)\n", - "数据列: ['ts_code', 'trade_date', 'open', 'high', 'turnover_rate', 'low', 'vol', 'amount', 'close', 'total_assets', 'total_mv', 'f_ann_date', 'ebit', 'revenue', 'n_income', 'total_liab', 'total_hldr_eqy_exc_min_int', 'total_cur_assets', 'total_cur_liab', 'n_cashflow_act', 'ma_5', 'ma_20', 'ma_ratio_5_20', 'bias_10', 'high_low_ratio', 'bbi_ratio', 'return_5', 'return_20', 'kaufman_ER_20', 'mom_acceleration_10_20', 'drawdown_from_high_60', 'up_days_ratio_20', 'volatility_5', 'volatility_20', 'volatility_ratio', 'std_return_20', 'sharpe_ratio_20', 'min_ret_20', 'volatility_squeeze_5_60', 'overnight_intraday_diff', 'upper_shadow_ratio', 'capital_retention_20', 'max_ret_20', 'volume_ratio_5_20', 'turnover_rate_mean_5', 'turnover_deviation', 'amihud_illiq_20', 'turnover_cv_20', 'pv_corr_20', 'close_vwap_deviation', 'roe', 'roa', 'profit_margin', 'debt_to_equity', 'current_ratio', 'net_profit_yoy', 'revenue_yoy', 'healthy_expansion_velocity', 'EP', 'BP', 'CP', 'market_cap_rank', 'turnover_rank', 'return_5_rank', 'EP_rank', 'pe_expansion_trend', 'value_price_divergence', 'active_market_cap', 'ebit_rank', 'turnover_rate_volatility', 'future_return_5_rank']\n", - "\n", - "前5行预览:\n", - "shape: (5, 71)\n", - "┌───────────┬────────────┬─────────┬─────────┬───┬────────────┬───────────┬────────────┬───────────┐\n", - "│ ts_code ┆ trade_date ┆ open ┆ high ┆ … ┆ active_mar ┆ ebit_rank ┆ turnover_r ┆ future_re │\n", - "│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ ket_cap ┆ --- ┆ ate_volati ┆ turn_5_ra │\n", - "│ str ┆ str ┆ f64 ┆ f64 ┆ ┆ --- ┆ f64 ┆ lity ┆ nk │\n", - "│ ┆ ┆ ┆ ┆ ┆ f64 ┆ ┆ --- ┆ --- │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ f64 ┆ f64 │\n", - "╞═══════════╪════════════╪═════════╪═════════╪═══╪════════════╪═══════════╪════════════╪═══════════╡\n", - "│ 000001.SZ ┆ 20200102 ┆ 1817.67 ┆ 1850.42 ┆ … ┆ null ┆ null ┆ null ┆ -0.008857 │\n", - "│ 000001.SZ ┆ 20200103 ┆ 1849.33 ┆ 1889.72 ┆ … ┆ null ┆ null ┆ null ┆ -0.01881 │\n", - "│ 000001.SZ ┆ 20200106 ┆ 1856.97 ┆ 1893.0 ┆ … ┆ null ┆ null ┆ null ┆ -0.008171 │\n", - "│ 000001.SZ ┆ 20200107 ┆ 1870.07 ┆ 1886.45 ┆ … ┆ null ┆ null ┆ null ┆ -0.014117 │\n", - "│ 000001.SZ ┆ 20200108 ┆ 1855.88 ┆ 1861.34 ┆ … ┆ null ┆ null ┆ null ┆ -0.017252 │\n", - "└───────────┴────────────┴─────────┴─────────┴───┴────────────┴───────────┴────────────┴───────────┘\n", - "\n", - "[4] 转换为排序学习格式\n", - "\n", - "================================================================================\n", - "准备排序学习数据(将 future_return_5_rank 转换为 20 分位数标签)\n", - "================================================================================\n", - "\n", - "原始 future_return_5_rank 统计:\n", - "shape: (9, 2)\n", - "┌────────────┬───────────┐\n", - "│ statistic ┆ value │\n", - "│ --- ┆ --- │\n", - "│ str ┆ f64 │\n", - "╞════════════╪═══════════╡\n", - "│ count ┆ 7.01659e6 │\n", - "│ null_count ┆ 28362.0 │\n", - "│ mean ┆ 0.003779 │\n", - "│ std ┆ 0.073221 │\n", - "│ min ┆ -0.969459 │\n", - "│ 25% ┆ -0.033163 │\n", - "│ 50% ┆ -0.001483 │\n", - "│ 75% ┆ 0.032547 │\n", - "│ max ┆ 10.361925 │\n", - "└────────────┴───────────┘\n", - "\n", - "转换后 future_return_5_rank_rank 统计:\n", - "shape: (9, 2)\n", - "┌────────────┬───────────┐\n", - "│ statistic ┆ value │\n", - "│ --- ┆ --- │\n", - "│ str ┆ f64 │\n", - "╞════════════╪═══════════╡\n", - "│ count ┆ 7.01659e6 │\n", - "│ null_count ┆ 28362.0 │\n", - "│ mean ┆ 9.495412 │\n", - "│ std ┆ 5.765668 │\n", - "│ min ┆ 0.0 │\n", - "│ 25% ┆ 4.0 │\n", - "│ 50% ┆ 9.0 │\n", - "│ 75% ┆ 14.0 │\n", - "│ max ┆ 19.0 │\n", - "└────────────┴───────────┘\n", - "\n", - "每日样本数统计:\n", - "shape: (9, 2)\n", - "┌────────────┬─────────────┐\n", - "│ statistic ┆ value │\n", - "│ --- ┆ --- │\n", - "│ str ┆ f64 │\n", - "╞════════════╪═════════════╡\n", - "│ count ┆ 1455.0 │\n", - "│ null_count ┆ 0.0 │\n", - "│ mean ┆ 4841.891409 │\n", - "│ std ┆ 560.948186 │\n", - "│ min ┆ 3740.0 │\n", - "│ 25% ┆ 4369.0 │\n", - "│ 50% ┆ 5060.0 │\n", - "│ 75% ┆ 5344.0 │\n", - "│ max ┆ 5458.0 │\n", - "└────────────┴─────────────┘\n", - "\n", - "分位数标签分布:\n", - "shape: (21, 2)\n", - "┌───────────────────────────┬────────┐\n", - "│ future_return_5_rank_rank ┆ count │\n", - "│ --- ┆ --- │\n", - "│ i64 ┆ u32 │\n", - "╞═══════════════════════════╪════════╡\n", - "│ null ┆ 28362 │\n", - "│ 0 ┆ 351599 │\n", - "│ 1 ┆ 350894 │\n", - "│ 2 ┆ 350944 │\n", - "│ 3 ┆ 351077 │\n", - "│ … ┆ … │\n", - "│ 15 ┆ 350910 │\n", - "│ 16 ┆ 350835 │\n", - "│ 17 ┆ 350848 │\n", - "│ 18 ┆ 350871 │\n", - "│ 19 ┆ 349137 │\n", - "└───────────────────────────┴────────┘\n", - "\n", - "[配置] 训练期: 20200101 - 20231231\n", - "[配置] 验证期: 20240101 - 20241231\n", - "[配置] 测试期: 20250101 - 20251231\n", - "[配置] 特征数: 50\n", - "[配置] 目标变量: future_return_5_rank_rank(20分位数)\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_26384\\2929956284.py:125: DeprecationWarning: `pl.count()` is deprecated. Please use `pl.len()` instead.\n", - "(Deprecated in version 0.20.5)\n", - " daily_counts = df_ranked.group_by(date_col).agg(pl.count().alias(\"count\"))\n" - ] - } - ], - "execution_count": 5 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -773,12 +532,7 @@ "source": "### 4.1 股票池筛选" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:34.925345Z", - "start_time": "2026-03-13T18:10:11.853318Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -803,46 +557,8 @@ " filtered_data = data\n", " print(\" 未配置股票池管理器,跳过筛选\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "股票池筛选\n", - "================================================================================\n", - "\n", - "[过滤] 应用 ST 过滤器...\n", - " ST 过滤后数据规模: (6823808, 72)\n", - "\n", - "执行每日独立筛选股票池...\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_26384\\4061767669.py:57: DeprecationWarning: `is_in` with a collection of the same datatype is ambiguous and deprecated.\n", - "Please use `implode` to return to previous behavior.\n", - "\n", - "See https://github.com/pola-rs/polars/issues/22149 for more information.\n", - " return df[\"ts_code\"].is_in(small_cap_codes)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 筛选前数据规模: (6823808, 72)\n", - " 筛选后数据规模: (1455000, 72)\n", - " 筛选前股票数: 5678\n", - " 筛选后股票数: 1934\n", - " 删除记录数: 5368808\n" - ] - } - ], - "execution_count": 6 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -850,12 +566,7 @@ "source": "### 4.2 数据划分" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:34.997214Z", - "start_time": "2026-03-13T18:10:34.932346Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -882,39 +593,8 @@ "else:\n", " raise ValueError(\"必须配置数据划分器\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "数据划分\n", - "================================================================================\n", - "\n", - "训练集数据规模: (970000, 72)\n", - "验证集数据规模: (242000, 72)\n", - "测试集数据规模: (243000, 72)\n", - "\n", - "训练集 group 数量: 970\n", - "验证集 group 数量: 242\n", - "测试集 group 数量: 243\n", - "训练集日均样本数: 1000.0\n", - "验证集日均样本数: 1000.0\n", - "测试集日均样本数: 1000.0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_26384\\2929956284.py:149: DeprecationWarning: `pl.count()` is deprecated. Please use `pl.len()` instead.\n", - "(Deprecated in version 0.20.5)\n", - " pl.count().alias(\"count\")\n" - ] - } - ], - "execution_count": 7 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -922,12 +602,7 @@ "source": "### 4.3 数据质量检查" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:37.417614Z", - "start_time": "2026-03-13T18:10:35.002702Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -945,52 +620,8 @@ "\n", "print(\"[成功] 数据质量检查通过,未发现异常\")\n" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "数据质量检查(必须在预处理之前)\n", - "================================================================================\n", - "\n", - "检查训练集...\n", - "\n", - "================================================================================\n", - "数据质量检查报告\n", - "================================================================================\n", - "\n", - "[严重] 发现 1657 个全空因子:\n", - " (某天的某个因子所有值都是 null,可能是数据缺失或计算错误)\n", - " - 日期 20200217: drawdown_from_high_60 (样本数: 1000)\n", - " - 日期 20200217: volatility_squeeze_5_60 (样本数: 1000)\n", - " - 日期 20200217: net_profit_yoy (样本数: 1000)\n", - " - 日期 20200217: revenue_yoy (样本数: 1000)\n", - " - 日期 20200217: healthy_expansion_velocity (样本数: 1000)\n", - " - 日期 20200217: pe_expansion_trend (样本数: 1000)\n", - " - 日期 20200217: value_price_divergence (样本数: 1000)\n", - " - 日期 20200213: drawdown_from_high_60 (样本数: 1000)\n", - " - 日期 20200213: volatility_squeeze_5_60 (样本数: 1000)\n", - " - 日期 20200213: net_profit_yoy (样本数: 1000)\n", - " ... 还有 1647 个\n", - "\n", - "--------------------------------------------------------------------------------\n", - "建议处理方式:\n", - " 1. 检查因子定义和数据源,确认计算逻辑是否正确\n", - " 2. 如果是预期内的缺失(如新股无历史数据),考虑调整因子计算窗口\n", - " 3. 如果是数据同步问题,重新同步相关数据\n", - " 4. 可以使用 filter 排除问题日期或因子\n", - "================================================================================\n", - "\n", - "检查验证集...\n", - "\n", - "检查测试集...\n", - "[成功] 数据质量检查通过,未发现异常\n" - ] - } - ], - "execution_count": 8 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -998,12 +629,7 @@ "source": "### 4.4 数据预处理" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:37.997346Z", - "start_time": "2026-03-13T18:10:37.438005Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -1030,32 +656,8 @@ "print(f\"处理后验证集形状: {val_data.shape}\")\n", "print(f\"处理后测试集形状: {test_data.shape}\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "数据预处理\n", - "================================================================================\n", - "\n", - "训练集处理...\n", - " [1/3] NullFiller\n", - " [2/3] Winsorizer\n", - " [3/3] StandardScaler\n", - "\n", - "验证集处理...\n", - "\n", - "测试集处理...\n", - "\n", - "处理后训练集形状: (970000, 72)\n", - "处理后验证集形状: (242000, 72)\n", - "处理后测试集形状: (243000, 72)\n" - ] - } - ], - "execution_count": 9 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -1063,12 +665,7 @@ "source": "### 4.4 训练 LambdaRank 模型" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:39.279997Z", - "start_time": "2026-03-13T18:10:38.028508Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -1099,48 +696,8 @@ ")\n", "print(\"训练完成!\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "训练 LambdaRank 模型\n", - "================================================================================\n", - "\n", - "训练样本数: 970000\n", - "验证样本数: 242000\n", - "特征数: 50\n", - "目标变量: future_return_5_rank_rank\n", - "\n", - "目标变量统计(训练集):\n", - "shape: (9, 2)\n", - "┌────────────┬──────────┐\n", - "│ statistic ┆ value │\n", - "│ --- ┆ --- │\n", - "│ str ┆ f64 │\n", - "╞════════════╪══════════╡\n", - "│ count ┆ 969665.0 │\n", - "│ null_count ┆ 335.0 │\n", - "│ mean ┆ 9.810091 │\n", - "│ std ┆ 5.346526 │\n", - "│ min ┆ 0.0 │\n", - "│ 25% ┆ 6.0 │\n", - "│ 50% ┆ 10.0 │\n", - "│ 75% ┆ 14.0 │\n", - "│ max ┆ 19.0 │\n", - "└────────────┴──────────┘\n", - "\n", - "开始训练...\n", - "Training until validation scores don't improve for 50 rounds\n", - "Early stopping, best iteration is:\n", - "[5]\ttrain's ndcg@10: 0.59368\tval's ndcg@10: 0.546167\n", - "训练完成!\n" - ] - } - ], - "execution_count": 10 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -1148,12 +705,7 @@ "source": "### 4.5 训练指标曲线" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:39.394848Z", - "start_time": "2026-03-13T18:10:39.285414Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -1206,57 +758,8 @@ " print(f\" {metric}: {best_val:.4f} (迭代 {best_iter_metric + 1})\")\n", " print(f\"\\n[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "训练指标曲线\n", - "================================================================================\n", - "[成功] 已从模型获取训练评估结果\n", - "\n", - "评估的 NDCG 指标: ['ndcg@10']\n", - "\n", - "[早停信息]\n", - " 配置的最大轮数: 2000\n", - " 实际训练轮数: 55\n", - " 早停状态: 已触发(最佳迭代: 5)\n", - "\n", - "最终 NDCG 指标:\n", - " ndcg@10: 训练集=0.6331, 验证集=0.5393\n", - "\n", - "[绘图] 使用 LightGBM 原生接口绘制训练曲线...\n" - ] - }, - { - "data": { - "text/plain": [ - "
" - ], - "image/png": "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CG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAckjWQ9uf//znMXTo0CgtLY2DDz44nnrqqW2ev2LFirjggguiX79+UVJSEnvvvXfceeedWzz3//2//xd5eXnxpS99qYVGDwAAAADQvAoji26++ea46KKL4pprrkkD26uvvjpOOOGEmDp1avTu3Xuz86uqquK4445LX7vllltiwIABMWvWrOjatetm506aNCl+9atfxejRo3fTpwEAAAAAaOWVtldddVWcd955ce6558bIkSPT8LZDhw5x/fXXb/H85PiyZcvin//8Zxx++OFphe7RRx8dBxxwwEbnrV69Os4888y47rrrolu3brvp0wAAAAAAtOJK26RqdvLkyfHNb36z8Vh+fn68613viscff3yL77ntttvi0EMPTdsj3HrrrdGrV68444wz4uKLL46CgoLG85LX3/Oe96TX+v73v/+2Y6msrEyXBhUVFem6uro6XYDc1/Csemah9fDcQuvk2YXWx3MLrY/ntu3a3twia6HtkiVLora2Nvr06bPR8WT/1Vdf3eJ7pk+fHg888EBaRZv0sZ02bVp87nOfSz/spZdemp7z5z//OZ555pm0PcL2+uEPfxiXX375ZscffPDBtPIXaD3uvffebA8B2EGeW2idPLvQ+nhuofXx3LY9a9euzf2etjuqrq4u7Wd77bXXppW148aNi7lz58aVV16ZhrZz5syJL37xi+kf6GRis+2VVPsmvXWbVtoOGjQojj322OjRo0cLfRqgOSX/8SZ59pO+10VFRW4utAKeW2idPLvQ+nhuofXx3LZdDb/hn7Ohbc+ePdPgdeHChRsdT/b79u27xff069cvDWOatkIYMWJELFiwoLHdwqJFi2Ls2LGNryfVvBMnToyf/exnaQuEpu9tUFJSki6bSn6W8AdaF88ttD6eW2idPLvQ+nhuofXx3LY925s1Zm0isuLi4rRS9v7779+okjbZT/rWbkky+VjSEiE5r8Frr72WhrnJ9d75znfGiy++GM8991zjMn78+LSdQrK9pcAWAAAAACCXZLU9QtKS4Oyzz06D1YMOOiiuvvrqWLNmTZx77rnp62eddVYMGDAg7Tmb+OxnP5tWzCYtED7/+c/H66+/HldccUV84QtfSF/v1KlTjBo1aqOfUV5enrY42PQ4AAAAAEAuympoe9ppp8XixYvjkksu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18lvpvt79e4YBwzq2hjSjmimfrK7Ivm1+nfsmyz1rQiaBrkN1bhJi4Xk2L59O6dVtIcM7572k02qeXNB0oN1n76d0uXjhwxJg/KkFUFDJW4S5i6qqIyaurqorctEde0mfRw2SDL1qpq6qNrJcSQtGpJJwxpaQowe2CVtXQAAALRdQlsAaEUWVqyP838/OW2JkFRifuukEfHJI4a1ir6hmwa51dXV8a/b74yT33toFBW1TBVwc0ru8aDuHdLlg+PqJzdrKgl1k4A2CXFrajNpBXQS5tbU1jVuJy0Y0mPp8cxGgW/98Y3fO6BbWauYDA0AAGheQlsAaCWenb08DWwXrapMWx387IwD48i9ekVr1payyCTULchLPlNB6FYAAADsiqz/VennP/95DB06NEpLS+Pggw+Op556apvnr1ixIi644ILo169flJSUxN577x133nln4+s//OEPY8KECdGpU6fo3bt3vP/974+pU6fuhk8CAC3nL0/PidN+9UQa2O7dp2PcduHhrT6wBQAAIAdD25tvvjkuuuiiuPTSS+OZZ56JAw44IE444YRYtGjRFs+vqqqK4447LmbOnBm33HJLGsZed911MWDAgMZzHn744TTUfeKJJ+Lee+9Nf/Xy+OOPjzVr1uzGTwYAzSP5dfrLbns5vn7LC1FVWxfHj+wTf//c4TGkR7lbDAAA0EZltT3CVVddFeedd16ce+656f4111wTd9xxR1x//fXxjW98Y7Pzk+PLli2Lxx57rLH3XVKl29Tdd9+90f4NN9yQVtxOnjw5jjrqqC2Oo7KyMl0aVFRUpOsk8E0WIPc1PKueWdqSZWuq4os3Px9PzFie7n/h2D3igmOGR35+pk38WffcQuvk2YXWx3MLrY/ntu3a3r/L5WWSWTOyIKma7dChQ1oxm7QwaHD22WenLRBuvfXWzd5z0kknRffu3dP3Ja/36tUrzjjjjLj44oujoGDLsyhPmzYt9tprr3jxxRdj1KhRWzznsssui8svv3yz4zfddFP6swBgd5u7JuLXUwtiWWVelORn4mN71cXo7ln5n2wAAACaydq1a9M8c+XKldG5c+fcq7RdsmRJ1NbWRp8+9TNIN0j2X3311S2+Z/r06fHAAw/EmWeemfaxTQLZz33uc2lCnbRY2FRdXV186UtfisMPP3yrgW3im9/8ZtqmoWml7aBBg+LYY4+NHj167NLnBHaP5J8DSUuUpIVKa5iFHrblrpcWxE///lKsq66Lwd3L4pozDoy9+nRsczfNcwutk2cXWh/PLbQ+ntu2q+E3/HO6PcKOSkLYpNXBtddem1bWjhs3LubOnRtXXnnlFkPbpLftSy+9FI888sg2r5tMaJYsm0qCH+EPtC6eW1qzurpMXHXva/GzB6el+0fu1TN+evqB0bVDcbRlnltonTy70Pp4bqH18dy2PdubNWYttO3Zs2cavC5cuHCj48l+3759t/iefv36pR+saSuEESNGxIIFC9J2C8XFb/2l9sILL4zbb789Jk6cGAMHDmzBTwIAu27V+ur48s3PxX1T6ifjPO/IYXHxiftGYUFW5wwFAAAgC7L2N8EkYE0qZe+///6NKmmT/UMPPXSL70naHCQtEZLzGrz22mtpmNsQ2CYtepPA9h//+EfaSmHYsGG74dMAwM6bvnh1nPqLx9LAtrgwP35y2gHx7feMFNgCAAC0U1kt30n6yF533XXxu9/9LqZMmRKf/exnY82aNXHuueemr5911llpv9kGyevLli2LL37xi2lYe8cdd8QVV1yRtkFokGz/4Q9/SCcR69SpU1qFmyzr1q3LymcEgG15aOqiOOXnj8a0Raujb+fSuOUzh8apB/oNEQAAgPYsqz1tTzvttFi8eHFccsklabA6ZsyYuPvuuxsnJ5s9e3bk57+VKyeTg91zzz3x5S9/OUaPHh0DBgxIA9yLL7648Zxf/vKX6fqYY47Z6Gf99re/jXPOOWe3fTYA2JbkN0N+NXF6/OjuVyOTiRg3pFv88mNjo3enUjcOAACgncv6RGRJK4Nk2ZKHHnpos2NJ64Qnnnhim38JBoBctq6qNi7+2wtx2/Pz0v3TDxoUl71vvygpfKtnOwAAAO1X1kNbAGhP5q5YF5++8el4eV5FFObnxaXv2y8+dvDgyMvLy/bQAAAAyBFCWwDaveS3NHZHaPrk9KXxuT8+E0vXVEWP8uL4xZlj4+DhPdr9/QcAAGBjQlsA2q1Fq9bHl/78XEyauSy6lxdHz44l0atTSbp+a7s4XffasN+lrGiHA94kFP7Dk7Pj8ttejpq6TOzXv3Nce9b4GNC1rMU+GwAAAK2X0BaAdunVBRXxyRueTtsVJBZWVKbL2ykqyNtiqLulwLdzaWFU12bi0tteij89NSd9/8kH9I///uDoKCvWvxYAAIAtE9oC0O48OHVRfP6mZ2N1ZU0M71kePzltTOTn5cXi1etjyaqqWLy6MhavqkzXS1ZVxpIN+xXra9IQdv7K9enydooL86OsqCBWrquOpDj34hP3jfOPGq5/LQAAANsktAWgXbnx8Zlx2W0vR10m4pDh3eOaj42Lrh2KN7zaZZvvraypjSWrq9Igd3GTMDdZJ8ebHltVWRNVNXXp0qm0MP7v9APj2H1675bPCAAAQOsmtAWgXaity8T373glfvvozHT/Q+MGxhWn7p9Ww26vksKCtA/t9vSiXV9dm4a3yaRjw3qWp71wAQAAYHsIbQFo89ZU1sQX/vRs3P/qonT/ayfsE587Zo8WbVNQWlQQg7p3SBcAAADYEUJbANq0+SvXpROOvTK/IkoK8+Oqj4yJ94zul+1hAQAAwFYJbQFos158c2V88neTYtGqyujZsTiuPWt8jB3cLdvDAgAAgG0S2gLQJv375QXxxT8/F+uqa2PvPh3jN2dP0KoAAACAVkFoC0Cbkslk4jePzIgf3DklMpmII/fqGT8/c2x0LjURGAAAAK2D0BaANqO6ti4uve3luOnJ2en+mQcPjsvft18UFuRne2gAAACw3YS2ALQJFeur44I/PhP/eX1J5OVFfPukEfHJI4ZFXrIDAAAArYjQFoBWb86ytfGJGybF64tWR1lRQfzf6QfGcSP7ZHtYAAAAsFOEtgC0as/MXh6fvvHpWLK6Kvp0LkknHBs1oEu2hwUAAAA7TWgLQKt1+wvz4it/eT4qa+piZL/O8Ztzxke/LmXZHhYAAADsEqEtAK1OJpOJXzz0Rlx5z9R0/10jesf/fvTAKC/xP2sAAAC0fv52C0CrUlVTF9/8+4vxt2feTPc/cfiw+PZ7RkRBvgnHAAAAaBuEtgC0GivWVsX5v58cT85Yloa0l71vv/j4IUOyPSwAAABoVkJbAFqFGUvWxCdumJSuO5YUxs/OODCO2ad3tocFAAAAzU5oC0DOe3L60jj/D5NjxdrqGNC1LJ1wbN++nbM9LAAAAGgRQlsAtltdXSaWrKmM+SvWx7wV62LuinUxb8X6WLRqfWQyEZnIpOcl2xutNz2+4XoN+7GF15PJxhq2H522JKprM3HAoK5x3VnjonenUt8aAAAAbZbQFoBGa6tqNoSx62N+GsjWbyfreSvXpWFtVW1dVu7YSfv3jf/58JgoKy7wjQEAANCmCW0Bsmh9dW3c8/KC+Pszc2PluuooLcqPsqKCNJgsTdYNS9P9dLv+vKbHGvebbCeTdTWorcukFbHbCmWT9gNvJy8vok+n0ujfNVnK0nYFvTuXRlFB/c/Ka3pik/0Nu5G34chb+1t+vemqZ6eSOHqvXpHf5PMAAABAWyW0BciCaYtWx5+fmh1/e+bNWL4dQenOKi7ITwPe4sKCWL62Kg1u306nksI0jG0IZRuC2WTdr0tp9O2SBLT5LTZmAAAAaO+EtgC7SWVNbdz90oK46cnZ8eSMZY3HkyD0tAmDYmS/zrG+pi7WV9XGuuoNS1VtWo2bLPXH6hqPbfn1ZPut9gVJK4P6dgY19f/Qz89LQ9c0jO2yhVC2a2l0Li3yZwIAAACySGgL0MLeWLw6/vTkxlW1yW/5v2Pf3nH6QYPjmH16b9TGoDkmC6usqdso+E0C4x7lJdGrU0mz/iwAAACg+QltAbJQVfuR8YPSytaWkPR9TfvamrALAAAAWiWhLUAzV9UmvWpvmbxxVe2x+/SOMw4eHEfv3SsK9YMFAAAAtkFoC9BMVbV/emp2PDH9raravp3rq2qTpaWqagEAAIC2R2gLsJOmJ71qt1JVW9+rVlUtAAAAsOOEtgA7QFUtAAAA0NKEtgDbYc6ytfH7J2alVbXL1lSlx1TVAgAAAC1BaAvwNp6ZvTw+/usnY01V7Ua9aj8yYVAM0KsWAAAAaGZCW4BteGnuyjj7+qfSwPaAQV3j88fuqVctAAAA0KKEtgBb8drCVXHW9U/FqvU1MWFot/jdJw6KDsX+sQkAAAC0rPwWvj5AqzRjyZo489dPpv1rRw/sEtefM0FgCwAAAOwWQluALUw6duZ1T8TiVZWxb99OceMnDopOpUXuEwAAALBbCG0Bmliwcn1aYTtv5frYo1d5/OFTB0fXDsXuEQAAALDbCG0BNliyujLO/PUTMXvZ2hjcvUP88VOHRM+OJe4PAAAAsFsJbQEiYsXaqvjYr5+MNxaviX5dSuOPnzo4+nYpdW8AAACA3U5oC7R7q9ZXx9nXPxWvLlgVvTqVxE3nHRKDundo9/cFAAAAyI7CLP1coB15c/nauP6RmTF51rL41JHD472j+0VeXl7kgrVVNfGJGybF82+ujG4ditIK22E9y7M9LAAAAKAdE9oCLeaVeRVx7cQ34l8vzI/aukx67PN/ejZufW5efP/9o7LefmB9dW2cd+PTMWnm8uhUWhi//+TBsXefTlkdEwAAAIDQFmhWmUwmHntjaVzz8Bvxn9eXNB4/fM8eMbJf57jhsZlx35SF8eT0pfHNk0bERycMivz83V91W1VTF5/74zPx6LSlUV5cEL/7xEExakCX3T4OAAAAgE0JbYFmUVNbF3e+tCB+9fAb8fK8ivRYksW+Z3T/OP+o4Y2B6IfHD4qv3/JCPDdnRXzrHy/Gbc/Pjf/3gdExdDe2JEjG+sU/PxsPvLooSgrz4zfnTIixg7vttp8PAAAAsC1CW2CXe8L+ZdKc+PUjM+LN5evSY6VF+fHRCYPjk0cM22xCr6T9wN8+e1hacfvje6bGE9OXxQlXT4yLjts7Pb+woGXnR6yry8TXbnkh7nppQRQX5Me1Z42PQ4b3aNGfCQAAALAjhLbATlm6ujJ+9/isuPHxmbFibXV6rHt5cZx96ND4+KFD0u2tKcjPSwPa40f2iW/+/cV4ZNqS+OFdr8btL8yPH31wdIzs37nFWjd8+58vxj+enZuO4WdnHBhH792rRX4WAAAAwM4S2gI7ZOaSNfHrR6bHX59+Mypr6tJjg7t3iPOOGh4fGjswyooLtvtaSRXu7z95UNwy+c343u2vxItzV8b7fvZInH/08Pj8O/aK0qLtv9b2BLbfvf2V+NNTc9K2DVefNiaO369vs10fAAAAoLkIbYHt8vycFXHtxOlx10vzoy5Tf2z0wC7xmaP3iBP265tWru6MvLy8tM/t0fv0iktvfTltW/DzB99I10nV7YSh3ZvlG/rxv6fGbx+dmW4n1z35gP7Ncl0AAACA5ia0BbZZnfrQa4vTycWS3rMNjtmnV5x/1B5xyPDuaejaHHp3Ko1ffmxc3P3S/PjOrS/H9MVr4sPXPB5nHTokvn7ivtGxZOf/cfWzB15Pg+DE907ZLw2JAQAAAHKV0BbYTFVNXfzr+XlpZe3Uhavq/2GRnxfvG9M/Pn3U8Ni3b8v0nE2cOKpfHDq8Z1xx55S4+ek5cePjs+K+VxbGDz6wfxy7T+8dvt6v/zM9fvzv19Ltb580Ij5+6NAWGDUAAABA8xHaAo1Wra+OPz81J65/dEbMX7k+PVZeXBBnHDw4zj18WPTvWrZb7laXDkXxow+NTkPiZKKy2cvWxrm/nRSnHjggvvPekduc5KypPzwxK75/x5R0+8vv2jvtuwsAAACQ64S2QOrBqYvii396NirW16T7vTqVxLmHD40zDx4SXcqKsnKXDt+zZ9z9pSPjqn+/lgbJ/3h2bkx8bXFc+r794uTR/bbZmiGZ3Oy//vlSup303f3CO/fcjSMHAAAA2HlCWyAmzVwWn/n95KisqYvhvcrj/KOGx/sPHBAlhQVZvzsdigvjv947Mt57QP+4+JYX0nYNX/jTs3Hrs3Pj+6eOin5dNq/+vf2FefH1W55Pt885bGhcfOI+zdZ7FwAAAKCl5bf4TwBy2pT5FfGJGyalge2x+/SKe750VJw2YXBOBLZNjRnUNf71+SPiouP2jqKCvLj/1UVx3FUT449Pzoq6ukzjecnxL/35uUgOfXTCoLjkvSMFtgAAAECrIrSFdmz20rVx1vVPxar1NTF+SLf4xZnjoqggd/+xUFyYH194515xxxeOjAMHd43VlTXx7X+8FKdf90TMXLomXl2RF5//8/NRU5eJU8b0jx+cun/k56uwBQAAAFqX3E1ngBa1aNX6+Pj1T8biVZWxb99O8ZuzJ0RZcW5V127N3n06xS2fOSwuPXlklBUVxJMzlsV7f/Z4/HpqflTXZuLE/frG/3z4gCgQ2AIAAACtkNAW2qGV66rj7Osnxayla2Ngt7L43ScOii4dsjPZ2M5KAtlzDx8W//7yUXHkXj3T9g7VdXlx9N494/9OPzAKc7hiGAAAAGBbTEQG7cz66to473dPp71se3Ysjj988uDo07k0WqtB3TvEjZ84KP75zJy45/Hn4sqPHpC2UQAAAABorYS20I7U1NbFhTc9G0/NXBadSgrjhnMPiqE9y6O1y8vLi/eO7hf5bz4bpUWto8UDAAAAwNYoR4N2IpPJxDf+/mLcN2VhWol63dnjY9SALtkeFgAAAACbUGkLzejBqYvS9TF790qrP3PJD+96NW6Z/GYkc3P97PQD45DhPbI9JAAAAAC2QGgLzeSNxavjEzdMikwm4qT9+8YP3r9/dCsvzon7e83Db8S1E6en2//vg6Pj+P36ZntIAAAAAGyF9gjQTO56cX4a2CbufHFBnHD1xHhoQ+VtNt08aXb8v7teTbe/ddK+8ZHxg7I9JAAAAAC2QWgLzeTulxek63MOGxp79CqPRasq45zfTorv/POlWFtVk5X7fPdLC+Kbf38x3T7/6OHx6aP2yMo4AAAAANh+QltoBnOWrY2X5lak/WI//449444vHJmGt4nfPzEr3vN/j8Szs5fv1nv9+BtL4wt/fjbqMhEfGT8wvnHivrv15wMAAACwc4S20Azu2VBle9Cw7tGjY0mUFhXEZe/bL37/yYOib+fSmLFkTXzomsfjJ/e+FtW1dS1+z1+auzLOu/HpqKqpi+NH9okrTt0/5yZGAwAAAGDLhLbQTG0IEiduMsHXkXv1inu+dFS874D+UVuXif+9//X40C8fSyctaynTF6+Os69/KlZX1sTBw7rH/51+YBQWeNQBAAAAWgtJDuyiRRXrY/KG1gcnjNo4tE106VCUBqfJ0rm0MJ5/c2W85//+Ezc+PjMyDTOXNZMFK9fHx3/zVCxdUxX79e8cvz57fFr1CwAAAEDrIbSFXXTPKwsjyV7HDOoa/bqUbfW8pNr2ni8fFUfs2TPWV9fFJbe+HGf/dlIsrFjfLN/BirVVcdb1T8bcFetiaI8OccO5B0Wn0qJmuTYAAAAAu4/QFnbRPQ2tEbZQZbupJNS98RMHxWUnj4ySwvyY+NriOP4nE+P2F+bt0hjWVtXEJ26YFK8tXB29O5XE7z95cPTqVLJL1wQAAAAgO4S2sAuWr6mKx6cv3WI/260+dPl5cc7hw+KOLxwR+w/oEivXVceFNz0bX/rzs+n2jkomNvvcH5+JZ2avSNsvJIHtoO4ddvg6AAAAAOQGoS3sgvumLEwnGNu3b6cY2rN8h967Z+9O8ffPHRZfeMeekZ8X8c/n5sWJV0+Mx6Yt2e5r1NVl4qt/fT4emro4Sovy47fnToh9+nbaiU8CAAAAQK4Q2sIuuOfl7W+NsCVFBflx0fH7xC2fPSztQzt/5fo449dPxnf/9Uqsr67d5nuTScy+e/srcetz86IwPy9++bFxMW5I950aBwAAAAC5Q2gLO2l1ZU1MfL2+Kvbdo/rt0n0cO7hb3PGFI+OMgwen+9c/OiNO/ukj8dLclVt9z08fmBY3PDYz3f7xhw+IY/fpvUtjAAAAACA3CG1hJz346qKoqqmLYT3LY+8+HXf5PpaXFMYVp+4f158zPnp2LInXF62O9//80fj5g9OiprZuo3N//8SsuOre19LtS08eGe8/cIDvEQAAAKCNENrCTrp7Q2uEE/brG3l5ec12H9+xb5/495ePSic2q6nLxJX3TI3Trn0iZi1dk75++wvz4pJbX0q3k3645x4+rNl+NgAAAADZJ7SFnZD0m00qbRPv3sl+ttvSvbw4fvmxsfE/Hz4gOpYUxuRZy+Pd//uf+OGdU+LLNz8XmUzEmQcPji8ft3ez/2wAAAAAsktoCzvhP68vibVVtdGvS2mMHtilRe5hUr37wXED4+4vHRkHDeue/rxfTZwe1bWZeM/ofvHdU0Y1a4UvAAAAALlBaAs74a6X5rdIa4QtGditQ/zpvEPiWyftG6VF+fHOfXvHVR85IAryBbYAAAAAbVFhtgcArU11bV3c98rCFmuNsCVJQPvpo/aIcw4bFkUFeSpsAQAAANowoS3soCemL42K9TXRo7w4xg/tvlvvX3Gh4ngAAACAtk4CBDvorpcWpOvj9+ujRQEAAAAAzU5oCzugti4T/365vjXCiaP6uXcAAAAANDuhLeyAZ2YvjyWrK6NTaWEcOryHewcAAABAsxPawg6468X61gjHjeijvywAAAAALUJoC9spk8nEPS/Xh7YnjOrrvgEAAADQIoS2sJ1emlsRc1esi7Kigjhqr17uGwAAAAAtQmgL2+mul+an62P37RVlxQXuGwAAAAAtQmgL29ka4e6XNrRG2E9rBAAAAABajtAWtsO0Ratj+pI1UVyQH+/Yt7d7BgAAAECLEdrCdrhrQ5XtEXv1jE6lRe4ZAAAAAC1GaAvboaE1wolaIwAAAADQwoS28DZmL10br8yviIL8vHjXyD7uFwAAAAAtSmgLb+Pul+en64OHdY/u5cXuFwAAAAAtSmgL29saYVRf9woAAACAFie0hW1YsHJ9PDN7Rbp9/EihLQAAAAAtT2gL2/DvV+qrbMcO7hp9u5S6VwAAAAC0OKEtbIPWCAAAAADsbkJb2Ipla6riyRnL0u0T9+vnPgEAAACwWwhtYSvue2Vh1NZlYmS/zjG4Rwf3CQAAAIDdQmgLW3H3y/X9bE8cZQIyAAAAAHYfoS1swar11fHI60vS7XcLbQEAAABoT6Htz3/+8xg6dGiUlpbGwQcfHE899dQ2z1+xYkVccMEF0a9fvygpKYm999477rzzzl26JmzqgVcXRVVtXQzvVR579u7oBgEAAADQPkLbm2++OS666KK49NJL45lnnokDDjggTjjhhFi0aNEWz6+qqorjjjsuZs6cGbfccktMnTo1rrvuuhgwYMBOXxO25J6G1gj79Y28vDw3CQAAAIDdpjCy6Kqrrorzzjsvzj333HT/mmuuiTvuuCOuv/76+MY3vrHZ+cnxZcuWxWOPPRZFRUXpsaSidleumaisrEyXBhUVFem6uro6XWhf1lXVxoOv1of8x+3by5+BVqLhWfXMQuvhuYXWybMLrY/nFlofz23btb25RV4mk8lEFiRVsx06dEgrZt///vc3Hj/77LPTFgi33nrrZu856aSTonv37un7ktd79eoVZ5xxRlx88cVRUFCwU9dMXHbZZXH55Zdvdvymm25Kr0f78sKyvPjN1ILoVpyJS8fWhkJbAAAAAJrD2rVr0zxz5cqV0blz59yrtF2yZEnU1tZGnz59Njqe7L/66qtbfM/06dPjgQceiDPPPDPtYztt2rT43Oc+lybUSTuEnblm4pvf/GbaUqFppe2gQYPi2GOPjR49euzyZ6V1eeCWFyNifpwybki856R9sz0ctlPyz4F77703baHSUIkP5DbPLbROnl1ofTy30Pp4btuuht/wz+n2CDuqrq4uevfuHddee21aWTtu3LiYO3duXHnllWlou7OSCc2SZVNJ8CP8aV+qaurigamL0+33HDDA998KeW6h9fHcQuvk2YXWx3MLrY/ntu3Z3qwxa6Ftz5490+B14cKFGx1P9vv27bvF9/Tr1y/9YMn7GowYMSIWLFiQtkbYmWtCU49PXxqr1tdEz44lMXZwNzcHAAAAgN0uP7KkuLg4rZS9//77N6qkTfYPPfTQLb7n8MMPT1siJOc1eO2119IwN7nezlwTmrr7pfnp+vj9+kRBfp6bAwAAAED7CW0TSR/Z6667Ln73u9/FlClT4rOf/WysWbMmzj333PT1s846K+032yB5fdmyZfHFL34xDWvvuOOOuOKKK+KCCy7Y7mvC1tTWZeLfL9dXab97lMpsAAAAALIjqz1tTzvttFi8eHFccsklaYuDMWPGxN133904kdjs2bMjP/+tXDmZHOyee+6JL3/5yzF69OgYMGBAGuBefPHF231N2JqnZy6LpWuqoktZURwy3AR0AAAAAGRH1iciu/DCC9NlSx566KHNjiVtDp544omdviZszV0vLUjX7xrRJ4oKslqEDgAAAEA7JpmCiMhkMnHPy/Wh7YlaIwAAAACQRUJbiIgX3lwZ81eujw7FBXHkXj3dEwAAAACyRmgLTVojHLtv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dthhccopp0RxsYAw2158c2VaaVuQnxefOnJYtocDAAAAALRUpe20adNixIgRcfbZZ8ezzz4bdXV16ZJsn3XWWbHffvul55Bd10ys72X7vgP6x8BuHXwdAAAAANBWK20/+9nPxv7775+GtJ07d97otYqKijS4veCCC+Kee+5p7nGynWYvXRt3vTg/3f70UcPdNwAAAABoy6Hto48+Gk899dRmgW0iOfa9730vDj744OYcHzvorpfmR10m4og9e8aIfpt/TwAAAABAG2qP0LVr15g5c+ZWX09eS84heybNXJ6uj967l68BAAAAANp6pe2nPvWptAXCd77znXjnO98Zffr0SY8vXLgw7r///vj+978fn//851tqrLyNTCYTz8yuD23HDe3mfgEAAABAWw9tv/vd70Z5eXlceeWV8ZWvfCXy8vIaw8K+ffvGxRdfHF//+tdbaqy8jelL1sSyNVVRUpgfo/p3cb8AAAAAoK2HtokkmE2WGTNmxIIFC9JjSWA7bNiwlhgfO2DyhtYIBwzqGsWFO9T5AgAAAADIETud7CUh7aGHHpouuxLY/vznP4+hQ4dGaWlpOolZMtHZ1txwww1pdW/TJXlfU6tXr44LL7wwBg4cGGVlZTFy5Mi45pproj2YNHNZuh4/RGsEAAAAAGitmrUcc86cOfGJT3xiu8+/+eab46KLLopLL700nnnmmTjggAPihBNOiEWLFm31PZ07d4758+c3LrNmzdro9eR6d999d/zhD3+IKVOmxJe+9KU0xL3tttuirZs8q77Sdrx+tgAAAADQajVraLts2bL43e9+t93nX3XVVXHeeefFueee21gR26FDh7j++uu3+p6kujZpx9CwNEyG1uCxxx6Ls88+O4455pi0gvfTn/50GgZvq4K3LVi6ujLtaZsYO1ilLQAAAAC0i562b1etOn369O2+VlVVVUyePDm++c1vNh7Lz8+Pd73rXfH4449v9X1J+4MhQ4ZEXV1djB07Nq644orYb7/9Gl8/7LDD0nEmFb/9+/ePhx56KF577bX4yU9+stVrVlZWpkuDioqKdF1dXZ0urcFT05ek6716l0d5UV6rGTc0l4Y/8/7sQ+vhuYXWybMLrY/nFlofz23btb25RV4mk8ls70WTUDWpdN3WW5LXa2tr3/Za8+bNiwEDBqSVsUlf3AZf//rX4+GHH44nn3xys/ckYe7rr78eo0ePjpUrV8aPf/zjmDhxYrz88stpD9tEEr4m1bU33nhjFBYWpmO+7rrr4qyzztrqWC677LK4/PLLNzt+0003pZW/rcGtM/Pjgfn5cVjvujhtj7psDwcAAAAA2MTatWvjjDPOSLPNpA1ss1Ta9uvXL37xi1/EKaecssXXn3vuuRg3bly0lIaJz5pW1Y4YMSJ+9atfxfe+97302E9/+tN44okn0mrbpCI3CXUvuOCCtOo2qeLdkqTaN+mF27TSdtCgQXHsscdGjx49ojW44dok5F4Z7z9idJx0YP9sDwey8l+q7r333jjuuOOiqKjINwCtgOcWWifPLrQ+nltofTy3bVfDb/i/nR0KbZNANmlpsLXQ9u2qcJvq2bNnFBQUxMKFCzc6nuwnvWq3RxLMHHjggTFt2rR0f926dfGtb30r/vGPf8R73vOe9FhSlZuEyUlV7tZC25KSknTZ0vVbQ/izvro2XppX/4UfskevVjFmaCmt5bkF3uK5hdbJswutj+cWWh/PbduzvZnFDk1E9rWvfS2tbt2aPffcMx588MHtulZxcXEaAt9///2Nx5I+tcl+02rabUnaMLz44otpBXDTHrRJS4SmknA4uXZb9cKbK6O6NhO9OpXEoO5l2R4OAAAAALALdqjS9sgjj9zm6+Xl5XH00Udv9/WSlgRnn312jB8/Pg466KC4+uqrY82aNXHuueemryd9aJO+tz/84Q/T/e9+97txyCGHpOHwihUr4sorr4xZs2bFpz71qfT1pA9E8vOTcLmsrCxtj5D0x03621511VXRVj09a1m6Hj+kW1rtDAAAAAC0k9C2uZ122mmxePHiuOSSS2LBggUxZsyYuPvuu6NPnz7p67Nnz96oanb58uVx3nnnped269YtrdRNJjIbOXJk4zl//vOf0x61Z555ZixbtiwNbn/wgx/EZz7zmWirJs9cnq7HDemW7aEAAAAAANkIbWfOnBmXXXZZ3HPPPWnFa1L5+vWvfz0+/vGP7/C1LrzwwnTZkoceemij/Z/85Cfpsi1JP9zf/va30V7U1WVi8uz60HbC0O7ZHg4AAAAAsIt2qKdt4vHHH09bFAwePDgeffTRtJr1l7/8Zdqq4De/+c2ujocd9Mbi1bFibXWUFRXEyP6d3T8AAAAAaE+hbRLQfuADH4jrr78+7S87fPjwtHfsEUcckbYlSI4lPvrRj8aiRYtaasw08fSs+irbAwZ1iaKCHc7gAQAAAIDW3B7hpz/9aRx77LFx0kknxahRo2Lt2rUbvf7mm2+mPWqTnrRJgPuzn/2sucfLJp7e0M92/BCtEQAAAACgLdih0szbb789zjjjjHT7K1/5SpSWlsb3v//9tM/ssGHD4hvf+Eb06NEj7VF78803t9SYaeLpWcvS9fihJiEDAAAAgHZXaTtr1qy0JUJD1W3Sy/boo49O94866qi0z+13vvOd2GuvvWLlypWxYMGCdGIwWsbiVZUxa+nayMuLGDtEaAsAAAAA7a7SNulfm/S1TSQ9a/Pz33p7Xl5e2i5hzZo1UVtbG3V1dVFYuEOZMDto8oYq2336dIrOpUXuHwAAAAC0t9D2gAMOiMmTJ6fbp556anz6059O2yD861//ig9+8INx2GGHpe0RnnnmmejZs2e6sBv62WqNAAAAAADtM7Q988wz08nFkkra//mf/0n721511VVxySWXxMiRI+Of//xnY+uEj370oy01ZjaYNMskZAAAAADQ1uxQ/4KPfOQjaR/bz372s/GrX/0q7V+bLE395je/ifvvvz+ef/755h4rTayrqo2X565Mt8fpZwsAAAAA7bPSNulb+7e//S1efvnldOKxu+66K1asWBGVlZXx9NNPxznnnBOXX3553HHHHVojtLDn31wRNXWZ6NO5JAZ2K2vpHwcAAAAA7CY7PFNY0rN24sSJ8etf/zp+8IMfxIsvvpi2S9hzzz3j/e9/f7zwwgvRtWvXlhktjSY3tEYY2j0N0wEAAACAdhraJgoKCuL8889PF7Jj0sxl6Xq81ggAAAAA0H7bI5Ab6uoy8YxJyAAAAACgTdqpStsDDzxwi7+SnxwrLS1NWyUk/W2PPfbY5hgjm3h90eqoWF8THYoLYkS/Tu4PAAAAALT3StsTTzwxpk+fHuXl5WkwmywdO3aMN954IyZMmBDz58+Pd73rXXHrrbc2/4hpbI1w4OCuUVigWBoAAAAAor1X2i5ZsiS+8pWvxHe+852Njn//+9+PWbNmxb///e+49NJL43vf+16ccsopzTVWNpmEbNyQ7u4JAAAAALQxO1Wm+Ze//CVOP/30zY5/9KMfTV9LJK9PnTp110fIZp6eZRIyAAAAAGirdiq0TfrWPvbYY5sdT44lryXq6uoat2k+CyvWx5xl6yI/r749AgAAAADQtuxUe4TPf/7z8ZnPfCYmT56c9rBNTJo0KX7961/Ht771rXT/nnvuiTFjxjTvaImnZ9a3Rti3b+foVFrkjgAAAABAG7NToe1//dd/xbBhw+JnP/tZ/P73v0+P7bPPPnHdddfFGWecke4noe5nP/vZ5h0tb7VGGNrN3QAAAACANminQtvEmWeemS5bU1ZWtrOXZrsmIRPaAgAAAEBbtFM9bZNWCE8++eRmx5NjTz/9dHOMiy1YW1UTL8+rSLcnDO3uHgEAAABAG7RToe0FF1wQc+bM2ez43Llz09doGc/NXhG1dZno36U0+ndVyQwAAAAAbdFOhbavvPJKjB07drPjBx54YPoaLePphtYIqmwBAAAAoM3aqdC2pKQkFi5cuNnx+fPnR2HhTrfJZTtD2wkmIQMAAACANmunQtvjjz8+vvnNb8bKlSsbj61YsSK+9a1vxXHHHdec42ODpC3CMyYhAwAAAIA2b6fKYn/84x/HUUcdFUOGDElbIiSee+656NOnT/z+979v7jESEVMXrIrVlTXRsaQw9u3b2T0BAAAAgDZqp0LbAQMGxAsvvBB//OMf4/nnn4+ysrI499xz4/TTT4+ioqLmHyUxeday9C4cOLhrFOTnuSMAAAAA0EbtdAPa8vLy+PSnP928o+Ft+9mOH9LdXQIAAACANmy7Q9vbbrttuy/6vve9b2fHw1Y8PXNDaGsSMgAAAABo07Y7tH3/+9+/0X5eXl5kMpmN9hvU1tY21/iIiPkr18XcFevStghjBnV1TwAAAACgDcvf3hPr6uoal3//+98xZsyYuOuuu2LFihXpcuedd8bYsWPj7rvvbtkRt+Mq2xH9OkV5yU53tAAAAAAAWoGdSgC/9KUvxTXXXBNHHHFE47ETTjghOnTokPa5nTJlSnOOsd17emb9JGT62QIAAABA27fdlbZNvfHGG9G16+a/pt+lS5eYOXNmc4yLLU1Cpp8tAAAAALR5OxXaTpgwIS666KJYuHBh47Fk+2tf+1ocdNBBzTm+dm91ZU1MmV+R3geVtgAAAADQ9u1UaHv99dfH/PnzY/DgwbHnnnumy6BBg2Lu3Lnx61//uvlH2Y49N3tF1GUiBnYri75dSrM9HAAAAAAgF3vaJiHtCy+8EPfdd19j/9oRI0bEu971rsjLy2vuMbZrkxr72XbL9lAAAAAAgFwNbRMPPPBAPPjgg7Fo0aKoq6uL5557Lv70pz81VuLSPCZv6Gc7bmh3txQAAAAA2oGdCm0vv/zy+O53vxvjx4+Pfv36qa5tITW1dfHs7A2TkKm0BQAAAIB2YadC22uuuSZuuOGG+PjHP978I6LRqwtWxZqq2uhUWhh79+nkzgAAAABAO7BTE5FVVVXFYYcd1vyjYSNPb+hnO3ZwtyjI1ysYAAAAANqDnQptP/WpT8VNN93U/KNhI09v6GerNQIAAAAAtB871R5h/fr1ce2118Z9990Xo0ePjqKioo1ev+qqq5prfO1WJpOJp2c2TELWLdvDAQAAAAByObR94YUXYsyYMen2Sy+9tNFreXl+jb85zF2xLhZUrI/C/LwYM6hrs1wTAAAAAGijoe2DDz7Y/CNhI5M3tEbYr3/n6FC8U18TAAAAANBeetrS8hpbIwzp7nYDAAAAQDsitM3xScgm6GcLAAAAAO2K0DYHVayvjlcXVKTbJiEDAAAAgPZFaJuDnp29IjKZiMHdO0TvTqXZHg4AAAAAsBsJbXPQ5JnL0vX4Id2yPRQAAAAAYDcT2uagSRsmIRs/1CRkAAAAANDeCG1zTHVtXTw3Z0W6Pd4kZAAAAADQ7ghtc8yU+RWxrro2OpcWxp69OmZ7OAAAAADAbia0zTFPb2iNMG5It8jPz8v2cAAAAACA3Uxom2OenrVhEjL9bAEAAACgXRLa5pBMJtNYaTt+SLdsDwcAAAAAyAKhbQ55c/m6WLSqMooK8uKAQV2zPRwAAAAAIAuEtjnYGmHUgC5RWlSQ7eEAAAAAAFkgtM0hk7RGAAAAAIB2T2ibQyZvCG3HDeme7aEAAAAAAFkitM0RK9dWx2uLVqXb40xCBgAAAADtltA2Rzwze3lkMhHDepZHr04l2R4OAAAAAJAlQtscm4RMlS0AAAAAtG9C2xzxtEnIAAAAAAChbW6oqqmL599ckW6PH2oSMgAAAABoz1Ta5oCX562M9dV10a1DUezRqzzbwwEAAAAAskhomwMmz1re2M82Ly8v28MBAAAAALJIaJtD/WzHDdEaAQAAAADaO6FtlmUymXh6Q6XthKHdsj0cAAAAACDLhLZZNmvp2liyujKKC/Jj1IAu2R4OAAAAAJBlQtssa6iy3X9glygtKsj2cAAAAACALBPaZtnkWcvS9fghWiMAAAAAAELbrJu0YRKy8UNNQgYAAAAACG2zasXaqpi2aHW6PU6lLQAAAAAgtM2uyRv62Q7vVR7dy4v9gQQAAAAA9LTNhUnIJgzRGgEAAAAAqGcisix6emb9JGTjhpqEDAAAAACoJ7TNksqa2nj+zZXp9nj9bAEAAACADYS2WfLS3IqoqqmLHuXFMaxnebaGAQAAAADkGKFttlsjDOkWeXl52RoGAAAAAJBjhLZZnoRsvH62AAAAAEATQtssyGQy8cyG0HbckO7ZGAIAAAAAkKOEtlkwY8maWLqmKooL82PUgM7ZGAIAAAAAkKOEtlnw9Mz6KtsxA7tGSWFBNoYAAAAAAOQooW0WPD1rwyRk+tkCAAAAAJsQ2mZzErIh3bLx4wEAAACAHFaY7QG0VWsqa2L+ynUxd8X6mLdi3Yalfnv64jXpOeOEtgAAAADAJoS2O6Gmti4WrqrcLIxNl5X12yvXVW/zGhOGdouuHYp35scDAAAAAG2Y0HYbHpq6OFbHqo3C2GRZWLE+6jJvf3M7lRbGgK5l0b9rWfTrUpquB2zYPmBQ12b8GgEAAACAtkJouw1f/uuLkV/SYYuvFRXkRb8u9QFsQzCbhrNd6/eT451Ki1rqewMAAAAA2iih7TaM7Ncphvbr1RjENgSz/buURs+OJZGfn7f7vikAAAAAoF0Q2m7DHz85IXr06LH7vg0AAAAAoN3Lb/d3AAAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAgh2Q9tP35z38eQ4cOjdLS0jj44IPjqaee2uq5N9xwQ+Tl5W20JO/b1JQpU+J973tfdOnSJcrLy2PChAkxe/bsFv4kAAAAAACtPLS9+eab46KLLopLL700nnnmmTjggAPihBNOiEWLFm31PZ07d4758+c3LrNmzdro9TfeeCOOOOKI2HfffeOhhx6KF154Ib7zne9sMdwFAAAAAMg1hdn84VdddVWcd955ce6556b711xzTdxxxx1x/fXXxze+8Y0tviepru3bt+9Wr/ntb387TjrppPjv//7vxmN77LHHNsdRWVmZLg0qKirSdXV1dboAua/hWfXMQuvhuYXWybMLrY/nFlofz23btb25RV4mk8lEFlRVVUWHDh3illtuife///2Nx88+++xYsWJF3HrrrVtsj/CpT30qBgwYEHV1dTF27Ni44oorYr/99ktfT44lLRG+/vWvxyOPPBLPPvtsDBs2LL75zW9u9DM2ddlll8Xll1++2fGbbropHSMAAAAAwK5au3ZtnHHGGbFy5cq0o0DOhbbz5s1Lw9fHHnssDj300MbjSeD68MMPx5NPPrnZex5//PF4/fXXY/To0ekH+/GPfxwTJ06Ml19+OQYOHBgLFiyIfv36pUHr97///Tj22GPj7rvvjm9961vx4IMPxtFHH73dlbaDBg1K2y/06NGjhe4A0Nz/peree++N4447LoqKitxcaAU8t9A6eXah9fHcQuvjuW27ktyxZ8+ebxvaZrU9wo5Kwt2mAe9hhx0WI0aMiF/96lfxve99L620TZxyyinx5S9/Od0eM2ZMGgwnrRe2FtqWlJSky6aS4Ef4A62L5xZaH88ttE6eXWh9PLfQ+nhu257tzRqzNhFZkigXFBTEwoULNzqe7G+rZ+2mH/LAAw+MadOmNV6zsLAwRo4cudF5SbA7e/bsZhw9AAAAAEDLyFpoW1xcHOPGjYv777+/8VhSKZvsN62m3Zba2tp48cUX05YIDdecMGFCTJ06daPzXnvttRgyZEgzfwIAAAAAgOaX1fYIF110UTrx2Pjx4+Oggw6Kq6++OtasWRPnnntu+vpZZ52V9r394Q9/mO5/97vfjUMOOST23HPPdLKyK6+8MmbNmpVOTtbga1/7Wpx22mlx1FFHNfa0/de//hUPPfRQ1j4nAAAAAECrCG2TcHXx4sVxySWXpJOIJf1nk5C1T58+6etJS4P8/LeKgZcvXx7nnXdeem63bt3SSt2kX23Tdginnnpq2r82CXq/8IUvxD777BN/+9vf4ogjjsjKZwQAAAAA2BFZn4jswgsvTJct2bQ69ic/+Um6vJ1PfOIT6QIAAAAA0NpkractAAAAAACbE9oCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLQAAAABADhHaAgAAAADkEKEtAAAAAEAOEdoCAAAAAOQQoS0AAAAAQA4R2gIAAAAA5BChLbknk8n2CAAAAAAga4S25I45kyJ++56IHw2NmPlotkcDAAAAAFkhtCX7lkyLuPnjEb95V8SsRyLWr4j46zkRFfOzPTIAAAAA2O2EtmTP6kURt18U8fODIqbcFpGXHzHmYxG994tYsyjilnMjaqt9QwAAAAC0K4XZHgDtUOXqiMd+Wr9Ur6k/ttcJEe+6LKLPyIilb0Rce0zE7Mcj7r004sQrsj1iAAAAANhthLbsPknV7DO/i3joR/WVtIn+YyOO/17E0CPeOq/HHhHv/2XEzWdGPPHziEETIvY71TcFAAAAQLsgtKXlZTL17Q/u/27E0mn1x7oPj3jnJREj3x+Rl7f5e0a8N+LwL0Y8+r8Rt15Y3zKh196+LQAAAADaPKEtLWvWYxH3XhLx5qT6/Q49I475RsTYsyMKi7f93ndcEjH3mYiZ/4n4y8cjPnV/RElH3xgAAAAAbZqJyGgZi16NuOmjEb99d31gW9Qh4uiLI774XMRB5719YJsoKIz40PURHftGLH414l9fqK/aBQAAAIA2TKUtzatiXsSDV0Q898eITF1EXkHE2LPqq2s79d3x63XsHfGR30Xc8J6Il/4WMejgiIPP960BAAAA0GYJbWke61dGPHJ1xBO/jKhZV39s3/dGvOuyiJ577dq1Bx8Scdz3Iu75ZsQ934rof2DEoIOaZdgAAAAAkGuEtuyamsqIp6+PePi/I9Ytqz82KAlZvxsx+ODmu7uHfDbizaciXv5HxF/Ojjh/YkTHXs13/f/f3n2AR1WmbRy/JyG0QOiEHkA6SJcuWChrZ21YQeyuuuu6ruXbtbviqotYUNldFbtY0VVBEQELTUAUUZAO0nuHQJLves6bSSOBBCYzZyb/33WdayYzyczJJO+U+zzv8wIAAAAAAAA+QWiLo5OeLs1/X5r4gLRthbusejNXWdv8dCkQCO0ja7d39tPS+vnSpl+ld4dKl491fW8BAAAAAACAGMJCZCi6pZOl/5wsvXeVC2wrJEtnjpBumCa1OCP0gW1QmYrSoNekhERp+dfSpH8Uz/0AAAAAAAAAEURoi6KxnrWvnCOtnSuVriid/Hfpj99LnYeGp+q1RnPpnKfd+W+GSws+Kf77BAAAAAAAAMKI0BaFt321a4dgOl0h/Wmu1OevUunE8D6Kbc6Tut7gzn9wg7R5SXjvHwAAAAAAAChGhLYovAl3Swf2uIXGrB1CYvXIPXq20Fn9rtL+7dLbg6XUPZHbFwAAAAAAACCECG1ROMu/kX56z1YEk05/tPj61hZWqdLSBaOlxBrS+p+kT/4iZWREdp8AAAAAAACAECC0xZGlHZQ+vd2dt961tdv541FLqiOd/6IUiJN+eEOaPTrSewQAAAAAAAAcM0JbHNmsF6UN86WylaVT7vbXI9aot3TqPe78uNul1XMivUcAAAAAAADAMSG0xeHt3ixNesidP+XvUvmq/nvEet4itThTSkuV3h4i7dkS6T0Cii51t7R3K48cAAAAAABQKR4DHNaXD0j7tkvJx0udr/Tng2X9dQc+K/37Z2nLUum9q6VL35Hi4iO9Z8CRWS/mGaOkCfdIafulclWkqsdJVRtL1TJPg5sfD5oAAAAAAICQI7RFwdZ8L81+2Z23xcf8HIKWrSRd+Kr0377SkonSV49JJ90Z6b0CDm/XBmnsH6TFE7Ivs2rb1bPclpe1KDkkzD0uO9CN9AKBAAAAAAAgJAhtkb/09MzFxzKk4y+QUnr4/5Gq1UY6a4T0wXXS5Eekup2lpn0jvVdA/n79XPrwD9LujVKpslL/h6R2F0tbl0tblriqcds2Z57uXCPt2yatmeO2/A5c5A1ygwFv+WoEugAAAAAARBFCW+TvxzHSbzOlhESp3wPR8yi1u0haNcMtnvb+1dK1U6QqKZHeKyDbgX2uFcLMUe7rmq2l81+QarbMPvhgW16pe6StyzKD3Byhrm07Vrs2JlYdb1teZZJcgNvyLOmEq6VylfmLAAAAAADgY4S2ONS+HS5UMr1vk5LqRNej9LtHpDVzXTXi24OlKz+TEspGeq+io7fqhl+kGs393Qojmq3/WXrvKmnDz+7rrjdIfe8r3P9n6fJScmu35XVgr6vQzRXm2vll0vbfpP07pLVz3fbtky647fYHqUKN0P+OAAAAAADgmBHa4lBfPSrt3uCmWHe/MfoeoVJlpAtfkUb1diHV+Duks56M9F753/TnpM/ukpqdJg16TYrn6SGkgfjM/0if/90tNpZYQxr4nNS0X2huP6Gcq9QNVuvmrey1QNd65E59Rtr4i/TNcGn6s1LHIVKPm6XK9UOzHwAAAAAAICTiQnMziBkbf3XhXbBi1QLQaGQh1Hn/lRSQZo+Wvn890nvkb7s3uz7A5tdx0v/+5ILGaHJwn8qmbpHv7NoovTFIGvdXF9g26SfdMDV0ge2RWBVvzRZSh8vc/V70hlS3k/d4eS0anmovjb1R2rQoPPsDAAAAwN/r22xbKS2eKM0YJX32N2nq0+7rneui73MiEMUopUM2e/K1qtT0g1Kz30nN+kf3o9PkVOnk/5Mm/UP65Fapdlup1vGR3iv/Vlfv3y4l1ZN2rpXmviYlVpf63a+osGuDSr18lgZsXKD03e9JXa93/8ORrhZe9IU09gZXuR5fRur/oNTl2sgtChYXJ7U4Q2p+urRsivT1v6RlX7m/99zXpVbnSCfaWGkXmf0DAAAAEL62iJsXSZsWZ54ukjbb+SXSwb0F/1y5qq5lmzfTr1X2+TIV+csBIUZoi2wLPpGWfCnFl5YGPBwbj8yJt0mrZkqLJ0hjLpeuncwiTHnZi/J3VpUs6ZxnXA/Uj26Svh3hpvH3uEm+ZpWsL5+twMYF3pdxy7+WbKtUXzrhKqnDYCmxWnj3yVoSTLzftSAwNVq6xcby60cbCRYaNz7Jbau+c+0SFn4q/TzWbVYNfOJfpJTukd5TAAAAAEcr7aC0bYULY71QNkdIu2t9wT8XlyBVbSRVa+oW9raFj219Dls3Y+8W93nLtpwqN3CLLCe3yg5zqzWR4hP4+wFHidAW2QsZWT9TYz0uqx0XG4+MVRae+2/p332krctc1eOg193lcCxctOpqC+qOO9ldtnuju/zzv7mK23YX+betwyvneH1aMyrU0re1hqp7zT2K//5Vafsq6Yv7pEnDpOMvkLpcI9VpX/z7tGGBW2xs/U/ua6us7feA6zvrR/VPkC5+U1o/X/rmCemn99xBDtsa9HDhrVWtR6o6GAAAoDCCU7Z5z4KSyD4XWRCbFc5mntoCxekHCv65xJpS9aYuXPVOm7rTyin5z1q03GDjQrewsn1+8E5/lnatcy0VbLN2e0FWEFa9WWaI2yo71E2qy1gFCoHQFs63T7kn2Ip1pF63xtajUr6qW5jshQGumtCqCnvfFum98oeVM6SfP5QCcS5YDOr1Z2n3Jmn6SOnDG6Xy1cLXg7Ww9myRXjlb2jBfqlBLBy8bq80zflX6yacr3tpiWPhoPVvX/pA5/f81qX5XF6K2PFsqVTr0HxRmveB6Plm/2PLVpYHPSs0GKCrYkXDrA22P3bdPSnPfkFZOlV6fKtVq68LblmdJcfGR3lMAAFCSHNzvtcJy2/rMLc95a0Vlp6bFmVKHS6WGvSnUQGxX0K74VvrlIzdj1lrcFaRUWRfK5gxmvfNNpLKVina/VohihTB5i2Hss1kwwLXPZ97pL1LqTlfMYtu8HN9fplJ2Ra61VqhUT6pYW0qq4z5HUWQFeAht4cJaCzKN9dwsUyH2HpU6HaTTH5P+90fpywelxV+4cNqCyJJ6NN5Cxs//7s63v9S9aAbZY9L/IWnPJunHMdLbg6XBH7mqTD8Ftvbib0eHh/xPqtzIVlHLXnzL3qy3v0T67Ttp5r+l+WOlVTPcViFZ6jRU6jxUqljr2PfHAu6PbnYHBcxxp0oDn5MqJivqVG0snfWk1OcOadpIadaL0rofpXeGuDd4Fui3vZBpTgAAxJqDqa7XvfWytOq4uFLu1Ntynk9wU6eD573Nvj+h8LPZ0tPc+6ecAWwweM0byu7bXrTfY97bbrO1GtpfLLW7OHZmEaJkC47RXz50Qe2ezbmvt/95C2KD1bLBkNYuL+4Q1AqlGvZyW87Pm5Y15K3KtYpgW09l5TS35WXPPRVqSUm1s4Nc+8xmBWbeZZlfhyu3sIDcnof2bZP2bpX2bst93nr52sxEe7xLaraAYkNoCxfcWWVgSi+pzXmx+4h0HOz6tVqvVntxeOMCNz3DQqjWv4/8olXhZkdlf5spJZSXTv7bodfbC/s5I92bAQu57fG68jOpRnNFlL04vjpQWjfP9dy94mOpRjPpQD7TfuxFs34Xt1kIPftlF0La9J0pj0hfP+4W3+pynfueo3mRtT7QH1zvPlTYB5a+97uF0KL96LC9ORrwD1dhO+N5t9kbrA//IE0eJvX4o9Txcv+2fQAAAIWfVm3vj777z+F7XBZGID6foDdHyGvvtSystcKAjPTC3679rB2or2Bbcp7THOftd/nhTemnd6Udv0lfPea2Bt3dwfxWA6WyScf2OwLhXitj6SQ3O9IKRHIexLAFwWyRYfs8k9JDKp3or7+NjXfrh2tb89Nyh8+bfnUhrm0bf3U9c61a2A7YWOs+G7+2HU6ZpMxQNzPczQp4g+dru+cNey5KT3dBsYWs9nnSC13zhK/By7Kut+/f6qqFC6NKIzfL0raUnlKpMsf2+KFg+3dKP7wlrZkrNejm/r+srWMMCmRkBJv/RM7IkSP12GOPad26dWrXrp2efvppdenSJd/vHT16tIYOHZrrsjJlymjfvn35fv/111+vUaNG6YknntAtt9xSqP3ZsWOHKlWqpE2bNqlatTAvYBRuSye7nqA2Pf66r6VabRTzdqx10/5nvSSl7nKXWc+enn90FaclIYSyF8pnu7oeR1ZRaVPiC5K621voS6tnud5DV33upq9Egr2IWmC75ns3bcYCW5tOY+9nDhzQp59+qtNPP10JCQmH/90X/E+a8W9p1fTsy60FQNfr3IGLwvwP2FS9iQ9I055xX9do4doL1DpeMfvCaB/opj7jKmGMhebd/uAWfCvq1CqgKOMWgK8wdmOE9eG3RVNtVpUVcBirbrOAJS1VSjuQuWWeTw+eP5h5mmqldEd///b5w97PHS6EDZ6WrVy0g+sWdC38xLV7sgPswYC4VDmp1dkuwC1h7RMYt1EkdY9bX+Lnj6Rfx2d/ZjUWQrY8MzOo7RV7hUf2/GIHjyzA3bFG2rlO2rnGfYbPOl1X+CDVnmdKV5T27zi25ytjt1OuilSukntO8s5Xlratcq0qvOfETAmJbr2Ypv3dZgHyUWDc5vO6ZYuoW2Cb83/A/s62HouNDTuQYYvi+Vwwd9y+fbuSkgo+mBjxET5mzBjdeuutev7559W1a1eNGDFCAwYM0MKFC1WzZs18f8Z+Ibs+KFDAC/gHH3yg6dOnq06dOsW2/1HN3nyNu8OdP+HqkhHYGnvCtKpLqyC0AT/9Obei5id/kSY/InW7Qep8lXsCjlUWvllgay/6VjF5OHbE9tJ3pBd/J21aKL36e1dxa1NgwsmOKr92bmZgW00a8lFWYFsk1svWglnbrN+ttU6Y965rAWD9ez+/21VlWxBZ0JO9HQ1+70pX7Wvs/8X+p0qXV8yyaT89/+Sqkq0/sPW9telOtmDdNyPcQm92PdUrAAD4l9XrLJkoTXvWnQbVbi91v9FVohal77+1OQgGuDnDXKuUyxv8Wuhr32/VUBbG2vu54uqVb62ygu/3LPixYNoCXKvus/O2VarvFtulfULJHAcH9mROec/cjP1PWIVmJMJ8K5D49TNXUWuzHG3/gqwdgB1ssHU5rKowlteYsBC6Ul23HenxyhXkrs0R9GYGu7ZlpLkK2yCbZeoFrpmh65HOB8NZK1A5XEBu+2MFcfY3XDTBzexc8LHbTO12UtPMKtw6HUvUAaNjlnbAtQOx7Gb519mX2wJ3FohbyxD7LL/iG7eNv9M93tbf3DbLDKK4bUXEK20tqD3hhBP0zDOuWi09PV3169fXzTffrDvvvDPfSlurmN22bdthb3f16tXebX/22Wc644wzvJ+h0jYPe7P22V3uDdPNs92TUUk9kvn9a9LUp6XtK7OnWnS+0lURRmNf0iNVqz7VQdq7RTrzCfd7Foa1lnihv5u6UrezC03DNQVn3w4X2Fp/WpsGZPedp6L1mI5CWo/cOa9I372Q/T9gR+uan+7CyEZ93BO9PV3Ofkka/3+u35vti7WQaHG6SuSLpy329vVwF+YHK9bPe8E/vY/he1QPANEpaseuzR7yPsSnS1WPK1kfmm3FdwsqrVhh44LMCwOuIsnCWmsfEMUfagvF3setni3NfV2a917uICfYPsFaptmB6hgUteO2wNB1b+7QNWvbVsDleTY7iFBQKw4Lb63i3N7b5jpt6ApXQjVWbOr9wvEuqLWK8LT92ddZ8YhV07Y8R6rbqWQ9X4WK1zt7o/ssaaGrhbDhaFlgrRgsRFz0uauUXj0nd5WvzTCwsLFZf+m4Uw47YzGmxm1R7VwvzR7tPn8HF9rL+ox+rdSod/ZY3LrCBbsWklsrzJztd2zNFgtvbVFtyzF8MpaiotI2NTVVs2fP1l133ZV1WVxcnPr27atp0/JpSJ1p165dSklJ8QLejh076uGHH1br1q2zrrfLL7/8cv31r3/NdXlB9u/f7205H7zgALEtJu3aoFKTH7a3ajp40t+UUapC/j1BS4JAgtRxqNTuMgV+/kDx055SwN7MfjtCGdOfU3rbi5Te/SbXoyYGxH31L8Xv3aKM6s108PiLC/93L58sXfS2Sr16pgKrZyl9zGClXfBq8S9ItX+n4t8apLjfvlNGuSo6eMl7UrUWh+x3cKwe1ZhNqCh1vVE64XoFFn2muFn/Vdzyr7KOjtpjld7xSgWWT1Hcr+O8H0lvdJLSznrGNcEvqWOn1XlSy98rsPBTxX9xjwLbVijjxQFK732H0nv8KbarABASxzRuAUSM78auVXHu2qCA1w9xnQJehdVaBazSaee6zNO1ClglVKaMspWUUa+LMup1VUb9rsqwRWtthfVYs3Od4ma/pLjvRyuQuWhRRukKSm9/qdI7XyNVaei+7+BBlQjJ7aQB7aRTH1Dg13GK++EtBZZNUiBzQaSMcXcoo8WZ3vv/DJt6bgFBjPDduM2PjVEbs1Y9aWM2s2rSje31CljQbgHcvu0KFBS6FkGG9WC2wMw2C/h2rFbAnk+2LHFbfj9TuoIXqGZUaqAMC3PtfOXs87LrD2fPZu+9c9yCj73PFgGrSg/edtXGSm9xttJbnOVatwUDqTSraE875t+3RCpbzW3KzE3D9f9fo7XbevzZC44DSyYqbvEEBZZ+qYD19P7hDW/LiCvlXoOa9FN6k/5uEbkcBwWKZdxmVZpnHuCwher8MlsyI0OBVdMVN/sFBWyMpLvxkZFYQ+ntL1d6xyGuZWPe160KdSR7TbNt9yYFFo1X3MJPFFg2RQGbYTz1KW/LqJCs9GanKaP5GcqwvsN2kCZCCvs3jWil7Zo1a1S3bl1NnTpV3bt3z7r89ttv15QpUzRjxoxDfsbC3EWLFqlt27ZeIv3444/rq6++0vz581WvnuuzOWzYME2aNMmrsrXWCQ0bNjxspe19992n+++//5DL33jjDZUvH5vTnduv+K9StnylbeUaakrz+2LqDckxy0hX8o4f1HT9x6q2e5G7SAGtrtxVi5LP0I7yKYpW5VI36dSf71B8xgFNb/xnra/Uoci3UWX3IvVY9E+VykjVqio9NCfl2mL7/4lP26fuSx5Xtd2/KjW+vKY2uVPby2d+uChmFfatVuONX6j+lm9UKj37oE56IF4/17lQS2oMYNzkUCptj9qtHK1621yf4E0Vmmt2yvXaVzrG+4IDAIpPRrrKHNypsge2Zm7bDjlf7sBW73sK62BcGe9Dob2Pycle37eVb6TNiU21JbGZtiQ2VWqCTz7EHoWkPSt03Mbxqrd1uuJserCtN1a6upbW6K+V1XrrYHxsfsY5GmVTt6je1qlqsPlrVdy/NjtbS6imVdV6aWXVXtpTJsZm3oVbRrpKH9ylcge2ZI1be9y90wOZp6lblJC+r2g3q4AOxJfXgfjEzNPyOlAqx/l8t+zr0+z5IEdAFshI8/YjMXWjytu23043eecT929U2YOHn+1r9peqqD2la2h36RraU6aGd962xP3rVWfbd6q2a4HilF0FuKNsXa2pfIK37SxbL/Yr3ku4QMZBVdv1q5c3JG+fm+s5x+wuXVPrK7XTuqR22lyhhdLjDhMq2mtZ+l6VPrhbCWm7cp2WTtuthIO73GnWdbu9cWin8Rk5DhYooB1l62lLBXv9c6+Be0pXD+v/on3ut+fhRhsnqtK+VVmXb05sqmXVT9Xayico3Ra0LKJSaXtVc8ePqr1ttpJ3zM31HGPPAeuS2mtt5U7aULGt0uLDu3Dcnj17dMkllxyx0jbqQtv80umWLVvq4osv1oMPPuhV7lo7hDlz5mT1sj1SaJtfpa21aFi7dm1MLkQWWD1HpUb3984fHDJOGfWYylzgY7VymuKmPqm4JV9kXZbe+FSl9/ijMqzRdZS9qMZ/eIPifnpH6Sk9lXbp2KPe/8DiLxT/zmXeka+0Ltcrve+DoX8sUncrfsxFirOqhzJJOnjp+67f2mGeCyZMmKB+/fqFdurIvh2Km/eW4uaMluJK6+BZT7kj38j/yOi8txX/2e0KpO5WRtnKSjvjCWVYtQAQznELwL9j16YsWqumPZsUsIUtrQJp96bMU/vazlvV7Dp3mqMK7bA3ax/mKtZShvWjrGCntbJP7fIKtpp4LTf1Pe2AAuvnKfDbTAVWzXBbcJHNnLdZrYlXiZtuVVD1u0pVGvv7vV96mgKLP1fczOcVZ4viBC+236Hr9cpodpoUF/ElTfz9PmbNHAV+eENxP3+ggLdwkZNev5vS216sDOspGqXtE4rlNdeqUm08W+Xr3s35VMna167XaGErYzPs8a1Yx43lirWVYf1cvTGc7LXzywhWxlo7O6tqDeeYtJYM23/zZpcFbG2H7e7Uvra1HgJWuVgIGcnHK72lVdSe6SorUXJtXeYqcG1b8a2r9M6UkVBeaSknasX2DKXUqKg4r9J8mwLWVsP+1/Zu8w40HC3vdbN0Yr7/tzbe3GyUzBkptv5RcVSlbl7kZoP8+GbWbJiMUuWU0eY8pXW6KrSLfB/cr8CKbxRY+Inifh2f63U/o1RZZTQ6SelWgWu9h8Owfo/ljtWrV/d3aGvtEayS9d1339XAgQOzLh8yZIjXs/bDDz8s1O1ccMEFKlWqlN58801vITNb2MzaLASlpaV5X1sQu3z58kL3lti0aVPshbbWX+W/p0pr5rim+79/PtJ7FB1swSlbeMl6eAb7o1jY3evPkvcGOAoqldfMlf7dx52/drJk0wCPxQ9jpA+udef73ucei1D2GX7jQtdo3N6QDR7rejkdRonu9+M3m5dI713tnmeMTWP53bDw9UBG1GDcAjEydq3PeWbY6vUP3LXRndrXec/btNBCBrFOQKpQ0y0O5G0WwtZ2C8tmfV3Hrc1wtO/H7OPQ1mXSyhmuF96qGTn6vip3H0JbBMjburuDuEVZtKu47N/l+rRav1r7PYyFs7aomK3PUO/w76FQQDhn/RFt8TLrNRrsR2krwtviZbbmwdEsiOvH19zgVGkLgiyA9U7z2byQKPh15vnUXUc3lpPquM07XzdzPNtltaM2FPfY42IBrvXXzHtq/VSth7QF/1Vjo+0eiuG5fNmUzMXMPs/u43okpcq518CsLXPxtCNt9tnMDnrYQVJ73Vs1U1o53S3Wnfcgi7UPss/jdgDT27ocfbBpi1Zar9/v/uMWb8vZe/aEq11/8eJebyk9za2X88v/XEvErTlyQmubktLD9cC1MVvJzeiPVE9bXyxE1qVLFz399NNZ/WgbNGigm266Kd+FyPKyQNb61tqLz/Dhw7V582avQjanAQMGeD1uhw4dqubNm5fs0HbOq9JHN0mlK7rFx2Jtka3itmWZW7DMFi4LNoqv0ULqeYt0/PnF39/1aNkwf/ksF4Ief6F03n9Cc7tTn5E+/5s7bwtydbgsNIHtm4PcKpD2f2qBbb3OR/wxwh+fOZgqTX5Y+maE+6BjVQTnv+BW8gQyMW4Bn7MPVtYP1irmbCFS73SN0rf/ps0rF6p62TQFLJC18KaorFIusaYLcRKru/OJNaQKNdz5YDBr5w+3YndxsUVK7UOsfYC1zQ5E5qiAyv4Q21lq0NWFuHZA3z4sh8u2VdLMUdLsV7IX1bKVzjtd4RZpOdLq6yic7avdIm4WjG9enH15wxNdwGAf6v36GSD4OWDdj0r76UOt/mWG6lWr4Cr2cgayef+3iySQuchTldwHVIJhrJ0GD7D4+XEC/DhuF36uJb/8oOPadFZ8heqHBq/2nJ9QNvQHrdZ87177vDB3Rv6v89Wbu/DWDmTW7yZVO+7wle924HbOy9Ksl6Qdv2VeGJCa/U7qcrXU+JTIFMNlZEjr57vw9pePpfXzcl9/wctS6+wi0xIX2o4ZM8arrB01apQX3lql7Ntvv60FCxYoOTlZgwcP9looWJ9a88ADD6hbt25q0qSJV4372GOPaezYsV5bhFatWuV7H0dqj1BiQls78vd0J1fh0P8hqcfNkd6j6LVrg6tm+O6/UnDqlK0yaguWdbzcfxWFdrTOKletT8vNs1yT/FCZcI+rQrYjUhe9LjU/7dheIN68yB1xs+lOl73vPggV5keptPWnpVOkD65zR4ptCo5VZVvVTzRUp6PYRfW4tQMTUx5xFYQ24+K4k6WEcpHeK6DwDu53z82ZQWx2KJsdztrCP7lWYD4c629vlaheCFsjM4DNez5HOOuHCtWiOLBPWjs3O8RdNT2fD7EBqWYrqUYzV/mUkLlZuGsfqr3L7Hz5zMvK5Tktn/v77DS/wHrVd9L0kdLPH0nBqbFVj5O63eAqlPz2PjRW2MdmK4CY+W9pwafZj71ViHa+Uuo0xP2f+4VVjs17R/rxHWnTwiN/v1Vnl6taiCq9ylLZHNV8Ftiy+CwQu++Vbba2HbCy172VmSHuZrf2Ty7lq2VX4lqQa60NS5VxFa0z/yP9PDb7AJE913Qc7J47q6T4r1BvwScuxLV9v/WXYnluL2xoG/GmRoMGDdLGjRt1zz33aN26dWrfvr3Gjx/vBbZm5cqVuVodbN26Vddcc433vVWqVFGnTp28nrgFBbbIYfIjLrCt3kzqch0PzbGwQdv3XqnXLdKsF6Vpz0rbV0nj73Bv5IZ+6o4m+6VKxoJV0/W60Aa2pu/90u7N0tzXpHeukC4fK6Vk96gu0oehty5xga1NPbv03UIHtvCxxn2kG6ZKH93sXvisMnvJRGng81T6I3rtXC+9fbl702rmvOLClSanuoorqxgIQy8s4PB9F1fnH8QGz9t7wsKwIMebtpy9pSUm6/tFa9S+Vz+Vsko6e19kH8Bi+YCchajB1ghZH2IX5Q5xbYXqDfPdFir2+GcFwFZNFXDTrYMa9Za63Sg17R/bj78fWAWZPd62bf/NVYvNHi1ZD9dJD0lT/umqsU64xlWfRaL/sVWIz3/fBbX2PxkUX0bpTQdowfYyata+m0rlV7EXnCoNADnZa4sdjLTNglZjn/+DVbi2rZ4j7dksLfzUbd7zTmmXiVj/5yBrsWDPka1/H/oK4VCx9iU9bnKbHZwt7lYNRxDxSls/islK2w2/SM/1dEeErXrRPlgidCxw/OENacpj7o1b8vHSFR+Hd4pcQezN5P/+5J5s/ji3ePbJguExl0m/jpPKVJKuHCclty5atc9bl0qLJ7gqEwtsG/aMvqOQKJi91Mx+SRr/f9LBva4aa+CzUrMBPGolWFSO29Wzpbcuc8/19nzX5vfS4i+l7StzVxzaYpUW4LY4XarSMJJ7jJLAXodtKuPSSa7/plWGFKZvrFV3ekFs3RyhbOZU5uB5q4rNEwZG5dgN1wEd+/Bqobi91tn7w+Cp9Qw9aKd7M08Pc51th2MfhI+/wFXWhnKRFhSdvYf9+UNXtGHjLsj6HVvf2zbnS6XLF+8ja63F7D34j29Li7/IMfYzQ+a2F3q9GW2ldMYtEF2i5vXWngvX/phZjZvZVsFmoxmb7WutJK2dTN2Okd5T34iaSluEKSwZd7sLbG2FSgLb0LOjRFba3/gk6YUBrg+KVY1e9l5kp8taM/Mv/+HO97mj+EJkm7p3/ovSq793T9Svnitd9XnhpjrYE/yYy3MEtu8UObBFFLDKDRsjFmS9d5W0/ifXssOq/vs94N8jrX5nFTXWY7tMBem4U/03vSjW2II0/7vF9TS3Pl4Xv+n6d3l9x+a5qVQLP3HnV3zjts/ukpLbuAC3+emurzOVTAgFq+pcMskFtUu/yu5pGmSzVqynaX6hbPDUDujy/xg6tlZEq7OP/XasiteeZyzEzQp5cwS69vxjvX8ReTb110JR2+zAycz/Sj+96/Wi9GYZfX63a53W+arQLj5lB2pswSJrf2AL6eRcEMwCY9ufNue5sR50IM/CQgAQyufC+ie4zdpwBhf43LTI9XtnBtpRI7QtCezory3qZNUUAzIDPBQPW/HQgtrRZ0grvpXevUq68JXILKBhbNE0W625SiP3ZrE4WRXBJW9JL50ubfhZeu1c6crPXP+6w/WEfHuItOgzN/XvkjFSw17Fu5+IrJotpKsnShPvl6Y/6xZQWf6NW6QsylZhjih7I2QLotiHwb1bsi+3Bd+a9HWbHfygx2popB2QPv+7NON597WFr78fJZXNPCpuoVfttm47+S63SrRNDbMQd8VUd5DCNps6a/3P7eetAjelZ3gWZUnd7XqR2Rtn22xK96Zf3fReWzjCWufYQUfCO3+zKXr2fi4Y1OZc6dhYX0n7OzY+2fVYrpzC3zRaWWVzXGZLBESPOh2kgSOl/g9K37/q1r6wacH2ftwW77UWFlZ9awdZj6aVhb32WzBsQe1P77me00HW/swWG7awtsaRF94GgGJl7yktG7ENx4TQNtbZdBn7oGl6/okpmuFgH9qt+sqqTa3i6uM/SWc/E/4PTjvXSVOfcuet/244Fvywih0Lra3a2AKC18+XhvxPKlMx/xDk3aFuOpcdULDA16ZwIfZZVe3vhknHnSKNvcH1/vv3Se6gkh1cIDg6crubj2+VVk51X9do6aroV810YZxtM55z48pCwWCIW70pj+3RsJ5d7wxxi8+YPne6mQuH+8BtFc82bdk2q4a2xSDt9WDxRNf/3A5W2GYhm/W/tSpc+xBvFdPHUplnLRssjN1kAe2vmeHs4hwr9ObDnoNtq9HChbdtB0XHIkbrfnKBhb1uxOpq5PY6adOtrd2BBbVr5uReFMx6ndpiHxbQ2orLddqzGBDgB1ZRZp+7bIHiRRNc6wTr529FCrZZMYVNE+5waeF6JVpVvfWonfe2e38dZH2krS+kBbX2XMD7JwCIOYS2se6bJ9wHRKvs6XlLpPem5LBq0Qtecn1ebeqy9e/sd39492HSP1yPNJuO0Gpg+O7XpmFd/oH0Yn9XDWCPwSVvuykTeQNbW5jKetxc9IarDkLJ0rSfW6TMglvrwfbJX1yoZQc5EmOkn3ioqyWtUnPaSNevztqJnHSXCwYttNq7zVXhWasRexxtsSH7kGibTdGv1MC1x7EA14KuYJUoCrYus9WNVUqVruCqa1ueWfQP7+0vdptNb7bFFr02CuPcQlA/jnGbPRfa86BV4FolbkGr1BZUNbt5iXvOL3A/qrlK7OpN3IKkdt56lVoIYG0fNi6QPv6z9MV9bpEJWyTCb+02dqzNXAl9jKtcNjVbS2c+ERsLV1oVnf1Ng31pbRZCzinPxqbFW0hrB73soMyxBP0AildcvNT8d26z52irvP3+dTdl2BZm/fIhqe0FUpdrD+1NvHuT9NP77jk6Z69cm5nW/DR3gM2eB8JRlAEAiBgWIovlhci2LJNGdnU9sWyKfqtzIr1HJc+cV6WPbnLn+z/k+ruEbeG5Hq4i58rPI/Nh1hbrGX2WdGC31Ppc6bwXXGWaBbbW09TadthCGhe9KTXtW3KatCP/CkGbdv7FvVJaqlShlnTuKIL8nBZ86nqT20E4Y/3Jf/eIVLl+weGPhXAW3logbu1a7LHNVaHXLTvEtQ+LEajQ8fW4tamnY290CwXZ1C47uBTKFh7paa462ipwf/nYfYjPEnArj1t4a+F8MJg9UtWs/V1tX/OGs1ZlfbheYvu2u+B2xqjs/bDF1Oz+u17vDkRGqoLLQmoLuX940wXewUpTe/2w8CLYx9WC5r73R1/PNKvk9nrSWlA7+dC/rx30tTDfq6Y92fWo9QFfj13Az+w5zRYMm/kfN9MoqEF3V31rr992YMoO3Nh6JMHnY3sesPYHduAwvxlshcC4BaIP4zZ2sRBZKNiLZjSztggW2FpFVcsQLIqAorOFB6ySyiqX7O9hH76s2qq4TbjHfbBteVbkqo/qdpIuek16/UJp/vuut+2AYdL712QHtoNeD0lgiyhnYX73P7gerO9d7cKpVwa6qYUn/61kV5FsWyWNu8MFe8aqZU9/1FXZHI4FbBYw2tbjJvchcfm3LsC1bcuS7IWyrL9whWQ3Pd9CXKvcibbgK5QsTP3yQTdTxdjjYj2XCzOFtagVWCnd3dbvQReyWzhpm02Dt1V3bTts1WxwywxnrTL2aFoFWJsGq9i2ai+bymsHUSxEtNkQttlCatY6wVarD0ePTfsbWDuKH8ZIv3yUu9rUDja0G+SmBNv7NHu9s96Rc15xj50dIG13sb+nCVvLDJsubX2PbaVl5Xi/aRXX9j8R7EubfPzR9b4E4E/WfqbzUKnTFdLKae65wBYSs/O25e2RaxW1Vvxgi9wBAEocKm0PV2m7cIaqNeuiqGTVVbYQVCBeuuFbFviJJPtQaYHtNOtrG++qtWyaVHGxSqRXznEVVzfOdCubR7pazRZksw+l1ntz4y9SXII06LWQPg4chYwRFi5+9n/S7NHZH1isSjvS/8fhZhXp1gbB2iHYlHcbz1ap3/uvoek3av3xvCrcia6lglXEZwm4gy7BXrh1O+buk2nPadae4eB+V71rW/C8d7rfLTKY63R/vt+XlrpXvy5Zribn3KaEWq0UcdZiwg4cWIsJYwcOTr03/H1Cd6xxgZ4FqPa6EQxnC1M1GyobFri+uz+8ld12wfonWtBwwlVSpXrFcJ+/uPuzFgjW3iOoSkMXxFrfxvwWtFgxzbV2sNcXY20DzhjuFj70W1WtvRewkCZnEG2heLCSNqVHVCw+xWsuEOLWL/a+x2Y82IHqNue5g2T2fB9CjFsg+jBuY1dhK20JbQ/z4G2Z+KSqnPJHRR37kGxT4206Zbc/uAV/EPnp3x/+wU3vtMWBLh/rKmmK437+3Uda96PU5TpXkecHM/4tjfurO2/hk7XrsIV3QogXtBjz80fSRzdL+7ZJCYnSgIdcS4CC+nzGEgugPrlV2vCz+7pBD+nM4aGdmp+TBagrp2dW4U7MPV3TlElyPalzBrA5KwNDxSp8rY9qswGRWUzJQkrrX2tVyDbt/pxnpOPPD/9++M3era7Vj03l3b7SXWZBss3ksOrcY138ZtcGad670o9vSWt/yF39a9VlFtZaq4gj3Ycd6Jj+rDT5kTwHOm6XSpdX5MPap91rYfAAibUk6Xajq26Pwuc1XnOB6MO4BaIP4zZ20R4hBAL2IVZRGNratEYLbG2BkZPujPTewNjUxrOfdlMibdXYNwdJQ8dJya1D+/jYYgUW2FrIYqub+0XXa90H1VkvSQMeDnlgixjU6mxX7fnBdW6atFXR2WYtRmzc2FazlTu1Ve8jHcqEKtj5wqZ6v5Y9Bd6mzbe/pHinelsg27iP2/o/6Ko8g71wbYq89Tu1nLYg1mvPpnRbdZB3Wiaz32gBpznOp8UlaOOi2Ure8aMC1r/PNmsBYZWc1qM0XG0abFr9+9dJqTvdwp0XvS7Vbhee+/Y7awvR849S9xvd4mn2HsPG5M9j3WaPk/W9tcqwnAtOHo4tyGaVxFZVa/9rwb6NFrQ2HeDaHzT7XeFvz1hbCKuMtrYJXkuRT12LC5vtcdpjxTvDpSC2kNDUp13gnRXWtnULCFqLEz+3cAAAAEDEUWl7mMR724ONVelvi6PrTfXO9dLTHd20u3NGSh0ui/QeIafUPdKrv5dWTXeLLV31mZv2GQr2Ifjpzm4RE5vOe+KtJeqx5yhkjLLelhZ6WL9Km9Kfb4VnwE2ZTrYQt012mGtjKxIVm0dTIT/3NWnCvdLeLe6yjkOkvvdFvrds2kFp00LXEiHfENa2Usc+bnu0VsL3L7vepFbZaWxWQpvzpS7XSHXaq9ge+68ekyY/7L5ueKJ0wWjXgxsFW/eTa51gi+kc3OcuswPFnYZKna+Ukmrn/1ivnOqCWutrvn9H9nV2gMYqaq2yNrFa6IL4T2/PXtjLKvVP+2fxtHUoTFhr4baFtRZGR9P7ygLwmgtEH8YtEH0Yt7GLStsQCOzeKG1e4lZgjhZWaWmBrfWBbHdJpPcGeVk14CVvSS+d7qY+W4B75edShRrH/lhNf859OE2q56asArHAQtdet7jN+t3aYk3rf3bjZ/1P7rwt9mdT2m2zxTyCEsq7KlwLc2tmVufa5qdAbv186eNb3YEcY/t55hORW0AwLwtkQz0jID+VU1yV78n/5yojZ4xyswYszLatXhcX3rY6p2jVl4ezf6f0wfVuoS1jLWUG/OPoFvIqaWq1cbNHTr1PmvOy9N1/XQ/arx6Vvhnuql2t+rZeZ2nTIhfUWsAbbK9grKLaetS2uyjkfRs9NqOjUR/XF9raJtjfeckk6eS73L4Vx9/ZC2ufkmb+N0dY2z4zrB0QE2EtAAAAwufoy2NKCltZO5pC26VT3KlVJ7HasH+nml72vvRif1c5+Pp50pCPpbIFN58u1AfF4Ernp94dFYuYAEVmC3BZRZ5teXtiWvhpmxfmznfhrvW1XDPHbTkl1szTYqGVVL15eFssWABtvTctTLJFvaxvb3GGSdHCnrtshkj7S6XfvnMLNs0fK/020222SJ0thGUVnZXqHv392AHZty51C1dZ1bAF5cxMKTqrirVZHdY71kJRC9tt9XNbSMw2O4gYrHQ11rrHgncLaq1Xc3G/TylTwR0MsPsLHhyxhUEtRLa/ufXKDYVdG11Ya+F1cNE2O3je507CWgAAABw1QtsjWf6N+4AYDWwRjhVT3XnrTQj/sqmjthjZC/3dwiu2+M2l70oJZY/u9qY86qaaWq+84y8M9d4C/maL+Nhmq6/nbKtgB0VyBrm2bV0u7d4gLbVtUp7bqeVaKnhbSo7zDd11oQqY8k7btgWdfvdIeKZtRwurSLRAzTbrgz37ZWnWC9LOta6dwdfDpZZnSl2ulVJ6Fq2C0Xr1vnul69VbsbY0yCp5OxfnbxP77ECDVdfatuZ7t+DWT++6/3FbtKxJX9entvnpkTmoaAdorI+814bkHlel/0K/Y29D4oW1T0rfvZA7rLXK2qb9qawFAADAMSG0LUxoa738omFK2+o5bjpeuapuii38rdpx0mXvSaPPdIu6vH+1dMHLRe/BuWmxCzOMVRRRYQ24cWRTrm1rPTB3hesGa7HwU+4w13rJ7lrntmCrgpysd2vlBvmHuja1vzCV8ttWurD213Hua7u90x93lXgomAXyff7qWmRY4G19Qm0WjPVFta1GS9c6oe0gV1lZEHstt2rIL+6TMtJdy4VBr0oVa/Hoh5KFlr9/Tur3gKuWtkDc/oaRZq+Ntrhd8zNccGsBrrV2sArh/v9w1biFfa9n1f3fPinNejFHWNsxM6ztFx3vGQEAAOB7hLaHkRGX4Kp6rFrLAja/W5bZGqHRiQR30cIW17FVyl8/3/Xi/ORW6cwRRfvAN/E+N73aqnoan1ScewvERouFep3cljPMs8WvrArXtm0rss97X6+S0vZLmxe5LT92sCxnZW7OULdCsjTjeVcRbwGPvbbYdPLefw1vS4aYqOYc6DYL2i28/XGMa3Fgz50WxlpbhROuPrStkS0C+dHNrvrTWHhngXmo+uPiUNarvcXp/mzpMNAWar3UtUyw/5+x10vfvyadOVyq0fzIYa1V1h7c6y6zdi0W1lo1MWEtAAAAQojQ9jAybPGITbNctW1UhLZfuVNbeAPRw1pZnPdf6Z0rpNm2ankN6ZS/F+5nV053YW8gzlU1ASg6C1pserRtdTseen3aQbfIUq4gN0ewu2ezq9S1LW//3LxSeh05GELhprufNcJNbf/hTRfg2kJ0M55z23GnutYJVvVofzvrX2sLm8WVcq0oLNglYCvZUnpI130lTR8pTf6nq95+rmf+B1R2rs/sWZszrO2cGdaeyv8SAAAAigWh7WFk1OvqQtsV30qdhsjXrIpo1Qx3ntA2+tjCLGcMlz6+xfVrLF9d6nb94X/GqgNtQRVjC+jUbBmWXQVKnPhSmZWzKZLyOSi2f6e0NW91bvDrFa5K18Z0/4eKNgUbR1austTtBqnLddLSL114++tn0pKJbrMWFNYSw4J1+xtc+IrUsCePLJxSpaVef5ZanyuNs9Yl46VvhruKbKvEtoP3XhsEC2v3uZ8hrAUAAECYENoeRkaDrtLckdHR19YC27RUqWKd6KgKxqE6D5X2bJK+fEgaf4er+mt7mEXFfrYV1b+TEspLJ/+NRxSIlDIVpVpt3JZXerq0a71UvpoLiFB8/UpterptW5a5kG3Oq66PsKndThr0ulS5Pn8BHMoOyFz8luuZPO4O93/zxoWulUn6Afc99U6QTrrTVXH7+f0gAAAAYgah7WFk1O7g3rAHp8VWbSTft0awqfZ8mIheJ94m7d7k+l+OvcH1yWza99DvO5gqfXG/O9/jjyykA/g5TEyqHem9KFnstdqqmk/6P2n++9LOdVL3G6WEcpHeM/iZvXdqeabrDT/lEWnasy6wtQXrvLD2FN5fAQAAIKwIbY+0YI31N7QqVqu29XVoG1yErHek9wTH+qFxwDA3lXfeO9Lbl0uDP5Lqn5D7+6yKbOsyt8CR9d8DAORmPUmtdQxQFGUquNC/4xXutbh+F8JaAAAARERcZO42ijTs5U6tr61f7dsurfnenSe0jY3KvHOeddN8baX5Ny6QNizIvn7vNmnKP915WwTFPmACAIDQqd5EsjZZzF4CAABAhBDaFja0tUpbv1oxVcpIl6oeJ1WqF+m9QShY70tbMMd66O3dKr36e2nbKnfd1/9yl9VoIXW4nMcbAAAAAAAgxhDaHkn9rlJcKWn7KrcKuB8tpTVCzLbnuORtF87uXOOC29VzpBmj3PX9HnCr2gMAAAAAACCmENoWJjir09Hf1bY5FyFDbClfVbrsfSmpnrR5kfRCPyltv9TwRKlp/0jvHQAAAAAAAIoBoW1hNOzp3762uzZKG+a78xbkIfZUqitd/oFUrqqUftBd1v9B+uwBAAAAAADEKELbIvW1/Vq+szyzyja5jZRYPdJ7g+JSo5l02btSpQZS95ukOh14rAEAAAAAAGIUDTEL29c2EC9tW+m2yg3ku9YIjWiNEPPqdpL+PC/SewEAAAAAAIBiRqVtYZSpmF3ZuNxnLRJYhAwAAAAAAACIKYS2Re1r66fFyKzqd+syVwWc0iPSewMAAAAAAAAgBAhtCyu4yNcKH4W2yzJ77NbtKJVNivTeAAAAAAAAAAgBQtui9rXdulza/pt8YdkUd9qod6T3BAAAAAAAAECIENoWllWy1m7nn762GRksQgYAAAAAAADEIELbomjYy50uz2xLEEmbF0s710rxZaT6XSK9NwAAAAAAAABChND2aELbFT6otF062Z1aYJtQLtJ7AwAAAAAAACBECG2LokE3KRAnbVkq7VijiFr2lTtt3Cey+wEAAAAAAAAgpAhti6JsJalW28j3tU1Pz27R0IjQFgAAAAAAAIglhLbR2Nd2/Txp71apdAWpTofI7QcAAAAAAACAkCO0jca+tsHWCCk9pfiEyO0HAAAAAAAAgJAjtC2qBt0lBaTNi6UdaxURS6e400a9I3P/AAAAAAAAAIoNoW1Rlass1To+ctW2aQekFVPdeUJbAAAAAAAAIOYQ2h6Nhie60+XfKOxWz5EO7JbKVZWS24T//gEAAAAAAAAUK0LbY1qMLAKh7bJga4QTpTj+fAAAAAAAAECsIfU7GinBvraLpJ3rFZFFyBr1Ce/9AgAAAAAAAAgLQtujUa6KVCuzNcGKMFbbHtgrrZrhzhPaAgAAAAAAADGJ0PZopQRbJIRxMbKV06W0VKliHanaceG7XwAAAAAAAABhQ2gbTX1tg60RGveRAoHw3S8AAAAAAACAsCG0PVopPdzppoXSro0K7yJkvcNzfwAAAAAAAADCjtD2aJWvKiWHsa/tvu3Smu/deUJbAAAAAAAAIGYR2h6LlJ7h62u7YqqUkS5VPU6qVK/47w8AAAAAAABARBDaRktf26W0RgAAAAAAAABKAkLbUFTabvxF2r1JYVmEjNYIAAAAAAAAQEwjtD0WidWkmq3c+RXF2CLBFjrbMN+dJ7QFAAAAAAAAYhqhbTS0SFieWWVrC58lVi+++wEAAAAAAAAQcYS20bAYWVZrhD7Fdx8AAAAAAAAAfIHQNlShrbUv2L1ZxYJ+tgAAAAAAAECJQWh7rCrUkGq0cOdXTlXIbVslbVkqBeKllB6hv30AAAAAAAAAvkJo6/e+tsEq27odpbJJob99AAAAAAAAAL5CaOv3vrbLprjTRr1Df9sAAAAAAAAAfIfQNpSVtut/kvZsUchkZNDPFgAAAAAAAChhCG1DoUJNqXozS1mlldMUMpsXSzvXSvFlpPpdQ3e7AAAAAAAAAHyL0NbPfW2XTnan9btICeVCd7sAAAAAAAAAfIvQ1s+hbXARskZ9QnebAAAAAAAAAHyN0DZUUjJD23XzpL1bj/320tOl5V+7840JbQEAAAAAAICSgtA2VComS9WaZva1nX7st7c+M/wtXUGq0yEUewgAAAAAAAAgChDahlLDnqFrkRBsjZDSU4pPOPbbAwAAAAAAABAVCG1DqeGJ7jTY1uBYLJ3iThv1PvbbAgAAAAAAABA1CG1Dyapis/rabjv620k7IK2Y6s4T2gIAAAAAAAAlCqFtKCXVlqoeJ2WkH1tf29VzpAO7pXJVpeQ2odxDAAAAAAAAAD5HaFtcfW1XHENf22XB1ggnSnH8iQAAAAAAAICShESw2PrafnPsi5DRGgEAAAAAAAAocQhti6uv7dofpH07iv7zB/ZKq2a4841OCu2+AQAAAAAAAPA9QttQq1RXqtLo6Pva2s+kpUoV60jVjgv57gEAAAAAAADwN0Lb4tCw19H3tc3ZGiEQCO1+AQAAAAAAAPA9QtviDG2XH0No27hPaPcJAAAAAAAAQFQgtC3OvrZr5kr7dxb+5/Ztl9bMcedZhAwAAAAAAAAokQhti0Pl+lLlFCkjTVqZuahYYayY6nrhVj1OqlSvWHYNAAAAAAAAgL8R2haXhicWva/t0inulCpbAAAAAAAAoMQitC0uDXsWva9tzkXIAAAAAAAAAJRIhLbF3dd29Rxp/64jf/+ujdKG+e48oS0AAAAAAABQYhHaFpcqKVKlBq6v7apC9LVdnlllm9xGSqxebLsFAAAAAAAAwN8IbYtTw16Fb5FAawQAAAAAAAAAhLZh6mu74tsihLZ9inefAAAAAAAAAPgalbbhqLRdPVtK3V3w921bJW1ZKgXipZQexbpLAAAAAAAAAPyN0LY4Vba+tvWl9IPSqplHrrKt00Eqm1SsuwQAAAAAAADA3whti1MgIKX0PHJf22Bo25jWCAAAAAAAAEBJR2gbrhYJBfW1zciQlk1x5xv1LvbdAQAAAAAAAOBvhLbhWozst1lS6p5Dr9+8WNq5VoovI9XvWuy7AwAAAAAAAMDfCG2LW5VGUlJdKf2A9Nt3h16/dLI7rd9FSihX7LsDAAAAAAAAwN8IbSPd1zbYz7YR/WwBAAAAAAAAENpGtq9terq0/Gt3nkXIAAAAAAAAABDahjm0tfYIB/ZmX75+nrR3q1S6glSnA/+QAAAAAAAAAGiPEBZVG0sVa0tpqW5BsrytEVJ6SPEJ/DsCAAAAAAAAILQNW1/bYLVtzr629LMFAAAAAAAAkAcLkYVL3sXI0g5IK6a68416h203AAAAAAAAAPhbqUjvQInR8MQcfW33SWt/kFJ3SeWqSsltIr13AAAAAAAAAHyCSttwqXacVCFZStsvrZ4lLZviLm90ohTHnwEAAAAAAACAQ1oYkb623+boZ0trBAAAAAAAAADZCG0j0dd28QRp1Qx3vtFJYd0FAAAAAAAAAP5GT9tI9bU1Feu4tgkAAAAAAAAAkIlK23Cq3lRKrJn9tbVGsLYJAAAAAAAAAJCJ0DbsfW0zWySYxn3CevcAAAAAAAAA/I/QNlJ9bXO2SwAAAAAAAACATPS0Dbem/aRSZaXk1lLl+mG/ewAAAAAAAAD+RmgbblUaSjfOlMomhf2uAQAAAAAAAPgfoW0kVEmJyN0CAAAAAAAA8D9f9LQdOXKkGjZsqLJly6pr166aOXNmgd87evRoBQKBXJv9XNCBAwd0xx136Pjjj1diYqLq1KmjwYMHa82aNWH6bQAAAAAAAAAgikPbMWPG6NZbb9W9996rOXPmqF27dhowYIA2bNhQ4M8kJSVp7dq1WduKFSuyrtuzZ493O3fffbd3+v7772vhwoU6++yzw/QbAQAAAAAAAEAUt0cYPny4rrnmGg0dOtT7+vnnn9cnn3yiF198UXfeeWe+P2PVtbVq1cr3ukqVKmnChAm5LnvmmWfUpUsXrVy5Ug0aNDjkZ/bv3+9tQTt27Miq2rUNgP8FxypjFogejFsgOjF2gejDuAWiD+M2dhU2t4hoaJuamqrZs2frrrvuyrosLi5Offv21bRp0wr8uV27diklJUXp6enq2LGjHn74YbVu3brA79++fbsX9FauXDnf64cNG6b777//kMsnTZqk8uXLF/n3AhA5eQ/aAPA/xi0QnRi7QPRh3ALRh3Ebe6xLgO9D202bNiktLU3Jycm5LrevFyxYkO/PNG/e3KvCbdu2rRfGPv744+rRo4fmz5+vevXqHfL9+/bt83rcXnzxxV5bhfxYaGwtGnJW2tavX18nn3yyqlWrdsy/J4DwHKmyF7N+/fopISGBhxyIAoxbIDoxdoHow7gFog/jNnYFZ/j7vj1CUXXv3t3bgiywbdmypUaNGqUHH3zwkH/wCy+8UBkZGXruuecKvM0yZcp4W14W/BD+ANGFcQtEH8YtEJ0Yu0D0YdwC0YdxG3sKmzVGNLStXr264uPjtX79+lyX29cF9azN7xft0KGDFi9enG9ga4uUffnllwVW2QIAAAAAAACAn8RF8s5Lly6tTp06aeLEiVmXWZ9a+zpnNe3hWHuFefPmqXbt2ocEtosWLdIXX3xBiwMAAAAAAAAAUSPi7RGsl+yQIUPUuXNndenSRSNGjNDu3bs1dOhQ7/rBgwerbt263mJh5oEHHlC3bt3UpEkTbdu2TY899phXTXv11VdnBbbnn3++5syZo48//tgLddetW+ddV7VqVS8oBgAAAAAAAAC/inhoO2jQIG3cuFH33HOPF662b99e48ePz1qcbOXKlYqLyy4I3rp1q6655hrve6tUqeJV6k6dOlWtWrXyrl+9erU++ugj77zdVk6TJk3SSSedFNbfDwAAAAAAAACiKrQ1N910k7flZ/Lkybm+fuKJJ7ytIA0bNvQWHgMAAAAAAACAaBTRnrYAAAAAAAAAgNwIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADAR3wR2o4cOVINGzZU2bJl1bVrV82cObPA7x09erQCgUCuzX4up4yMDN1zzz2qXbu2ypUrp759+2rRokVh+E0AAAAAAAAAIMpD2zFjxujWW2/Vvffeqzlz5qhdu3YaMGCANmzYUODPJCUlae3atVnbihUrcl3/6KOP6qmnntLzzz+vGTNmKDEx0bvNffv2heE3AgAAAAAAAIAoDm2HDx+ua665RkOHDlWrVq28oLV8+fJ68cUXC/wZq66tVatW1pacnJyrynbEiBH6+9//rnPOOUdt27bVK6+8ojVr1mjs2LFh+q0AAAAAAAAA4OiUUgSlpqZq9uzZuuuuu7Iui4uL89oZTJs2rcCf27Vrl1JSUpSenq6OHTvq4YcfVuvWrb3rli1bpnXr1nm3EVSpUiWv7YLd5kUXXXTI7e3fv9/bgnbs2OGdHjhwwNsA+F9wrDJmgejBuAWiE2MXiD6MWyD6MG5jV2Fzi4iGtps2bVJaWlquSlljXy9YsCDfn2nevLlXhWsVtNu3b9fjjz+uHj16aP78+apXr54X2AZvI+9tBq/La9iwYbr//vsPuXzSpEle1S+A6DFhwoRI7wKAImLcAtGJsQtEH8YtEH0Yt7Fnz549/g9tj0b37t29LcgC25YtW2rUqFF68MEHj+o2rdLX+urmrLStX7++Tj75ZFWrVi0k+w2g+I9U2YtZv379lJCQwMMNRAHGLRCdGLtA9GHcAtGHcRu7gjP8fR3aVq9eXfHx8Vq/fn2uy+1r61VbGBbOdOjQQYsXL/a+Dv6c3Ubt2rVz3Wb79u3zvY0yZcp4W363TfgDRBfGLRB9GLdAdGLsAtGHcQtEH8Zt7Cls1hjRhchKly6tTp06aeLEiVmXWZ9a+zpnNe3hWHuFefPmZQW0jRo18oLbnLdpCfaMGTMKfZsAAAAAAAAAECkRb49gbQmGDBmizp07q0uXLhoxYoR2796toUOHetcPHjxYdevW9frOmgceeEDdunVTkyZNtG3bNj322GNasWKFrr76au/6QCCgW265RQ899JCaNm3qhbh333236tSpo4EDB0b0dwUAAAAAAAAA34e2gwYN0saNG3XPPfd4C4VZC4Px48dnLSS2cuVKxcVlFwRv3bpV11xzjfe9VapU8Sp1p06dqlatWmV9z+233+4Fv9dee60X7Pbq1cu7zbJly0bkdwQAAAAAAACAqAltzU033eRt+Zk8eXKur5944glvOxyrtrWKXNsAAAAAAAAAIJr4IrT1m4yMDO90586dLEQGRNHKmnv27PF6WLOAIBAdGLdAdGLsAtGHcQtEH8Zt7LLcImf+WBBC23xs3rzZO7V+uAAAAAAAAAAQSlYsWqlSpQKvJ7TNR9WqVbP66R7uwQPgryNV9evX16pVq5SUlBTp3QFQCIxbIDoxdoHow7gFog/jNnZZha0FtnXq1Dns9xHa5iO48JkFtoQ/QHSxMcu4BaIL4xaIToxdIPowboHow7iNTYUpEnXpJAAAAAAAAADAFwhtAQAAAAAAAMBHCG3zUaZMGd17773eKYDowLgFog/jFohOjF0g+jBugejDuEUgw7rfAgAAAAAAAAB8gUpbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQtt8jBw5Ug0bNlTZsmXVtWtXzZw5M/x/GQD5+uqrr3TWWWepTp06CgQCGjt2bK7rbW3Fe+65R7Vr11a5cuXUt29fLVq0iEcTiKBhw4bphBNOUMWKFVWzZk0NHDhQCxcuzPU9+/bt04033qhq1aqpQoUKOu+887R+/fqI7TNQ0j333HNq27atkpKSvK179+4aN25c1vWMWcD/HnnkEe/98i233JJ1GWMX8Jf77rvPG6c5txYtWmRdz5gt2Qht8xgzZoxuvfVW3XvvvZozZ47atWunAQMGaMOGDZH5CwHIZffu3d64tIMr+Xn00Uf11FNP6fnnn9eMGTOUmJjojWF7sQMQGVOmTPEC2enTp2vChAk6cOCA+vfv743noD//+c/63//+p3feecf7/jVr1ujcc8/lTwZESL169bzAZ/bs2Zo1a5ZOOeUUnXPOOZo/fz5jFogC3333nUaNGuUdfMmJ11vAf1q3bq21a9dmbd98803WdYzZki2QYWVpyGKVtVYN9Mwzz3hfp6enq379+rr55pt155138kgBPmJHIT/44AOvas/Y05lV4P7lL3/Rbbfd5l22fft2JScna/To0brooosivMcAzMaNG72KWwtne/fu7Y3TGjVq6I033tD555/vfc+CBQvUsmVLTZs2Td26deOBA3ygatWqeuyxx7xxypgF/GvXrl3q2LGjnn32WT300ENq3769RowYwest4NNKW5s9Onfu3EOu4z0yqLTNITU11asmsOnUQXFxcd7X9qERgL8tW7ZM69atyzWGK1Wq5B2MYQwD/mFvQIMBkLHXXqu+zTl2bVpYgwYNGLuAD6Slpemtt97yquOtTQJjFvA3m91yxhln5HpdNYxdwJ+snZ8VHzVu3FiXXnqpVq5c6V3OmEUpHoJsmzZt8t6UWlVeTva1VfwA8DcLbE1+Yzh4HYDIshks1luvZ8+eatOmjXeZjc/SpUurcuXKub6XsQtE1rx587yQ1loMWa9pm93SqlUrrxqIMQv4kx1gsTZ/1h4hL15vAf+xAiObFdq8eXOvNcL999+vE088UT/99BNjFoS2AAAgvNU/9iY0Z68uAP5kHyAtoLXq+HfffVdDhgzx2poA8KdVq1bpT3/6k9c/3hbVBuB/p512WtZ560FtIW5KSorefvttb2FtlGy0R8ihevXqio+PP2S1avu6Vq1a4f7bACii4DhlDAP+dNNNN+njjz/WpEmTvEWOco5da1G0bdu2XN/P6y8QWVZN26RJE3Xq1EnDhg3zFgJ98sknGbOAT9lUaltA2/rZlipVytvsQIst0mvnbQYLr7eAv9nMs2bNmmnx4sW83oLQNu8bU3tTOnHixFzTOO1rmxoGwN8aNWrkvbDlHMM7duzQjBkzGMNABNkigRbY2tTqL7/80hurOdlrb0JCQq6xu3DhQq+fF6+/gH/Y++L9+/czZgGfOvXUU722JlYhH9w6d+7s9cgMnuf1FvD/QoJLlixR7dq1eb0F7RHyuvXWW72pX/aC1qVLF2+VTVt0YejQofy7AD55EbOjjkG2+Ji9CbUFjWzRIuuVaavkNm3a1AuG7r77bq+p+8CBAyO630BJb4nwxhtv6MMPP1TFihWzekzbQoE27ctOr7rqKu812MZyUlKSbr75Zi+w7datW6R3HyiR7rrrLm/Kpr227ty50xvDkydP1meffcaYBXzKXmOD/eKDEhMTVa1atazLeb0F/OW2227TWWed5bVEWLNmje69915vBvjFF1/M6y0IbfMaNGiQNm7cqHvuucf7UNm+fXuNHz/+kIWNAETGrFmzdPLJJ2d9bSGPsYMt1sD99ttv9w60XHvttd5U6169enljmL5eQOQ899xz3ulJJ52U6/KXXnpJV1xxhXf+iSeeUFxcnM477zyvkm/AgAF69tlnI7K/AORNsR48eLC3KIodWLE+exbY9uvXjzELRDFebwF/+e2337yAdvPmzapRo4b3+XX69OneecOYLdkCGTZnEQAAAAAAAADgCyxEBgAAAAAAAAA+QmgLAAAAAAAAAD5CaAsAAAAAAAAAPkJoCwAAAAAAAAA+QmgLAAAAAAAAAD5CaAsAAAAAAAAAPkJoCwAAAAAAAAA+QmgLAAAAAAAAAD5CaAsAAAAUg4YNG2rEiBE8tgAAACgyQlsAAABEvSuuuEIDBw70zp900km65ZZbwnbfo0ePVuXKlQ+5/LvvvtO1114btv0AAABA7CgV6R0AAAAA/Cg1NVWlS5c+6p+vUaNGSPcHAAAAJQeVtgAAAIipitspU6boySefVCAQ8Lbly5d71/3000867bTTVKFCBSUnJ+vyyy/Xpk2bsn7WKnRvuukmr0q3evXqGjBggHf58OHDdfzxxysxMVH169fXH/7wB+3atcu7bvLkyRo6dKi2b9+edX/33Xdfvu0RVq5cqXPOOce7/6SkJF144YVav3591vX2c+3bt9err77q/WylSpV00UUXaefOnVnf8+6773r7Uq5cOVWrVk19+/bV7t27w/DIAgAAIJwIbQEAABAzLKzt3r27rrnmGq1du9bbLGjdtm2bTjnlFHXo0EGzZs3S+PHjvcDUgtOcXn75Za+69ttvv9Xzzz/vXRYXF6ennnpK8+fP967/8ssvdfvtt3vX9ejRwwtmLYQN3t9tt912yH6lp6d7ge2WLVu8UHnChAlaunSpBg0alOv7lixZorFjx+rjjz/2NvveRx55xLvObvviiy/WlVdeqV9++cULjM8991xlZGQU4yMKAACASKA9AgAAAGKGVada6Fq+fHnVqlUr6/JnnnnGC2wffvjhrMtefPFFL9D99ddf1axZM++ypk2b6tFHH811mzn741oF7EMPPaTrr79ezz77rHdfdp9WYZvz/vKaOHGi5s2bp2XLlnn3aV555RW1bt3a6317wgknZIW71iO3YsWK3tdWDWw/+49//MMLbQ8ePOgFtSkpKd71VnULAACA2EOlLQAAAGLeDz/8oEmTJnmtCYJbixYtsqpbgzp16nTIz37xxRc69dRTVbduXS9MtSB18+bN2rNnT6Hv3ypjLawNBramVatW3gJmdl3OUDgY2JratWtrw4YN3vl27dp5+2FB7QUXXKD//Oc/2rp161E8GgAAAPA7QlsAAADEPOtBe9ZZZ2nu3Lm5tkWLFql3795Z32d9a3Oyfrhnnnmm2rZtq/fee0+zZ8/WyJEjsxYqC7WEhIRcX1sFr1Xfmvj4eK+twrhx47zA9+mnn1bz5s296l0AAADEFkJbAAAAxBRrWZCWlpbrso4dO3o9aa2StUmTJrm2vEFtThbSWmj6r3/9S926dfPaKKxZs+aI95dXy5YttWrVKm8L+vnnn71euxbAFpaFuD179tT999+v77//3rvvDz74oNA/DwAAgOhAaAsAAICYYsHsjBkzvCrZTZs2eaHrjTfe6C0CZgt5WQ9Za4nw2WefaejQoYcNXC3UPXDggFfVaguHvfrqq1kLlOW8P6vktd6zdn/5tU3o27ev19bg0ksv1Zw5czRz5kwNHjxYffr0UefOnQv1e9nvZD15bSG1lStX6v3339fGjRu9QBgAAACxhdAWAAAAMeW2227zWglYBWuNGjW8gLNOnTr69ttvvYC2f//+XoBqC4xZT9m4uILfElsf2eHDh+uf//yn2rRpo9dff13Dhg3L9T09evTwFiYbNGiQd395FzILVsh++OGHqlKliteOwULcxo0ba8yYMYX+vZKSkvTVV1/p9NNP9yp+//73v3sVwKeddloRHyEAAAD4XSAjIyMj0jsBAAAAAAAAAHCotAUAAAAAAAAAHyG0BQAAAAAAAAAfIbQFAAAAAAAAAB8htAUAAAAAAAAAHyG0BQAAAAAAAAAfIbQFAAAAAAAAAB8htAUAAAAAAAAAHyG0BQAAAAAAAAAfIbQFAAAAAAAAAB8htAUAAAAAAAAAHyG0BQAAAAAAAAD5x/8DKQ00aD5KeVoAAAAASUVORK5CYII=" - }, - "metadata": {}, - "output_type": "display_data", - "jetTransient": { - "display_id": null - } - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "[指标分析]\n", - " 各NDCG指标在验证集上的最佳值:\n", - " ndcg@10: 0.5462 (迭代 5)\n", - "\n", - "[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!\n" - ] - } - ], - "execution_count": 11 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -1264,12 +767,7 @@ "source": "### 4.6 模型评估" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:39.655028Z", - "start_time": "2026-03-13T18:10:39.403487Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -1310,61 +808,11 @@ " for i, (feature, score) in enumerate(top_features.items(), 1):\n", " print(f\" {i:2d}. {feature:30s} {score:10.2f}\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "模型评估\n", - "================================================================================\n", - "\n", - "生成预测...\n", - "\n", - "计算 NDCG 指标...\n", - "\n", - "NDCG 评估结果:\n", - "----------------------------------------\n", - " ndcg@1: 0.4710\n", - " ndcg@5: 0.4927\n", - " ndcg@10: 0.5003\n", - " ndcg@20: 0.5075\n", - "\n", - "特征重要性(Top 20):\n", - "----------------------------------------\n", - " 1. roe 345.77\n", - " 2. ma_20 244.73\n", - " 3. profit_margin 223.10\n", - " 4. revenue_yoy 216.85\n", - " 5. active_market_cap 214.10\n", - " 6. std_return_20 149.44\n", - " 7. close_vwap_deviation 136.42\n", - " 8. max_ret_20 110.61\n", - " 9. debt_to_equity 105.78\n", - " 10. bbi_ratio 104.63\n", - " 11. market_cap_rank 104.10\n", - " 12. up_days_ratio_20 99.44\n", - " 13. turnover_rank 80.60\n", - " 14. min_ret_20 72.72\n", - " 15. ebit_rank 64.03\n", - " 16. drawdown_from_high_60 61.28\n", - " 17. turnover_rate_mean_5 57.16\n", - " 18. healthy_expansion_velocity 54.10\n", - " 19. BP 51.36\n", - " 20. EP 51.29\n" - ] - } - ], - "execution_count": 12 + "outputs": [], + "execution_count": null }, { - "metadata": { - "ExecuteTime": { - "end_time": "2026-03-13T18:10:39.872615Z", - "start_time": "2026-03-13T18:10:39.663834Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "# 确保输出目录存在\n", @@ -1413,41 +861,8 @@ "\n", "print(\"\\n训练流程完成!\")" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "[1/1] 保存每日 Top 5 股票...\n", - " 保存路径: output\\rank_output.csv\n", - " 保存行数: 1215(243个交易日 x 每日top5)\n", - "\n", - " 预览(前15行):\n", - "shape: (15, 3)\n", - "┌────────────┬──────────┬───────────┐\n", - "│ trade_date ┆ score ┆ ts_code │\n", - "│ --- ┆ --- ┆ --- │\n", - "│ str ┆ f64 ┆ str │\n", - "╞════════════╪══════════╪═══════════╡\n", - "│ 2025-01-02 ┆ 0.022198 ┆ 002480.SZ │\n", - "│ 2025-01-02 ┆ 0.018886 ┆ 000826.SZ │\n", - "│ 2025-01-02 ┆ 0.018845 ┆ 600683.SH │\n", - "│ 2025-01-02 ┆ 0.016044 ┆ 000632.SZ │\n", - "│ 2025-01-02 ┆ 0.014577 ┆ 002072.SZ │\n", - "│ … ┆ … ┆ … │\n", - "│ 2025-01-06 ┆ 0.03183 ┆ 600683.SH │\n", - "│ 2025-01-06 ┆ 0.023015 ┆ 000701.SZ │\n", - "│ 2025-01-06 ┆ 0.018886 ┆ 000826.SZ │\n", - "│ 2025-01-06 ┆ 0.016044 ┆ 000632.SZ │\n", - "│ 2025-01-06 ┆ 0.014577 ┆ 002072.SZ │\n", - "└────────────┴──────────┴───────────┘\n", - "\n", - "训练流程完成!\n" - ] - } - ], - "execution_count": 13 + "outputs": [], + "execution_count": null }, { "metadata": {}, diff --git a/src/experiment/learn_to_rank.py b/src/experiment/learn_to_rank.py index aa3a268..bf85985 100644 --- a/src/experiment/learn_to_rank.py +++ b/src/experiment/learn_to_rank.py @@ -293,6 +293,7 @@ SELECTED_FACTORS = [ "net_profit_yoy", "revenue_yoy", "healthy_expansion_velocity", + "ebit_rank", # ================= 6. 基本面估值与截面动量共振 ================= "EP", "BP", @@ -304,12 +305,11 @@ SELECTED_FACTORS = [ "pe_expansion_trend", "value_price_divergence", "active_market_cap", - "ebit_rank", ] # 因子定义字典(完整因子库) FACTOR_DEFINITIONS = { - "turnover_rate_volatility": "ts_std(log(turnover_rate), 20)" + # "turnover_rate_volatility": "ts_std(log(turnover_rate), 20)" } # Label 因子定义(不参与训练,用于计算目标) @@ -341,7 +341,7 @@ MODEL_PARAMS = { "max_depth": 4, "min_data_in_leaf": 20, "n_estimators": 2000, - "early_stopping_round": 300, + "early_stopping_round": 100, "subsample": 0.8, "colsample_bytree": 0.8, "reg_alpha": 0.1, @@ -372,7 +372,7 @@ def stock_pool_filter(df: pl.DataFrame) -> pl.Series: ) valid_df = df.filter(code_filter) - n = min(1000, len(valid_df)) + n = min(500, len(valid_df)) small_cap_codes = valid_df.sort("total_mv").head(n)["ts_code"] return df["ts_code"].is_in(small_cap_codes) diff --git a/src/training/components/models/lightgbm.py b/src/training/components/models/lightgbm.py index 56638df..d3cf82e 100644 --- a/src/training/components/models/lightgbm.py +++ b/src/training/components/models/lightgbm.py @@ -29,33 +29,14 @@ class LightGBMModel(BaseModel): name = "lightgbm" - def __init__( - self, - params: Optional[dict] = None, - objective: str = "regression", - metric: str = "rmse", - num_leaves: int = 31, - learning_rate: float = 0.05, - n_estimators: int = 100, - **kwargs, - ): + def __init__(self, params: Optional[dict] = None): """初始化 LightGBM 模型 - 支持两种方式传入参数: - 1. 通过 params 字典传入所有参数(推荐方式) - 2. 通过独立参数传入(向后兼容) - Args: - params: LightGBM 参数字典,如果提供则直接使用此字典 - objective: 目标函数,默认 "regression" - metric: 评估指标,默认 "rmse" - num_leaves: 叶子节点数,默认 31 - learning_rate: 学习率,默认 0.05 - n_estimators: 迭代次数,默认 100 - **kwargs: 其他 LightGBM 参数 + params: LightGBM 参数字典,直接传递给 lgb.train()。 + 包含所有模型参数和训练控制参数(如 n_estimators)。 Examples: - >>> # 方式1:通过 params 字典(推荐) >>> model = LightGBMModel(params={ ... "objective": "regression", ... "metric": "rmse", @@ -63,32 +44,8 @@ class LightGBMModel(BaseModel): ... "learning_rate": 0.05, ... "n_estimators": 100, ... }) - >>> - >>> # 方式2:通过独立参数(向后兼容) - >>> model = LightGBMModel( - ... objective="regression", - ... num_leaves=31, - ... learning_rate=0.05, - ... ) """ - if params is not None: - # 方式1:直接使用 params 字典 - self.params = dict(params) # 复制一份,避免修改原始字典 - self.params.setdefault("verbose", -1) # 默认抑制训练输出 - # n_estimators 可能存在于 params 中 - self.n_estimators = self.params.pop("n_estimators", n_estimators) - else: - # 方式2:通过独立参数构建 params - self.params = { - "objective": objective, - "metric": metric, - "num_leaves": num_leaves, - "learning_rate": learning_rate, - "verbose": -1, # 抑制训练输出 - **kwargs, - } - self.n_estimators = n_estimators - + self.params = dict(params) if params is not None else {} self.model = None self.feature_names_: Optional[list] = None @@ -113,21 +70,19 @@ class LightGBMModel(BaseModel): "使用 LightGBMModel 需要安装 lightgbm: pip install lightgbm" ) - # 保存特征名称 self.feature_names_ = X.columns - - # 转换为 numpy X_np = X.to_numpy() y_np = y.to_numpy() - # 创建数据集 train_data = lgb.Dataset(X_np, label=y_np) - # 训练 + # 从 params 中提取 num_boost_round,默认 100 + num_boost_round = self.params.pop("n_estimators", 100) + self.model = lgb.train( self.params, train_data, - num_boost_round=self.n_estimators, + num_boost_round=num_boost_round, ) return self @@ -148,7 +103,8 @@ class LightGBMModel(BaseModel): raise RuntimeError("模型尚未训练,请先调用 fit()") X_np = X.to_numpy() - return self.model.predict(X_np) + result = self.model.predict(X_np) + return np.asarray(result) def feature_importance(self) -> Optional[pd.Series]: """返回特征重要性 @@ -179,7 +135,6 @@ class LightGBMModel(BaseModel): self.model.save_model(path) - # 同时保存特征名称(LightGBM 原生格式不保存这个) import json meta_path = path + ".meta.json" @@ -188,7 +143,6 @@ class LightGBMModel(BaseModel): { "feature_names": self.feature_names_, "params": self.params, - "n_estimators": self.n_estimators, }, f, ) @@ -211,16 +165,13 @@ class LightGBMModel(BaseModel): instance = cls() instance.model = lgb.Booster(model_file=path) - # 加载元数据 meta_path = path + ".meta.json" try: with open(meta_path, "r") as f: meta = json.load(f) instance.feature_names_ = meta.get("feature_names") - instance.params = meta.get("params", instance.params) - instance.n_estimators = meta.get("n_estimators", instance.n_estimators) + instance.params = meta.get("params", {}) except FileNotFoundError: - # 如果没有元数据文件,继续运行(feature_names_ 为 None) pass return instance diff --git a/src/training/components/models/lightgbm_lambdarank.py b/src/training/components/models/lightgbm_lambdarank.py index 01fa5d1..38f6851 100644 --- a/src/training/components/models/lightgbm_lambdarank.py +++ b/src/training/components/models/lightgbm_lambdarank.py @@ -30,35 +30,14 @@ class LightGBMLambdaRankModel(BaseModel): name = "lightgbm_lambdarank" - def __init__( - self, - params: Optional[dict] = None, - learning_rate: float = 0.05, - num_leaves: int = 31, - n_estimators: int = 100, - min_data_in_leaf: int = 20, - ndcg_at: Optional[List[int]] = None, - early_stopping_rounds: int = 50, - **kwargs, - ): + def __init__(self, params: Optional[dict] = None): """初始化 LambdaRank 模型 - 支持两种方式传入参数: - 1. 通过 params 字典传入所有参数(推荐方式) - 2. 通过独立参数传入(向后兼容) - Args: - params: LightGBM 参数字典,如果提供则直接使用此字典 - learning_rate: 学习率,默认 0.05 - num_leaves: 叶子节点数,默认 31 - n_estimators: 迭代次数,默认 100 - min_data_in_leaf: 叶子最小样本数,默认 20 - ndcg_at: NDCG 评估的 k 值列表,默认 [1, 5, 10, 20] - early_stopping_rounds: 早停轮数,默认 50 - **kwargs: 其他 LightGBM 参数 + params: LightGBM 参数字典,直接传递给 lgb.train()。 + 包含所有模型参数和训练控制参数。 Examples: - >>> # 方式1:通过 params 字典(推荐) >>> model = LightGBMLambdaRankModel(params={ ... "objective": "lambdarank", ... "metric": "ndcg", @@ -66,39 +45,13 @@ class LightGBMLambdaRankModel(BaseModel): ... "num_leaves": 31, ... "learning_rate": 0.05, ... "n_estimators": 1000, + ... "early_stopping_round": 50, ... }) """ - if ndcg_at is None: - ndcg_at = [1, 5, 10, 20] - - if params is not None: - # 方式1:直接使用 params 字典 - self.params = dict(params) # 复制一份,避免修改原始字典 - self.params.setdefault("objective", "lambdarank") - self.params.setdefault("metric", "ndcg") - self.params.setdefault("verbose", -1) - self.n_estimators = self.params.pop("n_estimators", n_estimators) - self.early_stopping_rounds = self.params.pop( - "early_stopping_rounds", early_stopping_rounds - ) - else: - # 方式2:通过独立参数构建 params - self.params = { - "objective": "lambdarank", - "metric": "ndcg", - "ndcg_at": ndcg_at, - "num_leaves": num_leaves, - "learning_rate": learning_rate, - "min_data_in_leaf": min_data_in_leaf, - "verbose": -1, - **kwargs, - } - self.n_estimators = n_estimators - self.early_stopping_rounds = early_stopping_rounds - + self.params = dict(params) if params is not None else {} self.model = None self.feature_names_: Optional[list] = None - self.evals_result_: Optional[dict] = None # 存储训练评估结果 + self.evals_result_: Optional[dict] = None def fit( self, @@ -112,10 +65,7 @@ class LightGBMLambdaRankModel(BaseModel): Args: X: 特征矩阵 (Polars DataFrame) y: 目标变量 (Polars Series),应为分位数标签 (0, 1, 2, ...) - group: 分组数组,表示每个 query 的样本数。 - 例如 [10, 15, 20] 表示第一个 query 有 10 个样本, - 第二个 query 有 15 个样本,第三个 query 有 20 个样本。 - 如果为 None,则假设所有样本属于同一个 query。 + group: 分组数组,表示每个 query 的样本数 eval_set: 验证集元组 (X_val, y_val, group_val),用于早停 Returns: @@ -133,19 +83,13 @@ class LightGBMLambdaRankModel(BaseModel): "使用 LightGBMLambdaRankModel 需要安装 lightgbm: pip install lightgbm" ) - # 保存特征名称 self.feature_names_ = X.columns - - # 转换为 numpy X_np = X.to_numpy() y_np = y.to_numpy() - # 处理 group 参数 if group is None: - # 如果未提供 group,假设所有样本属于同一个 query group = np.array([len(y_np)]) - # 验证 group 参数 if not isinstance(group, np.ndarray): group = np.array(group) if group.sum() != len(y_np): @@ -153,10 +97,8 @@ class LightGBMLambdaRankModel(BaseModel): f"group 数组的和 ({group.sum()}) 必须等于样本数 ({len(y_np)})" ) - # 创建训练数据集 train_data = lgb.Dataset(X_np, label=y_np, group=group) - # 准备验证集和验证集名称 valid_sets = [train_data] valid_names = ["train"] if eval_set is not None: @@ -173,19 +115,22 @@ class LightGBMLambdaRankModel(BaseModel): valid_sets.append(val_data) valid_names.append("val") - # 初始化评估结果存储 self.evals_result_ = {} - # 训练 + # 从 params 提取训练控制参数 + params_copy = dict(self.params) + num_boost_round = params_copy.pop("n_estimators", 100) + early_stopping_round = params_copy.pop("early_stopping_round", 50) + callbacks = [ - lgb.early_stopping(stopping_rounds=self.early_stopping_rounds), + lgb.early_stopping(stopping_rounds=early_stopping_round), lgb.record_evaluation(self.evals_result_), ] self.model = lgb.train( - self.params, + params_copy, train_data, - num_boost_round=self.n_estimators, + num_boost_round=num_boost_round, valid_sets=valid_sets, valid_names=valid_names, callbacks=callbacks, @@ -200,7 +145,7 @@ class LightGBMLambdaRankModel(BaseModel): X: 特征矩阵 (Polars DataFrame) Returns: - 预测分数 (numpy ndarray),分数越高表示排序越靠前 + 预测分数 (numpy ndarray) Raises: RuntimeError: 模型未训练时调用 @@ -209,18 +154,37 @@ class LightGBMLambdaRankModel(BaseModel): raise RuntimeError("模型尚未训练,请先调用 fit()") X_np = X.to_numpy() - return self.model.predict(X_np) + result = self.model.predict(X_np) + return np.asarray(result) def get_evals_result(self) -> Optional[dict]: """获取训练评估结果 Returns: - 评估结果字典,包含训练集和验证集的指标历史 - 格式: {'train': {'metric_name': [...]}, 'val': {'metric_name': [...]}} - 如果模型尚未训练,返回 None + 评估结果字典,如果模型尚未训练返回 None """ return self.evals_result_ + def get_best_iteration(self) -> Optional[int]: + """获取最佳迭代轮数(考虑早停) + + Returns: + 最佳迭代轮数,如果模型未训练返回 None + """ + if self.model is None: + return None + return self.model.best_iteration + + def get_best_score(self) -> Optional[dict]: + """获取最佳评分 + + Returns: + 最佳评分字典,如果模型未训练返回 None + """ + if self.model is None: + return None + return self.model.best_score + def plot_metric( self, metric: Optional[str] = None, @@ -230,25 +194,14 @@ class LightGBMLambdaRankModel(BaseModel): ): """绘制训练指标曲线 - 使用 LightGBM 原生的 plot_metric 接口绘制训练曲线。 - Args: - metric: 要绘制的指标名称,如 'ndcg@5'、'ndcg@10' 等。 - 如果为 None,则自动选择第一个可用的 NDCG 指标。 + metric: 要绘制的指标名称,如 'ndcg@5' figsize: 图大小,默认 (10, 6) - title: 图表标题,如果为 None 则自动生成 - ax: matplotlib Axes 对象,如果为 None 则创建新图 + title: 图表标题 + ax: matplotlib Axes 对象 Returns: matplotlib Axes 对象 - - Raises: - RuntimeError: 模型尚未训练 - ValueError: 指定的指标不存在 - - Examples: - >>> model.plot_metric('ndcg@20') # 绘制 ndcg@20 曲线 - >>> model.plot_metric() # 自动选择指标 """ if self.model is None: raise RuntimeError("模型尚未训练,请先调用 fit()") @@ -259,7 +212,6 @@ class LightGBMLambdaRankModel(BaseModel): import lightgbm as lgb import matplotlib.pyplot as plt - # 如果没有指定指标,自动选择第一个 NDCG 指标 if metric is None: available_metrics = list(self.evals_result_.get("train", {}).keys()) ndcg_metrics = [m for m in available_metrics if "ndcg" in m.lower()] @@ -270,20 +222,17 @@ class LightGBMLambdaRankModel(BaseModel): else: raise ValueError("没有可用的评估指标") - # 检查指标是否存在 if metric not in self.evals_result_.get("train", {}): available = list(self.evals_result_.get("train", {}).keys()) raise ValueError(f"指标 '{metric}' 不存在。可用的指标: {available}") - # 创建图表 if ax is None: - fig, ax = plt.subplots(figsize=figsize) + _, ax = plt.subplots(figsize=figsize) - # 使用 LightGBM 原生接口绘制 lgb.plot_metric(self.evals_result_, metric=metric, ax=ax) - # 设置标题 if title is None: + assert metric is not None title = f"Training Metric ({metric.upper()}) over Iterations" ax.set_title(title, fontsize=12, fontweight="bold") @@ -297,18 +246,13 @@ class LightGBMLambdaRankModel(BaseModel): ): """绘制所有训练指标曲线 - 在一个图表中绘制多个指标的训练曲线。 - Args: - metrics: 要绘制的指标列表,如果为 None 则绘制所有 NDCG 指标 + metrics: 要绘制的指标列表 figsize: 图大小,默认 (14, 10) max_cols: 每行最多显示的子图数,默认 2 Returns: matplotlib Figure 对象 - - Raises: - RuntimeError: 模型尚未训练 """ if self.model is None: raise RuntimeError("模型尚未训练,请先调用 fit()") @@ -321,7 +265,6 @@ class LightGBMLambdaRankModel(BaseModel): available_metrics = list(self.evals_result_.get("train", {}).keys()) - # 如果没有指定指标,使用所有 NDCG 指标(最多 4 个) if metrics is None: ndcg_metrics = [m for m in available_metrics if "ndcg" in m.lower()] metrics = ndcg_metrics[:4] if ndcg_metrics else available_metrics[:4] @@ -329,7 +272,6 @@ class LightGBMLambdaRankModel(BaseModel): if not metrics: raise ValueError("没有可用的评估指标") - # 计算子图布局 n_metrics = len(metrics) n_cols = min(max_cols, n_metrics) n_rows = (n_metrics + n_cols - 1) // n_cols @@ -362,34 +304,12 @@ class LightGBMLambdaRankModel(BaseModel): transform=ax.transAxes, ) - # 隐藏多余的子图 for idx in range(n_metrics, len(axes)): axes[idx].axis("off") plt.tight_layout() return fig - def get_best_iteration(self) -> Optional[int]: - """获取最佳迭代轮数(考虑早停) - - Returns: - 最佳迭代轮数,如果模型未训练返回 None - """ - if self.model is None: - return None - return self.model.best_iteration - - def get_best_score(self) -> Optional[dict]: - """获取最佳评分 - - Returns: - 最佳评分字典,格式: {'valid_0': {'metric': value}, 'valid_1': {...}} - 如果模型未训练返回 None - """ - if self.model is None: - return None - return self.model.best_score - def feature_importance(self) -> Optional[pd.Series]: """返回特征重要性 @@ -405,9 +325,6 @@ class LightGBMLambdaRankModel(BaseModel): def save(self, path: str) -> None: """保存模型(使用 LightGBM 原生格式) - 使用 LightGBM 的原生格式保存,不依赖 pickle, - 可以在不同环境中加载。 - Args: path: 保存路径 @@ -419,7 +336,6 @@ class LightGBMLambdaRankModel(BaseModel): self.model.save_model(path) - # 同时保存特征名称和其他元数据 import json meta_path = path + ".meta.json" @@ -428,8 +344,6 @@ class LightGBMLambdaRankModel(BaseModel): { "feature_names": self.feature_names_, "params": self.params, - "n_estimators": self.n_estimators, - "early_stopping_rounds": self.early_stopping_rounds, }, f, ) @@ -438,8 +352,6 @@ class LightGBMLambdaRankModel(BaseModel): def load(cls, path: str) -> "LightGBMLambdaRankModel": """加载模型 - 从 LightGBM 原生格式加载模型。 - Args: path: 模型文件路径 @@ -452,19 +364,13 @@ class LightGBMLambdaRankModel(BaseModel): instance = cls() instance.model = lgb.Booster(model_file=path) - # 加载元数据 meta_path = path + ".meta.json" try: with open(meta_path, "r") as f: meta = json.load(f) instance.feature_names_ = meta.get("feature_names") - instance.params = meta.get("params", instance.params) - instance.n_estimators = meta.get("n_estimators", instance.n_estimators) - instance.early_stopping_rounds = meta.get( - "early_stopping_rounds", instance.early_stopping_rounds - ) + instance.params = meta.get("params", {}) except FileNotFoundError: - # 如果没有元数据文件,继续运行(feature_names_ 为 None) pass return instance @@ -476,24 +382,13 @@ class LightGBMLambdaRankModel(BaseModel): ) -> np.ndarray: """从日期列生成 group 数组 - 将数据按日期分组,每个日期作为一个 query。 - Args: df: 包含日期列的 DataFrame date_col: 日期列名,默认 "trade_date" Returns: - group 数组,表示每个日期的样本数 - - Example: - >>> df = pl.DataFrame({ - ... "trade_date": ["20240101", "20240101", "20240102", "20240102", "20240102"], - ... "feature": [1, 2, 3, 4, 5] - ... }) - >>> group = LightGBMLambdaRankModel.prepare_group_from_dates(df) - >>> print(group) # array([2, 3]) + group 数组 """ - # 按日期统计样本数 group_counts = df.group_by(date_col, maintain_order=True).agg( pl.count().alias("count") ) @@ -509,35 +404,19 @@ class LightGBMLambdaRankModel(BaseModel): ) -> pl.DataFrame: """将连续标签转换为分位数标签 - 对每个日期的数据分别进行分位数划分,生成 0, 1, 2, ..., n_quantiles-1 的标签。 - 值越大表示原始值越大(排序越靠前)。 - Args: df: 输入 DataFrame - label_col: 原始标签列名(如 "future_return_5") + label_col: 原始标签列名 date_col: 日期列名,默认 "trade_date" n_quantiles: 分位数数量,默认 20 - new_col_name: 新列名,默认在原始列名后加 "_rank" + new_col_name: 新列名 Returns: 添加了分位数标签列的 DataFrame - - Example: - >>> df = pl.DataFrame({ - ... "trade_date": ["20240101"] * 5 + ["20240102"] * 5, - ... "future_return_5": [0.01, 0.02, 0.03, 0.04, 0.05, - ... 0.02, 0.03, 0.04, 0.05, 0.06] - ... }) - >>> df = LightGBMLambdaRankModel.convert_to_quantile_labels( - ... df, "future_return_5", n_quantiles=5 - ... ) - >>> print(df["future_return_5_rank"]) # [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] """ if new_col_name is None: new_col_name = f"{label_col}_rank" - # 使用 qcut 按日期分组进行分位数划分 - # qcut 返回的是 Categorical,使用 to_physical() 转换为整数(0, 1, 2, ...) return df.with_columns( pl.col(label_col) .qcut(n_quantiles) @@ -560,17 +439,15 @@ class LightGBMLambdaRankModel(BaseModel): X: 特征矩阵 y: 真实标签 group: 分组数组 - k: NDCG@k 的 k 值,None 表示使用所有位置 + k: NDCG@k 的 k 值 Returns: NDCG 分数 """ from sklearn.metrics import ndcg_score - # 获取预测分数 y_pred = self.predict(X) - # 将数据按 group 拆分 y_true_list = [] y_score_list = [] @@ -581,15 +458,13 @@ class LightGBMLambdaRankModel(BaseModel): y_score_list.append(y_pred[start_idx:end_idx]) start_idx = end_idx - # 计算平均 NDCG ndcg_scores = [] for y_true, y_score in zip(y_true_list, y_score_list): - if len(y_true) > 1: # 至少要有 2 个样本才能计算 NDCG + if len(y_true) > 1: try: score = ndcg_score([y_true], [y_score], k=k) ndcg_scores.append(score) except ValueError: - # 如果标签都相同,跳过 pass - return np.mean(ndcg_scores) if ndcg_scores else 0.0 + return float(np.mean(ndcg_scores)) if ndcg_scores else 0.0