基于回归算法预测股市价格(Part 2)

Hakuna 2025-02-26 2025-02-27 4945 字 25 minutes Regression

由于内容偏多,我将《基于回归算法预测股市价格》实验项目拆分成了多篇文章。这是该系列的第 篇文章!这里主要介绍数据拆分与特征工程等内容。

相关文章链接


Spliting the Data into Training, Validation and Test sets

# split the data into train, validation and test sets

total_len = len(train_data)
train_end = int(total_len * 0.8)  # first 80% for training

X_train, y_train = train_data.iloc[:train_end], train_data['Close'][:train_end]
X_test, y_test = train_data.iloc[train_end:], train_data['Close'][train_end:]

X_train.shape, y_train.shape, X_test.shape, y_test.shape    # Display the shapes of the train and test sets
((10684, 6), (10684,), (2672, 6), (2672,))

🤓 Note

此时,X_trainX_test 包含目标值 Close。主要是由于特征工程时部分新特征需要基于目标值 Close进行计算。

Feature Engineering

在使用回归模型对股价进行预测时,特征工程(Feature Engineering) 对模型性能的提升尤为关键。除了直接使用原始特征(如 OpenHighLowCloseVolume 等)外,还可以通过以下方式对特征进行深度挖掘和转化,从而帮助模型更好地学习价格走势的规律。

😂 Warning

预测时需考虑信息可用性
如果咱们的目标是预测“当天的收盘价”,在交易尚未结束时,当日最高价 (High)、最低价 (Low) 和成交量 (Volume) 尚不确定,因此这些数据在真实交易场景中并不可用。如果在模型训练或推理阶段仍将这些当日尚未完成的指标纳入特征,就会造成“数据泄漏”(Data Leakage),让模型使用到预测时不可能提前得知的信息。 比如,

这意味着在实际预测中,如果需要在开盘时或盘中实时预测收盘价,就无法利用当天尚未确定的最高价、最低价和成交量。因此,一定要根据实际预测的时点,只使用在该时点可获取的特征,才能避免误导性的高精度结果并确保模型落地可行。

针对该数据集,以下是一些常见的特征工程思路,可结合实际场景灵活应用:

  1. 时间特征提取
    • 将交易日期 (Date) 转化为多种时间特征,如:星期几、月份、季度等,以捕捉周期性规律。比如说,星期一或周五可能因市场情绪不同而对股价产生影响。
    • 如果需要更细粒度的时间序列分析,可在后续步骤考虑加入假日或周末信息(如长周末、节假日等)对价格的影响,比如可以考虑美国股市在感恩节或劳动节前后的交易活跃度变化情况。
特征名称 符号 公式/定义 可用性
星期几(Day of Week) $\text{DoW}$ Date → [Mon, Tue, …, Fri] ✔️
月份(Month) $\text{Month}$ Date → [1-12] ✔️
季度(Quarter) $\text{Quarter}$ Date → [Q1-Q4] ✔️
是否为月末(Month End Flag) $\text{MonthEnd}$ 二元标记 (1 if end of month, 0 otherwise) ✔️
是否为季末(Quarter End Flag) $\text{QuarterEnd}$ 二元标记 (1 if end of quarter, 0 otherwise) ✔️
  1. 价格衍生特征
    • 日收益率:用来衡量每日价格的涨跌幅度,这是捕捉短期趋势的基础指标。
    • 移动平均线:在一定时间窗口内(如 5 天、 7 天、14 天、30 天)计算 Close 的平均值,以捕捉中短期趋势。还可以扩展到指数移动平均(Exponential Moving Average,EMA),对近期数据赋予更高权重。
    • 移动标准差:在相同时间窗口下计算价格波动的标准差,衡量近期市场的波动程度。该指标主要反映的是价格的不确定性。
    • 最高-最低价差:用于度量当天股价的波动区间。如果要预测当天收盘价,则当天的 HighLow只能在收盘后才得知,需根据实际预测场景决定是否可用。
    • 滞后特征:可以考虑加入前几天的收盘价,为模型提供历史上下文。
    • 价格跳跃:该指标可以反映开盘时的市场预期变化。
    • 历史波动性:基于更长时间窗口(如 20 天或 50 天)的收益率标准差,捕捉长期趋势中的风险特征。
