{ "cells": [ { "cell_type": "code", "execution_count": 53, "id": "79a7758178bafdd3", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:46:06.987506Z", "start_time": "2025-04-03T12:46:06.259551Z" }, "jupyter": { "source_hidden": true } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The autoreload extension is already loaded. To reload it, use:\n", " %reload_ext autoreload\n", "e:\\PyProject\\NewStock\\main\\train\n" ] } ], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", "\n", "import gc\n", "import os\n", "import sys\n", "sys.path.append('../../')\n", "print(os.getcwd())\n", "import pandas as pd\n", "from main.factor.factor import get_rolling_factor, get_simple_factor\n", "from main.utils.factor import read_industry_data\n", "from main.utils.factor_processor import calculate_score\n", "from main.utils.utils import read_and_merge_h5_data, merge_with_industry_data\n", "\n", "import warnings\n", "\n", "warnings.filterwarnings(\"ignore\")" ] }, { "cell_type": "code", "execution_count": 54, "id": "a79cafb06a7e0e43", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:00.212859Z", "start_time": "2025-04-03T12:46:06.998047Z" }, "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "daily data\n", "daily basic\n", "inner merge on ['ts_code', 'trade_date']\n", "stk limit\n", "left merge on ['ts_code', 'trade_date']\n", "money flow\n", "left merge on ['ts_code', 'trade_date']\n", "cyq perf\n", "left merge on ['ts_code', 'trade_date']\n", "\n", "RangeIndex: 8595791 entries, 0 to 8595790\n", "Data columns (total 32 columns):\n", " # Column Dtype \n", "--- ------ ----- \n", " 0 ts_code object \n", " 1 trade_date datetime64[ns]\n", " 2 open float64 \n", " 3 close float64 \n", " 4 high float64 \n", " 5 low float64 \n", " 6 vol float64 \n", " 7 pct_chg float64 \n", " 8 turnover_rate float64 \n", " 9 pe_ttm float64 \n", " 10 circ_mv float64 \n", " 11 total_mv float64 \n", " 12 volume_ratio float64 \n", " 13 is_st bool \n", " 14 up_limit float64 \n", " 15 down_limit float64 \n", " 16 buy_sm_vol float64 \n", " 17 sell_sm_vol float64 \n", " 18 buy_lg_vol float64 \n", " 19 sell_lg_vol float64 \n", " 20 buy_elg_vol float64 \n", " 21 sell_elg_vol float64 \n", " 22 net_mf_vol float64 \n", " 23 his_low float64 \n", " 24 his_high float64 \n", " 25 cost_5pct float64 \n", " 26 cost_15pct float64 \n", " 27 cost_50pct float64 \n", " 28 cost_85pct float64 \n", " 29 cost_95pct float64 \n", " 30 weight_avg float64 \n", " 31 winner_rate float64 \n", "dtypes: bool(1), datetime64[ns](1), float64(29), object(1)\n", "memory usage: 2.0+ GB\n", "None\n" ] } ], "source": [ "from main.utils.utils import read_and_merge_h5_data\n", "\n", "print('daily data')\n", "df = read_and_merge_h5_data('../../data/daily_data.h5', key='daily_data',\n", " columns=['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'pct_chg'],\n", " df=None)\n", "\n", "print('daily basic')\n", "df = read_and_merge_h5_data('../../data/daily_basic.h5', key='daily_basic',\n", " columns=['ts_code', 'trade_date', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv', 'volume_ratio',\n", " 'is_st'], df=df, join='inner')\n", "\n", "print('stk limit')\n", "df = read_and_merge_h5_data('../../data/stk_limit.h5', key='stk_limit',\n", " columns=['ts_code', 'trade_date', 'pre_close', 'up_limit', 'down_limit'],\n", " df=df)\n", "print('money flow')\n", "df = read_and_merge_h5_data('../../data/money_flow.h5', key='money_flow',\n", " columns=['ts_code', 'trade_date', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol',\n", " 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol'],\n", " df=df)\n", "print('cyq perf')\n", "df = read_and_merge_h5_data('../../data/cyq_perf.h5', key='cyq_perf',\n", " columns=['ts_code', 'trade_date', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct',\n", " 'cost_50pct',\n", " 'cost_85pct', 'cost_95pct', 'weight_avg', 'winner_rate'],\n", " df=df)\n", "print(df.info())" ] }, { "cell_type": "code", "execution_count": 55, "id": "cac01788dac10678", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:10.527104Z", "start_time": "2025-04-03T12:47:00.488715Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "industry\n" ] }, { "ename": "KeyboardInterrupt", "evalue": "", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[55], line 45\u001b[0m\n\u001b[0;32m 41\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n\u001b[0;32m 44\u001b[0m \u001b[38;5;66;03m# 使用示例\u001b[39;00m\n\u001b[1;32m---> 45\u001b[0m df \u001b[38;5;241m=\u001b[39m merge_with_industry_data(df, industry_df)\n", "Cell \u001b[1;32mIn[55], line 19\u001b[0m, in \u001b[0;36mmerge_with_industry_data\u001b[1;34m(df, industry_df)\u001b[0m\n\u001b[0;32m 16\u001b[0m df_sorted \u001b[38;5;241m=\u001b[39m df\u001b[38;5;241m.\u001b[39msort_values([\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtrade_date\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mts_code\u001b[39m\u001b[38;5;124m'\u001b[39m])\n\u001b[0;32m 18\u001b[0m \u001b[38;5;66;03m# 使用 merge_asof 进行向后合并\u001b[39;00m\n\u001b[1;32m---> 19\u001b[0m merged \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mmerge_asof(\n\u001b[0;32m 20\u001b[0m df_sorted,\n\u001b[0;32m 21\u001b[0m industry_df_sorted,\n\u001b[0;32m 22\u001b[0m by\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mts_code\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;66;03m# 按 ts_code 分组\u001b[39;00m\n\u001b[0;32m 23\u001b[0m left_on\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtrade_date\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[0;32m 24\u001b[0m right_on\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124min_date\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[0;32m 25\u001b[0m direction\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mbackward\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[0;32m 26\u001b[0m )\n\u001b[0;32m 28\u001b[0m \u001b[38;5;66;03m# 获取每个 ts_code 的最早 in_date 记录\u001b[39;00m\n\u001b[0;32m 29\u001b[0m min_in_date_per_ts \u001b[38;5;241m=\u001b[39m (industry_df_sorted\n\u001b[0;32m 30\u001b[0m \u001b[38;5;241m.\u001b[39mgroupby(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mts_code\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m 31\u001b[0m \u001b[38;5;241m.\u001b[39mfirst()\n\u001b[0;32m 32\u001b[0m \u001b[38;5;241m.\u001b[39mreset_index()[[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mts_code\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124ml2_code\u001b[39m\u001b[38;5;124m'\u001b[39m]])\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\reshape\\merge.py:708\u001b[0m, in \u001b[0;36mmerge_asof\u001b[1;34m(left, right, on, left_on, right_on, left_index, right_index, by, left_by, right_by, suffixes, tolerance, allow_exact_matches, direction)\u001b[0m\n\u001b[0;32m 456\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 457\u001b[0m \u001b[38;5;124;03mPerform a merge by key distance.\u001b[39;00m\n\u001b[0;32m 458\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 689\u001b[0m \u001b[38;5;124;03m4 2016-05-25 13:30:00.048 AAPL 98.00 100 NaN NaN\u001b[39;00m\n\u001b[0;32m 690\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 691\u001b[0m op \u001b[38;5;241m=\u001b[39m _AsOfMerge(\n\u001b[0;32m 692\u001b[0m left,\n\u001b[0;32m 693\u001b[0m right,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 706\u001b[0m direction\u001b[38;5;241m=\u001b[39mdirection,\n\u001b[0;32m 707\u001b[0m )\n\u001b[1;32m--> 708\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m op\u001b[38;5;241m.\u001b[39mget_result()\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\reshape\\merge.py:1946\u001b[0m, in \u001b[0;36m_OrderedMerge.get_result\u001b[1;34m(self, copy)\u001b[0m\n\u001b[0;32m 1943\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 1944\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfill_method must be \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mffill\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m or None\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m-> 1946\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_reindex_and_concat(\n\u001b[0;32m 1947\u001b[0m join_index, left_join_indexer, right_join_indexer, copy\u001b[38;5;241m=\u001b[39mcopy\n\u001b[0;32m 1948\u001b[0m )\n\u001b[0;32m 1949\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_maybe_add_join_keys(result, left_indexer, right_indexer)\n\u001b[0;32m 1951\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\reshape\\merge.py:879\u001b[0m, in \u001b[0;36m_MergeOperation._reindex_and_concat\u001b[1;34m(self, join_index, left_indexer, right_indexer, copy)\u001b[0m\n\u001b[0;32m 877\u001b[0m left\u001b[38;5;241m.\u001b[39mcolumns \u001b[38;5;241m=\u001b[39m llabels\n\u001b[0;32m 878\u001b[0m right\u001b[38;5;241m.\u001b[39mcolumns \u001b[38;5;241m=\u001b[39m rlabels\n\u001b[1;32m--> 879\u001b[0m result \u001b[38;5;241m=\u001b[39m concat([left, right], axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m, copy\u001b[38;5;241m=\u001b[39mcopy)\n\u001b[0;32m 880\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\reshape\\concat.py:395\u001b[0m, in \u001b[0;36mconcat\u001b[1;34m(objs, axis, join, ignore_index, keys, levels, names, verify_integrity, sort, copy)\u001b[0m\n\u001b[0;32m 380\u001b[0m copy \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[0;32m 382\u001b[0m op \u001b[38;5;241m=\u001b[39m _Concatenator(\n\u001b[0;32m 383\u001b[0m objs,\n\u001b[0;32m 384\u001b[0m axis\u001b[38;5;241m=\u001b[39maxis,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 392\u001b[0m sort\u001b[38;5;241m=\u001b[39msort,\n\u001b[0;32m 393\u001b[0m )\n\u001b[1;32m--> 395\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m op\u001b[38;5;241m.\u001b[39mget_result()\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\reshape\\concat.py:684\u001b[0m, in \u001b[0;36m_Concatenator.get_result\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 680\u001b[0m indexers[ax] \u001b[38;5;241m=\u001b[39m obj_labels\u001b[38;5;241m.\u001b[39mget_indexer(new_labels)\n\u001b[0;32m 682\u001b[0m mgrs_indexers\u001b[38;5;241m.\u001b[39mappend((obj\u001b[38;5;241m.\u001b[39m_mgr, indexers))\n\u001b[1;32m--> 684\u001b[0m new_data \u001b[38;5;241m=\u001b[39m concatenate_managers(\n\u001b[0;32m 685\u001b[0m mgrs_indexers, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnew_axes, concat_axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbm_axis, copy\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcopy\n\u001b[0;32m 686\u001b[0m )\n\u001b[0;32m 687\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcopy \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m using_copy_on_write():\n\u001b[0;32m 688\u001b[0m new_data\u001b[38;5;241m.\u001b[39m_consolidate_inplace()\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\internals\\concat.py:131\u001b[0m, in \u001b[0;36mconcatenate_managers\u001b[1;34m(mgrs_indexers, axes, concat_axis, copy)\u001b[0m\n\u001b[0;32m 124\u001b[0m \u001b[38;5;66;03m# Assertions disabled for performance\u001b[39;00m\n\u001b[0;32m 125\u001b[0m \u001b[38;5;66;03m# for tup in mgrs_indexers:\u001b[39;00m\n\u001b[0;32m 126\u001b[0m \u001b[38;5;66;03m# # caller is responsible for ensuring this\u001b[39;00m\n\u001b[0;32m 127\u001b[0m \u001b[38;5;66;03m# indexers = tup[1]\u001b[39;00m\n\u001b[0;32m 128\u001b[0m \u001b[38;5;66;03m# assert concat_axis not in indexers\u001b[39;00m\n\u001b[0;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m concat_axis \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m--> 131\u001b[0m mgrs \u001b[38;5;241m=\u001b[39m _maybe_reindex_columns_na_proxy(axes, mgrs_indexers, needs_copy)\n\u001b[0;32m 132\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m mgrs[\u001b[38;5;241m0\u001b[39m]\u001b[38;5;241m.\u001b[39mconcat_horizontal(mgrs, axes)\n\u001b[0;32m 134\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(mgrs_indexers) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m mgrs_indexers[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m0\u001b[39m]\u001b[38;5;241m.\u001b[39mnblocks \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m:\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\internals\\concat.py:230\u001b[0m, in \u001b[0;36m_maybe_reindex_columns_na_proxy\u001b[1;34m(axes, mgrs_indexers, needs_copy)\u001b[0m\n\u001b[0;32m 220\u001b[0m mgr \u001b[38;5;241m=\u001b[39m mgr\u001b[38;5;241m.\u001b[39mreindex_indexer(\n\u001b[0;32m 221\u001b[0m axes[i],\n\u001b[0;32m 222\u001b[0m indexers[i],\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 227\u001b[0m use_na_proxy\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m, \u001b[38;5;66;03m# only relevant for i==0\u001b[39;00m\n\u001b[0;32m 228\u001b[0m )\n\u001b[0;32m 229\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m needs_copy \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m indexers:\n\u001b[1;32m--> 230\u001b[0m mgr \u001b[38;5;241m=\u001b[39m mgr\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[0;32m 232\u001b[0m new_mgrs\u001b[38;5;241m.\u001b[39mappend(mgr)\n\u001b[0;32m 233\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m new_mgrs\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\internals\\managers.py:604\u001b[0m, in \u001b[0;36mBaseBlockManager.copy\u001b[1;34m(self, deep)\u001b[0m\n\u001b[0;32m 601\u001b[0m res\u001b[38;5;241m.\u001b[39m_blklocs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_blklocs\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[0;32m 603\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m deep:\n\u001b[1;32m--> 604\u001b[0m res\u001b[38;5;241m.\u001b[39m_consolidate_inplace()\n\u001b[0;32m 605\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m res\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\internals\\managers.py:1791\u001b[0m, in \u001b[0;36mBlockManager._consolidate_inplace\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 1789\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_consolidated \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m 1790\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_known_consolidated \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[1;32m-> 1791\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_rebuild_blknos_and_blklocs()\n", "File \u001b[1;32minternals.pyx:755\u001b[0m, in \u001b[0;36mpandas._libs.internals.BlockManager._rebuild_blknos_and_blklocs\u001b[1;34m()\u001b[0m\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\internals\\base.py:84\u001b[0m, in \u001b[0;36mDataManager.shape\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 82\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[0;32m 83\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mshape\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Shape:\n\u001b[1;32m---> 84\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mlen\u001b[39m(ax) \u001b[38;5;28;01mfor\u001b[39;00m ax \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39maxes)\n", "File \u001b[1;32me:\\Python\\anaconda\\envs\\new_trader\\Lib\\site-packages\\pandas\\core\\internals\\base.py:84\u001b[0m, in \u001b[0;36m\u001b[1;34m(.0)\u001b[0m\n\u001b[0;32m 82\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[0;32m 83\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mshape\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Shape:\n\u001b[1;32m---> 84\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mlen\u001b[39m(ax) \u001b[38;5;28;01mfor\u001b[39;00m ax \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39maxes)\n", "\u001b[1;31mKeyboardInterrupt\u001b[0m: " ] } ], "source": [ "print('industry')\n", "industry_df = read_and_merge_h5_data('../../data/industry_data.h5', key='industry_data',\n", " columns=['ts_code', 'l2_code', 'in_date'],\n", " df=None, on=['ts_code'], join='left')\n", "\n", "\n", "def merge_with_industry_data(df, industry_df):\n", " # 确保日期字段是 datetime 类型\n", " df['trade_date'] = pd.to_datetime(df['trade_date'])\n", " industry_df['in_date'] = pd.to_datetime(industry_df['in_date'])\n", "\n", " # 对 industry_df 按 ts_code 和 in_date 排序\n", " industry_df_sorted = industry_df.sort_values(['in_date', 'ts_code'])\n", "\n", " # 对原始 df 按 ts_code 和 trade_date 排序\n", " df_sorted = df.sort_values(['trade_date', 'ts_code'])\n", "\n", " # 使用 merge_asof 进行向后合并\n", " merged = pd.merge_asof(\n", " df_sorted,\n", " industry_df_sorted,\n", " by='ts_code', # 按 ts_code 分组\n", " left_on='trade_date',\n", " right_on='in_date',\n", " direction='backward'\n", " )\n", "\n", " # 获取每个 ts_code 的最早 in_date 记录\n", " min_in_date_per_ts = (industry_df_sorted\n", " .groupby('ts_code')\n", " .first()\n", " .reset_index()[['ts_code', 'l2_code']])\n", "\n", " # 填充未匹配到的记录(trade_date 早于所有 in_date 的情况)\n", " merged['l2_code'] = merged['l2_code'].fillna(\n", " merged['ts_code'].map(min_in_date_per_ts.set_index('ts_code')['l2_code'])\n", " )\n", "\n", " # 保留需要的列并重置索引\n", " result = merged.reset_index(drop=True)\n", " return result\n", "\n", "\n", "# 使用示例\n", "df = merge_with_industry_data(df, industry_df)\n", "# print(mdf[mdf['ts_code'] == '600751.SH'][['ts_code', 'trade_date', 'l2_code']])" ] }, { "cell_type": "code", "execution_count": null, "id": "c4e9e1d31da6dba6", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:10.719252Z", "start_time": "2025-04-03T12:47:10.541247Z" }, "jupyter": { "source_hidden": true } }, "outputs": [], "source": [ "def calculate_indicators(df):\n", " \"\"\"\n", " 计算四个指标:当日涨跌幅、5日移动平均、RSI、MACD。\n", " \"\"\"\n", " df = df.sort_values('trade_date')\n", " df['daily_return'] = (df['close'] - df['pre_close']) / df['pre_close'] * 100\n", " # df['5_day_ma'] = df['close'].rolling(window=5).mean()\n", " delta = df['close'].diff()\n", " gain = delta.where(delta > 0, 0)\n", " loss = -delta.where(delta < 0, 0)\n", " avg_gain = gain.rolling(window=14).mean()\n", " avg_loss = loss.rolling(window=14).mean()\n", " rs = avg_gain / avg_loss\n", " df['RSI'] = 100 - (100 / (1 + rs))\n", "\n", " # 计算MACD\n", " ema12 = df['close'].ewm(span=12, adjust=False).mean()\n", " ema26 = df['close'].ewm(span=26, adjust=False).mean()\n", " df['MACD'] = ema12 - ema26\n", " df['Signal_line'] = df['MACD'].ewm(span=9, adjust=False).mean()\n", " df['MACD_hist'] = df['MACD'] - df['Signal_line']\n", "\n", " # 4. 情绪因子1:市场上涨比例(Up Ratio)\n", " df['up_ratio'] = df['daily_return'].apply(lambda x: 1 if x > 0 else 0)\n", " df['up_ratio_20d'] = df['up_ratio'].rolling(window=20).mean() # 过去20天上涨比例\n", "\n", " # 5. 情绪因子2:成交量变化率(Volume Change Rate)\n", " df['volume_mean'] = df['vol'].rolling(window=20).mean() # 过去20天的平均成交量\n", " df['volume_change_rate'] = (df['vol'] - df['volume_mean']) / df['volume_mean'] * 100 # 成交量变化率\n", "\n", " # 6. 情绪因子3:波动率(Volatility)\n", " df['volatility'] = df['daily_return'].rolling(window=20).std() # 过去20天的日收益率标准差\n", "\n", " # 7. 情绪因子4:成交额变化率(Amount Change Rate)\n", " df['amount_mean'] = df['amount'].rolling(window=20).mean() # 过去20天的平均成交额\n", " df['amount_change_rate'] = (df['amount'] - df['amount_mean']) / df['amount_mean'] * 100 # 成交额变化率\n", "\n", " return df\n", "\n", "\n", "def generate_index_indicators(h5_filename):\n", " df = pd.read_hdf(h5_filename, key='index_data')\n", " df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')\n", " df = df.sort_values('trade_date')\n", "\n", " # 计算每个ts_code的相关指标\n", " df_indicators = []\n", " for ts_code in df['ts_code'].unique():\n", " df_index = df[df['ts_code'] == ts_code].copy()\n", " df_index = calculate_indicators(df_index)\n", " df_indicators.append(df_index)\n", "\n", " # 合并所有指数的结果\n", " df_all_indicators = pd.concat(df_indicators, ignore_index=True)\n", "\n", " # 保留trade_date列,并将同一天的数据按ts_code合并成一行\n", " df_final = df_all_indicators.pivot_table(\n", " index='trade_date',\n", " columns='ts_code',\n", " values=['daily_return', 'RSI', 'MACD', 'Signal_line',\n", " 'MACD_hist', 'up_ratio_20d', 'volume_change_rate', 'volatility',\n", " 'amount_change_rate', 'amount_mean'],\n", " aggfunc='last'\n", " )\n", "\n", " df_final.columns = [f\"{col[1]}_{col[0]}\" for col in df_final.columns]\n", " df_final = df_final.reset_index()\n", "\n", " return df_final\n", "\n", "\n", "# 使用函数\n", "h5_filename = '../../data/index_data.h5'\n", "index_data = generate_index_indicators(h5_filename)\n", "index_data = index_data.dropna()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "a735bc02ceb4d872", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:10.821169Z", "start_time": "2025-04-03T12:47:10.751831Z" } }, "outputs": [], "source": [ "import talib\n", "import numpy as np" ] }, { "cell_type": "code", "execution_count": null, "id": "53f86ddc0677a6d7", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:15.944254Z", "start_time": "2025-04-03T12:47:10.826179Z" }, "jupyter": { "source_hidden": true }, "scrolled": true }, "outputs": [], "source": [ "from main.utils.factor import get_act_factor\n", "\n", "\n", "def read_industry_data(h5_filename):\n", " # 读取 H5 文件中所有的行业数据\n", " industry_data = pd.read_hdf(h5_filename, key='sw_daily', columns=[\n", " 'ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'pe', 'pb', 'vol'\n", " ]) # 假设 H5 文件的键是 'industry_data'\n", " industry_data = industry_data.sort_values(by=['ts_code', 'trade_date'])\n", " industry_data = industry_data.reindex()\n", " industry_data['trade_date'] = pd.to_datetime(industry_data['trade_date'], format='%Y%m%d')\n", "\n", " grouped = industry_data.groupby('ts_code', group_keys=False)\n", " industry_data['obv'] = grouped.apply(\n", " lambda x: pd.Series(talib.OBV(x['close'].values, x['vol'].values), index=x.index)\n", " )\n", " industry_data['return_5'] = grouped['close'].apply(lambda x: x / x.shift(5) - 1)\n", " industry_data['return_20'] = grouped['close'].apply(lambda x: x / x.shift(20) - 1)\n", "\n", " industry_data = get_act_factor(industry_data, cat=False)\n", " industry_data = industry_data.sort_values(by=['trade_date', 'ts_code'])\n", "\n", " # # 计算每天每个 ts_code 的因子和当天所有 ts_code 的中位数的偏差\n", " # factor_columns = ['obv', 'return_5', 'return_20', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4'] # 因子列\n", " # \n", " # for factor in factor_columns:\n", " # if factor in industry_data.columns:\n", " # # 计算每天每个 ts_code 的因子值与当天所有 ts_code 的中位数的偏差\n", " # industry_data[f'{factor}_deviation'] = industry_data.groupby('trade_date')[factor].transform(\n", " # lambda x: x - x.mean())\n", "\n", " industry_data['return_5_percentile'] = industry_data.groupby('trade_date')['return_5'].transform(\n", " lambda x: x.rank(pct=True))\n", " industry_data['return_20_percentile'] = industry_data.groupby('trade_date')['return_20'].transform(\n", " lambda x: x.rank(pct=True))\n", " industry_data = industry_data.drop(columns=['open', 'close', 'high', 'low', 'pe', 'pb', 'vol'])\n", "\n", " industry_data = industry_data.rename(\n", " columns={col: f'industry_{col}' for col in industry_data.columns if col not in ['ts_code', 'trade_date']})\n", "\n", " industry_data = industry_data.rename(columns={'ts_code': 'cat_l2_code'})\n", " return industry_data\n", "\n", "\n", "industry_df = read_industry_data('../../data/sw_daily.h5')\n" ] }, { "cell_type": "code", "execution_count": null, "id": "dbe2fd8021b9417f", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:15.969344Z", "start_time": "2025-04-03T12:47:15.963327Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['ts_code', 'open', 'close', 'high', 'low', 'circ_mv', 'total_mv', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg', 'in_date']\n" ] } ], "source": [ "origin_columns = df.columns.tolist()\n", "origin_columns = [col for col in origin_columns if\n", " col not in ['turnover_rate', 'pe_ttm', 'volume_ratio', 'vol', 'pct_chg', 'l2_code', 'winner_rate']]\n", "origin_columns = [col for col in origin_columns if col not in index_data.columns]\n", "origin_columns = [col for col in origin_columns if 'cyq' not in col]\n", "print(origin_columns)" ] }, { "cell_type": "code", "execution_count": null, "id": "85c3e3d0235ffffa", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:16.089879Z", "start_time": "2025-04-03T12:47:15.990101Z" } }, "outputs": [], "source": [ "fina_indicator_df = read_and_merge_h5_data('../../data/fina_indicator.h5', key='fina_indicator',\n", " columns=['ts_code', 'ann_date', 'undist_profit_ps', 'ocfps', 'bps'],\n", " df=None)\n", "cashflow_df = read_and_merge_h5_data('../../data/cashflow.h5', key='cashflow',\n", " columns=['ts_code', 'ann_date', 'n_cashflow_act'],\n", " df=None)\n", "balancesheet_df = read_and_merge_h5_data('../../data/balancesheet.h5', key='balancesheet',\n", " columns=['ts_code', 'ann_date', 'money_cap', 'total_liab'],\n", " df=None)" ] }, { "cell_type": "code", "execution_count": null, "id": "92d84ce15a562ec6", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:01.612695Z", "start_time": "2025-04-03T12:47:16.121802Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "使用 'ann_date' 作为财务数据生效日期。\n", "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n", "开始计算因子: AR, BR (原地修改)...\n", "因子 AR, BR 计算成功。\n", "因子 AR, BR 计算流程结束。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n", "计算 BBI...\n", "Index(['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol',\n", " 'pct_chg', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv',\n", " 'volume_ratio', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol',\n", " 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol',\n", " 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct',\n", " 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg',\n", " 'winner_rate', 'l2_code', 'undist_profit_ps', 'ocfps', 'AR', 'BR',\n", " 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor', 'book_to_price_ratio',\n", " 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor',\n", " 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol',\n", " 'flow_divergence_diff', 'flow_divergence_ratio', 'total_buy_vol',\n", " 'lg_elg_buy_prop', 'flow_struct_buy_change',\n", " 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel',\n", " 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy',\n", " 'cost_support_15pct_change', 'cat_winner_price_zone',\n", " 'flow_chip_consistency', 'profit_taking_vs_absorb', '_is_positive',\n", " '_is_negative', 'cat_is_positive', '_pos_returns', '_neg_returns',\n", " '_pos_returns_sq', '_neg_returns_sq', 'upside_vol', 'downside_vol',\n", " 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate',\n", " 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike',\n", " 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike',\n", " 'vol_std_5', 'atr_14', 'atr_6', 'obv'],\n", " dtype='object')\n", "Calculating lg_flow_mom_corr_20_60...\n", "Finished lg_flow_mom_corr_20_60.\n", "Calculating lg_flow_accel...\n", "Finished lg_flow_accel.\n", "Calculating profit_pressure...\n", "Finished profit_pressure.\n", "Calculating underwater_resistance...\n", "Finished underwater_resistance.\n", "Calculating cost_conc_std_20...\n", "Finished cost_conc_std_20.\n", "Calculating profit_decay_20...\n", "Finished profit_decay_20.\n", "Calculating vol_amp_loss_20...\n", "Finished vol_amp_loss_20.\n", "Calculating vol_drop_profit_cnt_5...\n", "Finished vol_drop_profit_cnt_5.\n", "Calculating lg_flow_vol_interact_20...\n", "Finished lg_flow_vol_interact_20.\n", "Calculating cost_break_confirm_cnt_5...\n", "Finished cost_break_confirm_cnt_5.\n", "Calculating atr_norm_channel_pos_14...\n", "Finished atr_norm_channel_pos_14.\n", "Calculating turnover_diff_skew_20...\n", "Finished turnover_diff_skew_20.\n", "Calculating lg_sm_flow_diverge_20...\n", "Finished lg_sm_flow_diverge_20.\n", "Calculating pullback_strong_20_20...\n", "Finished pullback_strong_20_20.\n", "Calculating vol_wgt_hist_pos_20...\n", "Finished vol_wgt_hist_pos_20.\n", "Calculating vol_adj_roc_20...\n", "Finished vol_adj_roc_20.\n", "\n", "Index: 4502216 entries, 0 to 4502215\n", "Columns: 157 entries, ts_code to vol_adj_roc_20\n", "dtypes: bool(10), datetime64[ns](1), float64(141), int32(3), object(2)\n", "memory usage: 5.0+ GB\n", "None\n", "['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'pct_chg', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv', 'volume_ratio', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg', 'winner_rate', 'cat_l2_code', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'flow_divergence_diff', 'flow_divergence_ratio', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'price_cost_divergence', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20']\n" ] } ], "source": [ "\n", "import numpy as np\n", "from main.factor.factor import *\n", "\n", "def filter_data(df):\n", " # df = df.groupby('trade_date').apply(lambda x: x.nlargest(1000, 'act_factor1'))\n", " df = df[~df['is_st']]\n", " df = df[~df['ts_code'].str.endswith('BJ')]\n", " df = df[~df['ts_code'].str.startswith('30')]\n", " df = df[~df['ts_code'].str.startswith('68')]\n", " df = df[~df['ts_code'].str.startswith('8')]\n", " df = df[df['trade_date'] >= '2019-01-01']\n", " if 'in_date' in df.columns:\n", " df = df.drop(columns=['in_date'])\n", " df = df.reset_index(drop=True)\n", " return df\n", "\n", "gc.collect()\n", "\n", "df = filter_data(df)\n", "df = df.sort_values(by=['ts_code', 'trade_date'])\n", "df = add_financial_factor(df, fina_indicator_df, factor_value_col='undist_profit_ps')\n", "df = add_financial_factor(df, fina_indicator_df, factor_value_col='ocfps')\n", "calculate_arbr(df, N=26)\n", "df['log_circ_mv'] = np.log(df['circ_mv'])\n", "df = calculate_cashflow_to_ev_factor(df, cashflow_df, balancesheet_df)\n", "df = caculate_book_to_price_ratio(df, fina_indicator_df)\n", "df = turnover_rate_n(df, n=5)\n", "df = variance_n(df, n=20)\n", "df = bbi_ratio_factor(df)\n", "df, _ = get_rolling_factor(df)\n", "df, _ = get_simple_factor(df)\n", "\n", "lg_flow_mom_corr(df, N=20, M=60)\n", "lg_flow_accel(df)\n", "profit_pressure(df)\n", "underwater_resistance(df)\n", "cost_conc_std(df, N=20)\n", "profit_decay(df, N=20)\n", "vol_amp_loss(df, N=20)\n", "vol_drop_profit_cnt(df, N=20, M=5)\n", "lg_flow_vol_interact(df, N=20)\n", "cost_break_confirm_cnt(df, M=5)\n", "atr_norm_channel_pos(df, N=14)\n", "turnover_diff_skew(df, N=20)\n", "lg_sm_flow_diverge(df, N=20)\n", "pullback_strong(df, N=20, M=20)\n", "vol_wgt_hist_pos(df, N=20)\n", "vol_adj_roc(df, N=20)\n", "\n", "calculate_complex_factor(df)\n", "cs_rank_net_lg_flow_val(df)\n", "cs_rank_flow_divergence(df)\n", "cs_rank_industry_adj_lg_flow(df) # Needs cat_l2_code\n", "cs_rank_elg_buy_ratio(df)\n", "cs_rank_rel_profit_margin(df)\n", "cs_rank_cost_breadth(df)\n", "cs_rank_dist_to_upper_cost(df)\n", "cs_rank_winner_rate(df)\n", "cs_rank_intraday_range(df)\n", "cs_rank_close_pos_in_range(df)\n", "cs_rank_opening_gap(df) # Needs pre_close\n", "cs_rank_pos_in_hist_range(df) # Needs his_low, his_high\n", "cs_rank_vol_x_profit_margin(df)\n", "cs_rank_lg_flow_price_concordance(df)\n", "cs_rank_turnover_per_winner(df)\n", "cs_rank_ind_cap_neutral_pe(df) # Placeholder - needs external libraries\n", "cs_rank_volume_ratio(df) # Needs volume_ratio\n", "cs_rank_elg_buy_sell_sm_ratio(df)\n", "cs_rank_cost_dist_vol_ratio(df) # Needs volume_ratio\n", "cs_rank_size(df) # Needs circ_mv\n", "\n", "df = df.rename(columns={'l1_code': 'cat_l1_code'})\n", "df = df.rename(columns={'l2_code': 'cat_l2_code'})\n", "\n", "# df = df.merge(index_data, on='trade_date', how='left')\n", "\n", "print(df.info())\n", "print(df.columns.tolist())" ] }, { "cell_type": "code", "execution_count": null, "id": "b87b938028afa206", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:03.658725Z", "start_time": "2025-04-03T13:08:02.469611Z" } }, "outputs": [], "source": [ "from scipy.stats import ks_2samp, wasserstein_distance\n", "\n", "\n", "def remove_shifted_features(train_data, test_data, feature_columns, ks_threshold=0.05, wasserstein_threshold=0.1,\n", " importance_threshold=0.05):\n", " dropped_features = []\n", "\n", " # **统计数据漂移**\n", " numeric_columns = train_data.select_dtypes(include=['float64', 'int64']).columns\n", " numeric_columns = [col for col in numeric_columns if col in feature_columns]\n", " for feature in numeric_columns:\n", " ks_stat, p_value = ks_2samp(train_data[feature], test_data[feature])\n", " wasserstein_dist = wasserstein_distance(train_data[feature], test_data[feature])\n", "\n", " if p_value < ks_threshold or wasserstein_dist > wasserstein_threshold:\n", " dropped_features.append(feature)\n", "\n", " print(f\"检测到 {len(dropped_features)} 个可能漂移的特征: {dropped_features}\")\n", "\n", " # **应用阈值进行最终筛选**\n", " filtered_features = [f for f in feature_columns if f not in dropped_features]\n", "\n", " return filtered_features, dropped_features\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "f4f16d63ad18d1bc", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:03.670700Z", "start_time": "2025-04-03T13:08:03.665739Z" } }, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import statsmodels.api as sm # 用于中性化回归\n", "from tqdm import tqdm # 可选,用于显示进度条\n", "\n", "# --- 常量 ---\n", "epsilon = 1e-10 # 防止除零\n", "\n", "# --- 1. 中位数去极值 (MAD) ---\n", "\n", "def cs_mad_filter(df: pd.DataFrame,\n", " features: list,\n", " k: float = 3.0,\n", " scale_factor: float = 1.4826):\n", " \"\"\"\n", " 对指定特征列进行截面 MAD 去极值处理 (原地修改)。\n", "\n", " 方法: 对每日截面数据,计算 median 和 MAD,\n", " 将超出 [median - k * scale * MAD, median + k * scale * MAD] 范围的值\n", " 替换为边界值 (Winsorization)。\n", " scale_factor=1.4826 使得 MAD 约等于正态分布的标准差。\n", "\n", " Args:\n", " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。\n", " features (list): 需要处理的特征列名列表。\n", " k (float): MAD 的倍数,用于确定边界。默认为 3.0。\n", " scale_factor (float): MAD 的缩放因子。默认为 1.4826。\n", "\n", " WARNING: 此函数会原地修改输入的 DataFrame 'df'。\n", " \"\"\"\n", " print(f\"开始截面 MAD 去极值处理 (k={k})...\")\n", " if not all(col in df.columns for col in features):\n", " missing = [col for col in features if col not in df.columns]\n", " print(f\"错误: DataFrame 中缺少以下特征列: {missing}。跳过去极值处理。\")\n", " return\n", "\n", " grouped = df.groupby('trade_date')\n", "\n", " for col in tqdm(features, desc=\"MAD Filtering\"):\n", " try:\n", " # 计算截面中位数\n", " median = grouped[col].transform('median')\n", " # 计算截面 MAD (Median Absolute Deviation from Median)\n", " mad = (df[col] - median).abs().groupby(df['trade_date']).transform('median')\n", "\n", " # 计算上下边界\n", " lower_bound = median - k * scale_factor * mad\n", " upper_bound = median + k * scale_factor * mad\n", "\n", " # 原地应用 clip\n", " df[col] = np.clip(df[col], lower_bound, upper_bound)\n", "\n", " except KeyError:\n", " print(f\"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的 MAD 处理。\")\n", " except Exception as e:\n", " print(f\"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的 MAD 处理。\")\n", "\n", " print(\"截面 MAD 去极值处理完成。\")\n", "\n", "\n", "# --- 2. 行业市值中性化 ---\n", "\n", "def cs_neutralize_industry_cap(df: pd.DataFrame,\n", " features: list,\n", " industry_col: str = 'cat_l2_code',\n", " market_cap_col: str = 'circ_mv'):\n", " \"\"\"\n", " 对指定特征列进行截面行业和对数市值中性化 (原地修改)。\n", " 使用 OLS 回归: feature ~ 1 + log(market_cap) + C(industry)\n", " 将回归残差写回原特征列。\n", "\n", " Args:\n", " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date', features 列,\n", " industry_col, market_cap_col。\n", " features (list): 需要处理的特征列名列表。\n", " industry_col (str): 行业分类列名。\n", " market_cap_col (str): 流通市值列名。\n", "\n", " WARNING: 此函数会原地修改输入的 DataFrame 'df' 的 features 列。\n", " 计算量较大,可能耗时较长。\n", " 需要安装 statsmodels 库 (pip install statsmodels)。\n", " \"\"\"\n", " print(\"开始截面行业市值中性化...\")\n", " required_cols = features + ['trade_date', industry_col, market_cap_col]\n", " if not all(col in df.columns for col in required_cols):\n", " missing = [col for col in required_cols if col not in df.columns]\n", " print(f\"错误: DataFrame 中缺少必需列: {missing}。无法进行中性化。\")\n", " return\n", "\n", " # 预处理:计算 log 市值,处理 industry code 可能的 NaN\n", " log_cap_col = '_log_market_cap'\n", " df[log_cap_col] = np.log1p(df[market_cap_col]) # log1p 处理 0 值\n", " # df[industry_col] = df[industry_col].cat.add_categories('UnknownIndustry')\n", " # df[industry_col] = df[industry_col].fillna('UnknownIndustry') # 填充行业 NaN\n", " # df[industry_col] = df[industry_col].astype('category') # 转为类别,ols 会自动处理\n", "\n", " dates = df['trade_date'].unique()\n", " all_residuals = [] # 用于收集所有日期的残差\n", "\n", " for date in tqdm(dates, desc=\"Neutralizing\"):\n", " daily_data = df.loc[df['trade_date'] == date, features + [log_cap_col, industry_col]].copy() # 使用 .loc 获取副本\n", "\n", " # 准备自变量 X (常数项 + log市值 + 行业哑变量)\n", " X = daily_data[[log_cap_col]]\n", " X = sm.add_constant(X, prepend=True) # 添加常数项\n", " # 创建行业哑变量 (drop_first=True 避免共线性)\n", " industry_dummies = pd.get_dummies(daily_data[industry_col], prefix=industry_col, drop_first=True)\n", " industry_dummies = industry_dummies.astype(int)\n", " X = pd.concat([X, industry_dummies], axis=1)\n", "\n", " daily_residuals = daily_data[[col for col in features]].copy() # 创建用于存储残差的df\n", "\n", " for col in features:\n", " Y = daily_data[col]\n", "\n", " # 处理 NaN 值,确保 X 和 Y 在相同位置有有效值\n", " valid_mask = Y.notna() & X.notna().all(axis=1)\n", " if valid_mask.sum() < (X.shape[1] + 1): # 数据点不足以估计模型\n", " print(f\"警告: 日期 {date}, 特征 {col} 有效数据不足 ({valid_mask.sum()}个),无法中性化,填充 NaN。\")\n", " daily_residuals[col] = np.nan\n", " continue\n", "\n", " Y_valid = Y[valid_mask]\n", " X_valid = X[valid_mask]\n", "\n", " # 执行 OLS 回归\n", " try:\n", " model = sm.OLS(Y_valid.to_numpy(), X_valid.to_numpy())\n", " results = model.fit()\n", " # 将残差填回对应位置\n", " daily_residuals.loc[valid_mask, col] = results.resid\n", " daily_residuals.loc[~valid_mask, col] = np.nan # 原本无效的位置填充 NaN\n", " except Exception as e:\n", " print(f\"警告: 日期 {date}, 特征 {col} 回归失败: {e},填充 NaN。\")\n", " daily_residuals[col] = np.nan\n", " break\n", "\n", " all_residuals.append(daily_residuals)\n", "\n", " # 合并所有日期的残差结果\n", " if all_residuals:\n", " residuals_df = pd.concat(all_residuals)\n", " # 将残差结果更新回原始 df (原地修改)\n", " # 使用 update 比 merge 更适合基于索引的原地更新\n", " # 确保 residuals_df 的索引与 df 中对应部分一致\n", " df.update(residuals_df)\n", " else:\n", " print(\"没有有效的残差结果可以合并。\")\n", "\n", "\n", " # 清理临时列\n", " df.drop(columns=[log_cap_col], inplace=True)\n", " print(\"截面行业市值中性化完成。\")\n", "\n", "\n", "# --- 3. Z-Score 标准化 ---\n", "\n", "def cs_zscore_standardize(df: pd.DataFrame, features: list, epsilon: float = 1e-10):\n", " \"\"\"\n", " 对指定特征列进行截面 Z-Score 标准化 (原地修改)。\n", " 方法: Z = (value - cross_sectional_mean) / (cross_sectional_std + epsilon)\n", "\n", " Args:\n", " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。\n", " features (list): 需要处理的特征列名列表。\n", " epsilon (float): 防止除以零的小常数。\n", "\n", " WARNING: 此函数会原地修改输入的 DataFrame 'df'。\n", " \"\"\"\n", " print(\"开始截面 Z-Score 标准化...\")\n", " if not all(col in df.columns for col in features):\n", " missing = [col for col in features if col not in df.columns]\n", " print(f\"错误: DataFrame 中缺少以下特征列: {missing}。跳过标准化处理。\")\n", " return\n", "\n", " grouped = df.groupby('trade_date')\n", "\n", " for col in tqdm(features, desc=\"Standardizing\"):\n", " try:\n", " # 使用 transform 计算截面均值和标准差\n", " mean = grouped[col].transform('mean')\n", " std = grouped[col].transform('std')\n", "\n", " # 计算 Z-Score 并原地赋值\n", " df[col] = (df[col] - mean) / (std + epsilon)\n", "\n", " except KeyError:\n", " print(f\"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的标准化处理。\")\n", " except Exception as e:\n", " print(f\"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的标准化处理。\")\n", "\n", " print(\"截面 Z-Score 标准化完成。\")\n", "\n", "def fill_nan_with_daily_median(df: pd.DataFrame, feature_columns: list[str]) -> pd.DataFrame:\n", " \"\"\"\n", " 对指定特征列进行每日截面中位数填充缺失值 (NaN)。\n", "\n", " 参数:\n", " df (pd.DataFrame): 包含多日数据的DataFrame,需要包含 'trade_date' 和 feature_columns 中的列。\n", " feature_columns (list[str]): 需要进行缺失值填充的特征列名称列表。\n", "\n", " 返回:\n", " pd.DataFrame: 包含缺失值填充后特征列的DataFrame。在输入DataFrame的副本上操作。\n", " \"\"\"\n", " processed_df = df.copy() # 在副本上操作,保留原始数据\n", "\n", " # 确保 trade_date 是 datetime 类型以便正确分组\n", " processed_df['trade_date'] = pd.to_datetime(processed_df['trade_date'])\n", "\n", " def _fill_daily_nan(group):\n", " # group 是某一个交易日的 DataFrame\n", "\n", " # 遍历指定的特征列\n", " for feature_col in feature_columns:\n", " # 检查列是否存在于当前分组中\n", " if feature_col in group.columns:\n", " # 计算当日该特征的中位数\n", " median_val = group[feature_col].median()\n", "\n", " # 使用当日中位数填充该特征列的 NaN 值\n", " # inplace=True 会直接修改 group DataFrame\n", " group[feature_col].fillna(median_val, inplace=True)\n", " # else:\n", " # print(f\"Warning: Feature column '{feature_col}' not found in daily group for {group['trade_date'].iloc[0]}. Skipping.\")\n", "\n", " return group\n", "\n", " # 按交易日期分组,并应用每日填充函数\n", " # group_keys=False 避免将分组键添加到结果索引中\n", " filled_df = processed_df.groupby('trade_date', group_keys=False).apply(_fill_daily_nan)\n", "\n", " return filled_df" ] }, { "cell_type": "code", "execution_count": null, "id": "40e6b68a91b30c79", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:04.694262Z", "start_time": "2025-04-03T13:08:03.694904Z" } }, "outputs": [], "source": [ "import pandas as pd\n", "\n", "\n", "def remove_outliers_label_percentile(label: pd.Series, lower_percentile: float = 0.01, upper_percentile: float = 0.99,\n", " log=True):\n", " if not (0 <= lower_percentile < upper_percentile <= 1):\n", " raise ValueError(\"Percentile values must satisfy 0 <= lower_percentile < upper_percentile <= 1.\")\n", "\n", " # Calculate lower and upper bounds based on percentiles\n", " lower_bound = label.quantile(lower_percentile)\n", " upper_bound = label.quantile(upper_percentile)\n", "\n", " # Filter out values outside the bounds\n", " filtered_label = label[(label >= lower_bound) & (label <= upper_bound)]\n", "\n", " # Print the number of removed outliers\n", " if log:\n", " print(f\"Removed {len(label) - len(filtered_label)} outliers.\")\n", " return filtered_label\n", "\n", "\n", "def calculate_risk_adjusted_target(df, days=5):\n", " df = df.sort_values(by=['ts_code', 'trade_date'])\n", "\n", " df['future_close'] = df.groupby('ts_code')['close'].shift(-days)\n", " df['future_open'] = df.groupby('ts_code')['open'].shift(-1)\n", " df['future_return'] = (df['future_close'] - df['future_open']) / df['future_open']\n", "\n", " df['future_volatility'] = df.groupby('ts_code')['future_return'].rolling(days, min_periods=1).std().reset_index(\n", " level=0, drop=True)\n", " sharpe_ratio = df['future_return'] * df['future_volatility']\n", " sharpe_ratio.replace([np.inf, -np.inf], np.nan, inplace=True)\n", "\n", " return sharpe_ratio\n", "\n", "\n", "def calculate_score(df, days=5, lambda_param=1.0):\n", " def calculate_max_drawdown(prices):\n", " peak = prices.iloc[0] # 初始化峰值\n", " max_drawdown = 0 # 初始化最大回撤\n", "\n", " for price in prices:\n", " if price > peak:\n", " peak = price # 更新峰值\n", " else:\n", " drawdown = (peak - price) / peak # 计算当前回撤\n", " max_drawdown = max(max_drawdown, drawdown) # 更新最大回撤\n", "\n", " return max_drawdown\n", "\n", " def compute_stock_score(stock_df):\n", " stock_df = stock_df.sort_values(by=['trade_date'])\n", " future_return = stock_df['future_return']\n", " # 使用已有的 pct_chg 字段计算波动率\n", " volatility = stock_df['pct_chg'].rolling(days).std().shift(-days)\n", " max_drawdown = stock_df['close'].rolling(days).apply(calculate_max_drawdown, raw=False).shift(-days)\n", " score = future_return - lambda_param * max_drawdown\n", " return score\n", "\n", " # # 确保 DataFrame 按照股票代码和交易日期排序\n", " # df = df.sort_values(by=['ts_code', 'trade_date'])\n", "\n", " # 对每个股票分别计算 score\n", " df['score'] = df.groupby('ts_code').apply(compute_stock_score).reset_index(level=0, drop=True)\n", "\n", " return df['score']\n", "\n", "\n", "def remove_highly_correlated_features(df, feature_columns, threshold=0.9):\n", " numeric_features = df[feature_columns].select_dtypes(include=[np.number]).columns.tolist()\n", " if not numeric_features:\n", " raise ValueError(\"No numeric features found in the provided data.\")\n", "\n", " corr_matrix = df[numeric_features].corr().abs()\n", " upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))\n", " to_drop = [column for column in upper.columns if any(upper[column] > threshold)]\n", " remaining_features = [col for col in feature_columns if col not in to_drop\n", " or 'act' in col or 'af' in col]\n", " return remaining_features\n", "\n", "\n", "def cross_sectional_standardization(df, features):\n", " df_sorted = df.sort_values(by='trade_date') # 按时间排序\n", " df_standardized = df_sorted.copy()\n", "\n", " for date in df_sorted['trade_date'].unique():\n", " # 获取当前时间点的数据\n", " current_data = df_standardized[df_standardized['trade_date'] == date]\n", "\n", " # 只对指定特征进行标准化\n", " scaler = StandardScaler()\n", " standardized_values = scaler.fit_transform(current_data[features])\n", "\n", " # 将标准化结果重新赋值回去\n", " df_standardized.loc[df_standardized['trade_date'] == date, features] = standardized_values\n", "\n", " return df_standardized\n", "\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "\n", "def neutralize_manual_revised(df: pd.DataFrame, features: list, industry_col: str, mkt_cap_col: str) -> pd.DataFrame:\n", " \"\"\"\n", " 手动实现简单回归以提升速度,通过构建 Series 确保索引对齐。\n", " 对特征在行业内部进行市值中性化。\n", "\n", " Args:\n", " df: 输入的 DataFrame,包含特征、行业分类和市值列。\n", " features: 需要进行中性化的特征列名列表。\n", " industry_col: 行业分类列的列名。\n", " mkt_cap_col: 市值列的列名。\n", "\n", " Returns:\n", " 中性化后的 DataFrame。\n", " \"\"\"\n", "\n", " df[mkt_cap_col] = pd.to_numeric(df[mkt_cap_col], errors='coerce')\n", " df_cleaned = df.dropna(subset=[mkt_cap_col]).copy()\n", " df_cleaned = df_cleaned[df_cleaned[mkt_cap_col] > 0].copy()\n", "\n", " if df_cleaned.empty:\n", " print(\"警告: 清理市值异常值后 DataFrame 为空。\")\n", " return df # 返回原始或空df,取决于清理前的状态\n", "\n", " processed_df = df\n", "\n", " for col in features:\n", " if col not in df_cleaned.columns:\n", " print(f\"警告: 特征列 '{col}' 不存在于清理后的 DataFrame 中,已跳过。\")\n", " # 对于原始 df 中该列不存在的,在结果 df 中也保持原样(可能全是NaN)\n", " processed_df[col] = df[col] if col in df.columns else np.nan\n", " continue\n", "\n", " # 跳过对控制变量本身进行中性化\n", " if col == mkt_cap_col or col == industry_col:\n", " print(f\"警告: 特征列 '{col}' 是控制变量或内部使用的列,跳过中性化。\")\n", " # 在结果 df 中也保持原样\n", " processed_df[col] = df[col] if col in df.columns else np.nan\n", " continue\n", "\n", " residual_series = pd.Series(index=df_cleaned.index, dtype=float)\n", "\n", " # 在分组前处理特征列的 NaN,只对有因子值的行进行回归计算\n", " df_subset_factor = df_cleaned.dropna(subset=[col]).copy()\n", "\n", " if not df_subset_factor.empty:\n", " for industry, group in df_subset_factor.groupby(industry_col):\n", " x = group[mkt_cap_col] # 市值对数\n", " y = group[col] # 因子值\n", "\n", " # 确保有足够的数据点 (>1) 且市值对数有方差 (>0) 进行回归计算\n", " # 检查 np.var > 一个很小的正数,避免浮点数误差导致的零方差判断问题\n", " if len(group) > 1 and np.var(x) > 1e-9:\n", " try:\n", " beta = np.cov(y, x)[0, 1] / np.var(x)\n", " alpha = np.mean(y) - beta * np.mean(x)\n", "\n", " # 计算残差\n", " resid = y - (alpha + beta * x)\n", "\n", " # 将计算出的残差存储到 residual_series 中,通过索引自动对齐\n", " residual_series.loc[resid.index] = resid\n", "\n", " except Exception as e:\n", " # 捕获可能的计算异常,例如np.cov或np.var因为极端数据报错\n", " print(f\"警告: 在行业 {industry} 计算回归时发生错误: {e}。该组残差将设为原始值或 NaN。\")\n", " # 此时该组的残差会保持 residual_series 初始化时的 NaN 或后续处理\n", " # 也可以选择保留原始值:residual_series.loc[group.index] = group[col]\n", "\n", " else:\n", " residual_series.loc[group.index] = group[col] # 保留原始因子值\n", " processed_df.loc[residual_series.index, col] = residual_series\n", "\n", "\n", " else:\n", " processed_df[col] = np.nan # 或 df[col] if col in df.columns else np.nan\n", "\n", " return processed_df\n", "\n", "\n", "import gc\n", "\n", "gc.collect()\n", "\n", "\n", "def mad_filter(df, features, n=3):\n", " for col in features:\n", " median = df[col].median()\n", " mad = np.median(np.abs(df[col] - median))\n", " upper = median + n * mad\n", " lower = median - n * mad\n", " df[col] = np.clip(df[col], lower, upper) # 截断极值\n", " return df\n", "\n", "\n", "def percentile_filter(df, features, lower_percentile=0.01, upper_percentile=0.99):\n", " for col in features:\n", " # 按日期分组计算上下百分位数\n", " lower_bound = df.groupby('trade_date')[col].transform(\n", " lambda x: x.quantile(lower_percentile)\n", " )\n", " upper_bound = df.groupby('trade_date')[col].transform(\n", " lambda x: x.quantile(upper_percentile)\n", " )\n", " # 截断超出范围的值\n", " df[col] = np.clip(df[col], lower_bound, upper_bound)\n", " return df\n", "\n", "\n", "from scipy.stats import iqr\n", "\n", "\n", "def iqr_filter(df, features):\n", " for col in features:\n", " df[col] = df.groupby('trade_date')[col].transform(\n", " lambda x: (x - x.median()) / iqr(x) if iqr(x) != 0 else x\n", " )\n", " return df\n", "\n", "\n", "def quantile_filter(df, features, lower_quantile=0.01, upper_quantile=0.99, window=60):\n", " df = df.copy()\n", " for col in features:\n", " # 计算 rolling 统计量,需要按日期进行 groupby\n", " rolling_lower = df.groupby('trade_date')[col].transform(lambda x: x.rolling(window=min(len(x), window)).quantile(lower_quantile))\n", " rolling_upper = df.groupby('trade_date')[col].transform(lambda x: x.rolling(window=min(len(x), window)).quantile(upper_quantile))\n", "\n", " # 对数据进行裁剪\n", " df[col] = np.clip(df[col], rolling_lower, rolling_upper)\n", " \n", " return df\n", "\n", "def select_top_features_by_rankic(df: pd.DataFrame, feature_columns: list, n: int, target_column: str = 'future_return') -> list:\n", " \"\"\"\n", " 计算给定特征与目标列的 RankIC,并返回 RankIC 绝对值最高的 n 个特征。\n", "\n", " Args:\n", " df: 包含特征列和目标列的 Pandas DataFrame。\n", " feature_columns: 包含所有待评估特征列名的列表。\n", " n: 希望选取的 RankIC 绝对值最高的特征数量。\n", " target_column: 目标列的名称,用于计算 RankIC。默认为 'future_return'。\n", "\n", " Returns:\n", " 包含 RankIC 绝对值最高的 n 个特征列名的列表。\n", " \"\"\"\n", " numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns\n", " numeric_columns = [col for col in numeric_columns if col in feature_columns]\n", " if target_column not in df.columns:\n", " raise ValueError(f\"目标列 '{target_column}' 不存在于 DataFrame 中。\")\n", "\n", " rankic_scores = {}\n", " for feature in numeric_columns:\n", " if feature not in df.columns:\n", " print(f\"警告: 特征列 '{feature}' 不存在于 DataFrame 中,已跳过。\")\n", " continue\n", "\n", " # 计算特征与目标列的 RankIC (斯皮尔曼相关系数)\n", " # dropna() 是为了处理缺失值,确保相关性计算不失败\n", " valid_data = df[[feature, target_column]].dropna()\n", " if len(valid_data) > 1: # 确保有足够的数据点进行相关性计算\n", " # 计算斯皮尔曼相关性\n", " correlation = valid_data[feature].corr(valid_data[target_column], method='spearman')\n", " rankic_scores[feature] = abs(correlation) # 使用绝对值来衡量相关性强度\n", " else:\n", " rankic_scores[feature] = 0 # 数据不足,RankIC设为0或跳过\n", "\n", " # 将 RankIC 分数转换为 Series 便于排序\n", " rankic_series = pd.Series(rankic_scores)\n", "\n", " # 按 RankIC 绝对值降序排序,选取前 n 个特征\n", " # handle case where n might be larger than available features\n", " n_actual = min(n, len(rankic_series))\n", " top_features = rankic_series.sort_values(ascending=False).head(n_actual).index.tolist()\n", " top_features = [col for col in feature_columns if col in top_features or col not in numeric_columns]\n", " return top_features" ] }, { "cell_type": "code", "execution_count": null, "id": "47c12bb34062ae7a", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T14:57:50.841165Z", "start_time": "2025-04-03T14:49:25.889057Z" } }, "outputs": [], "source": [ "days = 2\n", "validation_days = 120\n", "\n", "import gc\n", "\n", "gc.collect()\n", "\n", "df = df.sort_values(by=['ts_code', 'trade_date'])\n", "df['future_return'] = df.groupby('ts_code', group_keys=False)['close'].apply(lambda x: x.shift(-days) / x - 1)\n", "# df['future_return'] = (df.groupby('ts_code')['close'].shift(-days) - df.groupby('ts_code')['open'].shift(-1)) / \\\n", "# df.groupby('ts_code')['open'].shift(-1)\n", "\n", "df['cat_up_limit'] = df['pct_chg'] > 5\n", "df['label'] = df.groupby('ts_code')['cat_up_limit'].rolling(window=5, min_periods=1).max().shift(-5).fillna(0).astype(int).reset_index(level=0, drop=True)\n", "\n", "filter_index = df['future_return'].between(df['future_return'].quantile(0.01), df['future_return'].quantile(0.99))\n", "\n", "# for col in [col for col in df.columns]:\n", "# train_data[col] = train_data[col].astype('str')\n", "# test_data[col] = test_data[col].astype('str')" ] }, { "cell_type": "code", "execution_count": null, "id": "b76ea08a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " ts_code trade_date log_circ_mv\n", "0 000001.SZ 2019-01-02 16.574219\n", "2738 000001.SZ 2019-01-03 16.583965\n", "5477 000001.SZ 2019-01-04 16.633371\n", "['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cat_up_limit', 'industry_obv', 'industry_return_5', 'industry_return_20', 'industry__ema_5', 'industry__ema_13', 'industry__ema_20', 'industry__ema_60', 'industry_act_factor1', 'industry_act_factor2', 'industry_act_factor3', 'industry_act_factor4', 'industry_act_factor5', 'industry_act_factor6', 'industry_rank_act_factor1', 'industry_rank_act_factor2', 'industry_rank_act_factor3', 'industry_return_5_percentile', 'industry_return_20_percentile', '000852.SH_MACD', '000905.SH_MACD', '399006.SZ_MACD', '000852.SH_MACD_hist', '000905.SH_MACD_hist', '399006.SZ_MACD_hist', '000852.SH_RSI', '000905.SH_RSI', '399006.SZ_RSI', '000852.SH_Signal_line', '000905.SH_Signal_line', '399006.SZ_Signal_line', '000852.SH_amount_change_rate', '000905.SH_amount_change_rate', '399006.SZ_amount_change_rate', '000852.SH_amount_mean', '000905.SH_amount_mean', '399006.SZ_amount_mean', '000852.SH_daily_return', '000905.SH_daily_return', '399006.SZ_daily_return', '000852.SH_up_ratio_20d', '000905.SH_up_ratio_20d', '399006.SZ_up_ratio_20d', '000852.SH_volatility', '000905.SH_volatility', '399006.SZ_volatility', '000852.SH_volume_change_rate', '000905.SH_volume_change_rate', '399006.SZ_volume_change_rate']\n", "去除极值\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 100%|██████████| 114/114 [00:22<00:00, 5.04it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 100%|██████████| 114/114 [00:18<00:00, 6.16it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 0it [00:00, ?it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 0it [00:00, ?it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "feature_columns: ['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cat_up_limit', 'industry_obv', 'industry_return_5', 'industry_return_20', 'industry__ema_5', 'industry__ema_13', 'industry__ema_20', 'industry__ema_60', 'industry_act_factor1', 'industry_act_factor2', 'industry_act_factor3', 'industry_act_factor4', 'industry_act_factor5', 'industry_act_factor6', 'industry_rank_act_factor1', 'industry_rank_act_factor2', 'industry_rank_act_factor3', 'industry_return_5_percentile', 'industry_return_20_percentile', '000852.SH_MACD', '000905.SH_MACD', '399006.SZ_MACD', '000852.SH_MACD_hist', '000905.SH_MACD_hist', '399006.SZ_MACD_hist', '000852.SH_RSI', '000905.SH_RSI', '399006.SZ_RSI', '000852.SH_Signal_line', '000905.SH_Signal_line', '399006.SZ_Signal_line', '000852.SH_amount_change_rate', '000905.SH_amount_change_rate', '399006.SZ_amount_change_rate', '000852.SH_amount_mean', '000905.SH_amount_mean', '399006.SZ_amount_mean', '000852.SH_daily_return', '000905.SH_daily_return', '399006.SZ_daily_return', '000852.SH_up_ratio_20d', '000905.SH_up_ratio_20d', '399006.SZ_up_ratio_20d', '000852.SH_volatility', '000905.SH_volatility', '399006.SZ_volatility', '000852.SH_volume_change_rate', '000905.SH_volume_change_rate', '399006.SZ_volume_change_rate']\n", "df最小日期: 2019-01-02\n", "df最大日期: 2025-05-06\n", "2058185\n", "train_data最小日期: 2020-01-02\n", "train_data最大日期: 2022-12-30\n", "1730349\n", "test_data最小日期: 2023-01-03\n", "test_data最大日期: 2025-05-06\n", " ts_code trade_date log_circ_mv\n", "0 000001.SZ 2019-01-02 16.574219\n", "2738 000001.SZ 2019-01-03 16.583965\n", "5477 000001.SZ 2019-01-04 16.633371\n" ] } ], "source": [ "train_data = df[filter_index & (df['trade_date'] <= '2023-01-01') & (df['trade_date'] >= '2020-01-01')]\n", "test_data = df[(df['trade_date'] >= '2023-01-01')]\n", "\n", "print(df[['ts_code', 'trade_date', 'log_circ_mv']].head(3))\n", "\n", "industry_df = industry_df.sort_values(by=['trade_date'])\n", "index_data = index_data.sort_values(by=['trade_date'])\n", "\n", "# train_data = train_data.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n", "# train_data = train_data.merge(index_data, on='trade_date', how='left')\n", "# test_data = test_data.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n", "# test_data = test_data.merge(index_data, on='trade_date', how='left')\n", "\n", "train_data, test_data = train_data.replace([np.inf, -np.inf], np.nan), test_data.replace([np.inf, -np.inf], np.nan)\n", "\n", "# feature_columns_new = feature_columns[:]\n", "# train_data, _ = create_deviation_within_dates(train_data, feature_columns)\n", "# test_data, _ = create_deviation_within_dates(test_data, feature_columns)\n", "\n", "# feature_columns = [\n", "# 'undist_profit_ps', \n", "# 'AR_BR', \n", "# 'pe_ttm',\n", "# 'alpha_22_improved', \n", "# 'alpha_003', \n", "# 'alpha_007', \n", "# 'alpha_013', \n", "# 'cat_up_limit', \n", "# 'cat_down_limit', \n", "# 'up_limit_count_10d', \n", "# 'down_limit_count_10d', \n", "# 'consecutive_up_limit', \n", "# 'vol_break', \n", "# 'weight_roc5', \n", "# 'price_cost_divergence', \n", "# 'smallcap_concentration', \n", "# 'cost_stability', \n", "# 'high_cost_break_days', \n", "# 'liquidity_risk', \n", "# 'turnover_std', \n", "# 'mv_volatility', \n", "# 'volume_growth', \n", "# 'mv_growth', \n", "# 'lg_flow_mom_corr_20_60', \n", "# 'lg_flow_accel', \n", "# 'profit_pressure', \n", "# 'underwater_resistance', \n", "# 'cost_conc_std_20', \n", "# 'profit_decay_20', \n", "# 'vol_amp_loss_20', \n", "# 'vol_drop_profit_cnt_5', \n", "# 'lg_flow_vol_interact_20', \n", "# 'cost_break_confirm_cnt_5', \n", "# 'atr_norm_channel_pos_14', \n", "# 'turnover_diff_skew_20', \n", "# 'lg_sm_flow_diverge_20', \n", "# 'pullback_strong_20_20', \n", "# 'vol_wgt_hist_pos_20', \n", "# 'vol_adj_roc_20',\n", "# 'cashflow_to_ev_factor',\n", "# 'ocfps',\n", "# 'book_to_price_ratio',\n", "# 'turnover_rate_mean_5',\n", "# 'variance_20',\n", "# 'bbi_ratio_factor'\n", "# ]\n", "# feature_columns = [col for col in feature_columns if col in train_data.columns]\n", "# feature_columns = [col for col in feature_columns if not col.startswith('_')]\n", "\n", "feature_columns = [col for col in test_data.head(10).merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left').merge(index_data, on='trade_date', how='left').columns]\n", "feature_columns = [col for col in feature_columns if col not in ['trade_date',\n", " 'ts_code',\n", " 'label']]\n", "feature_columns = [col for col in feature_columns if 'future' not in col]\n", "feature_columns = [col for col in feature_columns if 'label' not in col]\n", "feature_columns = [col for col in feature_columns if 'score' not in col]\n", "feature_columns = [col for col in feature_columns if 'gen' not in col]\n", "feature_columns = [col for col in feature_columns if 'is_st' not in col]\n", "feature_columns = [col for col in feature_columns if 'pe_ttm' not in col]\n", "# feature_columns = [col for col in feature_columns if 'volatility' not in col]\n", "feature_columns = [col for col in feature_columns if 'circ_mv' not in col]\n", "feature_columns = [col for col in feature_columns if 'code' not in col]\n", "feature_columns = [col for col in feature_columns if col not in origin_columns]\n", "feature_columns = [col for col in feature_columns if not col.startswith('_')]\n", "# feature_columns = [col for col in feature_columns if col not in ['ts_code', 'trade_date', 'vol_std_5', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_007', 'consecutive_up_limit', 'mv_volatility', 'volume_growth', 'mv_growth', 'arbr']]\n", "feature_columns = [col for col in feature_columns if col not in ['intraday_lg_flow_corr_20', \n", " 'cap_neutral_cost_metric', \n", " 'hurst_net_mf_vol_60', \n", " 'complex_factor_deap_1', \n", " 'lg_buy_consolidation_20',\n", " 'cs_rank_ind_cap_neutral_pe',\n", " 'cs_rank_opening_gap',\n", " 'cs_rank_ind_adj_lg_flow']]\n", "\n", "numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns\n", "numeric_columns = [col for col in numeric_columns if col in feature_columns]\n", "# feature_columns = select_top_features_by_rankic(df, numeric_columns, n=10)\n", "print(feature_columns)\n", "\n", "train_data = fill_nan_with_daily_median(train_data, feature_columns)\n", "test_data = fill_nan_with_daily_median(test_data, feature_columns)\n", "\n", "train_data = train_data.dropna(subset=[col for col in feature_columns if col in train_data.columns])\n", "train_data = train_data.dropna(subset=['label'])\n", "train_data = train_data.reset_index(drop=True)\n", "# print(test_data.tail())\n", "test_data = test_data.dropna(subset=[col for col in feature_columns if col in train_data.columns])\n", "# test_data = test_data.dropna(subset=['label'])\n", "test_data = test_data.reset_index(drop=True)\n", "\n", "transform_feature_columns = feature_columns\n", "transform_feature_columns = [col for col in transform_feature_columns if col in feature_columns and not col.startswith('cat') and col in train_data.columns]\n", "# transform_feature_columns.remove('undist_profit_ps')\n", "print('去除极值')\n", "cs_mad_filter(train_data, transform_feature_columns)\n", "# print('中性化')\n", "# cs_neutralize_industry_cap(train_data, transform_feature_columns)\n", "# print('标准化')\n", "# cs_zscore_standardize(train_data, transform_feature_columns)\n", "\n", "cs_mad_filter(test_data, transform_feature_columns)\n", "# cs_neutralize_industry_cap(test_data, transform_feature_columns)\n", "# cs_zscore_standardize(test_data, transform_feature_columns)\n", "\n", "mad_filter_feature_columns = [col for col in feature_columns if col not in transform_feature_columns and not col.startswith('cat') and col in train_data.columns]\n", "cs_mad_filter(train_data, mad_filter_feature_columns)\n", "cs_mad_filter(test_data, mad_filter_feature_columns)\n", "\n", "\n", "print(f'feature_columns: {feature_columns}')\n", "\n", "\n", "print(f\"df最小日期: {df['trade_date'].min().strftime('%Y-%m-%d')}\")\n", "print(f\"df最大日期: {df['trade_date'].max().strftime('%Y-%m-%d')}\")\n", "print(len(train_data))\n", "print(f\"train_data最小日期: {train_data['trade_date'].min().strftime('%Y-%m-%d')}\")\n", "print(f\"train_data最大日期: {train_data['trade_date'].max().strftime('%Y-%m-%d')}\")\n", "print(len(test_data))\n", "print(f\"test_data最小日期: {test_data['trade_date'].min().strftime('%Y-%m-%d')}\")\n", "print(f\"test_data最大日期: {test_data['trade_date'].max().strftime('%Y-%m-%d')}\")\n", "\n", "cat_columns = [col for col in feature_columns if col.startswith('cat')]\n", "for col in cat_columns:\n", " train_data[col] = train_data[col].astype('category')\n", " test_data[col] = test_data[col].astype('category')\n", "\n", "print(df[['ts_code', 'trade_date', 'log_circ_mv']].head(3))\n" ] }, { "cell_type": "code", "execution_count": null, "id": "e23d1759", "metadata": {}, "outputs": [], "source": [ "# feature_columns = [\n", "# 'undist_profit_ps', \n", "# 'AR_BR', \n", "# 'pe_ttm',\n", "# 'alpha_22_improved', \n", "# 'alpha_003', \n", "# 'alpha_007', \n", "# 'alpha_013', \n", "# 'cat_up_limit', \n", "# 'cat_down_limit', \n", "# 'up_limit_count_10d', \n", "# 'down_limit_count_10d', \n", "# 'consecutive_up_limit', \n", "# 'vol_break', \n", "# 'weight_roc5', \n", "# 'price_cost_divergence', \n", "# 'smallcap_concentration', \n", "# 'cost_stability', \n", "# 'high_cost_break_days', \n", "# 'liquidity_risk', \n", "# 'turnover_std', \n", "# 'mv_volatility', \n", "# 'volume_growth', \n", "# 'mv_growth', \n", "# 'lg_flow_mom_corr_20_60', \n", "# 'lg_flow_accel', \n", "# 'profit_pressure', \n", "# 'underwater_resistance', \n", "# 'cost_conc_std_20', \n", "# 'profit_decay_20', \n", "# 'vol_amp_loss_20', \n", "# 'vol_drop_profit_cnt_5', \n", "# 'lg_flow_vol_interact_20', \n", "# 'cost_break_confirm_cnt_5', \n", "# 'atr_norm_channel_pos_14', \n", "# 'turnover_diff_skew_20', \n", "# 'lg_sm_flow_diverge_20', \n", "# 'pullback_strong_20_20', \n", "# 'vol_wgt_hist_pos_20', \n", "# 'vol_adj_roc_20',\n", "# 'cashflow_to_ev_factor',\n", "# 'ocfps',\n", "# 'book_to_price_ratio',\n", "# 'turnover_rate_mean_5',\n", "# 'variance_20',\n", "# 'bbi_ratio_factor'\n", "# ]\n", "# feature_columns = [col for col in feature_columns if col in train_data.columns]" ] }, { "cell_type": "code", "execution_count": null, "id": "8f134d435f71e9e2", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T14:57:51.050696Z", "start_time": "2025-04-03T14:57:51.034030Z" }, "jupyter": { "source_hidden": true } }, "outputs": [], "source": [ "from sklearn.preprocessing import StandardScaler\n", "from sklearn.linear_model import LogisticRegression\n", "import matplotlib.pyplot as plt # 保持 matplotlib 导入,尽管LightGBM的绘图功能已移除\n", "from sklearn.decomposition import PCA\n", "import pandas as pd\n", "import numpy as np\n", "import datetime # 用于日期计算\n", "from catboost import CatBoostClassifier\n", "from catboost import Pool\n", "\n", "\n", "def train_model(train_data_df, feature_columns,\n", " print_info=True, # 调整参数名,更通用\n", " validation_days=180, use_pca=False, split_date=None,\n", " target_column='label'): # 增加目标列参数\n", "\n", " print('train data size: ', len(train_data_df))\n", "\n", " # 确保数据按时间排序\n", " train_data_df = train_data_df.sort_values(by='trade_date')\n", "\n", " # 识别数值型特征列\n", " numeric_feature_columns = train_data_df[feature_columns].select_dtypes(include=['float64', 'int64']).columns.tolist()\n", "\n", " # 去除标签为空的样本\n", " initial_len = len(train_data_df)\n", " train_data_df = train_data_df.dropna(subset=[target_column])\n", "\n", " if print_info:\n", " print(f'原始样本数: {initial_len}, 去除标签为空后样本数: {len(train_data_df)}')\n", "\n", " train_data_split = train_data_df\n", "\n", " # 提取特征和标签,只取数值型特征用于线性回归\n", " \n", " if split_date is None:\n", " all_dates = train_data_df['trade_date'].unique() # 获取所有唯一的 trade_date\n", " split_date = all_dates[-validation_days] # 划分点为倒数第 validation_days 天\n", " train_data_split = train_data_df[train_data_df['trade_date'] < split_date] # 训练集\n", " val_data_split = train_data_df[train_data_df['trade_date'] >= split_date] # 验证集\n", " \n", " X_train = train_data_split[feature_columns]\n", " y_train = train_data_split[target_column]\n", " \n", " X_val = val_data_split[feature_columns]\n", " y_val = val_data_split['label']\n", "\n", " # # 标准化数值特征 (使用 StandardScaler 对训练集fit并transform, 对验证集只transform)\n", " scaler = StandardScaler()\n", " # X_train = scaler.fit_transform(X_train)\n", "\n", " # 训练线性回归模型\n", " # model = LogisticRegression(random_state=42)\n", " \n", " # # 使用处理后的特征和样本权重进行训练\n", " # model.fit(X_train, y_train)\n", " \n", " \n", " cat_features = [i for i, col in enumerate(feature_columns) if col.startswith('cat')]\n", " print(f'cat_features: {cat_features}')\n", " # cat_features = []\n", "\n", " params = {\n", " 'loss_function': 'CrossEntropy', # 适用于二分类\n", " 'eval_metric': 'Precision', # 评估指标\n", " 'iterations': 500,\n", " 'learning_rate': 0.05,\n", " 'depth': 8, # 控制模型复杂度\n", " 'l2_leaf_reg': 3, # L2 正则化\n", " 'verbose': 500,\n", " 'early_stopping_rounds': 100,\n", " # 'one_hot_max_size': 50,\n", " # 'class_weights': [0.6, 1.2]\n", " 'task_type': 'GPU'\n", " }\n", "\n", " train_pool = Pool(data=X_train, label=y_train, cat_features=cat_features)\n", " val_pool = Pool(data=X_val, label=y_val, cat_features=cat_features)\n", "\n", "\n", " model = CatBoostClassifier(**params)\n", " model.fit(train_pool,\n", " eval_set=val_pool, plot=True, use_best_model=True)\n", "\n", "\n", " return model, scaler, None # 返回训练好的模型、scaler 和 pca 对象" ] }, { "cell_type": "code", "execution_count": null, "id": "c6eb5cd4-e714-420a-ac48-39af3e11ee81", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T15:03:18.426481Z", "start_time": "2025-04-03T15:02:19.926352Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "train data size: 36400\n", "原始样本数: 36400, 去除标签为空后样本数: 36400\n", "cat_features: [27, 30, 37, 39, 41, 80, 86, 87, 88, 100, 102, 125]\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a830a64e308a436b919bfc0dc59eb9a7", "version_major": 2, "version_minor": 0 }, "text/plain": [ "MetricVisualizer(layout=Layout(align_self='stretch', height='500px'))" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "0:\tlearn: 0.6527197\ttest: 0.3052632\tbest: 0.3052632 (0)\ttotal: 170ms\tremaining: 1m 25s\n", "bestTest = 0.4869565217\n", "bestIteration = 83\n", "Shrink model to first 84 iterations.\n" ] } ], "source": [ "\n", "gc.collect()\n", "\n", "use_pca = False\n", "# feature_contri = [2 if feat.startswith('act_factor') or 'buy' in feat or 'sell' in feat else 1 for feat in feature_columns]\n", "# light_params['feature_contri'] = feature_contri\n", "# print(f'feature_contri: {feature_contri}')\n", "model, scaler, pca = train_model(train_data.dropna(subset=['label']).groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(50, 'total_mv')).merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left').merge(index_data, on='trade_date', how='left'), feature_columns)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "5d1522a7538db91b", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T15:04:39.656944Z", "start_time": "2025-04-03T15:04:39.298483Z" } }, "outputs": [], "source": [ "# train_data = train_data.sort_values(by='trade_date')\n", "# all_dates = train_data['trade_date'].unique() # 获取所有唯一的 trade_date\n", "# split_date = all_dates[-120] # 划分点为倒数第 validation_days 天\n", "# print(split_date)\n", "# print(all_dates)\n", "# val_data_split = train_data[train_data['trade_date'] >= split_date] # 验证集\n", "\n", "score_df = test_data\n", "score_df = fill_nan_with_daily_median(score_df, ['pe_ttm'])\n", "score_df = score_df[score_df['pe_ttm'] > 0]\n", "score_df = score_df.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n", "score_df = score_df.merge(index_data, on='trade_date', how='left')\n", "# score_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(50, 'total_mv')).reset_index()\n", "numeric_columns = score_df.select_dtypes(include=['float64', 'int64']).columns\n", "numeric_columns = [col for col in feature_columns if col in numeric_columns]\n", "# score_df.loc[:, numeric_columns] = scaler.transform(score_df[numeric_columns])\n", "# score_df = cross_sectional_standardization(score_df, numeric_columns)\n", "\n", "score_df['score'] = model.predict_proba(score_df[feature_columns])[:, 1]\n", "score_df['score_ranks'] = score_df.groupby('trade_date')['score'].rank(ascending=True)\n", "\n", "score_df = score_df.groupby('trade_date', group_keys=False).apply(\n", " lambda x: x[x['score'] >= x['score'].quantile(0.90)] # 计算90%分位数作为阈值,筛选分数>=阈值的行\n", ").reset_index(drop=True) # drop=True 避免添加旧索引列\n", "# save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nlargest(1, 'score')).reset_index()\n", "save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(1, 'total_mv')).reset_index()\n", "save_df[['trade_date', 'score', 'ts_code']].to_csv('predictions_test.tsv', index=False)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "09b1799e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "174\n", "['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cat_up_limit', 'industry_obv', 'industry_return_5', 'industry_return_20', 'industry__ema_5', 'industry__ema_13', 'industry__ema_20', 'industry__ema_60', 'industry_act_factor1', 'industry_act_factor2', 'industry_act_factor3', 'industry_act_factor4', 'industry_act_factor5', 'industry_act_factor6', 'industry_rank_act_factor1', 'industry_rank_act_factor2', 'industry_rank_act_factor3', 'industry_return_5_percentile', 'industry_return_20_percentile', '000852.SH_MACD', '000905.SH_MACD', '399006.SZ_MACD', '000852.SH_MACD_hist', '000905.SH_MACD_hist', '399006.SZ_MACD_hist', '000852.SH_RSI', '000905.SH_RSI', '399006.SZ_RSI', '000852.SH_Signal_line', '000905.SH_Signal_line', '399006.SZ_Signal_line', '000852.SH_amount_change_rate', '000905.SH_amount_change_rate', '399006.SZ_amount_change_rate', '000852.SH_amount_mean', '000905.SH_amount_mean', '399006.SZ_amount_mean', '000852.SH_daily_return', '000905.SH_daily_return', '399006.SZ_daily_return', '000852.SH_up_ratio_20d', '000905.SH_up_ratio_20d', '399006.SZ_up_ratio_20d', '000852.SH_volatility', '000905.SH_volatility', '399006.SZ_volatility', '000852.SH_volume_change_rate', '000905.SH_volume_change_rate', '399006.SZ_volume_change_rate']\n" ] } ], "source": [ "print(len(feature_columns))\n", "print(feature_columns)" ] }, { "cell_type": "code", "execution_count": null, "id": "7e9023cc", "metadata": {}, "outputs": [], "source": [ "def analyze_factors(\n", " df: pd.DataFrame,\n", " feature_columns: list[str],\n", " target_column: str = 'target', # 假设目标列默认为 'target'\n", " trade_date_col: str = 'trade_date', # 假设日期列默认为 'trade_date'\n", " mcap_col: str = 'total_mv', # 新增: 市值列名称\n", " mcap_bins: int = 5 # 新增: 市值分位数的数量 (例如 5 表示五分位数)\n", ") -> pd.DataFrame:\n", " \"\"\"\n", " 分析DataFrame中指定特征列的各种指标,包括基本统计、相关性、日间IC、ICIR以及在不同市值分位数上的IC。\n", "\n", " Args:\n", " df (pd.DataFrame): 包含日期、目标列、特征列和市值列的DataFrame。\n", " 需要包含 trade_date_col, target_column, feature_columns 和 mcap_col 中的所有列。\n", " feature_columns (list[str]): 需要分析的特征列名称列表。\n", " target_column (str): 目标变量列的名称。\n", " trade_date_col (str): 交易日期列的名称。\n", " mcap_col (str): 市值列的名称。\n", " mcap_bins (int): 市值分位数的数量 (例如 5 表示五分位数)。\n", "\n", " Returns:\n", " pd.DataFrame: 包含各个因子分析指标的汇总DataFrame。\n", " 同时打印因子在不同市值分位数上的平均IC表格。\n", " 如果输入数据或列有问题,可能返回空或包含NaN的DataFrame。\n", " \"\"\"\n", "\n", " # --- 数据校验 ---\n", " required_cols = [trade_date_col, target_column, mcap_col] + feature_columns\n", " if not all(col in df.columns for col in required_cols):\n", " missing = [col for col in required_cols if col not in df.columns]\n", " print(f\"错误: 输入DataFrame缺少必需的列: {missing}\")\n", " return pd.DataFrame() # 返回空DataFrame\n", "\n", " # 确保日期列是 datetime 类型\n", " df = df.copy() # 在副本上操作\n", " df[trade_date_col] = pd.to_datetime(df[trade_date_col], errors='coerce')\n", " df.dropna(subset=[trade_date_col], inplace=True) # 移除日期转换失败的行\n", "\n", " # 过滤掉那些在 feature_columns, target_column, mcap_col 上有 NaN 的行,以确保后续计算是在完整数据上\n", " # 直接在 df 副本上进行清洗\n", " initial_rows_before_clean = len(df)\n", " df.dropna(subset=feature_columns + [target_column, mcap_col], inplace=True)\n", " rows_dropped_clean = initial_rows_before_clean - len(df)\n", " if rows_dropped_clean > 0:\n", " print(f\"警告: 移除了 {rows_dropped_clean} 行,因为其特征、目标或市值列存在空值。\")\n", "\n", " if df.empty:\n", " print(\"错误: 清理缺失值后数据为空,无法进行因子分析。\")\n", " return pd.DataFrame() # 返回空DataFrame\n", "\n", "\n", " print(f\"开始分析 {len(feature_columns)} 个因子指标...\")\n", "\n", " # --- 1. 基本因子统计量 ---\n", " basic_stats = df[feature_columns].describe().T\n", "\n", " print(\"\\n--- 基本因子统计量 ---\")\n", " print(basic_stats)\n", "\n", " # --- 2. 因子与目标变量的整体相关性 ---\n", " overall_correlation = {}\n", " for feature in feature_columns:\n", " # 在清理后的 df 上计算相关性\n", " if df[[feature, target_column]].dropna().shape[0] > 1: # 确保至少有两个有效数据点\n", " overall_correlation[feature] = {\n", " 'Pearson_Correlation_with_Target': df[feature].corr(df[target_column], method='pearson'),\n", " 'Spearman_Correlation_with_Target': df[feature].corr(df[target_column], method='spearman')\n", " }\n", " else:\n", " overall_correlation[feature] = {\n", " 'Pearson_Correlation_with_Target': np.nan,\n", " 'Spearman_Correlation_with_Target': np.nan\n", " }\n", " overall_corr_df = pd.DataFrame.from_dict(overall_correlation, orient='index')\n", "\n", " print(\"\\n--- 因子与目标变量的整体相关性 ---\")\n", " print(overall_corr_df)\n", "\n", " # --- 3. 因子之间的相关性矩阵 ---\n", " # 在清理后的 df 上计算相关性\n", " factor_correlation_matrix = df[feature_columns].corr(method='spearman') # 改回 Spearman\n", "\n", " print(\"\\n--- 因子之间的相关性矩阵 (Spearman) ---\") # 修正打印信息\n", " print(factor_correlation_matrix)\n", "\n", " # --- 4. 日间 IC 和 ICIR ---\n", " print(\"\\n--- 计算日间 IC (Spearman 相关性) 和 ICIR ---\")\n", "\n", " # 直接在清理后的 df 上计算每日 IC\n", " if df.empty: # 理论上上面已经检查过,这里再检查一次更安全\n", " daily_ic_series = pd.Series(dtype=float) # 空 Series\n", " ic_stats = pd.DataFrame({\n", " 'Mean_IC (Spearman)': np.nan, 'Std_Dev_IC': np.nan, 'ICIR': np.nan\n", " }, index=feature_columns)\n", " else:\n", " daily_ic_series = df.groupby(trade_date_col).apply(\n", " lambda day_group: {\n", " feature: day_group[feature].corr(day_group[target_column], method='spearman')\n", " for feature in feature_columns if day_group.shape[0] > 1 # 确保每日数据点多于1才能计算相关性\n", " }\n", " ).apply(pd.Series) # 将字典结果转换为 DataFrame\n", "\n", " # 计算 IC 的统计量\n", " if not daily_ic_series.empty:\n", " ic_mean = daily_ic_series.mean()\n", " ic_std = daily_ic_series.std()\n", " # 避免除以零\n", " ic_ir = ic_mean / ic_std.replace(0, np.nan) # 使用 replace 0 为 NaN\n", "\n", " ic_stats = pd.DataFrame({\n", " 'Mean_IC (Spearman)': ic_mean,\n", " 'Std_Dev_IC': ic_std,\n", " 'ICIR': ic_ir\n", " })\n", " print(\"\\n--- 日间 IC 和 ICIR (Spearman) ---\")\n", " print(ic_stats)\n", " else:\n", " ic_stats = pd.DataFrame({\n", " 'Mean_IC (Spearman)': np.nan, 'Std_Dev_IC': np.nan, 'ICIR': np.nan\n", " }, index=feature_columns)\n", "\n", "\n", " # --- 5. 因子在不同市值分位数上的平均 IC ---\n", " print(f\"\\n--- 计算因子在 {mcap_bins} 个市值分位数上的平均 IC (Spearman) ---\")\n", "\n", " # 在清理后的 df 上计算每日市值分位数,直接添加到 df 中\n", " # 使用 transform() 和 qcut() 在每个日期分组内计算分位数\n", " # labels=False 返回整数 0 to mcap_bins-1\n", " # duplicates='drop' 处理在某些日期股票数量少于 bins 导致分位数边缘重复的情况,会返回 NaN\n", " # 添加一个临时列来存储分位数\n", " mcap_bin_col_name = f'_mcap_bin_{mcap_bins}'\n", " df[mcap_bin_col_name] = df.groupby(trade_date_col)[mcap_col].transform(\n", " lambda x: pd.qcut(x, q=mcap_bins, labels=False, duplicates='drop') if len(x) >= mcap_bins else np.nan # 确保股票数量足够进行分位数划分\n", " )\n", "\n", " # 过滤掉无法划分分位数 (NaN) 的行,进行分位数 IC 计算\n", " # 创建一个临时 DataFrame df_binned_analysis\n", " df_binned_analysis = df.dropna(subset=[mcap_bin_col_name]).copy()\n", "\n", " if df_binned_analysis.empty:\n", " print(\"错误: 划分市值分位数后数据为空,无法计算分位数上的 IC。\")\n", " avg_ic_by_bin = pd.DataFrame(index=range(mcap_bins), columns=feature_columns) # Placeholder\n", " else:\n", " # 按日期和市值分位数分组,计算每个分组内的因子与目标变量的截面相关性 (分位数IC)\n", " binned_ic_by_day = df_binned_analysis.groupby([trade_date_col, mcap_bin_col_name]).apply(\n", " lambda group: {\n", " feature: group[feature].corr(group[target_column], method='spearman')\n", " for feature in feature_columns if group.shape[0] > 1 # 确保分位数组内数据点多于1\n", " }\n", " ).apply(pd.Series) # 将嵌套结果转为 DataFrame\n", "\n", " # 对每个分位数组的每日 IC 求平均\n", " # unstack(level=mcap_bin_col_name) 将 mcap_bin 作为列\n", " # mean(axis=0) 对日期索引求平均\n", " avg_ic_by_bin = binned_ic_by_day.unstack(level=mcap_bin_col_name).mean(axis=0).unstack()\n", "\n", " # 重命名索引和列,使表格更清晰\n", " if not avg_ic_by_bin.empty:\n", " # Index name will be the original column name used for grouping ('_mcap_bin_X')\n", " # Rename the index name explicitly\n", " avg_ic_by_bin.index.name = 'MarketCap_Bin'\n", " avg_ic_by_bin.columns.name = 'Feature'\n", " # 可以根据需要对分位数 bin 索引进行排序 (虽然 pd.qcut labels=False usually sorts)\n", " avg_ic_by_bin = avg_ic_by_bin.sort_index()\n", "\n", " print(avg_ic_by_bin)\n", "\n", "\n", " # --- 6. 汇总所有指标 ---\n", " # 将基本统计、整体相关性、IC/ICIR 合并到一个 DataFrame\n", " # 注意:合并时需要根据索引进行对齐 (因子名称)\n", " summary_df = basic_stats\n", " summary_df = summary_df.merge(overall_corr_df, left_index=True, right_index=True, how='left')\n", " summary_df = summary_df.merge(ic_stats, left_index=True, right_index=True, how='left')\n", "\n", " # print(\"\\n--- 因子分析汇总报告 ---\")\n", " # print(summary_df)\n", "\n", " # --- 清理临时列 'mcap_bin' ---\n", " # 修正:在函数结束时从我们一直在操作的 df 副本中删除临时列\n", " if mcap_bin_col_name in df.columns:\n", " df.drop(columns=[mcap_bin_col_name], inplace=True)\n", "\n", "\n", " return summary_df # 主要返回汇总报告,分位数IC单独打印\n", "\n", "# # 运行分析函数\n", "# factor_analysis_report = analyze_factors(test_data.copy(), feature_columns, 'future_return')\n", "\n", "# print(\"\\n--- 最终汇总报告 DataFrame ---\")\n", "# print(factor_analysis_report)" ] }, { "cell_type": "code", "execution_count": null, "id": "a0000d75", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "开始分析 'score' 在 'circ_mv' 和 'future_return' 下的表现...\n", "准备数据,处理 NaN 值...\n", "原始数据 173284 行,移除 NaN 后剩余 172470 行用于分析。\n", "对 'circ_mv' 和 'future_return' 进行 100 分位数分箱...\n", "按二维分箱分组计算 Spearman Rank IC...\n", "整理结果用于绘图...\n", "circ_mv_bin 0 1 2 3 4 5 6 7 8 9 ... 90 91 92 \\\n", "future_return_bin ... \n", "0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "1 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "2 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "3 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "4 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "... .. .. .. .. .. .. .. .. .. .. ... .. .. .. \n", "95 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "96 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "97 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "98 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "99 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n", "\n", "circ_mv_bin 93 94 95 96 97 98 99 \n", "future_return_bin \n", "0 NaN NaN NaN NaN NaN NaN NaN \n", "1 NaN NaN NaN NaN NaN NaN NaN \n", "2 NaN NaN NaN NaN NaN NaN NaN \n", "3 NaN NaN NaN NaN NaN NaN NaN \n", "4 NaN NaN NaN NaN NaN NaN NaN \n", "... .. .. .. .. .. .. .. \n", "95 NaN NaN NaN NaN NaN NaN NaN \n", "96 NaN NaN NaN NaN NaN NaN NaN \n", "97 NaN NaN NaN NaN NaN NaN NaN \n", "98 NaN NaN NaN NaN NaN NaN NaN \n", "99 NaN NaN NaN NaN NaN NaN NaN \n", "\n", "[100 rows x 100 columns]\n", "生成热力图...\n", "分析完成。\n" ] }, { "data": { "image/png": "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nz59dOedd95w31WqVNH8+fNlGMZ1/y0DAADA/7FQDgCAyeLi4tSuXTsdOXJEtWvXVvHixXX48GHNnj1b27dv17x589yLYi6XS926ddOqVatUvHhxtWnTRqdPn9ayZcv0008/afr06am28Hjvvfc0bdo0FSpUSE899ZROnz6tpUuXavXq1ZoyZYoqVaqUbk8DBw7U3LlzVbRoURUrVkzh4eGpnl+9erW6d++uyMhI1atXT+Hh4frpp580cOBA/fPPP+rRo4dHP4v27du7b1x5PY899th1M7t375Z0ZSuX+fPnKyQkRElJSbp8+bLmz5+vxMRE5cqVK92F8tOnT+uBBx7I9PxOp1OzZs1SQECA3n//fZ07d06ffPJJlvqXpMcff1wTJ07Ugw8+qDlz5ujll19O9bxhGJo+fbr7+x9++EH/93//pyFDhqhVq1ZyuVwyDENNmzbV22+/7c61bt1ap06dSveGpzfi+PHj6d7AMz2JiYn6/fffNXDgQHdPKS5fvqzOnTtr48aNqlKlimrVqqVTp07phx9+0LZt2/Ttt9+m2rJEkvbu3at3331XpUqVUsuWLfXLL7/ou+++U3x8fIZ7nxuGocGDB/tskVySjh07JklpPuBKkZXf2/QEBQWpSZMmmjp1qjZv3qyqVau6n/vtt9905MgRPffcc6m243n11Ve1fPly3X///Wrbtq3i4uK0ZMkStW/fXgsXLvTKYvL17Ny5U+3atVNycrIaNmyo/Pnz6/fff9eoUaN04sQJDRo0KFX+0KFD6tSpkx588EG1atVK69at0yeffKJdu3bpk08+cS9YJyUl6cUXX9T69etVpUoV1ahRQ4cPH9YPP/ygDRs2aMGCBVn+i5CMFC5cWE6nU2fPnmWfcgAAABtgoRwAAJNt2LBBhw8f1tNPP613333X/fj777+vJUuW6PDhw7r77rslXbnB5KpVq/TYY49p/Pjx7issv/vuO73yyisaP368e3/k1atXa9q0aSpbtqymT5/u3uZhzZo1eumll/Tqq69q6dKlaa7S/OabbxQXF6cpU6ak2TpCki5evKh+/fopV65cWrBgge644w5JUq9evfTkk09q0qRJiomJUb58+bL9s3jllVcUGBio3LlzKzIy0v240+nUoEGDdPr0ab366qsqXry4EhMT0128crlcSkhI0JkzZ9yP1a1bVw6HQ5LUr18/rVixQps3b05zXIrz58/r/Pnzaa6Clq5coZzywUVkZKRmzpypzZs369FHH1VCQoKaNGmi8PBwbdq0Sc8//7w6dOiQZj/qHTt26Omnn3YvOteuXVu1a9dOU+vbb7/VgQMHlD9/fp05c0bVq1fXggUL9OGHH6pBgwa6fPmypCtbufhCQkLCdbcLKVu2bKrvAwIC9Nxzz6ldu3bux3799Vdt3LjR/UFBigceeEBDhw7V6tWr1aRJk1Tn+fzzz/Xcc8/pjTfeUEBAgJxOp+rXr6/Vq1fr0qVLaT7MkaShQ4dq7ty5Plskl678rkpSWFiY18/drFkz91X4Vy+Uf//99+7nU5w/f17Lly9X8eLFNWvWLPfvbM2aNTV8+HBt2bLFJwvln332mZxOp8aMGaNGjRq5H2/ZsqXmzp2rN954I9XV8adPn1ZMTIz7Axan06nnnntOq1at0o8//qi6detKkr788kutX79eL774ol599VX38V988YWGDRumqVOn6vXXX7+h3lN+16/+iw4AAADkXCyUAwBgspSrHvfv36+4uDjdcsstkqS+ffuqb9++qbILFy6UdGWx9+oF7nr16mnUqFGptnr4+uuvJUl9+vRJtbhZs2ZN1atXT8uWLdO6dev0+OOPp6px6NAhffHFF3rooYfS7Xft2rU6ffq0oqKiNGvWrFTP3XLLLTp27Ji2bt2a7sLv9aR3E86LFy+qW7duOn36tHr06KF27dqpVq1aaty4sd5880137sKFC3r33Xf12muvZfkGnFe7em/zlD3Br92eRrpypfTVi7Lh4eF69NFHJV1ZHE1ZID18+LAkqXjx4mnOcfLkSUnK9OrsxMREjRkzRiEhIWrdurUmTpyo/Pnz6z//+Y8mTJigCRMm6Omnn5YkFShQIDsv1WPh4eH6999/M8107txZ0pX9zPfs2aMRI0akWsCVpEcffdR9tX+KgwcPavv27ZKubC9zrSJFiqhv377uK4ojIyNVqVIlLVmyRP/++2+aRd/33ntPM2fOVLly5Xy2SC7J3d/VH7x4S7ly5VS2bFktW7ZMgwcPVlBQkJKTk7VkyRKVLFlSFSpUcGdz5cqlyMhInTlzRgcPHlSJEiUkSfXr11f9+vW93ltGPvzwQ3344Yfu7xMTE7VlyxadO3dOCQkJOn78uPvDNkkKDQ1N9ZcVkZGR6tChg1599VWtXLnSvVC+aNEiSVe2lxk9erQ7HxcXJ+nKB5A36vTp05J890EUAAAAzMVCOQAAJrv//vvVtGlTLVq0SI888ojKli2rqKgoVatWTfXq1Uu1IL5//36FhYW5F71ShIWF6Yknnkj12J9//ilJ6e5FfN9992nZsmX6888/0yyU165dO8NFckn666+/JF1ZTE5ZUL7W8ePHM37B2XDo0CF169ZNu3fv1sMPP6zu3btLurJ38IIFC9S5c2cVLlxYLpdLr7zyin766Sft2rVLM2fOzPbiVvny5d1X7v/000+SrlydOm3aNM2ePVuVK1eWy+VScnJyls63Y8cOSWmvsJaytlA+btw4HTlyRO3atUu1kNi+fXsdOHBArVq1cs/i9ttvz1JPN+rOO++87mx79+4tSapUqZJefPFFLViwIM1CuXTlZzB79mxt2rRJu3bt0tmzZ91XFqf3M27QoEGav35ImbFhGKkenzt3ro4fP65SpUpp165d+umnn9L8nt8sKR90pSzYXqtNmzbauXOnVq5c6dHWIM2bN9eIESO0fv16Pfroo9q8ebNOnDiR6op96cpWLa+//rqGDh2qxo0bq0SJEoqKilLFihXVuHFjjz5M8tSGDRu0ePFibdu2Tfv371dSUlKq7aSuVqRIkTTb7pQuXVrS/z58kqQDBw5IUqqtia7mjfeg48eP65ZbbuGmqwAAADYReP0IAAC42T744APNmzdP3bt3V7FixfTLL7+oT58+atq0qc6dO5elc1y6dEnx8fFpFg2zexO6e++9N9PnU84/YMAA7d69O92vtm3bZqtmer7++mu1bNlSe/fulaRUW7l069ZNly5d0uDBg5WQkKCePXvqp59+Ur169fTll196dAVo79699dZbb8nlcmnp0qWqUKGCgoKCdOTIEUlXFmoHDx6cakuYjCQnJ+uXX35RRESEoqOj0zyfslBeqFChdI9fvXq1Jk+erHz58qXZ7z1//vwaPXq0SpUq5b5RZspC4s1Wvnx5rVu3LkvZmjVr6r777tP69eu1bdu2VM/t2LFDDRo00KRJk5Q/f369+OKLmjx5coZ7jUvpX5mfkePHj+ull17SnDlzVLBgQb3zzjs+2z4j5a8Qjhw5ku5V5SdPnlRSUlKaxeCsatKkiYKCgtzbrXz//fcKCAhQ06ZN02SfeeYZLV++XAMHDlTlypX1119/6e2331bdunX1+++/e1Q/uz788EO1b99ea9euVZUqVTRo0CAtXLgw3Q9PpCsL/NdKWVS/+r3NMAzlyZMnw/egtWvX3nDva9euVfny5W/4PAAAAPAPLJQDAGCyQ4cO6bffftM999yjl156SSNHjtTKlSvVoUMH/fXXX/rqq6/c2ZIlSyohIcF9JfHVnnjiCT344IM6f/68pP8tnqZ31feNLLCmLATu27cv3fPOmDHDfTW1J7Zt26aYmBgNGDBAYWFh+uKLL9JkoqKi9Nxzz+mnn37SE088oeXLl+v555/XuHHj3Ff0Xs++ffv0xRdfqEuXLqke//bbb3XkyBH3NjBX75+cVcuXL9eRI0dUp06dVDdXTHHixAlJGV9Rnj9/fuXJk0evvfZapjcR3LBhg0JCQny2UF6nTh1t2LAhy1frvvTSS5Kk8ePHp3p87Nixio+P12effaYxY8aoY8eOqlmzZpoPea6W3s8xI82aNdMrr7yi3Llzq1evXjp06FCqvdBvprvuuktFixbVxYsXtXXr1lTPnT59WseOHVOxYsU83sO8YMGCeuihh7RixQpdvHhRy5YtU5UqVVSkSJE0tX777TeFh4erXbt2Gjp0qObPn68JEyYoPj4+1XYlN8vJkyc1efJklShRQosXL9agQYPUpk0bRUVFufdyv9bhw4cVHx+f6rH9+/dLUqq/rChRooTOnz/v/rd0tVmzZmnmzJk31HtiYqKWLVvm3uoFAAAAOR8L5QAAmGzOnDlq06aNVq5cmerxUqVKSZJOnTrlfizlKszhw4crMTHR/fjGjRt15MgRlStXzr1NQMuWLSVJo0ePdi+eS1eukly2bJmKFCmihx9+ONv9PvLII7r11lv13Xffac+ePe7HXS6Xhg4dqrfffltnz57N9nklaeLEiXrmmWe0ceNGNWzYUN98842qVauWJrd48WI1a9ZM9913nw4dOqSqVauqX79+GV49Hx8frx07dmj27Nn6448/FBcXp8aNG2vYsGHuq7sl6ezZs/roo49UsmRJ988vu+Li4jRs2DAFBAToP//5T7qZlJlmtFBeoUIFffnll+49yNOTshf8Aw88kGZLkpvl8ccfV9GiRTV27Ngs5evXr6+SJUvql19+ce8/Lv3vg4KrFz4PHTqkESNGeKXPq2/C2qJFC913332aPHmyDh486JXzX0/79u0lXbma+uor2cePH6/k5ORUN7X0RPPmzXX+/HkNGzZMp0+fVvPmzdNk/vjjD7Vp0ybNhxRlypSRlPp95WY5efKkDMNQwYIFU/2OfvPNN1q+fHm6xyQkJOiTTz5J9X3Kh2VXb5+TcrPXjz76KNWV+6tXr9abb76pb7755oZ6nzRpkoKCgjK88h0AAAA5D3uUAwBgshYtWmjGjBnq16+fli5dqiJFiujUqVNaunSpAgMD1aBBA3f2ueee09q1a7V69Wo1adJENWrU0JkzZ7R06VKFhoZq8ODB7uxjjz2m//znP5o2bZqefPJJ1a5dW6dPn9aKFSsUERGhkSNHerTAGhERoffee089evRQy5YtVbduXRUqVEi//vqrdu3apQYNGqhGjRoe/Sw6dOigPXv2qGXLluku4h86dEjvvvuuVq1apa5du2rixInq2LGjNm7cqGeeeUbPPvusatWqlWZbi5iYGP3xxx+SrtwcsH79+nrsscf0yCOPuBdrk5KS9Nprr+no0aP68ssv0/3ZLFq0SFu2bNEzzzyT7t7vCQkJ6tatm44dO6Znn3021c0Vr3b06FGFhYWl2k7mWuXKlXP/7/SutJ48ebKSk5NVq1atDM/hbUFBQerXr5+6du2qxo0bu29impHAwEC9+OKL6t+/vyZMmODeWqVGjRravXu3OnbsqMcff1xHjx7VTz/95L5J6tUf7NyowMBADRgwQO3atdPbb7+tKVOmeO3cGYmJidHmzZu1bNkyNW3aVI8//rgcDoc2b96s0qVLq2PHjjd0/rp16ypXrlyaPXu2wsPDU71HpKhWrZruuecezZw5UwcOHFC5cuV06dIl9wL1jS7WZ0Xp0qV1xx13aOPGjerWrZuKFi2qLVu2aMeOHcqbN6/OnTuXZi/3vHnzaurUqfrjjz9UsmRJrV+/Xvv27VP16tVTvc727dtr9erVmjt3rn7//XdVq1ZN58+f15IlSxQZGakBAwZ43PeePXv06aefqn///qlukAwAAICcjSvKAQAwWYkSJfT111/rySef1B9//KEZM2bo559/VpUqVTR16tRUV1QHBwfrk08+0euvv67g4GDNmjVLv/zyi2rVqqW5c+eqUqVKqc49YMAAvf/++ypYsKDmz5+vDRs2qH79+vr666/TZLOjVq1amjVrlmrUqKG1a9dq9uzZCgwM1NChQ29oS4fQ0FB9+OGHqRbJU67I3bRpkxo3bqxVq1apXr16atiwoQoWLKiZM2fq2Wef1Y4dO9S3b19VrVpVjRs3Vvfu3d2LcA0bNlT16tX1wQcfaN26dRo3bpyefvpp9yL5P//8o44dO2rNmjV67bXXVLVqVXf9q69W3blzp2bNmuW+UerVDh48qLZt22rDhg16+OGH012oMwxDGzdu1O7du1Nd9Xw9SUlJkv53k8stW7Zo1qxZioyMdG8RczWXy5XpNiY3ok6dOmrdurX69OmjXbt2XTfftGlT3XnnnVq1apV7y59evXqpS5cuSkxM1IwZM/THH3/oueee0/Tp0xUYGKiVK1d6dU/xypUrq1GjRvr555+1bNkyr503I4GBgRozZowGDhyo0NBQffXVVzp48KCee+45zZw584YXXyMiItyLxnXr1k33fKGhoZoxY4a6d+/uvnHqokWLVLhwYb399tvq2rXrDfWQFaGhoZo6darq1q2rzZs3a9asWQoKCtLo0aPdf22xePHiVMcUL15cY8eOdfd84cIFdezY0X2F99Xn/uyzz9SnTx+5XC7Nnj1bv/76q+rVq6cFCxbo/vvv96jno0ePqnPnzqpevbqeffZZz188AAAA/E6AcbP+vygAAAAvWLFihbp16yZJuueeezR48GBVqVIlTW7fvn366quv9O233+r8+fN66qmnNHz4cElXFpjT22v89OnTmj59ur744gslJSVp8ODBat26tfv5Dh06aP369Ro0aJDCwsL0wQcfKDExUWvWrHHvhX7s2DFNmzZNX331lRITE9WgQYN0r9aPiYnRli1b3Avvffv21QsvvJCln8GUKVP0wQcfaOrUqbrzzjvVtm1bnT59Wl27dlXPnj3T5Js0aaIzZ87ol19+ydL5sysxMVEdO3bUn3/+qSVLlnh8Y0rASi5fvqymTZsqKChIM2fOzPL9DgAAAJAzsPUKAACwtJo1a+r+++9XlSpV1LNnzwy3iylVqpQGDx6sAQMG6Lfffku1rUlGN+T86aefNGXKFJUtW1bvvPNOqu1OJKldu3bau3evhg4dKunK1f+9e/dOtYCWnJysNWvWKDg4WK+//rqee+65dGu1a9dOf/31l6Kjo9W0aVM1btw4yz+DlP3oExISVKxYMQ0dOlSTJk1y3ywzvfzFixezfP7sCg0N1aeffqp169axSI4cIzg4WG+88YbKlSvHIjkAAIANcUU5AACwvMTExJt2w8q//vpLxYsXz/BGoFlx+PBhhYaGqlChQl7s7H9Onz6tU6dOqUiRIu5tNgzDuKGeAQAAAAD/w0I5AAAAAAAAAMDWuJknAAAAAAAAAMDWWCgHAAAAAAAAANgaC+UAAAAA4GcSEhKum2GXTQAAgKxjoRwAkKP88MMP+vrrrzN8fuHChfruu++ydK74+HglJiYqOTk5y/UvX76s+Ph4nT17NsvHwLqSkpJ06dIls9sAkAN89913OnfunPv7S5cuaejQoXrjjTeyfa6zZ8/qkUceUYcOHXTo0KF0My6XS0888YQ6d+6sf//91+O+AQAA7CLY7AYAAPCmTz75RIcOHVLLli3TfX7YsGHKmzevnnzyyeuea+DAgVq8eLFHfRQoUEBr16716FiYIzk5WSdPntT+/fu1e/du/f7771qzZo169uyp9u3bm90eAD92/Phx9evXT7fddptWrFihkJAQhYeHa8+ePdq6dav++9//qlSpUlk+3/jx4xUXFyeXy6XExETt27dP0pUP90JDQ1WyZEl9//332rdvn+6//37ddttt7mMNw9DFixcVHh6uwECumwIAAEjBQjkAwK+dOXNGBw4cUFhYmMLCwhQUFKSQkBD3osG1goOD3c8bhqGEhATly5dPd955Z5ps1apVlT9/fgUHBysoKCjVc8uXL9ehQ4fUunVr5c6d2/345cuXlZiYqPDwcO++UNwUJ0+eVJcuXXT69GmdOHFCSUlJ7ufCw8N15513au/evSZ2CCAnmDx5spKSktShQweFhIS4H+/Vq5fatWunoUOHatq0aVk61+bNmzVz5kxJ0oYNG9S4ceNUzzdo0EAjR47UmDFjJF35S6pvvvnG/bzL5ZIk/fjjjypatOiNvCwAAIAcJcBg4zoAgB9bsWKFevXq5V4kj4+Pl8vlUt68edPNx8XFKSAgQLlz55ZhGEpMTFSHDh3Us2fPbNXt1KmTfvrpJ0suNMTExGjjxo3avXu32a34hZdfflkul0t33HGHwsLCNGXKFPXt21f//e9/FRAQ4M5t3bpV27dvT/ccQUFBiomJueFekpKSNGnSJC1cuFDHjh1Trly5VL16dY0dO/aGzw1ca9++fXrmmWfUv39/tWjRIt1MXFycJkyYoKVLl+rUqVMqX768+vfvr+jo6DTZxMREffbZZ+7f31KlSunVV1/VI4884rWeBw8erF9++UUrV67MMLNu3TqNHz9eu3btUmRkpFq2bKkePXooODjtNUIOh0NjxozR9u3bFRAQoEaNGum1115Trly5vNbzgQMH9OSTT6pgwYJaunSpQkNDUz3fs2dPLVmyRP3799fzzz+f6bkOHz6sVq1a6fz585o8ebK2bt2qcePG6cMPP1RUVJQSExMVGRmpuXPnavLkyerUqZMqVqyoqVOn6o8//tD777+vpKQkJSQkqF69el59nQAAAP6OK8oBAH6tbt26cjgc7u9jYmK0f//+DLc9efLJJxUZGak5c+b4qkVY3EcffeT+3/v27dOUKVMUGRmZapFcurL4Nm7cuHTPERkZ6ZWF8jFjxmjKlCl66KGHVL9+fZ05c0a//fbbDZ8XuNbp06fVuXNnnT9/PsNMfHy8OnTooB07dqhChQpq0KCB1qxZo5iYGM2bNy/VViEul0svv/yyVq1apVKlSqldu3basmWLXnzxRU2dOlXVq1e/4Z6/+OILzZ49O92/AErx3Xff6bXXXlN4eLiefPJJJSYmasqUKTp58qTee++9VNmNGzeqY8eOcrlcaty4sSIiIrRw4UIdOHBAn3/+eZr3AE8kJydrwIABSkpKUp8+fdIskktXtvlav369Ro4cqdKlS+vRRx9N91w7duxQt27ddPr0afXs2VMPP/ywwsLCNG7cOO3bt8+9pdjatWs1depU3XffferVq5cCAwP1999/a+PGjcqbN6+qVKlyw68LAAAgJ2KhHACQ45w6dUply5bN8Pn777/fh9343ogRI3Tx4kWz28hxUrbTWb58uYoUKeJ+/Pnnn9eff/7plRrffvut7r777lSLdNm5mWx6Dh8+rDp16uipp57S8OHDvdGmLWzYsEHt27dX9+7d1aNHD7Pb8aq9e/eqa9eu+vvvvzPNTZgwQTt27NATTzyhkSNHKjAwUD169FDz5s31xhtvaNasWe7s7NmztWrVKlWuXFmff/65QkND5XK5FBMTowEDBmjp0qWpthzJjuTkZI0ZM0YTJ07MNHfq1CkNHjxYISEhmjFjhu677z5JUsWKFfXmm2+qcePG7kXohIQE9evXT4mJifr000/1+OOPS5Lq1aunjh07as6cOWrTpo1H/V5t6tSp2rJliypWrJjhvTEKFiyo4cOHq2vXrurRo4c++ugj1ahRI1XGMAx98sknOn78uJo3b64uXbpIku69914FBQVp8+bNkq78rFI+0BsyZIh7D/Lq1asrICBADoeDhXIAAIAMcPcWAECOkzt3bg0bNizdr8KFC5vd3k1XpEiRbN0Uzs4uXLigpKQkZbQTXWJioi5cuCCn0+netiEwMFDBwcHur4CAgDR72Hvq+PHjuv3221NdycrN9uBN+/fvV+vWrSVd2UIqI5cvX9bs2bMVEhKigQMHun8PIyMj9cILL2jbtm3av3+/O//VV19JkgYNGuS+ajooKEhdu3bVkSNHtH79eo97fvPNNzVx4kR16dIl06vJFy5cqPj4eLVt29a9SC5JTz/9tO644w59/fXX7sdWrlypI0eOqG7duu5FckmqUaOGKlasmCrrqZ07d2rs2LGKjIzUsGHDMr1CvXbt2ho0aJCcTqc6deqkyZMnp/qQLCAgQOPGjdOQIUPc52revLn69eunKVOm6LPPPpMkOZ1ODR06VEOGDFFkZKT27dunffv2KTg4WB9//LFq1qyZam4AAAD4H64oBwDkOCEhIRleNR4REXHd4+Pj4xUSEqKQkBCP//TeMAy5XC4lJCQoLCws3b1xYb6nnnoq3atq33rrLb311lvu71988UUVKFDAh50BN8epU6fci7KZ7fMdGxuruLg4Pfzww7r11ltTPZey5/jatWtVsmRJnT59Wn/++aeKFSumcuXKpcpWrVpVISEhWrdunWrWrOlRz/Hx8fr4449Vt25dLVq0KMPcpk2bJEn169dP9XhwcLCqV6+uVatWXTeb8vo+/vhjXbhwIdXNmrPj6NGj6tSpkxITEzV06FCVKFHiuse0a9dOERERGjRokEaOHKkff/xRAwcOdO8HHxQUpGeeecadP3z4sHLlyqWHH37Y/djq1avVp0+fTOtERkZq27ZtHr0uAACAnIz/rx0AkOOcOXNGjRs3zvD562290qZNG+3duzdLterUqXPdzJdffqlq1apl6XzX2rp1qyZMmKBt27YpMDBQJUqUUExMjJ588skMF/GzcjPP2rVrS7pyVeX+/fv12Wefad26dWrYsKFef/31VNmzZ8/q448/1vLly3X69GkVKVJENWvWVNeuXZUvX75sv6b58+erf//+GjBggHbs2KEVK1aoRIkS+uijj/TFF19o/vz5KlCggIYNG6bKlSurV69e+uGHH/TDDz+oZMmSqc7Vr18/LViwQAsWLNC9996b7V66deumpKQk9xXhCxYs0MaNG9W8eXNVq1ZNLpdLSUlJuueee1Lthe9N124TtHHjxlSPXf37k/J607uJ7NUzTdlu5WopP6cUV58jZSbDhg1Lc1PH9Gpeuy3J4sWLNWvWLO3evVvTpk1Ls1i6c+dOTZgwQZs2bVJ8fLxKlCih9u3bq1WrVtn+eWX0mq/3e3zhwgVNmjRJS5Ys0dGjR5UvXz7Vrl1bvXv3Vv78+VO9rquNHz9e48ePd39/9b+rcePGafz48en+G0/v3+HVP+fmzZtrzpw5WrBggfbt26dVq1bplltuSdVH9+7d1bp1a40YMUJr165VcnKyKlasqEGDBumuu+7y6GdWqVIlVa1a9bq5EydOSFK6/66KFi2qkJAQ94dMmWVDQ0NVpEiRVB9IjR8/XuPGjVOHDh3Ur18/9+OjR4/Wp59+qs6dO6t3797uxz/44IMs/dXGiRMnFBAQkG4fxYoV09mzZxUXF6dbbrnF3fPVV55fnTUMQ4cOHVJUVNR1617r0KFD+s9//qMTJ06oTZs2WrZsmX7++WcFBwdn6S9E3nvvPQ0fPlzbtm3TK6+8om+++ca99dPVgoOD0/xcUj4Mnjx5crofTMTExGT5/74BAADYDQvlAIAcp0CBAhnezDNlYS0zdevWVdWqVdNdhEixfPlyHTp0SK1bt073isPk5GS5XC5dvHjR4yuRFy1apH79+ikkJEQNGjRQnjx59OOPP+rVV1+Vw+FQ//79PTrv1X7++We9/PLLkqQyZcqk2ZrmxIkTevbZZ3Xo0CFVrlxZDRo0cC+Grly5UvPnz1eePHk8qj1mzBg9+OCDeuCBB7R+/Xo9/fTTKliwoBo2bKj58+dr9OjR+uqrr9SsWTP98MMPWrx4sbp37+4+PjExUStWrNA999zj0SK5JDVv3tz9vy9fvqyxY8dKkh544AE1aNBAISEh7m0kbtZCeefOnd3/+9NPP1WRIkXUtGlT92NX74eeVXny5HGf98KFC5oxY4bKli2rWrVqpcp4w8CBAzV37lwVLVpUxYoVS7Ogt3r1anXv3l2RkZGqV6+ewsPD9dNPP2ngwIH6559/vLL/9/V+j+Pi4vTss89q7969qlGjhurUqaM9e/Zo9uzZ2rJli+bNm6eIiAgVKVLE/XM7evSoFi1apMqVK6ty5co33OPVLl++rJdeekk///yzSpYsqZIlS6a7gHry5En3e0zz5s21detWrV69WsePH9eCBQs82pYnq9sEXbhwQZIyfP+65ZZbdPz48Sxl8+TJ485KV37nV65cqenTp6tFixa655579Ndff+mzzz7TPffco27dunncc2RkZLp/OZTy+/7PP//olltucfd82223pcnmzZtX0pWtkDxZKP/hhx/c27q8/vrrevDBB7N1fN++fVW9enX1799fffv2df+b+uabb/TBBx8oNDRUgYGBOn/+vH7//XfVqlVLiYmJiomJcb8Xnj9/XidPnkxz7qs/GAQAAEBqLJQDAPzW33//rXPnzrm3SZGkixcvyuVyad++fekek5SUpISEhFTPJyUlyeVyua8s7NWr13Vr79+/X4cOHVKnTp3SXNnrDSdOnNCgQYMUFhamefPmufccf/nll9WoUSN9+eWX6tixowoWLOhxjbi4OL3yyitq166dunTpoly5cqXJDBkyRIcOHVLPnj3VtWtX9+ODBw/W7NmzNXv2bL344ose1a9QoYI+++wz7dq1S82aNZMkzZw50/2BwLFjxyRd2TP41ltvTbNQvmbNGsXFxaVaaL4R8+bN0z///CPpyvYUzzzzjO67776bfgPMq6+c/fTTT1W0aNFUj3kiT5487nMcPnxYM2bM0L333nvD573WN998o7i4OE2ZMiXNzQelK/8e+/Xrp1y5cmnBggW64447JF35N/bkk09q0qRJiomJ8egvE1Jk5fd49OjR2rNnj9566y21bdvW/fi7776rL7/8UvPnz1e7du101113uX9GGzZs0KJFi1S9enWv38xz0qRJMgxDc+fOVYUKFTLMzZkzR/Xq1dOoUaMUEhIil8ulli1bKjY2VgcOHEjzFxbelLKYmt6VzNKVK8UvXbqU5ey5c+fc3wcHB2v48OFq0aKFhg4dqunTp+vtt9+WYRgaMWKE+8Op7AoMDMxwe62Uc6bc6Dil5/Ty12az66WXXlK5cuVUvXp1hYaGauPGjYqIiFBwcLDq1Kkjp9OpDRs2pDnuySef1MGDB91b3UydOjXV81WqVNG7776r0NBQff/995o7d66ioqI0YMAAXb58WYULF3Zfuf/KK69k2B/bSAEAAKSPhXIAgN+aOHGi5s2bl+5zmW29cuLEiTTPZ3YVuhmWLFmiS5cu6aWXXkp1Y848efJo9OjROnnypPvDAU+dP39ezZo106uvvpru8+fOndPKlStVuHDhNDf969y5s6pVq6ZixYp5XD9la5CUK/KrVKnivurz6qv0g4OD9cQTT2j69OnatWuXe1uPxYsXKzAwUE2aNPG4hxRxcXH66KOPFBERoYsXL+rWW29VgQIFtGDBApUpU0YvvPDCDdfIiQ4dOqQvvvhCDz30ULrPr127VqdPn1ZUVJRmzZqV6rlbbrlFx44d09atW7P0lx4Zud7vcXJysr777jsFBwfr2LFjGj16tPu5M2fOSJJ+/fVXtWvXzuMesuv48eNauHDhdW+6mytXLg0ZMsT9bz0oKEgPP/ywYmNjderUqZu6UB4ZGSnpyl9upCcpKUlJSUnZzqa455571L17d40ePVq9e/fW2rVr1a1bN4//OkS68vM6ffp0hj1c/d+rew4LC0uVTXkd1/acHVdve5JyhXpiYqL++eefDD8cOXnyZKY3nC5SpIj7L0yGDRsm6cq/o9jYWJUrV07Fixd3L5R/9NFH6f677NSpU7r3ZQAAAAAL5QAAP9avXz+99tprCg8PV1hYmJKSktSzZ0+tXr1akydP1iOPPKJFixYpLi5Obdu2lWEY6ty5s3777Tf3IkLKHtSeXjl4s+zfv19S2v2rJWVpf+GsCA0NzXBxUZIOHjyo5ORklSlTJs2f6l+9YOOpa7esyeymec2bN9f06dO1ePFilStXThcvXtSqVav00EMPZbqwlFVDhgzRv//+q169emnMmDEKDAzUqFGj1LRpU40ePdp980Irc7lcN2VLBZfLleFztWvXznCRXJL++usvSVduDBkbG5tu5uotOTxxvd/jM2fOuK9mnjhx4k3pIT2Z/dzatGlz3UVy6cpNJa+9kWbKFdCGYdxYg9dx++23S7ryYci1Ll++rLNnz7o/2MosK0n//vtvmtchXblJ7vLly/XDDz+obNmy6tKlyw31XLhwYf3xxx86d+6ce3H66h6k/y1ap7xvHDp0KM3i/LVZb/njjz+UnJyc7vt6YmKizp49q9KlS1/3PGvXrtXu3bsVEBCgkydPasKECUpKStJXX33lzkRERKS7vRI3lgYAAMhY9jc2BADAIm655Rbly5dP4eHh2rFjh5555hmtXr1aQ4YMcS9sfvbZZ+6Fz6CgII0ePVqlSpVSx44dNWrUKCUmJio8PNx9Mz9/cPnyZcXHx9/Q1Y6SVLBgQRUqVMjj4+Pj4332AUN0dLRKly6txYsXS5JWrVolp9OZai9vTy1evFjffvut6tWrp/r167sfz58/v/r376+kpCStWLHihuvcTImJienuR+wNR44cyfC56139m7KYO2DAAO3evTvdr6u3QvHE9X6PU3qIiorKsIc5c+bcUA/puZGfW4rixYt7qZvsK1WqlMLCwrRjx440z+3YsUMul8u99VPevHl15513yuFwpFnAP3HihI4cOZLuNlHx8fHuRenTp0+79w33VMrPdfv27Wme++233yT9b9uRrGRvZGur9KxevVrSlb+euVbKv9/rvSe7XC69//77uvfee3XbbbepYMGCmjx5si5fvqwuXbooLi5O0pUPIcqWLZvma+PGjV59TQAAADkJC+UAAL916dIlrVixQi+88IJat26tw4cP65lnntGtt96qtWvXat26dUpISFBQUJDWrVundevWaePGjYqJiVGdOnU0ceJEPfbYY3rnnXe0evXqDLcNMEPKlgq7d+9O89ynn36qBx98MNXVgzdDsWLFFBgYqL1796a5Ovbo0aN68MEH1aZNm5vaw9WaNWumQ4cOafv27Vq8eLEiIyNTLWx7Yvv27XrjjTdUsGBBDR06NM3zjRs31ueff55qb/QVK1bom2++cX+dOnXqhnrIjoCAAElpr1betGlThlcwZ+WGjxmd9+zZs9q1a5cnrUr63+9xevcM2Llzp2bMmJHuQqw33XrrrcqbN68OHjyY5t/4pUuXNGPGDH3zzTdpjruRn9tff/3llavUb3R7pRsRFhamGjVq6LfffktzI9sFCxZIkh5++GH3Y3Xr1tWJEye0fPny62ZTDBkyRMeOHVPHjh118uRJvfnmmzfUc7169SQpzXvjkSNHtGHDBt13333u/fAfe+wxhYSEaNasWUpOTnZnL1y4oGXLlunWW29N98pvT128eFFz585VRESEHn/88TTPpyyUX+8vZCZMmKBdu3alujfEfffdpwEDBigmJsb9lznvvPOOFi9enOarfPnyXntNAAAAOQ0L5QAAv/Xf//5X3bp109q1a9W0aVP98MMP+vnnn9W1a1f997//VYcOHfTXX3/pzJkz6tChgzp06KAuXbrovffe00cffaT3339fgYGBmj59+k2/YWN2NWzYUGFhYZoxY0aqRcZLly5pyZIlkqRq1ard1B7y5s2rWrVq6fjx42m2rJg/f75Perha06ZNFRgYqJkzZ2rNmjWqX7++e59hT/z111966aWXlJCQoA8//DDdrSGktAt8w4YNU9++fd1fKdvk+EJKj3v37nU/lpiYqJEjR2Z4TKFChRQSEpJmW4zLly+nOe+ePXtSZcaMGaP4+HiP+03ZOuS7775LdW6Xy6WhQ4fq7bff1tmzZz0+f1YEBgbqiSeekNPp1KRJk1I9N3/+fL399tvuK32vduedd0pKu53I9X5uhmFo+PDhN31rFF944YUXFBgYqD59+ujPP/9034D066+/1h133KFHH33UnY2JiVFERITefPNNbdmyRZK0cuVKTZw4Ublz51bDhg1Tnfv777/Xd999p2bNmum1115Tq1attHTpUvfCuifKlSunGjVqaNWqVRo3bpySkpJ0/PhxvfLKK0pKSlLLli3d2dtuu819Y9Q333xTTqdT58+fV9++fXXmzBk99dRTXt3K6MMPP9SpU6fUokUL3XLLLWmeT/nALbOF8tWrV+uTTz7RAw88oEaNGqV6rlWrVnr++efdH94ULlxYpUqVSvOV0c1OAQAAwB7lAAA/1qdPHy1atEjPP/+8+8rViRMnKiQkRMHBwQoICFCXLl10+vRpzZ49Wy6XS+fOnZPT6ZR05QrlunXr6quvvtIdd9yh0NBQM19OKoUKFdLQoUM1YMAAtWzZUvXr11fu3Ln1008/6ciRI4qJiVFUVNRN7+PNN9/Url27NHbsWK1bt0733Xef9uzZo3Xr1umuu+664T2Fs+P2229X1apV3QtpzZo1u6HzFSpUSJUrV1blypWztOCfctXw8uXLU+3P/vzzz+vPP/+8oV6yqn79+vrss880fPhwhYSEKDQ0VB9//LGCgoLcW0pcKzg4WE2aNNH8+fPVrVs3lShRQsePH9exY8c0Y8YMSVKlSpVUoEABzZkzR2XKlNE999yj+fPna+nSpapYsaK2bdvmUb8RERF677331KNHD7Vs2VJ169ZVoUKF9Ouvv2rXrl1q0KCBatSo4fHPI6t69+6tjRs3aty4cfr55591//33659//tGKFSt02223qXfv3mmOKVKkiKpXr65vv/1WhmGoUKFCOnTokIKDgzVq1ChJV65KDg8P16effqpChQqpUKFC+uKLL7Rv3z6VLl3aZ78XN8uDDz6ofv36acSIEXriiSfcN7uNjIzUyJEjU71n3nXXXRo2bJj69eunZ5991p0NCQnR+++/n2obk3/++UdDhgxRgQIFNGDAAEnS66+/rjVr1uidd95RlSpVVLRoUY96Hj58uJ5//nmNHz9ekyZNksvlksvlUp06dfTss8+myvbr10/79+/XnDlztGDBAhmGocuXL6tChQrq2bOnR/XTM3v2bE2fPl2FCxfO8LwpW/VktFBuGIZGjRqlkJAQvfPOOwoICEj3w5isfEBz9RX0AAAA+B8WygEAfitlkfNqKQvmKUJCQhQUFOS+2VzKVaIpcuXKpZdeeilL9fbs2ePeGzzlCtKsbM/gqebNm6to0aL65JNPtGLFCl2+fFmlS5dW165d9fTTT9+0ulcrXLiw5s2bpwkTJmjZsmXatm2bChQooOeee07du3f3+d7uzZs316+//qrChQurevXqN3SuXLlyacyYMalubpfZIlPKfuyBgYGpjgkICFBCQsIN9ZJV999/v0aOHKkJEyaoR48euvXWW9WoUSP16NFDTz75ZIbHDRo0SPny5dPSpUu1atUq5cmTJ1U+d+7cmjRpkt59910NGzZMoaGheuihhzRv3jx9/PHHHi+US1KtWrU0a9YsTZgwQWvXrlViYqJKlCihoUOH+uz3OE+ePJo9e7Y+/fRTLVmyRDNnzlShQoXUqlUrdevWLcN9oUePHq1Ro0Zp9erV+vfff3XbbbcpJibG/fydd96pCRMmaOTIkerfv79y586tWrVqac6cOV5daDXTf/7zH1WqVElffvmlDh06pNKlS6tz585p3kslqVGjRrr33nv12Wef6c8//1SRIkXce2WnMAxD/fr107lz5/TRRx+5t0K55ZZbNGTIEHXu3Fl9+/bVjBkzPHp/LVCggObPn6/p06drzZo1Cg4OVuPGjdWiRQv31dYpIiIiNG3aNM2bN09Lly7V5cuXVatWLT333HNeuellYmKixo4dqylTpuiWW27RxIkT071B6JkzZ/Ttt99KUoYfEAQEBGjMmDHavn27ypQpI0nuDwGulvL91VuzXCvlZw4AAIDUAoyc8HehAADbSkxM1Pjx4xUWFua+ivxqM2fOVHx8fJpFg+TkZLlcLiUmJurpp5/WXXfddd1ay5YtU48ePdzfh4aGatOmTQoPD/fOi4HpHA6HWrZsqUGDBum5555L9dzff/+tw4cP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jB07tqz8+PHjy65hoxwAAAAAyIRSKZd0C6RUuzfKn3rqqejWrVt88YtfrEY/AAAAAADQopI3xMvVrjPKX3jhhTj11FPjhBNOiDfffLNaPQEAAAAAsJK99957MXLkyOjXr18ccsgh8dJLL5W1/le/+lWMGDGi1eezZs2KESNGRL9+/eLII4+M2bNnV6vllabijfIZM2bEmDFj4oorrohx48bF97///Xjvvfeq2RsAAAAAACtBqVSKUaNGxfvvvx+33nprjBgxIkaOHBkff/zxCq2/6aabYsKECa0+X7BgQRx99NHRrVu3uOuuu2K33XaLUaNGxeLFi6t9C1VV0Ub5rFmz4vjjj4+LLroott1229hjjz1i1KhRcfTRR8fcuXOr3SMAAAAAQPuVVsF/KvT000/HM888E+eff35sueWWcfDBB8cWW2wRDz744OeuvfPOO+MPf/hDHHrooa2+u+++++K9996L888/P3r27BnHHntszJ8/P55++unKm62BsjfK33rrrTj22GPjnHPOiR133LHl86997WvxjW98I773ve/F/Pnzq9okAAAAAADVM3369Nh4441jq622avmsX79+8dxzz33u2h133DF+85vfxHrrrbfM6/bp0ycaGxtbPuvbt+8KXTdJuVKptML/3eGdd96Jb3/72zFmzJjYZ599lpm58sorY9q0aXHttdfG6quvXrVGqyWfzyfdAgAAAABURXNzc9It1JUtblr5PwpZa73+a/lvgE+ZMmWZn1999dXxxBNPxA033NDy2eTJk+Pxxx+Pq666aoVq/+xnP4snn3wyJk+e3PLZGWecEaVSKS688MKWzy666KJYvHhxnH766St03SSstqLBjz/+OI4++ug45phj2twkj4gYNWpUnHfeeXHSSSfFFVdcUZUmq23pB0ixWIxCoRBNTU3R0NDQKp/P58t66Mgnm29rzfLmnLZ7kK9sjRmnN1+tGmZcu3xSPZlx5fk09mTGydcw42TzaezJjJOvUQ/5eppxGnuqh3ySM65FjazloVKrrbZadO7ceYnP1lhjjSgWi+26bseOHaNjx46trpv237dc4Y3yrl27xrnnnhv9+vX73OyZZ54ZU6dObVdjAAAAAADVVCrlkm6h6tp6Y/zzNDY2ttq8njdvXnTq1Kld/TQ2NsbMmTOrft2Vrawzype1SX7nnXfGRx991OrzAQMGVN4VAAAAAAArTd++feOVV15ZYm/3hRdeiI022qhd1+3Xr18888wzsWjRoqped2Ur+8c8/92iRYti7NixMXv27Gr1AwAAAADASrb11ltHr169YtKkSbF48eJ48cUX4/77749BgwbF4sWL46OPPlpis3tF7bbbbrFw4cK4/vrrIyLi4Ycfjueffz4GDRpU7VuoqnZtlEdElPFboAAAAAAApMT48eNjypQpseuuu8Y3vvGNOOCAA2LPPfeM2bNnx4ABA+Lll18u+5qrr756TJw4MX75y1/GrrvuGscff3wcf/zxseWWW66EO6ieFT6jHAAAAACgrnnndwnbbLNN3HfffTF16tRobGyM7bbbLiIiNt1005gxY8bnrj/hhBOW+fnuu+8eDzzwQEybNi169uwZW2+9dVX7XhlslAMAAAAAZFRDQ0PstddeVb/u2muvHYMHD676df/ls88+i8WLF0fnzp3j448/jr/85S+x6aabRu/evSu6XruPXgEAAAAAgFp58sknY7fddovHH388Pv744zjwwANj1KhRcfDBB8ftt99e0TXbvVGey+XaewkAAAAAAFghF110Ueyzzz7Rv3//uO+++2LBggVx3333xYgRI+Laa6+t6Jp+zBMAAAAAgLoxc+bMOOCAA2LNNdeMZ555Jvbdd9/YbLPNYsiQIfH2229XdM1cKWM73fl8PukWAAAAAKAqmpubk26hrmx+44SkW6i6WUeemnQLNTdo0KA46qij4hvf+EZ87Wtfi5NPPjmGDBkS9957b0ycODGmTJlS9jUz+WOeSz9AisViFAqFaGpqioaGhlb5fD5f1kNHPtl8W2uWN+e03YN8ZWvMOL35atUw49rlk+rJjCvPp7EnM06+hhknm09jT2acfI16yNfTjNPYUz3kk5xxLWpkLQ9ZdNhhh8WFF14Yl1xySXTv3j322GOPmDJlSkyYMCEOOOCAiq6ZyY1yAAAAAADq0zHHHBO9evWKN998M/bdd99Yc8014/3334/DDjssvv/971d0TRvlAAAAAADUlb333nuJPx966KHtul67f8wTAAAAAKAulFbBfzLo1Vdfrfo1bZQDAAAAAFA39t9//xg2bFhcc8018be//a0q17RRDgAAAABA3fjjH/8YBx54YPzpT3+KIUOGxCGHHBLXX399zJ49u+JrOqMcAAAAAIC6sdVWW8VWW20V3/ve9+LDDz+MRx99NB5++OH45S9/GVtssUX85je/KfuaNsoBAAAAgGzI6Jneq7Lu3bvHNttsE3PmzIk333wzXn755YquU/FG+bvvvhuvv/56/N///V988skn0aVLl1h//fWjubk5OnRwogsAAAAAANW3cOHCmDp1ajz88MPxyCOPxDvvvBN77bVXHH300bHXXntVdM1cqVQq67+j3HvvvXHVVVfFq6++GmuttVY0NDRELpeLDz/8MObPnx/du3eP448/Po488siKGlrZ8vl80i0AAAAAQFU0Nzcn3UJd2fxXE5JuoepmHXVq0i3U3A477BALFy6M3XffPYYOHRqDBg2KhoaGdl2zrDfKJ0+eHFdffXWcfPLJsffee0f37t2X+P6vf/1r3HDDDTF+/PhYc80145BDDmlXcyvL0g+QYrEYhUIhmpqalvn/0Hw+X9ZDRz7ZfFtrljfntN2DfGVrzDi9+WrVMOPa5ZPqyYwrz6exJzNOvoYZJ5tPY09mnHyNesjX04zT2FM95JOccS1qZC0PWXTmmWfG3nvvHWuttVbVrlnWRvl1110X5513Xuy9997L/H7LLbeMc889N/7+97/H7373u9RulAMAAAAAGVTKJd0BVXDwwQdX/ZplHSa+aNGimDNnzufmPvnkk+jWrVvFTQEAAAAAQK2U9Ub517/+9bj44otjwYIFsf/++8f666+/xPeFQiGuvfbaePrpp+PXv/51VRsFAAAAAICVoayN8tGjR0cul4vLL788LrnkkujevXt069YtOnToEHPmzIn58+fHNttsE9dff31sv/32K6tnAAAAAAComrI2ynO5XIwePTqOPvroePrpp2POnDmxYMGC6NSpU6yzzjrRu3fv2HTTTVdWrwAAAAAAFSuVku6AtFrhjfI33ngjevToERERa665Zuy5555lrQEAAAAAgDRaoR/zfPXVV+Ob3/xmPPDAAyt84cmTJ8eIESPio48+qrg5AAAAAABY2VZoo3yrrbaK66+/Pi666KI49dRT4913320z+8Ybb8Rxxx0Xt912W9x8883RrVu3qjULAAAAAADVtsJHrzQ1NcU999wTV199dQwdOjS23Xbb6NevX6y33npRKpXinXfeiSeeeCJef/31OProo+Ooo46K1VYr6wh0AAAAAICVxxnltCFXKpV/hP2nn34ajz32WMyYMSPefffd6NChQ6y33nqx7bbbxs4775zqDfJ8Pp90CwAAAABQFc3NzUm3UFc2u/7ipFuouteP/nHSLSTiww8/jIceeihmzZoVw4cPj2nTpsXGG28c2223XUXXq2hHu1OnTjFo0KAYNGhQRUWTtvQDpFgsRqFQiKampmhoaGiVz+fzZT105JPNt7VmeXNO2z3IV7bGjNObr1YNM65dPqmezLjyfBp7MuPka5hxsvk09mTGydeoh3w9zTiNPdVDPskZ16JG1vKQRYVCIY466qiYN29eLF68OIYMGRJTp06N3/3ud3H11VfHnnvuWfY1V+iMcgAAAAAASIPzzjsvBgwYEH/+85/jXwemnHnmmXHEEUfEZZddVtE1bZQDAAAAANlQyq16/2TQv94o7969+xKff/WrX42ZM2dWdE0b5QAAAAAA1I311lsvCoVCq8+fe+65WH/99Su6Znp/dRMAAAAAAJZy1FFHxfjx4+ONN96IXC4X//u//xt33XVX3HzzzfHjH1f246Y2ygEAAAAAqBuHH354dOnSJa688soolUoxadKk2HjjjeOss86K//iP/6jomjbKAQAAAACoKwcffHAcfPDB8fHHH0epVIo111yzXdezUQ4AAAAAZEKulHQHVFvXrl2rch0/5gkAAAAAQN34wx/+EK+99lpVr2mjHAAAAACAunHppZfGn/70p6peM1cqlTL1Fw7y+XzSLQAAAABAVTQ3NyfdQl3Z/JeXJN1C1c36/ilJt1BzEydOjOeeey4mT55ctWtm8ozypR8gxWIxCoVCNDU1RUNDQ6t8Pp8v66Ejn2y+rTXLm3Pa7kG+sjVmnN58tWqYce3ySfVkxpXn09iTGSdfw4yTzaexJzNOvkY95OtpxmnsqR7ySc64FjWylqdMmXpleNX1wx/+MH74wx/G6NGj4/TTT48NNtig3ddcqRvln376aXTq1GlllgAAAAAAIEP222+/iIiYPXt2PPzww7Heeust8f2UKVPKvmbZG+XvvfdefPnLX45nn302Vlut7eULFy6M4cOHx5gxY2KXXXYpuzEAAAAAAFjaCSecUPVrlr1R3rlz51i4cGEMGzYsvvCFL8Smm24aW221VfTv3z+am5tj9dVXj4iICy+8MGbMmBHrrrtu1ZsGAAAAACCbDj744Kpfs+KjV0aNGhXvvvtuvPPOO/HUU0/FddddF6VSKQ455JD47LPP4ve//31ccsklsfXWW1ezXwAAAACAypRySXdAlX3wwQdRKpWisbGxXddZoY3yUqkUM2fOjC233DIiInK5XHzta19bIrN48eK49NJL45e//GXkcrk4++yzY8iQIe1qDgAAAAAAlnb33XfH5ZdfHm+99VZERGyyySYxevToGDZsWEXXW6GN8ieeeCKOOuqo2GqrrWLAgAER8c8f6vzkk09i+vTpMXXq1HjggQfi7bffju985zuxePHi+PnPfx5f+cpXqvKLowAAAAAAEBFx1113xdixY+OAAw5oOa/88ccfj9NOOy0ioqLN8hXaKO/Tp0/86le/ihdffDH+/Oc/R+fOnWPHHXeMzz77LNZZZ53Yc88945hjjomvfOUr0bVr14iIeOedd+JHP/pR/OY3vym7KQAAAAAAWJarr746jj322Bg9enTLZwceeGBsuOGGceWVV668jfKuXbtGc3Nz7LzzzrHDDjvESy+9FA0NDXH77bfH1KlTY7311ov999+/Jb9gwYI4+eST49BDD41bbrklDj300LIbAwAAAACoqlLSDVANb7/9dgwcOLDV5wMHDozrr7++omvmSqXS5/7rUSwW48tf/nIMHTo0unfvHq+++mpcddVVccEFF8Qmm2wSjY2Ncd1118XQoUPjiCOOiNtvvz0effTRGDx4cGy++eax6667VtTcypDP55NuAQAAAACqorm5OekW6srmP5+YdAtVN+u4k5NuoeYOO+yw2GCDDWLSpEnRsWPHiPjnb2iOGTMm5syZE7/97W/LvuYKbZRHRLz++uvxq1/9KtZYY4146623YqONNoqpU6fGNddcExERgwcPjn79+sX06dOjc+fOMW7cuBg6dGjZDa1s+Xy+1QOkWCxGoVCIpqamaGhoWKE15daQr12+rTXLm3Pa7kG+sjVmnN58tWqYce3ySfVkxpXn09iTGSdfw4yTzaexJzNOvkY95OtpxmnsqR7ySc64FjWylqc8NspXDc8991wcddRRsd5660X//v0jIuLpp5+Od955J2644YbYbrvtyr5mhxUJvfTSS/H4449Hnz594v/+7//i7bffjjXWWCMOOOCA+Nvf/hYREZ06dYrJkyfHOeecE++//3688MILZTcDAAAAAADLs/3228ctt9wS/fr1ixdeeCGef/756NevX9x6660VbZJHrOAZ5fl8Pm644YZYbbXVYu7cufH3v/893n333WhsbIyXXnopXn/99ZbsokWLYqeddop77703dt5559hrr70qagwAAAAAoKqcUb7K2GqrrWLChAlVu94KbZR//etfj69//esxderU+Otf/xqXXnppfPLJJ3H++edHQ0NDXHrppfHss8/GN77xjejcuXMccMABsc4668T48eNjzz33jFwuV7WGAQAAAADItg8//DDefvvt6N27d7z11lvx//7f/4t999031l133Yqut0JHryxevDjOPPPMOPHEE+Mf//hH7LDDDnHyySfHMcccE2ussUaccsopLRvkr7/+euy2224xePDgyOVy8eCDD1bUGAAAAAAALO3FF1+M/fbbr+X3M99///2YMGFC7L///vHSSy9VdM0V2igvlUrRtWvXuOuuu6Jr166xYMGC+PrXvx577713nHvuufHpp5/GZ599FsOHD48HHnggNt5444iIGDJkSLzyyisVNQYAAAAAAEu76KKLon///nHmmWdGRESfPn1i6tSpsdNOO1V8HMsKHb3SsWPHOO200yIi4tBDD40DDjggIiJOOumkePPNN2PDDTeMq666KiIiOnfu3LLu29/+dnTv3r2ixgAAAAAAqsoZ5auEF198MX7xi18sccxK586d4/DDD4+RI0dWdM0VeqP833Xt2jXWWWediIjo1q1bbLPNNtG1a9f48pe/3CprkxwAAAAAgGrq1q1bvPzyy60+f/nll2PNNdes6Jor9Eb5v3z22Wfx+9//Pg488MDlFvz0009jyJAhcc0118QXv/jFihoDAAAAAIClfetb34qJEyfGvHnzYocddoiIiKeeeiquvfbaOO644yq6Zlkb5R06dIjzzz8/9t577+VulHfq1Cneeuut6NixY0VNAQAAAADAshx77LExb968uOqqq+Kzzz6LUqkUq6++ehx55JFx7LHHVnTNXKlUKutknt69e8ef/vSnuOeee+KFF16IQYMGxc477xyNjY2tcvfee29sscUWFTW2suTz+aRbAAAAAICqaG5uTrqFurL5VT9NuoWqm3X8SUm3kJhisRivvvpqRERsueWW0bVr14qvVdYb5RERuVwuIiLefPPNuO++++Luu++ODh06xFZbbRUDBw6MAQMGtLzunlZLP0CKxWIUCoVoamqKhoaGVvl8Pl/WQ0c+2Xxba5Y357Tdg3xla8w4vflq1TDj2uWT6smMK8+nsSczTr6GGSebT2NPZpx8jXrI19OM09hTPeSTnHEtamQtD1nW0NAQ2223XVWutcIb5R988EHMnTu35c+nn356nHrqqTFr1qyYPn16TJ8+PZ599tn4/e9/H5999lnLhjoAAAAAAFTLRx99FJMmTYqDDjoo+vbtGxMmTIhbb701evToERMnToxevXqVfc0OnxdYvHhx/OpXv4p99903Lr/88iW+69ixY2y55ZYxbNiwOPXUU+M3v/lNPPXUU/Gf//mfZTcCAAAAAACf5yc/+Uk88cQTsdZaa8UTTzwRN954Yxx++OEREXHBBRdUdM3P3Sjv0KFD/Pd//3ccdNBBceqpp0ZbR5rPmjUrfvvb38aZZ54ZO+64Y5s5AAAAAIAk5Eqr3j9Z9Oijj8bJJ58cW265ZTz22GMxePDgGDNmTJx44onx7LPPVnTNFTp65eabb47VVmsdfeqpp2LKlCnxwAMPxFtvvRWdO3eOfv36xd///veKmgEAAAAAgOXJ5XLRuXPniIh49tlnY7/99ouIiNVWWy1WX331iq65Qhvl/9okX7x4ceRyuVi0aFE8+eSTceSRR0avXr1i6NChsddee8V2221XcSMAAAAAAPB5Bg4cGOeee25st9128fTTT8e5554b7777bvzmN7+Jvn37VnTNFf4xz4iI+fPnR6lUigULFsSAAQPizjvvjN69e7fK/evYlUWLFlXUFAAAAAAALMtZZ50VZ599drzyyisxbty42GKLLeLCCy+Ml156Ka655pqKrrlCG+UvvPBCbLrpptHY2BhTp06NNddcM3K53DI3ySMiPvvssxg2bFjL6+8AAAAAAFAN6623Xlx99dVLfHbiiSfG6aefXvE1V2ij/Oyzz46ZM2fGV7/61dhvv/2isbHxc9d861vfijlz5sQbb7wRu+66a8UNAgAAAABURUZ//LKezZs3L9Zcc83Pza2xxhplr/l3udK/zklpQ6lUij/96U/x8MMPx/333x/vvvtu5HK5Jb5fboFcLgqFQllNrUz5fD7pFgAAAACgKpqbm5Nuoa5s8bOfJt1C1b12wklJt7DSvPLKK/Hd7343rrzyyth+++1XaM2DDz4Y5513Xtx9993RrVu3Fa71uRvl/27hwoVx//33x4033hjPPfdcHHDAATFmzJjYcMMNW2UXLVoUCxcujLlz58a66667wg2tbPl8vtUDpFgsRqFQiKampmhoaFihNeXWkK9dvq01y5tz2u5BvrI1ZpzefLVqmHHt8kn1ZMaV59PYkxknX8OMk82nsSczTr5GPeTracZp7Kke8knOuBY1spanPDbK68+jjz4ap556anzrW9+KY489Njp16rTM3Lx58+LSSy+Nhx56KK666qrYZpttyqpT1o95rrbaajF06NAYOnRoPPLII3HhhRfG/vvvHzfffHN88YtfXCLbsWPH6Nixo3PKAQAAAACoyB577BF33HFHXHzxxbHnnnvG4MGDo1+/frHeeutFqVSKd955J5544ol47LHHYujQoXHXXXeV9Sb5v5S1Uf7v9tprr9hpp53ipZdearVJDgAAAAAA1bDBBhvET3/605gzZ07cf//98eyzz7YcEb7++uvHwIED44wzzoh11lmn4hoVb5RHRPzjH/+Ivn37tucSAAAAAADwuTbYYIMYMWLESrl2h0oXLl68OH74wx/Gj370oyq2AwAAAAAAtVXxRvk111wTL774Yuyzzz7V7AcAAAAAAGqqoqNX7r333rjqqqvi0EMPjVwuF88++2xsuOGGsf7660eHDhXvvQMAAAAArDS5UtIdkFZlb5RPnjw5LrroojjwwAPjqKOOiq997WuRy+UiIqJDhw6x3nrrxYYbbhh9+/aNUaNGxZprrln1pgEAAAAAoFpWeKN81qxZcd5558Vf/vKX+P73vx9jxoyJmTNnRkTEww8/HH//+9/jnXfeiTlz5sRrr70Wv/vd7+LTTz+Ns846a6U1DwAAAAAA7bVCG+W33357jBs3Lpqbm+O3v/1tbLfddi3f5XK52HDDDWPDDTdcYk2HDh3ioYceslEOAAAAAECq5Uql0ueezPPGG2/EX//61/jyl7+8xOczZ86Mr33ta1EoFFqtufHGG6NYLMZxxx33uU1Mnjw5zj///CU+Gzt2bBx11FHx/PPPx09+8pOYOXNm7LbbbnH++efH2muv/bnXbEs+n694LQAAAACkSXNzc9It1JVel09KuoWqmzn6xKRbWCWs0BvlPXr0iB49epR14SOPPHKFs88880yMGjUqvv3tb7d8tsYaa8R7770XRx99dHzta1+Lyy67LP7rv/4rxo0bF1deeWVZvSxt6QdIsViMQqEQTU1N0dDQ0Cqfz+fLeujIJ5tva83y5py2e5CvbI0ZpzdfrRpmXLt8Uj2ZceX5NPZkxsnXMONk82nsyYyTr1EP+XqacRp7qod8kjOuRY2s5YHq6LCiwb/+9a/L/LxUKkW/fv3iG9/4RkyaNClee+21spt45plnYtddd41u3bq1/NOpU6e45ZZbokuXLnHGGWdEjx494tRTT40nn3wy/v73v5ddAwAAAAAAlmWFNsrfeeed+I//+I8YPnx4PPPMM62+Hzt2bGy33XZxzz33xP777x8TJ06MFTjRJSIi/v73v8fs2bPj3HPPjT59+sTgwYPjxhtvjIiI6dOnxy677BKrr756RER07tw5evfuHc8+++wK3h4AAAAAACzfCh29sv7668ekSZPi2muvjcMPPzyGDh0aZ555ZixatChyuVwceuihkcvl4vTTT4/f/OY3cdFFF8W7774bEyZM+Nxrv/TSS9GzZ8848cQTo6mpKf785z/HuHHjYrPNNou5c+dG7969l8h379495syZU9nd/v8Vi8Ul/jx//vwl/u+KrCm3hnxt88ta83lzTts9yJe/xozTna9GDTOubb4WNcy4uvla1DDj6uZrUcOMk83XooYZVzdfixppz9fbjGtRY1XLJz3jWtTIUn5ZxwizHCv2bi8ZtEI/5vnvHnzwwTjnnHMil8vFCSecEPfee2/84he/iE6dOrVk/vCHP8Spp54a55xzTnzzm98su6kf//jHsXDhwvjwww9jl112ie9973st351yyinRq1ev+MEPflD2dSP+ec7TggULylrTuXPnstbIJ5tPY0/y1c2nsSf55GvIVzefxp7kk68hX918GnuSr24+jT3JJ19Dvrr5NPYkn3yNrOX79++/wlkiel22Cv6Y54+y82Oef/3rX+PVV1+N7bffPjbccMOI+Od+9aeffhrbbrttbLbZZhVfu+yN8oiIDz/8MM4888x46KGH4pxzzomvf/3rrTLnnntu3HPPPXHvvffGOuusU9b1J06cGE8++WT07Nkz1l133TjttNNavjvuuONiwIABcfTRR5fbdkT8c6O8V69eS3w2f/78mDVrVmy++ebRpUuXVmtmzpzZas3yyCebb2vN8uactnuQr2yNGac3X60aZly7fFI9mXHl+TT2ZMbJ1zDjZPNp7MmMk69RD/l6mnEae6qHfJIzrkWNrOW9UV4eG+X16aOPPoqxY8fGlClTIpfLxS9/+cvYfffdIyLi8MMPj6effjpyuVzstdde8dOf/jS6du1ado0VOnplad27d48rrrgiJkyYEGeffXYMHDgwevbsuUTmmGOOibfeeis6d+683Gv97Gc/i86dO8cxxxzT8tm0adNio402in79+sWdd97Z8nmpVIoXX3wxDjjggErabtHWA6RLly5tflfuQ0c+2fzy1rQ157Tdg3zla8w4nflq1jDj2uRrUcOMq5uvRQ0zrm6+FjXMONl8LWqYcXXztahRL/l6mXEtaqyq+aRmXIsaWcvDqu6cc86JmTNnxnXXXRd9+vSJ7t27t3x30003xQcffBBPPfVUnHfeeXH++efH+PHjy65R0Ub5v5x66qmx5557ttokj4jYcMMN4+c//3nkcrnlXmO77baLk046KTbbbLPYdNNN44477ohnn302brzxxthiiy1i/Pjxcc8998TXvva1uPnmm+Ojjz6K3XbbrT1tAwAAAABZ5IzyuvTwww/H5Zdf3vIW+b/r0KFDrLPOOrHPPvtEqVSKsWPH1n6jPCJil112afO7z9skj4jYa6+9YsyYMXHBBRfEBx98ENtss03ceOONMWDAgIiIOP/881tu7oMPPohzzjlnif9iAAAAAADAqmuttdaKd99993Nzc+fOrfhvZLR7o7wajjjiiDjiiCOW+d1BBx0Uu+66azz//PPxpS99KXr06FHj7gAAAAAASMrw4cPj/PPPj48++ij222+/2GijjZb4ft68efHggw/G+PHjY/jw4RXVqOjHPOtZPp9PugUAAAAAqIrm5uakW6grvS5dBX/Mc8yq/2OeERG/+MUv4uc//3l88sknscYaa0S3bt2iY8eO8fHHH8dHH30UpVIpDj300PjJT34SHTp0KPv6qXijvNaWfoAUi8UoFArR1NS0zFfz8/l8WQ8d+WTzba1Z3pzTdg/yla0x4/Tmq1XDjGuXT6onM648n8aezDj5GmacbD6NPZlx8jXqIV9PM05jT/WQT3LGtaiRtTzlyWXqleFVy7HHHhtHHHFEPPbYY/HKK6/E+++/H59++mk0NDTE5ptvHrvvvntsttlmFV8/kxvlAAAAAADUlzXXXDP222+/2G+//ap+7fLfQQcAAAAAgFWIjXIAAAAAADLN0SsAAAAAAKTWlClTysoPHjy47Bo2ygEAAACAbPBjnnXp+OOPX+FsLpeLQqFQdg0b5QAAAAAApFa5b5RXol0b5S+++GKcffbZ8dJLL8WiRYtafV/Jzj0AAAAAAPzLJptsstJrtGujfOzYsRERMXHixFhnnXWq0hAAAAAAANRSrlQqVXwyT9++feOaa66JXXbZpZo9rVT5fD7pFgAAAACgKpqbm5Nuoa5sOXFS0i1U3V9PPjHpFlYJ7XqjvLm5OV544YW62iiPaP0AKRaLUSgUoqmpKRoaGlrl8/l8WQ8d+WTzba1Z3pzTdg/yla0x4/Tmq1XDjGuXT6onM648n8aezDj5GmacbD6NPZlx8jXqIV9PM05jT/WQT3LGtaiRtTxQHR3as/i8886L3//+93HDDTfEp59+Wq2eAAAAAACgZtr1RvnRRx8dxWIxxo8fHxdffHGsv/760aHD/7f3XotfIwUAAAAAgPZo10b5CSecUK0+AAAAAABWqlzFv9bIqq5dG+UHH3xwtfoAAAAAAIBEtOuMcgAAAAAAqHft2igfNmxY3HXXXdXqBQAAAAAAaq5dR680NjbGK6+8Uq1eAAAAAABWnlIu6Q5IqXa9UT569Oi47bbbIp/PV6sfAAAAAACoyCOPPFLRuna9Uf7GG2/EV7/61Tj88MPj0EMPjT59+izx/UEHHdSeywMAAAAAwBJOPvnkmDBhQnTs2LHls1dffTXGjx8ff/nLX2L69OllXzNXKpVKlTY0aNCgti+cy8WUKVMqvfRK4+13AAAAAFYVzc3NSbdQV7a6+NKkW6i6V388JukWam6fffaJXr16xRVXXBHz5s2Lyy+/PG699dbYeeed46STToptttmm7Gu2643yhx56qD3LE7P0A6RYLEahUIimpqZoaGholc/n82U9dOSTzbe1ZnlzTts9yFe2xozTm69WDTOuXT6pnsy48nwaezLj5GuYcbL5NPZkxsnXqId8Pc04jT3VQz7JGdeiRtbylKniV4ZJk5tvvjm+//3vxze+8Y146623YvPNN4/rr78+dt5554qv2a4zygEAAAAAoJbWXXfdmDx5cnTr1i3WWGON+MUvftGuTfKIdr5Rfueddy73e2eUAwAAAADQHlOnTl3m58ccc0ycf/75MWLEiBg3blystto/t7sHDBhQdo12bZRfccUVLf+7VCrFu+++G4sWLYouXbpEY2OjjXIAAAAAANplxIgRn5v5zne+ExH//O3MQqFQdo2qnlG+aNGiuP/++2PSpElx8cUXt+fSAAAAAABVlXNGeV166aWXVnqNdm2UL61jx44xZMiQ2GijjeL888+PW2+9tZqXBwAAAACAqqvqRvm/NDc3x2uvvbYyLg0AAAAAQIbl8/k455xz4qWXXopFixa1+r7mR68s6xD1+fPnxx133BGbbLJJey4NAAAAAACtnH766RERMXHixFhnnXWqcs12bZQv6xD1jh07xpe+9KW48MIL23NpAAAAAIDqckb5KuFvf/tbXHPNNbHLLrtU7Zq5UqmUqX898vl80i0AAAAAQFU0Nzcn3UJd2Xr8pUm3UHWvjB2TdAs1N3z48Nhzzz3jmGOOqdo1V8oZ5Wm39AOkWCxGoVCIpqamaGhoaJXP5/NlPXTkk823tWZ5c07bPchXtsaM05uvVg0zrl0+qZ7MuPJ8Gnsy4+RrmHGy+TT2ZMbJ16iHfD3NOI091UM+yRnXokbW8pBF5513Xnz/+9+Pzp07x7e+9a3o1KlTu6/ZoT2L77zzzvjwww9bfX7jjTfG9773vfZcGgAAAAAAWjn66KNj3rx5MX78+OjXr1985StficGDB7f8U4l2vVE+duzYuPXWW6N79+5LfN7U1BSXXHJJey4NAAAAAACtnHDCCVW/Zrs2ykulUuRyuVaff/DBB602zwEAAAAAkpTL1K81rroOPvjgql+z7I3yO+64I+64446WP5955pnRtWvXlj8vWrQopk+fHscdd1x1OgQAAAAAgH/z5ptvRj6fj08++aTls4ULF8a0adNi/PjxZV+v7I3yTTbZJAYOHBgREU8++WRsu+22sf7667d836lTp/jhD38YO+20U9nNAAAAAADA8txzzz1x6qmnRqlUilKpFBtuuGF8+OGHUSwWK/4x3LI3ygcOHNiyUX7llVfGN7/5zdh2220rKg4AAAAAAOW48sor4wc/+EEceuihMWjQoPj1r38dnTt3jiOOOCKGDh1a0TU7tKehAQMGLHHsCgAAAABAapVWwX8y6O23345ddtkl1l9//dh6663j2WefjXXWWSd+8IMfxOTJkyu6Zrs2yidPnhybb755ey4BAAAAAAArbJNNNok//elPERHRv3//mDp1akRErLfeevH+++9XdM2yj15Z2uOPPx533XVXvP766zF+/Pi4++67Y6211opvf/vb7b00AAAAAAAs4dhjj41TTz01evToEfvtt18cddRRMX/+/HjmmWcqPqM8VyqVKn5B/5577omTTjopttlmmygUCnHHHXfEtGnT4uKLL47Ro0fHd7/73UovvdLk8/mkWwAAAACAqqh0UzCrvnjBpUm3UHUvnzEm6RYSMW3atOjatWv07t07brrpprjtttuisbExxo0bF7169Sr7eu16o/yqq66KkSNHxg9/+MPo3bt3REQcccQR0bFjx7juuutSuVEeEXHAC38oK/+HPgeU9dDJ5/PyCebbWlMsFqNQKERTU1M0NDTUtCf56ubbWmPG6c1Xq4YZ1y6fVE9mXHk+jT2ZcfI1zDjZfBp7MuPka9RDvp5mnMaeapEfs91PVjh/6fNnlz3jI698YIWvf+Oor1Y0s23WPWSF89Pfuz3O+ODnK5y/YO3jUjezcvPTOh+zwvnm+PMKZ4nMnum9Kurfv3/L/x4+fHgMHz68Xddr1xnlb731Vuy2226tPu/Vq1e888477bk0AAAAAABERMTcuXPjrrvuimuvvTZuueWWmDNnTqvM22+/Heeee25F12/XG+Vf+tKX4u67727Zvc/lchER8cc//rHlDXMAAAAAAKjUzJkzY/jw4fH+++9Hly5dYv78+dGpU6eYNGlS7L333vG3v/0tfvGLX8Rdd90V66yzTpx11lll12jXRvnJJ58cRx99dDz33HORy+XiyiuvjNmzZ8fLL78c1113XXsuDQAAAAAAMXHixFh77bXjxhtvjK222irmzp0bP/nJT+Lss8+O//mf/4l777031l133Tj11FPjm9/8ZkU12rVRPnDgwPjDH/4Q1157beRyuXjrrbfii1/8YlxyySUVHZgOAAAAALCy5JxRXpeefvrpOPvss2OrrbaKiIi11lorTj/99Nh1113j6aefjtNPPz0OPfTQ6NSpU8U12rVRHhGxxRZbxPjx49t7GQAAAAAAaOWDDz6ITTfddInP1llnnYiIuPrqq6tyDHi7N8oBAAAAAGBlmjVrVnTo0KHV5zNnzoxFixYt8dm2225b9vXbtVF+1llnxdChQ2PnnXduz2UAAAAAAKBNP/7xj5f5+Yknnhi5XC4iIkqlUuRyuSgUCmVfv10b5c8880xsuummNsoBAAAAAOrMe++9F2eddVb85S9/iS222CIuvPDCFTrGZNGiRTFx4sS4/fbbo6GhIU455ZQYOnRoy/cXXXRR/Nd//dcSa6666qrYe++9K+rzxhtvrGhdOdq1Uf7tb387rr322jjssMOiW7du1eoJAAAAAICVqFQqxahRoyIi4tZbb43nn38+Ro4cGXfffXd07dp1uWsvu+yyuOuuu+JnP/tZrL766nHCCSfEZptt1nLkyTPPPBMXXHBB7LPPPi1runTpUnGvAwcOrHjtisqVSqWKf+t19uzZcd1118Wf/vSnOPHEE6NPnz5LfL/xxhu3u8Fqy+fzSbcAAAAAAFXR3NycdAt15UvnXZp0C1U348wxFa2bNm1aHH744XHPPffEVlttFRERRx99dBxwwAFx4IEHtrnu008/jZ122ilOOeWUOPzwwyMi4tprr43XX389Lrjggvj000+jf//+8cADD8SGG25YUW9JaNcb5YMGDWr536NHj67KWTC1sPQDpFgsRqFQiKampmhoaGiVz+fzZT105JPNt7VmeXNO2z3IV7bGjNObr1YNM65dPqmezLjyfBp7MuPka5hxsvk09mTGydeoh3w9zTiNPdVDPskZ16JG1vIwePDg5X4/ZcqUZX4+ffr02HjjjVs2ySMi+vXrF88999xyN8pfe+21KBaLscceeyyx7g9/+ENERDz//PMR8c9N97/97W/Rs2fPGDlyZHzta19b4XtKQrs2ytv6fzIAAAAAQOpUfLbGqmfu3LnRs2fPJT7r3r375778PHfu3OjYsWP06NGj5bNu3brFnDlzIiLilVdeia222ipOP/302GyzzeLuu++Ok08+Obbaaqv40pe+VP0bqZJ2bZRvsskm1eoDAAAAAIAyVfoy82qrrRadO3de4rM11lgjisXi567r1KnTEp916dKlZd23vvWt+Na3vtXy3dFHHx0PPfRQ/PGPf0z1RnmHlXnxxYsXx8CBA+OVV15ZmWUAAAAAAChDY2NjvPfee0t8Nm/evFab4MtaN3/+/Jg3b17LZ3Pnzl3uuvXXXz/efPPN9jW8kq3UjfJSqRQfffRRLFq0aGWWAQAAAACgDH379o1XXnklPvroo5bPXnjhhdhoo42Wu65Hjx6x7rrrxrRp05a57vTTT285rzwiYuHChfHcc8997nWTtlI3ygEAAAAA0iJXWvX+qdTWW28dvXr1ikmTJsXixYvjxRdfjPvvvz8GDRoUixcvbvMF6A4dOsTQoUPjiiuuiHnz5sX//d//xa9+9asYNGhQREQ0NzfHT3/603j00Ufj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xp7MOPkaZpxsPo09mXHyNeohX08zTmNP9ZBPcsa1qJG1PGTRlClTqn7Ndm2Ut+XAAw+M8ePHr4xLAwAAAACQYUu/wF0NHap9wQULFsTNN9+8zDezAQAAAACgPY455ph48MEHq3rNdr1R3rt372X+mOfqq68e5557bnsuDQAAAABQXRX/WiNp8sEHH8RLL70Ue++9d9Wu2a6N8htvvLHVZx07doxevXpFY2Njey4NAAAAAACtHHvssXHOOefEQQcdFJtuumlVrtmujfKBAwdWpQkAAAAAAFgR3bp1i7333jsOPfTQ+P73vx99+vRZ4vsBAwaUfc12bZTPnj071l9//VhttSUvc88998SsWbPi+OOPb8/lAQAAAABgCSNGjGj53xdffPES3+VyuSgUCmVfs10b5YMHD45bb701tt122yU+7969e1x//fU2ygEAAACA1Mg5o3yV8NJLL1X9mh3as7hUWva/WZ06dYoOHdp1aQAAAAAAqImy3yh/8skn48knn2z58+9+97tYf/31W/68aNGieOCBB+IrX/lKdToEAAAAAIAV8Oabb1b0A5+5UluvhbfhjjvuiNtvvz0iIqZOnRrbbLNNdO3ateX7zp07x/bbbx/f+973okuXLmU39Morr8Shhx4aN9xwQ2y//fYREfHII4/EhAkTYs6cOTFkyJA488wzo3PnzmVfOyIin89XtA4AAAAA0qa5uTnpFurKtmMvTbqFqntx/JikW6i5Dz74ICZOnBjPPvtszJ8/v+XzRYsWxbvvvhsvvvhi2dcse6P83/Xu3Ttuu+22VmeUV+qzzz6Lb37zmzFgwIAYO3ZsRETMmDEj/uM//iNGjhwZw4YNi4svvjg23njjlu/Llc/nWz1AisViFAqFaGpqioaGhhVaU24N+drl21qzvDmn7R7kK1tjxunNV6uGGdcun1RPZlx5Po09mXHyNcw42XwaezLj5GvUQ76eZpzGnuohn+SMa1Eja3nKY6N81fCjH/0oXnnlldh5553jt7/9bZx99tnxt7/9Lf7zP/8zTjvttDjyyCPLvmaqDhK/5ppr4qOPPoof/ehHLZ9Nnjw5mpqaYuTIkdGjR48YN25c3HLLLbFgwYLkGgUAAAAA6k9pFfwng/7yl7/E6aefHmeeeWZ07949tthiizj55JPj8MMPj0cffbSia7Zro/yll16q2tvk+Xw+fvGLX8RXv/rVuPfee2PWrFkRETF9+vTYY489WnIbbLBBNDY2xssvv1yVugAAAAAA1Jd/HZSy/fbbtxy1su+++8ZTTz1V0fXK/jHPf3fllVcu9/tRo0at0HVKpVKcffbZ0bVr18jlcjFjxoyYMGFC/OAHP4i5c+dGz549l8h379495syZE3369Kmo72KxuMSf/3WOzb+fZ/N5a8qtIV/b/LLWfN6c03YP8uWvMeN056tRw4xrm69FDTOubr4WNcy4uvla1DDjZPO1qGHG1c3Xokba8/U241rUWNXySc+4FjWylF/WMcKwqtt1113jwgsvjMsuuyx23nnnuPPOO2OfffaJxx9/fInf0yxHu84oHzFiRMv/LpVK8fbbb8dbb70VjY2NsfXWW8eNN964Qtd56qmn4ogjjoif//zn8ZWvfCUiIh544IEYPXp0bLzxxnHSSSfFkCFDWvKHH354HHbYYXHAAQeU3XM+ny/72JbOnTuXtUY+2Xwae5Kvbj6NPcknX0O+uvk09iSffA356ubT2JN8dfNp7Ek++Rry1c2nsSf55GtkLd+/f/8VzhKx7Wmr4BnlF2XvjPJ//OMfccopp8SwYcPiq1/9ahx44IExe/bsiIgYPXp0HHfccWVfs10b5cvy/PPPx+mnnx5jxoyJwYMHr9Cau+++O0477bR4/vnno2PHjhERMWfOnNhzzz2jQ4cOcfrppy+xKT9s2LA4/vjjY7/99iu7v3w+H7169Vris/nz58esWbNi8803jy5durRaM3PmzFZrlkc+2Xxba5Y357Tdg3xla8w4vflq1TDj2uWT6smMK8+nsSczTr6GGSebT2NPZpx8jXrI19OM09hTPeSTnHEtamQt743y8mx76iq4UT4hexvlS/v444/j8ccfj8bGxthhhx0quka7jl5Zlu222y6uuOKK+OEPf7jCG+Ubb7xxLF68OD755JOWV+PffPPNiIg46KCDYtq0aS0b5R9//HG89tprsfHGG1fcY1sPkC5durT5XbkPHflk88tb09ac03YP8pWvMeN05qtZw4xrk69FDTOubr4WNcy4uvla1DDjZPO1qGHG1c3Xoka95OtlxrWosarmk5pxLWpkLQ9Z17Vr1xXei25Lu37Msy0bbbRRvP322yuc33777WOLLbaIs846K95444148cUX44ILLohdd901RowYEQ8++GBM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I0bEyJEj4+OPP/7ctZdddlncdddd8bOf/SwmTZoUF154Ybz44osREbFgwYI4+uijo1u3bnHXXXfFbrvtFqNGjYrFixev7Ftql1RslC/tqquuiv322y+23HLLmD59euyxxx4t322wwQbR2NgYL7/8coIdAgAAAAB1p7QK/lOhp59+Op555pk4//zzY8stt4yDDz44tthii3jwwQeXu+7TTz+Nm266KUaNGhUDBw6Mfv36xZFHHhk333xzRETcd9998d5778X5558fPXv2jGOPPTbmz58fTz/9dOXN1kAqjl75d++9917cddddcccdd0RExNy5c6Nnz55LZLp37x5z5syJPn36VFSjWCwu8ef58+cv8X9XZE25NeRrm1/Wms+bc9ruQb78NWac7nw1aphxbfO1qGHG1c3XooYZVzdfixpmnGy+FjXMuLr5WtRIe77eZlyLGqtaPukZ16JGlvLLOkaYbBk8ePByv58yZcoyP58+fXpsvPHGsdVWW7V81q9fv3juuefiwAMPbPN6r732WhSLxSVebu7Xr1/84Q9/aLlunz59orGxseX7vn37xnPPPRc77rjjCt1TElLxY57/7rLLLosZM2bENddcExER++23X4wePTqGDBnSkjn88MPjsMMOiwMOOKDs6+fz+bKPbencuXNZa+STzaexJ/nq5tPYk3zyNeSrm09jT/LJ15Cvbj6NPclXN5/GnuSTryFf3Xwae5JPvkbW8v3791/hLBEDv73q/ZjnWm/es9zv29oov/rqq+OJJ56IG264oeWzyZMnx+OPPx5XXXVVm9d76qmn4sgjj4zp06e3fDZjxowYPnx4TJ06Nc4444wolUpx4YUXtnx/0UUXxeLFi+P0009f0duquVS9Ub548eK444474owzzmj5rLGxMd57770lcnPnzo1OnTpVXKepqWmJP8+fPz9mzZoVm2++eXTp0qVVfubMma3WLI98svm21ixvzmm7B/nK1phxevPVqmHGtcsn1ZMZV55PY09mnHwNM042n8aezDj5GvWQr6cZp7GnesgnOeNa1MhaHtraCP88q622WnTu3HmJz9ZYY43P/RsNq622Wqu92S5durSs69ixY3Ts2LHVdZfe402bVG2U/+Uvf4mPP/44vvzlL7d81rdv35g2bVqMGDEiIiI+/vjjeO2119r1K6lt/ZWULl26tPlduX+NRT7Z/PLWtDXntN2DfOVrzDid+WrWMOPa5GtRw4yrm69FDTOubr4WNcw42XwtaphxdfO1qFEv+XqZcS1qrKr5pGZcixpZy7Picqk6WyNZy3pBed68eZ/7gnJjY2PMnz8/5s2bF2uuuWZELPlic2NjY8ycObPs6yYtVT/mOWXKlBg4cOAS/08bNmxYPPjggzF16tSIiLjyyiujsbExmpubk2oTAAAAAKCu9e3bN1555ZX46KOPWj574YUXYqONNlruuh49esS6664b06ZNW+a6fv36xTPPPBOLFi0q67pJS9VG+aOPPhoDBw5c4rNtttkmTjjhhDjqqKNit912i9/+9rdxwQUXRIcOqWodAAAAAKBubL311tGrV6+YNGlSLF68OF588cW4//77Y9CgQbF48eL46KOPltjs/pcOHTrE0KFD44orroh58+bF//3f/8WvfvWrGDRoUERE7LbbbrFw4cK4/vrrIyLi4Ycfjueff77l+7RK3Y95tuWNN96IGTNmRJ8+fWKDDTao+Dr5fL6KXQEAAABAcpy6UJ6djlz1fszziRtPrHjt9OnT49hjj43PPvss5s6dGwceeGBceOGF8eabb8bgwYPjzjvvXOaZ+XPnzo3vf//78fLLL0epVIrNNtssbrrpppajWB577LEYM2ZMrL766vHBBx/EyJEjY9SoURX3WQt1s1FeLfl8vtUDpFgsRqFQiKampmWeAbWsNeXWkK9dvq01y5tz2u5BvrI1ZpzefLVqmHHt8kn1ZMaV59PYkxknX8OMk82nsSczTr5GPeTracZp7Kke8knOuBY1spanPDuNWAU3yidXvlEe8c9nwtSpU6OxsTG22267FV63ePHimDZtWnz66acxcODAWH311Zf4/oMPPohp06ZFz549Y+utt25Xj7WQqh/zBAAAAACgdhoaGmKvvfYqe12HDh1iwIABbX6/9tprx+DBg9vTWk056BsAAAAAgEyzUQ4AAAAAQKbZKAcAAAAAINOcUQ4AAAAAZEKulHQHpJU3ygEAAAAAyDQb5QAAAAAAZJqNcgAAAAAAMs0Z5QAAAABANpQcUs6y5UqlbP3bkc/nk24BAAAAAKqiubk56Rbqys5H/DTpFqru8V+flHQLq4RMvlG+9AOkWCxGoVCIpqamaGhoaJXP5/NlPXTkk823tWZ5c07bPchXtsaM05uvVg0zrl0+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t7BKSPzHPN98880444wzYtKkSdG7d+945pln4pRTTonNN988Nthggzj++ONj5MiRMWzYsLj44otj0qRJMXbs2HbVXPoBUiwWo1AoRFNTUzQ0NLTK5/P5sh468snm21qzvDmn7R7kK1tjxunNV6uGGdcun1RPZlx5Po09mXHyNcw42XwaezLj5GvUQ76eZpzGnuohn+SMa1Eja3nKtDjpBkirxI9eeeGFF6Jnz56xxx57xHrrrRf77LNPbLnlljFz5syYPHlyNDU1xciRI6NHjx4xbty4uOWWW2LBggVJtw0AAAAAwCoi8Y3yrbfeOl555ZV4+OGHY/78+XH//ffHq6++GrvvvntMnz499thjj5bsBhtsEI2NjfHyyy8n2DEAAAAAAKuSxI9e2WqrreKYY46J4447ruWzs88+O7baaquYO3du9OzZc4l89+7dY86cOdGnT5+KaxaLxSX+PH/+/CX+74qsKbeGfG3zy1rzeXNO2z3Il7/GjNOdr0YNM65tvhY1zLi6+VrUMOPq5mtRw4yTzdeihhlXN1+LGmnP19uMa1FjVcsnPeNa1MhSflnHCAPlS/zHPAuFQhxxxBExYcKE2GOPPSKfz8fJJ58cp556alx++eUxevToGDJkSEv+8MMPj8MOOywOOOCAiurl8/myj27p3LlzWWvkk82nsSf56ubT2JN88jXkq5tPY0/yydeQr24+jT3JVzefxp7kk68hX918GnuST75G1vL9+/df4SwRgweNT7qFqpvyUPt+z5F/Snyj/KKLLoo333wzrrzyypbPrr322nj00Udj4cKFMXTo0BgxYkTLd8OGDYvjjz8+9ttvv4rq5fP56NWr1xKfzZ8/P2bNmhWbb755dOnSpdWamTNntlqzPPLJ5ttas7w5p+0e5CtbY8bpzVerhhnXLp9UT2ZceT6NPZlx8jXMONl8Gnsy4+Rr1EO+nmacxp7qIZ/kjGtRI2t5b5SXx0Y5bUn86JWFCxfGe++9t8Rn7733XixevDj69u0b06ZNa9ko//jjj+O1116LjTfeuF0123qAdOnSpc3vyn3oyCebX96atuactnuQr3yNGaczX80aZlybfC1qmHF187WoYcbVzdeihhknm69FDTOubr4WNeolXy8zrkWNVTWf1IxrUSNreaD9Ev8xz759+8azzz4bEydOjP/+7/+Oyy67LG6++ebYd999Y9iwYfHggw/G1KlTIyLiyiuvjMbGxmhubk64awAAAAAAVhWJv1G+//77x/vvvx8333xz3HDDDbHWWmvFiBEjYvjw4dGhQ4c44YQT4qijjoq11147isViXH755dGhQ+L7+wAAAABAvUn0EGrSLPEzylfEG2+8ETNmzIg+ffrEBhts0K5r5fP5KnUFAAAAAMly8kJ5Bn9lFTyj/GFnlFdD4m+Ur4gePXpEjx49qna9pR8gxWIxCoVCNDU1LfMMqHw+X9ZDRz7ZfFtrljfntN2DfGVrzDi9+WrVMOPa5ZPqyYwrz6exJzNOvoYZJ5tPY09mnHyNesjX04zT2FM95JOccS1qZC0PVIczTAAAAAAAyLS6eKMcAAAAAKDd0n8KNQnxRjkAAAAAAJlmoxwAAAAAgEyzUQ4AAAAAQKY5oxwAAAAAyIScI8ppgzfKAQAAAADINBvlAAAAAABkmo1yAAAAAAAyLVcqlTJ1Mk8+n0+6BQAAAACoiubm5qRbqCt773lB0i1U3YP/e0bSLawSMvljnks/QIrFYhQKhWhqaoqGhoZW+Xw+X9ZDRz7ZfFtrljfntN2DfGVrzDi9+WrVMOPa5ZPqyYwrz6exJzNOvoYZJ5tPY09mnHyNesjX04zT2FM95JOccS1qZC0PVIejVwAAAAAAyDQb5QAAAAAAZFomj14BAAAAALIntzjpDkgrb5QDAAAAAJBpNsoBAAAAAMg0G+UAAAAAAGSajXIAAAAAADLNj3kCAAAAANlQKiXdASnljXIAAAAAADItVypl6z+j5PP5pFsAAAAAgKpobm5OuoW68tXdzk+6hap74E/jkm5hlZDJo1eWfoAUi8UoFArR1NQUDQ0NrfL5fL6sh458svm21ixvzmm7B/nK1phxevPVqmHGtcsn1ZMZV55PY09mnHwNM042n8aezDj5GvWQr6cZp7GnesgnOeNa1MhaHqiOTG6UAwAAAAAZlKmzNSiHM8oBAAAAAMg0G+UAAAAAAGSajXIAAAAAADLNGeUAAAAAQCbkSg4pZ9m8UQ4AAAAAQKbZKAcAAAAAINNslAMAAAAAkGm5UilbB/Pk8/mkWwAAAACAqmhubk66hbqyz87nJt1C1d3/+FlJt7BKyOSPeS79ACkWi1EoFKKpqSkaGhpa5fP5fFkPHflk822tWd6c03YP8pWtMeP05qtVw4xrl0+qJzOuPJ/Gnsw4+RpmnGw+jT2ZcfI16iFfTzNOY0/1kE9yxrWokbU8UB2OXgEAAAAAINNslAMAAAAAkGk2ygEAAACAbFi8Cv6zEjzyyCMxdOjQ6N+/f4wbNy4WLFiwwmtnzZoVI0aMiH79+sWRRx4Zs2fPbvmuVCrFTjvtFF/60pda/tlxxx1Xxi2UzUY5AAAAAAARETFjxow4/vjjY//9948777wzPvzww5g0adIKrV2wYEEcffTR0a1bt7jrrrtit912i1GjRsXixf/c0X/ttddiwYIF8fjjj8fUqVNj6tSp8fDDD6/M21lhNsoBAAAAAIiIiMmTJ0dTU1OMHDkyevToEePGjYtbbrllhd4qv+++++K9996L888/P3r27BnHHntszJ8/P55++umIiHjmmWeiX79+0djYGN26dYtu3brFWmuttbJvaYWslnQDAAAAAABUZvDgwcv9fsqUKWVdb/r06fHlL3+55c8bbLBBNDY2xssvvxx9+vT53LV9+vSJxsbGls/69u0bzz33XOy4447x9NNPx+uvvx677LJLzJ8/P3beeec444wzokePHmX1uDLYKAcAAAAAMiFXKiXdQiqMGTMmHn300WV+17179+jZs2erz+bMmfO5G+Vz585tc23EP49e2XPPPeO73/1ufPbZZ3HeeefFmDFj4tZbb23H3VSHjXIAAAAAgDpV7hvjERHjxo2L+fPnL/O77373u9G5c+clPltjjTWiWCx+7nU7duwYHTt2bLX2vffei4iIm2++eYnvzjvvvNh7771j5syZ0atXr3JuoepslAMAAAAAZMgXvvCF5X73r43tf5k7d2506tTpc6/b2NgYM2fOXOKzefPmtbl2/fXXj4iIN998M/GN8lyplK2/b5DP55NuAQAAAACqorm5OekW6sq+A36SdAtVd9/Us6t6vQkTJsTbb78dl112WUREfPzxx7HTTjvFzTffHNttt91y1/6///f/Yty4cfHII4+0vFn+zW9+M/bee+845JBDYvjw4XHbbbdFQ0NDRET8+c9/ju985ztxzz33xFZbbVXV+yhXJt8oX/oBUiwWo1AoRFNTU8uQ/l0+ny/roSOfbL6tNcubc9ruQb6yNWac3ny1aphx7fJJ9WTGlefT2JMZJ1/DjJPNp7EnM06+Rj3k62nGaeypHvJJzrgWNbKWp0zZeme4IsOGDYtvfOMbMXXq1BgwYEBceeWV0djY2PLv5YIFC+Kzzz6LNddcs9Xa3XbbLRYuXBjXX399HHPMMfHwww/H888/HxdeeGF84QtfiDXXXDPOOOOMOProo+Pdd9+N8847L3bdddfEN8kjMrpRDgAAAABAa9tss02ccMIJcdRRR8Xaa68dxWIxLr/88ujQoUNERFx77bXx4IMPxl133dVq7eqrrx4TJ06MMWPGxK9+9av44IMP4vjjj48tt9wyIiKuuOKKGDduXBx++OHRrVu3GDZsWIwaNaqm99cWG+UAAAAAALQ49thjY+jQoTFjxozo06dPbLDBBi3fnXDCCXHCCSe0uXb33XePBx54IKZNmxY9e/aMrbfeuuW7jTbaKK6//vqV2nulbJQDAAAAALCEHj16RI8ePSpau/baa8fgwYOr3NHK1SHpBgAAAAAAIEneKAcAAAAAssGPedIGb5QDAAAAAJBpNsoBAAAAAMg0G+UAAAAAAGRarlTK1sE8+Xw+6RYAAAAAoCqam5uTbqGu7Nvv7KRbqLr7nvlJ0i2sEjL5Y55LP0CKxWIUCoVoamqKhoaGVvl8Pl/WQ0c+2Xxba5Y357Tdg3xla8w4vflq1TDj2uWT6smMK8+nsSczTr6GGSebT2NPZpx8jXrI19OM09hTPeSTnHEtamQtD1SHo1cAAAAAAMg0G+UAAAAAAGRaJo9eAQAAAACyJ5etn2ukDN4oBwAAAAAg02yUAwAAAACQaTbKAQAAAADINGeUAwAAAADZ4Ixy2uCNcgAAAAAAMs1GOQAAAAAAmZYrlbL19w3y+XzSLQAAAABAVTQ3NyfdQl3Zb/szk26h6v7nufOSbmGVkMkzypd+gBSLxSgUCtHU1BQNDQ2t8vl8vqyHjnyy+bbWLG/OabsH+crWmHF689WqYca1yyfVkxlXnk9jT2acfA0zTjafxp7MOPka9ZCvpxmnsad6yCc541rUyFqeMmXrnWHK4OgVAAAAAAAyzUY5AAAAAACZZqMcAAAAAIBMy+QZ5QAAAABABjmjnDZ4oxwAAAAAgEyzUQ4AAAAAQKbZKAcAAAAAINOcUQ4AAAAAZMPipBsgrXKlUrZOsM/n80m3AAAAAABV0dzcnHQLdWW/bc9IuoWq+58XL0i6hVVCJt8oX/oBUiwWo1AoRFNTUzQ0NLTK5/P5sh468snm21qzvDmn7R7kK1tjxunNV6uGGdcun1RPZlx5Po09mXHyNcw42XwaezLj5GvUQ76eZpzGnuohn+SMa1Eja3mgOpxRDgAAAABApmXyjXIAAAAAIHty2TqFmjJ4oxwAAAAAgEyzUQ4AAAAAQKbZKAcAAAAAINNslAMAAAAAkGl+zBMAAAAAyAY/5kkbvFEOAAAAAECm2SgHAAAAACDTcqVStv6+QT6fT7oFAAAAAKiK5ubmpFuoK0OaxibdQtXdWxifdAurhEyeUb70A6RYLEahUIimpqZoaGholc/n82U9dOSTzbe1ZnlzTts9yFe2xozTm69WDTOuXT6pnsy48nwaezLj5GuYcbL5NPZkxsnXqId8Pc04jT3VQz7JGdeiRtbylGlxpt4ZpgyOXgEAAAAAINNslAMAAAAAkGk2ygEAAAAAyLRMnlEOAAAAAGRQyRnlLJs3ygEAAAAAyDQb5QAAAAAAZJqNcgAAAAAAMs0Z5QAAAABANjijnDZ4oxwAAAAAgEzLlUrJ/2eUxx57LK6++up46aWXYuONN44f/OAH8bWvfS0iIh555JGYMGFCzJkzJ4YMGRJnnnlmdO7cueJa+Xy+Wm0DAAAAQKKam5uTbqGuDNn6x0m3UHX3vnJx0i2sEhI/eqVQKMRxxx0Xp512WvzsZz+Lxx57LE499dRYvHhxfPGLX4zjjz8+Ro4cGcOGDYuLL744Jk2aFGPHjm1XzaUfIMViMQqFQjQ1NUVDQ0OrfD6fL+uhI59svq01y5tz2u5BvrI1ZpzefLVqmHHt8kn1ZMaV59PYkxknX8OMk82nsSczTr5GPeTracZp7Kke8knOuBY1spYHqiPxjfLbbrst+vfvH8OHD4+IiAMPPDCmTJkS//3f/x1PPPFENDU1xciRIyMiYty4cTFkyJA48cQT2/VWOQAAAACQQckfrkFKJX5G+fvvvx+bbLLJEp+tvvrq0bFjx5g+fXrsscceLZ9vsMEG0djYGC+//HKt2wQAAAAAYBWV+Bvlzc3NccMNN8RHH30U3bp1i9mzZ8cjjzwSp512WvziF7+Inj17LpHv3r17zJkzJ/r06VNxzWKxuMSf58+fv8T/XZE15daQr21+WWs+b85puwf58teYcbrz1ahhxrXN16KGGVc3X4saZlzdfC1qmHGy+VrUMOPq5mtRI+35eptxLWqsavmkZ1yLGlnKL+sYYaB8if+Y5yeffBI//vGP44UXXohtt902pk6dGl26dIn77rsvDjzwwBg9enQMGTKkJX/44YfHYYcdFgcccEBF9fL5fCxYsKCsNZ07dy5rjXyy+TT2JF/dfBp7kk++hnx182nsST75GvLVzaexJ/nq5tPYk3zyNeSrm09jT/LJ18havn///iucJWLIVqck3ULV3fvqJUm3sEpIfKP8X+bMmRP5fD5GjhwZEydOjGHDhsW3vvWtGDp0aIwYMaIlN2zYsDj++ONjv/32q6hOPp+PXr16LfHZ/PnzY9asWbH55ptHly5dWq2ZOXNmqzXLI59svq01y5tz2u5BvrI1ZpzefLVqmHHt8kn1ZMaV59PYkxknX8OMk82nsSczTr5GPeTracZp7Kke8knOuBY1spb3Rnl5hvQ6OekWqu7emROTbmGVkPjRK/+ywQYbxMSJE2PAgAExbNiwiIjo27dvTJs2rWWj/OOPP47XXnstNt5443bVausB0qVLlza/K/ehI59sfnlr2ppz2u5BvvI1ZpzOfDVrmHFt8rWoYcbVzdeihhlXN1+LGmacbL4WNcy4uvla1KiXfL3MuBY1VtV8UjOuRY2s5YH2S/zHPP/lhRdeiHvvvTfOOuusls+GDRsWDz74YEydOjUiIq688spobGyM5ubmpNoEAAAAAGAVk4o3ykulUpx//vlx5JFHxhe/+MWWz7fZZps44YQT4qijjoq11147isViXH755dGhQ2r29wEAAAAAqHOpOaN8ed54442YMWNG9OnTJzbYYIN2XSufz1epKwAAAABIlpMXyjNkixOTbqHq7n1tUtItrBJS8Ub55+nRo0f06NGjatdb+gFSLBajUChEU1PTMs+AyufzZT105JPNt7VmeXNO2z3IV7bGjNObr1YNM65dPqmezLjyfBp7MuPka5hxsvk09mTGydeoh3w9zTiNPdVDPskZ16JG1vJAdTjDBAAAAACATLNRDgAAAABAptkoBwAAAAAg0+rijHIAAAAAgHYrlZLugJTyRjkAAAAAAJlmoxwAAAAAgEyzUQ4AAAAAQKY5oxwAAAAAyIbFzihn2bxRDgAAAABApuVKpWz91Gs+n0+6BQAAAACoiubm5qRbqCtDev4o6Raq7t6/XZZ0C6uETB69svQDpFgsRqFQiKampmhoaGiVz+fzZT105JPNt7VmeXNO2z3IV7bGjNObr1YNM65d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RUT+h+VOp6MXd4pYr/8ehCK2KTs7G6PRiMlk0nQrNkrHsYjIk1OiXERERERERERERESKNQ3HFREREREREREREZFiTYlyERERERERERERESnWlCgXERERERERERERkWJNiXIRERERERERERERKdaUKBcRERERERERERGRYk2JchERERF5rOzs7AKVi4iIiIiIWBMlykVERESKifj4ePO/jUYjmzdvJiYmJl/rvvnmm8yePTtP2datW+nYsSNpaWm/u+69e/cAiImJYeXKlQDcvXvXvPz48eN5YisKmZmZxMbGFmmblrJr1y6GDRtGenr679a7ceMGycnJf05QIiIiIiI2RolyERERkWJi+PDhTJo0CQA7OzvCwsJYvHjxY9dLSkriyJEjGAyGPOV+fn4kJiYSGhr6yHXj4+N55ZVXiI6OJioqyry9adOmsXDhQgA++ugjZs6cWdjdeqh33nmH5cuXF2mbvyc1NfWJ27h+/TqxsbH8+uuveT5JSUns3r2b77///oFlly5d4sqVKwB88803jB49mqysrCeORURERESkuClh6QBERERELOHw4cMEBgYSFRVl6VD+FPv37yciIoIJEyaYy8aOHcuwYcPo06cPjRo1euS627Ztw2g00rt3bzIyMjAYDDg6OuLl5cXQoUMpW7asua7JZOLevXuUKlUKAC8vL/7617/yz3/+kwEDBuDg4EBcXBw7d+5ky5Yt5OTkcO7cOd5///0i29eVK1cSGxtLeHi4uSwyMpLp06cTGRlJgwYNmDVrFhUrViyybY4dO5YWLVoQFBRU6DbmzZvH999/j4ODwwPLXF1d+de//vVAeU5ODs899xzh4eEMGTKEkydPMn/+fPMNERERERERyR+DyWQyWToIERERkT9bamoqFy9epH79+pYO5Q+Xk5ND7969cXV15fPPP8+zbOjQoVy4cIFvvvkGNze3h67fuXNn7O3t2bhxIzNmzGDFihW/u72yZcty+PBhAA4cOEB0dDSZmZnExcWxbds2Ro4cSUREBHXr1qVBgwb07duXH374wbz9nJwcHBwccHFxKfC+JiYm0qVLF7788ktq1qxpLuvcuTO1a9dm8ODBbN26lTNnzvDtt99SokTRjBtJTk5m8ODBtG7dmpEjRxZJm4WRlJREly5dCA8PN++/iIiIiIg8nhLlIiIiIjZu6dKlLFiwgHXr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tQKga9eurFixgm3bttGsWbNC7t19zz77rHlUfq46depw7NixPGURERHmGxRFKSEhgQEDBtC+ffsCJckh740SgMmTJ9OsWTM6duxo/rz88ssPjNz/7Y2F//u//8Pe3j7fTx+IiIiIiIgS5SIiImJD6tSpA8DRo0fNZb/++iuvv/46p0+ftppt5PL392fPnj3s2bOHpk2bUq5cuUK3dffuXUJCQnjllVfMo8xzxcfHFypRfvPmTWJjY/nll1+oWbMm7u7uD627atUqbt26xZkzZ/j5559ZsGABAwYMwMnJCYB+/fpx+vRpIiIiHjuX+uO88sorbNiw4YGyAwcOcO7cOeB+f+3cufOJk/K/deXKFfr3709gYCBBQUEFXv+3iXJXV1f69+/Pd999Z/5MmzbtsVO5bNy4kVdeeeWB9kRERERE5NE09YqIiIjYjBo1atCxY0dmz56N0WjkqaeeIiQkhKpVq/Liiy9azTZydezYkblz55Kens6rr75a6HZu377N8OHDuXPnDhMmTMizLCkpidu3b+drqg+j0Uh2djYlS5bE19eXH374gapVq3Lo0KE8o9RzcnIwmUzmhG7fvn2pVasWBw8eZOjQoaSlpeHq6srp06epX78+d+/excnJCZPJRHJy8iMT7vnRq1cv/P39OXXqFL6+vgC0bt2axo0bM3DgQNq0acOBAwfw8PCgd+/ehd7Ob0VGRjJixAimTp1a6JH/v51fPTk5ma+//pqwsDBzWU5ODqVKlXpkG1evXuU///kP69atK1QMIiIiIiLFlRLlIiIiYlPmzJnDvHnzmDNnDjk5OTRq1IiZM2eaRy9byzbg/tzaDRo04NixYyxevLjA6xuNRrZv3878+fO5desWoaGh5pHjUVFRbN26lSNHjlCiRAnq1av3yHays7OB+yOmO3To8NA6x44d4+uvvzb/PWvWLHNyv2TJktSqVYtNmzZRpkwZ5s+fz48//sixY8dISEggODiYbt26ERMTQ2BgIEuXLv3deH6Pq6srU6dOZeLEiaxatQp3d3cMBgNLlizhk08+Yd++fTRp0oQJEyY8MLL+Sfz73/9mxowZNG3atNBtGI3GPH/PnTv3kXUTEhI4f/48N27cME+9kpGRwdtvv82bb75Z6BeiioiIiIgUVwbTb4euiIiIiEihGY3GBxKe/83Ozi7fc58/qSNHjvDGG2/w4osvMm3aNKpUqWJelpGRQdu2balatSp/+9vfaNOmzSPb+fDDDzl16hTLli3jwoULODo6/u60HllZWVSoUAEPDw/27t3Lt99+y969e+nSpQtvv/02ZcuW5cqVK8yYMYODBw8yYcIEAgMDSU1NZdiwYZw4cYJZs2bRtWvXQu/7vHnzuHHjBrNnzy50G3+2Cxcu0Llz53y9zPPSpUv4+/tTs2ZNFi9ejLe3NwsXLiQ2Npa5c+dq2hURERERkQJSolxERESkCE2ZMoVvv/32kcsDAwN55513/rR4Ll26xNNPP/1EbfzjH//g/PnzrF69usDrRkVFsXnzZl577TW8vb3N5VlZWSxatIiePXtStWpVc3lOTg6rV6+md+/eODg4FDpmk8lESkoKbm5uhW7jf116ejqlS5fO87e9vT0lS5a0YFQiIiIiItZJiXIRERGRInTt2jVu3779yOXu7u54eXn9iRGJiIiIiIjI4yhRLiIiIiIiIiIiIiLF2p8zQaaIiIiIiIiIiIiIyP8oJcpFREREREREREREpFhTolxEREREREREREREijUlykVERERERERERESkWFOiXERERERERERERESKNSXKRURERERERERERKRYU6JcRERERERERERERIo1JcpFREREREREREREpFj7f0KofOQ+kD+UAAAAAElFTkSuQmCC", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from scipy.stats import spearmanr\n", "from tqdm import tqdm # 用于显示进度条 (可选)\n", "\n", "# 设置 Matplotlib/Seaborn 样式 (可选)\n", "sns.set_theme(style=\"whitegrid\")\n", "plt.rcParams['font.sans-serif'] = ['SimHei'] # 或者其他支持中文的字体\n", "plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题\n", "\n", "def analyze_score_performance_2d(score_df: pd.DataFrame,\n", " score_col: str = 'score',\n", " label_col: str = 'label',\n", " condition1_col: str = 'circ_mv',\n", " condition2_col: str = 'future_return',\n", " n_bins: int = 100,\n", " min_samples_per_bin: int = 30): # 每个格子最少样本数\n", " \"\"\"\n", " 分析 score 在两个条件下 (如市值、未来收益) 的二维分箱表现。\n", "\n", " Args:\n", " score_df (pd.DataFrame): 包含分数、标签和条件列的 DataFrame。\n", " score_col (str): 预测分数所在的列名。\n", " label_col (str): 目标标签所在的列名 (应为数值或可排序类别)。\n", " condition1_col (str): 第一个条件列名 (例如 'circ_mv')。\n", " condition2_col (str): 第二个条件列名 (例如 'future_return')。\n", " n_bins (int): 每个条件划分的箱数 (分位数数量)。\n", " min_samples_per_bin (int): 计算指标所需的最小样本数,小于此数目的格子结果将被屏蔽。\n", "\n", " Returns:\n", " tuple: 包含 (performance_pivot, count_pivot, fig)\n", " performance_pivot: 以二维分箱为索引/列的 Spearman 相关系数矩阵。\n", " count_pivot: 每个二维分箱的样本数量矩阵。\n", " fig: 生成的热力图 Matplotlib Figure 对象。\n", " \"\"\"\n", " print(f\"开始分析 '{score_col}' 在 '{condition1_col}' 和 '{condition2_col}' 下的表现...\")\n", "\n", " required_cols = [score_col, label_col, condition1_col, condition2_col]\n", " if not all(col in score_df.columns for col in required_cols):\n", " missing = [col for col in required_cols if col not in score_df.columns]\n", " raise ValueError(f\"输入 DataFrame 缺少必需列: {missing}\")\n", "\n", " # --- 1. 数据准备和清洗 ---\n", " print(\"准备数据,处理 NaN 值...\")\n", " # 只保留需要的列,并移除包含 NaN 的行,避免影响分箱和计算\n", " analysis_df = score_df[required_cols].dropna().copy()\n", " n_original = len(score_df)\n", " n_after_drop = len(analysis_df)\n", " print(f\"原始数据 {n_original} 行,移除 NaN 后剩余 {n_after_drop} 行用于分析。\")\n", "\n", " if n_after_drop < min_samples_per_bin * n_bins: # 检查数据量是否过少\n", " print(f\"警告: 清理 NaN 后数据量 ({n_after_drop}) 可能不足以支持 {n_bins}x{n_bins} 的精细分箱分析。\")\n", " if n_after_drop < min_samples_per_bin:\n", " print(\"错误: 有效数据过少,无法进行分析。\")\n", " return None, None, None\n", "\n", " # --- 2. 二维分箱 ---\n", " print(f\"对 '{condition1_col}' 和 '{condition2_col}' 进行 {n_bins} 分位数分箱...\")\n", " bin1_col = f'{condition1_col}_bin'\n", " bin2_col = f'{condition2_col}_bin'\n", "\n", " try:\n", " # 使用 qcut 进行分位数分箱,labels=False 返回 0 到 n_bins-1 的整数标签\n", " # duplicates='drop' 会丢弃导致边界不唯一的重复值所在的箱子,可能导致某些箱号缺失\n", " # 对于可视化,这通常可以接受,但如果需要严格的等分,需先 rank\n", " analysis_df[bin1_col] = pd.qcut(analysis_df[condition1_col], q=n_bins, labels=False, duplicates='drop')\n", " analysis_df[bin2_col] = pd.qcut(analysis_df[condition2_col], q=n_bins, labels=False, duplicates='drop')\n", " except Exception as e:\n", " print(f\"错误: 分箱失败,请检查数据分布或减少 n_bins。错误信息: {e}\")\n", " # 可以尝试先 rank 再 qcut\n", " # analysis_df[bin1_col] = pd.qcut(analysis_df[condition1_col].rank(method='first'), q=n_bins, labels=False, duplicates='raise')\n", " # analysis_df[bin2_col] = pd.qcut(analysis_df[condition2_col].rank(method='first'), q=n_bins, labels=False, duplicates='raise')\n", " return None, None, None\n", "\n", " # --- 3. 分组计算表现指标 (Spearman Rank IC) ---\n", " print(\"按二维分箱分组计算 Spearman Rank IC...\")\n", "\n", " def safe_spearmanr(x, y):\n", " \"\"\"安全计算 Spearman 相关性,处理数据量过少的情况\"\"\"\n", " if len(x) < max(2, min_samples_per_bin): # 要求至少有 min_samples_per_bin 个点才计算\n", " return np.nan\n", " corr, p_value = spearmanr(x, y)\n", " return corr if not np.isnan(corr) else np.nan # 确保返回 NaN 而不是 None 或其他\n", "\n", " # 按两个分箱列分组\n", " grouped = analysis_df.groupby([bin1_col, bin2_col])\n", "\n", " # 计算每个格子的 Spearman 相关系数\n", " # apply 可能较慢,但计算相关性通常需要 apply\n", " performance_series = grouped.apply(lambda sub: safe_spearmanr(sub[score_col], sub[label_col]))\n", "\n", " # 计算每个格子的样本数量\n", " count_series = grouped.size()\n", "\n", " # --- 4. 结果整理成 Pivot Table (用于绘图) ---\n", " print(\"整理结果用于绘图...\")\n", " try:\n", " # 将 performance_series 转换成二维矩阵\n", " # index 为 condition1_bin, columns 为 condition2_bin\n", " performance_pivot = performance_series.unstack(level=0) # level=0 对应第一个 groupby key (bin1_col)\n", " count_pivot = count_series.unstack(level=0)\n", "\n", " # 可选:按列和索引排序,确保顺序正确\n", " performance_pivot = performance_pivot.sort_index(axis=0).sort_index(axis=1)\n", " count_pivot = count_pivot.sort_index(axis=0).sort_index(axis=1)\n", " \n", " print(performance_pivot)\n", "\n", " except Exception as e:\n", " print(f\"错误: 无法将结果转换为二维矩阵,可能因为分箱不均匀或数据问题: {e}\")\n", " return None, None, None\n", "\n", " # --- 5. 可视化:绘制热力图 ---\n", " print(\"生成热力图...\")\n", " fig, ax = plt.subplots(figsize=(16, 12)) # 调整图像大小\n", "\n", " # 使用 count_pivot 创建一个 mask,屏蔽掉样本量过小的格子\n", " mask = count_pivot < min_samples_per_bin\n", "\n", " # 绘制热力图\n", " sns.heatmap(performance_pivot,\n", " annot=False, # 100x100 个格子加注释会太密集\n", " fmt=\".2f\",\n", " cmap=\"viridis\", # 选择颜色映射, 'viridis', 'coolwarm', 'RdYlGn' 等都不错\n", " linewidths=.5,\n", " linecolor='lightgray',\n", " # mask=mask, # 应用 mask\n", " ax=ax,\n", " cbar_kws={'label': f'Spearman Rank IC ({score_col} vs {label_col})'}) # 颜色条标签\n", "\n", " # 设置标题和轴标签\n", " ax.set_title(f'{score_col} 表现分析 (Rank IC vs {label_col})\\n基于 {condition1_col} 和 {condition2_col} {n_bins}x{n_bins} 分箱', fontsize=16)\n", " ax.set_xlabel(f'{condition1_col} 分位数 (0 -> 高)', fontsize=12)\n", " ax.set_ylabel(f'{condition2_col} 分位数 (0 -> 高)', fontsize=12)\n", "\n", " # 可选:调整刻度标签,避免显示所有 100 个刻度\n", " if n_bins > 20:\n", " tick_interval = n_bins // 10 # 大约显示 10 个刻度\n", " ax.set_xticks(np.arange(0, n_bins, tick_interval) + 0.5)\n", " ax.set_yticks(np.arange(0, n_bins, tick_interval) + 0.5)\n", " ax.set_xticklabels(np.arange(0, n_bins, tick_interval))\n", " ax.set_yticklabels(np.arange(0, n_bins, tick_interval))\n", "\n", " plt.xticks(rotation=45, ha='right')\n", " plt.yticks(rotation=0)\n", " plt.tight_layout() # 调整布局\n", "\n", " print(\"分析完成。\")\n", " return performance_pivot, count_pivot, fig\n", "\n", "# --- 如何使用 ---\n", "# 假设你的包含预测结果和所需列的 DataFrame 是 final_predictions_df\n", "# 确保它包含 'score', 'label', 'circ_mv', 'future_return'\n", "\n", "# # 示例调用 (你需要有实际的 score_df)\n", "try:\n", " # 确保数据类型正确\n", " cols_to_numeric = ['score', 'label', 'circ_mv', 'future_return']\n", " for col in cols_to_numeric:\n", " if col in score_df.columns:\n", " score_df[col] = pd.to_numeric(score_df[col], errors='coerce')\n", "\n", " # 调用分析函数\n", " performance_matrix, count_matrix, heatmap_figure = analyze_score_performance_2d(\n", " score_df,\n", " n_bins=100, # 你要求的100分箱\n", " min_samples_per_bin=50 # 每个格子至少需要50个样本才显示IC,可以调整\n", " )\n", "\n", " # 显示图像\n", " if heatmap_figure:\n", " plt.show()\n", "\n", " # 可以查看具体的 performance_matrix 和 count_matrix\n", " # print(\"\\nPerformance Matrix (Spearman IC):\")\n", " # print(performance_matrix)\n", " # print(\"\\nCount Matrix:\")\n", " # print(count_matrix)\n", "\n", "except ValueError as ve:\n", " print(f\"数据错误: {ve}\")\n", "except Exception as e:\n", " print(f\"发生未知错误: {e}\")" ] } ], "metadata": { "kernelspec": { "display_name": "new_trader", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.11" } }, "nbformat": 4, "nbformat_minor": 5 }