Files
NewStock/main/train/Classify2.ipynb
2025-06-04 13:50:02 +08:00

2568 lines
286 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "code",
"execution_count": 4,
"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",
"/mnt/d/PyProject/NewStock\n"
]
}
],
"source": [
"%load_ext autoreload\n",
"%autoreload 2\n",
"# %load_ext cudf.pandas\n",
"\n",
"import gc\n",
"import os\n",
"import sys\n",
"sys.path.append('/mnt/d/PyProject/NewStock/')\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\")\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "4a481c60",
"metadata": {},
"outputs": [],
"source": [
"# 设置使用核心\n",
"import os\n",
"os.environ[\"MODIN_CPUS\"] = \"4\"\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"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",
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 8692146 entries, 0 to 8692145\n",
"Data columns (total 33 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 amount float64 \n",
" 8 pct_chg float64 \n",
" 9 turnover_rate float64 \n",
" 10 pe_ttm float64 \n",
" 11 circ_mv float64 \n",
" 12 total_mv float64 \n",
" 13 volume_ratio float64 \n",
" 14 is_st bool \n",
" 15 up_limit float64 \n",
" 16 down_limit float64 \n",
" 17 buy_sm_vol float64 \n",
" 18 sell_sm_vol float64 \n",
" 19 buy_lg_vol float64 \n",
" 20 sell_lg_vol float64 \n",
" 21 buy_elg_vol float64 \n",
" 22 sell_elg_vol float64 \n",
" 23 net_mf_vol float64 \n",
" 24 his_low float64 \n",
" 25 his_high float64 \n",
" 26 cost_5pct float64 \n",
" 27 cost_15pct float64 \n",
" 28 cost_50pct float64 \n",
" 29 cost_85pct float64 \n",
" 30 cost_95pct float64 \n",
" 31 weight_avg float64 \n",
" 32 winner_rate float64 \n",
"dtypes: bool(1), datetime64[ns](1), float64(30), object(1)\n",
"memory usage: 2.1+ 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('/mnt/d/PyProject/NewStock/data/daily_data.h5', key='daily_data',\n",
" columns=['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'amount', 'pct_chg'],\n",
" df=None)\n",
"\n",
"print('daily basic')\n",
"df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/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('/mnt/d/PyProject/NewStock/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('/mnt/d/PyProject/NewStock/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('/mnt/d/PyProject/NewStock/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": 7,
"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"
]
}
],
"source": [
"print('industry')\n",
"industry_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/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": 8,
"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": [
"from main.factor.factor import *\n",
"\n",
"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",
" # df = sentiment_panic_greed_index(df)\n",
" # df = sentiment_market_breadth_proxy(df)\n",
" # df = sentiment_reversal_indicator(df)\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', \n",
" 'RSI', 'MACD', 'Signal_line', 'MACD_hist', \n",
" # 'sentiment_panic_greed_index',\n",
" '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 = '/mnt/d/PyProject/NewStock/data/index_data.h5'\n",
"index_data = generate_index_indicators(h5_filename)\n",
"index_data = index_data.dropna()\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"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": 10,
"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",
"\n",
" # cs_rank_intraday_range(industry_data)\n",
" # cs_rank_close_pos_in_range(industry_data)\n",
"\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('/mnt/d/PyProject/NewStock/data/sw_daily.h5')\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"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', 'amount', '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": 12,
"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('/mnt/d/PyProject/NewStock/data/fina_indicator.h5', key='fina_indicator',\n",
" columns=['ts_code', 'ann_date', 'undist_profit_ps', 'ocfps', 'bps', 'roa', 'roe'],\n",
" df=None)\n",
"cashflow_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/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('/mnt/d/PyProject/NewStock/data/balancesheet.h5', key='balancesheet',\n",
" columns=['ts_code', 'ann_date', 'money_cap', 'total_liab'],\n",
" df=None)\n",
"top_list_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/top_list.h5', key='top_list',\n",
" columns=['ts_code', 'trade_date', 'reason'],\n",
" df=None)\n",
"\n",
"top_list_df = top_list_df.sort_values(by='trade_date', ascending=False).drop_duplicates(subset=['ts_code', 'trade_date'], keep='first').sort_values(by='trade_date')\n",
"\n",
"stk_holdertrade_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/stk_holdertrade.h5', key='stk_holdertrade',\n",
" columns=['ts_code', 'ann_date', 'in_de', 'change_ratio', 'after_ratio'],\n",
" df=None)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"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",
"使用 '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",
"--- 计算日级别偏离度 (使用 pct_chg) ---\n",
"--- 计算日级别动量基准 (使用 pct_chg) ---\n",
"日级别动量基准计算完成 (使用 pct_chg)。\n",
"日级别偏离度计算完成 (使用 pct_chg)。\n",
"--- 计算日级别行业偏离度 (使用 pct_chg 和行业基准) ---\n",
"--- 计算日级别行业动量基准 (使用 pct_chg 和 cat_l2_code) ---\n",
"错误: 计算日级别行业动量基准需要以下列: ['pct_chg', 'cat_l2_code', 'trade_date', 'ts_code']。\n",
"错误: 计算日级别行业偏离度需要以下列: ['pct_chg', 'daily_industry_positive_benchmark', 'daily_industry_negative_benchmark']。请先运行 daily_industry_momentum_benchmark(df)。\n",
"Index(['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol',\n",
" 'amount', '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', 'roa', 'roe',\n",
" 'AR', 'BR', 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor',\n",
" 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20',\n",
" 'bbi_ratio_factor', 'daily_deviation', 'lg_elg_net_buy_vol',\n",
" 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'flow_divergence_diff',\n",
" 'flow_divergence_ratio', 'total_buy_vol', 'lg_elg_buy_prop',\n",
" 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change',\n",
" 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness',\n",
" 'floating_chip_proxy', 'cost_support_15pct_change',\n",
" 'cat_winner_price_zone', 'flow_chip_consistency',\n",
" 'profit_taking_vs_absorb', '_is_positive', '_is_negative',\n",
" 'cat_is_positive', '_pos_returns', '_neg_returns', '_pos_returns_sq',\n",
" '_neg_returns_sq', 'upside_vol', 'downside_vol', 'vol_ratio',\n",
" '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",
"Calculating cs_rank_net_lg_flow_val...\n",
"Finished cs_rank_net_lg_flow_val.\n",
"Calculating cs_rank_flow_divergence...\n",
"Finished cs_rank_flow_divergence.\n",
"Calculating cs_rank_ind_adj_lg_flow...\n",
"Finished cs_rank_ind_adj_lg_flow.\n",
"Calculating cs_rank_elg_buy_ratio...\n",
"Finished cs_rank_elg_buy_ratio.\n",
"Calculating cs_rank_rel_profit_margin...\n",
"Finished cs_rank_rel_profit_margin.\n",
"Calculating cs_rank_cost_breadth...\n",
"Finished cs_rank_cost_breadth.\n",
"Calculating cs_rank_dist_to_upper_cost...\n",
"Finished cs_rank_dist_to_upper_cost.\n",
"Calculating cs_rank_winner_rate...\n",
"Finished cs_rank_winner_rate.\n",
"Calculating cs_rank_intraday_range...\n",
"Finished cs_rank_intraday_range.\n",
"Calculating cs_rank_close_pos_in_range...\n",
"Finished cs_rank_close_pos_in_range.\n",
"Calculating cs_rank_opening_gap...\n",
"Error calculating cs_rank_opening_gap: Missing 'pre_close' column. Assigning NaN.\n",
"Calculating cs_rank_pos_in_hist_range...\n",
"Finished cs_rank_pos_in_hist_range.\n",
"Calculating cs_rank_vol_x_profit_margin...\n",
"Finished cs_rank_vol_x_profit_margin.\n",
"Calculating cs_rank_lg_flow_price_concordance...\n",
"Finished cs_rank_lg_flow_price_concordance.\n",
"Calculating cs_rank_turnover_per_winner...\n",
"Finished cs_rank_turnover_per_winner.\n",
"Calculating cs_rank_ind_cap_neutral_pe (Placeholder - requires statsmodels)...\n",
"Finished cs_rank_ind_cap_neutral_pe (Placeholder).\n",
"Calculating cs_rank_volume_ratio...\n",
"Finished cs_rank_volume_ratio.\n",
"Calculating cs_rank_elg_buy_sell_sm_ratio...\n",
"Finished cs_rank_elg_buy_sell_sm_ratio.\n",
"Calculating cs_rank_cost_dist_vol_ratio...\n",
"Finished cs_rank_cost_dist_vol_ratio.\n",
"Calculating cs_rank_size...\n",
"Finished cs_rank_size.\n",
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 4554725 entries, 0 to 4554724\n",
"Columns: 181 entries, ts_code to cs_rank_size\n",
"dtypes: bool(10), datetime64[ns](1), float64(165), int64(3), object(2)\n",
"memory usage: 5.8+ GB\n",
"None\n",
"['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'amount', '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', 'roa', 'roe', 'AR', 'BR', 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'daily_deviation', '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', 'cs_rank_net_lg_flow_val', 'cs_rank_flow_divergence', 'cs_rank_ind_adj_lg_flow', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_opening_gap', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_ind_cap_neutral_pe', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size']\n"
]
}
],
"source": [
"\n",
"import numpy as np\n",
"from main.factor.factor import *\n",
"from main.factor.money_factor import *\n",
"\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",
"\n",
"# df = price_minus_deduction_price(df, n=120)\n",
"# df = price_deduction_price_diff_ratio_to_sma(df, n=120)\n",
"# df = cat_price_vs_sma_vs_deduction_price(df, n=120)\n",
"# df = cat_reason(df, top_list_df)\n",
"# df = cat_is_on_top_list(df, top_list_df)\n",
"\n",
"# df = ts_turnover_rate_acceleration_5_20(df)\n",
"# df = ts_vol_sustain_10_30(df)\n",
"# df = cs_turnover_rate_relative_strength_20(df)\n",
"# df = cs_amount_outlier_10(df)\n",
"# df = holder_trade_factors(stk_holdertrade_df, df)\n",
"\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",
"df = add_financial_factor(df, fina_indicator_df, factor_value_col='roa')\n",
"df = add_financial_factor(df, fina_indicator_df, factor_value_col='roe')\n",
"\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",
"\n",
"df = turnover_rate_n(df, n=5)\n",
"df = variance_n(df, n=20)\n",
"df = bbi_ratio_factor(df)\n",
"df = daily_deviation(df)\n",
"df = daily_industry_deviation(df)\n",
"df, _ = get_rolling_factor(df)\n",
"df, _ = get_simple_factor(df)\n",
"\n",
"df = df.rename(columns={'l1_code': 'cat_l1_code'})\n",
"df = df.rename(columns={'l2_code': 'cat_l2_code'})\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",
"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",
"\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": 14,
"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": 15,
"id": "f4f16d63ad18d1bc",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T13:08:03.670700Z",
"start_time": "2025-04-03T13:08:03.665739Z"
}
},
"outputs": [],
"source": [
"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",
"from tqdm import tqdm\n",
"\n",
"def cs_neutralize_market_cap_numpy(df: pd.DataFrame,\n",
" features: list,\n",
" market_cap_col: str = 'circ_mv'):\n",
" \"\"\"\n",
" 对 DataFrame 中的指定特征进行截面市值中性化 (NumPy 优化)。\n",
"\n",
" Args:\n",
" df (pd.DataFrame): 包含数据的 DataFrame需要有 'trade_date' 和 market_cap_col 列。\n",
" features (list): 需要进行市值中性化的特征列名列表。\n",
" market_cap_col (str): 包含市值数据的列名,默认为 'circ_mv'。\n",
" \"\"\"\n",
" print(\"开始截面市值中性化 (NumPy 优化)...\")\n",
" required_cols = features + ['trade_date', 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",
" df_copy = df\n",
" log_cap_col = '_log_market_cap'\n",
" df_copy[log_cap_col] = np.log1p(df_copy[market_cap_col])\n",
"\n",
" # 创建一个 DataFrame 来存储所有日期的残差结果\n",
" residuals_container = pd.DataFrame(index=df_copy.index, columns=features, dtype=float)\n",
"\n",
" for date, group_df in tqdm(df_copy.groupby('trade_date'), desc=\"Neutralizing by Date (NumPy)\"):\n",
" # 准备 X 矩阵 (自变量):常数项和对数市值\n",
" X_daily = np.concatenate([np.ones((len(group_df), 1)), group_df[[log_cap_col]].values], axis=1)\n",
"\n",
" for feature_col in features:\n",
" Y_daily = group_df[feature_col].values\n",
"\n",
" # 处理 NaN只对有效数据对进行回归\n",
" valid_mask_y = ~np.isnan(Y_daily)\n",
" valid_mask_x = ~np.isnan(X_daily).any(axis=1)\n",
" valid_mask = valid_mask_y & valid_mask_x\n",
"\n",
" current_feature_indices = group_df.index[valid_mask]\n",
"\n",
" if np.sum(valid_mask) < X_daily.shape[1] + 1:\n",
" # 有效数据不足,此特征在此日期保持 NaN\n",
" continue\n",
"\n",
" Y_valid = Y_daily[valid_mask]\n",
" X_valid = X_daily[valid_mask, :]\n",
"\n",
" try:\n",
" # 使用 np.linalg.lstsq 进行 OLS 计算\n",
" beta, sum_sq_resid, rank, s = np.linalg.lstsq(X_valid, Y_valid, rcond=None)\n",
"\n",
" # 计算预测值 Y_hat = X_valid @ beta\n",
" Y_hat_valid = X_valid @ beta\n",
"\n",
" # 计算残差 residuals = Y_valid - Y_hat_valid\n",
" residuals_valid = Y_valid - Y_hat_valid\n",
"\n",
" # 将计算得到的残差放回 residuals_container\n",
" residuals_container.loc[current_feature_indices, feature_col] = residuals_valid\n",
"\n",
" except np.linalg.LinAlgError:\n",
" pass\n",
" except Exception as e:\n",
" pass\n",
"\n",
" # 将所有计算得到的残差更新回原始的 df (原地修改)\n",
" for feature_col in features:\n",
" df[feature_col] = residuals_container[feature_col]\n",
"\n",
" # 清理临时列\n",
" df.drop(columns=[log_cap_col], inplace=True, errors='ignore')\n",
" print(\"截面市值中性化完成 (NumPy 优化)。\")\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": 16,
"id": "40e6b68a91b30c79",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T13:08:04.694262Z",
"start_time": "2025-04-03T13:08:03.694904Z"
}
},
"outputs": [],
"source": [
"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",
"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\n",
"\n",
"def create_deviation_within_dates(df, feature_columns):\n",
" groupby_col = 'cat_l2_code' # 使用 trade_date 进行分组\n",
" new_columns = {}\n",
" ret_feature_columns = feature_columns[:]\n",
"\n",
" # 自动选择所有数值型特征\n",
" num_features = [col for col in feature_columns if 'cat' not in col and 'index' not in col]\n",
"\n",
" # num_features = ['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'cat_vol_spike', 'obv', 'maobv_6', 'return_5', 'return_10', 'return_20', 'std_return_5', 'std_return_15', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'act_factor5', 'act_factor6', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'alpha_022', 'alpha_003', 'alpha_007', 'alpha_013']\n",
" num_features = [col for col in num_features if 'cat' not in col and 'industry' not in col]\n",
" num_features = [col for col in num_features if 'limit' not in col]\n",
" num_features = [col for col in num_features if 'cyq' not in col]\n",
"\n",
" # 遍历所有数值型特征\n",
" for feature in num_features:\n",
" if feature == 'trade_date': # 不需要对 'trade_date' 计算偏差\n",
" continue\n",
"\n",
" # grouped_mean = df.groupby(['trade_date'])[feature].transform('mean')\n",
" # deviation_col_name = f'deviation_mean_{feature}'\n",
" # new_columns[deviation_col_name] = df[feature] - grouped_mean\n",
" # ret_feature_columns.append(deviation_col_name)\n",
"\n",
" grouped_mean = df.groupby(['trade_date', groupby_col])[feature].transform('mean')\n",
" deviation_col_name = f'deviation_mean_{feature}'\n",
" new_columns[deviation_col_name] = df[feature] - grouped_mean\n",
" ret_feature_columns.append(deviation_col_name)\n",
"\n",
" # 将新计算的偏差特征与原始 DataFrame 合并\n",
" df = pd.concat([df, pd.DataFrame(new_columns)], axis=1)\n",
"\n",
" # for feature in ['obv', 'return_20', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4']:\n",
" # df[f'deviation_industry_{feature}'] = df[feature] - df[f'industry_{feature}']\n",
"\n",
" return df, ret_feature_columns\n"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "47c12bb34062ae7a",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T14:57:50.841165Z",
"start_time": "2025-04-03T14:49:25.889057Z"
}
},
"outputs": [],
"source": [
"days = 5\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().groupby('ts_code').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": 18,
"id": "29221dde",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"191\n"
]
}
],
"source": [
"feature_columns = [col for col in df.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",
"feature_columns = [col for col in feature_columns if col not in ['roa', 'roe']]\n",
"print(len(feature_columns))"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "03ee5daf",
"metadata": {},
"outputs": [],
"source": [
"# df = fill_nan_with_daily_median(df, feature_columns)\n",
"for feature_col in [col for col in feature_columns if col in df.columns]:\n",
" # median_val = df[feature_col].median()\n",
" df[feature_col].fillna(0, inplace=True)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"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",
"1 000001.SZ 2019-01-03 16.583965\n",
"2 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', 'daily_deviation', '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', 'cs_rank_net_lg_flow_val', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size', '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%|██████████| 131/131 [00:16<00:00, 7.78it/s]\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"截面 MAD 去极值处理完成。\n",
"开始截面 MAD 去极值处理 (k=3.0)...\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"MAD Filtering: 100%|██████████| 131/131 [00:11<00:00, 11.06it/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', 'daily_deviation', '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', 'cs_rank_net_lg_flow_val', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size', '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-30\n",
"2057465\n",
"train_data最小日期: 2020-01-02\n",
"train_data最大日期: 2022-12-30\n",
"1781706\n",
"test_data最小日期: 2023-01-03\n",
"test_data最大日期: 2025-05-30\n",
" ts_code trade_date log_circ_mv\n",
"0 000001.SZ 2019-01-02 16.574219\n",
"1 000001.SZ 2019-01-03 16.583965\n",
"2 000001.SZ 2019-01-04 16.633371\n"
]
}
],
"source": [
"split_date = '2023-01-01'\n",
"train_data = df[filter_index & (df['trade_date'] <= split_date) & (df['trade_date'] >= '2020-01-01')]\n",
"test_data = df[(df['trade_date'] >= split_date)]\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, [col for col in feature_columns if col in train_data.columns])\n",
"# test_data, _ = create_deviation_within_dates(test_data, [col for col in feature_columns if col in train_data.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",
"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_market_cap_numpy(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_market_cap_numpy(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": 21,
"id": "3ff2d1c5",
"metadata": {},
"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 datetime # 用于日期计算\n",
"from catboost import CatBoostClassifier\n",
"from catboost import Pool\n",
"import lightgbm as lgb\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', type='light'): # 增加目标列参数\n",
"\n",
" print('train data size: ', len(train_data_df))\n",
" print(train_data_df[['ts_code', 'trade_date', 'log_circ_mv']])\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",
" # 提取特征和标签,只取数值型特征用于线性回归\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",
"\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",
" if type == 'cat':\n",
" params = {\n",
" 'loss_function': 'Logloss', # 适用于二分类\n",
" 'eval_metric': 'Logloss', # 评估指标\n",
" 'iterations': 1500,\n",
" 'learning_rate': 0.01,\n",
" 'depth': 10, # 控制模型复杂度\n",
" 'l2_leaf_reg': 50, # L2 正则化\n",
" 'verbose': 5000,\n",
" 'early_stopping_rounds': 300,\n",
" # 'od_type': 'Iter', # Overfitting detector type\n",
" # 'od_wait': 300, # Number of iterations to wait after the bes\n",
" 'one_hot_max_size': 50,\n",
" 'class_weights': [0.6, 1.2],\n",
" 'task_type': 'GPU',\n",
" 'has_time': True,\n",
" 'random_seed': 7\n",
" }\n",
" cat_features = [i for i, col in enumerate(feature_columns) if col.startswith('cat')]\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, \n",
" plot=True, \n",
" use_best_model=True\n",
" )\n",
" elif type == 'light':\n",
" params = {\n",
" 'objective': 'binary',\n",
" 'metric': 'average_precision',\n",
" 'learning_rate': 0.01,\n",
" 'is_unbalance': True,\n",
" 'num_leaves': 2048,\n",
" 'min_data_in_leaf': 1024,\n",
" 'max_depth': 32,\n",
" 'max_bin': 1024,\n",
" 'feature_fraction': 0.5,\n",
" 'bagging_fraction': 0.5,\n",
" 'bagging_freq': 1,\n",
" 'lambda_l1': 50,\n",
" 'lambda_l2': 50,\n",
" 'verbosity': -1,\n",
" 'num_threads' : 8\n",
" }\n",
" categorical_feature = [col for col in feature_columns if 'cat' in col]\n",
" train_dataset = lgb.Dataset(\n",
" X_train, label=y_train,\n",
" categorical_feature=categorical_feature\n",
" )\n",
" val_dataset = lgb.Dataset(\n",
" X_val, label=y_val,\n",
" categorical_feature=categorical_feature\n",
" )\n",
"\n",
" evals = {}\n",
" callbacks = [lgb.log_evaluation(period=1000),\n",
" lgb.callback.record_evaluation(evals),\n",
" lgb.early_stopping(100, first_metric_only=True)\n",
" ]\n",
" # 训练模型\n",
" model = lgb.train(\n",
" params, train_dataset, num_boost_round=1000,\n",
" valid_sets=[train_dataset, val_dataset], valid_names=['train', 'valid'],\n",
" callbacks=callbacks\n",
" )\n",
"\n",
" # 打印特征重要性(如果需要)\n",
" if True:\n",
" lgb.plot_metric(evals)\n",
" lgb.plot_importance(model, importance_type='split', max_num_features=20)\n",
" plt.show()\n",
"\n",
"\n",
" return model, scaler, None # 返回训练好的模型、scaler 和 pca 对象"
]
},
{
"cell_type": "code",
"execution_count": 22,
"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",
" ts_code trade_date log_circ_mv\n",
"0 600306.SH 2020-01-02 11.552040\n",
"1 603269.SH 2020-01-02 11.324801\n",
"2 002633.SZ 2020-01-02 11.759023\n",
"3 603991.SH 2020-01-02 11.181150\n",
"4 000691.SZ 2020-01-02 11.677910\n",
"... ... ... ...\n",
"36395 600615.SH 2022-12-30 12.027909\n",
"36396 603829.SH 2022-12-30 12.034572\n",
"36397 603037.SH 2022-12-30 12.035767\n",
"36398 002767.SZ 2022-12-30 11.896427\n",
"36399 600561.SH 2022-12-30 11.858571\n",
"\n",
"[36400 rows x 3 columns]\n",
"原始样本数: 36400, 去除标签为空后样本数: 36400\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "182bcb05602b45a586d931fa2bcb3dd1",
"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.6890148\ttest: 0.6905108\tbest: 0.6905108 (0)\ttotal: 114ms\tremaining: 2m 50s\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"bestTest = 0.5226514078\n",
"bestIteration = 527\n",
"Shrink model to first 528 iterations.\n"
]
}
],
"source": [
"\n",
"gc.collect()\n",
"\n",
"use_pca = False\n",
"type = 'cat'\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\n",
" .dropna(subset=['label']).groupby('trade_date', group_keys=False)\n",
" .apply(lambda x: x.nsmallest(50, 'total_mv'))\n",
" .merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
" .merge(index_data, on='trade_date', how='left'), feature_columns, type=type)\n"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "5d1522a7538db91b",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T15:04:39.656944Z",
"start_time": "2025-04-03T15:04:39.298483Z"
}
},
"outputs": [],
"source": [
"score_df = test_data.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(300, 'total_mv'))\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",
"if type == 'cat':\n",
" score_df['score'] = model.predict_proba(score_df[feature_columns])[:, 1]\n",
"elif type == 'light':\n",
" score_df['score'] = model.predict(score_df[feature_columns])\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(2, 'total_mv')).reset_index()\n",
"save_df = save_df.sort_values(['trade_date', 'score'])\n",
"save_df[['trade_date', 'score', 'ts_code']].to_csv('predictions_test.tsv', index=False)\n"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "09b1799e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"191\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', 'daily_deviation', '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', 'cs_rank_net_lg_flow_val', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size', '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"
]
}
],
"source": [
"print(len(feature_columns))\n",
"print(feature_columns)\n",
"print([col for col in feature_columns if 'total_mv' in col])"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "e53b209a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5595 2057465\n",
" ts_code trade_date turnover_rate\n",
"0 000001.SZ 2023-01-03 1.1307\n",
"1 000001.SZ 2023-01-04 1.1284\n",
"2 000001.SZ 2023-01-05 0.8582\n",
"3 000001.SZ 2023-01-06 0.6162\n",
"4 000001.SZ 2023-01-09 0.5450\n",
"... ... ... ...\n",
"1781701 605599.SH 2025-05-26 0.6188\n",
"1781702 605599.SH 2025-05-27 1.2576\n",
"1781703 605599.SH 2025-05-28 2.0432\n",
"1781704 605599.SH 2025-05-29 2.0954\n",
"1781705 605599.SH 2025-05-30 1.4392\n",
"\n",
"[1781706 rows x 3 columns]\n"
]
}
],
"source": [
"print(len(train_data[train_data['pct_chg'] > 7]), len(train_data))\n",
"print(test_data[['ts_code', 'trade_date', 'turnover_rate']])"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "364e821a",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, roc_curve\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"def calculate_binary_classification_metrics(df: pd.DataFrame, score_col: str, label_col: str, future_return_col: str = None, total_mv_col: str = None, n_mv_bins: int = 10, threshold: float = 0.5):\n",
" \"\"\"\n",
" 计算二分类模型的评估指标,可选择计算 score 和 future_return 的相关性,\n",
" 并可选择计算 score 在按总市值 (total_mv) 分为 n 份后的每个分组上的预测性能ROC AUC。\n",
"\n",
" Args:\n",
" df: 包含 score (预测概率或置信度), label (真实二分类标签), 可选的 future_return 和 total_mv 的 Pandas DataFrame。\n",
" score_col: 包含模型预测 score 的列名。\n",
" label_col: 包含真实二分类标签 (0 或 1) 的列名。\n",
" future_return_col: (可选) 包含未来收益率的列名,用于计算相关性。\n",
" total_mv_col: (可选) 包含总市值的列名,用于按市值分 n 份分析预测性能。\n",
" n_mv_bins: (可选) 将总市值分为多少份,默认为 5。\n",
" threshold: 将 score 转换为预测类别的阈值,默认为 0.5。\n",
"\n",
" Returns:\n",
" 一个包含以下评估指标的字典:\n",
" - accuracy: 准确率\n",
" - precision: 精确率\n",
" - recall: 召回率\n",
" - f1: F1 分数\n",
" - roc_auc: ROC AUC 值\n",
" - fpr: ROC 曲线的假正率 (False Positive Rate)\n",
" - tpr: ROC 曲线的真正率 (True Positive Rate)\n",
" - thresholds: ROC 曲线的阈值\n",
" - score_return_correlation: (如果 future_return_col 提供) score 和 future_return 的皮尔逊相关系数\n",
" - mv_roc_auc: (如果 total_mv_col 提供) 一个字典,包含按总市值分为 n 份后的每个市值分组对应的 ROC AUC 值\n",
" \"\"\"\n",
" y_true = df[label_col].values\n",
" y_score = df[score_col].values\n",
" y_pred = (y_score >= threshold).astype(int)\n",
"\n",
" metrics = {}\n",
" metrics['accuracy'] = accuracy_score(y_true, y_pred)\n",
" metrics['precision'] = precision_score(y_true, y_pred)\n",
" metrics['recall'] = recall_score(y_true, y_pred)\n",
" metrics['f1'] = f1_score(y_true, y_pred)\n",
" metrics['roc_auc'] = roc_auc_score(y_true, y_score)\n",
" metrics['fpr'], metrics['tpr'], metrics['thresholds'] = roc_curve(y_true, y_score)\n",
"\n",
" if future_return_col in df.columns:\n",
" metrics['score_return_correlation'] = df[score_col].corr(df[future_return_col])\n",
"\n",
" if total_mv_col in df.columns and n_mv_bins > 1:\n",
" metrics['mv_roc_auc'] = {}\n",
" df['mv_quantile'] = pd.cut(df[total_mv_col], bins=n_mv_bins, labels=False, duplicates='drop')\n",
" for i in range(df['mv_quantile'].nunique()):\n",
" mv_group = df[df['mv_quantile'] == i]\n",
" if len(mv_group) > 0 and len(np.unique(mv_group[label_col])) > 1 and len(np.unique(mv_group[score_col])) > 1:\n",
" roc_auc_mv = roc_auc_score(mv_group[label_col], mv_group[score_col])\n",
" lower_bound = df[total_mv_col][df['mv_quantile'] == i].min()\n",
" upper_bound = df[total_mv_col][df['mv_quantile'] == i].max()\n",
" metrics['mv_roc_auc'][f'{lower_bound:.0e}-{upper_bound:.0e}'] = roc_auc_mv\n",
" else:\n",
" lower_bound = df[total_mv_col][df['mv_quantile'] == i].min()\n",
" upper_bound = df[total_mv_col][df['mv_quantile'] == i].max()\n",
" metrics['mv_roc_auc'][f'{lower_bound:.0e}-{upper_bound:.0e}'] = np.nan\n",
" print(f'{lower_bound:.0e}-{upper_bound:.0e}')\n",
" df.drop(columns=['mv_quantile'], inplace=True)\n",
"\n",
" return metrics\n",
"\n",
"def plot_roc_curve(metrics: dict):\n",
" plt.figure(figsize=(8, 6))\n",
" plt.plot(metrics['fpr'], metrics['tpr'], color='darkorange', lw=2, label=f'ROC curve (AUC = {metrics[\"roc_auc\"]:.2f})')\n",
" plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n",
" plt.xlabel('False Positive Rate')\n",
" plt.ylabel('True Positive Rate')\n",
" plt.title('Receiver Operating Characteristic (ROC)')\n",
" plt.legend(loc=\"lower right\")\n",
" plt.grid(True)\n",
" plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "1f6e6336",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"6e+04-9e+04\n",
"9e+04-1e+05\n",
"1e+05-1e+05\n",
"1e+05-1e+05\n",
"1e+05-2e+05\n",
"2e+05-2e+05\n",
"2e+05-2e+05\n",
"2e+05-2e+05\n",
"2e+05-3e+05\n",
"3e+05-3e+05\n",
"二分类评估指标:\n",
"accuracy: 0.6543\n",
"precision: 0.4629\n",
"recall: 0.1754\n",
"f1: 0.2544\n",
"roc_auc: 0.6206\n",
"fpr: (array of length 7484)\n",
"tpr: (array of length 7484)\n",
"thresholds: (array of length 7484)\n",
"score_return_correlation: -0.0293\n",
"mv_roc_auc: {'6e+04-9e+04': np.float64(0.5687499999999999), '9e+04-1e+05': np.float64(0.5662772981208735), '1e+05-1e+05': np.float64(0.5818040450154087), '1e+05-2e+05': np.float64(0.5694847679411162), '2e+05-2e+05': np.float64(0.6027478841082352), '2e+05-3e+05': np.float64(0.6169522445569707), '3e+05-3e+05': np.float64(0.6131663537577515)}\n"
]
},
{
"data": {
"image/png": "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",
"text/plain": [
"<Figure size 800x600 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\n",
"for sd in [\n",
" # save_df1, \n",
" # save_df2, \n",
" score_df\n",
" ]:\n",
" # 计算二分类指标\n",
" evaluation_metrics = calculate_binary_classification_metrics(sd, score_col='score', label_col='label', threshold=0.6,\n",
" future_return_col='future_return', total_mv_col='total_mv')\n",
"\n",
" # 打印指标\n",
" print(\"二分类评估指标:\")\n",
" for metric, value in evaluation_metrics.items():\n",
" if isinstance(value, (float, int)):\n",
" print(f\"{metric}: {value:.4f}\")\n",
" elif isinstance(value, (list, tuple, np.ndarray)):\n",
" print(f\"{metric}: (array of length {len(value)})\")\n",
" else:\n",
" print(f\"{metric}: {value}\")\n",
"\n",
" # 绘制 ROC 曲线\n",
" plot_roc_curve(evaluation_metrics)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"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": 29,
"id": "a0000d75",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"开始分析 'score' 在 'circ_mv' 和 'future_return' 下的表现...\n",
"准备数据,处理 NaN 值...\n",
"原始数据 17430 行,移除 NaN 后剩余 17123 行用于分析。\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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VuncuXN6+eWXs5T39/dX9+7d9d1332nHjh05XJ3nmDJliiSpT58+9gV0SXruuec0YcIE/fjjjyyie7j27dtr4sSJWrhwoV566SWzywEAAIDJWEQHAAAeJzIyUn/++aeCg4PT7UR3dJyLYRhKTk5WcnKyS2qw2WwqVqxYlrLz5s3T1KlTVbRoUY0aNSrd2eU3CgsL05QpU9S3b1+tXbtWzz77rF544QV1797dvuh6//33S5IuXbqk2NhYSdd+QZB6PZW/v7/279+vL7/8MsO5rl69qitXrqh///5ZupfMrFmzRg899FC2duYWKVJEknTlypVbnt8bJCYmasOGDfL391f9+vXTvJYnTx6VKFFC//zzjy5cuJDmmCB4lsDAQEVFRWn16tUsogMAAIBFdAAAPMG///6rCRMmaOPGjTpz5oyKFi2qBg0aqHfv3vZFyFQ2m00zZ87UggULdOTIERUpUkTVqlVTnz59VLFixXTvvXTpUs2YMUMHDhxQWFiYHnjgAb366quqUKFCmtyND2VctmyZ5syZo3379mnGjBnpzrPes2ePJk2apC1btighIUHly5dXx44d1bZtW6e/DwcPHlRwcLACAwMdLkTbbDbZbDb9+++/Dl8/efKkkpKS7DXv2bNHc+bMUXBwsCwWixITE/XBBx8oKSlJderUUVRUVLr3SUlJydIi+rx58/T2228rKChIbdu21bvvvquQkBAFBATcdDG9atWqqlSpkqZMmaJp06YpJCREffv2TZNZvHixEhMT7f/+559/VKZMGft7G4ahgwcPasaMGfbMhQsXZLPZlD9/fiUmJiopKckli+gWi+Wmu+xvtGfPHklK9xDVhIQETZs2TT/++KNOnDihggULqkaNGurXr5/Kli1rzy1cuFCDBg3SyJEjVa1aNX388cfatm2bAgMD9eCDD+qtt97K9K8FUlJSNGjQIC1evFjdu3dP9/11tYMHDyo5OVklS5a0P3T1ekOGDNH58+ez/fBXm82mhx9+WH5+flq3bl26ZwU0atRIVqtVv/32m/0vFK5cuaJp06bphx9+0PHjx5UnTx7dc8896tOnjyIiIpy/yWzau3evJk2apB07dig+Pl4lS5ZUq1at1Llz5wy/D+vXr9fo0aP1119/qUiRImrdurV69OiRLpucnKyvv/5aS5cu1ZEjRxQeHq6HHnpI/fv3V6lSpW657lq1amnhwoUyDOOm/y0DAAAgd2MRHQAAk8XHx6tDhw46duyYGjVqpHLlyuno0aOaO3eudu3apQULFtgXzGw2m3r27Km1a9eqXLlyat++vc6ePauVK1fql19+0cyZM9McCzJixAjNmDFDxYsX15NPPqmzZ89qxYoVWrdunaZOnaqaNWtmWNOQIUM0f/58lS5dWmXLllVoaGia19etW6devXopPDxcTZs2VWhoqH755RcNGTJE//77r3r37u3U96Jjx472h2jezMMPP3zTzL59+yRdOx5m4cKFCgoKUnJysq5evaqFCxcqKSlJefLkyXAR/ezZs6pevXqm72+1WjVnzhz5+fnpo48+0oULF/T5559nqX5JeuSRRzR58mTdd999mjdvnl599dU0rxuGoZkzZ9r//dNPP+l///uf3n33XbVt21Y2m02GYahly5Z677337Ll27drpzJkzGT589VacPHkyw4eJZiQpKUk7d+7UkCFD7DWlunr1qrp166bNmzerVq1aatiwoc6cOaOffvpJO3bs0Pfff5/mGBRJOnDggD744ANVrFhRTz31lH7//Xf98MMPSkhIcHjWumEYGjZsmNsW0CXpxIkTkpTul1+psvJzm5GAgAC1aNFC06ZN09atW1W7dm37a3/++aeOHTum559/Ps0RP6+//rp+/vln3XvvvXr22WcVHx+v5cuXq2PHjlq8eLFLFppvZs+ePerQoYNSUlL02GOPqVChQtq5c6fGjBmjU6dOaejQoWnycXFx6tq1q+677z61bdtWGzZs0Oeff669e/fq888/ty9mJycn6+WXX9bGjRtVq1Yt1a9fX0ePHtVPP/2kTZs2adGiRVn+SxJHSpQoIavVqvPnz3MuOgAAgI9jER0AAJNt2rRJR48e1dNPP60PPvjAfv2jjz7S8uXLdfToUd1xxx2Srj3scu3atXr44Yc1ceJE+87MH374Qa+99pomTpxoP4953bp1mjFjhqpUqaKZM2faj4749ddf9corr+j111/XihUr0u3uXLJkieLj4zV16tR0x1FI0uXLlzVw4EDlyZNHixYt0u233y5J6tu3r5544gl9+eWXio6OVsGCBbP9vXjttdfk7++vvHnzKjw83H7darVq6NChOnv2rF5//XWVK1dOSUlJGS5s2Ww2JSYm6ty5c/ZrTZo0kcVikSQNHDhQq1at0tatW9ONS3Xx4kVdvHgx3e5p6drO5tRfaoSHh2v27NnaunWrHnroISUmJqpFixYKDQ3Vli1b1KlTJ3Xu3Dnd+dcxMTF6+umn7QvSjRo1UqNGjdLN9f333+vvv/9WoUKFdO7cOdWtW1eLFi3SJ598ombNmunq1auSrh0P4w6JiYk3PYKkSpUqaf7t5+en559/Xh06dLBf++OPP7R582b7LxFSVa9eXcOHD9e6devUokWLNO/z9ddf6/nnn9dbb70lPz8/Wa1WPfroo1q3bp2uXLmS7hc9kjR8+HDNnz/fbQvo0rWfVUkKCQlx+Xu3atXKvnv/+kX0H3/80f56qosXL+rnn39WuXLlNGfOHPvPbIMGDfThhx9q27ZtbllE/+qrr2S1WjVu3Dg1b97cfv2pp57S/Pnz9dZbb6XZVX/27FlFR0fbf/litVr1/PPPa+3atVq9erWaNGkiSfrmm2+0ceNGvfzyy3r99dft46dPn66RI0dq2rRpevPNN2+p9tSf9ev/EgQAAAC+iUV0AABMlrpb8tChQ4qPj1e+fPkkSW+88YbeeOONNNnFixdLurYQfP3id9OmTTVmzJg0x0d89913kqT+/funWfhs0KCBmjZtqpUrV2rDhg165JFH0swRFxen6dOn64EHHsiw3vXr1+vs2bOKiIjQnDlz0ryWL18+nThxQtu3b89wUfhmMnog6OXLl9WzZ0+dPXtWvXv3VocOHdSwYUNFRUXp7bfftucuXbqkDz74QAMGDMjyw0Cvd/1Z6qlnkN945I10bYf19Qu2oaGheuihhyRdWzhNXTw9evSoJKlcuXLp3uP06dOSlOmu7qSkJI0bN05BQUFq166dJk+erEKFCumFF17QpEmTNGnSJD399NOSpKJFi2bnVp0WGhqq//77L9NMt27dJF07P33//v0aNWpUmsVdSXrooYfsfyWQ6siRI9q1a5eka0fW3KhkyZJ644037DuRw8PDVbNmTS1fvlz//fdfugXhESNGaPbs2apatarbFtAl2eu7/pcyrlK1alVVqVJFK1eu1LBhwxQQEKCUlBQtX75cFSpUULVq1ezZPHnyKDw8XOfOndORI0dUvnx5SdKjjz6qRx991OW1OfLJJ5/ok08+sf87KSlJ27Zt04ULF5SYmKiTJ0/afxEnScHBwWn+IiM8PFydO3fW66+/rjVr1tgX0ZcuXSrp2pE1Y8eOtefj4+MlXfvl5K06e/asJPf9kgoAAACei0V0AABMdu+996ply5ZaunSpHnzwQVWpUkURERGqU6eOmjZtmmax/NChQwoJCbEviKUKCQnR448/nubaX3/9JUkZnn189913a+XKlfrrr7/SLaI3atTI4QK6JB0+fFjStYXm1MXmG508edLxDWdDXFycevbsqX379qlevXrq1auXpGtnFS9atEjdunVTiRIlZLPZ9Nprr+mXX37R3r17NXv27GwvfN1zzz32Hf+//PKLpGu7WmfMmKG5c+fq/vvvl81mU0pKSpbeLyYmRlL6ndlS1hbRJ0yYoGPHjqlDhw5pFhk7duyov//+W23btrX34rbbbstSTbeqVKlSN+1tv379JEk1a9bUyy+/rEWLFqVbRJeufQ/mzp2rLVu2aO/evTp//rx9R3JG3+NmzZql+6uJ1B4bhpHm+vz583Xy5ElVrFhRe/fu1S+//JLu5zynpP4SLHUx90bt27fXnj17tGbNGqeOG2ndurVGjRqljRs36qGHHtLWrVt16tSpNDv9pWvHv7z55psaPny4oqKiVL58eUVERKhGjRqKiopy6hdNztq0aZOWLVumHTt26NChQ0pOTk5zRNX1SpYsme4on0qVKkn6v19MSdLff/8tSWmOO7qeKz6DTp48qXz58vEAWAAAAMj/5hEAAJDTPv74Yy1YsEC9evVS2bJl9fvvv6t///5q2bKlLly4kKX3uHLlihISEtItKGb3gXh33XVXpq+nvv/gwYO1b9++DL+effbZbM2Zke+++05PPfWUDhw4IElpjofp2bOnrly5omHDhikxMVF9+vTRL7/8oqZNm+qbb75xaudov3799M4778hms2nFihWqVq2aAgICdOzYMUnXFnGHDRuW5pgZR1JSUvT7778rLCxMkZGR6V5PXUQvXrx4huPXrVunKVOmqGDBgunOly9UqJDGjh2rihUr2h/ambrImNPuuecebdiwIUvZBg0a6O6779bGjRu1Y8eONK/FxMSoWbNm+vLLL1WoUCG9/PLLmjJlisOzzaWMd/Q7cvLkSb3yyiuaN2+eihUrpvfff99tR3Kk/vXCsWPHMtyNfvr0aSUnJ6dbKM6qFi1aKCAgwH6Ey48//ig/Pz+1bNkyXfaZZ57Rzz//rCFDhuj+++/X4cOH9d5776lJkybauXOn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7LHHVKxYMc2ePVvPPfecYmJi9MYbb6h27dqKiopSr1697At0jz32mOrWrauPP/5YGzZs0IQJE/T000/bF9D//fdfdenSRb/++qsGDBig2rVr2+e/fpfrnj17NGfOHPtDW6935MgRPfvss9q0aZPq1auX4SKeYRjavHmz9u3bl2a39M0kJydL+r8Hbm7btk1z5sxReHi4/diZ69lstkyPRrkVjRs3Vrt27dS/f3/t3bv3pvmWLVuqVKlSWrt2rf0Yob59+6p79+5KSkrSrFmztHv3bj3//POaOXOm/P39tWbNGpeeYX7//ferefPm+u2337Ry5UqXva8j/v7+GjdunIYMGaLg4GB9++23OnLkiJ5//nnNnj37lhdmw8LC7AvKTZo0yfD9goODNWvWLPXq1cv+ENelS5eqRIkSeu+999SjR49bqiErgoODNW3aNDVp0kRbt27VnDlzFBAQoLFjx9r/SmPZsmVpxpQrV06ffvqpveZLly6pS5cu9p3h17/3V199pf79+8tms2nu3Ln6448/1LRpUy1atEj33nuvUzUfP35c3bp1U926dfXcc885f/MAAADIVfyMnPr/YQEAALjAqlWr1LNnT0nSnXfeqWHDhqlWrVrpcgcPHtS3336r77//XhcvXtSTTz6pDz/8UNK1xeeMzjY/e/asZs6cqenTpys5OVnDhg1Tu3bt7K937txZGzdu1NChQxUSEqKPP/5YSUlJ+vXXX+1nr584cUIzZszQt99+q6SkJDVr1izDXf7R0dHatm2bfVH+jTfe0EsvvZSl78HUqVP18ccfa9q0aSpVqpSeffZZnT17Vj169FCfPn3S5Vu0aKFz587p999/z9L7Z1dSUpK6dOmiv/76S8uXL3f6IZmAJ7l69apatmypgIAAzZ49O8vPVwAAAEDux3EuAADAozVo0ED33nuvatWqpT59+jg8gqZixYoaNmyYBg8erD///DPNUSmOHg76yy+/aOrUqapSpYref//9NEeoSFKHDh104MABDR8+XNK1vxro169fmsW1lJQU/frrrwoMDNSbb76p559/PsO5OnTooMOHDysyMlItW7ZUVFRUlr8HqeffJyYmqmzZsho+fLi+/PJL+4M7M8pfvnw5y++fXcHBwfriiy+0YcMGFtCRawQGBuqtt95S1apVWUAHAABAGuxEBwAAHi8pKSnHHp55+PBhlStXzuFDSbPi6NGjCg4OVvHixV1Y2f85e/aszpw5o5IlS9qP7jAM45ZqBgAAAABkDYvoAAAAAAAAAAA4wINFAQAAAAAAAABwgEV0AAAAAAAAAAAcYBEdAAAAALxMYmLiTTOc3AkAAOAaLKIDAHKVn376Sd99953D1xcvXqwffvghS++VkJCgpKQkpaSkZHn+q1evKiEhQefPn8/yGHiu5ORkXblyxewyAOQCP/zwgy5cuGD/95UrVzR8+HC99dZb2X6v8+fP68EHH1Tnzp0VFxeXYcZms+nxxx9Xt27d9N9//zldNwAAAKRAswsAAMCVPv/8c8XFxempp57K8PWRI0eqQIECeuKJJ276XkOGDNGyZcucqqNo0aJav369U2NhjpSUFJ0+fVqHDh3Svn37tHPnTv3666/q06ePOnbsaHZ5ALzYyZMnNXDgQBUpUkSrVq1SUFCQQkNDtX//fm3fvl0vvviiKlasmOX3mzhxouLj42Wz2ZSUlKSDBw9KuvaLv+DgYFWoUEE//vijDh48qHvvvVdFihSxjzUMQ5cvX1ZoaKj8/dlTBQAAkBUsogMAvNq5c+f0999/KyQkRCEhIQoICFBQUJB9QeFGgYGB9tcNw1BiYqIKFiyoUqVKpcvWrl1bhQoVUmBgoAICAtK89vPPPysuLk7t2rVT3rx57devXr2qpKQkhYaGuvZGkSNOnz6t7t276+zZszp16pSSk5Ptr4WGhqpUqVI6cOCAiRUCyA2mTJmi5ORkde7cWUFBQfbrffv2VYcOHTR8+HDNmDEjS++1detWzZ49W5K0adMmRUVFpXm9WbNmGj16tMaNGyfp2l9gLVmyxP66zWaTJK1evVqlS5e+ldsCAADwGX4GB+UBALzYqlWr1LdvX/sCekJCgmw2mwoUKJBhPj4+Xn5+fsqbN68Mw1BSUpI6d+6sPn36ZGverl276pdffvHIRYjo6Ght3rxZ+/btM7sUr/Dqq6/KZrPp9ttvV0hIiKZOnao33nhDL774ovz8/Oy57du3a9euXRm+R0BAgKKjo2+5luTkZH355ZdavHixTpw4oTx58qhu3br69NNPb/m9gRsdPHhQzzzzjAYNGqQ2bdpkmImPj9ekSZO0YsUKnTlzRvfcc48GDRqkyMjIdNmkpCR99dVX9p/fihUr6vXXX9eDDz7ospqHDRum33//XWvWrHGY2bBhgyZOnKi9e/cqPDxcTz31lHr37q3AwPT7hywWi8aNG6ddu3bJz89PzZs314ABA5QnTx6X1fz333/riSeeULFixbRixQoFBweneb1Pnz5avny5Bg0apE6dOmX6XkePHlXbtm118eJFTZkyRdu3b9eECRP0ySefKCIiQklJSQoPD9f8+fM1ZcoUde3aVTVq1NC0adO0e/duffTRR0pOTlZiYqKaNm3q0vsEAADIzdiJDgDwak2aNJHFYrH/Ozo6WocOHXJ4lMoTTzyh8PBwzZs3z10lwsONHz/e/n8fPHhQU6dOVXh4eJoFdOnawtyECRMyfI/w8HCXLKKPGzdOU6dO1QMPPKBHH31U586d059//nnL7wvc6OzZs+rWrZsuXrzoMJOQkKDOnTsrJiZG1apVU7NmzfTrr78qOjpaCxYsSHP8iM1m06uvvqq1a9eqYsWK6tChg7Zt26aXX35Z06ZNU926dW+55unTp2vu3LkZ/uVQqh9++EEDBgxQaGionnjiCSUlJWnq1Kk6ffq0RowYkSa7efNmdenSRTabTVFRUQoLC9PixYv1999/6+uvv073GeCMlJQUDR48WMnJyerfv3+6BXTp2tFhGzdu1OjRo1WpUiU99NBDGb5XTEyMevbsqbNnz6pPnz6qV6+eQkJCNGHCBB08eNB+TNn69es1bdo03X333erbt6/8/f31zz//aPPmzSpQoIBq1ap1y/cFAADga1hEBwDkOmfOnFGVKlUcvn7vvfe6sRr3GzVqlC5fvmx2GblO6hE9P//8s0qWLGm/3qlTJ/31118umeP777/XHXfckWYBLzsPts3I0aNH1bhxYz355JP68MMPXVGmT9i0aZM6duyoXr16qXfv3maX41IHDhxQjx499M8//2SamzRpkmJiYvT4449r9OjR8vf3V+/evdW6dWu99dZbmjNnjj07d+5crV27Vvfff7++/vprBQcHy2azKTo6WoMHD9aKFSvSHGOSHSkpKRo3bpwmT56cae7MmTMaNmyYgoKCNGvWLN19992SpBo1aujtt99WVFSUfYE6MTFRAwcOVFJSkr744gs98sgjkqSmTZuqS5cumjdvntq3b+9UvdebNm2atm3bpho1ajh8FkexYsX04YcfqkePHurdu7fGjx+v+vXrp8kYhqHPP/9cJ0+eVOvWrdW9e3dJ0l133aWAgABt3bpV0rXvVeov+9599137med169aVn5+fLBYLi+gAAABO4EkyAIBcJ2/evBo5cmSGXyVKlDC7vBxXsmTJbD2gzpddunRJycnJcnS6XVJSki5duiSr1Wo/CsLf31+BgYH2Lz8/v3Rn5jvr5MmTuu2229LsgOXBf3ClQ4cOqV27dpKuHUvlyNWrVzV37lwFBQVpyJAh9p/D8PBwvfTSS9qxY4cOHTpkz3/77beSpKFDh9p3WwcEBKhHjx46duyYNm7c6HTNb7/9tiZPnqzu3btnugt98eLFSkhI0LPPPmtfQJekp59+Wrfffru+++47+7U1a9bo2LFjatKkiX0BXZLq16+vGjVqpMk6a8+ePfr0008VHh6ukSNHZrqzvVGjRho6dKisVqu6du2qKVOmpPkFmp+fnyZMmKB3333X/l6tW7fWwIEDNXXqVH311VeSJKvVquHDh+vdd99VeHi4Dh48qIMHDyowMFCfffaZGjRokKZvAAAAyBp2ogMAcp2goCCHu83DwsJuOj4hIUFBQUEKCgpy+s/5DcOQzWZTYmKiQkJCMjyLF+Z78sknM9yN+8477+idd96x//vll19W0aJF3VgZkDPOnDljX7DN7Fzx2NhYxcfHq169eipcuHCa11LPOF+/fr0qVKigs2fP6q+//lLZsmVVtWrVNNnatWsrKChIGzZsUIMGDZyqOSEhQZ999pmaNGmipUuXOsxt2bJFkvToo4+muR4YGKi6detq7dq1N82m3t9nn32mS5cupXlwdHYcP35cXbt2VVJSkoYPH67y5cvfdEyHDh0UFhamoUOHavTo0Vq9erWGDBliP38+ICBAzzzzjD1/9OhR5cmTR/Xq1bNfW7dunfr375/pPOHh4dqxY4dT9wUAAOCr+P/RAwBynXPnzikqKsrh6zc7zqV9+/Y6cOBAluZq3LjxTTPffPON6tSpk6X3u9H27ds1adIk7dixQ/7+/ipfvryio6P1xBNPOFzgz8qDRRs1aiTp2m7MQ4cO6auvvtKGDRv02GOP6c0330yTPX/+vD777DP9/PPPOnv2rEqWLKkGDRqoR48eKliwYLbvaeHChRo0aJAGDx6smJgYrVq1SuXLl9f48eM1ffp0LVy4UEWLFtXIkSN1//33q2/fvvrpp5/0008/qUKFCmnea+DAgVq0aJEWLVqku+66K9u19OzZU8nJyfad5IsWLdLmzZvVunVr1alTRzabTcnJybrzzjvTnL3vSjcePbR58+Y0167/+Um934weaHt9T1OPcLle6vcp1fXvkdqTkSNHpnvAZEZz3njUybJlyzRnzhzt27dPM2bMSLeQumfPHk2aNElbtmxRQkKCypcvr44dO6pt27bZ/n45uueb/RxfunRJX375pZYvX67jx4+rYMGCatSokfr166dChQqlua/rTZw4URMnTrT/+/r/riZMmKCJEydm+N94Rv8dXv99bt26tebNm6dFixbp4MGDWrt2rfLly5emjl69eqldu3YaNWqU1q9fr5SUFNWoUUNDhw5VmTJlnPqe1axZU7Vr175p7tSpU5KU4X9XpUuXVlBQkP0XUJllg4ODVbJkyTS/rJo4caImTJigzp07a+DAgfbrY8eO1RdffKFu3bqpX79+9usff/xxlv7a49SpU/Lz88uwjrJly+r8+fOKj49Xvnz57DVfv2P9+qxhGIqLi1NERMRN571RXFycXnjhBZ06dUrt27fXypUr9dtvvykwMDBLf1kyYsQIffjhh9qxY4dee+01LVmyxH6c1PUCAwPTfV9Sf1E8ZcqUDH9pER0dneX/9w0AAAD/h0V0AECuU7RoUYcPFk1ddMtMkyZNVLt27QwXKFL9/PPPiouLU7t27TLcqZiSkiKbzabLly87vYN56dKlGjhwoIKCgtSsWTPlz59fq1ev1uuvvy6LxaJBgwY59b7X++233/Tqq69KkipXrpzuuJtTp07pueeeU1xcnO6//341a9bMvlC6Zs0aLVy4UPnz53dq7nHjxum+++5T9erVtXHjRj399NMqVqyYHnvsMS1cuFBjx47Vt99+q1atWumnn37SsmXL1KtXL/v4pKQkrVq1SnfeeadTC+iS1Lp1a/v/ffXqVX366aeSpOrVq6tZs2YKCgqyH02RU4vo3bp1s//fX3zxhUqWLKmWLVvar11//npW5c+f3/6+ly5d0qxZs1SlShU1bNgwTcYVhgwZovnz56t06dIqW7ZsusW+devWqVevXgoPD1fTpk0VGhqqX375RUOGDNG///7rkvPGb/ZzHB8fr+eee04HDhxQ/fr11bhxY+3fv19z587Vtm3btGDBAoWFhalkyZL279vx48e1dOlS3X///br//vtvucbrXb16Va+88op+++03VahQQRUqVMhwcfX06dP2z5jWrVtr+/btWrdunU6ePKlFixY5ddRPVo8eunTpkiQ5/PzKly+fTp48maVs/vz57Vnp2s/8mjVrNHPmTLVp00Z33nmnDh8+rK+++kp33nmnevbs6XTN4eHhGf7FUerP+7///qt8+fLZay5SpEi6bIECBSRdO17JmUX0n376yX5UzJtvvqn77rsvW+PfeOMN1a1bV4MGDdIbb7xh/29qyZIl+vjjjxUcHCx/f39dvHhRO3fuVMOGDZWUlKTo6Gj7Z+HFixd1+vTpdO99/S8NAQAAkHUsogMAvNY///yjCxcu2I9ekaTLly/LZrPp4MGDGY5JTk5WYmJimteTk5Nls9nsOxL79u1707kPHTqkuLg4de3aNd2OYFc4deqUhg4dqpCQEC1YsMB+xvmrr76q5s2b65tvvlGXLl1UrFgxp+eIj4/Xa6+9pg4dOqh79+7KkydPusy7776ruLg49enTRz169LBfHzZsmObOnau5c+fq5Zdfdmr+atWq6auvvtLevXvVqlUrSdLs2bPtvyw4ceKEpGtnFBcuXDjdIvqvv/6q+Pj4NIvQt2LBggX6999/JV078uKZZ57R3XffneMP47x+x+0XX3yh0qVLp7nmjPz589vf4+jRo5o1a5buuuuuW37fGy1ZskTx8fGaOnVqugchStf+exw4cKDy5MmjRYsW6fbbb5d07b+xJ554Ql9++aWio6Od+ouGVFn5OR47dqz279+vd955R88++6z9+gcffKBvvvlGCxcuVIcOHVSmTBn792jTpk1aunSp6tat6/IHi3755ZcyDEPz589XtWrVHObmzZunpk2basyYMQoKCpLNZtNTTz2l2NhY/f333+n+MsOVUhdaM9oBLV3bYX7lypUsZy9cuGD/d2BgoD788EO1adNGw4cP18yZM/Xee+/JMAyNGjXK/our7PL393d4ZFfqe6Y+dDm15ozyN2az65VXXlHVqlVVt25dBQcHa/PmzQoLC1NgYKAaN24sq9WqTZs2pRv3xBNP6MiRI/bjc6ZNm5bm9Vq1aumDDz5QcHCwfvzxR82fP18REREaPHiwrl69qhIlSth3/L/22msO6+NoKgAAgOxjER0A4LUmT56sBQsWZPhaZse5nDp1Kt3rme1eN8Py5ct15coVvfLKK2keEpo/f36NHTtWp0+ftv/iwFkXL15Uq1at9Prrr2f4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5+zZoKAgffTRR2mORvn333/17rvvqmjRoho8eLAk6c0339Svv/6q999/X7Vq1VLp0qWdqvnDDz9Up06dNHHiRH355Zey2Wyy2Wxq3LixnnvuuTTZgQMH6tChQ5o3b54WLVokwzB09epVVatWTX369HFq/ozMnTtXM2fOVIkSJRy+b+rxP44W0Q3D0JgxYxQUFKT3339ffn5+Gf6iJiu/vLl+5z0AAACyhkV0AIDXSl0AvV7qYnqqoKAgBQQE2B98l7q7NFWePHn0yiuvZGm+/fv3288iT915mpUjH5zVunVrlS5dWp9//rlWrVqlq1evqlKlSurRo4eefvrpHJv3eiVKlNCCBQs0adIkrVy5Ujt27FDRokX1/PPPq1evXm4/S75169b6448/VKJECdWtW/eW3itPnjwaN25cmgftZbYAlXr+u7+/f5oxfn5+SkxMvKVasuree+/V6NGjNWnSJPXu3VuFCxdW8+bN1bt3bz3xxBMOxw0dOlQFCxbUihUrtHbtWuXPnz9NPm/evPryyy/1wQcfaOTIkQoODtYDDzygBQsW6LPPPnN6EV2SGjZsqDlz5mjSpElav369kpKSVL58eQ0fPtxtP8f58+fX3Llz9cUXX2j58uWaPXu2ihcvrrZt26pnz54Oz6EeO3asxowZo3Xr1um///5TkSJFFB0dbX+9VKlSmjRpkkaPHq1BgwYpb968atiwoebNm+fSRVgzvfDCC6pZs6a++eYbxcXFqVKlSurWrVu6z1JJat68ue666y599dVX+uuvv1SyZEn72dypDMPQwIEDdeHCBY0fP95+vEq+fPn07rvvqlu3bnrjjTc0a9Yspz5fixYtqoULF2rmzJn69ddfFRgYqKioKLVp08a+SztVWFiYZsyYoQULFmjFihW6evWqGjZsqOeff94lD+BMSkrSp59+qqlTpypfvnyaPHlyhg8rPXfunL7//ntJcvjLAz8/P40bN067du1S5cqVJcn+C4Lrpf77+uNebpT6PQcAAEDW+Rm54W9NAQA+KykpSRMnTlRISIh99/n1Zs+erYSEhHQLCikpKbLZbEpKStLTTz+tMmXK3HSulStXqnfv3vZ/BwcHa8uWLQoNDXXNzcB0FotFTz31lIYOHarnn38+zWv//POPjh49qpo1ayokJMR+/fz587p69arDneAAfEtSUpK+//57ffHF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cZwLAAAAAAAAAHipxo0bZ/r66tWrM7weHx+vO+64I821AgUKKDY2NtP3i4+PV0BAgMqUKWO/lj9/fp08eTLD/KRJk1SyZEk1bNhQ0rVF+AYNGujFF19UcnKy3nvvPfXr108LFizIdN6s2LVrl5YvX659+/bp1KlTCggIULFixRQZGanHHntMVapUcep9WUQHAAAAAAAA4BM4E/3/BAYGKiQkJM210NBQWa3Wm44LDg5Ocy0sLCzDcRs3btTMmTP19ddfKzDw2lJ06rEvqd577z01adJEhw4dsu9Wz659+/bpvffe0/nz5/XYY4+pS5cuKlasmGw2m06fPq3NmzerZ8+eqly5st566y2VLl06W+/PIjoAAAAAAAAAeClHO81vplChQjpz5kyaa5cuXUq3QJ7RuMuXL+vSpUvKmzevpGu7028cFxcXp/79+6t///6qVauWw/crXry4JOno0aNOLaLPnz9f48aNU79+/fT000+ne71KlSp66KGH1KdPH3399dd65plnNGLECDVo0CDLc3AmOgAAAAAAAAD4mOrVq+vAgQO6ePGi/VpMTIxuv/32TMeVKVNGRYsW1bZt2xyOO3v2rLp27aqGDRvqxRdftF//77//1Lx58zS71lPfp2TJktm+hwMHDujrr7/W7NmzM1xAv15AQIC6dOmiiRMn6oMPPkhz3zfDIjoAAAAAAAAA+JjKlSurQoUKGjNmjFJSUrR7926tXLlSjRo1UkpKii5evCibzZZunL+/v6KiojR+/HhdunRJ586d0/Tp09WoUSNJUmJiorp06aLChQtr4MCBSkhIUEJCgpKSklSkSBHlzZtXb731liwWi9auXashQ4aoXr16aR5wmp17+OGHH1S2bNksj6levbp++ukn5c+fP8tjWEQHAAAAAAAA4BOMXPh1K0aOHKnVq1erXr16ateunVq2bKkGDRro+PHjqlWrlvbv35/huFdffVVBQUFq0KCBGjVqpNDQUHXr1k2S9Pvvv2v37t3asmWLatWqpfvuu0/33Xefhv0/9u49zsZ6////c81gWBhGTskg0bZYI5JDSQoVaqSDDk7ZqcghKeWQQyEiKaWDdnaF7N2mHZ3syuHbrvZWk1MulkNJiSK7jRlrGoz1+6Nf89ljrDHXmmvW+1qzHvfbbW41az3f7/fr6tXt+uPt8r4mTpQkPfPMMzpy5Ih69+6tCRMm6Oqrr9bcuXMjvoaEBPtb3HbHeEKhUHH/W8cUy7JMlwAAAAAAAAA4wu/3my4hpvzh75NNl+C47TdMLNb4YDCojIwMpaSkqHnz5kUed/LkSa1bt07Hjh1TmzZtVLZs2WLV4WZx+WLRU28uwWBQgUBAPp9PXq+3QN6yLFs3JPJm8+HGFNZnt10D+cjG0GP35p1agx5HL2+qJnoced6NNdFj82vQY7N5N9ZEj82vEQv5WOqxG2uKhbzJHkdjjXjLA8Xl9XrVsWNH2+MSEhIKfWFoaRKXm+gAAAAAAAAAgNKhU6dO8ng8Rc6vWrXK1vxsogMAAAAAAACIC6FQ0TdaETuGDx9eovOziQ4AAAAAAAAAiFnXX399ic5v/9WlAAAAAAAAAADECTbRAQAAAAAAAAAII6LjXA4ePKgPPvhA27dv14EDB5SYmKgaNWrI7/fryiuvVJUqVZyuEwAAAAAAAACKJ2S6AETT8ePHtW/fPp199tk6cuSIqlevHtE8tp5EP3jwoB566CGlp6dry5YtSktL02233aYbb7xRPp9P//73v3XVVVfpscceU1ZWVkQFAQAAAAAAAAAQqezsbI0ePVoXXnihunXrpl27dmnGjBnq2bOnDhw4YHu+Im+if/rpp+rRo4fOOeccrVmzRtOmTVOvXr3UsWNHderUSbfccouefPJJffTRRzpx4oTS09O1bds22wUBAAAAAAAAABCpxx9/XP/+97/10EMP6eTJk5Kku+66SwkJCZoxY4bt+Yq0ib5z506NGTNGzz33nEaMGKHy5cuHzSYnJ2vSpEkaN26c7r77bh05csR2UQAAAAAAAAAAROKjjz7ShAkT1K9fv7zPzj//fI0cOVKffvqp7fk8oVCoSCcBZWVlqVKlSrYmj2RMSbMsy3QJAAAAAAAAgCP8fr/pEmJK4yVTTZfguJ29xpsuwXXatGmjWbNm6bLLLlOTJk20bNkyNWnSRCtXrtS4ceP0xRdf2JqvyC8WTUxM1Nq1a9WuXbsiT+62DfTfnXpzCQaDCgQC8vl88nq9BfKWZdm6IZE3mw83prA+u+0ayEc2hh67N+/UGvQ4enlTNdHjyPNurIkem1+DHpvNu7Ememx+jVjIx1KP3VhTLORN9jgaa8RbHkBBV1xxhZ544gnVrFkz77MdO3Zozpw5uuKKK2zPV+RN9H379mnQoEHatGlTge/atm2rChUqKDEx8f8mLlNGPXv21D333GO7KAAAAAAAAAAAIjFu3DgNGzZMPXv2lCTddNNNys3N1UUXXaSxY8fanq/Im+hJSUkqU+b08cOHD2vSpEn5Plu7dq3mz5/PJjoAAAAAAAAAIGqqVKmihQsX6osvvtCOHTskSY0bN1bbtm0jmq/Im+gejyfvSfOnnnpKSUlJOn78uO69915JUvfu3fPl69atq2PHjunYsWMqV65cRMUBAAAAAAAAgFOK9nZIlBZt2rRRmzZtij1PkTfR/9e8efPUuXNnffbZZxo2bFje52+88YbKli2rnJwc3XbbbWrevHmxCwQAAAAAAAAAwI5PP/1Ur732mr777jvl5uaqfv366tu3rzp16mR7roRICvB4PHruuedUpUqVvN8l6fHHH9frr7+uxx57LJJpAQAAAAAAAAAolsWLF+vOO+9UVlaWrrjiCl111VU6fvy4hg4dqr/97W+254voSfTf/b55/rvatWvrzTffVOvWrYszLQAAAAAAAAAAEZk3b57uvPNOjRo1Kt/nTz31lF5++WXdfPPNtuYr0ib6G2+8UaQd+lM31QEAAAAAAADALUIh9i/jQWZmptq3b1/g83bt2mnBggW25yvScS6bN2/W8ePHbU8OAAAAAAAAAEA03XDDDXrllVd09OjRvM+ys7O1aNEidevWzfZ8nlDozO+kzc3N1Y8//qgbbrhBX3zxhXw+nwKBgC6//HKtWrVKfr9fgUBA3bt31/vvv682bdroiy++sF2MJC1fvlxz5szR6tWrJUlvvfWWnn32WR09elQ333yzRo4cqYSEiI5ylyRZlhXxWAAAAAAAAMBN/H6/6RJiynlvlL53OX5zy8OmSzBu8ODB+X4PhUL67LPPVL58eTVp0kQej0c7duxQVlaWLr74Yr388su25i/ScS6JiYlFOqrlu+++k9/vV25urq0ifvfTTz9p6tSpqly5siTpn//8px5++GE9+uijat26tcaMGaNFixapf//+Ec3/u1NvLsFgUIFAQD6fT16vt0DesixbNyTyZvPhxhTWZ7ddA/nIxtBj9+adWoMeRy9vqiZ6HHnejTXRY/Nr0GOzeTfWRI/NrxEL+VjqsRtrioW8yR5HY414ywOQUlJSCnyWnp6e7/e6detGPH9ELxYNhUIaNmyYDh06lPe7JP3jH/9QcnKyDh8+HNGcY8eOVe3atfMes3/llVd01VVXqVevXpKkMWPG6KGHHir2JjoAAAAAAAAAoHSYPn16ic4f0Sb6LbfcogoVKuimm25SQkJC3lPqqampkqQqVarYnnPRokXat2+fxo4dq8mTJ0uStm7dqoceeigvk5aWpn379umXX35RtWrVIikdAAAAAAAAQLzixaJxIxgM6ptvvtGvv/6a99mJEye0bt06DRs2zNZcRd5ED4VCOnnypCTp0UcfLfDdhAkTTjtuypQpZ5z722+/1dNPP61XXnlF2dnZeZ9nZmaqfv36eb8nJiaqYsWKOnDgQLE20YPBYL7ff1/zf9c+0xi7a5CPbv50Y87UZ7ddA3n7Y+ixu/NOrEGPo5uPxhr02Nl8NNagx87mo7EGPTabj8Ya9NjZfDTWcHs+1nocjTVKW950j6OxRjzlT3c0MRDv/v3vf+vee+9VVlaWpP87ScXj8SglJcX2JnqRXiwqSd98842uu+66076Y87777lO5cuXynkoPhULKzc3V8ePH9fTTTxc6b25urm677TZ17NhRQ4cO1eeff66xY8dq9erVSktL0+uvv67mzZvn5Tt27KjZs2erVatWti70d5ZlKScnx9aYpKQkW2PIm827sSbyzubdWBN582uQdzbvxprIm1+DvLN5N9ZE3tm8G2sib34N8s7m3VgTefNrxFs+0j2yeHXeX6eZLsFx39w6znQJrtOzZ09deOGF6tmzp3r37q1//vOf+u9//6s//vGPGjVqlHr06GFrviI/iV6vXj2tWLHitN+daaO8MC+++KISEhIKvEFV+u1A+IMHD+b7LCsrS+XKlYt4PUny+Xz5fs/Oztbu3bvVoEEDVahQoUB+165dBcYUhrzZfLgxhfXZbddAPrIx9Ni9eafWoMfRy5uqiR5HnndjTfTY/Br02GzejTXRY/NrxEI+lnrsxppiIW+yx9FYI97yAAravXu3Jk6cqObNm6t+/fpav369unTpomHDhumFF14ouU30smXL5p157qQ333xT//nPf9S2bVtJvz2Znp2drYsuukg+n0/r1q1Tp06dJP12E8nKytLZZ59drDXD/TWXChUqhP3O7l+NIW82X9iYcH122zWQj3wMPXZn3sk16HF08tFYgx47m4/GGvTY2Xw01qDHZvPRWIMeO5uPxhqxko+VHkdjjdKaN9XjaKwRb3kU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bdmyRf/617+UlJSkiy66SMePH1e1atV02WWX6e6779YVV1yhihUrSpIOHDig++67T3/5y19sFQQAAAAAAAAAQKSef/55DRo0SCNGjMj77LrrrlPt2rU1d+7cktlEr1ixovx+v9q1a6cLL7xQ27Ztk9fr1d///ndlZGSoRo0auvbaa/PyOTk5GjVqlHr16qUlS5aoV69etooCAAAAAAAAAMeFTBeAaPjxxx/Vpk2bAp+3adNG8+fPtz2fJxQKnfF/nWAwqMsvv1zdu3dXlSpV9PXXX+u5557TY489pnPOOUcpKSl6+eWX1b17d/Xp00d///vf9cknn6hz585q0KCBLrnkEtuFlRTLskyXAAAAAAAAADjC7/ebLiGmNHhxlukSHLd78CjTJbjOrbfeqlq1amn27NlKTEyU9Ns7PUeOHKn9+/frr3/9q635irSJLknfffedXn31VZUvX1579+7V2WefrYyMDL3wwguSpM6dO6tly5baunWrkpKSNH78eHXv3t3m5ZU8y7IK3FyCwaACgYB8Pp+8Xm+Rxthdg3z08uHGFNZnt10D+cjG0GP35p1agx5HL2+qJnoced6NNdFj82vQY7N5N9ZEj82vEQv5WOqxG2uKhbzJHkdjjXjLwx420ePDpk2bNGDAANWoUUOtWrWSJK1fv14HDhzQa6+9pubNm9uaL6EooW3btmnt2rVKS0vTf//7X/34448qX768evTooe+//16SVK5cOS1cuFCPPPKIfvnlF23evNnmpQEAAAAAAAAAUDwXXHCBlixZopYtW2rz5s366quv1LJlSy1dutT2BrpUxDPRLcvSa6+9pjJlyigzM1M//fSTfv75Z6WkpGjbtm367rvv8rK5ublq27atVqxYoXbt2qljx462iwIAAAAAAAAAx3Emetxo1KiRZsyY4chcRdpEv+mmm3TTTTcpIyND33zzjZ566in9+uuvmjp1qrxer5566ilt3LhRN998s5KSktSjRw9Vq1ZN06dP12WXXSaPx+NIsQAAAAAAAAAAnMnhw4f1448/qkmTJtq7d6/+3//7f7r66qtVvXp123MV6TiXkydPasKECbr//vv1n//8RxdeeKFGjRqlu+++W+XLl9eDDz6Yt3n+3XffqX379urcubM8Ho9WrlxpuygAAAAAAAAAACKxZcsWde3aNe99nr/88otmzJiha6+9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r18/x+dNiHTgyZMnde+99+q+++5zsBwAAAAAAAAAANwj4k30F154QVu2bNFVV13lZD0AAAAAAAAAALhGRJvoK1as0HPPPadevXrJ4/Fo48aN+umnn3Ty5Emn6wMAAAAAAAAAR3hCpe+nOA4ePKghQ4aoZcuWuuGGG7Rt27YijcvNzdWMGTPUtm1bXXHFFXr//ffzfb97927169dPLVu2VP/+/bVv375837/11lvq1KmT2rZtqyeffNL1+8q2z0RfuHChHn/8cV133XUaMGCArrnmGnk8HklSQkKCatSoodq1a6tFixYaNmyYKlWq5HjRAAAAAAAAAIDIhUIhDRs2TJK0dOlSffXVVxoyZIjeeecdVaxYsdCxTz/9tJYvX65nn31WZcuW1fDhw1W/fn01a9ZMOTk5GjhwoJo0aaLly5drxYoVGjZsmJYuXaqEhAT985//1MMPP6xHH31UrVu31pgxY7Ro0SL1798/GpcdkSI/ib57924NHDhQ06dP15133qnp06fnbZ6vWbNGf/nLX/TUU09p4MCBatq0qd544w3Nnj27xAoHAAAAAAAAAERm/fr12rBhg6ZOnarzzjtP119/vc4991ytXLmy0HHHjh3TokWLNGzYMLVp0ybvafPFixdLkj744AMdPHhQU6dOVb169TRo0CBlZ2dr/fr1kqRXXnlFV111lXr16qUGDRrkbaK7WZGeRP/73/+u8ePHy+/3669//auaN2+e953H41Ht2rVVu3btfGMSEhK0evVqTZw40dmKAQAAAAAAAACSpM6dOxf6/apVq077+datW1WnTh01atQo77OWLVtq06ZNuu6668LO9+233yoYDKpDhw75xr399tt586alpSklJSXv+xYtWmjTpk266KKLtHXrVj300EN536WlpWnfvn365ZdfVK1atcIv1hBPKBQ648k5e/bs0TfffKPLL7883+e7du3SNddco0AgUGDMggULFAwGNXjw4DMWsXDhQk2dOjXfZ2PHjtWAAQP01Vdf6dFHH9WuXbvUvn17TZ06VVWrVj3jnOFYlhXxWAAAAAAAAMBN/H6/6RJiSsM5pe/kjHPffq/Q78Ntoj///PP6/PPP9dprr+V9tnDhQq1du1bPPfdc2Pm+/PJL9e/fX1u3bs37bPv27erbt68yMjL08MMPKxQKadq0aXnfP/744zp58qTGjRun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NeIt36pVqyJnITUbUwrPRH+cM9FP9Z///EcPPvig0tPTdeWVV+q6667Tvn37JEkjRozQ4MGDbc1XrE300/nqq680btw4jRw5Up07dy7SmHfeeUdjxozRV199pcTEREnS/v37ddlllykhIUHjxo3Lt2Gfnp6uoUOHqmvXrrbrsyxLDRs2zPdZdna2du/erQYNGqhChQoFxuzatavAmMKQN5sPN6awPrvtGshHNoYeuzfv1Br0OHp5UzXR48jzbqyJHptfgx6bzbuxJnpsfo1YyMdSj91YUyzkTfY4GmvEW54n0e1pNroUbqLPYBP9TI4ePaq1a9cqJSVFF154oe3xxTrO5XSaN2+uZ555Rvfee2+RN9Hr1KmjkydP6tdff817nP6HH36QJPXs2VPr1q3L20Q/evSovv32W9WpUyfiGsPdXCpUqBD2O7s3JPJm84WNCddnt10D+cjH0GN35p1cgx5HJx+NNeixs/lorEGPnc1HYw16bDYfjTXosbP5aKwRK/lY6XE01iiteVM9jsYa8ZYHULiKFSsWea/6dIr1YtFwzj77bP34449Fzl9wwQU699xzNXHiRO3Zs0dbtmzRY489pksuuUT9+vXTypUrlZGRIem3c9hTUlLk9/tLonQAAAAAAAAAQIx788031a9fP3Xs2FFff/21pkyZomnTpunEiRO25yrWk+jLli0r8Fl2drbeffddnX/++UUvokwZzZ8/XzNmzNBNN92kY8eO6eKLL9aUKVN01llnafjw4RowYICqVq2qYDCoOXPmKCGhRPb/AQAAAAAAAAAx7LXXXtPMmTPVqVMnffnllzpx4oRatWqlyZMnq3z58rr//vttzVesTfRnnnmm4IRlyqhJkyZ68MEHbc119tln6+mnnz7td4MGDVL37t21fft2paWlqVatWpGUCwAAAAAAACCOeRx9OyTcauHChRo7dqz69u2rJk2aSJK6d++ukydP6oknnrC9ie74i0XdzrIs0yUAAAAAAAAAjuDIY3v8D5W+F4taM3mx6KlatGihl19+WRdddJGaNGmiZcuWqUmTJvr3v/+te+65Rxs3brQ1n60n0T/55BMlJyfrggsusLWI25x6cwkGgwoEAvL5fKd9cYNlWbZuSOTN5sONKazPbrsG8pGNocfuzTu1Bj2OXt5UTfQ48rwba6LH5tegx2bzbqyJHptfIxby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OXKkli5dqszMTCUmJio1NTVvbHJyct5YkzyhUKgETs5xL8uyTJcAAAAAAAAAOMLv95suIaZcesMs0yU4LunwikK/P90m+n/+8x9lZ2efNn/HHXdo5MiR6tatW95nvXv31q233qoePXoUutbEiROVmJioSZMm5X329NNP5x3xcqo9e/aoS5cuWrFihY4cOaIBAwZo48aNed9///336tatm/Fz0ePySfRTby7BYFCBQEA+n09er7dA3rIsWzck8mbz4cYU1me3XQP5yMbQY/fmnVqDHkcvb6omehx53o010WPza9Bjs3k31kSPza8RC/lY6rEba4qFvMkeR2ONeMsDp9skP5Ozzjqr0O8OHjyY77PMzEyVK1fujPOmpKRo165d+T7LysoKO7ZmzZqSpB9++EH169dXdna2srKyVKlSJVvrljTORAcAAAAAAAAASPrtDPN169bl/X706FF9++23qlOnzhnHtmzZUhs2bFBubm7eZ5s3b9bZZ5+t//znP+rWrZuCwWDed7+vU6dOHaWmpqp69er51v59rGlsogMAAAAAAAAAJEnp6elauXKlMjIyJElz585VSkpK3t+QyMnJUVZW1mnHtm/fXidOnND8+fMlSWvWrNFXX32lTp066ayzzlKlSpX08MMPy7IsrVmzRuPHj9cll1yiRo0aKSEhQd27d9czzzyjrKws/fe//9Wrr76qTp06RefCCxGXx7kAAAAAAAAAiD+euHo7ZGSaNm2q4cOHa8CAAapataqCwaDmzJmjhITfnsd+6aWXtHLlSi1fvrzA2LJly2rWrFkaOXKkXn31VR06dEhDhw7VeeedJ0l65plnNH78ePXu3VvJyclKT0/XsGHD8sbfe++9uuuuu3TZZZcpFAqpfv36Gjx4cHQuvBBsogMAAAAAAAAA8gwaNEjdu3fX9u3blZaWplq1auV9N3z4cA0fPjzs2EsvvVQfffSR1q1bp3r16qlx48Z535199tl5T6mfTuXKlbV48WKtW7dOx44dU5s2bVS2bFlnLqoY2EQHAAAAAAAAAOSTmpqq1NTUiMZWrVpVnTt3jmhsQkKCWrduHdHYksKZ6AAAAAAAAAAAhMGT6AAAAAAAAADiQ4hD0WEfT6IDAAAAAAAAABCGJxSKrz9+sSzLdAkAAAAAAACAI/x+v+kSYkqHnk+YLsFxnyx70HQJpV5cHudy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Ffvv45M6duyou+66S2vWrLGveX8N92rXEhkZqQEDBuj555/XpEmTFBAQoIiICPXp00ePPPKIFi5cyOsuF5eQkKDBgwfriSee0Jdffqlx48ZpyZIlCgsLU6dOnfTJJ59o48aNOd74pMauJSkpSWPHjlWvXr20ePFiffDBBypdurRq1qypWrVqKTw8XBs3buTnGACcyG0fLHpNYGCgRowYoWXLlumVV17RN998Iw8PD+3bt0/Dhg2Tj4+P0Snib5hMJnl7eys2NlZHjhxRiRIl1Lt3bwUGBmrGjBl68cUXNW/ePPXo0UMhISGSrr6oSExMZKkeF5GZmSlfX1/17t1bktS5c2ft3r1bn332mSIiIpSWlqavv/5aO3bs0MMPP6zU1FRFR0drwIABBmeOvLBarapevbrat28v6WpD5pNPPtG2bdvUtm1bHTt2TJs2bdKOHTvUsmVLWSwW6uxisrKyZDKZVK5cOXXv3l0XLlxQWFiYFi9erIoVK+qNN95QSEiImjRpovPnz+v06dNq1KiR0WnjFpnNZtWrV0/9+vWT1WrVwIEDlZKSosTERLVr105paWk6c+aMJkyYoJCQEGVkZGjXrl2aOHEinwpzQbVr11ZERIQaNmyo7du3a8eOHbLZbIqPj1dCQoJ27NihFi1aKC0tjXu1Czl37pzCwsIUGhqqESNGSJJatmypxYsXS7r6KaH333+f110u7syZMwoKCtLo0aMlXX1WxbJly7R79249+OCDOnTokH766Sdec7mwc+fOqUyZMgoNDZWPj4/atGmj5ORkvf3221q9erVWr16tFStW8HMMAE7k9k10SfYnzbdp00Y//vijkpOT9c4776hFixZGp4ZbYLVa5eHhoWrVqikzM1Pbt2+XyWRS7969VbVqVc2aNUvvvvuuli5dqrlz5yooKEgbNmzQiy++yJskLiIzM1OJiYm6dOmSfVuzZs3syzCNGTNGW7Zs0d69ezV//nyZTCaNHDlSrVu3Nipl5FN6erpOnjypWrVqaenSpfr999+VkpIiSapcubKqVasmPz8/hYeHy9PTkzq7mGuzVu+//34lJSXp+eef19SpUzVkyBANGjRINptNp0+f1urVq+Xh4aGhQ4eqWbNmBmeNW9WsWTOtXbtWixcv1h9//KHSpUvr3Xffldls1t69e2WxWHTvvfcqKChIq1atkslk0ssv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8X/9mw7/+c9/5OnpecufWgAAAABwczTRAQCA26hbt64k6bfffrNv+/PPP/XUU0/p8OHDLjPGNV27dtXWrVu1detWtWjRQv7+/vk+1+XLlzVv3jw9+uij9tnp1yQkJOSriX7+/HnFxcXp999/V82aNRUQEHDD2GXLlunChQsym8369ddfNWvWLA0cOFDe3t6SpAEDBujw4cOKjIz827Xb/86jjz6q1atX59q2c+dORUVFSbpar82bN992w/56p06d0tNPP63Q0FCFhYXl+fjrm+ilS5fW008/re+//97+NXny5L9dHmbNmjV69NFHc50PAAAAQP6wnAsAAHAb9957rzp37qy3335bVqtVd911l+bNm6fq1avroYcecpkxruncubNmzJihtLQ0Pf744/k+z8WLFzV06FClpKRo7NixOfYlJSXp4sWLt7R8iNVqVVZWlooXL66GDRvqxx9/VPXq1bV79+4cs9uzs7Nls9nszd7+/furdu3a2rVrl1544QVZLBaVLl1ahw8fVoMGDXT58mV5e3vLZrMpOTnZYTP+VvTp00ddu3bVoUOH1LBhQ0lSu3bt1KxZMz3zzDMKCQnRzp07Va5cOfXt2zff41wvOjpaw4YN04QJE/L9iYHr13NPTk7WV199pcWLF9u3ZWdnq2TJkg7P8ddff+nLL7/UypUr85UDAAAAgNxoogMAALcyffp0zZw5U9OnT1d2draaNm2qqVOn2mc9u8oY0tW1vBs1aqR9+/Zp7ty5eT7earVq48aNev/993XhwgWFh4fbZ5zHxMTou+++0969e1WsWDHVr1/f4XmysrIkXZ1p3alTpxvG7Nu3T1999ZX9+2nTptkb/8WLF1ft2rW1du1alSlTRu+//75+/vln7du3T+fOndPEiRPVs2dPxcbGKjQ0VAsWLLhpPjdTunRpTZgwQePGjdOyZcsUEBAgk8mk+fPn66OPPtL27dvVvHlzjR07NteM/Nvx4Ycf6q233lKLFi3yfQ6r1Zrj+xkzZjiMPXfunI4ePaqzZ8/al3NJT0/Xyy+/rOeeey7fD2cFAAAAkJvJdv2UFwAAAOSb1WrN1Qz9Xx4eHre81vrt2rt3r5599lk99NBDmjx5sqpVq2bfl56ervbt26t69er65z//qZCQEIfneffdd3Xo0CEtXLhQx48fV4kSJW66VEhmZqYqVKigcuXKadu2bfr222+1bds2de/eXS+//LLKli2rU6dO6a233tKuXbs0duxYhYaGKjU1VUOGDNGBAwc0bdo09ejRI9/XPnPmTJ09e1Zvv/12vs9R0I4fP65u3brd0oNF//jjD3Xt2lU1a9bU3LlzVbVqVc2ePVtxcXGaMWMGS7kAAAAATkQTHQAAwInGjx+vb7/91uH+0NBQvfbaawWWzx9//KG77777ts7x+uuv6+jRo1q+fHmej42JidG6dev05JNPqmrVqvbtmZmZmjNnjp544glVr17dvj07O1vLly9X37595eXlle+cbTabLl26JD8/v3yfo7BLS0tTqVKlcnzv6emp4sWLG5gVAAAA4H5oogMAADjR6dOndfHiRYf7AwICFBgYWIAZAQAAAABuB010AAAAAAAAAAAcKJgFOQEAAAAAAAAAcEE00QEAAAAAAAAAcIAmOgAAAAAAAAAADtBEBwAAAAAAAADAAZroAAAAAAAAAAA4QBMdAAAAAAAAAAAHaKIDAAAAAAAAAOAATXQAAAAAAAAAABz4f+vX7CVnnsudAAAAAElFTkSuQmCC",
"text/plain": [
"<Figure size 1600x1200 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"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}\")"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "a436dba4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Empty DataFrame\n",
"Columns: [ts_code, trade_date, is_st]\n",
"Index: []\n"
]
}
],
"source": [
"print(df[(df['ts_code'] == '600242.SH') & (df['trade_date'] >= '2023-06-01')][['ts_code', 'trade_date', 'is_st']])"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "stock",
"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.13.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}