特征 简写 公式/说明 可用性(开盘时预测场景)
前日收盘价 (Previous Close) $\text{Close}_{t-1}$ $\text{Close}_{t-1}$ ✔️(直接滞后值)
日收益率 (Daily Return) $\text{Return}_{t-1}$ $(Close_{t-1} - Close_{t-2}) / Close_{t-2}$ ✔️
移动平均线 (Moving Average) $\text{MA}(n)$ $MA(n) = Avg(Close_{t-n, t-1})$ ✔️(n=5,7,14 等)
指数移动平均 (Exponential MA) $\text{EMA}(n)$ 加权平均,近期权重更高 ✔️
开盘价-前日收盘价差 (Opening Gap) $\text{Gap}_t$ $Open_t - Close_{t-1}$ ✔️(开盘时已知)
历史波动率 (Historical Volatility) $\text{HV}(n)$ $Std(Return_{t-n, t-1})$ ✔️
价格范围 (Historical Price Range) $\text{Range}_{t-1}$ $ High*{t-1} - Low*{t-1}$ ✔️(使用前日数据)
禁用特征:当日 High/Low/Close,当日 Range,当日 Return。
  1. 成交量衍生特征

    • 成交量移动平均:同样可以对 Volume 进行移动平均或移动标准差处理,以此反映交易量的趋势和波动,帮助判断市场参与度。
    • 成交量变化率:用于判断交易热度的上升或下降。同理,当日 Volume 在盘中并非最终值,也需要注意信息的可用性。
    • 成交量加权平均价格:一天内基于成交量加权的平均价格,反映市场真实的交易重心。
    • 成交量与价格交互:可能捕捉交易活跃度与价格波动的联合效应。
指标名称 简称 公式/说明 可用
成交量移动平均 (Volume MA) $\text{Vol_MA}(n)$ $Avg(Volume_{(t-n,t-1)}) $ ✔️
成交量变化率 (Volume Change) $\text{Vol_Chg}$ $(Volume_{(t-1)} - Volume_{(t-2)}) / Volume_{(t-2)}$ ✔️
量价交互 (Volume-Price Interaction) $\text{VPT}$ $ Volume_{(t-1)} \times Return_{(t-1)}$ ✔️
当日 Volume 相关指标。
  1. 技术指标特征

    • 可基于开盘价、收盘价、成交量等信息,计算技术分析常用指标(如 RSI、ATR、MACD、KDJ、布林带等)。这些指标从不同角度衡量价格动能、趋势强度和市场情绪:
      • RSI(相对强弱指数):衡量超买或超卖状态。
      • 平均真实波幅 (ATR):衡量价格波动的真实幅度,常用于评估市场风险。
      • MACD:短期与长期趋势的差异,捕捉买卖信号。
      • 布林带:基于移动平均和标准差,反映价格的上下限。
      • KDJ:结合动量和超买超卖信号。
    • 若技术指标包含当日尚未确定的数据,需注意时点的合理性,确保预测时只使用已知信息。
    • 傅里叶变换或小波变换:从时间序列中提取周期性模式,适合挖掘隐藏的长期规律。
指标名称 简称 公式/说明 可用性
相对强弱指数 (Relative Strength Index) $\text{RSI}(n)$ $100 - 100/(1 + AvgGain/AvgLoss) (n=14)$ ✔️ (滞后计算)
移动平均线收敛-发散 (MACD) $\text{MACD}$ $EMA(12) - EMA(26)$ ✔️
布林带 (Bollinger Bands) $\text{BB}(n,\sigma)$ $MA(n) ± \sigma*Std(Close)$ ✔️
平均真实波幅 (Average True Range) $\text{ATR}(n)$ $ Avg(TR)$ , $TR=\max(High-Low, High-Close_{(p)}$ , $Low-Close_{(p)}$ ) ✔️
所有指标需使用前 t-1 的收盘价计算。
  1. 特征交互与组合

    • 特征组合能帮助模型捕捉多变量间的复杂关系。将现有特征进行组合,挖掘潜在关联。
    • 市场微观结构特征:如有高频数据,可加入买卖价差(Bid-Ask Spread)或订单簿数据,反映市场流动性和交易压力。
    • 外部交互:将 IBM 股价特征与大盘指数(如标普 500 日收益率)组合,捕捉系统性风险的影响。
