From 3f8ca2cebfa05af0c4ae93cc10b2203175af7099 Mon Sep 17 00:00:00 2001 From: liaozhaorun <1300336796@qq.com> Date: Fri, 13 Mar 2026 22:24:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(training):=20=E6=B7=BB=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=B4=A8=E9=87=8F=E6=A3=80=E6=9F=A5=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=B9=B6=E9=87=8D=E6=9E=84=E5=AE=9E=E9=AA=8C=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=20-=20=E6=96=B0=E5=A2=9E=20check=5Fdata=5Fquality=20=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=94=A8=E4=BA=8E=E6=A3=80=E6=B5=8B=E5=85=A8=E7=A9=BA?= =?UTF-8?q?/=E5=85=A8=E9=9B=B6/=E5=85=A8NaN=E6=95=B0=E6=8D=AE=E8=B4=A8?= =?UTF-8?q?=E9=87=8F=E9=97=AE=E9=A2=98=20-=20=E9=87=8D=E6=9E=84=20register?= =?UTF-8?q?=5Ffactors=20=E5=87=BD=E6=95=B0=EF=BC=8C=E6=B6=88=E9=99=A4=20FE?= =?UTF-8?q?ATURE=5FCOLS=20=E5=92=8C=20PROCESSORS=20=E5=86=97=E4=BD=99?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=20-=20=E4=BF=AE=E5=A4=8D=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=B8=AD=E7=89=B9=E5=BE=81=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=A4=84=E7=90=86=E5=99=A8=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E6=89=80=E6=9C=89=E7=89=B9=E5=BE=81=20-=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20LambdaRank=20=E6=A8=A1=E5=9E=8B=E5=8F=82=E6=95=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/experiment/learn_to_rank.ipynb | 836 ++++++++++++++---- src/experiment/learn_to_rank.py | 195 ++-- src/experiment/regression.py | 40 +- src/training/__init__.py | 5 + .../components/models/lightgbm_lambdarank.py | 193 +++- src/training/utils.py | 171 ++++ 6 files changed, 1135 insertions(+), 305 deletions(-) create mode 100644 src/training/utils.py diff --git a/src/experiment/learn_to_rank.ipynb b/src/experiment/learn_to_rank.ipynb index 983aada..459a5ac 100644 --- a/src/experiment/learn_to_rank.ipynb +++ b/src/experiment/learn_to_rank.ipynb @@ -24,8 +24,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-11T16:02:49.975545Z", - "start_time": "2026-03-11T16:02:48.487347Z" + "end_time": "2026-03-13T14:14:03.291580Z", + "start_time": "2026-03-13T14:14:01.936601Z" } }, "cell_type": "code", @@ -49,6 +49,7 @@ " Winsorizer,\n", " NullFiller,\n", " StandardScaler,\n", + " check_data_quality,\n", ")\n", "from src.training.components.models import LightGBMLambdaRankModel\n", "from src.training.config import TrainingConfig\n", @@ -65,8 +66,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-11T16:02:49.989220Z", - "start_time": "2026-03-11T16:02:49.981542Z" + "end_time": "2026-03-13T14:14:03.306725Z", + "start_time": "2026-03-13T14:14:03.299251Z" } }, "cell_type": "code", @@ -286,8 +287,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-11T16:02:50.000875Z", - "start_time": "2026-03-11T16:02:49.994082Z" + "end_time": "2026-03-13T14:14:03.317598Z", + "start_time": "2026-03-13T14:14:03.314983Z" } }, "cell_type": "code", @@ -355,7 +356,9 @@ "]\n", "\n", "# 因子定义字典(完整因子库)\n", - "FACTOR_DEFINITIONS = {\"turnover_volatility_ratio\": \"log(ts_std(turnover_rate, 20))\"}\n", + "FACTOR_DEFINITIONS = {\n", + " # \"turnover_rate_volatility\": \"ts_std(turnover_rate, 20)\"\n", + "}\n", "\n", "# Label 因子定义(不参与训练,用于计算目标)\n", "LABEL_FACTOR = {\n", @@ -373,8 +376,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-11T16:02:50.009081Z", - "start_time": "2026-03-11T16:02:50.005330Z" + "end_time": "2026-03-13T14:14:03.327552Z", + "start_time": "2026-03-13T14:14:03.321870Z" } }, "cell_type": "code", @@ -387,27 +390,31 @@ "TEST_START = \"20250101\"\n", "TEST_END = \"20251231\"\n", "\n", + "\n", + "# 分位数配置\n", + "N_QUANTILES = 20 # 将 label 分为 20 组\n", + "\n", "# LambdaRank 模型参数配置\n", "MODEL_PARAMS = {\n", " \"objective\": \"lambdarank\",\n", " \"metric\": \"ndcg\",\n", - " \"ndcg_at\": [1, 5, 10, 20], # 评估 NDCG@k\n", - " \"learning_rate\": 0.05,\n", + " \"ndcg_at\": 10, # 评估 NDCG@k\n", + " \"learning_rate\": 0.01,\n", " \"num_leaves\": 31,\n", - " \"max_depth\": 6,\n", + " \"max_depth\": 4,\n", " \"min_data_in_leaf\": 20,\n", - " \"n_estimators\": 1000,\n", - " \"early_stopping_rounds\": 50,\n", + " \"n_estimators\": 2000,\n", + " \"early_stopping_round\": 300,\n", " \"subsample\": 0.8,\n", " \"colsample_bytree\": 0.8,\n", " \"reg_alpha\": 0.1,\n", " \"reg_lambda\": 1.0,\n", " \"verbose\": -1,\n", " \"random_state\": 42,\n", + " \"lambdarank_truncation_level\": 10,\n", + " \"label_gain\": [i for i in range(1, N_QUANTILES + 1)],\n", "}\n", "\n", - "# 分位数配置\n", - "N_QUANTILES = 20 # 将 label 分为 20 组\n", "\n", "# 特征列(用于数据处理器)\n", "FEATURE_COLS = SELECTED_FACTORS\n", @@ -466,8 +473,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-11T16:02:50.330018Z", - "start_time": "2026-03-11T16:02:50.012964Z" + "end_time": "2026-03-13T14:14:25.239464Z", + "start_time": "2026-03-13T14:14:03.331272Z" } }, "cell_type": "code", @@ -478,7 +485,7 @@ "\n", "# 1. 创建 FactorEngine(启用 metadata 功能)\n", "print(\"\\n[1] 创建 FactorEngine\")\n", - "engine = FactorEngine(metadata_path=\"data/factors.jsonl\")\n", + "engine = FactorEngine()\n", "\n", "# 2. 使用 metadata 定义因子\n", "print(\"\\n[2] 定义因子(从 metadata 注册)\")\n", @@ -511,7 +518,7 @@ "print(f\"[配置] 目标变量: {target_col}({N_QUANTILES}分位数)\")\n", "\n", "# 6. 创建排序学习模型\n", - "model = LightGBMLambdaRankModel(params=MODEL_PARAMS)\n", + "model: LightGBMLambdaRankModel = LightGBMLambdaRankModel(params=MODEL_PARAMS)\n", "\n", "# 7. 创建数据处理器\n", "processors = PROCESSORS\n", @@ -565,27 +572,204 @@ "注册因子\n", "================================================================================\n", "\n", - "注册特征因子(从 metadata):\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", + "\n", + "注册 Label 因子(表达式):\n", + " - future_return_5_rank: (ts_delay(close, -5) / ts_delay(open, -1)) - 1\n", + "\n", + "特征因子数: 49\n", + " - 来自 metadata: 49\n", + " - 来自表达式: 0\n", + "Label: future_return_5_rank\n", + "已注册因子总数: 50\n", + "\n", + "[3] 准备数据\n", + "\n", + "================================================================================\n", + "准备数据\n", + "================================================================================\n", + "\n", + "计算因子: 20200101 - 20251231\n", + "[FinancialLoader] 加载 financial_fina_indicator 失败: Binder Error: Referenced column \"f_ann_date\" not found in FROM clause!\n", + "Candidate bindings: \"ann_date\", \"end_date\", \"ocf_to_debt\", \"arturn_days\", \"nca_to_assets\"\n" ] }, { - "ename": "QueryError", - "evalue": "查询执行失败: Binder Error: Referenced column \"name\" not found in FROM clause!\nCandidate bindings: \"json\"\n\nLINE 4: WHERE name = 'ma_5'\n ^\nSQL: \n SELECT *\n FROM read_json_auto('D:\\PyProject\\ProStock\\src\\experiment\\data\\factors.jsonl')\n WHERE name = 'ma_5'\n ", - "output_type": "error", - "traceback": [ - "\u001B[31m---------------------------------------------------------------------------\u001B[39m", - "\u001B[31mBinderException\u001B[39m Traceback (most recent call last)", - "\u001B[36mFile \u001B[39m\u001B[32mD:\\PyProject\\ProStock\\src\\factors\\metadata\\manager.py:296\u001B[39m, in \u001B[36mFactorManager._execute_query\u001B[39m\u001B[34m(self, sql)\u001B[39m\n\u001B[32m 295\u001B[39m conn = \u001B[38;5;28mself\u001B[39m._get_connection()\n\u001B[32m--> \u001B[39m\u001B[32m296\u001B[39m result = \u001B[43mconn\u001B[49m\u001B[43m.\u001B[49m\u001B[43mexecute\u001B[49m\u001B[43m(\u001B[49m\u001B[43msql\u001B[49m\u001B[43m)\u001B[49m.pl()\n\u001B[32m 297\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m result\n", - "\u001B[31mBinderException\u001B[39m: Binder Error: Referenced column \"name\" not found in FROM clause!\nCandidate bindings: \"json\"\n\nLINE 4: WHERE name = 'ma_5'\n ^", - "\nDuring handling of the above exception, another exception occurred:\n", - "\u001B[31mQueryError\u001B[39m Traceback (most recent call last)", - "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[5]\u001B[39m\u001B[32m, line 11\u001B[39m\n\u001B[32m 9\u001B[39m \u001B[38;5;66;03m# 2. 使用 metadata 定义因子\u001B[39;00m\n\u001B[32m 10\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[33m[2] 定义因子(从 metadata 注册)\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m---> \u001B[39m\u001B[32m11\u001B[39m feature_cols = \u001B[43mcreate_factors_with_metadata\u001B[49m\u001B[43m(\u001B[49m\n\u001B[32m 12\u001B[39m \u001B[43m \u001B[49m\u001B[43mengine\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mSELECTED_FACTORS\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mFACTOR_DEFINITIONS\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mLABEL_FACTOR\u001B[49m\n\u001B[32m 13\u001B[39m \u001B[43m)\u001B[49m\n\u001B[32m 15\u001B[39m \u001B[38;5;66;03m# 3. 准备数据\u001B[39;00m\n\u001B[32m 16\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[33m[3] 准备数据\u001B[39m\u001B[33m\"\u001B[39m)\n", - "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[2]\u001B[39m\u001B[32m, line 15\u001B[39m, in \u001B[36mcreate_factors_with_metadata\u001B[39m\u001B[34m(engine, selected_factors, factor_definitions, label_factor)\u001B[39m\n\u001B[32m 13\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[33m注册特征因子(从 metadata):\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m 14\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m name \u001B[38;5;129;01min\u001B[39;00m selected_factors:\n\u001B[32m---> \u001B[39m\u001B[32m15\u001B[39m \u001B[43mengine\u001B[49m\u001B[43m.\u001B[49m\u001B[43madd_factor\u001B[49m\u001B[43m(\u001B[49m\u001B[43mname\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 16\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33mf\u001B[39m\u001B[33m\"\u001B[39m\u001B[33m - \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mname\u001B[38;5;132;01m}\u001B[39;00m\u001B[33m\"\u001B[39m)\n\u001B[32m 18\u001B[39m \u001B[38;5;66;03m# 注册 FACTOR_DEFINITIONS 中的因子(通过表达式,尚未在 metadata 中)\u001B[39;00m\n", - "\u001B[36mFile \u001B[39m\u001B[32mD:\\PyProject\\ProStock\\src\\factors\\engine\\factor_engine.py:225\u001B[39m, in \u001B[36mFactorEngine.add_factor\u001B[39m\u001B[34m(self, name, expression, data_specs)\u001B[39m\n\u001B[32m 182\u001B[39m \u001B[38;5;250m\u001B[39m\u001B[33;03m\"\"\"注册因子(支持多种调用方式)。\u001B[39;00m\n\u001B[32m 183\u001B[39m \n\u001B[32m 184\u001B[39m \u001B[33;03m这是 register 方法的增强版,支持以下调用方式:\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 221\u001B[39m \u001B[33;03m ... .add_factor(\"golden_cross\", \"ma5 > ma10\"))\u001B[39;00m\n\u001B[32m 222\u001B[39m \u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 223\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m expression \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;28;01mNone\u001B[39;00m:\n\u001B[32m 224\u001B[39m \u001B[38;5;66;03m# 从 metadata 查询表达式\u001B[39;00m\n\u001B[32m--> \u001B[39m\u001B[32m225\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_add_factor_from_metadata\u001B[49m\u001B[43m(\u001B[49m\u001B[43mname\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mname\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mdata_specs\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 227\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28misinstance\u001B[39m(expression, \u001B[38;5;28mstr\u001B[39m):\n\u001B[32m 228\u001B[39m \u001B[38;5;66;03m# Fail-Fast:立即解析,失败立即报错\u001B[39;00m\n\u001B[32m 229\u001B[39m node = \u001B[38;5;28mself\u001B[39m._parser.parse(expression)\n", - "\u001B[36mFile \u001B[39m\u001B[32mD:\\PyProject\\ProStock\\src\\factors\\engine\\factor_engine.py:159\u001B[39m, in \u001B[36mFactorEngine._add_factor_from_metadata\u001B[39m\u001B[34m(self, name, factor_name_in_metadata, data_specs)\u001B[39m\n\u001B[32m 153\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mRuntimeError\u001B[39;00m(\n\u001B[32m 154\u001B[39m \u001B[33m\"\u001B[39m\u001B[33m引擎未配置 metadata 路径。请在初始化时传入 metadata_path 参数,\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 155\u001B[39m + \u001B[33m\"\u001B[39m\u001B[33m例如:FactorEngine(metadata_path=\u001B[39m\u001B[33m'\u001B[39m\u001B[33mdata/factors.jsonl\u001B[39m\u001B[33m'\u001B[39m\u001B[33m)\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 156\u001B[39m )\n\u001B[32m 158\u001B[39m \u001B[38;5;66;03m# 从 metadata 查询因子\u001B[39;00m\n\u001B[32m--> \u001B[39m\u001B[32m159\u001B[39m df = \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_metadata\u001B[49m\u001B[43m.\u001B[49m\u001B[43mget_factors_by_name\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfactor_name_in_metadata\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 161\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mlen\u001B[39m(df) == \u001B[32m0\u001B[39m:\n\u001B[32m 162\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\n\u001B[32m 163\u001B[39m \u001B[33mf\u001B[39m\u001B[33m\"\u001B[39m\u001B[33m在 metadata 中未找到因子 \u001B[39m\u001B[33m'\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mfactor_name_in_metadata\u001B[38;5;132;01m}\u001B[39;00m\u001B[33m'\u001B[39m\u001B[33m。\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 164\u001B[39m + \u001B[33m\"\u001B[39m\u001B[33m请确认因子名称正确,或先使用 FactorManager 添加该因子。\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 165\u001B[39m )\n", - "\u001B[36mFile \u001B[39m\u001B[32mD:\\PyProject\\ProStock\\src\\factors\\metadata\\manager.py:177\u001B[39m, in \u001B[36mFactorManager.get_factors_by_name\u001B[39m\u001B[34m(self, name)\u001B[39m\n\u001B[32m 154\u001B[39m \u001B[38;5;250m\u001B[39m\u001B[33;03m\"\"\"根据名称查询因子。\u001B[39;00m\n\u001B[32m 155\u001B[39m \n\u001B[32m 156\u001B[39m \u001B[33;03m使用DuckDB执行SQL查询,返回Polars DataFrame。\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 170\u001B[39m \u001B[33;03m ... print(df[\"dsl\"][0])\u001B[39;00m\n\u001B[32m 171\u001B[39m \u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 172\u001B[39m sql = \u001B[33mf\u001B[39m\u001B[33m\"\"\"\u001B[39m\n\u001B[32m 173\u001B[39m \u001B[33m SELECT *\u001B[39m\n\u001B[32m 174\u001B[39m \u001B[33m FROM read_json_auto(\u001B[39m\u001B[33m'\u001B[39m\u001B[38;5;132;01m{\u001B[39;00m\u001B[38;5;28mself\u001B[39m.filepath\u001B[38;5;132;01m}\u001B[39;00m\u001B[33m'\u001B[39m\u001B[33m)\u001B[39m\n\u001B[32m 175\u001B[39m \u001B[33m WHERE name = \u001B[39m\u001B[33m'\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mname\u001B[38;5;132;01m}\u001B[39;00m\u001B[33m'\u001B[39m\n\u001B[32m 176\u001B[39m \u001B[33m\u001B[39m\u001B[33m\"\"\"\u001B[39m\n\u001B[32m--> \u001B[39m\u001B[32m177\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_execute_query\u001B[49m\u001B[43m(\u001B[49m\u001B[43msql\u001B[49m\u001B[43m)\u001B[49m\n", - "\u001B[36mFile \u001B[39m\u001B[32mD:\\PyProject\\ProStock\\src\\factors\\metadata\\manager.py:299\u001B[39m, in \u001B[36mFactorManager._execute_query\u001B[39m\u001B[34m(self, sql)\u001B[39m\n\u001B[32m 297\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m result\n\u001B[32m 298\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m \u001B[38;5;167;01mException\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m e:\n\u001B[32m--> \u001B[39m\u001B[32m299\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m QueryError(sql, e)\n", - "\u001B[31mQueryError\u001B[39m: 查询执行失败: Binder Error: Referenced column \"name\" not found in FROM clause!\nCandidate bindings: \"json\"\n\nLINE 4: WHERE name = 'ma_5'\n ^\nSQL: \n SELECT *\n FROM read_json_auto('D:\\PyProject\\ProStock\\src\\experiment\\data\\factors.jsonl')\n WHERE name = 'ma_5'\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, 70)\n", + "数据列: ['ts_code', 'trade_date', 'high', 'close', 'amount', 'open', 'turnover_rate', 'low', 'vol', 'total_assets', 'total_mv', 'f_ann_date', 'n_income', 'revenue', 'total_liab', 'total_cur_assets', 'total_cur_liab', 'total_hldr_eqy_exc_min_int', 'n_cashflow_act', 'ebit', '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', 'future_return_5_rank']\n", + "\n", + "前5行预览:\n", + "shape: (5, 70)\n", + "┌───────────┬────────────┬─────────┬─────────┬───┬────────────┬────────────┬───────────┬───────────┐\n", + "│ ts_code ┆ trade_date ┆ high ┆ close ┆ … ┆ value_pric ┆ active_mar ┆ ebit_rank ┆ future_re │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ e_divergen ┆ ket_cap ┆ --- ┆ turn_5_ra │\n", + "│ str ┆ str ┆ f64 ┆ f64 ┆ ┆ ce ┆ --- ┆ f64 ┆ nk │\n", + "│ ┆ ┆ ┆ ┆ ┆ --- ┆ f64 ┆ ┆ --- │\n", + "│ ┆ ┆ ┆ ┆ ┆ f64 ┆ ┆ ┆ f64 │\n", + "╞═══════════╪════════════╪═════════╪═════════╪═══╪════════════╪════════════╪═══════════╪═══════════╡\n", + "│ 000001.SZ ┆ 20200102 ┆ 1850.42 ┆ 1841.69 ┆ … ┆ null ┆ null ┆ null ┆ -0.008857 │\n", + "│ 000001.SZ ┆ 20200103 ┆ 1889.72 ┆ 1875.53 ┆ … ┆ null ┆ null ┆ null ┆ -0.01881 │\n", + "│ 000001.SZ ┆ 20200106 ┆ 1893.0 ┆ 1863.52 ┆ … ┆ null ┆ null ┆ null ┆ -0.008171 │\n", + "│ 000001.SZ ┆ 20200107 ┆ 1886.45 ┆ 1872.26 ┆ … ┆ null ┆ null ┆ null ┆ -0.014117 │\n", + "│ 000001.SZ ┆ 20200108 ┆ 1861.34 ┆ 1818.76 ┆ … ┆ 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", + "[配置] 特征数: 49\n", + "[配置] 目标变量: future_return_5_rank_rank(20分位数)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_4168\\966642096.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" ] } ], @@ -597,10 +781,13 @@ "source": "### 4.1 股票池筛选" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:14:48.474674Z", + "start_time": "2026-03-13T14:14:25.243457Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"股票池筛选\")\n", @@ -623,7 +810,47 @@ "else:\n", " filtered_data = data\n", " print(\" 未配置股票池管理器,跳过筛选\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "股票池筛选\n", + "================================================================================\n", + "\n", + "[过滤] 应用 ST 过滤器...\n", + " ST 过滤后数据规模: (6823808, 71)\n", + "\n", + "执行每日独立筛选股票池...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_4168\\2439474879.py:68: 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, 71)\n", + " 筛选后数据规模: (1455000, 71)\n", + " 筛选前股票数: 5678\n", + " 筛选后股票数: 1934\n", + " 删除记录数: 5368808\n" + ] + } + ], + "execution_count": 6 }, { "metadata": {}, @@ -631,10 +858,13 @@ "source": "### 4.2 数据划分" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:14:48.541185Z", + "start_time": "2026-03-13T14:14:48.480047Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"数据划分\")\n", @@ -659,18 +889,137 @@ " print(f\"测试集日均样本数: {np.mean(test_group):.1f}\")\n", "else:\n", " raise ValueError(\"必须配置数据划分器\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "数据划分\n", + "================================================================================\n", + "\n", + "训练集数据规模: (970000, 71)\n", + "验证集数据规模: (242000, 71)\n", + "测试集数据规模: (243000, 71)\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_4168\\966642096.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 }, { "metadata": {}, "cell_type": "markdown", - "source": "### 4.3 数据预处理" + "source": "### 4.3 数据质量检查" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:15:07.312974Z", + "start_time": "2026-03-13T14:14:48.544339Z" + } + }, + "cell_type": "code", + "source": [ + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"数据质量检查(必须在预处理之前)\")\n", + "print(\"=\" * 80)\n", + "\n", + "print(\"\\n检查训练集...\")\n", + "check_data_quality(train_data, feature_cols, raise_on_error=True)\n", + "\n", + "print(\"\\n检查验证集...\")\n", + "check_data_quality(val_data, feature_cols, raise_on_error=True)\n", + "\n", + "print(\"\\n检查测试集...\")\n", + "check_data_quality(test_data, feature_cols, raise_on_error=True)\n", + "\n", + "print(\"[成功] 数据质量检查通过,未发现异常\")\n" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "数据质量检查(必须在预处理之前)\n", + "================================================================================\n", + "\n", + "检查训练集...\n", + "\n", + "================================================================================\n", + "数据质量检查报告\n", + "================================================================================\n", + "\n", + "[严重] 发现 3326 个全空因子:\n", + " (某天的某个因子所有值都是 null,可能是数据缺失或计算错误)\n", + " - 日期 20200910: net_profit_yoy (样本数: 1000)\n", + " - 日期 20200910: revenue_yoy (样本数: 1000)\n", + " - 日期 20200910: healthy_expansion_velocity (样本数: 1000)\n", + " - 日期 20200910: value_price_divergence (样本数: 1000)\n", + " - 日期 20200910: ebit_rank (样本数: 1000)\n", + " - 日期 20230620: value_price_divergence (样本数: 1000)\n", + " - 日期 20230620: ebit_rank (样本数: 1000)\n", + " - 日期 20230802: value_price_divergence (样本数: 1000)\n", + " - 日期 20230802: ebit_rank (样本数: 1000)\n", + " - 日期 20230512: value_price_divergence (样本数: 1000)\n", + " ... 还有 3316 个\n", + "\n", + "--------------------------------------------------------------------------------\n", + "建议处理方式:\n", + " 1. 检查因子定义和数据源,确认计算逻辑是否正确\n", + " 2. 如果是预期内的缺失(如新股无历史数据),考虑调整因子计算窗口\n", + " 3. 如果是数据同步问题,重新同步相关数据\n", + " 4. 可以使用 filter 排除问题日期或因子\n", + "================================================================================\n" + ] + }, + { + "ename": "ValueError", + "evalue": "数据质量检查失败: 发现 3326 个问题,详见上方报告。如需忽略,请设置 raise_on_error=False", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mValueError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[8]\u001B[39m\u001B[32m, line 6\u001B[39m\n\u001B[32m 3\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33m\"\u001B[39m\u001B[33m=\u001B[39m\u001B[33m\"\u001B[39m * \u001B[32m80\u001B[39m)\n\u001B[32m 5\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[33m检查训练集...\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m----> \u001B[39m\u001B[32m6\u001B[39m \u001B[43mcheck_data_quality\u001B[49m\u001B[43m(\u001B[49m\u001B[43mtrain_data\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mfeature_cols\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mraise_on_error\u001B[49m\u001B[43m=\u001B[49m\u001B[38;5;28;43;01mTrue\u001B[39;49;00m\u001B[43m)\u001B[49m\n\u001B[32m 8\u001B[39m \u001B[38;5;28mprint\u001B[39m(\u001B[33m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[33m检查验证集...\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m 9\u001B[39m check_data_quality(val_data, feature_cols, raise_on_error=\u001B[38;5;28;01mTrue\u001B[39;00m)\n", + "\u001B[36mFile \u001B[39m\u001B[32mD:\\PyProject\\ProStock\\src\\training\\utils.py:166\u001B[39m, in \u001B[36mcheck_data_quality\u001B[39m\u001B[34m(df, feature_cols, date_col, check_all_null, check_all_zero, check_all_nan, raise_on_error)\u001B[39m\n\u001B[32m 163\u001B[39m \u001B[38;5;28mprint\u001B[39m(report)\n\u001B[32m 165\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m raise_on_error:\n\u001B[32m--> \u001B[39m\u001B[32m166\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\n\u001B[32m 167\u001B[39m \u001B[33mf\u001B[39m\u001B[33m\"\u001B[39m\u001B[33m数据质量检查失败: 发现 \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mtotal_issues\u001B[38;5;132;01m}\u001B[39;00m\u001B[33m 个问题,\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 168\u001B[39m \u001B[33mf\u001B[39m\u001B[33m\"\u001B[39m\u001B[33m详见上方报告。如需忽略,请设置 raise_on_error=False\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 169\u001B[39m )\n\u001B[32m 171\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m issues\n", + "\u001B[31mValueError\u001B[39m: 数据质量检查失败: 发现 3326 个问题,详见上方报告。如需忽略,请设置 raise_on_error=False" + ] + } + ], + "execution_count": 8 }, { "metadata": {}, + "cell_type": "markdown", + "source": "### 4.4 数据预处理" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:15:07.323298800Z", + "start_time": "2026-03-12T15:56:24.492487Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"数据预处理\")\n", @@ -695,7 +1044,33 @@ "print(f\"\\n处理后训练集形状: {train_data.shape}\")\n", "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, 71)\n", + "处理后验证集形状: (242000, 71)\n", + "处理后测试集形状: (243000, 71)\n" + ] + } + ], + "execution_count": 8 }, { "metadata": {}, @@ -703,10 +1078,13 @@ "source": "### 4.4 训练 LambdaRank 模型" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:15:07.323298800Z", + "start_time": "2026-03-12T15:56:25.031866Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"训练 LambdaRank 模型\")\n", @@ -735,7 +1113,49 @@ " eval_set=(X_val, y_val, val_group),\n", ")\n", "print(\"训练完成!\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "训练 LambdaRank 模型\n", + "================================================================================\n", + "\n", + "训练样本数: 970000\n", + "验证样本数: 242000\n", + "特征数: 49\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", + "[2]\ttrain's ndcg@10: 0.5742\tval's ndcg@10: 0.570382\n", + "训练完成!\n" + ] + } + ], + "execution_count": 9 }, { "metadata": {}, @@ -743,129 +1163,115 @@ "source": "### 4.5 训练指标曲线" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:15:07.323298800Z", + "start_time": "2026-03-12T15:56:26.444220Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"训练指标曲线\")\n", "print(\"=\" * 80)\n", "\n", - "# 重新训练以收集指标(因为之前的训练没有保存评估结果)\n", - "print(\"\\n重新训练模型以收集训练指标...\")\n", + "# 从模型获取训练评估结果\n", + "evals_result = model.get_evals_result()\n", "\n", - "import lightgbm as lgb\n", - "\n", - "# 准备数据(使用 val 做验证,test 不参与训练过程)\n", - "X_train_np = X_train.to_numpy()\n", - "y_train_np = y_train.to_numpy()\n", - "X_val_np = val_data.select(feature_cols).to_numpy()\n", - "y_val_np = val_data.select(target_col).to_series().to_numpy()\n", - "\n", - "# 创建数据集\n", - "train_dataset = lgb.Dataset(X_train_np, label=y_train_np, group=train_group)\n", - "val_dataset = lgb.Dataset(\n", - " X_val_np, label=y_val_np, group=val_group, reference=train_dataset\n", - ")\n", - "\n", - "# 用于存储评估结果\n", - "evals_result = {}\n", - "\n", - "# 使用与原模型相同的参数重新训练\n", - "# 正确的三分法:train用于训练,val用于验证,test不参与训练过程\n", - "booster_with_eval = lgb.train(\n", - " MODEL_PARAMS,\n", - " train_dataset,\n", - " num_boost_round=MODEL_PARAMS.get(\"n_estimators\", 1000),\n", - " valid_sets=[train_dataset, val_dataset],\n", - " valid_names=[\"train\", \"val\"],\n", - " callbacks=[\n", - " lgb.record_evaluation(evals_result),\n", - " lgb.early_stopping(stopping_rounds=50, verbose=True),\n", - " ],\n", - ")\n", - "\n", - "print(\"训练完成,指标已收集\")\n", - "\n", - "# 获取评估的 NDCG 指标\n", - "ndcg_metrics = [k for k in evals_result[\"train\"].keys() if \"ndcg\" in k]\n", - "print(f\"\\n评估的 NDCG 指标: {ndcg_metrics}\")\n", - "\n", - "# 显示早停信息\n", - "actual_rounds = len(list(evals_result[\"train\"].values())[0])\n", - "expected_rounds = MODEL_PARAMS.get(\"n_estimators\", 1000)\n", - "print(f\"\\n[早停信息]\")\n", - "print(f\" 配置的最大轮数: {expected_rounds}\")\n", - "print(f\" 实际训练轮数: {actual_rounds}\")\n", - "if actual_rounds < expected_rounds:\n", - " print(f\" 早停状态: 已触发(连续50轮验证指标未改善)\")\n", + "if evals_result is None or not evals_result:\n", + " print(\"[警告] 没有可用的训练指标,请确保训练时使用了 eval_set 参数\")\n", "else:\n", - " print(f\" 早停状态: 未触发(达到最大轮数)\")\n", + " print(\"[成功] 已从模型获取训练评估结果\")\n", "\n", - "# 显示各 NDCG 指标的最终值\n", - "print(f\"\\n最终 NDCG 指标:\")\n", - "for metric in ndcg_metrics:\n", - " train_ndcg = evals_result[\"train\"][metric][-1]\n", - " val_ndcg = evals_result[\"val\"][metric][-1]\n", - " print(f\" {metric}: 训练集={train_ndcg:.4f}, 验证集={val_ndcg:.4f}\")" - ] - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": [ - "# 绘制 NDCG 训练指标曲线\n", - "import matplotlib.pyplot as plt\n", + " # 获取评估的 NDCG 指标\n", + " ndcg_metrics = [k for k in evals_result[\"train\"].keys() if \"ndcg\" in k]\n", + " print(f\"\\n评估的 NDCG 指标: {ndcg_metrics}\")\n", "\n", - "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", - "axes = axes.flatten()\n", + " # 显示早停信息\n", + " actual_rounds = len(list(evals_result[\"train\"].values())[0])\n", + " expected_rounds = MODEL_PARAMS.get(\"n_estimators\", 1000)\n", + " print(f\"\\n[早停信息]\")\n", + " print(f\" 配置的最大轮数: {expected_rounds}\")\n", + " print(f\" 实际训练轮数: {actual_rounds}\")\n", "\n", - "for idx, metric in enumerate(ndcg_metrics[:4]): # 最多显示4个NDCG指标\n", - " ax = axes[idx]\n", - " train_metric = evals_result[\"train\"][metric]\n", - " val_metric = evals_result[\"val\"][metric]\n", - " iterations = range(1, len(train_metric) + 1)\n", + " best_iter = model.get_best_iteration()\n", + " if best_iter is not None and best_iter < actual_rounds:\n", + " print(f\" 早停状态: 已触发(最佳迭代: {best_iter})\")\n", + " else:\n", + " print(f\" 早停状态: 未触发(达到最大轮数)\")\n", "\n", - " ax.plot(\n", - " iterations, train_metric, label=f\"Train {metric}\", linewidth=2, color=\"blue\"\n", - " )\n", - " ax.plot(iterations, val_metric, label=f\"Val {metric}\", linewidth=2, color=\"red\")\n", - " ax.set_xlabel(\"Iteration\", fontsize=10)\n", - " ax.set_ylabel(metric.upper(), fontsize=10)\n", - " ax.set_title(\n", - " f\"Training and Validation {metric.upper()}\", fontsize=12, fontweight=\"bold\"\n", - " )\n", - " ax.legend(fontsize=9)\n", - " ax.grid(True, alpha=0.3)\n", + " # 显示各 NDCG 指标的最终值\n", + " print(f\"\\n最终 NDCG 指标:\")\n", + " for metric in ndcg_metrics:\n", + " train_ndcg = evals_result[\"train\"][metric][-1]\n", + " val_ndcg = evals_result[\"val\"][metric][-1]\n", + " print(f\" {metric}: 训练集={train_ndcg:.4f}, 验证集={val_ndcg:.4f}\")\n", "\n", - " # 标记最佳验证指标点\n", - " best_iter = val_metric.index(max(val_metric))\n", - " best_metric = max(val_metric)\n", - " ax.axvline(x=best_iter + 1, color=\"green\", linestyle=\"--\", alpha=0.7)\n", - " ax.scatter([best_iter + 1], [best_metric], color=\"green\", s=80, zorder=5)\n", - " ax.annotate(\n", - " f\"Best: {best_metric:.4f}\",\n", - " xy=(best_iter + 1, best_metric),\n", - " xytext=(best_iter + 1 + len(iterations) * 0.05, best_metric),\n", - " fontsize=8,\n", - " arrowprops=dict(arrowstyle=\"->\", color=\"green\", alpha=0.7),\n", - " )\n", + " # 使用封装好的方法绘制所有指标\n", + " print(\"\\n[绘图] 使用 LightGBM 原生接口绘制训练曲线...\")\n", + " fig = model.plot_all_metrics(metrics=ndcg_metrics[:4], figsize=(14, 10))\n", + " plt.show()\n", "\n", - "plt.tight_layout()\n", - "plt.show()\n", - "\n", - "print(f\"\\n[指标分析]\")\n", - "print(f\" 各NDCG指标在验证集上的最佳值:\")\n", - "for metric in ndcg_metrics:\n", - " val_metric_list = evals_result[\"val\"][metric]\n", - " best_iter = val_metric_list.index(max(val_metric_list))\n", - " best_val = max(val_metric_list)\n", - " print(f\" {metric}: {best_val:.4f} (迭代 {best_iter + 1})\")\n", - "print(f\"\\n[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!\")" - ] + " print(f\"\\n[指标分析]\")\n", + " print(f\" 各NDCG指标在验证集上的最佳值:\")\n", + " for metric in ndcg_metrics:\n", + " val_metric_list = evals_result[\"val\"][metric]\n", + " best_iter_metric = val_metric_list.index(max(val_metric_list))\n", + " best_val = max(val_metric_list)\n", + " 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", + " 实际训练轮数: 52\n", + " 早停状态: 已触发(最佳迭代: 2)\n", + "\n", + "最终 NDCG 指标:\n", + " ndcg@10: 训练集=0.6456, 验证集=0.5425\n", + "\n", + "[绘图] 使用 LightGBM 原生接口绘制训练曲线...\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAABW0AAAPdCAYAAADxjUr8AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAA175JREFUeJzs3QeYXHW5P/B3e0nvvZEQIIQQmtRLURAFEbCBoBQvFooNC+IVFAv+vSjiVa8Iir2AXFSUJh0h9BJ6eu99k022z/85ZwsJSSAJuzuzu5+Pz3lOmTOzvzmzJ5Jv3nl/eZlMJhMAAAAAAOSE/GwPAAAAAACA1whtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAMhp55xzTuTl5aXL+PHjo6GhoeWxa6+9tuWxX//61+mx0aNHtxwrKCiIbt26pcdOPPHE+MMf/hD19fXb/DmrVq2Kyy+/PCZPnhw9evSI7t27x1577RUXXHBBvPTSS1ucW1VVFT/96U/jqKOOin79+kVxcXEMGzYsDjnkkPj6178es2fPbpX3XlNTE1/84hfjsMMOi9LS0pb3deedd27z/Mceeyze+c53Rs+ePdP3ffjhh8ftt9/eKmMBAKD9FLbjzwIAgLdkxowZcdNNN8Xpp5++Q+cnAe/GjRtj3rx56ZIEmDfccEP89a9/TYPNZs8++2wa6i5ZsmSL57/66qvpkoSy11xzTXps8eLFccIJJ8TUqVO3ODc5niyPP/54FBUVxde+9rWtxpOEucnr3HHHHbFw4cJ0DJMmTYqzzjorzjzzzMjP37KmIhn7D37wgx16rw899FAcd9xxadDbbMqUKfGe97wnDas//OEP79DrAACQfSptAQDoUK688srIZDI7dG5yXmVlZdx7772x3377pcfuu+++OO+881rO2bBhQ7z3ve9tCWw/+tGPxqxZs6K6ujqmTZsWl112WVq12vx673vf+1oC26OPPjoNaZPK23Xr1sWDDz4YF110Ucv5m/uf//mftHL3xz/+ccycOTN9zvLly+Oee+5JQ9vktZJq380l4e+FF16Yhq6f+tSn3vC9Jo8ngW2vXr3iySefTH/GyJEj0zF/+tOfjk2bNu3QNQMAIPtU2gIA0GEk7Q5eeOGFuPXWW+Pkk0/eoeeUl5fH29/+9rjrrrti3LhxUVFREX/5y1/S19lnn33iF7/4RVr1mkjaG/z2t79teW7SjuGb3/xmS0uFf/zjH2lImxgxYkRauVtWVpbul5SUxJFHHpkur/e///u/8dnPfjb69++ftlv4wAc+ELvttlsasiYBaxLk/vOf/0yrYpPgN6nsTSTh709+8pN0e/r06dt9j88880y88sor6XZShXzggQe2BLlf/epX0zA4aalw6qmn7uCVBgAgm1TaAgDQYXzoQx9K19/5znd2+rkDBgxIWxA0S1oUJDbvD5sEq9sLizd/TiIJX5sD2zeyYMGCuPjii2PixInx3HPPpVW9Z5xxRgwePDh+9atfxWc+85lYv359fO9730t70iYVuTsrCW2b7bnnntvc3vwcAABym9AWAIAOIwk4kz6wSXXqv/71r51+flI522zu3LnpOul1u62Qc1s2P3ePPfZo2U4qWpsnCUuWJJBt9qMf/Sg9lvTRHTRoULz//e+PF198MW3L8OUvfzltkZBItpOK4KTyd2etWLGiZXvzXr2bbzf/HAAAcp/QFgCADqN3795pheuuVtsmE5M1S4LUzdc7a0efl1TyJn1wk9YMSYuGJPh917veFWvWrEknK0vWzZJK4KSPbtLCoTVs3vt3V98nAADtT2gLAECHkrQaSPrUPvTQQ/Hwww/v1HM37ws7evTodJ1M1tUsCUzfyKhRo7b5Wtdee20akG7+eLM5c+bE2972tnR7xowZLZW5SQD9+c9/fotzmyt0kyrcnW390CyZEK1Z0nZhW+cAAJDbhLYAAHQoSfj48Y9/PN3+85//vFMtBP74xz+27L/73e9O10nVa7Pt9ZNtnohs83OToLa2tnaHfnZzT9zNK1+3tf/ss89GUVFR9OvXL3bG/vvvv83g+dVXX93mOQAA5DahLQAAHc6XvvSlKC4ubglT38imTZvi/vvvTwPX5srT0047LZ0YLHHeeefF8OHD0+0pU6bEueeem1bHJoFsUk172WWXpUvipJNOigMPPDDdTs459dRT44UXXkjPTSYcq6qq2urnJxW9Tz/99BY9dX/+85/H2rVrW0LipG3DbbfdFldddVW84x3viJKSkpbnr1y5Ml02btzYcixpn5Aca66qTQLZvfbaqyXIfuqpp2LmzJlpsJxIQuDNA2cAAHJbXub1/7wPAAA55Jxzzonf/OY36fYrr7zSMlnYJz/5ybjuuutazvvVr36VnpuEpJtPGPZ6SSh6yy23bDFJV1LhesIJJ8TSpUu3+ZzPfvazcc0116TbSTh7/PHHp2PZnmTCsebXSlogJONMql6HDBmS9rZtHl9SgZuMo7mvba9evdLgeMKECTvUi/aoo46KBx54IN1+8MEH453vfGfU1NRscU7y/D/84Q/x4Q9/eLuvAwBAblFpCwBAh3TJJZdEYWHhG56TBJZlZWVpr9kklE3aIySTgW0e2Cb222+/tGI2mRhs0qRJ0a1bt7Rv7h577JH2n02qcZuNGDEinnzyybQq9uCDD05fKwlfk2rWww47LL761a/Gfffd13L+Zz7zmbQS95RTTknD2b/85S9pKJv8jO9+97tpkJv0sv3IRz6SVshuHtjujCTATYLb4447Lnr06JGOPxnPP//5T4EtAEAHo9IWAADa2NVXXx1f+MIX0oA2CZuTNgtJS4akzcH8+fPTatkk/N08HAYAoOsS2gIAQDv43ve+F//1X/+1zT68SX/e66+/Ps466yyfBQAAQlsAAGgvL7/8ctob95577onFixdHnz590j60l156aUuvXgAAUGkLAAAAAJBDTEQGAAAAAJBDhLYAAAAAADmkMNsDyEUNDQ1pj7EePXpEXl5etocDAAAAAHQCmUwm1q9fH0OHDo38/O3X0wpttyEJbEeMGNGWnw8AAAAA0EUtWLAghg8fvt3HhbbbkFTYJubMmRN9+/Ztu08HyLra2tr417/+lc7cXVRUlO3hAG3I/Q5dh/sdug73O3QdtZ3k7+8VFRVpsWhz/rg9QtttaG6JkFy8nj17ts0nBOTMH/rl5eXpvd6R/9AH3pz7HboO9zt0He536DpqO9nf39+sJauJyAAAAAAAcojQFgAAAAAghwhtAQAAAAByiJ62AAAAAECLhoaGqKmpybmetoWFhVFVVRX19fWRq5J+uwUFBW/5dYS2AAAAAEAqCWvnzJmTBre5JJPJxODBg2PBggVvOolXtvXu3Tsd61sZp9AWAAAAAEiD0SVLlqSVoiNGjIj8/NzprNrQ0BAbNmyI7t2759S4Xn/9Nm7cGMuXL0/3hwwZssuvJbQFAAAAAKKuri4NHYcOHRrl5eU52bKhtLQ0Z0PbRFlZWbpOgtuBAwfucquE3H2HAAAAAEC7ae4VW1xc7Kq/Bc2Bd9KHd1cJbQEAAACAFrneM7YrXD+hLQAAAABADhHaAgAAAADkEKEtAAAAAEBEjB49Oq655pqsX4vCbA8AAAAAAGBXHX300TF58uRWCVuffPLJ6NatW9Y/DKEtAAAAANBpZTKZqK+vj8LCN49CBwwYELlAewQAAAAAYJth58aauqwsmUxmhz6Rc845Jx588MH40Y9+FHl5eeny61//Ol3fcccdccABB0RJSUk8/PDDMWvWrDj55JNj0KBB0b179zjooIPinnvuecP2CMnr/OIXv4hTTz01ysvLY/fdd49bb721zX9bVNoCAAAAAFvZVFsfEy6/KytX5uVvHh/lxW8eXSZh7fTp02PixInxzW9+Mz320ksvpeuvfOUr8f3vfz9222236NOnTyxYsCBOOOGE+M53vpMGub/97W/jpJNOimnTpsXIkSO3+zOuuOKK+O///u+46qqr4sc//nGceeaZMW/evOjbt2+0FZW2AAAAAECH1KtXryguLk6rYAcPHpwuBQUF6WNJiHvcccfF2LFj04B13333jU9+8pNpwJtUzH7rW99KH3uzytmkmvfDH/5wjBs3Lq688srYsGFDPPHEE236vlTaAgAAAABbKSsqSCtes/Wz36oDDzxwi/0kbP3GN74Rt912WyxZsiTq6upi06ZNMX/+/Dd8nUmTJrVsJ5OU9ezZM5YvXx5tSWgLAAAAAGwl6ee6Iy0KclW3bt222P/iF78Yd999d9oyIamaLSsriw984ANRU1Pzhq9TVFS01XVpaGiIttRxrzoAAAAA0OUVFxdHfX39m16HRx55JG11kEwq1lx5O3fu3Jy8fnraAgAAAAAd1ujRo+Pxxx9PA9iVK1dutwo26WN7yy23xHPPPRdTp06NM844o80rZneV0BYAAAAA6LC++MUvppOPTZgwIQYMGLDdHrVXX3119OnTJw477LA46aST4vjjj4/9998/cpH2CAAAAABAhzV+/Ph49NFHtziWtEHYVkXufffdt8WxCy+8cIv917dLyGQyW73O2rVro62ptAUAAAAAyCFCWwAAAACAHCK0BQAAAADIIUJbAAAAAIAcIrQFAAAAAMghQlsAAAAAgBwitAUAAAAAyCFCWwAAAACAHCK0BQAAAADIIUJbAAAAAKDLGj16dFxzzTWRS4S2AAAAAAA5RGgLAAAAAJBDhLYAAAAAwNYymYiayuwsmcwOfSLXXXddDB06NBoaGrY4fvLJJ8fHPvaxmDVrVro9aNCg6N69exx00EFxzz335PynXZjtAQAAAAAAOah2Y8SVQ7Pzs7+6OKK425ue9sEPfjA+/elPx/333x/veMc70mOrV6+OO++8M26//fbYsGFDnHDCCfGd73wnSkpK4re//W2cdNJJMW3atBg5cmTkKpW2AAAAAECH1KdPn3j3u98df/zjH1uO3XzzzdG/f/845phjYt99941PfvKTMXHixNh9993jW9/6VowdOzZuvfXWyGUqbQEAAACArRWVN1a8Zutn76AzzzwzPv7xj8f//u//ptW0f/jDH+L000+P/Pz8tNL2G9/4Rtx2222xZMmSqKuri02bNsX8+fMjl2W90vanP/1pjB49OkpLS+Pggw+OJ5544g3PX7t2bVx44YUxZMiQ9EMYP358Wuq8uUWLFsVHPvKR6NevX5SVlcU+++wTTz31VBu/EwAAAADoRPLyGlsUZGPJy9vhYSbtDjKZTBrMLliwIP7973+nQW7ii1/8Yvz1r3+NK6+8Mj3+3HPPpVlhTU1N5LKsVtreeOONcfHFF8e1116bBrbXXHNNHH/88WlPiYEDB251fnIxjzvuuPSxpMx52LBhMW/evOjdu3fLOWvWrInDDz88LX++4447YsCAATFjxoy0VBoAAAAA6FxKS0vjfe97X1phO3PmzNhjjz1i//33Tx975JFH4pxzzolTTz013U8qb+fOnRu5Lquh7dVXX52WLp977rnpfhLeJon4DTfcEF/5yle2Oj85njQSnjJlShQVFaXHkirdzX3ve9+LESNGxK9+9auWY2PGjGnz9wIAAAAAZMeZZ54Z73nPe+Kll15Kv4HfLOlje8stt6TVuHl5eXHZZZdFQ0NDzn9MWQttk6rZp59+Oi699NKWY0mfiWOPPTYeffTRbT4naRB86KGHpu0R/v73v6dVtGeccUZccsklUVBQ0HJOUq2bzBz34IMPptW4F1xwQRoOb091dXW6NKuoqEjXtbW16QJ0Xs33uHsdOj/3O3Qd7nfoOtzv0Pr3VNJmIAk1cy3YzGQyLettje3oo4+Ovn37pt/gT/rZNp/z/e9/P84777w47LDD0snJvvzlL6fZ3+tfZ3uvuyuS10leL7mezZllsx3NH/Iyze+4nS1evDgNVJOq2SSIbZZcuCRsffzxx7d6zp577pmWLyfJeRLEJuXOyfozn/lMfP3rX28ph04kbReS4PbJJ5+Mz372s2kV79lnn73NsSTNiK+44oqtjiezzpWX73jTYwAAAADoqAoLC2Pw4MHpt9iLi4uzPZwOKylWTXrrLl26NJ34bHMbN25Mi1DXrVsXPXv27ByhbTLpWFVVVcyZM6clpU5aLFx11VXp7G+J5BfqwAMPTF+3WRLqJuHt9ip4t1Vpm/xyJq+ZTGYGdF7Jv3Ddfffdab/s5rYrQOfkfoeuw/0OXYf7HVpXkrslYWPSjrS5MDJXZDKZWL9+ffTo0SNtc5Dr1zEpPE3yxddfxyR3TCp+3yy0zVp7hGRwSfC6bNmyLY4n+0mivy1DhgxJQ5XNy4r32muvNLVOEuwksE3OmTBhwhbPS875v//7v+2OpaSkJF1eL/lZQhzoGtzv0HW436HrcL9D1+F+h9ZRX1+fBqJJC9NkySUNTa0LmseXy5LxJePc1p9NO5o1Zu0dJgHrAQccEPfee+8WFz/Z37zydnOHH3542hJh8/4S06dPT4Pa5pLt5Jykd8XmknNGjRrVZu8FAAAAAKC1ZDWWTvrOXn/99fGb3/wmXnnllTj//POjsrIyzj333PTxs846a4uJypLHV69enfaoTYLY2267La688sp0YrJmn//85+Oxxx5LjycBb9KX9rrrrtviHAAAAACAXJW19giJ0047LVasWBGXX3552uJg8uTJceedd8agQYPSx+fPn79FuXPSB+Kuu+5Kg9lJkyalPXGTAPeSSy5pOeeggw6Kv/71r2nY+81vfjPGjBkT11xzTTp5GQAAAADwxrI0BVan0bBZl4AOGdomLrroonTZlgceeGCrY0nrhKSS9o285z3vSRcAAAAAYMck/VaTXqxJkeWAAQNyasKvhoaGdE6rZJKvXO1pm4TdyRiT65eMsbmda4cMbQEAAACA7CsoKIjhw4fHwoULY+7cuZFrgeimTZuirKwsp8LkbSkvL4+RI0e+pXBZaAsAAAAApLp37x6777571NbW5tQVqa2tjYceeiiOPPLItCI4l4PvwsLCtxwsC20BAAAAgC2Cx2TJJQUFBVFXVxelpaU5Hdq2ltxsAAEAAAAA0EUJbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHKI0BYAAAAAIIcIbQEAAAAAcojQFgAAAAAghwhtAQAAAAByiNAWAAAAACCHCG0BAAAAAHJIYbYHAAAAAACwLXX1DbFiQ3UsXl0ZizdGLFlXFf165EV5cUHk5eV12osmtAUAAAAA2l19QyaWr69Kg9gla5P1pnR76bqqWLxuU7pevr46Pa9RYXxv6kONW/l50bOsKHqWFkavZJ1uJ+vCzbaLGh8rLdxsu/GcksKCnP7EhbYAAAAAQKtKgtYV66tbwtfFaxvXaUDbFM5uGchuX2F+XvTrXhwbNlZFdUN+1DVk0mV1ZU267IrSovxtBrvDepfFl9+1Z2Sb0BYAAAAA2CG19Q2xprImViXLhmRdnYazmweyyfaynQhkB/UsjSG9SmNwr9IY2rssBjftD+ldlq77dy+Jhvq6uP322+Pd735n1EV+rNtUGxWb6qKiqjbWbaxN1xWbamNd07HG7ebjdS3b66vq0p9bVdsQVbXVaXC8uTH9uwltAQAAAKAr2lBdl4afJUX5UVpUEKWFBVFUkNfufVqTYHXtxsaK1ZVNIWzL9obG7SScXdl0fO3G2h1+7e0FskN7J/uvBbIF+W/+nhvqG9fJ9SkvKozy4sIY0mvX3u+GqqawNw1+twx2y4pzo22CSlsAAAAAaEMNDZmYvbIynpm/Jp6dvyaembc2pi9fH5nXFaImeW0S3qZBbmFB+hX+ks3WJZvtJ0FvSeGW69efn6yTn5FUxTaGr9VbVMg2txfYgYLYLSQZa99uxdGvW0natqBf95LGYHYXA9n2VJCfF73Ki9JlROQuoS0AAAAAu1SxmHwVfu7KjTF3VWXMW1UZSyuqo3dZUQzqWRIDe5TGgJ4lMahHaQzsWRJ9y4sjP8cCvLaSVG4+N39tPDt/bRrUPrdgbVrF+XpJ2Fpd19CynwSsm2rr0yVixytaW0Pv8qLot0UQu9l2y7oxoE0+467yWWaL0BYAAACAbaqrb4jFa6taQtk5Kzem62R/wepNUVPfsOMhVH5eDOiRhLklMaApyG0OdJNjydfok3W/HKzOfLMq2lkrNqThbFJB++yCNTFj+YatqmiTqtdJw3vHfiN7x/4j+6TrJNjOZDLpdUx6rFbX1Ud107p5v7H3an0a7m6+3vzxzdfJ8zc/LxlGY1XsdoLY7sXRp7w4igrys3UJ2QahLQAAAEAXn1hq0ZpNaRA7d2USyDYGs/NWbYwFazZGbf32vztfXJAfI/qWxeh+3WJUv27pV+KTitLl66tiWUXjJE8r1lel/VHr0srcxsmqItZt9zWTvDb5Wn1jmNsY5A5sCnSbt5Oq3Z5lhdGjtKjdA95k0qvnFq6NZ+ataamibZ7canMj+5bH/klAO6pP7DeiT+w5pMc2g9GkR2va+qAw6aVa1E7vglwntAUAAADo5GrqGmLhmsY2BnNbqmUb9xeu2ZS2Otie4sL8GNW3PA1lR/crj9H9uzWFtOXpxFI7EpomwfDKDdWxvKI6llVUpWFuc6DbGO5WpY8l5yRDaX48ouJNX7tHSWH0LCtKl15lhdGzNFk37xdFz9LCtH/p1seL0urXN5r4K6miTapmW3rRzl8bM5dv2Oq8sqKC2HdEr9hvZJ+0inbyiN5pVTHsKqEtAAAAQCeQfBV+weokiH2thUFSLZusk0raN5psKgkvR/XtFqP7l7dUzTYHtMnkUm+1f2lSYToknZyq7A3PS8LjZLKsJLBtCXeToLcp1E1C3uTY2o21TX1fI9ZX16XLorWbdnpcSaVwUrGbhr6bhbo9Sgtj/qqNMTWpoq3euoo2uTZpi4O0irZ37Dm4RxRqL0ArEtoCAAAAdBCbaupj3uotq2XT9crKWFJRtVUf1c2VFxe0hLGvr5pN2g7kwsRSSdVu2gqhZ2lMHNbrTauHkwm/KjbVpi0ZKqrqGtfpdtOxTXWv23/t3CQgTnrJJq0bkmV7uhUnVbSv9aJNqmiTvrvQloS2AAAAADlkQ3VdUxDbWCW7eTibtBJ4I91LCtNq2S3D2cYK2gHdS96wFUBHk7RtSHrfJsvOSib/qqypfy3E3Ubom7xuEtLuMbhHh5oYjc5BaAsAAACQBUlwOG3Z+pgyc1W8tLiiJZxN+rq+kaRH65ikQrb/a20Mmtd9uxV3qmC2rSTXKAm4kyXpywu5RmgLAAAA0E6SnrOPzFwZj8xaFY/OWrndr+X361acTvTV0l92s+rZ3uXFPi/o5IS2AAAAAG1kxfrqmDJrZVpNO2X2yliwesvJssqKCuKgMX3jwFF9Gqtnk5C2f3k6KRbQdQltAQAAAFrJ+qraeHz26nikKahN2h9sEcTk56UTWR02rn8cPrZfTB7ZO0oKC1x/YAtCWwAAAIBdVFVbH8/MX5MGtElQ+/zCdVHfkNninL2G9EwD2sPH9U+rapM+qgBvxJ8SAAAA0EUsWrsp/vj4vHhyzproUVoYfboVpxNX9SlP1kXRt1tJum7cL06/op+fb1KrzSWB7IuL1rVU0j45d3VU1zVscU7Sd7axkrZ/HLJb3+jXvaSdP2mgoxPaAgAAQCfW0JCJf89cGb97dF7c9+qyeF0R6BtK8tokwE3D3aYgtzHofS3Y3fyxZCkvLoi8vM4T9GYymZi1YkM8klTSzlwZj81eFRVVdVucM6BHSVpJmwS1h43tF8P7lGdtvEDnILQFAACATmjtxpq4+emF8fvH5sXcVRtbjh8+rl+cvO+waMhkYlVlTayprInVG5vXtek6WdZX16UBb3JOsuyo4sL8NMTtXV6UhrdJ6JnJRGSS/2Ui/blJbpwea9pOj2W2PLa9c5N1Mq7mdV1dQfzXM/dtMYbksS32t3hs8+OZ7Rzf8rVq67d8vaRK+ZDd+rW0PBg3sHunCqqB7BPaAgAAQCfy/MK1aVXtrVMXt3xtPwkZP3DA8Djz4FFpwLgjauoaYs3Gmlj9+mC3sjY93hL4Juum/eQ5ybK0oipd2kdeRP2Wla+traQwPw4a3TcObQppJw7tGYUF+W36M4GuTWgLAAAAnWAyrH9MXZxW1U5duG6LCbDOOnRUnDx5aJQX71wEkFTMDupZmi47IqlI3VRb3xTy1sbaTTVp9WpSgJoXeWmrhdhsO6lMTR5rbJnbvJ08Gi3b6SPNx7d4buPz6+vq4sEHH4yjjz4qCguL0uc227zwtfFVtzwW2zt3s53mraTtQ2lRwQ5fO4C3SmgLAAAAHdS8VZVpUHvTUwtj3aba9FhxQX6cOGlIfOSQUbH/yN7t9rX95OckwXCyDO/TLj8yamtr45WyZOKvblFUVNQ+PxSgHQhtAQAAoAOpb8jE/a8uj989Ni8enL6i5fiw3mVx5iEj47QDR0S/7iVZHSMAb43QFgAAADqAlRuq48YnF8QfH58fi9ZuSo8lRbRHjR8QHz1kVBy9x8AoaOw1AEAHJ7QFAACAHJX0iX163pq0qvb2F5ZEbX0mPd67vCitqD3j4JExql+3bA8TgFYmtAUAAGCX1dQ1xIbqulhfVRvrq5L1a9vNxytr6qNnaVH0614c/dOlJP36fj+TO21XZXVd/P25xWlY+8qSipbjk0f0Tqtqk561JsYC6LyEtgAAAF1QQ0MmKmuag9Vth60bquqioumxDdWbP/baudV1DW9pHD1KCpvC3CTILU7D3GS7Jdzt1nhsQPeS6FlW2GqTaiUVrMnYm99T8l7XVze+53S/6X02P9a8X9fQEIX5eWkbgsL8/HTdvLx2PC/yW/bzW45v67yCbZzz3IK18X9PL4z11XXpWEsK8+PkyUPjo4eMjn2G92qV9w9AbhPaAgAAdBBJ0Liptr4pYKxLqzG32G5eqrbeTh7f/DlJ9Wtr6lZcEN1LC6NHaVF0L0nWhWl1bbJdVlyQBp5JT9ZVldWxcn1Nuk6+6p+MKVnmrtr4pj+jqCAv+nZ7rVL39cFuQyazndC1dsv9pmtQ19DYaiBXje5XHh85ZFR84IDh0bu8ONvDAaAdCW0BAAByTBIqXvfQ7Pj3jBVbhJBJ4NraOWNS2ZkErOlSUpQGrz1fF76m2y3HC6N7SdFWz9nZCbCSALqiOcjdUBOrNlSn2ys31Lx2rPK1/SRwTULeZRXV6dJaksLd9H2WFKbvI9nuXlrUuL/ZseS9dispTCtik3A4CXzrGzJRV9+4rs9svt/Q8nh6bIt1Q9Q3xBbnJOuk8rl5v0+34vjgAcPjiHH904pdALoeoS0AAECOqKtviJueWhhX3z09DSrfLGhsXpIwsTFMbdzePGTc/LyWUHKz7eSr963VcmBnJD+zV1lRuowd8ObnV9fVN4W7jSFuY9VuTaxc37TeUB1FBfkt721boWsSNm+5XxhlRQWCUQByjtAWAAAgBzw4fUVcedsrMW3Z+pavxl9wzLgY3rusMXxtCiKT7fLigqwErdlUUlgQQ3uXpQsAdHb5kQN++tOfxujRo6O0tDQOPvjgeOKJJ97w/LVr18aFF14YQ4YMiZKSkhg/fnzcfvvt2zz3//2//5f+x8znPve5Nho9AAA7K/la9KK1m6KqtnV7akJHNG3p+jjrhifi7BueSAPbpPL0svdMiH99/qj40IEj4rBx/WPfEb1j7IDuMbBnaRradrXAFgC6mqxX2t54441x8cUXx7XXXpsGttdcc00cf/zxMW3atBg4cOBW59fU1MRxxx2XPnbzzTfHsGHDYt68edG7d++tzn3yySfj5z//eUyaNKmd3g0AANuSzDI/dcG6eGb+mng2WRasjbUba6O4MD8mj+gdh4zpGwfv1i/2G9k7youz/p+o0C6Wr6+KH949PW58ckHapzaZZOusQ0fHp98+zqRTANDFZf2/iK+++ur4+Mc/Hueee266n4S3t912W9xwww3xla98Zavzk+OrV6+OKVOmRFFRUXosqdJ9vQ0bNsSZZ54Z119/fXz7299uh3cCAEAimUxn1ooNTQHt2nQ9Y/mGyLxu8qRkbp2auoZ4Ys7qdIn7ZqYT/Ewa3isNcA8e0zcOHN037TlJ57C6siamL1sfPUuLYnjfsnTdFW2qqY9fPjw7fvbArKisaaw2f/fEwXHJu/aM0f27ZXt4AEAOyOp/ASdVs08//XRceumlLcfy8/Pj2GOPjUcffXSbz7n11lvj0EMPTdsj/P3vf48BAwbEGWecEZdcckkUFBS0nJc8fuKJJ6av9WahbXV1dbo0q6ioSNe1tbXpAnRezfe4ex06v85yvyftBNZsrI2+3YrTyYNywbpNtTF14bo0oH1u4bp0O5nl/fWG9ymL/Ub0Sitrk/Ueg3qkLRKenLsmnmhalqyrimfSoHdtGmgls9HvPaRHHDS6T+Myqk/0LOuaQV9Hk/xevLi4Il5cVBEvLFoXLy2uiIVrq7Y4p2dpYQzrXZb+bgzrXRrD+pTFiN5l6To5nkwU1Znu9+QfNG59fkn84O4ZsbSi8e8fk4b1jEvfvUccOKpPTo4Zcl2u3u9A66vtJPf7jo4/q6HtypUro76+PgYNGrTF8WT/1Vdf3eZzZs+eHffdd19aRZv0sZ05c2ZccMEF6Rv++te/np7z5z//OZ555pm0PcKO+O53vxtXXHHFVsfvv//+KC8v36X3BnQsd999d7aHALSTXL/fk2rUyrqIlVXJkherqhvXjUtERe1rfSy7FWaiZ3FEr6JM9CqOxu3iTPQsalonx4oiWjPbTb7CvXRjxNwNeTF3fV66XrZp696axfmZGNk9YlT3TIzpkUnXPYuTyZXWR6xeGAtWRyxoOrd7RLy9POKYvSJWV0fMrMhLl1kVje//+UUV6fLLR+ZFXmRiaHnEuJ6ZGNu0dJfhZt2muoiFlXkxf0PEgnSdfHbb7rnaryQT1fURG+ryoqKqLiqWro9XljZOvPV65QWZ6Fsa0bckE31Ltlz3K4l4s0w3l+73mesi/javIL0+iT7FmXjPyIbYv//qWP7So3H7S9keIXRsuXS/A23r7g5+v2/cuHGHzutw3zVraGhI+9led911aWXtAQccEIsWLYqrrroqDW0XLFgQn/3sZ9MPMJnYbEcklb5JX93NK21HjBgRxxxzTPTr168N3w2Qbck/+CR/XiS9sptbrgCdUy7d73X1DbGkoirmr94U81dvTNcLmtbz12yMyiTRegNJW4EkPK2sy0sD3iXxxhMS9SkvikE9SmJgz5IYkKx7lKT7zdvJ0r97SdpfdltfZ0+raBesTXvSTl20bpvjG9W3vKmKtrGSdo9B3aOw4K2nxUnlbdo+oakSd+6qjbFoY8SijXnx4NLGc8YP7J5W4b4tWcb0Sd8LbWdDdV1aNZssLyyqSKtpk89lW0b2LYt9hvaKicN6xsShPWPvoT2iR1NLhI01dbFobVVabb1ozaa0Cnfhmk3pfrJOKso31ufFxsrGQHhbepcVxbA+pY3Vui0VuqUxuHtRzHzu0Tjh+Ozf73NWVsZ/3zU97nl1RbrfraQgzj9ytzj70JFRWvTaNwWBjv//70Dbqu0k93vzN/xzOrTt379/GrwuW7Zsi+PJ/uDBg7f5nCFDhqQfzOatEPbaa69YunRpS7uF5cuXx/7779/yeFLN+9BDD8VPfvKTtA3C5s9NlJSUpMvrJT+nI/8SADvO/Q5dR3vd75XVdWkgO2/VxjSQnbe6Mt1OjiUBVV2Sur6BIb1KY0Tf8jQMHdWvPEb26xYjm/Z7lxelgVYyidGyiupYVlEVK9Y3rpcn++sb18njtfWZ9NxkeXXZhjf8mf26FadB7qCepWkf2ZeXVKSB0+t1Ky5IZ7Lff2SfdOKw/Ub2Sds1tIWR/YtiZP8e8YGDRqX7yyuq4vE5q+PxOavi8dmr016505uWPzzRWLu724BucfCYfnHIbn3T9eBeO/YP+WwtCVaTcPb5hevixUXr4vmFa2P2ysqt+hMnkhYHST/ifYb1jn2GNQa1vcu3/3vRq6goenUriwnDGtsCbOseagxwN6YhbuOS3E+N6+R3eu2mxuWlxVtX6hbkFcRvFz6d/n4mS/KPCcm9lJf3xv/I0VrWVNbEj+6dEb9/bF56vyetPj78thHxuWPH+4cFaAP+ex66jqIOntft6NizGtoWFxenlbL33ntvnHLKKS2VtMn+RRddtM3nHH744fHHP/4xPS/pf5uYPn16GuYmr/eOd7wjXnjhhS2ek0xytueee27V9xYAYEeqYpP+rBVVtS3rik11sX6z/WS9vun4ig3VaTi7ckP1G/93UGF+jOhTFqOawtg0kO3XuAzvU/6mFXhJSJose27737lb+mcmgVYa5raEuq9tJ4FvEvY2h7urKmvS5dXXfVV97IBuaejVHNKOH9QjDaCyYWDP0jhp36Hpkli1oTqtxE2C3Mdmr4ppy9bH7BWV6fKnJ+an55QVFURZcUG6Li3KT7fLiwqjND2W3/J4cs3T7dftlyfbTc/f8rVe287W9WhNSUiafPYvLFwbzy9qDGlnLt+QVnW/3tBepbHP8F4xaXjvmDgsCWp7tXpw362kMP1dS5btVfymFbotoe5r4e781ZWxblNdS2uN3zw6r6XqPAlvJ49o/F1O/vGhVyv3SK6uq4/fTpkXP75vRtr+IfH2PQfGpe/eM3bfznsBAMi59ghJW4Kzzz47DjzwwHjb294W11xzTVRWVqZBa+Kss86KYcOGpX1nE+eff35aMZu0QPj0pz8dM2bMiCuvvDI+85nPpI/36NEjJk6cuMXP6NatW9rm4PXHAYCuIQkvkwrXWRUR901bEZtqM40h6+Zh7KZth7Cbat+4VcEbSSpik8rYEc2BbN9uMTKpmu1bHoN7lkZ+Gwd9yes3h7t7DXnj67NmY81rwe766li3sTZ2H9Q9DbjeqFoy2/p1L4l37zMkXRJrN9a0hLhJNe7LiyvSz/CtfI47+lnvPrB7jBuYhIzdY/emdVK53F6VnTuqtr4hDbWTgHva0oqYtnR9up1UsG7LoJ4lafVsYxVtUkHbK31f2ZZUg+8xuEe6vF7yDbzf//WO6DNuv3hh8YZ4dsGaeGlRRVqde/+0Femy+T9KNIe4ye/7noN77FJrj0wmE7e/sDS+d+er6Z83ieS1vnbihDhi9/5v8d0CAF1N1kPb0047LVasWBGXX3552uJg8uTJceedd7ZMTjZ//vyWitpE0mv2rrvuis9//vMxadKkNNBNAtykihYAoKq2PmYs2xAvL1mXBnbJV/xfWbI+rcpL/9PnpWd36SIl1ZbJTPZJP86eTetkv2dZ07rpeN9uJY2Vs/3KW72Cry3D3ST8TJa9hvSMjiwJmN+59+B0SSSfe/I19TS4rWkMb5OlqqY+NjbtV73+sS32G9Jzmx9rOd60brZ2Y208OXdNumwu+Z1IKkV3bwpyk3WyP7AdwtwkjE/aCzSHsul66fqYvXJDWlm9LUkY2xzMTkoqaIf3SttldDTJte1XGnHCpCFx6gFFLRWwyZ8Fz81fk/Zofm7B2rQqftaKynT5v2cWpucl1diThvWOyUnrj6Qqd2TvGNKr7A1/3rPz18S3b3slnp7X+Pknn+8X37lHvP+A4Z2iChsA6IKhbSJphbC9dggPPPDAVscOPfTQeOyxx3b49bf1GgBAx5eEca8saQxmmwPa5Ovc2+oXW1SQF70KG2Jwv17Rq7woepRsHbq2hLJlr+0n6+6lhVHUCpNqkZ1qzGRpC0llZXVdQxr+JhXK05etT//BYMbyxvXcVZVpNfdT89aky+Z6NIe5A7unX5lP1sl+UtW6K2Fu0iZi82A22Z6+dH1U1my7wji5JkklcFqpOiipVu2ZbrdVb+JcUFJY0NQaoXecs9l1m7pwbTw3f21LkJtU2j8xN5n8bnXLc5PPZb8RfVqC3CTMLi8uTPtV//dd0+IfUxe3BL6fOHJsfPLI3dL2DgAAu8p/SQAAOS8Jx5Kvbm9ePZusF6+r2u5X1ScM6dm4DG1cRvYuibvvujNOOOGQDj1xAbkjCVeTvrbJ0tiCYssq5aSyM2lDkEyWNqMp0J2+fH1a3ZkEg0lVZnNl5uZhbhrkNlXlJoFuEq4m7TSSn5dMDDY9eZ2l69P+s0lQnKy310M5+ceKsQOawtmWgLZHDOtdlnNtG7IhqS5/+56D0qW5OjmZaC2pnE0C3Gfnr00D8KT/850vLU2XRFI9m3xOybk1dQ2RXMr37z88ra418R0A0BqEtgBATkmCrrS9wWbhbFJNuz5tb7C1pBVBSzjbtB7SqzHg2lxtbW07vQN4rbIzCXK3FebOWVmZhq8zk4rYpurcuU1h7jPz16bL6ytjk3Ybi9dtisy2Oxuk98LmwWyyjOnfTZX4TrYKGZf2Ju4eHzxwRHosCcpfWLiuJcRN1ksrqlom7DtsbL/4rxP3ir2H9vKrDwC0GqEtANDukyCt2lCTVgauqqyJleurY8WG6rRiMAlot9feoLggP8YP7r5ZBW2v2HNIj7R9AXS0MHfPwT3T5fVh7tyVGxvbLDRV5ybbSZib9OZt7Msc0b97SewxuHvsMSh5jR4xfnBjewVfx28bSRuEg3frly7NlqzbFFMXrI0+5cXxtjF9VS0DAK1OaAsAvOXWBUnfzCR8XVVZHSvW16TrlU3rJKBNQtmkd+TKDTWxbtObV7wmFYV7b1Y5myzJV7z1laWzh7nNFbKbS75+n1Tmrt1Yk1aAJl/pJ7uSicnebHIyAIC3QmgLAGxXXX1D2nMz+Srwyubq2KbwtXmdHEsmY9oZST/IpAdoUjHYv3tx9OtWHGP6d28JaIduo70BdFXFhflbBbkAAHRuQlsAYCsVVbVx4xML4tdT5saitZt26AqVFxdEv+6NQWy/biUxoEcSxpa8dqx7cQxI1yXRu6wo7R0JAADA1oS2AECLBas3xg2PzImbnlyQtjxI9CkvSqv8Gqtimypjm7ZfC2KL076PAAAAvHX+dgUAXVzSkzZpgfDLh+fEXS8tjeY5wJKJjf7ziDFxyn7DorSoINvDBAAA6DKEtgDQRdXWN8QdLy5Nw9pkFvRmR44fkIa1R+7eX19ZAACALBDaAkAXs25Tbfz5iflpv9ol66paJjp6337D4mNHjInxg0x4BAAAkE1CWwDoIuatqoxfPTI3bnpqQWxs6leb9Kf9yCGj0iXpUQsAAED2CW0BoJP3q31y7pr4xb9nx92vLItMU7/aPQb1SFsgvHfyUP1qAQAAcozQFgA6ab/a219YEr/495x4YdG6luNH7zEgzjtitzh8XD/9agEAAHKU0BYAOpF1G2vjj0/Mj99MmRtLKxr71ZYk/Wr3Hx7/ecToGDdQv1oAAIBcJ7QFgE5gzsqkX+2c+MtTC2NTbXO/2pI4+9BRccbBI6OffrUAAAAdhtAWADpwv9rH56xOWyDc++pr/Wr3HNwjzvuP3eKkfYdESWFBtocJAADAThLaAkAHNHvFhrjs7y/GIzNXtRx7+54D47wjxsShY/WrBQAA6MiEtgDQgVTV1sfPHpiVLjX1DVFcmB8fPGB4nHv4mBg3sHu2hwcAAEArENoCQAfx7xkr4rK/vRhzV21M948aPyC+dfLEGNmvPNtDAwAAoBUJbQEgxy1fXxXf/ucrcevUxen+oJ4l8fWT9o53TxwceXl52R4eAAAArUxoCwA5qr4hE398Yn78952vxvqqusjPizjr0NHxhXeOjx6lRdkeHgAAAG1EaAsAOejFReviv/72YkxdsDbdnzS8V3znlH1in+G9sj00AAAA2pjQFgByyIbqurj6X9Pj11PmREMmokdJYXzpXXvEmQePioKk1BYAAIBOT2gLQJeTyWTS1gOFBfmRS2O688WlccU/Xo6lFVXpsfdMGhKXvWdCDOpZmu3hAQAA0I6EtgB0Ges21cZvp8yNGx6ZE1W1DXHE7v3j2L0GxjF7DoyBPbIXjC5YvTEu//uLcf+0Fen+yL7l8a1TJsZR4wdkbUwAAABkj9AWgE5vdWVN/OqROfHrR+bG+uq6luN3v7wsXRL7jugd79hzYLxjr4ExYUjPyMtr+1YENXUN8YuHZ8f/3DsjDZGLCvLi/KPGxgXHjIvSooI2//kAAADkJqEtAJ3W8vVV8Yt/z4nfPzYvNtbUp8fGD+oeFx4zLsYO6B73vrI87n11WTy/cF064VeyXH339BjaqzTevlcS4A6KQ3fr1yYB6hNzVsd//fWFmLF8Q7p/yG5949un7BPjBnZv9Z8FAABAxyK0BaDTWbx2U1z30Oz40xPzo7quIT02cVjPuOiY3eOdEwZFftOEXhOH9YrPHrt7LKuoivteXR73vrIsHp65Mhavq4rfPzY/XcqLC+KIcUkbhUFx9J4D3nIbhaTq97u3vxJ/eXphut+vW3H814l7xan7DWuX6l4AAAByn9AWgE5j/qqN8bMHZ8bNTy+M2vpMemy/kb3jM2/fPY7eY8B2Q9Fkoq8Pv21kulTV1seUWSvjnleWx32vLE8nBfvXy8vSpbmNwrFpG4VBsdeQHjsctCYTjSVBbRLYrtlYmx5Lft4l79ojepcXt9o1AAAAoOMT2gLQ4c1cvj7+9/5Z8fepi6O+oTGsTdoafPrt4+LQsf12qoI1aYXw9j0HpUvmlEy8tLgi7nllWVqJu3kbhR/cPT2G9S6Ltzf1wT3kDdooTF+2Pr721xfjibmr0/09B/eI75w6MQ4Y1beVrgAAAACdidAWgA7rlSUV8ZP7ZsbtLy6JTGNWG0eNH5CGtQeOfuuBaBL2Ji0UkuVzx47fqo3CorWb4nePzUuXzdsoHLPnwBjQoyQ21dTH/9w3I65/aHbUNWSirKggPn/c7nHu4WOiqCD/rV8AAAAAOiWhLQAdznML1qZhbVIB2yzpVXvR28fFpOG92+znvr6NwiMzm9oovLosllVUt7RRSAp79x3eO1ZuqI6Fazalzz1uwqD4xnv3TqtzAQAA4I0IbQHoMJ6Yszp+fN+M+PeMlel+Eo6euM+QNKzdc3DPdh1L0goh6WubLJnMxJY2Cve+sjxeWLQuDZYTQ3uVpmHtO/ce3K7jAwAAoOMS2gKQ05IJvB6ZuSptM5CEtomC/Lw4ZfKwuOCYsTF2QPdsD3G7bRTq6hviffsPj24l/u8WAACAHedvkQDkbFibBJ8/vm9mS9VqUUFefOCAEXHB0WNjRN/yyFXNbRQAAABgVwhtAcgpDQ2ZuOulpWlY+/KSivRYSWF+GoJ+8qjdYkgvPWEBAADo3IS2AOSE6rr6+Ptzi+O6h2bHzOUb0mPlxQXx0UNGxXn/sVsM6FGS7SECAABAuxDaApBVFVW18afH58cNj8yJZRXV6bEepYVx7mGj49zDx0SfbsU+IQAAALoUoS0AWbF0XVX86pE58cfH58f66rr02MAeJfGxI8bEGQePjJ6lRT4ZAAAAuiShLQDtasay9WkLhL89tyhq6zPpsXEDu8cnjtwtTp48NEoKC3wiAAAAdGlCWwDaXCaTiSfnromfPzgr7n11ecvxg0b3iU8eOTbevufAyM/P80kAAACA0BaAtlTfkIm7X14WP39oVjw7f216LC8v4p0TBsUnjhwbB4zq4wMAAACA11FpC0Crq6qtj1ueWRS/+PfsmL2yMj1WXJgf799/eHz8P8bEbgO6u+oAAACwHUJbAFrNuo218fvH58WvHpkbKzdUp8d6lhbGRw8dFWcfNjoG9ih1tQEAAOBNCG0BeMsWrd0UNzw8J/70xPzYWFOfHhvaqzT+8z92i9MOGhHdS/zfDQAAAOwof4sGYJe9sqQirntodvxj6uKoa8ikx/Yc3CM+edRu8Z5JQ6OoIN/VBQAAgJ0ktAVgp2QymXh09qr4+YOz48HpK1qOHza2X3zyqLFx5O79Iy+ZbQwAAADYJUJbAHZ4crG7X16WVta+sGhdeiw/L+Ld+wyJTx65W0wa3tuVBAAAgFYgtAVguyqr6+KBaSvijheXxP2vLo/Kpn61pUX58aEDR8R5R+wWI/uVu4IAAADQioS2AGxh3abauO/VZXHHC0vT9gfVdQ0tjw3uWZpOLHbWoaOiX/cSVw4AAADagNAWgFi1oTptfXDHi0tjyqyVUVvfOKlYYmTf8nj3xMHxromDY9/hvSM/6YkAAAAAtBmhLUAXtXRdVdz10tK09cETc1ZHw2s5bew+sHsa1B4/cXBMGNLTxGIAAADQjoS2AF3IgtUb484XG4PaZ+av3eKxicN6xrv2Tipqh8S4gd2zNkYAAADo6oS2AJ3czOUb4s4Xl6StD15aXLHFY/uP7B3vnjgkbX0woq8JxQAAACAXCG0BOplMJhMvL6loqqhdmoa2zZJ2tAeP6Rfv3mdwHL/34BjUszSrYwUAAAC2JrQF6CRB7XML1rYEtfNXb2x5rKggLw4f1z/tUXvsXoOiX/eSrI4VAAAAeGNCW4AOHtY+MG1F/PCe6fH8wnUtx0sK8+PoPQakbQ/evueg6FVWlNVxAgAAADtOaAvQQcPah2asjB/ePT2tsE2UFRXEsRMGpRW1SWBbXuyPeAAAAOiI/I0eoIOFtY/MXJVW1j49b016rLQoP84+dHR84sjdtD4AAACATkBoC9BBPDprVVpZ+8Tc1S0tED5yyKj41FFjY0APfWoBAACgsxDaAuS4J+asTsPaR2evSveLC/PjjLeNjAuOHhsDe5Zme3gAAABAKxPaAuSopP1BEtY+PHNlul9ckB+nv21EXHD0uBjcS1gLAAAAnZXQFiDHJBOLJWHtg9NXpPtFBXnxwQNHxIXHjIthvcuyPTwAAACgjQltAXLECwvXpROM3ffq8nS/ID8vPnjA8DSsHdG3PNvDAwAAANqJ0BYgy15avC6uuWdG3P3ysnQ/Py/iffsPj0+/fVyM6tct28MDAAAA2pnQFiBLXl1aET+6Z0bc8eLSlrD2lMnD4tPv2D3G9BfWAgAAQFcltAVoZzOWrY9r7p0Rtz2/JN3Py4s4adLQ+Mw7do9xA7v7PAAAAKCLE9oCtJOZyzfE/9w7I/7x/OLIZBqPnThpSHzuHbvH7oN6+BwAAACAlNAWoI0tWL0xfnj39Pjbc4uioSmsfffEwfHZY3ePPQf3dP0BAACALQhtAdpIJpOJW55ZFJf//cWorKlPjx03YVB87tjdY++hvVx3AAAAYJuEtgBtoKKqNr721xfj1qmL0/23je4bl71nQuwzXFgLAAAAvDGhLUAre3remvjsn5+NhWs2RUF+Xnz+2N3j/KPHpdsAAAAAb0ZoC9BK6hsy8dP7Z8aP7p2Rbo/oWxY/On2/2H9kH9cYAAAA2GFCW4BWsGjtpvj8n5+LJ+auTvdP3W9YfPPkvaNHaZHrCwAAAOwUoS3AW3Tb80vi0luej4qquuheUhjfOmXvOHW/4a4rAAAAsEuEtgC7qLK6Lq74x0tx01ML0/3JI3rHj06fHKP6dXNNAQAAgF0mtAXYBS8uWhef+dOzMXtlZeTlRVx49Lj47LG7R1FBvusJAAAAvCVCW4Cd0NCQiV88PDuuumta1NZnYkiv0vjhaZPjkN36uY4AAABAqxDaAuyg5RVV8YW/TI1/z1iZ7r9r78Hx/96/T/QuL3YNAQAAgFYjtAXYAfe+siy+dPPzsbqyJkqL8uPrJ+0dpx80IvKS3ggAAAAArUhoC/AGqmrr47u3vxK/eXReuj9hSM/4nw/vF+MGdnfdAAAAgDYhtAXYjmlL16eTjU1btj7dP++IMfGld+0RJYUFrhkAAADQZoS2AK+TyWTid4/Ni2/f9krU1DVE/+4l8YMP7RtHjR/gWgEAAABtTmgLsJmkZ+2Xb54a97yyPN0/Zo8BcdUH902DWwAAAID2ILQFaPLwjJVx8U3PxfL11VFckB+XnrBnnHPYaJONAQAAAO1KaAt0eXUNEd+7a3r84uG56bVIJhn78Yf3i72G9Ozy1wYAAABof0JboEubuXxDXPNiQSyobAxszzx4ZHztxAlRVmyyMQAAACA7hLZAl7NifXXc9vziuHXq4nhm/tqIyIveZUXxvQ9MiuP3Hpzt4QEAAABdnNAW6BLWbaqNu15cmga1U2atjIZM4/G8vIi9ezfEz847NEb065HtYQIAAAAIbYHOa1NNfdz76rK49bnF8cC0FVFT39Dy2L4jesfJ+w6N4ycMiKf+fW8M7lma1bECAAAANFNpC3QqtfUN8fCMlfH35xbF3S8vi8qa+pbHxg/qHu/dd2ictO/QGNWvW+P5tbVZHC0AAADA1oS2QIfX0JCJJ+auTlsf3PHCkliz8bUgdnifsjSofe/kobHn4J5ZHScAAADAjhDaAh1SJpOJFxdVpBW1/3x+SSytqGp5rH/3knjPpCFpRe3+I3tHXtK4FgAAAKCDENoCHcrM5RvSitp/TF0cc1ZWthzvUVoY7544ON6777A4ZLe+UViQn9VxAgAAAOwqoS2w09Ztqo1bnlkYVbUNUVaUH2XFBVFWXBhlRQVRXlwQpUUFLduNjzXuF+1ikLpo7aY0pE0mFHt5SUXL8dKi/HjHXoPS9gdH7zEgSgoLfJoAAABAhye0BXaqJcE/nl8S3/rny7FiffXO/4GTn9cS4LaEu8VN4W5Rc/Cb37JdXJAXj85eFU/OXbPFaxw5fkAa1B43YVB0K/HHGAAAANC5SDuAHTJvVWV87W8vxr9nrEz3d+vfLQ4Y1Sc21tZHVU19bKypj0219bGped20vbGmLhoyja9R15CJ9VV16bIzkpa0B4/pm7Y+SFog9OlW7FMDAAAAOi2hLfCGauoa4vp/z47/uXdGVNc1RHFBflxwzNg4/+ixO9SOIKnOralviKqahjTITULczcPdJOytagl4tw5+k3D4PZOGxuBepT4pAAAAoEsQ2gLb9cSc1fFff30hZizfkO4fNrZffPuUibHbgO47fNXy8vLScDdZekWRqw0AAADwJoS2wFbWVNbEd+94JW56amG6369bcXztPXvFKZOHpSEsAAAAAG1HaAts0crg/55ZFFfe/kqsrqxJj334bSPiknftGb3L9ZEFAAAAaA9CWyA1a8WGtBXCY7NXp/vjB3WPK0/dJw4c3dcVAgAAAGhHQlvo4pJJwP73gVlx7QOz0gnDSovy4zPv2D3OO2K3KC7Mz/bwAAAAALocoS10YY/MXBlf+9uLMWdlZbp/9B4D4lsnT4wRfcuzPTQAAACALktoC13Qyg3V8e1/vhx/e25xuj+wR0l8/aS944R9BptoDAAAACDLhLbQhTQ0ZOLGpxbEd29/JSqq6iIvL+KsQ0bFF47fI3qWFmV7eAAAAAAIbaHrmLZ0fTrR2FPz1qT7ew/tmU40tu+I3tkeGgAAAACbUWkLndymmvr40b0z4hf/nh11DZkoLy6Ii48bH+ccNjoKC0w0BgAAAJBrhLbQid3/6vK47O8vxsI1m9L9d04YFN94794xtHdZtocGAAAAwHbkRJndT3/60xg9enSUlpbGwQcfHE888cQbnr927dq48MILY8iQIVFSUhLjx4+P22+/veXx7373u3HQQQdFjx49YuDAgXHKKafEtGnT2uGdQG5YVlEVF/zh6Tj310+mge3QXqVx/VkHxnVnHSiwBQAAAMhxWa+0vfHGG+Piiy+Oa6+9Ng1sr7nmmjj++OPTkDUJXF+vpqYmjjvuuPSxm2++OYYNGxbz5s2L3r1f68v54IMPpqFuEtzW1dXFV7/61XjnO98ZL7/8cnTr1q2d3yG0r5ufXhjfuPWl2FBdFwX5efGxw0fH544dH91Ksn67AwAAALADsp7iXH311fHxj388zj333HQ/CW9vu+22uOGGG+IrX/nKVucnx1evXh1TpkyJoqLG2e6TKt3N3XnnnVvs//rXv05D3qeffjqOPPLIrV6zuro6XZpVVFSk69ra2nSBjqCyui6+8Y9X4m9Tl6T7k4b3jG+9d0JMGNIzIjJ+l7ej+R53r0Pn536HrsP9Dl2H+x26jtpO8vf3HR1/XiaTyUSWJFWz5eXlacVs0sKg2dlnn522QPj73/++1XNOOOGE6Nu3b/q85PEBAwbEGWecEZdcckkUFBRs8+fMnDkzdt9993jhhRdi4sSJWz3+jW98I6644oqtjv/xj39Mfw7kukWVEb+eXhDLq/IiLzJxwoiGOHZYJvLzsj0yAAAAAJpt3LgxzTLXrVsXPXsmhXY5WGm7cuXKqK+vj0GDBm1xPNl/9dVXt/mc2bNnx3333Rdnnnlm2sc2CWQvuOCCNKX++te/vtX5DQ0N8bnPfS4OP/zwbQa2iUsvvTRt0bB5pe2IESPimGOOiX79+r3l9wltJfk3lz89uTCueXJa1NQ1xKCeJfHDD06Kg0b3cdF3UPJnx9133522XWmu3gc6J/c7dB3ud+g63O/QddR2kr+/N3/DP+fbI+ysJIRNWh1cd911aWXtAQccEIsWLYqrrrpqm6Ft0tv2xRdfjIcffni7r5lMZpYsr5f8AnTkXwI6t4qq2rj0lhfjtucb2yEcs8eA+MGHJkffbsXZHlqH5H6HrsP9Dl2H+x26Dvc7dB1FHTyv29GxZzW07d+/fxq8Llu2bIvjyf7gwYO3+ZwhQ4akb27zVgh77bVXLF26NG23UFz8WmB10UUXxT//+c946KGHYvjw4W34TqB9Pb9wbVz0x2dj/uqNUZifF5e8a8/4zyPGRL5+CAAAAAAdXn42f3gSsCaVsvfee+8WlbTJ/qGHHrrN5yRtDpKWCMl5zaZPn56Guc2BbfKV8SSw/etf/5q2UhgzZkw7vBtoe8nv9g0Pz4n3/2xKGtgO610Wf/nUofHxI3cT2AIAAAB0ElkNbRNJL9nrr78+fvOb38Qrr7wS559/flRWVsa5556bPn7WWWelPWebJY+vXr06PvvZz6Zh7W233RZXXnll2gahWbL9+9//Pp1IrEePHmkVbrJs2rQpK+8RWsPajTXx8d8+Hd/858tRW5+Jd+09OG7/zH/EfiP1rwUAAADoTLLe0/a0006LFStWxOWXX54Gq5MnT44777yzZXKy+fPnR37+a9lyMkHYXXfdFZ///Odj0qRJMWzYsDTAveSSS1rO+dnPfpaujz766C1+1q9+9as455xz2u29QWt5et7q+PQfn43F66qiuCA/vvaeveKjh4yKvLw8FxkAAACgk8l6aJtIWhkky7Y88MADWx1LWic89thjb/gVcugMGhoy8fOHZsf3/zUt6hsyMbpfefzkjP1j4rBe2R4aAAAAAJ05tAW2tnJDdVx809R4aPqKdP+kfYfGladOjB6lHXeGRAAAAADenNAWctCjs1bFZ//8bCxfXx0lhflxxXv3jtMOGqEdAgAAAEAXILSFHJK0QPjxfTPif+6dEQ2ZiHEDu8dPz9g/9hjcI9tDAwAAAKCdCG0hRyyrqIrP/fm5eHT2qnT/gwcMjytO3jvKi92mAAAAAF2JNAhywIPTV8TFNz4Xqyprory4IL5z6sQ4db/h2R4WAAAAAFkgtIUsqqtviB/cPT1+9sCsdH/PwT3ip2fuH2MHdPe5AAAAAHRRQlvIksVrN8Vn/vRsPDVvTbr/kUNGxtdOnBClRQU+EwAAAIAuTGgLWXDPy8viizdPjbUba6NHSWH8v/dPihMnDfFZAAAAACC0hfZUU9cQ37vz1fjlw3PS/UnDe8VPPrx/jOxX7oMAAAAAIKXSFtrJqg3V8YnfPR1PN7VD+M8jxsQl79ozigvzfQYAAAAAtBDaQjuYsWx9fOw3T8aC1ZuiR2lhXP2hyXHchEGuPQAAAABbEdpCG/v3jBVxwR+eifVVdTGyb3nccM5BMW5gd9cdAAAAgG0S2kIb+sPj8+Lyv78U9Q2ZOGh0n/j5Rw+Mvt2KXXMAAAAAtktoC20gCWmvvP2VlgnHTt1vWPy/9+8TJYUFrjcAAAAAb0hoC62ssrouPvvnZ+OeV5an+184bnxc9PZxkZeX51oDAAAA8KaEttCKlqzbFP/566fi5SUVUVyYHz/44L5x0r5DXWMAAAAAdpjQFlrJCwvXxXm/fTKWVVRH/+7Fcd1ZB8b+I/u4vgAAAADsFKEttIK7Xloan/vzc7Gptj7GD+oevzz7oBjRt9y1BQAAAGCnCW3hLchkMnH9v2fHd+94NTKZiCPHD4ifnLFf9Cwtcl0BAAAA2CVCW9hFtfUNcdnfXow/P7kg3f/oIaPi6ydNiMKCfNcUAAAAgF0mtIVdsG5jbZz/h6djyqxVkZ8Xcdl7JsQ5h42OvLw81xMAAACAt0RoCztp3qrKOPfXT8bsFZXRrbggfnzGfvH2PQe5jgAAAAC0CqEt7IQn5qyOT/7uqVizsTaG9iqNX55zUOw1pKdrCAAAAECrEdrCDvrrswvjkptfiJr6hth3eK+4/qwDY2DPUtcPAAAAgFYltIU30dCQiR/eMz1+fN/MdP/dEwfH1R+aHGXFBa4dAAAAAK1OaAtvoKq2Pr74l6nxz+eXpPsXHD02vvjOPSI/mX0MAAAAANqA0Ba2Y8X66vjE756KZ+evjaKCvPjOqfvEhw4c4XoBAAAA0KaEtrAN05auj4/9+slYtHZT9Corip9/9IA4ZLd+rhUAAAAAbU5oC6/z4PQVceEfnokN1XUxpn+3+OXZB8ZuA7q7TgAAAAC0C6EtbOZ3j86Nb/zj5ahvyMTBY/rGtR85IPp0K3aNAAAAAGg3Qlto8r07X42fPTAr3f7AAcPjylP3ieLCfNcHAAAAgHYltIWIuH/a8pbA9svv2iPOP2ps5OXluTYAAAAAtDuhLV1eRVVtfPWWF9LrcN4RY+KCo8d1+WsCAAAAQPb47jdd3ndvfzWWrKuKUf3K4wvv3KPLXw8AAAAAsktoS5f2yMyV8acn5qfb33v/pCgrLsj2kAAAAADo4oS2dFmV1XVxyf89n26fdeioOGS3ftkeEgAAAAAIbem6rrprWixcsymG9S6LL79rz2wPBwAAAABSKm3pkp6Yszp+PWVuuv3/3r9PdC8xJx8AAAAAuUFoS5dTVVvf0hbh9INGxH/sPiDbQwIAAACAFkJbupyr754ec1ZWxuCepfHVE/fK9nAAAAAAYAtCW7qUZ+eviV/8e3a6feX7JkbP0qJsDwkAAAAAtiC0pcuorquPL9/8fDRkIt6337B4+56Dsj0kAAAAANiK0JYu48f3zowZyzdE/+4lcflJE7I9HAAAAADYJqEtXcKLi9bFzx6clW5/+5S9o3d5cbaHBAAAAADbJLSl06utb4gv3fx81Ddk4sR9hsS7Jg7J9pAAAAAAYLuEtnR6P3tgVryypCL6lBfFFSfvne3hAAAAAMAbEtrSqU1buj5+fN+MdPsb79077WcLAAAAALlMaEunVVffEF++eWrU1mfi2L0GxXv3HZrtIQEAAADAmxLa0mn98uE5MXXhuuhZWhjfOXVi5OXlZXtIAAAAAPCmhLZ0SrNWbIgf3D093b7sPRNiUM/SbA8JAAAAAHaI0JZOp74hE1+++fmoqWuII8cPiA8cMDzbQwIAAACAHSa0pdP57aNz4+l5a6J7SWF89337aIsAAAAAQIcitKVTmbeqMv77zmnp9qUn7BnDepdle0gAAAAAsFOEtnQaDQ2Z+Mr/vRCbauvj0N36xYcPGpntIQEAAADAThPa0mn86cn58ejsVVFWVBDfe/+kyM/Py/aQAAAAAGCnCW3pFBat3RTfvf3VdPvL79ojRvYrz/aQAAAAAGCXCG3p8DKZTFx6ywuxobouDhzVJ84+dHS2hwQAAAAAu0xoS4d389ML46HpK6KkMD++9wFtEQAAAADo2IS2dGjLKqriW/98Od2++LjxMXZA92wPCQAAAADeEqEtHbotwn/99cWoqKqLfYf3iv88Yky2hwQAAAAAb5nQlg7r1qmL455XlkVRQV789wf2jcICv84AAAAAdHxSLjqklRuq4xu3vpRuf+btu8ceg3tke0gAAAAA0CqEtnRIX7/1pVizsTYmDOkZnzp6bLaHAwAAAACtRmhLh3Pni0vitueXREF+0hZhUhRpiwAAAABAJyK0pUNZU1kTX/tbY1uE848aGxOH9cr2kAAAAACgVQlt6VC+9c+X0362uw/sHp9+x7hsDwcAAAAAWp3Qlg7jvleXxS3PLor8vEjbIpQUFmR7SAAAAADQ6oS2dAjrNtXGpbe8kG6f9x+7xX4j+2R7SAAAAADQJoS2dAjfvf2VWFZRHWP6d4uLjxuf7eEAAAAAQJsR2pLz7nl5Wfz5yQWR19QWobRIWwQAAAAAOi+hLTlt6bqq+NLNU9Pt/zx8TBw0um+2hwQAAAAAbUpoS86qb8jE5298LtZsrI2Jw3rGl9+1Z7aHBAAAAABtTmhLzrr2wVnx6OxVUV5cEP9z+n5RXOjXFQAAAIDOTwpGTnpm/pq4+u7p6fY3T54Yuw3onu0hAQAAAEC7ENqScyqqauMzf3o2bY9w8uSh8f79h2V7SAAAAADQboS25JRMJhP/9dcXY+GaTTGib1l8+5SJkZeXl+1hAQAAAEC7EdqSU25+emH8Y+riKMzPS/vY9igtyvaQAAAAAKBdCW3JGbNWbIiv3/pSun3xO8fHfiP7ZHtIAAAAANDuhLbkhOq6+rSP7caa+jh8XL/41JFjsz0kAAAAAMgKoS054b/vnBYvLa6IPuVFcfWHJkd+vj62AAAAAHRNQluy7v5py+OXD89Jt7//wX1jUM/SbA8JAAAAALJGaEtWLV9fFV+8aWq6fc5ho+Mdew3yiQAAAADQpQltyZqGhkx84aapsaqyJvYa0jO+8u49fRoAAAAAdHlCW7Lm+n/Pjn/PWBmlRfnx4w9PjtKiAp8GAAAAAF2e0JasmLpgbVx117R0+xsn7R3jBvbwSQAAAACA9ghkw4bquvjMn5+NuoZMnLjPkDjtoBE+CAAAAABootKWdnf5316Meas2xrDeZXHl+/aJvLw8nwIAAAAANBHa0q5ueWZh3PLsosjPi/jR6ZOjV1mRTwAAAAAANiO0pd3MXVkZl/3txXT7c8eOjwNH93X1AQAAAOB1hLa0i5q6hvjsn5+Nypr6eNuYvnHhMeNceQAAAADYBqEt7eIHd0+LqQvXRe/yorQtQkHSHwEAAAAA2IrQljb30PQV8fMHZ6fb33v/pBjSq8xVBwAAAIDtENrSplZuqI6Lb5qabn/kkJFx/N6DXXEAAAAAeANCW9pMQ0MmvviXqWlwO35Q9/jaiRNcbQAAAAB4E0Jb2syvpsyNB6atiJLC/Pjxh/eP0qICVxsAAAAA3oTQljbx4qJ18f/ueCXd/tp7JsQeg3u40gAAAACwA4S2tLrK6rr4zJ+ejdr6TLxzwqD4yMEjXWUAAAAA2EFCW1rdN259KWavrIwhvUrjvz8wKfLy8lxlAAAAANhBQlta1a1TF8dfnl4Y+XkRPzxtcvQuL3aFAQAAAGAnCG1pNQtWb4z/uuWFdPuiY8bFIbv1c3UBAAAAYCcJbWkVtfUN8Zk/Pxvrq+vigFF94jPv2N2VBQAAAIBdILSlVVxzz/R4dv7a6FFaGD86fXIUFvjVAgAAAIBdIVnjLZsyc2X87wOz0u3/975JMbxPuasKAAAAALtIaMtbsnZjTXz+pucik4k4/aARceKkIa4oAAAAALwFQlvekj89sSCWVVTHbv27xeUnTXA1AQAAAOAtEtqyyzKZTPzlqQXp9qeOGhvlxYWuJgAAAAC8RUJbdtnT89bE7JWVUV5cECdoiwAAAAAArUJoyy678cnGKtsT9xkS3UtU2QIAAABApwltf/rTn8bo0aOjtLQ0Dj744HjiiSfe8Py1a9fGhRdeGEOGDImSkpIYP3583H777W/pNdk5G6rr4rYXlqTbpx00wuUDAAAAgM4S2t54441x8cUXx9e//vV45plnYt99943jjz8+li9fvs3za2pq4rjjjou5c+fGzTffHNOmTYvrr78+hg0btsuvyc67/fklsbGmPp2A7IBRfVxCAAAAAOgsoe3VV18dH//4x+Pcc8+NCRMmxLXXXhvl5eVxww03bPP85Pjq1avjb3/7Wxx++OFpNe1RRx2VBrO7+prsvBubJiD74IEjIi8vzyUEAAAAgFaS1UakSdXs008/HZdeemnLsfz8/Dj22GPj0Ucf3eZzbr311jj00EPT9gh///vfY8CAAXHGGWfEJZdcEgUFBbv0mtXV1enSrKKiIl3X1tamC1uataIynYSsID8v3jtpkGtEh9Z8j7vXofNzv0PX4X6HrsP9Dl1HbSf5+/uOjj+roe3KlSujvr4+Bg0atMXxZP/VV1/d5nNmz54d9913X5x55plpH9uZM2fGBRdckL7hpB3Crrzmd7/73bjiiiu2On7//fenFbps6dZ5SYF2fuzVqz6e+ve9Lg+dwt13353tIQDtxP0OXYf7HboO9zt0HXd38L+/b9y4MfdD213R0NAQAwcOjOuuuy6trD3ggANi0aJFcdVVV6Wh7a5IqnKTHribV9qOGDEijjnmmOjXr18rjr7jq61viG99/6GkTjoueNf+cdyEgdkeErwlyT/4JH/gJ72yi4qKXE3oxNzv0HW436HrcL9D11HbSf7+3vwN/5wObfv3758Gr8uWLdvieLI/ePDgbT5nyJAh6QeTPK/ZXnvtFUuXLk1bI+zKa5aUlKTL6yU/pyP/ErSFB2Ysi5UbaqJ/9+I4buKQKCrIeltkaBXud+g63O/Qdbjfoetwv0PXUdTB87odHXtWE7fi4uK0Uvbee+/dopI22U/61m5LMvlY0hIhOa/Z9OnT0zA3eb1deU123E1NE5C9b//hAlsAAAAAaANZL5NM2hJcf/318Zvf/CZeeeWVOP/886OysjLOPffc9PGzzjpri0nFksdXr14dn/3sZ9Ow9rbbbosrr7wynZhsR1+TXbN8fVXc9+rydPtDBw53GQEAAACgDWS9p+1pp50WK1asiMsvvzxtcTB58uS48847WyYSmz9/fuTnv5YtJ71m77rrrvj85z8fkyZNimHDhqUB7iWXXLLDr8mu+eszi6K+IRP7j+wd4wb2cBkBAAAAoDOGtomLLrooXbblgQce2OpY0ubgscce2+XXZOdlMpmW1ggfOnCESwgAAAAAnbU9Ah3DM/PXxKwVlVFWVBAnThqS7eEAAAAAQKcltGWH3PTkwnSdBLY9SjvuDH0AAAAAkOuEtrypyuq6+Ofzi9NtrREAAAAAoG0JbXlTt72wJCpr6mNM/25x0Og+rhgAAAAAtCGhLW/qL00TkH3wwOGRl5fnigEAAABAGxLa8oZmr9gQT85dE/l5Ee/ff7irBQAAAABtTGjLG7rpqcYJyI7eY2AM6lnqagEAAABAGxPasl119Q3xf880hrYmIAMAAACA9iG0ZbsenL4iVqyvjn7diuPtew50pQAAAACgHQht2a4bn2ycgOzU/YZFcaFfFQAAAABoD5I4timpsL3v1eXp9ocOGuEqAQAAAEA7EdqyTX97dlHUNWRi8ojeMX5QD1cJAAAAANqJ0JatZDKZuPGpxtYIJiADAAAAgPYltGUrzy5YGzOXb4jSovw4ad8hrhAAAAAAtKPCnX1CTU1N/O1vf4tHH300li5dmh4bPHhwHHbYYXHyySdHcXFxW4yTdvSXpirbE/YZEj1Ki1x7AAAAAMjVStuZM2fGXnvtFWeffXY8++yz0dDQkC7J9llnnRV77713eg4d18aauvjH1CXpttYIAAAAAJDjlbbnn39+7LPPPmlI27Nnzy0eq6ioSIPbCy+8MO66667WHift5PYXlsaG6roY3a88Dh7T13UHAAAAgFwObR955JF44okntgpsE8mxb33rW3HwwQe35vhoZzc1tUb44IEjIi8vz/UHAAAAgFxuj9C7d++YO3fudh9PHkvOoWOas7IynpizOvLzIt6///BsDwcAAAAAuqSdqrQ977zz0hYIl112WbzjHe+IQYMGpceXLVsW9957b3z729+OT3/60201VtppArKjxg+Iwb1KXW8AAAAAyPXQ9pvf/GZ069YtrrrqqvjCF77Q8vX5TCYTgwcPjksuuSS+/OUvt9VYaUN19Q3xf88sTLdNQAYAAAAAHSS0TSTBbLLMmTMnli5dmh5LAtsxY8a0xfhoJw/NWBHLKqqjb7fieMdejRXUAAAAAEAHCG2bJSGtoLbzuOnJxirbU/cbFsWFO9XqGAAAAABoRa2azi1YsCA+9rGPteZL0g5WbaiOe15Zlm5rjQAAAAAAnSi0Xb16dfzmN79pzZekHfz12UVR15CJfYf3ij0G93DNAQAAAKCjtEe49dZb3/Dx2bNnv9Xx0M6SSeRufHJBuv2hg0a4/gAAAADQkULbU045JfLy8tKgb3uSx+k4pi5cFzOWb4iSwvw4ad+h2R4OAAAAAHR5O9UeYciQIXHLLbdEQ0PDNpdnnnmmy1/Qjqa5yvaEfYZEz9KibA8HAAAAALq8nQptDzjggHj66ae3+/ibVeGSWzbV1Mc/pi5Ot01ABgAAAAAdsD3Cl770paisrNzu4+PGjYv777+/NcZFO7jjxSWxobouRvYtj4PH9HXNAQAAAKCjhbb/8R//8YaPd+vWLY466qi3OibauTXCBw8YHvn5ehEDAAAAQIdrj0DnMXdlZTw+Z3Uk88Z94MDh2R4OAAAAAPBWQtu5c+fGOeeck05MVlZWFvvss0/87ne/25WXIktufnphuj5y9wExpFeZzwEAAAAAOmpo++ijj8YhhxwSI0eOjEceeSRWr14dP/vZz+Kqq66KX/7yl20zSlpVfUOmJbQ1ARkAAAAAdODQNglo3/e+98UNN9wQ3/zmN2O33XZLK22POOKI+POf/5weS5x++umxfPnythozb9FDM1bE0oqq6FNeFMdOGOh6AgAAAEBHnYjsxz/+cRxzzDFxwgknxMSJE2Pjxo1bPL5w4cJYsWJFDBo0KA1wf/KTn7T2eGkFf3mqcQKyU/YbFiWFBa4pAAAAAHTUStt//vOfccYZZ6TbX/jCF6K0tDS+/e1vxw9/+MMYM2ZMfOUrX4l+/frFRRddFDfeeGNbjZm3YNWG6rj75WXpttYIAAAAANDBK23nzZuXtkRorrpNetkeddRR6f6RRx6Z9rm97LLLYvfdd49169bF0qVLY/DgwW0zcnbJ355bHLX1mZg0vFfsNaSnqwgAAAAAHbnSNulfm/S1TSQ9a/PzX3t6Xl5e2i6hsrIy6uvro6GhIQoLdyoTpo1lMpmW1ggfPHCE6w0AAAAAHT203XfffePpp59Ot0899dT4xCc+kbZB+Mc//hHvf//747DDDkvbIzzzzDPRv3//dCF3PL9wXby6dH2UFObHe/cdmu3hAAAAAABvNbQ988wz08nFkkraH/zgB2l/26uvvjouv/zymDBhQvztb39raZ1w+umn78xL0w5uaqqyfffEwdGrrMg1BwAAAIActFP9Cz70oQ+lfWzPP//8+PnPf572r02Wzf3yl7+Me++9N6ZOndraY+Ut2FRTH7c+tzjdNgEZAAAAAHSSStukb+3//d//xUsvvZROPHbHHXfE2rVro7q6Op566qk455xz4oorrojbbrtNa4Qcc+dLS2J9dV2M6FsWh+zWL9vDAQAAAAC2Y6dnCkt61j700EPxi1/8Ir7zne/ECy+8kLZLGDduXJxyyinx/PPPR+/evXf2ZWljNz25MF1/8IARkZ+f53oDAAAAQGcJbRMFBQXxyU9+Ml3IffNXbYxHZ6+KvLyI9x8wPNvDAQAAAABaqz0CHdNfnm6cgOyIcf1jWO+ybA8HAAAAAGjtStv99tsv7W/7esmx0tLStFVC0t/2mGOO2ZWXpxXVN2Ti5qcbWyOcdtAI1xYAAAAAOmOl7bve9a6YPXt2dOvWLQ1mk6V79+4xa9asOOigg2LJkiVx7LHHxt///vfWHzE75eGZK2PJuqroXV4Ux00Y5OoBAAAAQGestF25cmV84QtfiMsuu2yL49/+9rdj3rx58a9//Su+/vWvx7e+9a04+eSTW2us7IKbnmxsjXDK5GFRUljgGgIAAABAZ6y0vemmm+LDH/7wVsdPP/309LFE8vi0adPe+gjZZasra+JfLy9Ntz90oNYIAAAAANBpQ9ukb+2UKVO2Op4cSx5LNDQ0tGyTHf+Yujhq6zMxcVjPmDC0p48BAAAAADpre4RPf/rT8alPfSqefvrptIdt4sknn4xf/OIX8dWvfjXdv+uuu2Ly5MmtO1p2yoPTV6Tr9+471JUDAAAAgM4c2n7ta1+LMWPGxE9+8pP43e9+lx7bY4894vrrr48zzjgj3U9C3fPPP791R8sOq61viMdnr0q3Dx/X35UDAAAAgM4c2ibOPPPMdNmesrKyXX1pWsHUBWujsqY++nYrjr0Ga40AAAAAAJ26p23SCuHxxx/f6nhy7KmnnmqNcfEWPTKzscr20N36RX5+nusJAAAAAJ05tL3wwgtjwYIFWx1ftGhR+hjZ98jMlelaawQAAAAA6AKh7csvvxz777//Vsf322+/9DGya2NNXTy7YE26ffi4fj4OAAAAAOjsoW1JSUksW7Zsq+NLliyJwsJdbpNLK3lizuqorc/EsN5lMbJvuesKAAAAAJ09tH3nO98Zl156aaxbt67l2Nq1a+OrX/1qHHfcca05PnbBlFmN/WyPGNc/8vL0swUAAACAjmSXymK///3vx5FHHhmjRo1KWyIknnvuuRg0aFD87ne/a+0xspMentHYz/YwrREAAAAAoGuEtsOGDYvnn38+/vCHP8TUqVOjrKwszj333Pjwhz8cRUVFrT9Kdtjqypp4eUlFun3Y2P6uHAAAAAB0MLvcgLZbt27xiU98onVHw1v2aFNrhD0H94gBPUpcUQAAAADorKHtrbfeusMv+t73vndXx8Nb9PDMptYIqmwBAAAAoHOHtqeccsoW+8kEV5lMZov9ZvX19a01PnbSlFmNoe3h+tkCAAAAQIeUv6MnNjQ0tCz/+te/YvLkyXHHHXfE2rVr0+X222+P/fffP+688862HTHbtXDNxpi3amMU5OfF28b0daUAAAAAoKv0tP3c5z4X1157bRxxxBEtx44//vgoLy9P+9y+8sorrTlGdtCUmY39bCeP6B09Sk0IBwAAAACdutJ2c7NmzYrevXtvdbxXr14xd+7c1hgXb6Gf7eFj+7l+AAAAANCVQtuDDjooLr744li2bFnLsWT7S1/6UrztbW9rzfGxg5L+wlNmNVbaHjauv+sGAAAAAF0ptL3hhhtiyZIlMXLkyBg3bly6jBgxIhYtWhS/+MUvWn+UvKnpyzbEyg3VUVZUEPuN3LoKGgAAAADoxD1tk5D2+eefj3vuuaelf+1ee+0Vxx57bOTl5bX2GNmJ1ggHjekbJYUFrhkAAAAAdKXQNnHffffF/fffH8uXL4+GhoZ47rnn4k9/+lNLJS7ta4p+tgAAAADQdUPbK664Ir75zW/GgQceGEOGDFFdm2W19Q3x+JzV6fbh+tkCAAAAQNcLba+99tr49a9/HR/96Edbf0TstOcXro0N1XXRu7woJgzp6QoCAAAAQFebiKympiYOO+yw1h8Nu+SRmavS9WFj+0V+vp7CAAAAANDlQtvzzjsv/vjHP7b+aNgljzT1sz1sbH9XEAAAAAC6YnuEqqqquO666+Kee+6JSZMmRVFR0RaPX3311a01Pt7Expq6eHb+2nT7CP1sAQAAAKBrhrbPP/98TJ48Od1+8cUXt3gsL8/X89vTk3PXRE19QwzrXRaj+pW3688GAAAAAHIktL3//vtbfyTskiktrRH6CcwBAAAAoKv2tCV3PDKrMbQ9Ynf9bAEAAACgMxDadmBrKmvipcUV6fahY/tlezgAAAAAQCsQ2nZgj85eFZlMxPhB3WNgj9JsDwcAAAAAaAVC2w7skZZ+tlojAAAAAEBnIbTtwKbMWpWujxgntAUAAACAzkJo20EtWrsp5qysjIL8vDh4t77ZHg4AAAAA0EqEth28NcKk4b2iR2lRtocDAAAAALQSoW0HNaUptNUaAQAAAAA6F6FtB5TJZOKRpn62JiEDAAAAgM5FaNsBzVi+IVasr47SovzYf1TvbA8HAAAAAGhFQtsO3M/2oNF9o6SwINvDAQAAAABakdC2A3pkZmNrhMPH9c/2UAAAAACAVia07WDq6hvi8dlNoe1YoS0AAAAAdDZC2w7m+UXrYn11XfQqK4oJQ3tmezgAAAAAQCsT2nYwU5r62R42tl8U5OdlezgAAAAAQCsT2nYwDzeHtvrZAgAAAECnJLTtQDbV1Mcz89am24eP7Zft4QAAAAAAbUBo24E8NW911NQ3xNBepTGmf7dsDwcAAAAAaANC2w7aGiEvTz9bAAAAAOiMhLYdyJSZq9L14eO0RgAAAACAzkpo20Gs3VgTLy5el24fNrZ/tocDAAAAALQRoW0H8djsVZHJROw+sHsM6lma7eEAAAAAAG1EaNvB+tkePk6VLQAAAAB0ZkLbDtbP9rCx+tkCAAAAQGcmtO0AFq/dFLNXVkZ+XsQhQlsAAAAA6NSEth3AI02tESYN7x09S4uyPRwAAAAAoA0JbTuAKbMaWyMcPk5rBAAAAADo7IS2OS6TybRU2pqEDAAAAAA6P6Ftjpu1YkMsX18dJYX5sf/IPtkeDgAAAADQxoS2Oe7hGY1VtgeN7hulRQXZHg4AAAAA0MaEtjnukaZ+tofpZwsAAAAAXUJOhLY//elPY/To0VFaWhoHH3xwPPHEE9s999e//nXk5eVtsSTP29yGDRvioosuiuHDh0dZWVlMmDAhrr322uho6uob4rHZjaHtEeP6Z3s4AAAAAEA7KIwsu/HGG+Piiy9OQ9UksL3mmmvi+OOPj2nTpsXAgQO3+ZyePXumjzdLgtvNJa933333xe9///s0DP7Xv/4VF1xwQQwdOjTe+973RkfxwqJ1sb6qLnqWFsbeQ3tlezgAAAAAQFcIba+++ur4+Mc/Hueee266n4S3t912W9xwww3xla98ZZvPSULawYMHb/c1p0yZEmeffXYcffTR6f4nPvGJ+PnPf55W8G4rtK2urk6XZhUVFem6trY2XbLl39OXp+uDx/SNhvq6aKjP2lCg02q+x7N5rwPtw/0OXYf7HboO9zt0HbWd5O/vOzr+rIa2NTU18fTTT8ell17aciw/Pz+OPfbYePTRR7f7vKT9wahRo6KhoSH233//uPLKK2Pvvfduefywww6LW2+9NT72sY+l1bUPPPBATJ8+PX74wx9u8/W++93vxhVXXLHV8fvvvz/Ky8sjW/7xUtK9Ij96VS2J229fnLVxQFdw9913Z3sIQDtxv0PX4X6HrsP9Dl3H3R387+8bN27M/dB25cqVUV9fH4MGDdrieLL/6quvbvM5e+yxR1qFO2nSpFi3bl18//vfT0Pal156Ke1hm/jxj3+cVtcm+4WFhWkQfP3118eRRx65zddMQuOkpcLmlbYjRoyIY445Jvr16xfZUFVbH1968v6IaIiPn3Rk7DagW1bGAZ1d8i9cyR/4xx13XBQVFWV7OEAbcr9D1+F+h67D/Q5dR20n+ft78zf8c749ws469NBD06VZEtjutddeafuDb33rWy2h7WOPPZZW2yYVuQ899FBceOGFadVtUsX7eiUlJenyeskvQLZ+CR6fuy5q6hpicM/SGD+k11Z9e4HWlc37HWhf7nfoOtzv0HW436HrKOrgf3/f0bFnNbTt379/FBQUxLJly7Y4nuy/Uc/a17/R/fbbL2bOnJnub9q0Kb761a/GX//61zjxxBPTY0lV7nPPPZdW5W4rtM1Fj8xama4PG9dPYAsAAAAAXUjSNDVriouL44ADDoh777235VjSpzbZ37ya9o0k7RVeeOGFGDJkyBaThyUtETaXhMPJa3cUU2Y2hrZHjOuf7aEAAAAAAO0o6+0Rkl6yZ599dhx44IHxtre9La655pqorKyMc889N338rLPOimHDhqWThSW++c1vxiGHHBLjxo2LtWvXxlVXXRXz5s2L8847L328Z8+ecdRRR8WXvvSlKCsrS9sjPPjgg/Hb3/42rr766ugI1m2sjecXrUu3DxfaAgAAAECXkvXQ9rTTTosVK1bE5ZdfHkuXLo3JkyfHnXfe2TI52fz587eoml2zZk18/OMfT8/t06dPWqk7ZcqUmDBhQss5f/7zn9PJxc4888xYvXp1Gtx+5zvfiU996lPRETw6e1VkMhFjB3SLQT1Lsz0cAAAAAKArhbaJiy66KF225YEHHthi/4c//GG6vJGkH+6vfvWr6KimNPWz1RoBAAAAALqerPa0Zdseaepne5jWCAAAAADQ5Qhtc8zSdVUxa0Vl5OdFHLJbv2wPBwAAAABoZ0LbHK2y3Wd47+hVVpTt4QAAAAAA7Uxom2Meaepne/hYVbYAAAAA0BUJbXNIJpNpqbQ9XD9bAAAAAOiShLY5JOllu6yiOooL8+OAUX2yPRwAAAAAIAuEtjlkSlNrhING94nSooJsDwcAAAAAyAKhbQ55eEZjaHvY2P7ZHgoAAAAAkCVC2xxR35CJx2avSrf1swUAAACArktomyNeXLQuKqrqokdpYewzrFe2hwMAAAAAZInQNkc80tTP9tDd+kVBfl62hwMAAAAAZInQNkc8MrMxtNUaAQAAAAC6NqFtDqiqrY+n5q5Jt4W2AAAAANC1CW1zwDPz1kR1XUMM6lkSYwd0y/ZwAAAAAIAsEtrmgIebWyOM7R95efrZAgAAAEBXJrTNAY/MWpWuDxvXP9tDAQAAAACyTGibZes21cYLC9em24eP65ft4QAAAAAAWSa0zbLHZ6+KhkzEbgO6xZBeZdkeDgAAAACQZULbLHtks362AAAAAABC2xzpZ3u4frYAAAAAgErb7FpWURUzl2+I/LyIQ3fTzxYAAAAA0B4hJ1ojTBzWK3qVF/l9BAAAAAC0R8imR2Y2tkY4TD9bAAAAAKCJnrZZkslkYsqsxkrbI/SzBQAAAACaCG2zZM7KyliyriqKC/PjwNF9sjUMAAAAACDHCG2z3M/2gJF9orSoIFvDAAAAAAByjNA2Sx6bvTpdHz6uX7aGAAAAAADkIKFtlryypCJd7zdSawQAAAAA4DVC2yyoqq2Puasq0+3xg3pkYwgAAAAAQI4S2mbBzOUboiET0bdbcfTvXpyNIQAAAAAAOUpomwXTl61P1+MHdY+8vLxsDAEAAAAAyFFC2yyY1hTa7qE1AgAAAADwOkLbLJi+tKnSdrB+tgAAAADAloS2WTB92YZ0rdIWAAAAAHg9oW07W19VG4vWbkq3d9ceAQAAAAB4HaFtliYhG9KrNHqVFbX3jwcAAAAAcpzQtp1NW9rYGmG8KlsAAAAAYBuEtlmqtN3DJGQAAAAAwDYIbdvZtKWNoa1KWwAAAABgW4S22aq01R4BAAAAANgGoW07WrmhOlZV1kReXsS4gd3b80cDAAAAAB2E0LYdTW9qjTCqb3mUFRe0548GAAAAADoIoW07mtbUGkE/WwAAAABge4S22ehnO7hHe/5YAAAAAKADEdq2o2lN7RFU2gIAAAAA2yO0bSeZTCamL9uQbu+p0hYAAAAA2A6hbTtZvK4qNlTXRVFBXozu3629fiwAAAAA0MEIbdvJ9KbWCGMHdI+iApcdAAAAANg26WE7mbbsdf1sV8+OeOzaiE1r22sIAAAAAEAHILRt50rbPZr72f7rsog7L4n42eERc/7dXsMAAAAAAHKc0DZblbYrZzSuKxZG/OakxhC3rrq9hgMAAAAA5CihbTuoq2+IGcs3pNt7JKFtJhOxdn7jg3ucGBGZiCn/E3H9OyKWvdweQwIAAAAAcpTQth3MW70xauoaoqyoIIb3KYvYsDyiblNE5EV88NcRp/8xorxfxLIXIq47OuLR/41oaGiPoQEAAAAAOUZo2479bMcP6h75+XkRa+c1PtBzWERhccSeJ0ac/2jE7u+MqK+OuOvSiN+dErFuUXsMDwAAAADIIULbbPSzbW6N0GfUayf1GBRxxk0RJ14dUVgWMefBiJ8dFvHiLe0xRAAAAAAgRwht28H0ptB2j8FNoe2auY3r3puFtom8vIiD/jPiU/+OGLpfRNXaiJvPjbjlExFV69pjqAAAAABAlhVmewBdwbSlr6+0bWqP0Hvktp/Qf/eI/7w74sHvRfz7BxHP3xgxb0rEqT+PGH145LykH2+mPqKhrmlJtjfbb3lsW8c3f2yzdfLYkH0jeg3P9rsDAAAAgDYltG1jVbX1MXfVxtdV2s7buj3C6xUURbz9axHjjov46ycaq3N/fWLE4Z+NOOa/GnvhZlMmE7HwqYjn/xzxyj8bq4KbA9bItM3P7D4o4jPPRhR3a5vXBwAAAIAcILRtY7NXVEZ9QyZ6lRXFwB4lW/a0fX17hG0ZeXDEpx6OuPMrEc/+PuKRayJm3Rvxvl9EDNwz2t2qWRHP39RY/btmzk4+OS8iv7BpKWhaCiPymtbpkv/adnq86bHVsyI2LIt46oaIwz7dRm8OAAAAALJPaNte/WwH9Yi8pGdtUo26buGbV9purqRHxMk/jRj/rohbPxOx9IWI646KOPaKiLd9ojHobEuVqyJeuqUxqF345GvHi8oj9jopYtKHIvqP33bYukUw+xbG+czvIm69KOKR/4k48D8jistb5a0BAAAAQK4R2raxaU2h7fjB3RsPVCyOaKiNyC+K6DFk514sCUiHHxTx9wsjZt4TceclEdPvjDjlZxE9d/K13kztpohpdzRW1c68u6ntQVIsmx+x2zER+54esccJESVN76utJT/vof9urFJ+5jcRh5zfPj8XAAAAANqZ0LaNTV/6WqXtFpOQJRNqJVWoO6vH4Igzb4548hcR//paxOz7I352aMR7ronY+5S3PoHYvIcbK2pfvjWiuuK1x5JJwCadFjHxAxE9BkW7S3r8/scXIv7x2YiHr4k44NyIotL2HwcAAAAAtDGhbXtV2g7aiUnI3kzSZuFtH48Yc1TELedFLJka8ZezI6afEfHu70WU9ty511v2cmNQ+8JfIioWvXa814jG1gf7fCg7/XNfb98zIh68KqJiYcQzv404+BPZHhEAAAAAtDqhbRvaUF0XC9dsSrf3GNxj5ychezMDxkf85z0RD/6/iId/GDH1j42Vsqf+PGLUYW/83IolES/e3BjWJj1ym5X0itj75IhJp0eMPLTt++XujMLiiP/4fMRtX2h8vwecHVHYNLkbAAAAAHQSQts2NKOpynZQz5LoXV68ZXuEt1Jp+/og8x2XR4w7LuKvn2gMhX91QsQRn484+tLGx5tVb4h45R+NQe2cByMyDY3Hk/66u78zYt/TInY/PrfbDuz30YiHfhCxfnHEs7+POOg/sz0iAAAAAGhVQts2NP31rRE2b4/QGpW2mxt1aMSnHom48ysRz/0h4uGrI2bdG3HKtY0tD5Kg9tXbImo3vvacEQc39qnd+9SI8r7RISSVtUd8LuKOLzdW2yYh7ubBNAAAAAB0cELbNjRt6YYtJyHbvNK2tUPbRNLL9pT/jRh/fOOEXUmv22SSss31HRux7+kR+3wgou9u0SHtf3bEv6+OWLcgYuqfGtskAAAA8P/buw/wqMr0/eN3OiQQeu9IR+kdBRQQu1jR9SeIiuuu6LrquvrfVWwrtrWsq+IW1nV1d+19BelYQBQsgHQFFAi9hRqS/K/nvJlkEpKQhCRzZub7ua73mjMlmZOZOZOZ+zzneQEAEcJHDUsjuNI20M/2yCFpz8bybY9QmE7nS7+YJ50w1J1PriP1+bl07UzpxoXS4NvDN7A11r5h4K/c8sd/lDIzQr1GAAAAAAAAQLmh0rYCLU/bm7/SdvdPkrKlhGQppV5F3rWU2kj6vzekbauk2q2kuARFlJ5XuRYQVrlsrR+6/1+o1wgAAAAAAAAoF1TaVpDt6Ye0Lf2Qt9y2QbUCrRGaSzExqnB2H/XaRV5gaxKTpQE3ueW5j0mZR0K9RgAAAAAAAEC5ILStICs3u362zWsnKzkxvsAkZM0r6m6jS6+rXeuHnT9IS14P9doAAAAAAAAA5YLQtqL72VbWJGTRKKma1H+8W577qJSVGeo1AgAAAAAAAI4boW0FWZET2rZvmNMawexaX/GTkEWbPuOkqrWk7aulpW+Fem0AAAAAAACA40ZoW0FWphVSaZvbHoHQttwkVZf63+CW5zxCtS0AAAAAAADCHqFtBcjOzg6qtC2kPQKVtuWrz3VSlRrSthXSd++U8y8HAAAAAAAAKhehbQVI23NQew8eUXxsjFrXzWmPcHiftG+rW2YisvJlgW2/Xwb1ts0q5zsAAAAAAAAAKg+hbQVYkdMaoVXdFCXGx+bvZ5tUw/VgRfnq+3MpKVXa8p20/H0eXQAAAAAAAIQtQtsKsDKnNUK7fK0RApOQNa+Iu4QF4X2vz+ttm53NYwIAAAAAAICwRGhbAVakpXun7ZmErHL1+4WUWE3avFha8WEl3zkAAAAAAABQPghtK7LStkEhk5DVbFERdwmTXNtNSmbmPES1LQAAAAAAAMISoW05y8zK1qotLrRtH9weYedad1qL0LZC9R8vJaRIm76RVn1UsfcFAAAAAAAAVABC23L24479OpiRpSoJsWpeO/nonrZU2laslDpS72vc8pyHqbYFAAAAAABA2CG0LWcrclojtK1fXXGxMUe3R6DStuINuEmKryptWCitmVEJdwgAAAAAAACUH0LbcrYyrZB+tgd2SQd3u+Wazcv7LlFQtXp51bazqbYFAAAAAABAeCG0LWfLcypt2zesdnSVbXJdKTGlvO8ShRlwoxRfRfppgfT9bB4jAAAAAAAAhA1C28qotN1Ja4RKV72h1PMqt0xvWwAAAAAAAIQRQttydOhIpn7Yts9bbt8wKLRlErLQGPgrKS5RWj9PWvtJiFYCAAAAAAAAKB1C23Jkge2RrGxVrxKvhqlV8q5gErLQSG0s9RiTV20LAAAAAAAAhAFC23K0Iqc1QvsG1RUTE3N0ewQmIat8J98sxSZIaz+W1n0WghUAAAAAAAAASofQthytzJmErF1wa4TgStuaLcrz7lASNZpK3f/PLc95hMcMAAAAAAAAvkdoW45WpKXnVtrmys7O62lbq2V53h1K6uRfS7Hx0vezpB8X8LgBAAAAAADA1whtK6LSNji03bdNytgvKcZVfaLy1Wohdb3cLdPbFgAAAAAAAD5HaFtO9h8+ovU79ueEttWObo1gk2LFJ5XX3aG0TrlViomTVk+XflrI4wcAAAAAAADfIrQtJ6s2u9YIdaslqU61oHB251p3yiRkoVW7ldT1Mrc8l962AAAAAAAA8C9C23KyIqc1QvuGQVW2hknIfFZtGyutnCJt/DrUawMAAAAAAAAUitC2nKxMK6SfrcmdhKxFed0VyqrOCdJJl7jluY/yOAIAAAAAAMCXCG3Lu9K2YGi7M6enbU1CW1845TY3Kdzy96W0xaFeGwAAAAAAAOAohLblZGVOaNuuYcFK20Bo27y87grHo1476cSL3PIcetsCAAAAAADAfwhty8Gu/Ye1ec8hb7lt/aCetlmZ0q4f3TLtEfxjUE617bJ3pc3fhXptAAAAAAAAgHwIbcvBys3p3mmTmlVVvUpC3hV706SsDCk2XkptUh53hfJQv6PU6Xy3TG9bAAAAAAAA+AyhbXn2sy2qNUKNplJsXHncFcrLoN+406VvSVtX8LgCAAAAAADANwhty8GKtD2Fh7ZMQuZfDU+UOpwjKZtqWwAAAAAAAPgKoW05WJnm2iO0b8AkZGFl8G/d6ZI3pG2rQr02AAAAAAAAgIfQ9jhlZ2fntkdo16CISlsmIfOnRl2k9mdJ2VnSx38M9doAAAAAAAAAHkLb47Rl7yHtPpChuNgYta6Xkv/KXevdac2Wx3s3qOjett++Km1fw+MMAAAAAACAkCO0PU4r0lyVbcs6yaqSEFf4RGRU2vpXkx5S29Ol7Ezpk8dDvTYAAAAAAAAAoe3xWpnTGuGoScgyM6Q9G9xyzea81MKht+03/5V2rg312gAAAAAAACDKUWlbTpW2R/Wz3f2j65UaX0Wq1uB47wYVqWkv6YShUtYR6WOqbQEAAAAAABBavghtn3nmGbVs2VJVqlRR3759tWDBgiJv+8ILLygmJibfsJ8raNmyZTrvvPNUo0YNpaSkqHfv3lq/PqfHbEVU2hY1CZlV2cbElPv9ooKqbb9+mWpbAAAAAAAARHdo+8orr+iWW27RhAkTtGjRInXt2lUjRozQli1bivyZ1NRUbdq0KXesW5cTkOZYs2aNTj75ZHXo0EGzZ8/Wt99+q7vuuqvQcPd4ZGVla+XmdG+5XcH2CLmTkLUo1/tEBWneVzrhNFdtO/MBHmYAAAAAAACETLxC7PHHH9e4ceM0duxY7/ykSZP0wQcfaPLkybrjjjsK/Rmrrm3YsGGRv/N3v/udzjrrLD3yyCO5l51wwglF3v7QoUPeCNizZ493mpGR4Y2irN+xXwcyMpUYH6vG1RPy3TZ2+w+yackyazRTVjG/Az4y5PdKWDNTWvyaMvr8QmrYJdRrhEoQ2G6L29YBRAa2dyB6sL0D0YPtHYgeGRHy/b2k6x/S0Pbw4cNauHCh7rzzztzLYmNjNWzYMM2bN6/In0tPT1eLFi2UlZWlHj166MEHH1Tnzp296+wyC31vv/12r2L3q6++UqtWrbz7GDlyZKG/b+LEibr33nuPunzWrFlKTk4ucj0W77C2B3Gql5ipj6ZOyXddz7Xz1FTS8k37tPp//yvR44HQ61FrgJrt/Ew7X71J89rcHurVQSWaNm0ajzcQJdjegejB9g5ED7Z3IHpMC/Pv7/v37/d/aLtt2zZlZmaqQYP8E3XZ+eXLlxf6M+3bt/eqcLt06aLdu3frscce04ABA7R06VI1bdrUa6tgoe5DDz2kBx54QA8//LCmTJmiCy+80AthBw8efNTvtEDXWjQEV9o2a9ZMp556qurUqVPk+q+b8720YrV6tW2ss846Kd91cS/8Sdopte83Qu06nlWGRwchsauzsif1V/29S3R2h2Rltx7CExHhbA+XveEPHz5cCQkJoV4dABWI7R2IHmzvQPRgeweiR0aEfH8PHOHv+/YIpdW/f39vBFhg27FjRz3//PO6//77vUpbc/755+vXv/61t9ytWzd99tlnXuuFwkLbpKQkbxRkL4DiXgSrt7pkvEOjGkffbveP3kl83db2i8r656Ky1Wsj9b5Wmv+s4mfdJ7UdauXfPA9R4FjbO4DIwfYORA+2dyB6sL0D0SMhzL+/l3TdQ5pG1a1bV3Fxcdq8eXO+y+18cT1rC/6h3bt31+rVq3N/Z3x8vDp16pTvdhbsrl+fMzlYOVm5ea932r5htfxXZByQ0nP+JiYiCz+n3CYlpUpp30pL3gj12gAAAAAAACDKhDS0TUxMVM+ePTVjxozcy6xS1s4HV9MWx9orLF68WI0aNcr9nb1799aKFSvy3W7lypVeH9zykpGZpTVb073ldg2q579yV044nFhdqlqr3O4TlSSljjTwV2555n3SkbxJ6gAAAAAAAICKFvL2CNZLdsyYMerVq5f69OmjJ598Uvv27dPYsWO960ePHq0mTZp4k4WZ++67T/369VObNm20a9cuPfroo1q3bp2uvfba3N/5m9/8RqNGjdKgQYO8vrTW0/a9997T7Nmzy229127bp4zMbKUkxqlJzar5r9y5zp3WaiHF2GRlCDv9fil98TcXwH/xd6n/L0O9RgAAAAAAAIgSIQ9tLVzdunWr7r77bqWlpXn9Zy1kDUxOZi0NYoN6iu7cuVPjxo3zblurVi2vUtf61Qa3Q7jgggu8/rUW9N50003e5GVvvPGGTj755HJb7+VprjVCu4bVFVMwmN2VE9rSGiF8JSZLQ+6U3rtJmvuo1P0KqUqNUK8VAAAAAAAAokDIQ1szfvx4bxSmYHXsE0884Y1jufrqq71RUXL72RZsjRAc2lqlLcJXtyukec9I21ZInz4lDb071GsEAAAAAACAKBDSnrbhbEWg0raw0DbQHoFK2/AWFy8Nm+CW5z0r7dkY6jUCAAAAAABAFCC0Pc5K2w4Ni6m0rdm87M8M/KH9WVKzftKRA9Js11cZAAAAAAAAqEiEtmVw4HCm1u3Yn9vT9ijBE5EhvFm/4uH3ueWvXpK2rgj1GgEAAAAAACDCEdqWweot6crOluqkJKputaT8Vx7cLR3c5ZZpjxAZmveVOpwjZWdJ0+8N9doAAAAAAAAgwhHalsGKzcX0s9213p0m15GSqh3fswP/GDpBiomTVnwgrZ8f6rUBAAAAAABABCO0PY5+tu2La41AP9vIUq+d1ONKt/zRXfJKrQEAAAAAAIAKQGhbBivSiqu0DYS29LONOEPulBKSpZ8WSMs/CPXaAAAAAAAAIEIR2h5XpW0h7Q+YhCxyVW8o9fulW55xr5R5JNRrBAAAAAAAgAhEaFtKuw9kaNPug95y2+J62lJpG5kG/sr1K962UvrqX6FeGwAAAAAAAEQgQttSWpVTZdu4RhWlVkkouj1CLdojRKQqqdKg293y7InS4X2hXiMAAAAAAABEGELbUlqRE9q2K2wSMpucKnciMkLbiNXravf8pm+W5j8b6rUBAAAAAABAhCG0LaWVOZOQtS+sNcL+7VJGTuVljWbH/+zAn+ITpaF3u+VPnpL2bQv1GgEAAAAAACCCENqWtdK20H62OVW21RtJCVWO/9mBf3W+UGrUTTq8V5r7aKjXBgAAAAAAABGE0LYUsrOztSJQaVtYewRaI0SP2Fhp+L1u+Yu/Szt+CPUaAQAAAAAAIEIQ2pbCtvTD2rk/QzExUpv61YqutK3ZvLyeH/hZ6yHSCadJWRnSzAdCvTYAAAAAAACIEIS2pRCosm1ZJ0VVEuKKrrStxSRkUWOYVdvGSEtelzZ+Feq1AQAAAAAAQAQgtC1TP9tCqmzzVdoS2kaNRl2kLpe65WkTrIdGqNcIAAAAAAAAYY7QthRWBvrZFjYJmdm13p1SaRtdTv2dFJco/TBHWjMz1GsDAAAAAACAMEdoW5ZK28ImIcvKygttqbSNLhbS9x7nlqdPcK8FAAAAAAAAoIwIbUsoKytbqzYXU2mbniZlHpZi4qTUJmV9PhCuBt0mJdWQ0hZLi18L9doAAAAAAAAgjBHaltCGXQe073CmEuNi1bJuStGTkNVoIsXFl+dzhHCQXFs6+Wa3PPMBKeNgqNcIAAAAAAAAYYrQtoRW5lTZtq6XooS4Qh42WiOg7/VS9cbS7vXSl3/n8QAAAAAAAECZENqWsp9t+8L62ZpdOZW2TEIWvRKTpVPvdMtzH5UO7Ar1GgEAAAAAACAMEdqW0Mq0nEnICutnG9wegUnIolvXn0n1OkgHdkqfPhnqtQEAAAAAAEAYIrQtoRWb04uehCy40pbQNrpZP+Nh97jl+c9JuzeEeo0AAAAAAAAQZghtS+BIZpbWbEkvvj1CoNKW9ghod4bUvL905KA0eyKPBwAAAAAAAEqF0LYE1m7fr8OZWUpOjFOTmlWPvkHmEWlPTkUllbaIiZGG3+ceh69flrYs5zEBAAAAAABAiRHalsDKnEnI2jaortjYmKNvsOcnKTtTikuSqjUo+aOPyNWsj9TxXCk7S5qe0y4BAAAAAAAAKAFC2xJYkTMJWfsG1Y4xCVkzKZaHFDmGTpBi4qSVH0rrPuNhAQAAAAAAQImQMJai0rYdk5ChNOq2lXqMdsvT7pays3n8AAAAAAAAcEyEtiWwIie0ZRIylNqQO6SEZOmnL6Rl7/EAAgAAAAAA4JgIbY/hYEam1m7b5y23L7LSdr07ZRIyFFS9odR/vFueca+UmcFjBAAAAAAAgGIR2h7D6i3pysqWaiYnqF71pMJvtCunp22tFsf6dYhGA26UkutI21dLi14M9doAAAAAAADA5whtS9HPNiYm5hgTkTUv32cHkaFKqjT4t2559kPSofRQrxEAAAAAAAB8jNC2pP1si2qNkHFASk9zyzVblu+zg8jRc6xUq5W0b4s0/9lQrw0AAAAAAAB8jND2GFam5VTaNiwitN39kztNrCYl1y7fZweRIz5RGnqXW/70KWnv5lCvEQAAAAAAAHyK0PYYVm5OL77SNrc1QgupqPYJgOl0gdS4u3Q4XXrjGinzCI8LAAAAAAAAjkJoW4y9B49ow64DxYe2u9a6U/rZ4lhiY6WRk6SEFGntx9KMe3jMAAAAAAAAcBRC22J8v3Wfd9owtYpqJCcUX2lbq0Vxvwpw6neQRub0tP3saWnJmzwyAAAAAAAAyIfQthhrtqUX38/W7ApqjwCUROeR0oCb3PI746Uty3jcAAAAAAAAkIvQthirt7hK2/YNqhV9o13r3SmVtiiNoROkVoOkjH3Sf6+QDu7m8QMAAAAAAICH0LYYa3LaI7Qrqp9twYnIgJKKi5cu/oeU2lTasUZ66xdSVhaPHwAAAAAAAAhti7N6i2uP0L6o9giH9koHdrhlJiJDaaXUlUa9KMUlSis+kD75I48hyi59q/TVS9KBXTyKAAAAAACEOSpti7Fjf4ZiYqQ29asVX2VbtZZUJbUinh9EuiY9pbMec8sz/yCtnh7qNUI42psmTT5deucGadIp0vrPQ71GAAAAAADgOBDaHkPz2slKTowvvp8trRFwPHqOkXqMkZQtvX6NtHMtjydKbt826cXzpR3fu/O710v/OFOa+6iUlckjCQAAAABAGCK0PYZi+9nuyqm0ZRIyHK+zHpUa95AO7pJe+T/p8H4eUxzbgZ3Sv0ZKW5dL1RtL138inXixlJ0pzXzAhbl7NvJIAgAAAAAQZghtj6F9iSYha16ezwmiUXySNOpfUnJdKW2x9MEtUnZ2qNcKfmY9tV+62L1eUupLY96TGp4kXfQ3aeRzUkKKtPZj6bmB0oopoV5bAAAAAABQCoS2x9CuqEnIgittaY+A8lCjqXTxZCkmVvrmP9IXf+NxReGsEvvfo6QNX7qe2qPfkeq2cddZI+5uP5N+Pkdq2MVNlvifUdKHv5WOHOIRBQAAAAAgDBDalkelba2W5fmcIJq1HiwNu9ctT7mDCaVwtIyD0n9/Jq37VEpKla58S2rQ6ejb1W0rXTtd6vdLd/7zSdLfhkrbVvGoAgAAAADgc4S2xYiPjVGruimFX2mHrjMRGSrCgBulTiOlrCPSq6OlvZt5nOFkZkivXSV9P8u1P7jidalx9+LbbpwxUfrZq1JyHddK4flB0lcv0X4DAAAAAAAfI7QtRos6VZUYH1v0BECH97rlms0q4rlBtLLD28//s1Svg5Se5kI6C+sQ3TKPSG9cK638UIqvIv3sv1LzviX72XYjpOs/lVoNkjL2S+/cIL1xjXRwd0WvNQAAAAAAKANC22K0qVet6Ct3rnWn1RpICVXL8tgDRUuqLo16yR3+vv4z6aO7eLSiWVaW9O546bu3pdgEadTLLoAtjdRG0pVvS0MnSDFx0pI3pEmnSD99WVFrDQAAAAAAyojQthgnFBfaMgkZKpr1JL1gklv+/Dnp29d4zKORtWL5361ucjoLWy95QWo7rGy/KzZOOuUW6eqpUs3m7n1s8gjp48ddMAwAAAAAAHyB0LYYbeoV0c/WBPrZ1mpR3s8JkKfD2dIpt7nld2+U0pbw6ERbYDv1d9KXk61vhnThX6SO5xz/723WW7r+E6nzha538ox7pZcukPamlcdaA0B02bJMenWMNPlM6dOn8iaqBQAAAI4DoW0xTqhfTGgb+EBek9AWFezU/yedMFQ6ckB65QrXTxnRYdYfpPnPuOXznpZOurj8fneVGtLFk6Xz/iwlJEvfz5aeGyitmlZ+9wGgYmQclLaulA7v4xEOpfQt0ns3S88NcO1rrJ3RtLulp7pIfz1N+uzpvJ38AAAAQCnFl/YHokmTmlVL0B6heaWtD6KUHdJ+0d+kvwx2vZTfvE66/BUpln0uEW3uY9LcR93yWY9JPa6smEnv7Pc26yu9frW0ebH08sVSvxukYROk+KTyv08AZa+83/S19NXL0uJX8yYSTG0i1W4t1Wkj1TnBndY+QarVUopP5NGuCBkHpHnPSJ88mTcpbYdzpFaDpWXvSus+lTYsdOOj30tNekmdL5A6nc/ktQAAACgxQttixMXGHLvSlvYIqAzJtd3EZH8/XVr1kTTnYenUO3nsI9W8Z6WZ97vl4fdLfcZV7P3VayddO12aPkH6fJKr7l33iXTRZKlum4q9bwDF27fdhbRfvSRtDmqRE5ckZR6S9mxwY+3H+X8uJtbtWA6EuF6omxPu1mjmdgiidKz39+LXpBn3SXt+cpc16iaNeFBqOdCd73udq8C18Hbp29LaT6QNX7rx0e+kpn2kziNdgFujKc8AAAAAikRoW9YP7YHD3WiPgMrSqKt0zhPS27+Q5jwkNe4utT+Dxz/SWP/aqTmB/JD/Jw28qXLuN6GKdObDUush0tu/lDZ9Iz0/SDr7j1LXy1xVbmVUEu7f4cKQxGouWKJSMLId3u+CyI1fu9OqtaSGJ0kNOkt12kpxUfoxJfOItGam9NW/pBUfSlkZeUGt9bXu/n+uqtOqbXd8L21fLW1fk3O62l12ON0dnWFD0/P//rhEqVarnMrcE4JC3ROk6o0qZ3sPN+s+k6b+P2njV+58alNp6N3SSZccfeRLtfpS72vd2Ls5J8B9y/2Onxa4Yb/LjnLoFAhwm4TkzwIAAIB/Rem3oeO0b4urbrEqFqokUJm6/cwdbvnF31ybhOtmuS/ZiAzf/Fd6/xa3PPBmafDtlb8O7c+UfvGpe31Z5d7b17vwyMLbKqnHH8paBdruH12LmV12uj7n/Hp3PiOoR6e9x1owYkc02KHeuaOVO7UKdMKl8Dqk3CZTtNDLDvO3oHbrcik7s/DbW0BZr31eiNvgRDdS6ihibVstff2Sey/YuynvcqvmtKDW+lpbsB1g24CNpr0K2dY25wW5O+w0Z1iga59htq1wo6CElJx2Cye4x7/nWCm1kaKWPWbWp3b5++687VA6+ddS/xukhGLaaAVUb+COlrBhkz1+lxPgrp8n/fi5G7ajrlm/nBYK50mpjRWxdm+Qlr3n+v827iH1Gut6rAMAAOAohLZlEWiNYGFCXEKZfgVQZiMmSpu+dZU6r1wpXTtNSixm0jyEB/sSb1XUypb6/Fwadk/oAkkLDEa/I33yuDRrojs0215vNnFZk57FH4WQnpYTwAaNQCi7+yfpyMFj339KPTfBUsZ+abf93PqjD/02idVzQtwWRwe6Na1Kl568IQ9oA+GsBbVFBbQp9aXG3Vw4e2CXq7jdvNRViqZ960awag2lhifmBLk5gW7dtuH7//hQupvEytofWJAXULW2q3LvdoX7e0vD3juqN3QjcNh+QFam2xaDg9xAsGufb2zHifW3thGo/re+6laFH02s6n/OI9IXf5WyjridSD3GuMlBrZK2LOz5sPYJNvZszAtwf5yfN6bcITXPCXA7nhcZgbm9/9vf+t077n9JgJ23/u29rpL6/oJqYwAAgAIIbcuCScgQSna4+KUvukPXtyyV3r3JfaGm4jB8rZgivXGtlJ0ldb9SOuOh0D+f1u9y0G+kloPcutkh1tZT+bS7XFVfbpVsIJz90QVBgcO4ixTjQmFrfWD9NmvmnHrnW7ijF6xVg1UK7tuad3h3wWE9PG0CoOBw6aj7aVKgQjdopNQN/WMcSQGthawWzFpAa0HtlmVFBLT1XGsXqxy1UwtrCzsc33YA2GvLgl/73d7zvNRVidqOgdU2puc/3N+qQgMhbsNAVW5d+ZK9vtfPd0GthXaBCnMLBtsMc1W17c6smPYgtm17OzpaSCeclv+6I4fd9hxos/DNf1yI/q8LXFh58q2RPwnmkUPSgr9Kcx/Jm+ytzXDp9Pul+h3L737sfbDf9W541aeBAPdzF97b+PC3UosBeQGuVe2Gix0/uFDWxsZFQVfEuFC65Smu4nbrMumzp6X5k1yriQE3Sg06hXDFAQAA/IPQtiyYhAyhZpU3l/5T+ue50pLXXYjWz6o0EXas9cCrV7pKLvvCeu5T/gpFmveVrv9Yeu8m9+XbJisrTkycq5byAtigYDawbEFqSYIoC/Gsms1Gsz5HX59x0FXwWjBQWKhrIZj1xrVhk6oVdgh468GuT7RVv6Fk7HH3AtpFOVW030hbvis6oA0OZ23ZgqqShOW2DQQCduvhGlyVaoFwIMQNjEN7pLTFbgSr1iCnrYIFuSdJddorNutw6J5tq660IPSrl11la4D1lLWgtuvloa2stG3TJh8MTEDY+xrpf7e5cHnmA9KPC6QLnnctGSKNBemB9zivD7Ck+p2lEQ8cHW6XN3vPtP/hNmznl62HBbg/fSGt+9SN//1GajFQ6niu+59vr+mStGeo7PYeVjVu6x9cIW87I2zdrXdvh3PyXuND7pRWT5M+/ZN7n/7m3260PV0acJPU8mR2rgEAgKhGaHtclbYtyvfZAErDqm9Of8AdSjn1d1LDLkcfBgt/W/up9J+fSZmH3RfxkZP8OaN71ZrSJf+UFv1TmvtHNzlUcHVscLWsVU1WxuRRVo1rh8TbKMir0t1WfJWuhbor/udCkWg89Lu0Qcy8p6WfFrqqONvBUFBy3fzhrJ1aQF/e1cxJ1aRmvd0Ifr6tOjTQViEtuCp3sxtrZng3tQYK59qPrLgtr32AtVuwCkZ77VrIG3x5YnL5VG7aZGIWfNp6WEW9tzIp0okXSN3+z1Ue+rHy20LB859x/VYtvF31kfT8YOnSF4pvlRJufvrS/R+19gTGXgen/d61pqjs92Q72sD65dqwIxgCAe6GL12wGdgJZTvI6nVwk5R6211Xt4PCtpHKtGV5XkWtHf0TYOvX6pS8oLawlhK2c6bdCDfs/eWzp1wbBXud2bCetwN/5f4/+vF/IwAAQAUjtD2e0NYOLQRCqe/1bmKyxa9Jr10l/XxOZE9gEkksJPj3pdKRA66q6KLJlRN2lpUFSj2vcsPvvCrdem4Eh3vBIZqFeu/e6IK+F0dKg3/rJn4jGMiTecSFtdbX2CauyhfQBsLZ7hUX0JaU3W/gcP8OZ+evyrU+uoEQd/MSZW9eophDexVzcJdkw64vTlJqToBbIMwtGPgmVT/6Z+1+raL221ekAzvyLm8+QOp+hdRpZOUHbGXV40oXCr46Wtr5gzT5DOmMiVKva/wZNpfmyKkZ90pL3nDn46tKA29yVZ5+eG5sh9iA8W7Yjomlb0s/zHFtSPZvcyGpDatO9cS4HVm2bdrz5Y0u5TvRl+0kse0pENQGT2YXG+92gFlQ2/7s0k0a2LSna/1kPZbnPSN9/bKr6H9tjOtVbo+Bheh+qy4GAACoQD5OCMKgPYJVlgGhZF+W7XD6zd+5L272hfqq/1VMH0SUH5tI7qUL3URLrQa5L6o8Z5XHJihr0kO6drr04e3SohelOQ+5HpJWdVvWSYYiyaZvpHfG5x3ibIeH97rahUFWCRgOQZ2FbnYYuY0cRw4f1rT3Xtfw/l2UcHCbtDctb1iv3L2bpb2bXHWuTYRnbRdsbFtZ/H0lVssf7Fo/WHsMA6yK11ofWOgUaD0Qbiz8sx2Db/9SWv6+9MGt0vrPXYsRPwScpWG9aj/+o+uj6u2QiJG6/cxV1/p1x6d95rRA2YYFp/Y69XpIf5MzvnaX2WvVhk0gGVC7dU6AGxTmlqbFhd2f3UcgqA1u7WH9pO39wQtqz5Sq1jq+v7POCdI5j7vWCTYJ3IK/uB0F9nqb9aCbqLP3taULhAEAAMIUoW1ZKo+s35ihPQL8IDFFuuwl6S9D3KHeU++Uzv5jqNcKxR1K+q+RLjRo1le67D9UDoWKVWyd97TU4mTp/ZtdBdukk6WL/u4O643WnrVzHpY+fcr1qa1S002M1/Wy8AhqjyUmRhnxKW7SsoQTiw+pDu0tPMy1UzvvXZ7mdr7Y2JGeP8yKTZA6nOUmF2x9qr8r6UvKKjZHvSTN+7M0bYILBi3Ytx1P9pj6XWaGtPAFafZEaf92d5ntOLNWQxZkhgvbFi1ctmGvsQB7XdrzEZgQ0HYQ2oR+1irEhrVZCA6Bc6txbXLAru7ohOBtYMOivB61gaPMTFyS1Ha4C2qttUF5VvIG2LrY5HfWHsEq1q3q36qNZz8offKE6wFtLSRqtyr/+wYAAPCJCPgGUcmsF6J9kbXKAqucAfzAqmgu/Js73P6Lv7leg1Y1BH+xwz5fPN+FBVbxdMVr4VehFom6jnKH+L86xvVsffE8FxacfKu/JoWraOs+cy0jrErU2OH7Zz0anZXHFopVSXWjXrvib2ttGHLDXAt5N7sdAh3Pj8xqQHtsBtzo/s+8Nta1mPjLqdJ5f5JOuli+ZAHkyqnStLvyqqbrtJVOv19qd0Zk7JAwXm/m4S5QDdi3XUrLqcYNVOZa5aoFoDaWvRf0843de6FVja+e7iZ7DLDWEe1Od0GttfQprCVIRe2Y7nudq/Rf9o7boWR/g1Xhfvl3tz7WzsKOngAAAIgwhLalZR9wjU24E01f5uF/9mVqyB2uguj9X7vecideRI/OY8nKVFKG9bfcbaU97tD5ivgCb+8dFthadZ7NSH7lWxVTnYSysSrBcTPcDO3WS3HmA9K6edKFf5FS6kb2o3pwj+vraTt8jPVptWr9jueEes3Cg+14sWGHdUfbZJjXfyy9frW09mPpjWuk9fOlEX9w76N+kJXpwlrrkRqYwCu5jjv03vpzx9nUdBHOdhxY+wIbAQd2uYrcTUFhru2s2btRWrExf9sPq6S1YLTNMBeghkpczmeazhdKP8x14a1N7GfVwzZaniINvFlqMzRyQngAABD1CG1Li0nI4GeDbpc2fiWtnCK9Oc4FuCf/WupyGT1TC7NlmeL/fZnO2LVWWnJTzoUxUnwVKaGKqywKnFoIYdVz3nWB0+Kuyzm1YQG69U61qiWr7hr9dun6CaJyWCAx8lmpxUDXP9ECgUmnSBdPllr0j8xnYeVHrjWEHUVieoyRht8nVa0Z6jVDOLAq7NHvSLP+4HrEWvWjTR51yQuh7ftv1aVfvSh9Mdm1Bwgc0t/vF9Ipt7DDzLZvawthI8DagaQtcW0VrA2YhfIW9Ppt4i8LZFsPdsMm+/vsaWnx627HgQ3bKWp9fy3gBQAACHOEtqXFJGTwM6v+vvgfrt/g/GddDzs73Hn2Q+5wVgtkEpNDvZb+YJU6//0/xRyyCttg2dKRA25oZ/neZ62W0ph3o/Nw83DS/QqpcXc3a7kdSv3C2dLQu6QBv4qcIyws1JpyR95kRfbaPPdPLggBSiM2Thp6t+vR/eZ10oaF0vODXMuetsMq97G0nZYL/upCPG+CMbmJsXqMlnqPk2o2q9z1CSfW7sB2ToXTDqqGJ7mjIU67S5r/nOtXbJOyvvVzacZ9iu3zc1U9XE3KOmJNzEO9tgAAAKVGaFvWSlsmIYNfWSg7+Hap3y/dFxirQrEqOgto5j7qLu8zLrorjb59TXr7F1JWhrKa9dOUmldq+NkXKEGZ0pGDUsaBwk+9ZTs9UMTpwQI/F3RdaiPp7Mf9OzM58mvQSRo3y7UasWBz+j2u5+sFz4d3lbT19lzyhqv8tt7KMbFuMp8h/48dOjg+dhj9z+e6nR0Wnr58sTToN65tjwW7FeXIITdR1oK/uMk4A2xirT7XuYpLv1WLonxZGH/Gg9Lg30hfTpbmT/I+98RNv1un29ved7e5fr01mhYYzXJOm7hJF2mrAAAAfIbQtqw9bWu1KP9nAyhP1mNxwHgX0FqPzk+edDsdZt7vesHZ5RbgRnq/zoKBlc06bf07TecLlHnO08r4aKabXDDBKnFSQ72W8NM2ZFVcLU92vW5XfeTaJVzyD6lZH4Wd3RukD25x7VOMHUZ8/tNuQimgPNhno6unup2EFp7NfUT6aYF00d/L/3+NvZ4X/sPtnNy31V0Wm+C9r3thbdNehHDRxqqqT7lV6neD9O0ryv78L8reukyxNoHwnp/cCJpbLR/r31tkqNvUhb7xiZX8BwEAgGhHaFvm9ggty//ZACqC9Vu1WZe7j3YVdp887mb7tv6D8551k7FY6wSrNIlkmUekD3OqcEz/8dLw+6XMzFCvGfzMKq96jnEzk786RtqxRvrHmdKwe9xrKBwqs7KyXLg1bYJ0eK/bQWEVkDZpDyEEKuJ/zjlPSM36uX7J38/O2dnxgtS87/HveFv3qauqXfa+ZGGcqd5I6nWN21ZpPwPrN99zjI50+Zn+98H7OmtQLyXs3+z6ylu/3txh5zdI+7dJh9PdZyMbhYqRqjc8OtRNbeJ2SAR62Of2uE9y562XcqS01QEAAJWO0La0h+Dt3eSWQznBBlDWmZe7jpJOukRa8T/p48fcIayfP+dmje92uQtxInEG9EPpbobzVVPdF68zH5b6/txdR2iLkvZOvG629N6vpKVvSh/93rVLsInLrLrLr7atlt67yQVdpmkf6bynpfodQr1miHT2/6ZRF+mVK6Xtq6QXznI7ymwysNLu7LD3cGtTYv1qt3yXd3mLk91RIx3OluLoWYpCWAsYC1trN3PV14U5vF/as7GIUDdnOTPnO4CN4DYcJWE7ygKTluYGu4GQN+d8fCHnc2+T5CqB7e/wRiMppb77XAcAACIa/+1LY5cdU5UtJSRH1yHliCxW8dHxHPcl9/tZ0tw/Sus+kRa9KH31ktT5Qje7doPOigh7N0v/vtTNiG1ffuwwXfv7gdKqkipdPFlqOVCacqfb+TFpkKsgbNrTf5Xl856WZk10YUNCipssygKuiuwvCgSr31G6bpabEHPpW9LUO6Uf50vn/dltT8eyfY3bqfjVy1Jg0kj7DNZllHstR8r/KYR+LoC6bdwoqsJ737a8ENfmCQiEuvbd4OAuV9jh9bM/5HrZZ2fl/XzmYTdy5sYrHzGuqtwLcRvnhbkFT5PrUOkLAEAYI7QtjV1r8yYhC4dDYoHi2Gv4hNPcWD/ftUuwnp1LXnej/VmuN1xRlSnhYOtK6eWLXC9q++Jy+StSs96hXiuE+3bT+1qpSS834dLOtdLkEdLp90t9r/fH/4ZN30jvjJfSvnXnbRs/50l6sSM0kqpLF/9Daj5Amvr/3KRhaUukUf8qPHTNypRWTXMtENbMyLu8dmup9zip28+kqjUr9U9AlLP39Wr13LBWOSXdcXYkEOIGJiY9mHc++Lp8gW/gNgXOH9wt7U1zI32zaw1ipzbsPb8osfFStUCFbhHBrp3aESN++P8FAADyIbQtDSYhQ6Rq3k+64jVp07cuvLUv1VZFaKPVYBfethoUXh/o182T/nOZq4CxL/tXvB6ZrR8QGo27ST+f68LRZe+6iZesBYFVEIYqULJQYM7DbqJB+0Jvs6Gf8ZDU9bLw2nYReez11/c6qXF36bWrXG/ovw6VznnchbBm/w53tIdV1tqkme4Hpbanu4nFbOcDvUERLqx1QVx1t9OivNmODav89do1pBV9ahP0ZR3Jm4St2PVNcjv2bMeIzXVAv3MAAHyB0LZMk5C1qJhnAwg16z946T+lbaukT57wZl/WD3PcaNrbhbftzvB/ALTkTemt691h4bbel/+XliYof1VqSJe+6CoCp/5OWvaelLZYuuSfLtStTNZf1w5B377ane80UjrrUSZlgr/YkQ62s+PNa6U1M6W3fyGt/dT9T1n8mqsqDGxb3a+Uel/jdroByGMtbqo3cKM4mRlS+pagIDcQ6hYIeA/scJ+Xtq10E7bOf0Y67S7XLosdJQAAhBShbWkEKj+YhAyRrm5bN8HSkDukz552/W5t4g2rXG1wonTyr6XOF/ivN6b1nbP1nXaXO9/hHOnCv7p+dUCFVRD+3LVLeP0q1y7h78OlEQ+6NgrlvYPDO0x2j3QoMPa6ynirTjR2GOzZf6RvM/wrpY478mHuY9LsidLXL+Vd1+AkV5F74sW8bwPHyybnq9HEjWMdpWFtFlZPk2Y/7P6PvXGN+zw1/F6p9RCeCwAAQoTQtiyVtnb4EBANbAeFVesN+o007xnpi79Lm5e4D/Oz/iD1v0HqcpmUVC3Ua+oOF7RD1K3q0Vh/UQvO/BYsIzLZRGRWQfj2DdKKD6T/3ebaJZz7JzfhklU8WcBqQasXuu7NO1/oZTnL1scw+DKbzKYoPcZIw++j3yf8z96Xh/zWVd5O/b1Ur73b+dGsr/+P5AAiTUKVnNYI17rPdPOfdW12bALXF893rUmG3SM16hrqNQUAIOoQ2pap0pbQFlHGZii2aouTb5YW/NV9oN/xvfTBrdL0e6VuV7gP+0XNvFzRDu+X3hwnLX/fnT89J1Dmyz8qk03kctnLbgfH9AnS0reklR+5WcRtUpnylJjTK9EC4WoNXOuS1oPL9z6AimZh0C8/43EG/MJ2wg++Xep1tauGt6M4rJWJjZMukU79nVS7VajXEgCAqEFoW1KH0qX9290y7REQzaGUfZjv90s3YYxVtdqEMp8/50abYW7CmDbDK68Pmk3G8e9R0oYv3UQaFz7vWjcAoWA7CgaMdxWDr4+Vdv+Y//qEZBe2JqXmnOYEr975gpfZco2jL7PAlj6DAICKklJXOvMhVwE/60Fp8auu7/TSt12vaTsCy24DAAAqFKFtSe1anzc5RqhmBgf8VInR73oX0H4/01XfrpwqrZ7uRq2Wbgbi7le4oLeibF8jvXSRtPOHnCrH/0gt+lfc/QElZYd937jQTQyWmJIXyFqPQQAAwoFV1V70V7czcvo9ruL280lux/3AX7md+H5okQUAQISqpFK4CEBrBOBoVu1n1bU/e0W6aZHUf7zbsWGTWHz0O+nxTtJ7v5LSlpT/o/fjAulvw1xgay1LrplGYAt/iU+SGnR2OzGSaxPYAgDCk/WzvfIt6cq3pUbdpMPpbm6DP3V3LRSsbzsAACh3hLYlxSRkQPFqt5ZG/EG6Zbl07lNS/c5Sxn5p4QvSpIHSP85yh9WVxwf7Ze9J/zxXOrBDatxduna6VLctzxAAAEBFOeFUadws6eLJUq1W0r4tbn6DZ/q4Pu7Z2Tz25eXALlcEwWMKAFGN9gilbY/AJGRA8RKTpZ5XuZns18+TPn/ehazrPnWjemM3wYXdplq90j+a8ydJU+6QlC21O8N9cbDDzwEAAFDxR1mdeJHU4Vxp0T+lOQ+7yWlfu0pq3MNNXNtqEM9CWWQclFZOkb59VVr1kZSVIdVoLrUZ6o5ss8fV+tsDAKIGoW1p2yPYYa4ASjYhU4sBbuzZKH052VXd7t0ozXpAmvuImzCsz8+lpj2P/fuysqSPfi/Nf8ad73WNdOYjUhxvYwAAAJUqPlHqM07qepk07xnp0z9JGxe5I6EsYBx2j9TwJJ6Ukny+XfeJ9O0r0nfvSof25F0XEyftXi8t/IcbsfFSs355Ia49vvZ5GwAQsUg7StseoWbzins2gEiV2lg67fdutuHv3nHVtxu+dB9QbVhlhk1qZiFuQpXCKw/eus79rLEvAgNv5oMqAABAKNkkm0PucEdRzX3U7aT3JqadIXW5VDr1d1KtFjxHBdl8D4tflRa/Lu3ZkHd5alOpyyXSSZe6YiE7Si0w0a9NbmoBr40Z90rVGkgnWIA7VDrhNNc/HwAQUQhtS8J6CTERGVAO7zhJ7gO8jQ0LpQV/k5a87ioz3r7eVdL2HOM++Ndo6n5m/w7pP5dLP86X4hKlkc9JJ13MswEAAOAX1epLZz0q9b3eTVK25A23Y9563fa+VjrlNimljqLa7p9cSGvtD7Yszbs8qYbUeaT7fNx8gGtBEdB2uBtmxw/SmhnSqunSD3Ol9M3SN/92QzFSk56uAtdGkx5SbFzl/40AgHJFaFsSB3bmHapCpS1QPuyD5QU9pdPvdz3Rvpgs7flJ+viP0idPSh3OduHsjPtcZUGVGtJl/5ZanswzAAAA4Ed1TnDzDQy4UZo2QfphjjT/Wemrl6SBN7kKUvs+FS2H9duEYsvedUHt2k/cnAzGChHani51GeVOCzvSrKDaraTa17oQ/Mghaf38vKpmC4HtKDYbcx6SqtR01bdeiDtUqt6wwv9UINdPC11fZqvET23k5jSx02oNS/ZaB5CL0LY0k5Cl1HeTLAEoPyl1pVNulQb8SlrxP2nBX6S1H7sPuDZMjWbSFa9L9TvwyAMAAPhd4+7SmHelNTNdeJv2rTTzATeS67rrrRrUWmTZqVXqRgoLVFdNc5XGK6dKmYfyrmsx0FXUdjpfqlrr+I5eaz3YDSuAsPkjLLy1EPf7WdLBXdLSN90wDU7K64XbrK/rSQyUp8yMnDZ4k6Sfvij6dlVru9Z51S3MbZi37J02dAFvcp38FedAFCO0LYnc1gj0swUqjE0o1uk8NzZ/J33xV+mb/0r1OrgKW9s7CwAAgPBh1Z6thrjw0CYss/B2/zZp9TQ3AmwHfXCQ27ibO8oqnCYUs1ZeVlFrLSEsNA2wz7JWUWtHkFXU90kLvHpc6UbmEdeGLNALd+NX0ubFbnz6pJRYTWo12IW4rQZJKfVcRSTtFFAW+7a5ifK++Lu0d1NeJXn7s6SYWGlvmpuIes8mtwPjwA43Ni8p+nfGJgSFukGVunYaHPSGe0Hd4X1u7iTLm+yxsskF7e+KliMRUCKEtqWZhIwm+kDlaNBJOucJ6cxH3T8w9rQCAACEJ/scZ4GlDZtc1sKaDYvcnAZ2um2ltPtHNwJHWZk6bfNX41qgkVBVvrJluZtQ7NvXpN05R2caC168v/lSt96VGcJYIUTzvm6c9jsXqq2Z5QJc64m7b6u04gM3glmYm5QqVUl1pxbkBpa90xqFXBa4nV2XSgVvNNn0rZtcevFredXkNjler2ukXmOPrp63eYKs7aQFuxbgBoJcO+9dttEFvPb6zMpw21PwNlUYe92lNnEhrp3anCiB84HlUAa7tgPF2v8Fgtmda92yndp5+1sLsiMRGnWVGnVxpw27SLVa8X04ihHalgSTkAGhYR86AQAAEBmsn2XTXm4EHNwjbfomL8S1U2tPt32VG9ZmwMTGS/U75oW4Nj9CvY4V/3nRwqaMA1LGflcZd2iv9P1st15WORyQWN0dMWbtD1qe4p/KVWtF1uUSN6wi2NY50AvXqnCPHHC3O5zuhoVpZRVfxYW4waGutYFoPcS1hEiurbDuT2zPqf190cpCSGtnZy0Q1n2ad7lVyff9hdT5gqKDe9txYc+/jQadi7mPjJzq3E3FBLybpIx90sHdbmz5rujfZ6+/1KY5QW6ToHA3J+i1UdY+u/beYDtFcgPZtfnDWZt8MDvz2MFzrZbSkcPSthXuSATbuWIj+L3FQlwLcANhbt12UlxC2dYbYYVEpDQ9bam0BQAAAIDyY+Feq1PcCLAgxALF3Ircha4qLW2xGzaJrYmv6kKMQJBrp4kpOQGrhZA5QasFPN5yel74GhjB54taDkwgVpAFyW2Gu6C2/Zn+qwQurOrZWk/YGHSbu8zCIpt028IvO7VQ2oJ077I9QdftLXBZ0Kk9vt7vOuhGwQrC796W/neba5dx4kXu0Hl73v1u9wZp2XuuAnzdZy60bdZPajvMTSBXv1N0HMpuFbKLXpQW/NVVxAde+xbE971eatq7/B4HCyJrNnOjuLDUXo9WnbtnQ87Y6EJSW7bnzU5te7d1t2HtQYpi1a3B1bmBcDdQtWs7bXID2XX5lwOv/SL/niTXFsWyJAtna7bIv1y1Zt5t7X6sTeCmr93OFatm3rxUOrzXheTBQbn9Xjs6NVCNa6cWhvv9PQilRmhbmvYItlEBAAAAACq2OrTtcDcCIY0FMsHVuBu/dqHhj5+7URksJLbDra11g1WudrpASqmjsGaVkfF13WN+PBWYFiwVFujaIe42QZWF7as+csMqci30tAC33Qh/BU07fnAh7XfvShu+zH9d1hFp3SduTL/H9Vi1ANeCe6smDocgurTtP6yq1qrKbSdGYCIxa3/Q+1oXaIaCBcT2WNsoaqJqL9jdkxfgBgLdguGuVZpbdauN4Mr5kq+Mexy8MLalC2SDl6s1LHlrA9sOmvZ0I7jy2FrIWIBrRyTYOtq2ZH+b7diykbsqca4CN9BewcJca88SHAzj2OyIBAv67TVf3M6DSkJoeyy2sQcqbZmIDAAAAAAqP6QJVN9ZdV/gi/WONfn741qgYSGHVdvaSEh2Iav1a/WWgy9PKfy8t1zN/VzBZb+0PPAba1Fhh6HbKMyg30hbV7oJ6Ra/7tpeWDBqwx5fq7y1HsCtTw1NX9ytK1xIuywnXM4VIzXvJ3U8T+p4rgttrbWEBc8/fOwO27cKVBtWeepV4ebsbAjXKlzbruzv+/w51wYkoMGJrqrWnic/hezFBrs13LCK1MIE+uzmVuf+lHOaU8Hrhbsb3d9baKWsjWZSfFLF/R1WeWwVtDa6XZ73HO38Ia8aNxDmWoX71mVufPvfvN9h6xporVC/s2szY39HNM0bc+SQe3xspOec7tvijurwLgss55xaWwt7zf8iqLo5RAhtj8WePK/PT4yb1RQAAAAAEFoWONRt60bXUXkhjAnHsCzS1WsnDblDGvxbF4wueUNa8qarxLXJ3GxUqen6AlsFbkX2BbbXiU2I5wW170pbl+evVmw5MC+ord4w/8/2GeeGHcpuh6uvsv7A06Ttq4OqcCe4Q+vbDA2fKlyriv76ZTe5mAWCxiaEtkC93y+kFgMjb7sK7rNrFanh9N5X5wQ3rI9w4DVtvYADAa6dWqBr21eg327wRI8JKa5K2QJc28HgnXZ2E8iFw/Ocne3atuzeGRTGBgevdtm2vMsO7S5b0OsDhLYlnYTM3nRDsdcPAAAAAHBs4RA2RDt7jrzJlLpIw+6RfvrCBbhL35LSN+dVrqbUd4GUBbjWM/V4qwIt5LFqbKumtbA2EEya2IScydLOk9qfXbKWF1Z92cZaIwyT9JC04/u8ANeqcK1SM7gKt3l/d1u/VeFuXyMt+Iv01cuuxYWx6tQeo6Xe45jXJ1zY6ym1kRvtz8i7fP8Ot5PEQlzbUWGTtlnVu/XitV7hNoJZ+4tAiGsVyrZcr0PltliwPtvehHOBnsUbc8675fjdG3RO+hbFfX2kdL/XtvOUeq4VjJ1aQB1Ytvcb7zJbruf6HPsk/yO0PRYmIQMAAAAAoPyDpmZ93BjxoLT2ExfgWg9cq5Zb8LwbdsTriRe6ANcO8y5p4JmV6fodexW177nD3wOsr66FqFZRa311jzeUqt1a6nudG8FVuNZqwNp4rP3YjeAqXOvr22pw5VfhWoC9ZqarqrX1C0y0V7e91PfnUtfLXKsQhD+rIm492I3gHtS208ImOduyzAW5NmzHw4EdeRXjwew1m1uRm3Nar33pW2XYhJBBAWzeadCwbb8YMdY1InAmKTUndM0JY70gtl7+EQhnrZLfLztLSoHQ9lisjNzQzxYAAAAAgPJnrRAC4dJZj0nfz3IB7vIPpN0/Sp8+5YZNAmfhrQ1ruVCQBVIWjno9c9/PHwDZIeHtTnd9ka1tQVK1inkmg6twzyxDFa73dxx2h2fnnh5yFYhHDh59Wb7T4J/JuX3B62wSv20r8ta37Qip3/Wup3AYhlooQw/qQGuZziPzLredDTbpmQW5uYHuMrezwwtXN7jXcIC1z7CdFblBbifXP3f/9qAQNjiU3SAd3FXCdUxyE7ylNsk5zRtHqtbXjC+W6rRzLlFC1eoR//QT2pa0PYI1agYAAAAAABXHDku26lcbFiStnOoCXDu1SczmPORGg5NcBW7bM1V/9zeKe3+qtPJDVy0YkFRDan+ma31wwmmhmUSr0CrcaW4UrMK1Q7izMip+nRKrS92vkPpc53qjArZtNOrqRrADu1zfZ68i1wJdO13qJnGzXs42rJK9pGznSY0mhYSyTaTq1uKhiasQLmIHQnZGhg5+k+aq5aMAoe2x7MwJbW2GQAAAAAAAUHlBklUD2rDJslZ86ALcNTOkzYu9kTDjXvUP/pnkOlKHs6WO50utBvmmN+XRVbgPF6jCnesqYwuyIDc+SYpLLHCa5P427zSpmOsKnNqh4h3O8f/kaPAHax3SvJ8bwS02bJKvQGuFQKC760fXkiA4iC1QKeu1NKCiu8QIbUva05ZKWwAAAAAAQsNCxq6j3LAJlqwFwpI3lP3DxzoUX0MJXS9S3IkjpeYD3CHg4SBfFe5Baf+2/AGrhbDHOwkbUN4sdK3ewI0TTuXxrUBh8k4WIta4fHdOs3IqbQEAAAAACD07fLrnVd44sn+Ppn40Q2edcY7iEhIUthKqSDWahnotAPgIu2yKk57m+snY4QjWWwMAAAAAAPir5YBNigQAEYZ3tmLEWD8OY3u7bDZLAAAAAAAAAKhghLbFoTUCAAAAAAAAgGgMbZ955hm1bNlSVapUUd++fbVgwYIib/vCCy8oJiYm37CfK8r111/v3ebJJ58s9XrF7MmptGUSMgAAAAAAAADREtq+8soruuWWWzRhwgQtWrRIXbt21YgRI7Rly5YifyY1NVWbNm3KHevWrSv0dm+99Zbmz5+vxo0bl2ndYqi0BQAAAAAAABBtoe3jjz+ucePGaezYserUqZMmTZqk5ORkTZ48ucifscrZhg0b5o4GDRocdZsNGzboxhtv1Msvv6yEss4guZtKWwAAAAAAAACVK14hdPjwYS1cuFB33nln7mWxsbEaNmyY5s2bV+TPpaenq0WLFsrKylKPHj304IMPqnPnzrnX2+VXXnmlfvOb3+S7vCiHDh3yRsCePXvyhbZHqjdRdkZGWf9MAD6WkbNtB04BRC62dyB6sL0D0YPtHYgeGRHy/b2k6x/S0Hbbtm3KzMw8qlLWzi9fvrzQn2nfvr1XhdulSxft3r1bjz32mAYMGKClS5eqadOm3m0efvhhxcfH66abbirRekycOFH33nvvUZfHpG+WkqTpX67SoW+KbtcAIPxNmzYt1KsAoJKwvQPRg+0diB5s70D0mBbm39/379/v/9C2LPr37++NAAtsO3bsqOeff17333+/V7n71FNPef1xrY1CSVilr/XVDa60bdasmWKUrez4ZA097zLryVAhfw+A0O/hsjf84cOHl72VCoCwwPYORA+2dyB6sL0D0SMjQr6/5x7h7+fQtm7duoqLi9PmzZvzXW7nrVdtSdiT1L17d61evdo7//HHH3uTmDVv3jz3NlbNe+utt+rJJ5/U2rVrj/odSUlJ3ihMTM3mSkhMLOVfBiDc2HtJOL/pAyg5tncgerC9A9GD7R2IHglh/v29pOse0onIEhMT1bNnT82YMSNfP1o7H1xNWxwLZBcvXqxGjRp5562X7bfffquvv/46dzRu3Njrbzt16tTSr2StFqX/GQAAAAAAAAAoo5C3R7C2BGPGjFGvXr3Up08frxp23759Gjt2rHf96NGj1aRJE6/vrLnvvvvUr18/tWnTRrt27dKjjz6qdevW6dprr/Wur1OnjjcKJthWuWv9cEutJqEtAAAAAAAAgCgKbUeNGqWtW7fq7rvvVlpamrp166YpU6bkTk62fv16xcbmFQTv3LlT48aN825bq1Ytr1L3s88+U6dOnSpmBam0BQAAAAAAABBNoa0ZP368Nwoze/bsfOefeOIJb5RGYX1sS4xKWwAAAAAAAACVKKQ9bcNCzbwJzQAAAAAAAACgohHaHgvtEQAAAAAAAABUIkLbYmQnVpeq1qq8ZwMAAAAAAABA1CO0LU6NZlH/AgEAAAAAAABQuQhti5Gd2rTyngkAAAAAAAAAILQtXnZNQlsAAAAAAAAAlYtK2+LQHgEAAAAAAABAJSO0LUY2oS0AAAAAAACASkZoWwxCWwAAAAAAAACVjdC2OFTaAgAAAAAAAKhkhLbFSahaaU8EAAAAAAAAABhCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARQlsAAAAAAAAA8BFCWwAAAAAAAADwEUJbAAAAAAAAAPARX4S2zzzzjFq2bKkqVaqob9++WrBgQZG3feGFFxQTE5Nv2M8FZGRk6Le//a1OOukkpaSkqHHjxho9erQ2btxYSX8NAAAAAAAAAIRxaPvKK6/olltu0YQJE7Ro0SJ17dpVI0aM0JYtW4r8mdTUVG3atCl3rFu3Lve6/fv3e7/nrrvu8k7ffPNNrVixQuedd14l/UUAAAAAAAAAUHbxCrHHH39c48aN09ixY73zkyZN0gcffKDJkyfrjjvuKPRnrLq2YcOGhV5Xo0YNTZs2Ld9lf/7zn9WnTx+tX79ezZs3P+pnDh065I2APXv25Fbt2gAQuQLbONs6EPnY3oHowfYORA+2dyB6ZETI9/eSrn9IQ9vDhw9r4cKFuvPOO3Mvi42N1bBhwzRv3rwify49PV0tWrRQVlaWevTooQcffFCdO3cu8va7d+/2gt6aNWsWev3EiRN17733HnX5rFmzlJycXOq/C0D4KbizB0DkYnsHogfbOxA92N6B6DEtzL+/W5cA34e227ZtU2Zmpho0aJDvcju/fPnyQn+mffv2XhVuly5dvDD2scce04ABA7R06VI1bdr0qNsfPHjQ63F7+eWXe20VCmOhsbVoCK60bdasmU499VTVqVPnuP9OAP7ew2Vv+MOHD1dCQkKoVwdABWJ7B6IH2zsQPdjegeiRESHf3wNH+Pu+PUJp9e/f3xsBFth27NhRzz//vO6///6jnsxLL71U2dnZeu6554r8nUlJSd4oyF4A4fwiAFBybO9A9GB7B6IH2zsQPdjegeiREOZ5XUnXPaShbd26dRUXF6fNmzfnu9zOF9WztrA/tHv37lq9enWhga1NUjZz5swiq2wBAAAAAAAAwE9iQ3nniYmJ6tmzp2bMmJF7mfWptfPB1bTFsfYKixcvVqNGjY4KbFetWqXp06fT4gAAAAAAAABA2Ah5ewTrJTtmzBj16tVLffr00ZNPPql9+/Zp7Nix3vWjR49WkyZNvMnCzH333ad+/fqpTZs22rVrlx599FGvmvbaa6/NDWwvvvhiLVq0SO+//74X6qalpXnX1a5d2wuKAQAAAAAAAMCvQh7ajho1Slu3btXdd9/thavdunXTlClTcicnW79+vWJj8wqCd+7cqXHjxnm3rVWrllep+9lnn6lTp07e9Rs2bNC7777rLdvvCjZr1iwNGTKkUv8+AAAAAAAAAAir0NaMHz/eG4WZPXt2vvNPPPGEN4rSsmVLb+IxAAAAAAAAAAhHIe1pCwAAAAAAAADIj9AWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHwkPtQr4EfZ2dne6d69e5WQkBDq1QFQgTIyMrR//37t2bOH7R2IcGzvQPRgeweiB9s7ED0yIuT7u61/cP5YFELbQmzfvt07bdWqVUU8NwAAAAAAAACi2N69e1WjRo0irye0LUTt2rW90/Xr1xf74AEIf7aHq1mzZvrxxx+Vmpoa6tUBUIHY3oHowfYORA+2dyB67ImQ7+9WYWuBbePGjYu9HaFtIWJjXatfC2zD+UUAoORsW2d7B6ID2zsQPdjegejB9g5Ej9QI+P5ekiJRJiIDAAAAAAAAAB8htAUAAAAAAAAAHyG0LURSUpImTJjgnQKIbGzvQPRgeweiB9s7ED3Y3oHokRRleV1MtnW/BQAAAAAAAAD4ApW2AAAAAAAAAOAjhLYAAAAAAAAA4COEtgAAAAAAAADgI4S2AAAAAAAAAOAjhLaFeOaZZ9SyZUtVqVJFffv21YIFCyr/mQFQrubOnatzzz1XjRs3VkxMjN5+++1819ucjHfffbcaNWqkqlWratiwYVq1ahXPAhBmJk6cqN69e6t69eqqX7++Ro4cqRUrVuS7zcGDB3XDDTeoTp06qlatmi666CJt3rw5ZOsMoGyee+45denSRampqd7o37+/Pvzww9zr2daByPXQQw95n+lvvvnm3MvY5oHIcM8993jbd0zQ6NChQ1Ru64S2Bbzyyiu65ZZbNGHCBC1atEhdu3bViBEjtGXLltA8QwDKxb59+7zt2XbKFOaRRx7Rn/70J02aNEmff/65UlJSvG3f/iEACB9z5szxPsTNnz9f06ZNU0ZGhk4//XTvPSDg17/+td577z299tpr3u03btyoCy+8MKTrDaD0mjZt6gU3Cxcu1JdffqnTTjtN559/vpYuXepdz7YORKYvvvhCzz//vLfTJhjbPBA5OnfurE2bNuWOTz75JCq39ZhsKy9DLqustQqdP//5z975rKwsNWvWTDfeeKPuuOMOHikgAtieurfeesurwDP2NmgVuLfeeqtuu+0277Ldu3erQYMGeuGFF3TZZZeFeI0BlNXWrVu9ilv7QDdo0CBv265Xr57+/e9/6+KLL/Zus3z5cnXs2FHz5s1Tv379eLCBMFa7dm09+uij3vbNtg5EnvT0dPXo0UPPPvusHnjgAXXr1k1PPvkk/9+BCKu0tSNjv/7666Oui7bP8lTaBjl8+LC3p94Oi859gGJjvfP25AOITD/88IPS0tLybfs1atTwduKw7QPhzT7YBYIcY//nrfo2eHu3w62aN2/O9g6EsczMTP33v//1quqtTQLbOhCZ7Gias88+O9//ccM2D0QWa1XYuHFjtW7dWldccYXWr18fldt6fKhXwE+2bdvmfeCz6rpgdt6SewCRyQJbU9i2H7gOQPixo2Ws193AgQN14oknepfZNp2YmKiaNWvmuy3bOxCeFi9e7IW01s7I+trZkTSdOnXyqnPY1oHIYjtmrIWhtUcoiP/vQOSw4ik74rV9+/Zea4R7771Xp5xyipYsWRJ12zqhLQAAiNhqHPtwF9wDC0BksS90FtBaVf3rr7+uMWPGeO1QAESWH3/8Ub/61a+8fvU2YTiAyHXmmWfmLnfp0sULcVu0aKFXX33VmzQ8mtAeIUjdunUVFxd31Kxzdr5hw4aV/dwAqCSB7ZttH4gc48eP1/vvv69Zs2Z5kxUFb+/WDmnXrl35bs//eiA8WbVNmzZt1LNnT02cONGbdPSpp55iWwcijB0SbZODWz/b+Ph4b9gOGptI2Jatyo7/70Bkqlmzptq1a6fVq1dH3f93QtsCH/rsA9+MGTPyHVpp5+2wKwCRqVWrVt4bfPC2v2fPHn3++eds+0CYsYkFLbC1Q6Rnzpzpbd/B7P98QkJCvu19xYoVXp8s/tcD4c8+ux86dIhtHYgwQ4cO9dqhWGV9YPTq1cvrdRlY5v87ELkTEK5Zs0aNGjWKuv/vtEco4JZbbvEOq7I3/T59+ngzUdqEBmPHjg3NMwSgXNgbve2ZC558zD7g2eRE1rTc+l7aDLRt27b1Qp677rrLa3w+cuRIngEgzFoi2Gyy77zzjqpXr57b28omF7TDqez0mmuu8f7f2/afmpqqG2+80fuQF2mzzQKR7s477/QOobT/43v37vW2/dmzZ2vq1Kls60CEsf/pgf70ASkpKapTp07u5fx/ByLDbbfdpnPPPddribBx40ZNmDDBOyr+8ssvj7r/74S2BYwaNUpbt27V3Xff7X3R69atm6ZMmXLUBEUAwsuXX36pU089Nfe8vckb20ljTc5vv/12bwfNdddd5x1qcfLJJ3vbPj2zgPDy3HPPeadDhgzJd/k//vEPXXXVVd7yE088odjYWF100UVeRd6IESP07LPPhmR9AZSdHSo9evRob5IS+xJnfe8ssB0+fLh3Pds6EF3Y5oHI8NNPP3kB7fbt21WvXj3vu/n8+fO95Wjb1mOy7ThCAAAAAAAAAIAv0NMWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAAAAAAB8hNAWAAAAAAAAAHyE0BYAAAAAAAAAfITQFgAAAKgALVu21JNPPsljCwAAgFIjtAUAAEDYu+qqqzRy5EhveciQIbr55psr7b5feOEF1axZ86jLv/jiC1133XWVth4AAACIHPGhXgEAAADAjw4fPqzExMQy/3y9evXKdX0AAAAQPai0BQAAQERV3M6ZM0dPPfWUYmJivLF27VrvuiVLlujMM89UtWrV1KBBA1155ZXatm1b7s9ahe748eO9Kt26detqxIgR3uWPP/64TjrpJKWkpKhZs2b65S9/qfT0dO+62bNna+zYsdq9e3fu/d1zzz2FtkdYv369zj//fO/+U1NTdemll2rz5s2519vPdevWTf/617+8n61Ro4Yuu+wy7d27N/c2r7/+urcuVatWVZ06dTRs2DDt27evEh5ZAAAAVCZCWwAAAEQMC2v79++vcePGadOmTd6woHXXrl067bTT1L17d3355ZeaMmWKF5hacBrsn//8p1dd++mnn2rSpEneZbGxsfrTn/6kpUuXetfPnDlTt99+u3fdgAEDvGDWQtjA/d12221HrVdWVpYX2O7YscMLladNm6bvv/9eo0aNyne7NWvW6O2339b777/vDbvtQw895F1nv/vyyy/X1VdfrWXLlnmB8YUXXqjs7OwKfEQBAAAQCrRHAAAAQMSw6lQLXZOTk9WwYcPcy//85z97ge2DDz6Ye9nkyZO9QHflypVq166dd1nbtm31yCOP5Pudwf1xrQL2gQce0PXXX69nn33Wuy+7T6uwDb6/gmbMmKHFixfrhx9+8O7TvPjii+rcubPX+7Z379654a71yK1evbp33qqB7Wf/8Ic/eKHtkSNHvKC2RYsW3vVWdQsAAIDIQ6UtAAAAIt4333yjWbNmea0JAqNDhw651a0BPXv2POpnp0+frqFDh6pJkyZemGpB6vbt27V///4S379VxlpYGwhsTadOnbwJzOy64FA4ENiaRo0aacuWLd5y165dvfWwoPaSSy7RX//6V+3cubMMjwYAAAD8jtAWAAAAEc960J577rn6+uuv841Vq1Zp0KBBubezvrXBrB/uOeecoy5duuiNN97QwoUL9cwzz+ROVFbeEhIS8p23Cl6rvjVxcXFeW4UPP/zQC3yffvpptW/f3qveBQAAQGQhtAUAAEBEsZYFmZmZ+S7r0aOH15PWKlnbtGmTbxQMaoNZSGuh6R//+Ef169fPa6OwcePGY95fQR07dtSPP/7ojYDvvvvO67VrAWxJWYg7cOBA3Xvvvfrqq6+8+37rrbdK/PMAAAAID4S2AAAAiCgWzH7++edeley2bdu80PWGG27wJgGzibysh6y1RJg6darGjh1bbOBqoW5GRoZX1WoTh/3rX//KnaAs+P6sktd6z9r9FdY2YdiwYV5bgyuuuEKLFi3SggULNHr0aA0ePFi9evUq0d9lf5P15LWJ1NavX68333xTW7du9QJhAAAARBZCWwAAAESU2267zWslYBWs9erV8wLOxo0b69NPP/UC2tNPP90LUG2CMespGxtb9Edi6yP7+OOP6+GHH9aJJ56ol19+WRMnTsx3mwEDBngTk40aNcq7v4ITmQUqZN955x3VqlXLa8dgIW7r1q31yiuvlPjvSk1N1dy5c3XWWWd5Fb+///3vvQrgM888s5SPEAAAAPwuJjs7OzvUKwEAAAAAAAAAcKi0BQAAAAAAAAAfIbQFAAAAAAAAAB8htAUAAAAAAAAAHyG0BQAAAAAAAAAfIbQFAAAAAAAAAB8htAUAAAAAAAAAHyG0BQAAAAAAAAAfIbQFAAAAAAAAAB8htAUAAAAAAAAAHyG0BQAAAAAAAAAfIbQFAAAAAAAAAPnH/wcMEdDPgbJLwQAAAABJRU5ErkJggg==" + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[指标分析]\n", + " 各NDCG指标在验证集上的最佳值:\n", + " ndcg@10: 0.5704 (迭代 2)\n", + "\n", + "[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!\n" + ] + } + ], + "execution_count": 10 }, { "metadata": {}, @@ -873,10 +1279,13 @@ "source": "### 4.6 模型评估" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:15:07.323298800Z", + "start_time": "2026-03-12T15:56:26.601415Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"模型评估\")\n", @@ -915,13 +1324,63 @@ " top_features = importance.sort_values(ascending=False).head(20)\n", " 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.4869\n", + " ndcg@5: 0.5123\n", + " ndcg@10: 0.5211\n", + " ndcg@20: 0.5237\n", + "\n", + "特征重要性(Top 20):\n", + "----------------------------------------\n", + " 1. turnover_rank 280.23\n", + " 2. mom_acceleration_10_20 75.44\n", + " 3. volatility_20 69.78\n", + " 4. profit_margin 64.57\n", + " 5. revenue_yoy 62.87\n", + " 6. BP 60.64\n", + " 7. std_return_20 48.58\n", + " 8. EP 45.02\n", + " 9. return_20 43.80\n", + " 10. drawdown_from_high_60 43.42\n", + " 11. pv_corr_20 43.31\n", + " 12. kaufman_ER_20 39.42\n", + " 13. active_market_cap 37.19\n", + " 14. turnover_rate_mean_5 32.11\n", + " 15. min_ret_20 29.80\n", + " 16. max_ret_20 28.82\n", + " 17. amihud_illiq_20 28.09\n", + " 18. up_days_ratio_20 27.88\n", + " 19. ma_20 26.77\n", + " 20. EP_rank 24.64\n" + ] + } + ], + "execution_count": 11 }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-13T14:15:07.323298800Z", + "start_time": "2026-03-12T15:56:26.866785Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "# 确保输出目录存在\n", "os.makedirs(OUTPUT_DIR, exist_ok=True)\n", @@ -968,7 +1427,42 @@ "print(topn_to_save.head(15))\n", "\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.011742 ┆ 605189.SH │\n", + "│ 2025-01-02 ┆ 0.010834 ┆ 002076.SZ │\n", + "│ 2025-01-02 ┆ 0.010567 ┆ 603041.SH │\n", + "│ 2025-01-02 ┆ 0.010191 ┆ 600493.SH │\n", + "│ 2025-01-02 ┆ 0.008633 ┆ 000679.SZ │\n", + "│ … ┆ … ┆ … │\n", + "│ 2025-01-06 ┆ 0.016587 ┆ 002343.SZ │\n", + "│ 2025-01-06 ┆ 0.011217 ┆ 002188.SZ │\n", + "│ 2025-01-06 ┆ 0.010834 ┆ 002076.SZ │\n", + "│ 2025-01-06 ┆ 0.007108 ┆ 000677.SZ │\n", + "│ 2025-01-06 ┆ 0.006257 ┆ 603117.SH │\n", + "└────────────┴──────────┴───────────┘\n", + "\n", + "训练流程完成!\n" + ] + } + ], + "execution_count": 12 }, { "metadata": {}, diff --git a/src/experiment/learn_to_rank.py b/src/experiment/learn_to_rank.py index b869599..b8c75de 100644 --- a/src/experiment/learn_to_rank.py +++ b/src/experiment/learn_to_rank.py @@ -31,6 +31,7 @@ from src.training import ( Winsorizer, NullFiller, StandardScaler, + check_data_quality, ) from src.training.components.models import LightGBMLambdaRankModel from src.training.config import TrainingConfig @@ -39,13 +40,13 @@ from src.training.config import TrainingConfig # %% md # ## 2. 辅助函数 # %% -def create_factors_with_metadata( +def register_factors( engine: FactorEngine, selected_factors: List[str], factor_definitions: dict, label_factor: dict, ) -> List[str]: - """注册因子(SELECTED_FACTORS 从 metadata 查询,FACTOR_DEFINITIONS 用表达式注册)""" + """注册因子(selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册)""" print("=" * 80) print("注册因子") print("=" * 80) @@ -326,14 +327,18 @@ VAL_END = "20241231" TEST_START = "20250101" TEST_END = "20251231" + +# 分位数配置 +N_QUANTILES = 20 # 将 label 分为 20 组 + # LambdaRank 模型参数配置 MODEL_PARAMS = { "objective": "lambdarank", "metric": "ndcg", - "ndcg_at": 2, # 评估 NDCG@k + "ndcg_at": 10, # 评估 NDCG@k "learning_rate": 0.01, "num_leaves": 31, - "max_depth": 6, + "max_depth": 4, "min_data_in_leaf": 20, "n_estimators": 2000, "early_stopping_round": 300, @@ -343,21 +348,10 @@ MODEL_PARAMS = { "reg_lambda": 1.0, "verbose": -1, "random_state": 42, + "lambdarank_truncation_level": 10, + "label_gain": [i for i in range(1, N_QUANTILES + 1)], } -# 分位数配置 -N_QUANTILES = 20 # 将 label 分为 20 组 - -# 特征列(用于数据处理器) -FEATURE_COLS = SELECTED_FACTORS - -# 数据处理器配置 -PROCESSORS = [ - NullFiller(feature_cols=FEATURE_COLS, strategy="mean"), - Winsorizer(feature_cols=FEATURE_COLS, lower=0.01, upper=0.99), - StandardScaler(feature_cols=FEATURE_COLS), -] - # 股票池筛选函数 def stock_pool_filter(df: pl.DataFrame) -> pl.Series: @@ -406,7 +400,7 @@ engine = FactorEngine() # 2. 使用 metadata 定义因子 print("\n[2] 定义因子(从 metadata 注册)") -feature_cols = create_factors_with_metadata( +feature_cols = register_factors( engine, SELECTED_FACTORS, FACTOR_DEFINITIONS, LABEL_FACTOR ) @@ -435,10 +429,14 @@ print(f"[配置] 特征数: {len(feature_cols)}") print(f"[配置] 目标变量: {target_col}({N_QUANTILES}分位数)") # 6. 创建排序学习模型 -model = LightGBMLambdaRankModel(params=MODEL_PARAMS) +model: LightGBMLambdaRankModel = LightGBMLambdaRankModel(params=MODEL_PARAMS) -# 7. 创建数据处理器 -processors = PROCESSORS +# 7. 创建数据处理器(使用函数返回的完整特征列表) +processors = [ + NullFiller(feature_cols=feature_cols, strategy="mean"), + Winsorizer(feature_cols=feature_cols, lower=0.01, upper=0.99), + StandardScaler(feature_cols=feature_cols), +] # 8. 创建数据划分器 splitter = DateSplitter( @@ -522,7 +520,25 @@ if splitter: else: raise ValueError("必须配置数据划分器") # %% md -# ### 4.3 数据预处理 +# ### 4.3 数据质量检查 +# %% +print("\n" + "=" * 80) +print("数据质量检查(必须在预处理之前)") +print("=" * 80) + +print("\n检查训练集...") +check_data_quality(train_data, feature_cols, raise_on_error=True) + +print("\n检查验证集...") +check_data_quality(val_data, feature_cols, raise_on_error=True) + +print("\n检查测试集...") +check_data_quality(test_data, feature_cols, raise_on_error=True) + +print("[成功] 数据质量检查通过,未发现异常") + +# %% md +# ### 4.4 数据预处理 # %% print("\n" + "=" * 80) print("数据预处理") @@ -584,112 +600,51 @@ print("\n" + "=" * 80) print("训练指标曲线") print("=" * 80) -# 重新训练以收集指标(因为之前的训练没有保存评估结果) -print("\n重新训练模型以收集训练指标...") +# 从模型获取训练评估结果 +evals_result = model.get_evals_result() -import lightgbm as lgb - -# 准备数据(使用 val 做验证,test 不参与训练过程) -X_train_np = X_train.to_numpy() -y_train_np = y_train.to_numpy() -X_val_np = val_data.select(feature_cols).to_numpy() -y_val_np = val_data.select(target_col).to_series().to_numpy() - -# 创建数据集 -train_dataset = lgb.Dataset(X_train_np, label=y_train_np, group=train_group) -val_dataset = lgb.Dataset( - X_val_np, label=y_val_np, group=val_group, reference=train_dataset -) - -# 用于存储评估结果 -evals_result = {} - -# 使用与原模型相同的参数重新训练 -# 正确的三分法:train用于训练,val用于验证,test不参与训练过程 -booster_with_eval = lgb.train( - MODEL_PARAMS, - train_dataset, - num_boost_round=MODEL_PARAMS.get("n_estimators", 1000), - valid_sets=[train_dataset, val_dataset], - valid_names=["train", "val"], - callbacks=[ - lgb.record_evaluation(evals_result), - lgb.early_stopping(stopping_rounds=50, verbose=True), - ], -) - -print("训练完成,指标已收集") - -# 获取评估的 NDCG 指标 -ndcg_metrics = [k for k in evals_result["train"].keys() if "ndcg" in k] -print(f"\n评估的 NDCG 指标: {ndcg_metrics}") - -# 显示早停信息 -actual_rounds = len(list(evals_result["train"].values())[0]) -expected_rounds = MODEL_PARAMS.get("n_estimators", 1000) -print(f"\n[早停信息]") -print(f" 配置的最大轮数: {expected_rounds}") -print(f" 实际训练轮数: {actual_rounds}") -if actual_rounds < expected_rounds: - print(f" 早停状态: 已触发(连续50轮验证指标未改善)") +if evals_result is None or not evals_result: + print("[警告] 没有可用的训练指标,请确保训练时使用了 eval_set 参数") else: - print(f" 早停状态: 未触发(达到最大轮数)") + print("[成功] 已从模型获取训练评估结果") -# 显示各 NDCG 指标的最终值 -print(f"\n最终 NDCG 指标:") -for metric in ndcg_metrics: - train_ndcg = evals_result["train"][metric][-1] - val_ndcg = evals_result["val"][metric][-1] - print(f" {metric}: 训练集={train_ndcg:.4f}, 验证集={val_ndcg:.4f}") -# %% -# 绘制 NDCG 训练指标曲线 -import matplotlib.pyplot as plt + # 获取评估的 NDCG 指标 + ndcg_metrics = [k for k in evals_result["train"].keys() if "ndcg" in k] + print(f"\n评估的 NDCG 指标: {ndcg_metrics}") -fig, axes = plt.subplots(2, 2, figsize=(14, 10)) -axes = axes.flatten() + # 显示早停信息 + actual_rounds = len(list(evals_result["train"].values())[0]) + expected_rounds = MODEL_PARAMS.get("n_estimators", 1000) + print(f"\n[早停信息]") + print(f" 配置的最大轮数: {expected_rounds}") + print(f" 实际训练轮数: {actual_rounds}") -for idx, metric in enumerate(ndcg_metrics[:4]): # 最多显示4个NDCG指标 - ax = axes[idx] - train_metric = evals_result["train"][metric] - val_metric = evals_result["val"][metric] - iterations = range(1, len(train_metric) + 1) + best_iter = model.get_best_iteration() + if best_iter is not None and best_iter < actual_rounds: + print(f" 早停状态: 已触发(最佳迭代: {best_iter})") + else: + print(f" 早停状态: 未触发(达到最大轮数)") - ax.plot( - iterations, train_metric, label=f"Train {metric}", linewidth=2, color="blue" - ) - ax.plot(iterations, val_metric, label=f"Val {metric}", linewidth=2, color="red") - ax.set_xlabel("Iteration", fontsize=10) - ax.set_ylabel(metric.upper(), fontsize=10) - ax.set_title( - f"Training and Validation {metric.upper()}", fontsize=12, fontweight="bold" - ) - ax.legend(fontsize=9) - ax.grid(True, alpha=0.3) + # 显示各 NDCG 指标的最终值 + print(f"\n最终 NDCG 指标:") + for metric in ndcg_metrics: + train_ndcg = evals_result["train"][metric][-1] + val_ndcg = evals_result["val"][metric][-1] + print(f" {metric}: 训练集={train_ndcg:.4f}, 验证集={val_ndcg:.4f}") - # 标记最佳验证指标点 - best_iter = val_metric.index(max(val_metric)) - best_metric = max(val_metric) - ax.axvline(x=best_iter + 1, color="green", linestyle="--", alpha=0.7) - ax.scatter([best_iter + 1], [best_metric], color="green", s=80, zorder=5) - ax.annotate( - f"Best: {best_metric:.4f}", - xy=(best_iter + 1, best_metric), - xytext=(best_iter + 1 + len(iterations) * 0.05, best_metric), - fontsize=8, - arrowprops=dict(arrowstyle="->", color="green", alpha=0.7), - ) + # 使用封装好的方法绘制所有指标 + print("\n[绘图] 使用 LightGBM 原生接口绘制训练曲线...") + fig = model.plot_all_metrics(metrics=ndcg_metrics[:4], figsize=(14, 10)) + plt.show() -plt.tight_layout() -plt.show() - -print(f"\n[指标分析]") -print(f" 各NDCG指标在验证集上的最佳值:") -for metric in ndcg_metrics: - val_metric_list = evals_result["val"][metric] - best_iter = val_metric_list.index(max(val_metric_list)) - best_val = max(val_metric_list) - print(f" {metric}: {best_val:.4f} (迭代 {best_iter + 1})") -print(f"\n[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!") + print(f"\n[指标分析]") + print(f" 各NDCG指标在验证集上的最佳值:") + for metric in ndcg_metrics: + val_metric_list = evals_result["val"][metric] + best_iter_metric = val_metric_list.index(max(val_metric_list)) + best_val = max(val_metric_list) + print(f" {metric}: {best_val:.4f} (迭代 {best_iter_metric + 1})") + print(f"\n[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!") # %% md # ### 4.6 模型评估 # %% diff --git a/src/experiment/regression.py b/src/experiment/regression.py index 8b4e780..39ca6ab 100644 --- a/src/experiment/regression.py +++ b/src/experiment/regression.py @@ -18,6 +18,7 @@ from src.training import ( Trainer, Winsorizer, NullFiller, + check_data_quality, ) from src.training.config import TrainingConfig @@ -25,13 +26,13 @@ from src.training.config import TrainingConfig # %% md # ## 2. 定义辅助函数 # %% -def create_factors_with_metadata( +def register_factors( engine: FactorEngine, selected_factors: List[str], factor_definitions: dict, label_factor: dict, ) -> List[str]: - """注册因子(SELECTED_FACTORS 从 metadata 查询,FACTOR_DEFINITIONS 用表达式注册)""" + """注册因子(selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册)""" print("=" * 80) print("注册因子") print("=" * 80) @@ -285,9 +286,6 @@ MODEL_PARAMS = { "random_state": 42, } -# 数据处理器配置(新 API:需要传入 feature_cols) -# 注意:processor 现在需要显式指定要处理的特征列 - # 股票池筛选函数 # 使用新的 StockPoolManager API:传入自定义筛选函数和所需列/因子 @@ -355,7 +353,7 @@ engine = FactorEngine(metadata_path="data/factors.jsonl") # 2. 使用 metadata 定义因子 print("\n[2] 定义因子(从 metadata 注册)") -feature_cols = create_factors_with_metadata( +feature_cols = register_factors( engine, SELECTED_FACTORS, FACTOR_DEFINITIONS, LABEL_FACTOR ) target_col = LABEL_NAME @@ -380,7 +378,7 @@ print(f"[配置] 目标变量: {target_col}") # 5. 创建模型 model = LightGBMModel(params=MODEL_PARAMS) -# 6. 创建数据处理器(新 API:需要传入 feature_cols) +# 6. 创建数据处理器(使用函数返回的完整特征列表) processors = [ NullFiller(feature_cols=feature_cols, strategy="mean"), Winsorizer(feature_cols=feature_cols, lower=0.01, upper=0.99), @@ -482,8 +480,26 @@ else: test_data = filtered_data print(" 未配置划分器,全部作为训练集") # %% -# 步骤 3: 训练集数据处理 -print("\n[步骤 3/6] 训练集数据处理") +# 步骤 3: 数据质量检查(必须在预处理之前) +print("\n[步骤 3/7] 数据质量检查") +print("-" * 60) +print(" [说明] 此检查在 fillna 等处理之前执行,用于发现数据问题") + +print("\n 检查训练集...") +check_data_quality(train_data, feature_cols, raise_on_error=True) + +if "val_data" in locals() and val_data is not None: + print("\n 检查验证集...") + check_data_quality(val_data, feature_cols, raise_on_error=True) + +print("\n 检查测试集...") +check_data_quality(test_data, feature_cols, raise_on_error=True) + +print(" [成功] 数据质量检查通过,未发现异常") + +# %% +# 步骤 4: 训练集数据处理 +print("\n[步骤 4/7] 训练集数据处理") print("-" * 60) fitted_processors = [] if processors: @@ -510,7 +526,7 @@ for col in feature_cols[:5]: # 只显示前5个特征的缺失值 print(f" {col}: {null_count} ({null_count / len(train_data) * 100:.2f}%)") # %% # 步骤 4: 训练模型 -print("\n[步骤 4/6] 训练模型") +print("\n[步骤 5/7] 训练模型") print("-" * 60) print(f" 模型类型: LightGBM") print(f" 训练样本数: {len(train_data)}") @@ -532,7 +548,7 @@ model.fit(X_train, y_train) print(" 训练完成!") # %% # 步骤 5: 测试集数据处理 -print("\n[步骤 5/6] 测试集数据处理") +print("\n[步骤 6/7] 测试集数据处理") print("-" * 60) if processors and test_data is not train_data: for i, processor in enumerate(fitted_processors, 1): @@ -548,7 +564,7 @@ else: print(" 跳过测试集处理") # %% # 步骤 6: 生成预测 -print("\n[步骤 6/6] 生成预测") +print("\n[步骤 7/7] 生成预测") print("-" * 60) X_test = test_data.select(feature_cols) print(f" 测试样本数: {len(X_test)}") diff --git a/src/training/__init__.py b/src/training/__init__.py index 843c546..0b78554 100644 --- a/src/training/__init__.py +++ b/src/training/__init__.py @@ -37,6 +37,9 @@ from src.training.components.filters import BaseFilter, STFilter # 训练核心 from src.training.core import StockPoolManager, Trainer +# 工具函数 +from src.training.utils import check_data_quality + # 配置 from src.training.config import TrainingConfig @@ -67,6 +70,8 @@ __all__ = [ # 训练核心 "StockPoolManager", "Trainer", + # 工具函数 + "check_data_quality", # 配置 "TrainingConfig", ] diff --git a/src/training/components/models/lightgbm_lambdarank.py b/src/training/components/models/lightgbm_lambdarank.py index 9c7878e..01fa5d1 100644 --- a/src/training/components/models/lightgbm_lambdarank.py +++ b/src/training/components/models/lightgbm_lambdarank.py @@ -98,6 +98,7 @@ class LightGBMLambdaRankModel(BaseModel): self.model = None self.feature_names_: Optional[list] = None + self.evals_result_: Optional[dict] = None # 存储训练评估结果 def fit( self, @@ -155,8 +156,9 @@ class LightGBMLambdaRankModel(BaseModel): # 创建训练数据集 train_data = lgb.Dataset(X_np, label=y_np, group=group) - # 准备验证集 + # 准备验证集和验证集名称 valid_sets = [train_data] + valid_names = ["train"] if eval_set is not None: X_val, y_val, group_val = eval_set X_val_np = X_val.to_numpy() if isinstance(X_val, pl.DataFrame) else X_val @@ -169,15 +171,23 @@ class LightGBMLambdaRankModel(BaseModel): val_data = lgb.Dataset(X_val_np, label=y_val_np, group=group_val) valid_sets.append(val_data) + valid_names.append("val") + + # 初始化评估结果存储 + self.evals_result_ = {} # 训练 - callbacks = [lgb.early_stopping(stopping_rounds=self.early_stopping_rounds)] + callbacks = [ + lgb.early_stopping(stopping_rounds=self.early_stopping_rounds), + lgb.record_evaluation(self.evals_result_), + ] self.model = lgb.train( self.params, train_data, num_boost_round=self.n_estimators, valid_sets=valid_sets, + valid_names=valid_names, callbacks=callbacks, ) @@ -201,6 +211,185 @@ class LightGBMLambdaRankModel(BaseModel): X_np = X.to_numpy() return self.model.predict(X_np) + def get_evals_result(self) -> Optional[dict]: + """获取训练评估结果 + + Returns: + 评估结果字典,包含训练集和验证集的指标历史 + 格式: {'train': {'metric_name': [...]}, 'val': {'metric_name': [...]}} + 如果模型尚未训练,返回 None + """ + return self.evals_result_ + + def plot_metric( + self, + metric: Optional[str] = None, + figsize: tuple = (10, 6), + title: Optional[str] = None, + ax=None, + ): + """绘制训练指标曲线 + + 使用 LightGBM 原生的 plot_metric 接口绘制训练曲线。 + + Args: + metric: 要绘制的指标名称,如 'ndcg@5'、'ndcg@10' 等。 + 如果为 None,则自动选择第一个可用的 NDCG 指标。 + figsize: 图大小,默认 (10, 6) + title: 图表标题,如果为 None 则自动生成 + ax: matplotlib Axes 对象,如果为 None 则创建新图 + + 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()") + + if self.evals_result_ is None or not self.evals_result_: + raise RuntimeError("没有可用的评估结果") + + 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()] + if ndcg_metrics: + metric = ndcg_metrics[0] + elif available_metrics: + metric = available_metrics[0] + 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) + + # 使用 LightGBM 原生接口绘制 + lgb.plot_metric(self.evals_result_, metric=metric, ax=ax) + + # 设置标题 + if title is None: + title = f"Training Metric ({metric.upper()}) over Iterations" + ax.set_title(title, fontsize=12, fontweight="bold") + + return ax + + def plot_all_metrics( + self, + metrics: Optional[list] = None, + figsize: tuple = (14, 10), + max_cols: int = 2, + ): + """绘制所有训练指标曲线 + + 在一个图表中绘制多个指标的训练曲线。 + + Args: + metrics: 要绘制的指标列表,如果为 None 则绘制所有 NDCG 指标 + figsize: 图大小,默认 (14, 10) + max_cols: 每行最多显示的子图数,默认 2 + + Returns: + matplotlib Figure 对象 + + Raises: + RuntimeError: 模型尚未训练 + """ + if self.model is None: + raise RuntimeError("模型尚未训练,请先调用 fit()") + + if self.evals_result_ is None or not self.evals_result_: + raise RuntimeError("没有可用的评估结果") + + import lightgbm as lgb + import matplotlib.pyplot as plt + + 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] + + 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 + + fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize) + if n_metrics == 1: + axes = [axes] + else: + axes = ( + axes.flatten() + if n_rows > 1 + else [axes] + if n_cols == 1 + else axes.flatten() + ) + + for idx, metric in enumerate(metrics): + if idx < len(axes): + ax = axes[idx] + if metric in available_metrics: + self.plot_metric(metric=metric, ax=ax) + ax.set_title(f"{metric.upper()}", fontsize=11, fontweight="bold") + else: + ax.text( + 0.5, + 0.5, + f"Metric '{metric}' not found", + ha="center", + va="center", + 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]: """返回特征重要性 diff --git a/src/training/utils.py b/src/training/utils.py new file mode 100644 index 0000000..dca4c8f --- /dev/null +++ b/src/training/utils.py @@ -0,0 +1,171 @@ +"""训练模块工具函数 + +提供数据质量检查、验证等通用工具函数。 +""" + +from typing import Dict, List, Optional, Union + +import polars as pl + + +def check_data_quality( + df: pl.DataFrame, + feature_cols: List[str], + date_col: str = "trade_date", + check_all_null: bool = True, + check_all_zero: bool = True, + check_all_nan: bool = True, + raise_on_error: bool = True, +) -> Dict[str, List[Dict[str, Union[str, int]]]]: + """检查数据质量,检测某天某个因子是否全部为空、0或NaN + + 此检查必须在 fillna、标准化等处理之前执行,否则错误会被掩盖。 + + Args: + df: 待检查的数据 + feature_cols: 特征列名列表 + date_col: 日期列名,默认 "trade_date" + check_all_null: 是否检查全空,默认 True + check_all_zero: 是否检查全零,默认 True + check_all_nan: 是否检查全NaN,默认 True + raise_on_error: 发现问题时是否抛出异常,默认 True + + Returns: + 检查结果字典,格式: + { + "all_null": [{"date": "20240101", "factor": "factor_name", "count": 500}], + "all_zero": [{"date": "20240101", "factor": "factor_name", "count": 500}], + "all_nan": [{"date": "20240101", "factor": "factor_name", "count": 500}], + } + + Raises: + ValueError: 当发现质量问题且 raise_on_error=True 时 + + Examples: + >>> import polars as pl + >>> df = pl.DataFrame({ + ... "trade_date": ["20240101", "20240101", "20240102"], + ... "ts_code": ["000001.SZ", "000002.SZ", "000001.SZ"], + ... "factor1": [1.0, 2.0, None], + ... "factor2": [0.0, 0.0, 1.0], + ... }) + >>> result = check_data_quality(df, ["factor1", "factor2"]) + """ + issues = { + "all_null": [], + "all_zero": [], + "all_nan": [], + } + + # 获取实际存在的特征列 + existing_cols = [col for col in feature_cols if col in df.columns] + if not existing_cols: + return issues + + # 按日期分组检查 + for date in df[date_col].unique(): + day_data = df.filter(pl.col(date_col) == date) + day_str = str(date) + + for col in existing_cols: + if not day_data[col].dtype.is_numeric(): + continue + + col_data = day_data[col] + non_null_count = col_data.count() + + if non_null_count == 0: + # 该日期该因子完全没有有效数据 + if check_all_null: + issues["all_null"].append( + { + "date": day_str, + "factor": col, + "count": len(day_data), + } + ) + continue + + # 检查是否全为零 + if check_all_zero: + abs_sum = col_data.abs().sum() + if abs_sum == 0: + issues["all_zero"].append( + { + "date": day_str, + "factor": col, + "count": non_null_count, + } + ) + + # 检查是否全为NaN(在Polars中表现为null) + if check_all_nan: + null_count = col_data.null_count() + if null_count == non_null_count: + issues["all_nan"].append( + { + "date": day_str, + "factor": col, + "count": non_null_count, + } + ) + + # 生成报告 + total_issues = sum(len(v) for v in issues.values()) + + if total_issues > 0: + report_lines = ["\n" + "=" * 80, "数据质量检查报告", "=" * 80] + + if issues["all_null"]: + report_lines.append(f"\n[严重] 发现 {len(issues['all_null'])} 个全空因子:") + report_lines.append( + " (某天的某个因子所有值都是 null,可能是数据缺失或计算错误)" + ) + for issue in issues["all_null"][:10]: # 最多显示10条 + msg = f" - 日期 {issue['date']}: {issue['factor']} (样本数: {issue['count']})" + report_lines.append(msg) + if len(issues["all_null"]) > 10: + report_lines.append(f" ... 还有 {len(issues['all_null']) - 10} 个") + + if issues["all_zero"]: + report_lines.append(f"\n[警告] 发现 {len(issues['all_zero'])} 个全零因子:") + report_lines.append( + " (某天的某个因子所有值都是 0,可能是计算错误或数据源问题)" + ) + for issue in issues["all_zero"][:10]: + msg = f" - 日期 {issue['date']}: {issue['factor']} (样本数: {issue['count']})" + report_lines.append(msg) + if len(issues["all_zero"]) > 10: + report_lines.append(f" ... 还有 {len(issues['all_zero']) - 10} 个") + + if issues["all_nan"]: + report_lines.append(f"\n[警告] 发现 {len(issues['all_nan'])} 个全NaN因子:") + report_lines.append(" (某天的某个因子所有值都是 NaN,可能是数值计算错误)") + for issue in issues["all_nan"][:10]: + msg = f" - 日期 {issue['date']}: {issue['factor']} (样本数: {issue['count']})" + report_lines.append(msg) + if len(issues["all_nan"]) > 10: + report_lines.append(f" ... 还有 {len(issues['all_nan']) - 10} 个") + + report_lines.extend( + [ + "\n" + "-" * 80, + "建议处理方式:", + " 1. 检查因子定义和数据源,确认计算逻辑是否正确", + " 2. 如果是预期内的缺失(如新股无历史数据),考虑调整因子计算窗口", + " 3. 如果是数据同步问题,重新同步相关数据", + " 4. 可以使用 filter 排除问题日期或因子", + "=" * 80, + ] + ) + + report = "\n".join(report_lines) + print(report) + + if raise_on_error: + raise ValueError( + f"数据质量检查失败: 发现 {total_issues} 个问题," + f"详见上方报告。如需忽略,请设置 raise_on_error=False" + ) + + return issues