{ "cells": [ { "cell_type": "code", "execution_count": 1, "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": [ "e:\\PyProject\\NewStock\\main\\train\n" ] } ], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", "\n", "import gc\n", "import os\n", "import sys\n", "sys.path.append('../../')\n", "print(os.getcwd())\n", "import pandas as pd\n", "from main.factor.factor import get_rolling_factor, get_simple_factor\n", "from main.utils.factor import read_industry_data\n", "from main.utils.factor_processor import calculate_score\n", "from main.utils.utils import read_and_merge_h5_data, merge_with_industry_data\n", "\n", "import warnings\n", "\n", "warnings.filterwarnings(\"ignore\")" ] }, { "cell_type": "code", "execution_count": 2, "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" ] }, { "name": "stdout", "output_type": "stream", "text": [ "daily basic\n", "inner merge on ['ts_code', 'trade_date']\n", "stk limit\n", "left merge on ['ts_code', 'trade_date']\n", "money flow\n", "left merge on ['ts_code', 'trade_date']\n", "cyq perf\n", "left merge on ['ts_code', 'trade_date']\n", "\n", "RangeIndex: 8595791 entries, 0 to 8595790\n", "Data columns (total 32 columns):\n", " # Column Dtype \n", "--- ------ ----- \n", " 0 ts_code object \n", " 1 trade_date datetime64[ns]\n", " 2 open float64 \n", " 3 close float64 \n", " 4 high float64 \n", " 5 low float64 \n", " 6 vol float64 \n", " 7 pct_chg float64 \n", " 8 turnover_rate float64 \n", " 9 pe_ttm float64 \n", " 10 circ_mv float64 \n", " 11 total_mv float64 \n", " 12 volume_ratio float64 \n", " 13 is_st bool \n", " 14 up_limit float64 \n", " 15 down_limit float64 \n", " 16 buy_sm_vol float64 \n", " 17 sell_sm_vol float64 \n", " 18 buy_lg_vol float64 \n", " 19 sell_lg_vol float64 \n", " 20 buy_elg_vol float64 \n", " 21 sell_elg_vol float64 \n", " 22 net_mf_vol float64 \n", " 23 his_low float64 \n", " 24 his_high float64 \n", " 25 cost_5pct float64 \n", " 26 cost_15pct float64 \n", " 27 cost_50pct float64 \n", " 28 cost_85pct float64 \n", " 29 cost_95pct float64 \n", " 30 weight_avg float64 \n", " 31 winner_rate float64 \n", "dtypes: bool(1), datetime64[ns](1), float64(29), object(1)\n", "memory usage: 2.0+ GB\n", "None\n" ] } ], "source": [ "from main.utils.utils import read_and_merge_h5_data\n", "\n", "print('daily data')\n", "df = read_and_merge_h5_data('../../data/daily_data.h5', key='daily_data',\n", " columns=['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'pct_chg'],\n", " df=None)\n", "\n", "print('daily basic')\n", "df = read_and_merge_h5_data('../../data/daily_basic.h5', key='daily_basic',\n", " columns=['ts_code', 'trade_date', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv', 'volume_ratio',\n", " 'is_st'], df=df, join='inner')\n", "\n", "print('stk limit')\n", "df = read_and_merge_h5_data('../../data/stk_limit.h5', key='stk_limit',\n", " columns=['ts_code', 'trade_date', 'pre_close', 'up_limit', 'down_limit'],\n", " df=df)\n", "print('money flow')\n", "df = read_and_merge_h5_data('../../data/money_flow.h5', key='money_flow',\n", " columns=['ts_code', 'trade_date', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol',\n", " 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol'],\n", " df=df)\n", "print('cyq perf')\n", "df = read_and_merge_h5_data('../../data/cyq_perf.h5', key='cyq_perf',\n", " columns=['ts_code', 'trade_date', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct',\n", " 'cost_50pct',\n", " 'cost_85pct', 'cost_95pct', 'weight_avg', 'winner_rate'],\n", " df=df)\n", "print(df.info())" ] }, { "cell_type": "code", "execution_count": 3, "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('../../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": 4, "id": "c4e9e1d31da6dba6", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:10.719252Z", "start_time": "2025-04-03T12:47:10.541247Z" }, "jupyter": { "source_hidden": true } }, "outputs": [], "source": [ "def calculate_indicators(df):\n", " \"\"\"\n", " 计算四个指标:当日涨跌幅、5日移动平均、RSI、MACD。\n", " \"\"\"\n", " df = df.sort_values('trade_date')\n", " df['daily_return'] = (df['close'] - df['pre_close']) / df['pre_close'] * 100\n", " # df['5_day_ma'] = df['close'].rolling(window=5).mean()\n", " delta = df['close'].diff()\n", " gain = delta.where(delta > 0, 0)\n", " loss = -delta.where(delta < 0, 0)\n", " avg_gain = gain.rolling(window=14).mean()\n", " avg_loss = loss.rolling(window=14).mean()\n", " rs = avg_gain / avg_loss\n", " df['RSI'] = 100 - (100 / (1 + rs))\n", "\n", " # 计算MACD\n", " ema12 = df['close'].ewm(span=12, adjust=False).mean()\n", " ema26 = df['close'].ewm(span=26, adjust=False).mean()\n", " df['MACD'] = ema12 - ema26\n", " df['Signal_line'] = df['MACD'].ewm(span=9, adjust=False).mean()\n", " df['MACD_hist'] = df['MACD'] - df['Signal_line']\n", "\n", " # 4. 情绪因子1:市场上涨比例(Up Ratio)\n", " df['up_ratio'] = df['daily_return'].apply(lambda x: 1 if x > 0 else 0)\n", " df['up_ratio_20d'] = df['up_ratio'].rolling(window=20).mean() # 过去20天上涨比例\n", "\n", " # 5. 情绪因子2:成交量变化率(Volume Change Rate)\n", " df['volume_mean'] = df['vol'].rolling(window=20).mean() # 过去20天的平均成交量\n", " df['volume_change_rate'] = (df['vol'] - df['volume_mean']) / df['volume_mean'] * 100 # 成交量变化率\n", "\n", " # 6. 情绪因子3:波动率(Volatility)\n", " df['volatility'] = df['daily_return'].rolling(window=20).std() # 过去20天的日收益率标准差\n", "\n", " # 7. 情绪因子4:成交额变化率(Amount Change Rate)\n", " df['amount_mean'] = df['amount'].rolling(window=20).mean() # 过去20天的平均成交额\n", " df['amount_change_rate'] = (df['amount'] - df['amount_mean']) / df['amount_mean'] * 100 # 成交额变化率\n", "\n", " return df\n", "\n", "\n", "def generate_index_indicators(h5_filename):\n", " df = pd.read_hdf(h5_filename, key='index_data')\n", " df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')\n", " df = df.sort_values('trade_date')\n", "\n", " # 计算每个ts_code的相关指标\n", " df_indicators = []\n", " for ts_code in df['ts_code'].unique():\n", " df_index = df[df['ts_code'] == ts_code].copy()\n", " df_index = calculate_indicators(df_index)\n", " df_indicators.append(df_index)\n", "\n", " # 合并所有指数的结果\n", " df_all_indicators = pd.concat(df_indicators, ignore_index=True)\n", "\n", " # 保留trade_date列,并将同一天的数据按ts_code合并成一行\n", " df_final = df_all_indicators.pivot_table(\n", " index='trade_date',\n", " columns='ts_code',\n", " values=['daily_return', 'RSI', 'MACD', 'Signal_line',\n", " 'MACD_hist', 'up_ratio_20d', 'volume_change_rate', 'volatility',\n", " 'amount_change_rate', 'amount_mean'],\n", " aggfunc='last'\n", " )\n", "\n", " df_final.columns = [f\"{col[1]}_{col[0]}\" for col in df_final.columns]\n", " df_final = df_final.reset_index()\n", "\n", " return df_final\n", "\n", "\n", "# 使用函数\n", "h5_filename = '../../data/index_data.h5'\n", "index_data = generate_index_indicators(h5_filename)\n", "index_data = index_data.dropna()\n" ] }, { "cell_type": "code", "execution_count": 5, "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": 6, "id": "53f86ddc0677a6d7", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:15.944254Z", "start_time": "2025-04-03T12:47:10.826179Z" }, "jupyter": { "source_hidden": true }, "scrolled": true }, "outputs": [], "source": [ "from main.utils.factor import get_act_factor\n", "\n", "\n", "def read_industry_data(h5_filename):\n", " # 读取 H5 文件中所有的行业数据\n", " industry_data = pd.read_hdf(h5_filename, key='sw_daily', columns=[\n", " 'ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'pe', 'pb', 'vol'\n", " ]) # 假设 H5 文件的键是 'industry_data'\n", " industry_data = industry_data.sort_values(by=['ts_code', 'trade_date'])\n", " industry_data = industry_data.reindex()\n", " industry_data['trade_date'] = pd.to_datetime(industry_data['trade_date'], format='%Y%m%d')\n", "\n", " grouped = industry_data.groupby('ts_code', group_keys=False)\n", " industry_data['obv'] = grouped.apply(\n", " lambda x: pd.Series(talib.OBV(x['close'].values, x['vol'].values), index=x.index)\n", " )\n", " industry_data['return_5'] = grouped['close'].apply(lambda x: x / x.shift(5) - 1)\n", " industry_data['return_20'] = grouped['close'].apply(lambda x: x / x.shift(20) - 1)\n", "\n", " industry_data = get_act_factor(industry_data, cat=False)\n", " industry_data = industry_data.sort_values(by=['trade_date', 'ts_code'])\n", "\n", " # # 计算每天每个 ts_code 的因子和当天所有 ts_code 的中位数的偏差\n", " # factor_columns = ['obv', 'return_5', 'return_20', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4'] # 因子列\n", " # \n", " # for factor in factor_columns:\n", " # if factor in industry_data.columns:\n", " # # 计算每天每个 ts_code 的因子值与当天所有 ts_code 的中位数的偏差\n", " # industry_data[f'{factor}_deviation'] = industry_data.groupby('trade_date')[factor].transform(\n", " # lambda x: x - x.mean())\n", "\n", " industry_data['return_5_percentile'] = industry_data.groupby('trade_date')['return_5'].transform(\n", " lambda x: x.rank(pct=True))\n", " industry_data['return_20_percentile'] = industry_data.groupby('trade_date')['return_20'].transform(\n", " lambda x: x.rank(pct=True))\n", " industry_data = industry_data.drop(columns=['open', 'close', 'high', 'low', 'pe', 'pb', 'vol'])\n", "\n", " industry_data = industry_data.rename(\n", " columns={col: f'industry_{col}' for col in industry_data.columns if col not in ['ts_code', 'trade_date']})\n", "\n", " industry_data = industry_data.rename(columns={'ts_code': 'cat_l2_code'})\n", " return industry_data\n", "\n", "\n", "industry_df = read_industry_data('../../data/sw_daily.h5')\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "dbe2fd8021b9417f", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:15.969344Z", "start_time": "2025-04-03T12:47:15.963327Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['ts_code', 'open', 'close', 'high', 'low', 'circ_mv', 'total_mv', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg', 'in_date']\n" ] } ], "source": [ "origin_columns = df.columns.tolist()\n", "origin_columns = [col for col in origin_columns if\n", " col not in ['turnover_rate', 'pe_ttm', 'volume_ratio', 'vol', 'pct_chg', 'l2_code', 'winner_rate']]\n", "origin_columns = [col for col in origin_columns if col not in index_data.columns]\n", "origin_columns = [col for col in origin_columns if 'cyq' not in col]\n", "print(origin_columns)" ] }, { "cell_type": "code", "execution_count": 8, "id": "85c3e3d0235ffffa", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T12:47:16.089879Z", "start_time": "2025-04-03T12:47:15.990101Z" } }, "outputs": [], "source": [ "fina_indicator_df = read_and_merge_h5_data('../../data/fina_indicator.h5', key='fina_indicator',\n", " columns=['ts_code', 'ann_date', 'undist_profit_ps', 'ocfps', 'bps'],\n", " df=None)\n", "cashflow_df = read_and_merge_h5_data('../../data/cashflow.h5', key='cashflow',\n", " columns=['ts_code', 'ann_date', 'n_cashflow_act'],\n", " df=None)\n", "balancesheet_df = read_and_merge_h5_data('../../data/balancesheet.h5', key='balancesheet',\n", " columns=['ts_code', 'ann_date', 'money_cap', 'total_liab'],\n", " df=None)" ] }, { "cell_type": "code", "execution_count": 9, "id": "92d84ce15a562ec6", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:01.612695Z", "start_time": "2025-04-03T12:47:16.121802Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "使用 'ann_date' 作为财务数据生效日期。\n", "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n", "开始计算因子: AR, BR (原地修改)...\n", "因子 AR, BR 计算成功。\n", "因子 AR, BR 计算流程结束。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "使用 'ann_date' 作为财务数据生效日期。\n", "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n", "计算 BBI...\n", "Index(['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol',\n", " 'pct_chg', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv',\n", " 'volume_ratio', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol',\n", " 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol',\n", " 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct',\n", " 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg',\n", " 'winner_rate', 'l2_code', 'undist_profit_ps', 'ocfps', 'AR', 'BR',\n", " 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor', 'book_to_price_ratio',\n", " 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor',\n", " 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol',\n", " 'flow_divergence_diff', 'flow_divergence_ratio', 'total_buy_vol',\n", " 'lg_elg_buy_prop', 'flow_struct_buy_change',\n", " 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel',\n", " 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy',\n", " 'cost_support_15pct_change', 'cat_winner_price_zone',\n", " 'flow_chip_consistency', 'profit_taking_vs_absorb', '_is_positive',\n", " '_is_negative', 'cat_is_positive', '_pos_returns', '_neg_returns',\n", " '_pos_returns_sq', '_neg_returns_sq', 'upside_vol', 'downside_vol',\n", " 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate',\n", " 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike',\n", " 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike',\n", " 'vol_std_5', 'atr_14', 'atr_6', 'obv'],\n", " dtype='object')\n", "Calculating lg_flow_mom_corr_20_60...\n", "Finished lg_flow_mom_corr_20_60.\n", "Calculating lg_flow_accel...\n", "Finished lg_flow_accel.\n", "Calculating profit_pressure...\n", "Finished profit_pressure.\n", "Calculating underwater_resistance...\n", "Finished underwater_resistance.\n", "Calculating cost_conc_std_20...\n", "Finished cost_conc_std_20.\n", "Calculating profit_decay_20...\n", "Finished profit_decay_20.\n", "Calculating vol_amp_loss_20...\n", "Finished vol_amp_loss_20.\n", "Calculating vol_drop_profit_cnt_5...\n", "Finished vol_drop_profit_cnt_5.\n", "Calculating lg_flow_vol_interact_20...\n", "Finished lg_flow_vol_interact_20.\n", "Calculating cost_break_confirm_cnt_5...\n", "Finished cost_break_confirm_cnt_5.\n", "Calculating atr_norm_channel_pos_14...\n", "Finished atr_norm_channel_pos_14.\n", "Calculating turnover_diff_skew_20...\n", "Finished turnover_diff_skew_20.\n", "Calculating lg_sm_flow_diverge_20...\n", "Finished lg_sm_flow_diverge_20.\n", "Calculating pullback_strong_20_20...\n", "Finished pullback_strong_20_20.\n", "Calculating vol_wgt_hist_pos_20...\n", "Finished vol_wgt_hist_pos_20.\n", "Calculating vol_adj_roc_20...\n", "Finished vol_adj_roc_20.\n", "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", "Error calculating cs_rank_ind_adj_lg_flow: Missing 'cat_l2_code' column. Assigning NaN.\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", "\n", "Index: 4502216 entries, 0 to 4502215\n", "Columns: 177 entries, ts_code to cs_rank_size\n", "dtypes: bool(10), datetime64[ns](1), float64(161), int32(3), object(2)\n", "memory usage: 5.6+ GB\n", "None\n", "['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'pct_chg', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv', 'volume_ratio', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg', 'winner_rate', 'cat_l2_code', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'flow_divergence_diff', 'flow_divergence_ratio', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'price_cost_divergence', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', '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", "\n", "def filter_data(df):\n", " # df = df.groupby('trade_date').apply(lambda x: x.nlargest(1000, 'act_factor1'))\n", " df = df[~df['is_st']]\n", " df = df[~df['ts_code'].str.endswith('BJ')]\n", " df = df[~df['ts_code'].str.startswith('30')]\n", " df = df[~df['ts_code'].str.startswith('68')]\n", " df = df[~df['ts_code'].str.startswith('8')]\n", " df = df[df['trade_date'] >= '2019-01-01']\n", " if 'in_date' in df.columns:\n", " df = df.drop(columns=['in_date'])\n", " df = df.reset_index(drop=True)\n", " return df\n", "\n", "gc.collect()\n", "\n", "df = filter_data(df)\n", "df = df.sort_values(by=['ts_code', 'trade_date'])\n", "df = add_financial_factor(df, fina_indicator_df, factor_value_col='undist_profit_ps')\n", "df = add_financial_factor(df, fina_indicator_df, factor_value_col='ocfps')\n", "calculate_arbr(df, N=26)\n", "df['log_circ_mv'] = np.log(df['circ_mv'])\n", "df = calculate_cashflow_to_ev_factor(df, cashflow_df, balancesheet_df)\n", "df = caculate_book_to_price_ratio(df, fina_indicator_df)\n", "df = turnover_rate_n(df, n=5)\n", "df = variance_n(df, n=20)\n", "df = bbi_ratio_factor(df)\n", "df, _ = get_rolling_factor(df)\n", "df, _ = get_simple_factor(df)\n", "\n", "lg_flow_mom_corr(df, N=20, M=60)\n", "lg_flow_accel(df)\n", "profit_pressure(df)\n", "underwater_resistance(df)\n", "cost_conc_std(df, N=20)\n", "profit_decay(df, N=20)\n", "vol_amp_loss(df, N=20)\n", "vol_drop_profit_cnt(df, N=20, M=5)\n", "lg_flow_vol_interact(df, N=20)\n", "cost_break_confirm_cnt(df, M=5)\n", "atr_norm_channel_pos(df, N=14)\n", "turnover_diff_skew(df, N=20)\n", "lg_sm_flow_diverge(df, N=20)\n", "pullback_strong(df, N=20, M=20)\n", "vol_wgt_hist_pos(df, N=20)\n", "vol_adj_roc(df, N=20)\n", "\n", "cs_rank_net_lg_flow_val(df)\n", "cs_rank_flow_divergence(df)\n", "cs_rank_industry_adj_lg_flow(df) # Needs cat_l2_code\n", "cs_rank_elg_buy_ratio(df)\n", "cs_rank_rel_profit_margin(df)\n", "cs_rank_cost_breadth(df)\n", "cs_rank_dist_to_upper_cost(df)\n", "cs_rank_winner_rate(df)\n", "cs_rank_intraday_range(df)\n", "cs_rank_close_pos_in_range(df)\n", "cs_rank_opening_gap(df) # Needs pre_close\n", "cs_rank_pos_in_hist_range(df) # Needs his_low, his_high\n", "cs_rank_vol_x_profit_margin(df)\n", "cs_rank_lg_flow_price_concordance(df)\n", "cs_rank_turnover_per_winner(df)\n", "cs_rank_ind_cap_neutral_pe(df) # Placeholder - needs external libraries\n", "cs_rank_volume_ratio(df) # Needs volume_ratio\n", "cs_rank_elg_buy_sell_sm_ratio(df)\n", "cs_rank_cost_dist_vol_ratio(df) # Needs volume_ratio\n", "cs_rank_size(df) # Needs circ_mv\n", "\n", "df = df.rename(columns={'l1_code': 'cat_l1_code'})\n", "df = df.rename(columns={'l2_code': 'cat_l2_code'})\n", "\n", "# df = df.merge(index_data, on='trade_date', how='left')\n", "\n", "print(df.info())\n", "print(df.columns.tolist())" ] }, { "cell_type": "code", "execution_count": 10, "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": 11, "id": "f4f16d63ad18d1bc", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:03.670700Z", "start_time": "2025-04-03T13:08:03.665739Z" } }, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import statsmodels.api as sm # 用于中性化回归\n", "from tqdm import tqdm # 可选,用于显示进度条\n", "\n", "# --- 常量 ---\n", "epsilon = 1e-10 # 防止除零\n", "\n", "# --- 1. 中位数去极值 (MAD) ---\n", "\n", "def cs_mad_filter(df: pd.DataFrame,\n", " features: list,\n", " k: float = 3.0,\n", " scale_factor: float = 1.4826):\n", " \"\"\"\n", " 对指定特征列进行截面 MAD 去极值处理 (原地修改)。\n", "\n", " 方法: 对每日截面数据,计算 median 和 MAD,\n", " 将超出 [median - k * scale * MAD, median + k * scale * MAD] 范围的值\n", " 替换为边界值 (Winsorization)。\n", " scale_factor=1.4826 使得 MAD 约等于正态分布的标准差。\n", "\n", " Args:\n", " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。\n", " features (list): 需要处理的特征列名列表。\n", " k (float): MAD 的倍数,用于确定边界。默认为 3.0。\n", " scale_factor (float): MAD 的缩放因子。默认为 1.4826。\n", "\n", " WARNING: 此函数会原地修改输入的 DataFrame 'df'。\n", " \"\"\"\n", " print(f\"开始截面 MAD 去极值处理 (k={k})...\")\n", " if not all(col in df.columns for col in features):\n", " missing = [col for col in features if col not in df.columns]\n", " print(f\"错误: DataFrame 中缺少以下特征列: {missing}。跳过去极值处理。\")\n", " return\n", "\n", " grouped = df.groupby('trade_date')\n", "\n", " for col in tqdm(features, desc=\"MAD Filtering\"):\n", " try:\n", " # 计算截面中位数\n", " median = grouped[col].transform('median')\n", " # 计算截面 MAD (Median Absolute Deviation from Median)\n", " mad = (df[col] - median).abs().groupby(df['trade_date']).transform('median')\n", "\n", " # 计算上下边界\n", " lower_bound = median - k * scale_factor * mad\n", " upper_bound = median + k * scale_factor * mad\n", "\n", " # 原地应用 clip\n", " df[col] = np.clip(df[col], lower_bound, upper_bound)\n", "\n", " except KeyError:\n", " print(f\"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的 MAD 处理。\")\n", " except Exception as e:\n", " print(f\"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的 MAD 处理。\")\n", "\n", " print(\"截面 MAD 去极值处理完成。\")\n", "\n", "\n", "# --- 2. 行业市值中性化 ---\n", "\n", "def cs_neutralize_industry_cap(df: pd.DataFrame,\n", " features: list,\n", " industry_col: str = 'cat_l2_code',\n", " market_cap_col: str = 'circ_mv'):\n", " \"\"\"\n", " 对指定特征列进行截面行业和对数市值中性化 (原地修改)。\n", " 使用 OLS 回归: feature ~ 1 + log(market_cap) + C(industry)\n", " 将回归残差写回原特征列。\n", "\n", " Args:\n", " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date', features 列,\n", " industry_col, market_cap_col。\n", " features (list): 需要处理的特征列名列表。\n", " industry_col (str): 行业分类列名。\n", " market_cap_col (str): 流通市值列名。\n", "\n", " WARNING: 此函数会原地修改输入的 DataFrame 'df' 的 features 列。\n", " 计算量较大,可能耗时较长。\n", " 需要安装 statsmodels 库 (pip install statsmodels)。\n", " \"\"\"\n", " print(\"开始截面行业市值中性化...\")\n", " required_cols = features + ['trade_date', industry_col, market_cap_col]\n", " if not all(col in df.columns for col in required_cols):\n", " missing = [col for col in required_cols if col not in df.columns]\n", " print(f\"错误: DataFrame 中缺少必需列: {missing}。无法进行中性化。\")\n", " return\n", "\n", " # 预处理:计算 log 市值,处理 industry code 可能的 NaN\n", " log_cap_col = '_log_market_cap'\n", " df[log_cap_col] = np.log1p(df[market_cap_col]) # log1p 处理 0 值\n", " # df[industry_col] = df[industry_col].cat.add_categories('UnknownIndustry')\n", " # df[industry_col] = df[industry_col].fillna('UnknownIndustry') # 填充行业 NaN\n", " # df[industry_col] = df[industry_col].astype('category') # 转为类别,ols 会自动处理\n", "\n", " dates = df['trade_date'].unique()\n", " all_residuals = [] # 用于收集所有日期的残差\n", "\n", " for date in tqdm(dates, desc=\"Neutralizing\"):\n", " daily_data = df.loc[df['trade_date'] == date, features + [log_cap_col, industry_col]].copy() # 使用 .loc 获取副本\n", "\n", " # 准备自变量 X (常数项 + log市值 + 行业哑变量)\n", " X = daily_data[[log_cap_col]]\n", " X = sm.add_constant(X, prepend=True) # 添加常数项\n", " # 创建行业哑变量 (drop_first=True 避免共线性)\n", " industry_dummies = pd.get_dummies(daily_data[industry_col], prefix=industry_col, drop_first=True)\n", " industry_dummies = industry_dummies.astype(int)\n", " X = pd.concat([X, industry_dummies], axis=1)\n", "\n", " daily_residuals = daily_data[[col for col in features]].copy() # 创建用于存储残差的df\n", "\n", " for col in features:\n", " Y = daily_data[col]\n", "\n", " # 处理 NaN 值,确保 X 和 Y 在相同位置有有效值\n", " valid_mask = Y.notna() & X.notna().all(axis=1)\n", " if valid_mask.sum() < (X.shape[1] + 1): # 数据点不足以估计模型\n", " print(f\"警告: 日期 {date}, 特征 {col} 有效数据不足 ({valid_mask.sum()}个),无法中性化,填充 NaN。\")\n", " daily_residuals[col] = np.nan\n", " continue\n", "\n", " Y_valid = Y[valid_mask]\n", " X_valid = X[valid_mask]\n", "\n", " # 执行 OLS 回归\n", " try:\n", " model = sm.OLS(Y_valid.to_numpy(), X_valid.to_numpy())\n", " results = model.fit()\n", " # 将残差填回对应位置\n", " daily_residuals.loc[valid_mask, col] = results.resid\n", " daily_residuals.loc[~valid_mask, col] = np.nan # 原本无效的位置填充 NaN\n", " except Exception as e:\n", " print(f\"警告: 日期 {date}, 特征 {col} 回归失败: {e},填充 NaN。\")\n", " daily_residuals[col] = np.nan\n", " break\n", "\n", " all_residuals.append(daily_residuals)\n", "\n", " # 合并所有日期的残差结果\n", " if all_residuals:\n", " residuals_df = pd.concat(all_residuals)\n", " # 将残差结果更新回原始 df (原地修改)\n", " # 使用 update 比 merge 更适合基于索引的原地更新\n", " # 确保 residuals_df 的索引与 df 中对应部分一致\n", " df.update(residuals_df)\n", " else:\n", " print(\"没有有效的残差结果可以合并。\")\n", "\n", "\n", " # 清理临时列\n", " df.drop(columns=[log_cap_col], inplace=True)\n", " print(\"截面行业市值中性化完成。\")\n", "\n", "\n", "# --- 3. Z-Score 标准化 ---\n", "\n", "def cs_zscore_standardize(df: pd.DataFrame, features: list, epsilon: float = 1e-10):\n", " \"\"\"\n", " 对指定特征列进行截面 Z-Score 标准化 (原地修改)。\n", " 方法: Z = (value - cross_sectional_mean) / (cross_sectional_std + epsilon)\n", "\n", " Args:\n", " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。\n", " features (list): 需要处理的特征列名列表。\n", " epsilon (float): 防止除以零的小常数。\n", "\n", " WARNING: 此函数会原地修改输入的 DataFrame 'df'。\n", " \"\"\"\n", " print(\"开始截面 Z-Score 标准化...\")\n", " if not all(col in df.columns for col in features):\n", " missing = [col for col in features if col not in df.columns]\n", " print(f\"错误: DataFrame 中缺少以下特征列: {missing}。跳过标准化处理。\")\n", " return\n", "\n", " grouped = df.groupby('trade_date')\n", "\n", " for col in tqdm(features, desc=\"Standardizing\"):\n", " try:\n", " # 使用 transform 计算截面均值和标准差\n", " mean = grouped[col].transform('mean')\n", " std = grouped[col].transform('std')\n", "\n", " # 计算 Z-Score 并原地赋值\n", " df[col] = (df[col] - mean) / (std + epsilon)\n", "\n", " except KeyError:\n", " print(f\"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的标准化处理。\")\n", " except Exception as e:\n", " print(f\"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的标准化处理。\")\n", "\n", " print(\"截面 Z-Score 标准化完成。\")\n", "\n", "def fill_nan_with_daily_median(df: pd.DataFrame, feature_columns: list[str]) -> pd.DataFrame:\n", " \"\"\"\n", " 对指定特征列进行每日截面中位数填充缺失值 (NaN)。\n", "\n", " 参数:\n", " df (pd.DataFrame): 包含多日数据的DataFrame,需要包含 'trade_date' 和 feature_columns 中的列。\n", " feature_columns (list[str]): 需要进行缺失值填充的特征列名称列表。\n", "\n", " 返回:\n", " pd.DataFrame: 包含缺失值填充后特征列的DataFrame。在输入DataFrame的副本上操作。\n", " \"\"\"\n", " processed_df = df.copy() # 在副本上操作,保留原始数据\n", "\n", " # 确保 trade_date 是 datetime 类型以便正确分组\n", " processed_df['trade_date'] = pd.to_datetime(processed_df['trade_date'])\n", "\n", " def _fill_daily_nan(group):\n", " # group 是某一个交易日的 DataFrame\n", "\n", " # 遍历指定的特征列\n", " for feature_col in feature_columns:\n", " # 检查列是否存在于当前分组中\n", " if feature_col in group.columns:\n", " # 计算当日该特征的中位数\n", " median_val = group[feature_col].median()\n", "\n", " # 使用当日中位数填充该特征列的 NaN 值\n", " # inplace=True 会直接修改 group DataFrame\n", " group[feature_col].fillna(median_val, inplace=True)\n", " # else:\n", " # print(f\"Warning: Feature column '{feature_col}' not found in daily group for {group['trade_date'].iloc[0]}. Skipping.\")\n", "\n", " return group\n", "\n", " # 按交易日期分组,并应用每日填充函数\n", " # group_keys=False 避免将分组键添加到结果索引中\n", " filled_df = processed_df.groupby('trade_date', group_keys=False).apply(_fill_daily_nan)\n", "\n", " return filled_df" ] }, { "cell_type": "code", "execution_count": 12, "id": "40e6b68a91b30c79", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T13:08:04.694262Z", "start_time": "2025-04-03T13:08:03.694904Z" } }, "outputs": [], "source": [ "import pandas as pd\n", "\n", "\n", "def remove_outliers_label_percentile(label: pd.Series, lower_percentile: float = 0.01, upper_percentile: float = 0.99,\n", " log=True):\n", " if not (0 <= lower_percentile < upper_percentile <= 1):\n", " raise ValueError(\"Percentile values must satisfy 0 <= lower_percentile < upper_percentile <= 1.\")\n", "\n", " # Calculate lower and upper bounds based on percentiles\n", " lower_bound = label.quantile(lower_percentile)\n", " upper_bound = label.quantile(upper_percentile)\n", "\n", " # Filter out values outside the bounds\n", " filtered_label = label[(label >= lower_bound) & (label <= upper_bound)]\n", "\n", " # Print the number of removed outliers\n", " if log:\n", " print(f\"Removed {len(label) - len(filtered_label)} outliers.\")\n", " return filtered_label\n", "\n", "\n", "def calculate_risk_adjusted_target(df, days=5):\n", " df = df.sort_values(by=['ts_code', 'trade_date'])\n", "\n", " df['future_close'] = df.groupby('ts_code')['close'].shift(-days)\n", " df['future_open'] = df.groupby('ts_code')['open'].shift(-1)\n", " df['future_return'] = (df['future_close'] - df['future_open']) / df['future_open']\n", "\n", " df['future_volatility'] = df.groupby('ts_code')['future_return'].rolling(days, min_periods=1).std().reset_index(\n", " level=0, drop=True)\n", " sharpe_ratio = df['future_return'] * df['future_volatility']\n", " sharpe_ratio.replace([np.inf, -np.inf], np.nan, inplace=True)\n", "\n", " return sharpe_ratio\n", "\n", "\n", "def calculate_score(df, days=5, lambda_param=1.0):\n", " def calculate_max_drawdown(prices):\n", " peak = prices.iloc[0] # 初始化峰值\n", " max_drawdown = 0 # 初始化最大回撤\n", "\n", " for price in prices:\n", " if price > peak:\n", " peak = price # 更新峰值\n", " else:\n", " drawdown = (peak - price) / peak # 计算当前回撤\n", " max_drawdown = max(max_drawdown, drawdown) # 更新最大回撤\n", "\n", " return max_drawdown\n", "\n", " def compute_stock_score(stock_df):\n", " stock_df = stock_df.sort_values(by=['trade_date'])\n", " future_return = stock_df['future_return']\n", " # 使用已有的 pct_chg 字段计算波动率\n", " volatility = stock_df['pct_chg'].rolling(days).std().shift(-days)\n", " max_drawdown = stock_df['close'].rolling(days).apply(calculate_max_drawdown, raw=False).shift(-days)\n", " score = future_return - lambda_param * max_drawdown\n", " return score\n", "\n", " # # 确保 DataFrame 按照股票代码和交易日期排序\n", " # df = df.sort_values(by=['ts_code', 'trade_date'])\n", "\n", " # 对每个股票分别计算 score\n", " df['score'] = df.groupby('ts_code').apply(compute_stock_score).reset_index(level=0, drop=True)\n", "\n", " return df['score']\n", "\n", "\n", "def remove_highly_correlated_features(df, feature_columns, threshold=0.9):\n", " numeric_features = df[feature_columns].select_dtypes(include=[np.number]).columns.tolist()\n", " if not numeric_features:\n", " raise ValueError(\"No numeric features found in the provided data.\")\n", "\n", " corr_matrix = df[numeric_features].corr().abs()\n", " upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))\n", " to_drop = [column for column in upper.columns if any(upper[column] > threshold)]\n", " remaining_features = [col for col in feature_columns if col not in to_drop\n", " or 'act' in col or 'af' in col]\n", " return remaining_features\n", "\n", "\n", "def cross_sectional_standardization(df, features):\n", " df_sorted = df.sort_values(by='trade_date') # 按时间排序\n", " df_standardized = df_sorted.copy()\n", "\n", " for date in df_sorted['trade_date'].unique():\n", " # 获取当前时间点的数据\n", " current_data = df_standardized[df_standardized['trade_date'] == date]\n", "\n", " # 只对指定特征进行标准化\n", " scaler = StandardScaler()\n", " standardized_values = scaler.fit_transform(current_data[features])\n", "\n", " # 将标准化结果重新赋值回去\n", " df_standardized.loc[df_standardized['trade_date'] == date, features] = standardized_values\n", "\n", " return df_standardized\n", "\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "\n", "def neutralize_manual_revised(df: pd.DataFrame, features: list, industry_col: str, mkt_cap_col: str) -> pd.DataFrame:\n", " \"\"\"\n", " 手动实现简单回归以提升速度,通过构建 Series 确保索引对齐。\n", " 对特征在行业内部进行市值中性化。\n", "\n", " Args:\n", " df: 输入的 DataFrame,包含特征、行业分类和市值列。\n", " features: 需要进行中性化的特征列名列表。\n", " industry_col: 行业分类列的列名。\n", " mkt_cap_col: 市值列的列名。\n", "\n", " Returns:\n", " 中性化后的 DataFrame。\n", " \"\"\"\n", "\n", " df[mkt_cap_col] = pd.to_numeric(df[mkt_cap_col], errors='coerce')\n", " df_cleaned = df.dropna(subset=[mkt_cap_col]).copy()\n", " df_cleaned = df_cleaned[df_cleaned[mkt_cap_col] > 0].copy()\n", "\n", " if df_cleaned.empty:\n", " print(\"警告: 清理市值异常值后 DataFrame 为空。\")\n", " return df # 返回原始或空df,取决于清理前的状态\n", "\n", " processed_df = df\n", "\n", " for col in features:\n", " if col not in df_cleaned.columns:\n", " print(f\"警告: 特征列 '{col}' 不存在于清理后的 DataFrame 中,已跳过。\")\n", " # 对于原始 df 中该列不存在的,在结果 df 中也保持原样(可能全是NaN)\n", " processed_df[col] = df[col] if col in df.columns else np.nan\n", " continue\n", "\n", " # 跳过对控制变量本身进行中性化\n", " if col == mkt_cap_col or col == industry_col:\n", " print(f\"警告: 特征列 '{col}' 是控制变量或内部使用的列,跳过中性化。\")\n", " # 在结果 df 中也保持原样\n", " processed_df[col] = df[col] if col in df.columns else np.nan\n", " continue\n", "\n", " residual_series = pd.Series(index=df_cleaned.index, dtype=float)\n", "\n", " # 在分组前处理特征列的 NaN,只对有因子值的行进行回归计算\n", " df_subset_factor = df_cleaned.dropna(subset=[col]).copy()\n", "\n", " if not df_subset_factor.empty:\n", " for industry, group in df_subset_factor.groupby(industry_col):\n", " x = group[mkt_cap_col] # 市值对数\n", " y = group[col] # 因子值\n", "\n", " # 确保有足够的数据点 (>1) 且市值对数有方差 (>0) 进行回归计算\n", " # 检查 np.var > 一个很小的正数,避免浮点数误差导致的零方差判断问题\n", " if len(group) > 1 and np.var(x) > 1e-9:\n", " try:\n", " beta = np.cov(y, x)[0, 1] / np.var(x)\n", " alpha = np.mean(y) - beta * np.mean(x)\n", "\n", " # 计算残差\n", " resid = y - (alpha + beta * x)\n", "\n", " # 将计算出的残差存储到 residual_series 中,通过索引自动对齐\n", " residual_series.loc[resid.index] = resid\n", "\n", " except Exception as e:\n", " # 捕获可能的计算异常,例如np.cov或np.var因为极端数据报错\n", " print(f\"警告: 在行业 {industry} 计算回归时发生错误: {e}。该组残差将设为原始值或 NaN。\")\n", " # 此时该组的残差会保持 residual_series 初始化时的 NaN 或后续处理\n", " # 也可以选择保留原始值:residual_series.loc[group.index] = group[col]\n", "\n", " else:\n", " residual_series.loc[group.index] = group[col] # 保留原始因子值\n", " processed_df.loc[residual_series.index, col] = residual_series\n", "\n", "\n", " else:\n", " processed_df[col] = np.nan # 或 df[col] if col in df.columns else np.nan\n", "\n", " return processed_df\n", "\n", "\n", "import gc\n", "\n", "gc.collect()\n", "\n", "\n", "def mad_filter(df, features, n=3):\n", " for col in features:\n", " median = df[col].median()\n", " mad = np.median(np.abs(df[col] - median))\n", " upper = median + n * mad\n", " lower = median - n * mad\n", " df[col] = np.clip(df[col], lower, upper) # 截断极值\n", " return df\n", "\n", "\n", "def percentile_filter(df, features, lower_percentile=0.01, upper_percentile=0.99):\n", " for col in features:\n", " # 按日期分组计算上下百分位数\n", " lower_bound = df.groupby('trade_date')[col].transform(\n", " lambda x: x.quantile(lower_percentile)\n", " )\n", " upper_bound = df.groupby('trade_date')[col].transform(\n", " lambda x: x.quantile(upper_percentile)\n", " )\n", " # 截断超出范围的值\n", " df[col] = np.clip(df[col], lower_bound, upper_bound)\n", " return df\n", "\n", "\n", "from scipy.stats import iqr\n", "\n", "\n", "def iqr_filter(df, features):\n", " for col in features:\n", " df[col] = df.groupby('trade_date')[col].transform(\n", " lambda x: (x - x.median()) / iqr(x) if iqr(x) != 0 else x\n", " )\n", " return df\n", "\n", "\n", "def quantile_filter(df, features, lower_quantile=0.01, upper_quantile=0.99, window=60):\n", " df = df.copy()\n", " for col in features:\n", " # 计算 rolling 统计量,需要按日期进行 groupby\n", " rolling_lower = df.groupby('trade_date')[col].transform(lambda x: x.rolling(window=min(len(x), window)).quantile(lower_quantile))\n", " rolling_upper = df.groupby('trade_date')[col].transform(lambda x: x.rolling(window=min(len(x), window)).quantile(upper_quantile))\n", "\n", " # 对数据进行裁剪\n", " df[col] = np.clip(df[col], rolling_lower, rolling_upper)\n", " \n", " return df\n", "\n", "def select_top_features_by_rankic(df: pd.DataFrame, feature_columns: list, n: int, target_column: str = 'future_return') -> list:\n", " \"\"\"\n", " 计算给定特征与目标列的 RankIC,并返回 RankIC 绝对值最高的 n 个特征。\n", "\n", " Args:\n", " df: 包含特征列和目标列的 Pandas DataFrame。\n", " feature_columns: 包含所有待评估特征列名的列表。\n", " n: 希望选取的 RankIC 绝对值最高的特征数量。\n", " target_column: 目标列的名称,用于计算 RankIC。默认为 'future_return'。\n", "\n", " Returns:\n", " 包含 RankIC 绝对值最高的 n 个特征列名的列表。\n", " \"\"\"\n", " numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns\n", " numeric_columns = [col for col in numeric_columns if col in feature_columns]\n", " if target_column not in df.columns:\n", " raise ValueError(f\"目标列 '{target_column}' 不存在于 DataFrame 中。\")\n", "\n", " rankic_scores = {}\n", " for feature in numeric_columns:\n", " if feature not in df.columns:\n", " print(f\"警告: 特征列 '{feature}' 不存在于 DataFrame 中,已跳过。\")\n", " continue\n", "\n", " # 计算特征与目标列的 RankIC (斯皮尔曼相关系数)\n", " # dropna() 是为了处理缺失值,确保相关性计算不失败\n", " valid_data = df[[feature, target_column]].dropna()\n", " if len(valid_data) > 1: # 确保有足够的数据点进行相关性计算\n", " # 计算斯皮尔曼相关性\n", " correlation = valid_data[feature].corr(valid_data[target_column], method='spearman')\n", " rankic_scores[feature] = abs(correlation) # 使用绝对值来衡量相关性强度\n", " else:\n", " rankic_scores[feature] = 0 # 数据不足,RankIC设为0或跳过\n", "\n", " # 将 RankIC 分数转换为 Series 便于排序\n", " rankic_series = pd.Series(rankic_scores)\n", "\n", " # 按 RankIC 绝对值降序排序,选取前 n 个特征\n", " # handle case where n might be larger than available features\n", " n_actual = min(n, len(rankic_series))\n", " top_features = rankic_series.sort_values(ascending=False).head(n_actual).index.tolist()\n", " top_features = [col for col in feature_columns if col in top_features or col not in numeric_columns]\n", " return top_features" ] }, { "cell_type": "code", "execution_count": 22, "id": "47c12bb34062ae7a", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T14:57:50.841165Z", "start_time": "2025-04-03T14:49:25.889057Z" } }, "outputs": [], "source": [ "days = 2\n", "validation_days = 120\n", "\n", "import gc\n", "\n", "gc.collect()\n", "\n", "df = df.sort_values(by=['ts_code', 'trade_date'])\n", "df['future_return'] = df.groupby('ts_code', group_keys=False)['close'].apply(lambda x: x.shift(-days) / x - 1)\n", "# df['future_return'] = (df.groupby('ts_code')['close'].shift(-days) - df.groupby('ts_code')['open'].shift(-1)) / \\\n", "# df.groupby('ts_code')['open'].shift(-1)\n", "\n", "df['cat_up_limit'] = df['pct_chg'] > 5\n", "df['label'] = df.groupby('ts_code')['cat_up_limit'].rolling(window=5, min_periods=1).max().shift(-5).fillna(0).astype(int).reset_index(level=0, drop=True)\n", "\n", "filter_index = df['future_return'].between(df['future_return'].quantile(0.01), df['future_return'].quantile(0.99))\n", "\n", "# for col in [col for col in df.columns]:\n", "# train_data[col] = train_data[col].astype('str')\n", "# test_data[col] = test_data[col].astype('str')" ] }, { "cell_type": "code", "execution_count": 23, "id": "b76ea08a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " ts_code trade_date log_circ_mv\n", "0 000001.SZ 2019-01-02 16.574219\n", "2738 000001.SZ 2019-01-03 16.583965\n", "5477 000001.SZ 2019-01-04 16.633371\n", "['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', '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%|██████████| 130/130 [00:30<00:00, 4.28it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 100%|██████████| 130/130 [00:23<00:00, 5.44it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 0it [00:00, ?it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "开始截面 MAD 去极值处理 (k=3.0)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "MAD Filtering: 0it [00:00, ?it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "截面 MAD 去极值处理完成。\n", "feature_columns: ['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', '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-06\n", "2058185\n", "train_data最小日期: 2020-01-02\n", "train_data最大日期: 2022-12-30\n", "1730349\n", "test_data最小日期: 2023-01-03\n", "test_data最大日期: 2025-05-06\n", " ts_code trade_date log_circ_mv\n", "0 000001.SZ 2019-01-02 16.574219\n", "2738 000001.SZ 2019-01-03 16.583965\n", "5477 000001.SZ 2019-01-04 16.633371\n" ] } ], "source": [ "train_data = df[filter_index & (df['trade_date'] <= '2023-01-01') & (df['trade_date'] >= '2020-01-01')]\n", "test_data = df[(df['trade_date'] >= '2023-01-01')]\n", "\n", "print(df[['ts_code', 'trade_date', 'log_circ_mv']].head(3))\n", "\n", "industry_df = industry_df.sort_values(by=['trade_date'])\n", "index_data = index_data.sort_values(by=['trade_date'])\n", "\n", "# train_data = train_data.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n", "# train_data = train_data.merge(index_data, on='trade_date', how='left')\n", "# test_data = test_data.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n", "# test_data = test_data.merge(index_data, on='trade_date', how='left')\n", "\n", "train_data, test_data = train_data.replace([np.inf, -np.inf], np.nan), test_data.replace([np.inf, -np.inf], np.nan)\n", "\n", "# feature_columns_new = feature_columns[:]\n", "# train_data, _ = create_deviation_within_dates(train_data, feature_columns)\n", "# test_data, _ = create_deviation_within_dates(test_data, feature_columns)\n", "\n", "# feature_columns = [\n", "# 'undist_profit_ps', \n", "# 'AR_BR', \n", "# 'pe_ttm',\n", "# 'alpha_22_improved', \n", "# 'alpha_003', \n", "# 'alpha_007', \n", "# 'alpha_013', \n", "# 'cat_up_limit', \n", "# 'cat_down_limit', \n", "# 'up_limit_count_10d', \n", "# 'down_limit_count_10d', \n", "# 'consecutive_up_limit', \n", "# 'vol_break', \n", "# 'weight_roc5', \n", "# 'price_cost_divergence', \n", "# 'smallcap_concentration', \n", "# 'cost_stability', \n", "# 'high_cost_break_days', \n", "# 'liquidity_risk', \n", "# 'turnover_std', \n", "# 'mv_volatility', \n", "# 'volume_growth', \n", "# 'mv_growth', \n", "# 'lg_flow_mom_corr_20_60', \n", "# 'lg_flow_accel', \n", "# 'profit_pressure', \n", "# 'underwater_resistance', \n", "# 'cost_conc_std_20', \n", "# 'profit_decay_20', \n", "# 'vol_amp_loss_20', \n", "# 'vol_drop_profit_cnt_5', \n", "# 'lg_flow_vol_interact_20', \n", "# 'cost_break_confirm_cnt_5', \n", "# 'atr_norm_channel_pos_14', \n", "# 'turnover_diff_skew_20', \n", "# 'lg_sm_flow_diverge_20', \n", "# 'pullback_strong_20_20', \n", "# 'vol_wgt_hist_pos_20', \n", "# 'vol_adj_roc_20',\n", "# 'cashflow_to_ev_factor',\n", "# 'ocfps',\n", "# 'book_to_price_ratio',\n", "# 'turnover_rate_mean_5',\n", "# 'variance_20',\n", "# 'bbi_ratio_factor'\n", "# ]\n", "# feature_columns = [col for col in feature_columns if col in train_data.columns]\n", "# feature_columns = [col for col in feature_columns if not col.startswith('_')]\n", "\n", "feature_columns = [col for col in test_data.head(10).merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left').merge(index_data, on='trade_date', how='left').columns]\n", "feature_columns = [col for col in feature_columns if col not in ['trade_date',\n", " 'ts_code',\n", " 'label']]\n", "feature_columns = [col for col in feature_columns if 'future' not in col]\n", "feature_columns = [col for col in feature_columns if 'label' not in col]\n", "feature_columns = [col for col in feature_columns if 'score' not in col]\n", "feature_columns = [col for col in feature_columns if 'gen' not in col]\n", "feature_columns = [col for col in feature_columns if 'is_st' not in col]\n", "feature_columns = [col for col in feature_columns if 'pe_ttm' not in col]\n", "# feature_columns = [col for col in feature_columns if 'volatility' not in col]\n", "feature_columns = [col for col in feature_columns if 'circ_mv' not in col]\n", "feature_columns = [col for col in feature_columns if 'code' not in col]\n", "feature_columns = [col for col in feature_columns if col not in origin_columns]\n", "feature_columns = [col for col in feature_columns if not col.startswith('_')]\n", "# feature_columns = [col for col in feature_columns if col not in ['ts_code', 'trade_date', 'vol_std_5', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_007', 'consecutive_up_limit', 'mv_volatility', 'volume_growth', 'mv_growth', 'arbr']]\n", "feature_columns = [col for col in feature_columns if col not in ['intraday_lg_flow_corr_20', \n", " 'cap_neutral_cost_metric', \n", " 'hurst_net_mf_vol_60', \n", " 'complex_factor_deap_1', \n", " 'lg_buy_consolidation_20',\n", " 'cs_rank_ind_cap_neutral_pe',\n", " 'cs_rank_opening_gap',\n", " 'cs_rank_ind_adj_lg_flow']]\n", "\n", "numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns\n", "numeric_columns = [col for col in numeric_columns if col in feature_columns]\n", "# feature_columns = select_top_features_by_rankic(df, numeric_columns, n=10)\n", "print(feature_columns)\n", "\n", "train_data = fill_nan_with_daily_median(train_data, feature_columns)\n", "test_data = fill_nan_with_daily_median(test_data, feature_columns)\n", "\n", "train_data = train_data.dropna(subset=[col for col in feature_columns if col in train_data.columns])\n", "train_data = train_data.dropna(subset=['label'])\n", "train_data = train_data.reset_index(drop=True)\n", "# print(test_data.tail())\n", "test_data = test_data.dropna(subset=[col for col in feature_columns if col in train_data.columns])\n", "# test_data = test_data.dropna(subset=['label'])\n", "test_data = test_data.reset_index(drop=True)\n", "\n", "transform_feature_columns = feature_columns\n", "transform_feature_columns = [col for col in transform_feature_columns if col in feature_columns and not col.startswith('cat') and col in train_data.columns]\n", "# transform_feature_columns.remove('undist_profit_ps')\n", "print('去除极值')\n", "cs_mad_filter(train_data, transform_feature_columns)\n", "# print('中性化')\n", "# cs_neutralize_industry_cap(train_data, transform_feature_columns)\n", "# print('标准化')\n", "# cs_zscore_standardize(train_data, transform_feature_columns)\n", "\n", "cs_mad_filter(test_data, transform_feature_columns)\n", "# cs_neutralize_industry_cap(test_data, transform_feature_columns)\n", "# cs_zscore_standardize(test_data, transform_feature_columns)\n", "\n", "mad_filter_feature_columns = [col for col in feature_columns if col not in transform_feature_columns and not col.startswith('cat') and col in train_data.columns]\n", "cs_mad_filter(train_data, mad_filter_feature_columns)\n", "cs_mad_filter(test_data, mad_filter_feature_columns)\n", "\n", "\n", "print(f'feature_columns: {feature_columns}')\n", "\n", "\n", "print(f\"df最小日期: {df['trade_date'].min().strftime('%Y-%m-%d')}\")\n", "print(f\"df最大日期: {df['trade_date'].max().strftime('%Y-%m-%d')}\")\n", "print(len(train_data))\n", "print(f\"train_data最小日期: {train_data['trade_date'].min().strftime('%Y-%m-%d')}\")\n", "print(f\"train_data最大日期: {train_data['trade_date'].max().strftime('%Y-%m-%d')}\")\n", "print(len(test_data))\n", "print(f\"test_data最小日期: {test_data['trade_date'].min().strftime('%Y-%m-%d')}\")\n", "print(f\"test_data最大日期: {test_data['trade_date'].max().strftime('%Y-%m-%d')}\")\n", "\n", "cat_columns = [col for col in feature_columns if col.startswith('cat')]\n", "for col in cat_columns:\n", " train_data[col] = train_data[col].astype('category')\n", " test_data[col] = test_data[col].astype('category')\n", "\n", "print(df[['ts_code', 'trade_date', 'log_circ_mv']].head(3))\n" ] }, { "cell_type": "code", "execution_count": 24, "id": "e23d1759", "metadata": {}, "outputs": [], "source": [ "# feature_columns = [\n", "# 'undist_profit_ps', \n", "# 'AR_BR', \n", "# # 'pe_ttm',\n", "# # 'alpha_22_improved', \n", "# # 'alpha_003', \n", "# # 'alpha_007', \n", "# # 'alpha_013', \n", "# # 'cat_up_limit', \n", "# # 'cat_down_limit', \n", "# # 'up_limit_count_10d', \n", "# # 'down_limit_count_10d', \n", "# # 'consecutive_up_limit', \n", "# # 'vol_break', \n", "# # 'weight_roc5', \n", "# # 'price_cost_divergence', \n", "# # 'smallcap_concentration', \n", "# # 'cost_stability', \n", "# # 'high_cost_break_days', \n", "# # 'liquidity_risk', \n", "# # 'turnover_std', \n", "# # 'mv_volatility', \n", "# # 'volume_growth', \n", "# # 'mv_growth', \n", "# # 'lg_flow_mom_corr_20_60', \n", "# # 'lg_flow_accel', \n", "# # 'profit_pressure', \n", "# # 'underwater_resistance', \n", "# # 'cost_conc_std_20', \n", "# # 'profit_decay_20', \n", "# # 'vol_amp_loss_20', \n", "# # 'vol_drop_profit_cnt_5', \n", "# # 'lg_flow_vol_interact_20', \n", "# # 'cost_break_confirm_cnt_5', \n", "# # 'atr_norm_channel_pos_14', \n", "# # 'turnover_diff_skew_20', \n", "# # 'lg_sm_flow_diverge_20', \n", "# # 'pullback_strong_20_20', \n", "# # 'vol_wgt_hist_pos_20', \n", "# # 'vol_adj_roc_20',\n", "# 'cashflow_to_ev_factor',\n", "# 'ocfps',\n", "# 'book_to_price_ratio',\n", "# 'turnover_rate_mean_5',\n", "# 'variance_20',\n", "# 'bbi_ratio_factor'\n", "# ]\n", "# feature_columns = [col for col in feature_columns if col in train_data.columns]" ] }, { "cell_type": "code", "execution_count": 25, "id": "8f134d435f71e9e2", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T14:57:51.050696Z", "start_time": "2025-04-03T14:57:51.034030Z" }, "jupyter": { "source_hidden": true } }, "outputs": [], "source": [ "from sklearn.preprocessing import StandardScaler\n", "from sklearn.linear_model import LogisticRegression\n", "import matplotlib.pyplot as plt # 保持 matplotlib 导入,尽管LightGBM的绘图功能已移除\n", "from sklearn.decomposition import PCA\n", "import pandas as pd\n", "import numpy as np\n", "import datetime # 用于日期计算\n", "from catboost import CatBoostClassifier\n", "from catboost import Pool\n", "\n", "\n", "def train_model(train_data_df, feature_columns,\n", " print_info=True, # 调整参数名,更通用\n", " validation_days=180, use_pca=False, split_date=None,\n", " target_column='label'): # 增加目标列参数\n", "\n", " print('train data size: ', len(train_data_df))\n", "\n", " # 确保数据按时间排序\n", " train_data_df = train_data_df.sort_values(by='trade_date')\n", "\n", " # 识别数值型特征列\n", " numeric_feature_columns = train_data_df[feature_columns].select_dtypes(include=['float64', 'int64']).columns.tolist()\n", "\n", " # 去除标签为空的样本\n", " initial_len = len(train_data_df)\n", " train_data_df = train_data_df.dropna(subset=[target_column])\n", "\n", " if print_info:\n", " print(f'原始样本数: {initial_len}, 去除标签为空后样本数: {len(train_data_df)}')\n", "\n", " train_data_split = train_data_df\n", "\n", " # 提取特征和标签,只取数值型特征用于线性回归\n", " \n", " if split_date is None:\n", " all_dates = train_data_df['trade_date'].unique() # 获取所有唯一的 trade_date\n", " split_date = all_dates[-validation_days] # 划分点为倒数第 validation_days 天\n", " train_data_split = train_data_df[train_data_df['trade_date'] < split_date] # 训练集\n", " val_data_split = train_data_df[train_data_df['trade_date'] >= split_date] # 验证集\n", " \n", " X_train = train_data_split[feature_columns]\n", " y_train = train_data_split[target_column]\n", " \n", " X_val = val_data_split[feature_columns]\n", " y_val = val_data_split['label']\n", "\n", " # # 标准化数值特征 (使用 StandardScaler 对训练集fit并transform, 对验证集只transform)\n", " scaler = StandardScaler()\n", " # X_train = scaler.fit_transform(X_train)\n", "\n", " # 训练线性回归模型\n", " # model = LogisticRegression(random_state=42)\n", " \n", " # # 使用处理后的特征和样本权重进行训练\n", " # model.fit(X_train, y_train)\n", " \n", " \n", " cat_features = [i for i, col in enumerate(feature_columns) if col.startswith('cat')]\n", " print(f'cat_features: {cat_features}')\n", " # cat_features = []\n", "\n", " params = {\n", " 'loss_function': 'Logloss', # 适用于二分类\n", " 'eval_metric': 'Precision', # 评估指标\n", " 'iterations': 500,\n", " 'learning_rate': 0.01,\n", " 'depth': 8, # 控制模型复杂度\n", " 'l2_leaf_reg': 1, # L2 正则化\n", " 'verbose': 500,\n", " 'early_stopping_rounds': 300,\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", "\n", " train_pool = Pool(data=X_train, label=y_train, cat_features=cat_features)\n", " val_pool = Pool(data=X_val, label=y_val, cat_features=cat_features)\n", "\n", "\n", " model = CatBoostClassifier(**params)\n", " model.fit(train_pool,\n", " eval_set=val_pool, plot=True, use_best_model=True)\n", "\n", "\n", " return model, scaler, None # 返回训练好的模型、scaler 和 pca 对象" ] }, { "cell_type": "code", "execution_count": 26, "id": "c6eb5cd4-e714-420a-ac48-39af3e11ee81", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T15:03:18.426481Z", "start_time": "2025-04-03T15:02:19.926352Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "train data size: 36400\n", "原始样本数: 36400, 去除标签为空后样本数: 36400\n", "cat_features: [27, 30, 37, 39, 41, 80, 86, 87, 88, 100, 102, 141]\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "66258c1f2f874fd0bfd05d169b09cc51", "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.6593407\ttest: 0.2558140\tbest: 0.2558140 (0)\ttotal: 118ms\tremaining: 58.9s\n", "499:\tlearn: 0.9223881\ttest: 0.3881579\tbest: 0.4090909 (449)\ttotal: 43.8s\tremaining: 0us\n", "bestTest = 0.4090909091\n", "bestIteration = 449\n", "Shrink model to first 450 iterations.\n" ] } ], "source": [ "\n", "gc.collect()\n", "\n", "use_pca = False\n", "# feature_contri = [2 if feat.startswith('act_factor') or 'buy' in feat or 'sell' in feat else 1 for feat in feature_columns]\n", "# light_params['feature_contri'] = feature_contri\n", "# print(f'feature_contri: {feature_contri}')\n", "model, scaler, pca = train_model(train_data\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)\n" ] }, { "cell_type": "code", "execution_count": 27, "id": "5d1522a7538db91b", "metadata": { "ExecuteTime": { "end_time": "2025-04-03T15:04:39.656944Z", "start_time": "2025-04-03T15:04:39.298483Z" } }, "outputs": [], "source": [ "# train_data = train_data.sort_values(by='trade_date')\n", "# all_dates = train_data['trade_date'].unique() # 获取所有唯一的 trade_date\n", "# split_date = all_dates[-120] # 划分点为倒数第 validation_days 天\n", "# print(split_date)\n", "# print(all_dates)\n", "# val_data_split = train_data[train_data['trade_date'] >= split_date] # 验证集\n", "\n", "score_df = test_data\n", "score_df = fill_nan_with_daily_median(score_df, ['pe_ttm'])\n", "score_df = score_df[score_df['pe_ttm'] > 0]\n", "score_df = score_df.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n", "score_df = score_df.merge(index_data, on='trade_date', how='left')\n", "# score_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(50, 'total_mv')).reset_index()\n", "numeric_columns = score_df.select_dtypes(include=['float64', 'int64']).columns\n", "numeric_columns = [col for col in feature_columns if col in numeric_columns]\n", "# score_df.loc[:, numeric_columns] = scaler.transform(score_df[numeric_columns])\n", "# score_df = cross_sectional_standardization(score_df, numeric_columns)\n", "\n", "score_df['score'] = model.predict_proba(score_df[feature_columns])[:, 1]\n", "score_df['score_ranks'] = score_df.groupby('trade_date')['score'].rank(ascending=True)\n", "\n", "score_df = score_df.groupby('trade_date', group_keys=False).apply(\n", " lambda x: x[x['score'] >= x['score'].quantile(0.90)] # 计算90%分位数作为阈值,筛选分数>=阈值的行\n", ").reset_index(drop=True) # drop=True 避免添加旧索引列\n", "# save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nlargest(1, 'score')).reset_index()\n", "save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(1, 'total_mv')).reset_index()\n", "save_df[['trade_date', 'score', 'ts_code']].to_csv('predictions_test.tsv', index=False)\n" ] }, { "cell_type": "code", "execution_count": 28, "id": "09b1799e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "190\n", "['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', '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" ] } ], "source": [ "print(len(feature_columns))\n", "print(feature_columns)" ] }, { "cell_type": "code", "execution_count": 29, "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": 30, "id": "a0000d75", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "开始分析 'score' 在 'circ_mv' 和 'future_return' 下的表现...\n", "准备数据,处理 NaN 值...\n", "原始数据 173284 行,移除 NaN 后剩余 172527 行用于分析。\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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6NKlSxo+fLjOnTunFi1apMvs2rVLbdu2TfdDiDJlykhK+7lyp5w5c0aGYahAgQJp/o5+9913WrFiRYZjEhMT9fnnn6f5c+oPw2483ib1Yaqffvppmp3369at0zvvvKPvvvvutmqfMmWKgoKCMt25DgAAAO/DGeEAAJisZcuWmjVrlvr3769ly5apcOHCOnv2rJYtW6bAwEA1bNjQnn3ppZe0YcMGrVu3Tk2bNlXNmjV1/vx5LVu2TKGhoRoyZIg9++STT+o///mPZsyYoWeeeUZ16tTRuXPntHLlSkVERGjUqFFOLaBGREToww8/VI8ePdSqVSvVq1dPBQsW1G+//aY9e/aoYcOGqlmzplPfiw4dOmjfvn1q1apVhov0R44c0QcffKA1a9aoa9eumjx5sjp27KgtW7bo+eef14svvqjatWunO3YiJiZGu3btknT94XsNGjTQk08+qccff9y+GJucnKy33npLx48f19dff53h9+b777/X9u3b9fzzz2d49npiYqK6deumEydO6MUXX0zz8MIbHT9+XGFhYWmOe7lZ+fLl7f87o53SU6dOVUpKimrXrp3pPVwtKChI/fv3V9euXdWkSRP7Q0IzExgYqFdffVUDBgzQxIkT7Uef1KxZU3v37lXHjh311FNP6fjx41q7dq39IaQ3/uDmdgUGBmrgwIFq166d3nvvPU2bNs1l985MTEyMtm3bpuXLl6tZs2Z66qmnZLFYtG3bNpUuXVodO3a8rfvXq1dPOXLk0Ny5cxUeHp7mMyJV9erVVbZsWc2ePVuHDh1S+fLldfXqVfsC9O0uxmdH6dKlde+992rLli3q1q2bihYtqu3btys2NlZ58uTRxYsX052lnidPHk2fPl27du1SyZIltWnTJh04cEA1atRI8z7bt2+vdevWaf78+frzzz9VvXp1Xbp0SUuXLlVkZKQGDhzodN379u3TpEmTNGDAgDQPIAYAAIB3Y0c4AAAmK1GihL799ls988wz2rVrl2bNmqVffvlFVatW1fTp09PsiA4ODtbnn3+ufv36KTg4WHPmzNGvv/6q2rVra/78+apcuXKaew8cOFAfffSRChQooIULF2rz5s1q0KCBvv3223RZR9SuXVtz5sxRzZo1tWHDBs2dO1eBgYEaNmzYbR25EBoaqk8++STNInjqjtqtW7eqSZMmWrNmjerXr69G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okUL/f7771q3bp1OnTqlRYsWOXVsTnaP8bl8+bIkZfr5lStXLp06dSpb2dy5c9uz0vW/86tXr9bMmTPVsmVLlS1bVn///be++OILlS1bVt26dXO65sjIyAx/8yf17/vJkyeVK1cue8133313umyePHkkXT+qyJmF8J9//tl+7Eq/fv30yCOPODS+b9++qlGjhgYMGKC+ffva/0199913+vjjjxUaGqrAwEBdunRJf/75p2rXrq2kpCTFxMTYPwsvXbqkM2fOpLv3jT/4AwAA8DcshAMAvNY///yjixcv2o8xkaQrV67IZrPpwIEDGY5JTk5WYmJimteTk5Nls9nsOwN79ep1y7kPHjyoI0eOqFOnTul25rrC6dOnNXjwYIWFhWnBggX2M79ff/11NW7cWF9//bU6duyoAgUKOD1HfHy83njjDbVr105dunRRjhw50mWGDh2qI0eOqGfPnuratav9+pAhQzR37lzNnTtXr776qlPzV6xYUV988YX27Nmj5s2bS5Jmz55tX/A/ceKEpOtn9ubLly/dQvj69esVHx+fZiH5dixYsEAnT56UdP34iOeff14PPvjgHX/A5I07XydNmqSiRYumueaM3Llz2+9x9OhRzZo1Sw888MBt3/dm3333neLj4zVt2rR0D/eTrv977N+/v3LkyKFFixbp3nvvlXT939gzzzyjKVOmKCYmxqnfLEiVnb/HY8aM0b59+/Tuu+/qhRdesF//4IMP9PXXX2vhwoVq166d7rvvPvv3aPPmzfr+++9Vo0YNlz8sc8qUKTIMQ/Pnz1fFihUzzc2bN0/169fX6NGjFRISIpvNplatWikuLk6HDh1K9xsSrpS6WJrRTmTp+k7vq1evZjt78eJF+5+Dg4M1YsQItWzZUsOGDdPMmTP13nvvyTAMjRw50v7DJ0cFBgZmevxV6j1THyScWnNG+ZuzjnrttddUvnx51ahRQ6GhodqyZYsiIiIUHBysunXrymq1avPmzenGPfPMMzp8+LD9KJrp06eneb1q1ar64IMPFBoaqp9++knz589XVFSUBg4cqGvXrqlQoUL2nfdvvPFGpvVxzBMAAPBXLIQDALzW5MmTtWDBggxfy+polNOnT6d7Patd5GZYunSprl69qtdeey3Ngy9z586tMWPG6MyZM/bFf2ddunRJzZs315tvvpnh6xcvXtTq1atV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9evXq0GDBvZzfp3x999/67XXXlNiYqI++eSTDI9ukNIv4A0fPlx9+/a1f6UeY+MOqTXu37/ffi0pKUmjRo3KdEzBggUVEhKS7tiKa9eupbvvvn370mTGjh2rhIQEp+tNPdrjxx9/THNvm82mYcOG6b333tOFCxecvn92BAYG6umnn5bVatWUKVPSvLZw4UK999579p26NypSpIik9Md93Or7ZhiGRowYccePLnGHV155RYGBgerTp4/++usv+wM+v/32W91777164okn7NmYmBhFRETonXfe0fbt2yVJq1ev1uTJk5UzZ041atQozb1/+ukn/fjjj2revLneeusttW7dWsuWLbMvnDujfPnyqlmzptasWaPx48crOTlZp06d0htvvKHk5GS1atXKnr377rvtDx595513ZLVadenSJfXt21fnz5/Xs88+69Kjhj755BOdPXtWLVu2VK5cudK9nvoDtawWwtetW6fPP/9cDz/8sBo3bpzmtdatW+vll1+2/3CmUKFCKlWqVLqvzB4mCgAA4A84JA4A4LX69Omj77//Xi+//LJ95+nkyZMVEhKi4OBgBQQEqEuXLjp37pzmzp0rm82mixcvymq1Srq+w7hevXr65ptvdO+99yo0NNTMt5NGwYIFNWzYMA0cOFCtWrVSgwYNlDNnTq1du1bHjh1TTEyMoqKi7ngd77zzjvbs2aNx48Zp48aNevDBB7Vv3z5t3LhR9913322f6euIe+65R9WqVbMvlDVv3vy27lewYEFVqVJFVapUydaCfuqu3xUrVqQ5H/3ll1/WX3/9dVu1ZFeDBg30xRdfaMSIEQoJCVFoaKg+++wzBQUF2Y98uFlwcLCaNm2qhQsXqlu3bipRooROnTqlEydOaNasWZKkypUrK3/+/Jo3b57KlCmjsmXLauHChVq2bJkqVaqkHTt2OFVvRESEPvzwQ/Xo0UOtWrVSvXr1VLBgQf3222/as2ePGjZsqJo1azr9/ciu3r17a8uWLRo/frx++eUXPfTQQzp58qRWrlypu+++W7179043pnDhwqpRo4Z++OEHGYahggUL6siRIwoODtbo0aMlXd9VHB4erkmTJqlgwYIqWLCgvvrqKx04cEClS5d229+LO+WRRx5R//79NXLkSD399NP2h8lGRkZq1KhRaT4z77vvPg0fPlz9+/fXiy++aM+GhIToo48+SnPMyMmTJzV06FDlz59fAwcOlCT169dP69ev1/vvv6+qVauqaNGiTtU8YsQIvfzyy5owYYKmTJkim80mm82munXr6sUXX0yT7d+/vw4ePKh58+Zp0aJFMgxD165dU8WKFdWzZ0+n5s/I3LlzNXPmTBUqVCjT+6YepZPZQrhhGBo9erRCQkL0/vvvKyAgIMMftmTnBzA37oAHAADwJyyEAwC8Vuoi5o1SF8RThYSEKCgoyP4wt9Rdnqly5Mih1157LVvz7du3z342d+oO0Owcn+CsFi1aqGjRovr888+1cuVKXbt2TaVLl1bXrl313HPP3bF5b1SoUCEtWLBAEydO1PLly7Vjxw7lz59fL730krp37+72s9VbtGih3377TYUKFVKNGjVu6145cuTQ2LFj0zw8LqtFpNTz0AMDA9OMCQgIUGJi4m3Vkl0PPfSQRo0apYkTJ6pHjx7Kly+fGjdurB49euiZZ57JdNzgwYN11113admyZVqzZo1y586dJp8zZ05NmTJFH3zwgYYPH67Q0FA9+uijWrBggT777DOnF8IlqXbt2pozZ44mTpyoDRs2KCkpSSVKlNCwYcPc9vc4d+7cmjt3riZNmqSlS5dq9uzZKliwoFq3bq1u3bplei7zmDFjNHr0aK1bt07//vuv7r77bsXExNhfL1KkiCZOnKhRo0ZpwIABypkzp2rXrq158+a5dCHVTP/5z39UuXJlff311zpy5IhKly6tzp07p/sslaTGjRvrgQce0BdffKG//vpLhQsXtp9VncowDPXv318XL17Up59+aj+qJFeuXBo6dKg6d+6svn37atasWU59vubPn18LFy7UzJkztX79egUHB6tJkyZq2bKlfbd0qoiICM2YMUMLFizQsmXLdO3aNdWuXVsvvfSSSx4qmZSUpHHjxmnatGnKlSuXJk+enOEDOM+fP68ffvhBkjL9AUBAQIDGjh2rnTt3qkyZMpJkX+S/Ueqfbzw65Wap33MAAAB/E2D4wu9tAgD8VlJSkiZMmKCwsDD7LvAbzZ49WwkJCekWBVJSUmSz2ZSUlKTnnntO99133y3nWr58uXr06GH/c2hoqLZu3arw8HDXvBmYzmKxqFWrVho8eLBeeumlNK/9888/Onr0qCpXrqywsDD79QsXLujatWuZ7sgG4F+SkpL0ww8/aNKkSfrnn39UqFAh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48ePj/nz58eBBx4YPXr0WOr7fD4fN910Uzz//PPxX//1XxVtFAAAAAAAylHSRvjw4cMjk8nEhAkT4oorrohu3bpF165do0OHDjFr1qyYN29ebL311nHLLbfEdtttt7J6BgAAAACANitpIzyTycTw4cPjhBNOiOeffz5mzZoV8+fPj44dO8baa68dvXv3jo033nhl9QoAAAAAULZiMekOSEqbN8LfeOON6NmzZ0RErLHGGrHnnnuWtAYAAAAAAJLQph/LfO211+KII46I3//+922+8O233x7HHntszJ49u+zmAAAAAABYOT744IMYNmxY9O3bNw477LCYOnVqm9Z99NFHccYZZ0Tfvn2jT58+ccopp8Q///nPiIj42c9+Fl/5ylda/LP33nuvzFv5Qm3aCN9yyy3jlltuicsvvzxGjhwZ77//fqvZN954I0455ZS455574o477oiuXbtWrFkAAAAAANqvWCzGaaedFh9++GHcfffdceyxx8awYcNi7ty5X7j2Bz/4QXz66adx7733xv333x8zZ86Myy+/PCIiTjrppJg0adJS/wwcODD22GOPlX1LK9Tmo1Gy2Ww8/PDDcd1118WgQYNim222ib59+8a6664bxWIx3nvvvfjLX/4Sf/vb3+KEE06IoUOHxqqrlnQEOQAAAADAyuOM8CWef/75mDx5cjz88MOxxRZbxBZbbBEPPfRQPPbYY3HwwQe3um727NnRpUuXuOyyy6Jz584REXHIIYfEfffdFxERnTp1ik6dOi3J5/P5+POf/xwTJ05cuTf0BTLFYulHxC9YsCD++Mc/xrRp0+L999+PDh06xLrrrhvbbLNN7LzzzqneAM/lckm3AAAAAAAV0dzcnHQLNWXTW8Yn3ULFbXnHIyv8/vHHH1/u57fffnv84he/iCeffHLJZ9dcc018+OGHMWbMmJJ6OPvss6NQKMT111/f4rsTTzwx+vXrF9/73vdKumZExMcffxxPPPFEzJw5M4YMGRLPPfdcbLjhhrHtttuWfK2ydqw7duwYe++9d+LnupRr2QdEoVCIfD4f2Ww2mpqaWuRzuVxJDxX5ZPOtrVnRnNN2D/LlrTHj9OYrVcOMq5dPqiczLj+fxp7MOPkaZpxsPo09mXHyNWohX0szTmNPtZBPcsbVqNFoeSjXnDlzYpNNNlnqs27dukU+ny/pOlOnTo2JEyfGrbfe2uK7fD4fzz//fFx11VUl95fP52Po0KHxySefxOLFi2PgwIExadKk+O///u+47rrrYs899yzpeul9dRsAAAAAgBVq7Y3vL7LqqqsudYRJRMTqq68ehUKhzdeYN29enH322XHYYYdF//79W3z/i1/8Ig477LCyfkfy4osvjv79+8ell14aO+20U0REjB49OlZdddX4yU9+UvJGeJt+LBMAAAAAoOYVM/X3T5m6d+8eH3zwwVKfffLJJ9GxY8e2/a+yWIyRI0fG6quvHqNHj27x/Zw5c+KRRx6JQw89tKz+/v1GeLdu3Zb6/Bvf+EbMmDGj5OvZCAcAAAAAaDDbb799TJ8+PWbPnr3ks5deeik22GCDNq0fP358vPDCC3Hddde1eLM8ImLixImx0UYbxTbbbFNWf+uuu+5yj2l54YUXokePHiVfz0Y4AAAAAECD2WqrraJXr15x1VVXxeLFi+Pll1+ORx99NPbee+9YvHhxzJ49OxYtWrTctTfffHPceeedMWHChFhjjTVi7ty5MXfu3KUyjz/+eOy+++5l9zd06NAYP358XHbZZZHJZOJ///d/Y9y4cfHTn/40jj/++JKv54xwAAAAAIAGNHbs2Dj55JPjd7/7XcyZMycOPvjg2HPPPePNN9+MAQMGxP333x/ZbLbFuptuuikKhUIcccQRS30+bdq0iIhYsGBBPPPMM3H44YeX3dvRRx8dnTt3jmuuuSaKxWJcddVVseGGG8aYMWPiP/7jP0q+no1wAAAAAIAGtPXWW8cjjzwSkyZNiu7du8e2224bEREbb7zxkk3t5XnmmWdWeN2OHTvG5MmT293foYceGoceemjMnTs3isVirLHGGmVfy0Y4AAAAANAQMsWkO0ifpqam2GuvvZJuY4W6dOnS7ms4IxwAAAAAgFT57W9/G6+//nrFrmcjHAAAAACAVLn66qvjT3/6U8WulykWiw31FwJyuVzSLQAAAABARTQ3NyfdQk3Z7OdXJN1Cxc387jlJt7BSXHnllfHCCy/E7bffXpHrNeQZ4cs+IAqFQuTz+chms9HU1NQin8vlSnqoyCebb23NiuactnuQL2+NGac3X6kaZly9fFI9mXH5+TT2ZMbJ1zDjZPNp7MmMk69RC/lamnEae6qFfJIzrkaNRstTooZ6Jbi2nXHGGXHGGWfE8OHD47zzzov11luvXddbqRvhCxYsiI4dO67MEgAAAAAA1Jn9998/IiLefvvtePLJJ2Pddddd6vvHH3+8pOuVvBH+wQcfxNe+9rWYMmVKrLpq68sXLlwYQ4YMiTPPPDN22WWXUssAAAAAANCgTj/99Iper+SN8E6dOsXChQtj8ODB8aUvfSk23njj2HLLLaNfv37R3Nwcq622WkREXHbZZTFt2rRYZ511KtowAAAAAAD17dBDD63o9co+GuW0006L999/P95777149tln4+abb45isRiHHXZYfPbZZ/Gb3/wmrrjiithqq60q2S8AAAAAQHmKmaQ7oAwfffRRFIvF6N69e9nXaNNGeLFYjBkzZsQWW2wRERGZTCYOOOCApTKLFy+Oq6++On7+859HJpOJCy64IAYOHFh2YwAAAAAANK4HH3wwJkyYEG+99VZERGy00UYxfPjwGDx4cMnXatNG+F/+8pcYOnRobLnlltG/f/+I+NcPYX766afxyiuvxKRJk+L3v/99vPPOO/Htb387Fi9eHDfccEN8/etfb/eveQIAAAAA0FgeeOCBGDVqVBx00EFLzgt/+umn49xzz42IKHkzvE0b4X369In//M//jJdffjn+7//+Lzp16hRf/epX47PPPou111479txzzzjppJPi61//enTp0iUiIt577734/ve/H7/+9a9LaggAAAAAgMZ23XXXxcknnxzDhw9f8tnBBx8c66+/flxzzTUrZyO8S5cu0dzcHDvvvHPssMMOMXXq1Ghqaop77703Jk2aFOuuu24ceOCBS/Lz58+PESNGxOGHHx533XVXHH744SU1BQAAAABQccWkG6Ct3nnnndhxxx1bfL7jjjvGLbfcUvL1MsVi8QvHXygU4mtf+1oMGjQounXrFq+99lpce+21cemll8ZGG20U3bt3j5tvvjkGDRoUxxxzTNx7773x1FNPxYABA2KzzTaLXXfdteTGVpZcLpd0CwAAAABQEc3NzUm3UFM2u+HKpFuouJmnjEi6hZXiyCOPjPXWWy+uuuqqWGWVVSLiX79TeeaZZ8asWbPizjvvLOl6bdoIj4j429/+Fv/5n/8Zq6++erz11luxwQYbxKRJk+L666+PiIgBAwZE375945VXXolOnTrF+eefH4MGDSrx9la+XC7X4gFRKBQin89HNpuNpqamNq0ptYZ89fKtrVnRnNN2D/LlrTHj9OYrVcOMq5dPqiczLj+fxp7MOPkaZpxsPo09mXHyNWohX0szTmNPtZBPcsbVqNFoeUpjI7x2vPDCCzF06NBYd911o1+/fhER8fzzz8d7770Xt956a2y77bYlXa9DW0JTp06Np59+Ovr06RP//Oc/45133onVV189DjrooPj73/8eEREdO3aM22+/PS688ML48MMP46WXXirx1gAAAAAAIGK77baLu+66K/r27RsvvfRSvPjii9G3b9+4++67S94Ej2jjGeG5XC5uvfXWWHXVVWPOnDnx7rvvxvvvvx/du3ePqVOnxt/+9rcl2UWLFsVOO+0UEydOjJ133jn22muvkpsCAAAAAKg4Z4TXlC233DLGjRtXkWu1aSP8m9/8Znzzm9+MSZMmxV//+te4+uqr49NPP41LLrkkmpqa4uqrr44pU6bEt771rejUqVMcdNBBsfbaa8fYsWNjzz33jEwmU5FmAQAAAABoDB9//HG888470bt373jrrbfi//l//p/Yb7/9Yp111in5Wm06GmXx4sUxevToOOuss+If//hH7LDDDjFixIg46aSTYvXVV49zzjlnyQb43/72t9htt91iwIABkclk4rHHHiu5KQAAAAAAGtfLL78c+++//5LfqPzwww9j3LhxceCBB8bUqVNLvl6bNsKLxWJ06dIlHnjggejSpUvMnz8/vvnNb8Y+++wTF110USxYsCA+++yzGDJkSPz+97+PDTfcMCIiBg4cGNOnTy+5KQAAAAAAGtfll18e/fr1i9GjR0dERJ8+fWLSpEmx0047lXVcSpuORllllVXi3HPPjYiIww8/PA466KCIiDj77LPjzTffjPXXXz+uvfbaiIjo1KnTknXHH398dOvWreSmAAAAAAAqzhnhNePll1+OG2+8caljUDp16hRHH310DBs2rOTrtemN8M/r0qVLrL322hER0bVr19h6662jS5cu8bWvfa1F1iY4AAAAAACl6tq1a7z66qstPn/11VdjjTXWKPl6bXoj/N8+++yz+M1vfhMHH3zwCostWLAgBg4cGNdff318+ctfLrkpAAAAAAAa11FHHRVXXnllfPLJJ7HDDjtERMSzzz4bN910U5xyyiklX6+kjfAOHTrEJZdcEvvss88KN8I7duwYb731VqyyyiolNwQAAAAAQGM7+eST45NPPolrr702PvvssygWi7HaaqvFcccdFyeffHLJ18sUi8WSTsbp3bt3/OlPf4qHH344Xnrppdh7771j5513ju7du7fITZw4MTbffPOSm1qZcrlc0i0AAAAAQEU0Nzcn3UJN2ezaHyfdQsXNPPXspFtYqQqFQrz22msREbHFFltEly5dyrpOSW+ER0RkMpmIiHjzzTfjkUceiQcffDA6dOgQW265Zey4447Rv3//Ja+qp9WyD4hCoRD5fD6y2Ww0NTW1yOdyuZIeKvLJ5ltbs6I5p+0e5MtbY8bpzVeqhhlXL59UT2Zcfj6NPZlx8jXMONl8Gnsy4+Rr1EK+lmacxp5qIZ/kjKtRo9HyUO+amppi2223bfd12rwR/tFHH8WcOXOW/Pm8886LkSNHxsyZM+OVV16JV155JaZMmRK/+c1v4rPPPluyYQ4AAAAAAKWYPXt2XHXVVXHIIYfE9ttvH+PGjYu77747evbsGVdeeWX06tWrpOt1+KLA4sWL4z//8z9jv/32iwkTJiz13SqrrBJbbLFFDB48OEaOHBm//vWv49lnn41f/OIXpd0VAAAAAAD8f370ox/FX/7yl1hzzTXjL3/5S9x2221x9NFHR0TEpZdeWvL1vnAjvEOHDvE///M/ccghh8TIkSOjtSPFZ86cGXfeeWeMHj06vvrVr7aaAwAAAABIQqZYf//Uq6eeeipGjBgRW2yxRfzxj3+MAQMGxJlnnhlnnXVWTJkypeTrtelolDvuuCNWXbVl9Nlnn43HH388fv/738dbb70VnTp1ir59+8a7775bciMAAAAAABDxr9+q7NSpU0RETJkyJfbff/+IiFh11VVjtdVWK/l6bdoI//cm+OLFiyOTycSiRYvimWeeieOOOy569eoVgwYNir322iu23XbbspoAAAAAAIB/23HHHeOiiy6KbbfdNp5//vm46KKL4v33349f//rXsf3225d8vTb/WGZExLx586JYLMb8+fOjf//+cf/990fv3r1b5P59LMqiRYtKbggAAAAAgMY2ZsyYuOCCC2L69Olx/vnnx+abbx6XXXZZTJ06Na6//vqSr9emjfCXXnopNt544+jevXtMmjQp1lhjjchkMsvdBI+I+Oyzz2Lw4MFLXl0HAAAAAIC2WnfddeO6665b6rOzzjorzjvvvLKu16aN8AsuuCBmzJgR3/jGN2L//feP7t27f+Gao446KmbNmhVvvPFG7LrrrmU1BwAAAABQMXX845K17JNPPok11ljjC3Orr756yWv+LVP89zkmrSgWi/GnP/0pnnzyyXj00Ufj/fffj0wms9T3KyyQyUQ+n29zQytbLpdLugUAAAAAqIjm5uakW6gpm//sx0m3UHGvn3520i20y/Tp0+M73/lOXHPNNbHddtu1ac1jjz0WF198cTz44IPRtWvXNq35wo3wz1u4cGE8+uijcdttt8ULL7wQBx10UJx55pmx/vrrt8guWrQoFi5cGHPmzIl11lmnrSVWulwu1+IBUSgUIp/PRzabjaampjatKbWGfPXyra1Z0ZzTdg/y5a0x4/TmK1XDjKuXT6onMy4/n8aezDj5GmacbD6NPZlx8jVqIV9LM05jT7WQT3LG1ajRaHlKYyM8nZ566qkYOXJkHHXUUXHyySdHx44dl5v75JNP4uqrr44nnngirr322th6663bXKOkH8tcddVVY9CgQTFo0KD4wx/+EJdddlkceOCBcccdd8SXv/zlpbKrrLJKrLLKKs4JBwAAAACgVXvssUfcd999MX78+Nhzzz1jwIAB0bdv31h33XWjWCzGe++9F3/5y1/ij3/8YwwaNCgeeOCBNr8J/m8lbYR/3l577RU77bRTTJ06tcUmOAAAAAAAtNV6660XP/7xj2PWrFnx6KOPxpQpU5Yc092jR4/Ycccd44c//GGsvfbaZV2/7I3wiIh//OMfsf3227fnEgAAAAAAEBH/2hA/9thjK37dDuUuXLx4cZxxxhnx/e9/v4LtAAAAAABAZZW9EX799dfHyy+/HPvuu28l+wEAAAAAgIoq62iUiRMnxrXXXhuHH354ZDKZmDJlSqy//vrRo0eP6NCh7L11AAAAAICVJlNMugOSUvJG+O233x6XX355HHzwwTF06NA44IADIpPJREREhw4dYt111431118/tt9++zjttNNijTXWqHjTAAAAAADQVm3eCJ85c2ZcfPHF8ec//zm++93vxplnnhkzZsyIiIgnn3wy3n333Xjvvfdi1qxZ8frrr8d///d/x4IFC2LMmDErrXkAAAAAAPgibdoIv/fee+P888+P5ubmuPPOO2Pbbbdd8l0mk4n1118/1l9//aXWdOjQIZ544gkb4QAAAAAAJCpTLBa/8GScN954I/7617/G1772taU+nzFjRhxwwAGRz+dbrLntttuiUCjEKaec8oVN3H777XHJJZcs9dmoUaNi6NCh8eKLL8aPfvSjmDFjRuy2225xySWXxFprrfWF12xNLpcrey0AAAAApElzc3PSLdSUXhOuSrqFipsx/KykW6gJbXojvGfPntGzZ8+SLnzccce1OTt58uQ47bTT4vjjj1/y2eqrrx4ffPBBnHDCCXHAAQfET37yk/jlL38Z559/flxzzTUl9bKsZR8QhUIh8vl8ZLPZaGpqapHP5XIlPVTkk823tmZFc07bPciXt8aM05uvVA0zrl4+qZ7MuPx8Gnsy4+RrmHGy+TT2ZMbJ16iFfC3NOI091UI+yRlXo0aj5YG26dDW4F//+tflfl4sFqNv377xrW99K6666qp4/fXXS25i8uTJseuuu0bXrl2X/NOxY8e46667onPnzvHDH/4wevbsGSNHjoxnnnkm3n333ZJrAAAAAADQmNq0Ef7ee+/Ff/zHf8SQIUNi8uTJLb4fNWpUbLvttvHwww/HgQceGFdeeWW04cSViIh499134+23346LLroo+vTpEwMGDIjbbrstIiJeeeWV2GWXXWK11VaLiIhOnTpF7969Y8qUKW28PQAAAAAAGl2bjkbp0aNHXHXVVXHTTTfF0UcfHYMGDYrRo0fHokWLIpPJxOGHHx6ZTCbOO++8+PWvfx2XX355vP/++zFu3LgvvPbUqVNjk002ibPOOiuy2Wz83//9X5x//vmx6aabxpw5c6J3795L5bt16xazZs0q727/P4VCYak/z5s3b6n/2ZY1pdaQr25+eWu+aM5puwf50teYcbrzlahhxtXNV6OGGVc2X40aZlzZfDVqmHGy+WrUMOPK5qtRI+35WptxNWrUWz7pGVejRiPll3fMLyvQtnd3qUNt+rHMz3vsscfiwgsvjEwmE6effnpMnDgxbrzxxujYseOSzG9/+9sYOXJkXHjhhXHEEUeU3NQPfvCDWLhwYXz88cexyy67xIknnrjku3POOSd69eoV3/ve90q+bsS/zlmaP39+SWs6depU0hr5ZPNp7Em+svk09iSffA35yubT2JN88jXkK5tPY0/ylc2nsSf55GvIVzafxp7kk6/RaPl+/fq1OUtEr5/U4Y9lfr++fizzr3/9a7z22mux3Xbbxfrrrx8R/9qTXrBgQWyzzTax6aablnXdkjfCIyI+/vjjGD16dDzxxBNx4YUXxje/+c0WmYsuuigefvjhmDhxYqy99tolXf/KK6+MZ555JjbZZJNYZ5114txzz13y3SmnnBL9+/ePE044odS2I+JfG+G9evVa6rN58+bFzJkzY7PNNovOnTu3WDNjxowWa1ZEPtl8a2tWNOe03YN8eWvMOL35StUw4+rlk+rJjMvPp7EnM06+hhknm09jT2acfI1ayNfSjNPYUy3kk5xxNWo0Wt4b4aWxEZ5es2fPjlGjRsXjjz8emUwmfv7zn8fuu+8eERFHH310PP/885HJZGKvvfaKH//4x9GlS5eSrt+mo1GW1a1bt/jpT38a48aNiwsuuCB23HHH2GSTTZbKnHTSSfHWW29Fp06dVnitn/3sZ9GpU6c46aSTlnz23HPPxQYbbBB9+/aN+++/f8nnxWIxXn755TjooIPKaXuJ1h4QnTt3bvW7Uh8q8snmV7SmtTmn7R7ky19jxunMV7KGGVcnX40aZlzZfDVqmHFl89WoYcbJ5qtRw4wrm69GjVrJ18qMq1GjXvNJzbgaNRotD/XgwgsvjBkzZsTNN98cffr0iW7dui357le/+lV89NFH8eyzz8bFF18cl1xySYwdO7ak65e1Ef5vI0eOjD333LPFJnhExPrrrx833HBDZDKZFV5j2223jbPPPjs23XTT2HjjjeO+++6LKVOmxG233Rabb755jB07Nh5++OE44IAD4o477ojZs2fHbrvt1p62AQAAAIBG5Izw1HryySdjwoQJS94C/7wOHTrE2muvHfvuu28Ui8UYNWpUdTfCIyJ22WWXVr/7ok3wiIi99torzjzzzLj00kvjo48+iq233jpuu+226N+/f0REXHLJJUtu7KOPPooLL7xwqX8bAAAAAABAbVtzzTXj/fff/8LcnDlzyvpbE+3eCK+EY445Jo455pjlfnfIIYfErrvuGi+++GJ85StfiZ49e1a5OwAAAAAAVqYhQ4bEJZdcErNnz479998/Nthgg6W+/+STT+Kxxx6LsWPHxpAhQ0q+flk/llnLcrlc0i0AAAAAQEU0Nzcn3UJN6XV1Hf5Y5pn18WOZERE33nhj3HDDDfHpp5/G6quvHl27do1VVlkl5s6dG7Nnz45isRiHH354/OhHP4oOHTqUdO1UvBFebcs+IAqFQuTz+chms8t9rT6Xy5X0UJFPNt/amhXNOW33IF/eGjNOb75SNcy4evmkejLj8vNp7MmMk69hxsnm09iTGSdfoxbytTTjNPZUC/kkZ1yNGo2WpzSZhnoluPacfPLJccwxx8Qf//jHmD59enz44YexYMGCaGpqis022yx233332HTTTcu6dkNuhAMAAAAAkD5rrLFG7L///rH//vtX9LqlvT8OAAAAAAA1xkY4AAAAAAB1zdEoAAAAAAAk6vHHHy8pP2DAgJLyNsIBAAAAgMbgxzJT69RTT21zNpPJRD6fL+n6NsIBAAAAAEhUqW+El6pdG+Evv/xyXHDBBTF16tRYtGhRi+9L3ZUHAAAAAKDxbLTRRiv1+u3aCB81alRERFx55ZWx9tprV6QhAAAAAACopEyxWCz7ZJztt98+rr/++thll10q2dNKlcvlkm4BAAAAACqiubk56RZqyhZXXpV0CxX31xFnJd1CTWjXG+HNzc3x0ksv1dRGeETLB0ShUIh8Ph/ZbDaamppa5HO5XEkPFflk862tWdGc03YP8uWtMeP05itVw4yrl0+qJzMuP5/Gnsw4+RpmnGw+jT2ZcfI1aiFfSzNOY0+1kE9yxtWo0Wh5oG06tGfxxRdfHL/5zW/i1ltvjQULFlSqJwAAAAAAqJh2vRF+wgknRKFQiLFjx8b48eOjR48e0aHD/7+3vrJ/6RMAAAAAAL5IuzbCTz/99Er1AQAAAACwUmXK/rVEal27NsIPPfTQSvUBAAAAAAArRbvOCAcAAAAAgLRr10b44MGD44EHHqhULwAAAAAAUHHtOhqle/fuMX369Er1AgAAAACw8hQzSXdAQtr1Rvjw4cPjnnvuiVwuV6l+AAAAAACgVX/4wx9KXtOuN8LfeOON+MY3vhFHH310HH744dGnT5+lvj/kkEPac3kAAAAAABrQiBEjYty4cbHKKqss+ey1116LsWPHxp///Od45ZVXSrpeplgsFsttZu+99279wplMPP744+VeeqXx9joAAAAA9aK5uTnpFmrKluOvTrqFinvtB2cm3cJKse+++0avXr3ipz/9aXzyyScxYcKEuPvuu2PnnXeOs88+O7beeuuSrteuN8KfeOKJ9ixPzLIPiEKhEPl8PrLZbDQ1NbXI53K5kh4q8snmW1uzojmn7R7ky1tjxunNV6qGGVcvn1RPZlx+Po09mXHyNcw42XwaezLj5GvUQr6WZpzGnmohn+SMq1Gj0fKUqOxXgqm2O+64I7773e/Gt771rXjrrbdis802i1tuuSV23nnnsq7XrjPCAQAAAACg0tZZZ524/fbbo2vXrrH66qvHjTfeWPYmeEQ73wi///77V/i9M8IBAAAAAPgikyZNWu7nJ510UlxyySVx7LHHxvnnnx+rrvqvLe3+/fuXdP12bYT/9Kc/XfKfi8VivP/++7Fo0aLo3LlzdO/e3UY4AAAAAABf6Nhjj/3CzLe//e2I+NfvU+bz+ZKuX9EzwhctWhSPPvpoXHXVVTF+/Pj2XBoAAAAAoKIyzghPralTp67U67drI3xZq6yySgwcODA22GCDuOSSS+Luu++u5OUBAAAAAKBkFd0I/7fm5uZ4/fXXV8alAQAAAACoc7lcLi688MKYOnVqLFq0qMX3VT0aZXkHmM+bNy/uu+++2GijjdpzaQAAAAAAGtR5550XERFXXnllrL322u2+Xrs2wpd3gPkqq6wSX/nKV+Kyyy5rz6UBAAAAACrLGeE14+9//3tcf/31scsuu1TkeplisdhQ48/lckm3AAAAAAAV0dzcnHQLNWWrsVcn3ULFTR91ZtItrBRDhgyJPffcM0466aSKXG+lnBGedss+IAqFQuTz+chms9HU1NQin8vlSnqoyCebb23NiuactnuQL2+NGac3X6kaZly9fFI9mXH5+TT2ZMbJ1zDjZPNp7MmMk69RC/lamnEae6qFfJIzrkaNRstDvbr44ovju9/9bnTq1CmOOuqo6NixY7uu16E9i++///74+OOPW3x+2223xYknntieSwMAAAAA0KBOOOGE+OSTT2Ls2LHRt2/f+PrXvx4DBgxY8k+p2vVG+KhRo+Luu++Obt26LfV5NpuNK664oj2XBgAAAACgQZ1++ukVvV67NsKLxWJkMpkWn3/00UctNscBAAAAAJKUaahfS6xthx56aEWvV/JG+H333Rf33Xffkj+PHj06unTpsuTPixYtildeeSVOOeWUynQIAAAAAEDDefPNNyOXy8Wnn3665LOFCxfGc889F2PHji3pWiVvhG+00Uax4447RkTEM888E9tss0306NFjyfcdO3aMM844I3baaadSLw0AAAAAAPHwww/HyJEjo1gsRrFYjPXXXz8+/vjjKBQKZf2gbMkb4TvuuOOSjfBrrrkmjjjiiNhmm21KLgwAAAAAAMtzzTXXxPe+9704/PDDY++9947/+q//ik6dOsUxxxwTgwYNKvl6HdrTTP/+/Zc6FgUAAAAAILWKdfhPnXrnnXdil112iR49esRWW20VU6ZMibXXXju+973vxe23317y9dq1EX777bfHZptt1p5LAAAAAADAUjbaaKP405/+FBER/fr1i0mTJkVExLrrrhsffvhhydcr+WiUZT399NPxwAMPxN/+9rcYO3ZsPPjgg7HmmmvG8ccf395LAwAAAADQgE4++eQYOXJk9OzZM/bff/8YOnRozJs3LyZPnlzWGeGZYrFY9gv0Dz/8cJx99tmx9dZbRz6fj/vuuy+ee+65GD9+fAwfPjy+853vlHvplSaXyyXdAgAAAABURDkbgo3sy5denXQLFffqD89MuoWV5rnnnosuXbpE796941e/+lXcc8890b179zj//POjV69eJV2rXW+EX3vttTFs2LA444wzonfv3hERccwxx8Qqq6wSN998cyo3wiNaPiAKhULk8/nIZrPR1NTUIp/L5Up6qMgnm29tzYrmnLZ7kC9vjRmnN1+pGmZcvXxSPZlx+fk09mTGydcw42TzaezJjJOvUQv5WppxGnuqhXySM65GjUbLU6I6PlO7HvXr12/Jfx4yZEgMGTKk7Gu164zwt956K3bbbbcWn/fq1Svee++99lwaAAAAAIAGMmfOnHjggQfipptuirvuuitmzZrVIvPOO+/ERRddVPK12/VG+Fe+8pV48MEHl+zMZzKZiIh46KGHlrwhDgAAAAAAKzJjxowYMmRIfPjhh9G5c+eYN29edOzYMa666qrYZ5994u9//3vceOON8cADD8Taa68dY8aMKen67doIHzFiRJxwwgnxwgsvRCaTiWuuuSbefvvtePXVV+Pmm29uz6UBAAAAAGgQV155Zay11lpx2223xZZbbhlz5syJH/3oR3HBBRfE7373u5g4cWKss846MXLkyDjiiCNKvn67NsJ33HHH+O1vfxs33XRTZDKZeOutt+LLX/5yXHHFFSUfVg4AAAAAsDJlnBGeWs8//3xccMEFseWWW0ZExJprrhnnnXde7LrrrvH888/HeeedF4cffnh07NixrOu3ayM8ImLzzTePsWPHtvcyAAAAAAA0qI8++ig23njjpT5be+21IyLiuuuua/dR3O3eCAcAAAAAgPaaOXNmdOjQocXnM2bMiEWLFi312TbbbFPStdu1ET5m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MuPka5hxsvk09mTGydeohXwtzTiNPdVCPskZV6NGo+UpjY3w2vH9738/pk+fHjvvvHPceeedccEFF8Tf//73+MUvfhHnnntuHHfccSVdL1UHeV9//fUxe/bs+P73v7/ks9tvvz2y2WwMGzYsevbsGeeff37cddddMX/+/OQaBQAAAABqT7EO/6lTf/7zn+O8886L0aNHR7du3WLzzTePESNGxNFHHx1PPfVUyddr10b41KlTK/Y2eC6XixtvvDG+8Y1vxMSJE2PmzJkREfHKK6/EHnvssSS33nrrRffu3ePVV1+tSF0AAAAAANLn34eZbLfddkuOQtlvv/3i2WefLflaJf9Y5uddc801K/z+tNNOa9N1isViXHDBBdGlS5fIZDIxbdq0GDduXHzve9+LOXPmxCabbLJUvlu3bjFr1qzo06dPWX0XCoWl/vzvM2Y+f9bMF60ptYZ8dfPLW/NFc07bPciXvsaM052vRA0zrm6+GjXMuLL5atQw48rmq1HDjJPNV6OGGVc2X40aac/X2oyrUaPe8knPuBo1Gim/vGN+oR7suuuucdlll8VPfvKT2HnnneP++++PfffdN55++umlfrOyrdp1Rvixxx675D8Xi8V455134q233oru3bvHVlttFbfddlubrvPss8/GMcccEzfccEN8/etfj4iI3//+9zF8+PDYcMMN4+yzz46BAwcuyR999NFx5JFHxkEHHVRyz7lcruRjVTp16lTSGvlk82nsSb6y+TT2JJ98DfnK5tPYk3zyNeQrm09jT/KVzaexJ/nka8hXNp/GnuSTr9Fo+X79+rU5S8Q259bhGeGX1+cZ4f/4xz/inHPOicGDB8c3vvGNOPjgg+Ptt9+OiIjhw4fHKaecUtL12rURvjwvvvhinHfeeXHmmWfGgAED2rTmwQcfjHPPPTdefPHFWGWVVSIiYtasWbHnnntGhw4d4rzzzltq033w4MFx6qmnxv77719yf7lcLnr16rXUZ/PmzYuZM2fGZpttFp07d26xZsaMGS3WrIh8svnW1qxozmm7B/ny1phxevOVqmHG1csn1ZMZl59PY09mnHwNM042n8aezDj5GrWQr6UZp7GnWsgnOeNq1Gi0vDfCS7PNyDrcCB9Xnxvhy5o7d248/fTT0b1799hhhx1KXt+uo1GWZ9ttt42f/vSnccYZZ7R5I3zDDTeMxYsXx6effrrktfY333wzIiIOOeSQeO6555ZshM+dOzdef/312HDDDcvusbUHROfOnVv9rtSHinyy+RWtaW3OabsH+fLXmHE685WsYcbVyVejhhlXNl+NGmZc2Xw1aphxsvlq1DDjyuarUaNW8rUy42rUqNd8UjOuRo1Gy0Mj6NKlS5v3m5enXT+W2ZoNNtgg3nnnnTbnt9tuu9h8881jzJgx8cYbb8TLL78cl156aey6665x7LHHxmOPPRaTJk2KiH+dS969e/dobm5eGa0DAAAAAJAC99xzTxx77LGx1157xWuvvRYXX3xxXHbZZbFw4cKSr9WuN8Lvv//+Fp/NmzcvHnroofjyl7/c9iZWXTVuueWWGDduXHzzm9+MBQsWxC677BIXX3xxfOlLX4rTTz89hg4dGmuttVYUCoWYMGFCdOiwUvbwAQAAAABI2K233hrjx4+PvffeO5599tlYuHBh9OvXLy666KJYffXV46yzzirpeu3aCP/pT3/a8oKrrhq9e/eOc845p6RrbbDBBvGTn/xkud+dfPLJMWjQoJg2bVr06dMn1ltvvXLaBQAAAAAaWKaiv5bIynT77bfHqFGjYsiQIdG7d++IiBg0aFAsXrw4rrjiipI3wiv+Y5lpl8vlkm4BAAAAACrC8cGlaf5B/f1YZm58ff5Y5vbbbx8333xzfPWrX43evXvH/fffH717944///nP8b3vfS+mTJlS0vVKeiP8qaeeiq5du8Z2221XUpG0WfYBUSgUIp/PRzabXe6PEeRyuZIeKvLJ5ltbs6I5p+0e5MtbY8bpzVeqhhlXL59UT2Zcfj6NPZlx8jXMONl8Gnsy4+Rr1EK+lmacxp5qIZ/kjKtRo9HyUK+23377uO2222L77bePiIhMJhOfffZZ/Nd//deSz0pR0kHb5557brz33ntL/jxgwICYPn16yUUBAAAAAKA15557bjzzzDOx1157RUTEmDFjYu+9945JkybFueeeW/L1SnojfM6cObHuuusu+fNbb70VCxYsKLkoAAAAAEDVNdQh0bWtd+/e8bvf/S5+9atfxauvvhoREbvvvnsMGTIkunfvXvL1StoI32abbeJXv/pVzJ8/Pzp0+NfL5K+88koUCoXl5vv3719yQwAAAAAAsNZaa8Vpp51WkWuVtBF+8cUXx3nnnRcnnHBCLFy4MDKZTIwePXq52UwmE/l8viJNAgAAAADQWD744IO455574m9/+1t06NAhNttsszjssMNi7bXXLvlaJW2Eb7nllvGb3/xmyZ979+4d99xzT2yzzTYlFwYAAAAAgOWZPHlyfOc734mIiF69ekWxWIyHHnoorr/++rjhhhtKPo2kpI1wAAAAAIBalXFGeM249NJLY5dddokrrrgiunTpEhH/+g3LESNGxMUXXxy//e1vS7peh/Y0c9ttt8Xmm2/enksAAAAAAMBSXnvttTj22GOXbIJHRKy55poxdOjQmDlzZsnXa9dG+I477hhNTU3tuQQAAAAAACxl2223jaeffrrF50899VTJx6JERGSKxWJD/YWAXC6XdAsAAAAAUBHNzc1Jt1BT+oy4OukWKu6lK89MuoWV4rLLLotf/epXsfPOO0ffvn2jWCzGpEmTYvLkyXH88cfHOuusExER3/72t9t0vYY8I3zZB0ShUIh8Ph/ZbHa5b7jncrmSHiryyeZbW7OiOaftHuTLW2PG6c1XqoYZVy+fVE9mXH4+jT2ZcfI1zDjZfBp7MuPka9RCvpZmnMaeaiGf5IyrUaPR8pSooV4Jrm2PPfZYrL/++jFz5syljkLp0aNHTJw4MSIiMpmMjXAAAAAAAGrTE088UdHr2QgHAAAAACAV3n777VhrrbWW+hsrv//97+OVV16JjTbaKPbbb79Yc801S76ujXAAAAAAABI1a9asOOecc2LSpEnxX//1X7HDDjtEsViM4cOHx+9///vo0qVLzJ8/PyZMmBC33XZbbL755iVdv8NK6hsAAAAAIF2KdfhPnTj//PPjnXfeiQkTJkQ2m42IiFtuuSUeffTROPHEE2PSpEnx5z//Ob7yla/EuHHjSr6+jXAAAAAAABI1adKkOP/882PfffeNzp07xyeffBI33XRT7LDDDnH22WdHJpOJNdZYI7797W/HlClTSr6+jXAAAAAAABK11lprxYIFC5b8+Ze//GXMmTMnhg8fvlRu7ty5seqqpZ/47YxwAAAAAAASdcghh8Sll14ab7/9dvzjH/+IX/ziF7H77rvHTjvtFBERn3zySUydOjV+/OMfL/msFDbCAQAAAABI1GmnnRYLFy6MG2+8MWbPnh177LFHjB07dsn3Q4YMialTp0Y2m41zzz235OvbCAcAAAAAGkIm6QZo1aqrrhojRoyIESNGxOLFi6NDh6VP9T7zzDNjzTXXjO222y5WWWWVkq+fKRaLdfTbol8sl8sl3QIAAAAAVERzc3PSLdSUbc+6OukWKu7Fq85MuoWa0JBvhC/7gCgUCpHP5yObzUZTU1OLfC6XK+mhIp9svrU1K5pz2u5Bvrw1ZpzefKVqmHH18kn1ZMbl59PYkxknX8OMk82nsSczTr5GLeRracZp7KkW8knOuBo1Gi0PtE2HL44AAAAAAEDtasg3wgEAAACABtRQh0Tzed4IBwAAAABoQB988EEMGzYs+vbtG4cddlhMnTq1Tes++uijOOOMM6Jv377Rp0+fOOWUU+Kf//xni9z//d//Rd++fePNN9+sdOslsxEOAAAAANBgisVinHbaafHhhx/G3XffHccee2wMGzYs5s6d+4Vrf/CDH8Snn34a9957b9x///0xc+bMuPzyy5fKzJkzJ84777w488wzY+ONN15Zt9FmNsIBAAAAABrM888/H5MnT45LLrkktthiizj00ENj8803j8cee2yF62bPnh1dunSJCRMmxOabbx5bbLFFHHLIITFlypSlcpdcckmsv/76MWTIkJV4F23njHAAAAAAoCFk6vCM8AEDBqzw+8cff3y5n7/yyiux4YYbxpZbbrnks759+8YLL7wQBx98cKvX69q1a1x99dVLfTZ9+vTo1avXUjXvv//+OPvss+O3v/1t7LzzzrH++uu35XZWGm+EAwAAAAA0mDlz5sQmm2yy1GfdunWLWbNmlXSdqVOnxsSJE+M73/lORER8+umncdFFF8X6668fc+fOjcmTJ8fgwYNj4sSJFeu9HN4IBwAAAACoUa298f1FVl111ejUqdNSn62++upRKBTafI158+bF2WefHYcddlj0798/IiIeeeSRePfdd+O3v/1tfOUrX4mIiM033zwuvPDC+MY3vhGrrprMlnSmWCzW4V8IaF0ul0u6BQAAAACoiObm5qRbqCnbff/qLw7VmBd+cmZZ6+6666749a9/Hffee++Sz375y1/G008/HTfeeOMXri8WizF8+PB466234o477liyqX7DDTfEXXfdtdQG/fPPPx9HHXVUPPXUU9GjR4+y+m2vhnwjfNkHRKFQiHw+H9lsNpqamlrkc7lcSQ8V+WTzra1Z0ZzTdg/y5a0x4/TmK1XDjKuXT6onMy4/n8aezDj5GmacbD6NPZlx8jVqIV9L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PDkefvjh2GKLLWKLLbaIhx56KB577LE4+OCDW103e/bs6NKlS1x22WVLNrcPOeSQuO+++yIiYsGCBfHKK6/E7rvvHl27dq3KvbSFH8sEAAAAABpCZnH9/VOuV155JTbccMPYcsstl3zWt2/feOGFF1a4rmvXrnH11Vcv9Yb39OnTo1evXhER8eKLL0ZExAknnBB9+vSJAw44IB5++OHyG60Qb4QDAAAAANSoAQMGrPD7xx9/fLmfz5kzJzbZZJOlPuvWrVvk8/mS6k+dOjUmTpwYt956a0T8a1N8yy23jPPOOy823XTTePDBB2PEiBGx5ZZbxle+8pWSrl1JNsIBAAAAABrMqquuGp06dVrqs9VXXz0KhUKbrzFv3rw4++yz47DDDov+/ftHRMRRRx0VRx111JLMCSecEE888UQ89NBDNsIBAAAAAChda298f5Hu3bvHBx98sNRnn3zySXTs2LFN64vFYowcOTJWX331GD169AqzPXr0iDfffLOsPivFGeEAAAAAAA1m++23j+nTp8fs2bOXfPbSSy/FBhts0Kb148ePjxdeeCGuu+66pd4sP++88+K3v/3tkj8vXLgwXnjhhTZfd2WxEQ4AAAAANIZisf7+KdNWW20VvXr1iquuuioWL14cL7/8cjz66KOx9957x+LFi2P27NmxaNGi5a69+eab484774wJEybEGmusEXPnzo25c+dGRERzc3P8+Mc/jqeeeipefPHFOOecc+LDDz+Mb33rW2X3WgmORgEAAAAAaEBjx46Nk08+OX73u9/FnDlz4uCDD44999wz3nzzzRgwYEDcf//9kc1mW6y76aabolAoxBFHHLHU59OmTYujjz463n///TjnnHPi008/jX79+sWdd94Zm222WZXuavkyxWI7/rVBDcrlckm3AAAAAAAV0dzcnHQLNeUbu12SdAsV9/s/nd+u9YVCISZNmhTdu3ePbbfdtkJdpU9DvhG+7AOiUChEPp+PbDYbTU1NLfK5XK6kh4p8svnW1qxozmm7B/ny1phxevOVqmHG1csn1ZMZl59PY09mnHwNM042n8aezDj5GrWQr6UZp7GnWsgnOeNq1Gi0PLRXU1NT7LXXXkm3sdI15EY4AAAAANCAGupsDD7Pj2UCAAAAAFDXbIQDAAAAAFDXbIQDAAAAAFDXnBEOAAAAADSETNEh4Y3KG+EA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TGzbto2ZM2cSHx9PYGCgZeV3eHg4mzdv5vDhw9jb21OtWrVHjpOWlgZkrHhu0aLFQ/scPXqUH374wfLzpEmTLMn7PHnyULFiRTZs2ICrqyszZ87k559/5ujRo8TExODv70/79u2JjIzE19eX+fPn/2E8f8TFxYUxY8YwYsQIli1bhpubGwaDgXnz5vHll1+yd+9e6taty/Dhwx9YGf9nfPHFF0yYMIF69erleAyTyZTl52nTpj2yb0xMDGfPnuXGjRuW0ijJycl88MEHvPPOOzl+4KiIiIiIiK0ymH+/9EREREREcsxkMj2Q0Pwto9GY7drjf9bhw4d5++23efnllxk7diylSpWybEtOTqZZs2aULl2ad999Fy8vr0eOM3XqVE6ePMnChQuJiIjA0dHxD8tupKamUrRoUQoXLsyePXtYs2YNe/bsoW3btnzwwQcULFiQS5cuMWHCBA4cOMDw4cPx9fXl7t279OvXj+PHjzNp0iTatWuX4/c+Y8YMbty4weTJk3M8xt8tIiKCNm3aZOthmRcuXKB169aUL1+euXPn4uHhwezZs4mKimLatGkqiyIiIiIi8jtKhIuIiIg8RaNHj2bNmjWP3O7r68uHH374t8Vz4cIFnn322T81xv/93/9x9uxZli9f/sT7hoeHs3HjRt544w08PDws7ampqcyZM4dOnTpRunRpS3t6ejrLly+nS5cuODg45Dhms9nM7du3KVCgQI7H+KdLSkoiX758WX62s7MjT548uRiViIiIiMg/kxLhIiIiIk/R1atXuXXr1iO3u7m54e7u/jdGJCIiIiIiIkqEi4iIiIiIiIiIiIhN+3sKVIqIiIiIiIiIiIiI5BIlwkVERERERERERETEpikRLiIiIiIiIiIiIiI2TYlwEREREREREREREbFpSoSLiIiIiIiIiIiIiE1TIlxEREREREREREREbJoS4SIiIiIiIiIiIiJi05QIFxERERERERERERGb9v++1TYwOm7ibAAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from scipy.stats import spearmanr\n", "from tqdm import tqdm # 用于显示进度条 (可选)\n", "\n", "# 设置 Matplotlib/Seaborn 样式 (可选)\n", "sns.set_theme(style=\"whitegrid\")\n", "plt.rcParams['font.sans-serif'] = ['SimHei'] # 或者其他支持中文的字体\n", "plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题\n", "\n", "def analyze_score_performance_2d(score_df: pd.DataFrame,\n", " score_col: str = 'score',\n", " label_col: str = 'label',\n", " condition1_col: str = 'circ_mv',\n", " condition2_col: str = 'future_return',\n", " n_bins: int = 100,\n", " min_samples_per_bin: int = 30): # 每个格子最少样本数\n", " \"\"\"\n", " 分析 score 在两个条件下 (如市值、未来收益) 的二维分箱表现。\n", "\n", " Args:\n", " score_df (pd.DataFrame): 包含分数、标签和条件列的 DataFrame。\n", " score_col (str): 预测分数所在的列名。\n", " label_col (str): 目标标签所在的列名 (应为数值或可排序类别)。\n", " condition1_col (str): 第一个条件列名 (例如 'circ_mv')。\n", " condition2_col (str): 第二个条件列名 (例如 'future_return')。\n", " n_bins (int): 每个条件划分的箱数 (分位数数量)。\n", " min_samples_per_bin (int): 计算指标所需的最小样本数,小于此数目的格子结果将被屏蔽。\n", "\n", " Returns:\n", " tuple: 包含 (performance_pivot, count_pivot, fig)\n", " performance_pivot: 以二维分箱为索引/列的 Spearman 相关系数矩阵。\n", " count_pivot: 每个二维分箱的样本数量矩阵。\n", " fig: 生成的热力图 Matplotlib Figure 对象。\n", " \"\"\"\n", " print(f\"开始分析 '{score_col}' 在 '{condition1_col}' 和 '{condition2_col}' 下的表现...\")\n", "\n", " required_cols = [score_col, label_col, condition1_col, condition2_col]\n", " if not all(col in score_df.columns for col in required_cols):\n", " missing = [col for col in required_cols if col not in score_df.columns]\n", " raise ValueError(f\"输入 DataFrame 缺少必需列: {missing}\")\n", "\n", " # --- 1. 数据准备和清洗 ---\n", " print(\"准备数据,处理 NaN 值...\")\n", " # 只保留需要的列,并移除包含 NaN 的行,避免影响分箱和计算\n", " analysis_df = score_df[required_cols].dropna().copy()\n", " n_original = len(score_df)\n", " n_after_drop = len(analysis_df)\n", " print(f\"原始数据 {n_original} 行,移除 NaN 后剩余 {n_after_drop} 行用于分析。\")\n", "\n", " if n_after_drop < min_samples_per_bin * n_bins: # 检查数据量是否过少\n", " print(f\"警告: 清理 NaN 后数据量 ({n_after_drop}) 可能不足以支持 {n_bins}x{n_bins} 的精细分箱分析。\")\n", " if n_after_drop < min_samples_per_bin:\n", " print(\"错误: 有效数据过少,无法进行分析。\")\n", " return None, None, None\n", "\n", " # --- 2. 二维分箱 ---\n", " print(f\"对 '{condition1_col}' 和 '{condition2_col}' 进行 {n_bins} 分位数分箱...\")\n", " bin1_col = f'{condition1_col}_bin'\n", " bin2_col = f'{condition2_col}_bin'\n", "\n", " try:\n", " # 使用 qcut 进行分位数分箱,labels=False 返回 0 到 n_bins-1 的整数标签\n", " # duplicates='drop' 会丢弃导致边界不唯一的重复值所在的箱子,可能导致某些箱号缺失\n", " # 对于可视化,这通常可以接受,但如果需要严格的等分,需先 rank\n", " analysis_df[bin1_col] = pd.qcut(analysis_df[condition1_col], q=n_bins, labels=False, duplicates='drop')\n", " analysis_df[bin2_col] = pd.qcut(analysis_df[condition2_col], q=n_bins, labels=False, duplicates='drop')\n", " except Exception as e:\n", " print(f\"错误: 分箱失败,请检查数据分布或减少 n_bins。错误信息: {e}\")\n", " # 可以尝试先 rank 再 qcut\n", " # analysis_df[bin1_col] = pd.qcut(analysis_df[condition1_col].rank(method='first'), q=n_bins, labels=False, duplicates='raise')\n", " # analysis_df[bin2_col] = pd.qcut(analysis_df[condition2_col].rank(method='first'), q=n_bins, labels=False, duplicates='raise')\n", " return None, None, None\n", "\n", " # --- 3. 分组计算表现指标 (Spearman Rank IC) ---\n", " print(\"按二维分箱分组计算 Spearman Rank IC...\")\n", "\n", " def safe_spearmanr(x, y):\n", " \"\"\"安全计算 Spearman 相关性,处理数据量过少的情况\"\"\"\n", " if len(x) < max(2, min_samples_per_bin): # 要求至少有 min_samples_per_bin 个点才计算\n", " return np.nan\n", " corr, p_value = spearmanr(x, y)\n", " return corr if not np.isnan(corr) else np.nan # 确保返回 NaN 而不是 None 或其他\n", "\n", " # 按两个分箱列分组\n", " grouped = analysis_df.groupby([bin1_col, bin2_col])\n", "\n", " # 计算每个格子的 Spearman 相关系数\n", " # apply 可能较慢,但计算相关性通常需要 apply\n", " performance_series = grouped.apply(lambda sub: safe_spearmanr(sub[score_col], sub[label_col]))\n", "\n", " # 计算每个格子的样本数量\n", " count_series = grouped.size()\n", "\n", " # --- 4. 结果整理成 Pivot Table (用于绘图) ---\n", " print(\"整理结果用于绘图...\")\n", " try:\n", " # 将 performance_series 转换成二维矩阵\n", " # index 为 condition1_bin, columns 为 condition2_bin\n", " performance_pivot = performance_series.unstack(level=0) # level=0 对应第一个 groupby key (bin1_col)\n", " count_pivot = count_series.unstack(level=0)\n", "\n", " # 可选:按列和索引排序,确保顺序正确\n", " performance_pivot = performance_pivot.sort_index(axis=0).sort_index(axis=1)\n", " count_pivot = count_pivot.sort_index(axis=0).sort_index(axis=1)\n", " \n", " print(performance_pivot)\n", "\n", " except Exception as e:\n", " print(f\"错误: 无法将结果转换为二维矩阵,可能因为分箱不均匀或数据问题: {e}\")\n", " return None, None, None\n", "\n", " # --- 5. 可视化:绘制热力图 ---\n", " print(\"生成热力图...\")\n", " fig, ax = plt.subplots(figsize=(16, 12)) # 调整图像大小\n", "\n", " # 使用 count_pivot 创建一个 mask,屏蔽掉样本量过小的格子\n", " mask = count_pivot < min_samples_per_bin\n", "\n", " # 绘制热力图\n", " sns.heatmap(performance_pivot,\n", " annot=False, # 100x100 个格子加注释会太密集\n", " fmt=\".2f\",\n", " cmap=\"viridis\", # 选择颜色映射, 'viridis', 'coolwarm', 'RdYlGn' 等都不错\n", " linewidths=.5,\n", " linecolor='lightgray',\n", " # mask=mask, # 应用 mask\n", " ax=ax,\n", " cbar_kws={'label': f'Spearman Rank IC ({score_col} vs {label_col})'}) # 颜色条标签\n", "\n", " # 设置标题和轴标签\n", " ax.set_title(f'{score_col} 表现分析 (Rank IC vs {label_col})\\n基于 {condition1_col} 和 {condition2_col} {n_bins}x{n_bins} 分箱', fontsize=16)\n", " ax.set_xlabel(f'{condition1_col} 分位数 (0 -> 高)', fontsize=12)\n", " ax.set_ylabel(f'{condition2_col} 分位数 (0 -> 高)', fontsize=12)\n", "\n", " # 可选:调整刻度标签,避免显示所有 100 个刻度\n", " if n_bins > 20:\n", " tick_interval = n_bins // 10 # 大约显示 10 个刻度\n", " ax.set_xticks(np.arange(0, n_bins, tick_interval) + 0.5)\n", " ax.set_yticks(np.arange(0, n_bins, tick_interval) + 0.5)\n", " ax.set_xticklabels(np.arange(0, n_bins, tick_interval))\n", " ax.set_yticklabels(np.arange(0, n_bins, tick_interval))\n", "\n", " plt.xticks(rotation=45, ha='right')\n", " plt.yticks(rotation=0)\n", " plt.tight_layout() # 调整布局\n", "\n", " print(\"分析完成。\")\n", " return performance_pivot, count_pivot, fig\n", "\n", "# --- 如何使用 ---\n", "# 假设你的包含预测结果和所需列的 DataFrame 是 final_predictions_df\n", "# 确保它包含 'score', 'label', 'circ_mv', 'future_return'\n", "\n", "# # 示例调用 (你需要有实际的 score_df)\n", "try:\n", " # 确保数据类型正确\n", " cols_to_numeric = ['score', 'label', 'circ_mv', 'future_return']\n", " for col in cols_to_numeric:\n", " if col in score_df.columns:\n", " score_df[col] = pd.to_numeric(score_df[col], errors='coerce')\n", "\n", " # 调用分析函数\n", " performance_matrix, count_matrix, heatmap_figure = analyze_score_performance_2d(\n", " score_df,\n", " n_bins=100, # 你要求的100分箱\n", " min_samples_per_bin=50 # 每个格子至少需要50个样本才显示IC,可以调整\n", " )\n", "\n", " # 显示图像\n", " if heatmap_figure:\n", " plt.show()\n", "\n", " # 可以查看具体的 performance_matrix 和 count_matrix\n", " # print(\"\\nPerformance Matrix (Spearman IC):\")\n", " # print(performance_matrix)\n", " # print(\"\\nCount Matrix:\")\n", " # print(count_matrix)\n", "\n", "except ValueError as ve:\n", " print(f\"数据错误: {ve}\")\n", "except Exception as e:\n", " print(f\"发生未知错误: {e}\")" ] } ], "metadata": { "kernelspec": { "display_name": "new_trader", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.11" } }, "nbformat": 4, "nbformat_minor": 5 }