指标名称(英文名称) 简称 公式说明 可用性
价量趋势 (Price-Volume Trend) $\text{PVT}$ $MA(Close, 5) * Vol\_MA(5)$ ✔️
波动率调整收益率 (Volatility-Adjusted Return) $\text{VAR}$ $Return_{t-1} / HV(10)$ ✔️
缺口方向 (Gap Direction) $\text{GapDir}$ 符号(+1/-1)与绝对值组合 ✔️
  1. 数据清洗与归一化
    • 对缺失值、异常值进行处理,确保数据质量。
    • 对数值特征进行标准化或归一化处理(如 Min-Max 归一化、Z-score 标准化等),在一定程度上可以加速模型收敛,并减少极端值的影响。
  2. 特征相关性检查
    • 在特征工程后使用皮尔逊相关系数(保留 $|r|>0.1$ 的特征)或方差膨胀因子(Variance Inflation Factor,VIF)分析 ($VIF<5$),避免多重共线性问题。

除了上述内容,以下是一些未纳入但可能对回归分析有潜在价值的特征,当有相关数据时,可以纳入:

  1. 市场宏观特征:
    • 大盘指数(如标普 500 或纳斯达克)的日收益率或波动性,IBM 股价可能受整体市场趋势驱动。
    • 利率数据(如美联储利率或 10 年期国债收益率),影响股票估值。
    • 美元指数(DXY),对 IBM 这样的跨国公司可能有间接作用。
  2. 行业相关特征:
    • 同行公司(如微软、谷歌)的股价趋势或收益率,可能与 IBM 存在相关性。
    • 行业新闻情感:若能获取科技行业新闻或 X 平台数据,可提取情感得分,反映市场信心。
  3. 外部经济指标:
    • CPI(消费者物价指数)、PPI(生产者物价指数)或失业率等,衡量宏观经济环境。
    • 公司基本面:如每股收益(EPS)、市盈率(P/E Ratio)或股息收益率(IBM 有股息历史)。
  4. 社交媒体数据:
    • 若能接入社交媒体平台,可以结合文本分析技术分析关于对应公司的情感倾向,可能捕捉投资者情绪波动。

通过合理地进行特征工程,能为回归模型提供更丰富、更具解释力的输入数据。尤其在金融时间序列预测中,适当的衍生特征往往能有效提升模型的预测准确度。但务必结合真实交易环境下的可用信息避免数据泄漏,确保模型具有实际可操作性。此外,在具体构建特征时,也许综合考虑数据可得性、预测场景、计算成本等因素。

根据前面的特征描述,定义 FeatureGenerator 特征生成器,并返回包含原始数据的 pandas.DataFrame

import logging
import warnings
from typing import Optional, Tuple

import numpy as np
import pandas as pd
from statsmodels.stats.outliers_influence import variance_inflation_factor

from balabs.utils.logger import setup_logging


class FeatureGenerator:
    def __init__(
        self,
        ma_windows: list = [5, 7, 14],
        ema_windows: list = [12, 26],
        rsi_window: int = 14,
        bb_window: int = 20,
        hv_window: int = 10,
        atr_window: int = 14,
        log_level=logging.INFO,
    ):

        self.ma_windows = ma_windows
        self.ema_windows = ema_windows
        self.rsi_window = rsi_window
        self.bb_window = bb_window
        self.hv_window = hv_window
        self.atr_window = atr_window

        self.logger = setup_logging(
            log_name=__name__, log_file=f"{__name__}.log", log_level=log_level
        )

    def fit(self, X: pd.DataFrame, y: pd.Series) -> "FeatureGenerator":
        self.columns = X.columns
        self.logger.info(f"Fitted FeatureGenerator with {len(self.columns)} columns.")
        return self

    def transform(
        self, X: pd.DataFrame, y: Optional[pd.Series] = None
    ) -> Tuple[pd.DataFrame, Optional[pd.Series]]:
        X_transformed = X.copy()

        self.logger.info("Starting feature transformation.")

        X_transformed = self._add_time_features(X_transformed)
        X_transformed = self._add_price_features(X_transformed)
        X_transformed = self._add_volume_features(X_transformed)
        X_transformed = self._add_technical_indicators(X_transformed)
        X_transformed = self._add_feature_interactions(X_transformed)

        if y is not None:
            X_transformed, y_aligned = self._handle_missing_values(X_transformed, y)
            if len(X_transformed) != len(y_aligned):
                self.logger.error(
                    "Data alignment failed; X and y have different lengths."
                )
                raise ValueError(
                    "Data alignment failed; X and y have different lengths."
                )
            self.logger.info(
                f"Feature transformation completed with target alignment: The shape of X is {X_transformed.shape}, and the shape of y is {y_aligned.shape}."
            )
            return X_transformed, y_aligned
        return X_transformed, None

    def _add_time_features(self, df: pd.DataFrame) -> pd.DataFrame:
        original_columns = df.columns

        df["DoW"] = df.index.dayofweek
        df["Month"] = df.index.month
        df["Quarter"] = df.index.quarter
        df["MonthEnd"] = df.index.is_month_end.astype(int)
        df["QuarterEnd"] = df.index.is_quarter_end.astype(int)

        self.logger.debug(
            f"Added time features: {list(df.columns.difference(original_columns))}."
        )
        return df

    def _add_price_features(self, df: pd.DataFrame) -> pd.DataFrame:
        original_columns = df.columns

        df["Close_t-1"] = df["Close"].shift(1)
        denominator = df["Close"].shift(1)
        df["Return_t-1"] = np.where(
            denominator == 0, 0, (df["Close"] - denominator) / denominator
        )
        for window in self.ma_windows:
            df[f"MA_{window}"] = df["Close"].rolling(window=window).mean()
        for window in self.ema_windows:
            df[f"EMA_{window}"] = df["Close"].ewm(span=window, adjust=False).mean()
        df["Gap"] = df["Open"] - df["Close"].shift(1)
        df[f"HV_{self.hv_window}"] = (
            df["Return_t-1"].rolling(window=self.hv_window).std()
        )
        df["Range_t-1"] = (df["High"] - df["Low"]).shift(1)

        self.logger.debug(
            f"Added time features: {list(df.columns.difference(original_columns))}."
        )
        return df

    def _add_volume_features(self, df: pd.DataFrame) -> pd.DataFrame:
        original_columns = df.columns

        for window in self.ma_windows:
            df[f"Vol_MA_{window}"] = df["Volume"].rolling(window=window).mean()
        denominator = df["Volume"].shift(1)
        df["Vol_Chg"] = np.where(
            denominator == 0, 0, (df["Volume"] - denominator) / denominator
        )
        df["VPT"] = df["Volume"].shift(1) * df["Return_t-1"]

        self.logger.debug(
            f"Added time features: {list(df.columns.difference(original_columns))}."
        )
        return df

    def _add_technical_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
        original_columns = df.columns

        delta = df["Close"].diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=self.rsi_window).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=self.rsi_window).mean()
        rs = gain / loss
        df["RSI"] = 100 - (100 / (1 + rs))

        ema_short = df["Close"].ewm(span=12, adjust=False).mean()
        ema_long = df["Close"].ewm(span=26, adjust=False).mean()
        df["MACD"] = ema_short - ema_long

        rolling_mean = df["Close"].rolling(window=self.bb_window).mean()
        rolling_std = df["Close"].rolling(window=self.bb_window).std()
        df["BB_upper"] = rolling_mean + (2 * rolling_std)
        df["BB_lower"] = rolling_mean - (2 * rolling_std)

        high_low = df["High"] - df["Low"]
        high_close = np.abs(df["High"] - df["Close"].shift(1))
        low_close = np.abs(df["Low"] - df["Close"].shift(1))
        tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
        df[f"ATR_{self.atr_window}"] = tr.rolling(window=self.atr_window).mean()

        self.logger.debug(
            f"Added time features: {list(df.columns.difference(original_columns))}."
        )
        return df

    def _add_feature_interactions(self, df: pd.DataFrame) -> pd.DataFrame:
        original_columns = df.columns

        df["PVT"] = df["MA_5"] * df["Vol_MA_5"]
        hv = df[f"HV_{self.hv_window}"]
        df["VAR"] = np.where(hv == 0, 0, df["Return_t-1"] / hv)
        df["GapDir"] = np.sign(df["Gap"])
        df["GapAbs"] = np.abs(df["Gap"])

        self.logger.debug(
            f"Added time features: {list(df.columns.difference(original_columns))}."
        )
        return df

    def _handle_missing_values(
        self, X: pd.DataFrame, y: pd.Series
    ) -> Tuple[pd.DataFrame, pd.Series]:
        mask = X.notna().all(axis=1)
        X_cleaned = X[mask]
        y_aligned = y[mask]
        removed_rows = len(X) - len(X_cleaned)
        if removed_rows > 0:
            self.logger.warning(f"Removed {removed_rows} rows due to NaN values.")
        else:
            self.logger.info("No rows removed; data is clean.")
        return X_cleaned, y_aligned

    def get_feature_correlation(
        self,
        X: pd.DataFrame,
        y: Optional[pd.Series] = None,
        threshold: float = 0.1,
        keep_all: bool = False,
    ) -> pd.DataFrame:

        if y is not None:
            corr_with_target = X.corrwith(y, method="pearson")
            corr_df = pd.DataFrame(corr_with_target, columns=["Correlation"])
            corr_df.index.name = "Feature"
            if not keep_all:
                corr_df = corr_df[abs(corr_df["Correlation"]) > threshold]
            result = corr_df.sort_values(by="Correlation", key=abs, ascending=False)
            self.logger.info(
                f"Computed correlation with target; {len(result)} features above threshold {threshold}."
            )
            self.logger.info(
                f"Feature Correlation Matrix (|r| > {threshold}):\n{result.to_string()}"
            )
            return result
        else:
            corr_matrix = X.corr(method="pearson")
            if not keep_all:
                result = corr_matrix[abs(corr_matrix) > threshold]
            else:
                result = corr_matrix
            self.logger.info("Computed feature-to-feature correlation matrix.")
            self.logger.info(
                f"Feature Correlation Matrix (|r| > {threshold}):\n{result}"
            )
            return result

    def get_vif(
        self, X: pd.DataFrame, threshold: float = 5.0, keep_all: bool = False
    ) -> pd.DataFrame:
        X_numeric = X.select_dtypes(include=[np.number])

        nan_count = X_numeric.isna().sum().sum()
        inf_count = np.isinf(X_numeric).sum().sum()
        self.logger.info(f"NaN count in data: {nan_count}")
        self.logger.info(f"Inf count in data: {inf_count}")

        if nan_count > 0 or inf_count > 0:
            self.logger.warning("Cleaning NaN and inf values...")
            X_cleaned = X_numeric.replace([np.inf, -np.inf], np.nan)
            X_cleaned = X_cleaned.dropna()
            self.logger.warning(
                f"Rows removed due to NaN/inf: {len(X_numeric) - len(X_cleaned)}"
            )
        else:
            self.logger.info("No NaN or inf values found, using original data.")
            X_cleaned = X_numeric.copy()

        if X_cleaned.shape[0] < 2 or X_cleaned.shape[1] < 1:
            self.logger.error("Insufficient data after cleaning for VIF calculation.")
            raise ValueError("Insufficient data after cleaning for VIF calculation")

        variances = X_cleaned.var()
        constant_cols = variances[variances == 0].index
        if len(constant_cols) > 0:
            self.logger.warning(f"Removing constant columns: {list(constant_cols)}")
            X_cleaned = X_cleaned.drop(columns=constant_cols)

        vif_data = pd.DataFrame()
        vif_data["Feature"] = X_cleaned.columns
        vif_values = []

        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=RuntimeWarning)
            for i in range(X_cleaned.shape[1]):
                vif = variance_inflation_factor(X_cleaned.values, i)
                if np.isinf(vif):
                    self.logger.warning(
                        f"Feature '{X_cleaned.columns[i]}' has perfect multicollinearity (VIF = inf)"
                    )
                vif_values.append(vif)

        vif_data["VIF"] = vif_values
        vif_data = vif_data.sort_values(by="VIF", ascending=True)

        if not keep_all:
            vif_data = vif_data[vif_data["VIF"] <= threshold]

        removed_features = vif_data[vif_data["VIF"] > threshold]["Feature"].tolist()
        if removed_features:
            self.logger.info(
                f"Features with VIF > {threshold} (removed): {removed_features}"
            )
        self.logger.info(
            f"Computed VIF; {len(vif_data)} features with VIF <= {threshold}."
        )

        self.logger.info(
            f"VIF Data (VIF <= {threshold}):\n{vif_data.to_string(index=False)}"
        )

        return vif_data

    def get_high_vif_high_corr_features(
        self,
        X: pd.DataFrame,
        y: pd.Series,
        vif_threshold: float = 10.0,
        corr_threshold: float = 0.1,
    ) -> pd.DataFrame:

        vif_df = self.get_vif(X, threshold=vif_threshold, keep_all=True)
        self.logger.info("Retrieved VIF data for all features.")

        corr_df = self.get_feature_correlation(
            X, y, threshold=corr_threshold, keep_all=True
        )
        self.logger.info("Retrieved correlation data with target for all features.")

        combined_df = vif_df.merge(corr_df, on="Feature", how="inner")
        self.logger.debug(
            f"Combined VIF and correlation data:\n{combined_df.to_string()}"
        )

        high_vif_high_corr = combined_df[
            (combined_df["VIF"] > vif_threshold)
            & (combined_df["Correlation"].abs() > corr_threshold)
        ]

        if high_vif_high_corr.empty:
            self.logger.warning("No features found with high VIF and high correlation.")
        else:
            self.logger.info(
                f"Found {len(high_vif_high_corr)} features with VIF > {vif_threshold} "
                f"and |Correlation| > {corr_threshold}:\n{high_vif_high_corr.to_string()}"
            )

        return high_vif_high_corr

🤓 Note

  1. 更为详细的代码,请参考 feature_engineering.py
  2. 滚动计算可能会在最初几行产生 NaN,因此通过 df.dropna() 删除。注意,df.dropna() 默认删除包含缺失值的行,为了保持训练数据集,验证数据集以及测试集中的维度一致,需要处理对应的训练、验证以及测试的目标值。

接着,我们可以使用以上生成器对原始数据进行处理,并构建新特征

from balabs.regression.processing.feature_engineering import FeatureGenerator
import logging

feature_gen = FeatureGenerator(log_level=logging.DEBUG)
feature_gen.fit(X_train, y_train)
X_train_transformed, y_train_aligned = feature_gen.transform(X_train, y_train)
X_test_transformed, y_test_aligned = feature_gen.transform(X_test, y_test)

经过以上处理后,X_train_transformed共有 35 个特征(包括原始特征)。进一步可以使用 FeatureGenerator 中定义的 get_feature_correlationget_vif 方法计算对应的相关系数和方差膨胀因子。通过计算,可以发现 X_train_transformed 只有 23 特征的相关系数大于阈值 0.1。当将方差膨胀因子的阈值设置为5时,不存在多重共线性的特征仅有 7 个。

Feature Correlation Matrix (|r| > 0.1):
                Correlation
Feature                    
Close              1.000000
High               0.999879
Low                0.999874
Open               0.999763
Close_t-1          0.999646
MA_5               0.999597
MA_7               0.999385
EMA_12             0.999190
MA_14              0.998674
EMA_26             0.998149
BB_upper           0.996720
BB_lower           0.996684
Adjusted Close     0.996421
PVT                0.853159
ATR_14             0.796893
Range_t-1          0.711059
GapAbs             0.461156
Vol_MA_14          0.306307
Vol_MA_7           0.290633
Vol_MA_5           0.282938
Volume             0.246980
MACD               0.214535
RSI                0.115112
VIF Data (VIF <= 5.0):
   Feature      VIF
    GapDir 1.486466
   Vol_Chg 1.544253
QuarterEnd 1.557439
  MonthEnd 1.571519
    GapAbs 2.080755
       DoW 3.000157
       VPT 3.339900

VIF 检验提出了大部分的新特征,这倒是在预想之内,因为价格相关特征(如 Close_t-1, MA_5, MA_7, EMA_12, EMA_26 等)都基于 Close 价格计算,通常高度相关。此外,技术类指标(如 RSI, MACD, BB_upper, BB_lower)也可能与价格特征高度相关。这种相关性必然导致 VIF 值升高。那么是不是需要进一步处理训练数据集,使其满足VIF指标?其实不一定。一方面,这里使用的是股票价格时间序列,特征之间天然存在相关性,完全消除共线性可能不现实。另一方面,咱们还得考虑选择的模型类型,比如线性回归(如 OLS),对多重共线性非常敏感,这个时候可能需要严格控制 VIF,因此可能需要只保留这 7 个特征。然而,树类模型,比如如随机森林、XGBoost,对多重共线性并不敏感,这个时候可以保留所有 35 个特征,让模型自己选择重要特征。除了模型类型外,可能还需要综合考虑咱们的预测目标,比如预测准确性和模型的可解释性等方面的因素。下面,咱们主要从选择的模型类型角度,对以上特征进行相应的处理。

Previous Next