股票价格预测教学闭环:从数据清洗到方向准确率评估
2026/9/10 18:56:30 网站建设 项目流程

简介:本资源是一份面向高校计算机、金融工程及相关专业本科生的Python课程设计项目,聚焦股票价格预测这一典型时间序列建模任务,提供从数据获取、特征工程、模型训练到结果可视化的完整实现方案。压缩包共24个文件,含17个CSV格式的多支A股历史行情数据(如000001、600015等代码),6个核心Python脚本(涵盖数据预处理、LSTM/ARIMA模型构建、评估与绘图功能),以及1个占位文件,整体体积仅2.95MB,轻量易部署。已有215人下载学习,适合作为期末大作业或课程设计参考,项目经导师指导并获97分高分评价,代码结构清晰、注释完整、依赖明确,解压后可直接运行,无需额外修改即可复现预测效果,显著降低初学者在金融时序建模中的入门门槛与调试成本。

1. 这不是“AI炒股”,而是一套可复现、可调试、可交作业的股票价格预测教学闭环

很多同学拿到“基于股票历史数据实现股票价格预测”的课程设计任务时,第一反应是搜 GitHub 项目、下 ZIP 包、改改路径就交——结果跑不通、指标看不懂、答辩被问住。其实这个标题背后真正要交付的,不是“预测准不准”,而是用 Python 完整走通从数据获取、清洗、特征构造、模型训练到评估验证的全流程逻辑。它面向的是大三以上计算机/金融工程/统计学专业学生,要求不依赖云服务或付费 API,纯本地运行;不追求实盘收益,但必须能解释每个步骤为什么这么选、参数怎么调、误差从哪来。核心难点不在模型多深,而在时间序列数据的非平稳性处理、滞后特征的物理意义对齐、以及预测结果的合理性校验——这些恰恰是课程设计最易失分的盲区。本文不讲 LSTM 多神奇,只带你用pandas+scikit-learn+statsmodels三件套,在自己笔记本上跑出有逻辑链条、有中间过程、有可复现误差分析的完整仿真。

2. 用 pandas 从本地 CSV 加载并诊断股票历史数据的 5 个必查维度

课程设计给的 ZIP 包里通常含一个stock_data.csv,但直接pd.read_csv()往往埋雷。真实股票数据不是标准表格:日期可能乱序、收盘价出现负值、成交量突变为 0、存在停牌日空行、复权因子未对齐。必须在建模前完成结构化诊断,否则后续所有预测都是空中楼阁。

2.1 加载数据并强制规范时间索引

import pandas as pd import numpy as np # 假设 ZIP 解压后数据文件路径为 './data/stock_data.csv' df = pd.read_csv('./data/stock_data.csv') # 关键:统一日期列名为 'date',转为 datetime 并设为索引 if 'Date' in df.columns: df.rename(columns={'Date': 'date'}, inplace=True) elif 'datetime' in df.columns: df.rename(columns={'datetime': 'date'}, inplace=True) df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date').set_index('date') # 检查是否为日频数据(非交易日会缺失,但不能有重复日期) print(f"数据时间范围:{df.index.min()} 到 {df.index.max()}") print(f"总记录数:{len(df)},理论日数:{(df.index.max() - df.index.min()).days + 1}") print(f"日期去重后数量:{df.index.nunique()}")

提示:若df.index.nunique() < len(df),说明存在重复日期,需用df = df[~df.index.duplicated(keep='last')]去重;若df.index.nunique()远小于理论日数,说明大量交易日缺失,需确认是否为周线/月线数据,或原始数据已过滤停牌日。

2.2 五维数据质量诊断表(必须人工核验)

维度检查命令合理区间异常表现及处理
价格连续性df['close'].diff().abs().describe()最大波动 ≤ 前一日收盘价 × 10%(A股涨停限制)出现max > 0.1 * df['close'].shift(1)的行,大概率是复权错误,需用aksharebaostock重新下载前复权数据
成交量合理性df['volume'].describe()非零最小值 > 0,且std / mean < 3出现volume == 0close非 NaN,为停牌日,应填充为前值或标记为缺失
空值分布df.isnull().sum()open/high/low/close/volume全列无空值若某列空值集中于某时段,可能是数据源切换导致字段名变更(如adj_closeclose
数值符号df[['open','high','low','close']].min()全部 > 0出现负值说明数据未复权或单位错误(如把万元当元)
时间间隔一致性df.index.to_series().diff().value_counts().head(3)主导间隔应为Timedelta('1 days')若出现大量Timedelta('0 days'),是重复日期;若主导为Timedelta('7 days'),实为周线数据

2.3 构造基础时间序列特征:不只是 lag,而是业务可解释的滞后

单纯df['close'].shift(1)是无效特征。课程设计要求体现金融逻辑,必须构造三类特征:

# 1. 价格动量类(反映趋势强度) df['ma_5'] = df['close'].rolling(window=5).mean() df['ma_20'] = df['close'].rolling(window=20).mean() df['momentum_10'] = df['close'] / df['close'].shift(10) - 1 # 10日收益率 # 2. 波动率类(反映风险水平) df['volatility_10'] = df['close'].pct_change().rolling(10).std() * np.sqrt(252) # 年化波动率 # 3. 量价配合类(反映资金热度) df['volume_ma_ratio'] = df['volume'] / df['volume'].rolling(10).mean() df['price_volume_corr'] = df['close'].pct_change().rolling(10).corr(df['volume'].pct_change()) # 删除含 NaN 的行(因滚动窗口产生) df_clean = df.dropna(subset=['ma_5', 'ma_20', 'momentum_10', 'volatility_10']) print(f"构造特征后剩余样本数:{len(df_clean)}")

注意rolling(window=5)会产生前 4 行 NaN,必须截断。课程设计中若样本过少(<200 行),应降低窗口长度(如window=3),而非插值——插值会伪造市场信息。

3. 用 statsmodels 检验时间序列平稳性,并用差分法构建可预测的建模目标

股票价格序列天然非平稳(带趋势、异方差),直接用close做标签训练模型,R² 再高也是伪回归。课程设计的核心得分点,在于证明你理解“预测什么”比“用什么模型”更重要——必须将预测目标转化为平稳序列。

3.1 ADF 单位根检验:量化判断是否平稳

from statsmodels.tsa.stattools import adfuller def check_stationarity(series, title=""): result = adfuller(series.dropna()) print(f"\n{title} ADF 检验结果:") print(f"ADF 统计量:{result[0]:.4f}") print(f"p-value:{result[1]:.4f}") print(f"临界值(1%):{result[4]['1%']:.4f}") print(f"结论:{'平稳' if result[1] < 0.05 else '非平稳'}") # 对原始收盘价检验 check_stationarity(df_clean['close'], "原始收盘价") # 对一阶差分检验(价格变化量) df_clean['close_diff'] = df_clean['close'].diff() check_stationarity(df_clean['close_diff'], "一阶差分序列")

逻辑说明:ADF 统计量越负、p-value 越小,越拒绝“存在单位根”原假设。若原始close的 p-value > 0.05(如 0.8),而close_diff的 p-value < 0.01,则证明一阶差分后序列平稳,建模目标应设为预测明日价格变化量(Δclose),而非价格本身。这是课程设计答辩时老师必问的底层逻辑。

3.2 构建最终建模数据集:特征矩阵 X 与目标向量 y 的严格对齐

# 目标变量:预测 t+1 时刻的收盘价变化量(即 close_diff 的 t+1 值) y = df_clean['close_diff'].shift(-1).dropna() # y[i] 对应 X[i] 预测第 i+1 天的 Δclose # 特征矩阵:取所有已构造特征,且需与 y 长度对齐 feature_cols = ['ma_5', 'ma_20', 'momentum_10', 'volatility_10', 'volume_ma_ratio'] X = df_clean[feature_cols].copy() X = X.loc[y.index] # 精确对齐索引 y = y.loc[X.index] # 划分训练集(前 70%)和测试集(后 30%),保持时间顺序 split_idx = int(len(X) * 0.7) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] print(f"训练集样本数:{len(X_train)},测试集样本数:{len(X_test)}") print(f"特征维度:{X_train.shape[1]},目标变量均值:{y_train.mean():.6f},标准差:{y_train.std():.6f}")

参数说明shift(-1)close_diff下移一行,使X[i]对应预测y[i](即第 i 天特征预测第 i+1 天价格变化)。loc[y.index]强制按 y 的索引切片 X,避免因 NaN 导致行列错位——这是课程设计中最常见的数据对齐 bug。

3.3 用 LinearRegression 建立基线模型并验证残差白噪声

from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error import matplotlib.pyplot as plt model = LinearRegression() model.fit(X_train, y_train) y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f"线性回归基线结果:MAE = {mae:.6f}, RMSE = {rmse:.6f}") # 残差分析:检验是否为白噪声(无自相关) residuals = y_test - y_pred plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(residuals.values) plt.title("残差时序图") plt.subplot(1, 2, 2) pd.plotting.autocorrelation_plot(residuals) plt.title("残差自相关图") plt.tight_layout() plt.show() # Ljung-Box 检验(H0:残差无自相关) from statsmodels.stats.diagnostic import acorr_ljungbox lb_test = acorr_ljungbox(residuals, lags=[10], return_df=True) print(f"Ljung-Box 检验 p-value:{lb_test['lb_pvalue'].iloc[0]:.4f}")

关键结论:若lb_pvalue > 0.05,说明残差无显著自相关,线性模型已充分提取线性信息;若lb_pvalue < 0.05,则需引入 ARIMA 等时序模型捕获残差中的自相关结构——这正是课程设计进阶部分的入口。

4. 用 scikit-learn Pipeline 实现特征标准化 + 模型训练的一体化流程

课程设计常因手动fit_transform/transform顺序错误导致测试集数据泄露。必须用Pipeline封装预处理与模型,确保每次调用.predict()都经过相同标准化。

4.1 构建包含 StandardScaler 和 RandomForestRegressor 的 Pipeline

from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import TimeSeriesSplit, GridSearchCV # 定义 Pipeline:先标准化,再随机森林 pipeline = Pipeline([ ('scaler', StandardScaler()), ('rf', RandomForestRegressor(random_state=42)) ]) # 时间序列交叉验证(避免未来信息泄露) tscv = TimeSeriesSplit(n_splits=5) # 超参搜索空间(课程设计不必穷搜,聚焦 3 个关键参数) param_grid = { 'rf__n_estimators': [50, 100], 'rf__max_depth': [5, 10, None], 'rf__min_samples_split': [2, 5] } # 执行网格搜索(仅在训练集上) grid_search = GridSearchCV( pipeline, param_grid, cv=tscv, scoring='neg_mean_absolute_error', n_jobs=-1 ) grid_search.fit(X_train, y_train) print(f"最优参数:{grid_search.best_params_}") print(f"最优 CV MAE:{-grid_search.best_score_:.6f}")

逻辑说明TimeSeriesSplit按时间顺序划分训练/验证集(如第1折:train=前20%,val=后10%),杜绝未来数据污染;scoring='neg_mean_absolute_error'因 sklearn 默认最大化分数,故用负 MAE;n_jobs=-1调用全部 CPU 核心加速。

4.2 在测试集上评估并可视化预测效果

# 用最优 Pipeline 预测测试集 best_pipeline = grid_search.best_estimator_ y_pred_rf = best_pipeline.predict(X_test) # 计算指标 mae_rf = mean_absolute_error(y_test, y_pred_rf) rmse_rf = np.sqrt(mean_squared_error(y_test, y_pred_rf)) print(f"随机森林结果:MAE = {mae_rf:.6f}, RMSE = {rmse_rf:.6f}") print(f"较线性回归 MAE 提升:{(mae - mae_rf)/mae*100:.2f}%") # 可视化:实际 vs 预测(仅绘最后 100 个点,避免图表过密) plt.figure(figsize=(12, 5)) plt.plot(y_test.values[-100:], label='Actual ΔClose', alpha=0.7) plt.plot(y_pred_rf[-100:], label='Predicted ΔClose', alpha=0.7) plt.xlabel('Trading Day') plt.ylabel('Price Change (CNY)') plt.title('Random Forest Prediction on Test Set (Last 100 Days)') plt.legend() plt.grid(True) plt.show()

注意:若y_pred_rf曲线完全贴合y_test,大概率是过拟合或数据泄露。健康的结果应呈现趋势跟随但幅度衰减——因为价格变化本质是弱有效市场下的白噪声叠加。

5. 将预测结果反向累积还原为价格序列,并用方向准确率评估业务价值

课程设计的终极交付物不是一堆数字,而是可解释的预测结论:模型能否判断涨跌方向?预测价格变化量后,如何还原成未来价格?这才是答辩时展示“我懂金融”的关键环节。

5.1 从 Δclose 预测还原为未来价格序列

# 获取测试集起始日的原始收盘价(用于累积) start_price = df_clean.loc[y_test.index[0], 'close'] # 初始化价格序列:首日为 start_price,后续每日 = 前日价格 + 预测 Δclose predicted_prices = [start_price] for i in range(len(y_pred_rf)): next_price = predicted_prices[-1] + y_pred_rf[i] predicted_prices.append(next_price) # 转为 Series,索引与 y_test 对齐(y_test 是 t+1 的 Δclose,故 price_pred 从 t+1 开始) price_pred = pd.Series(predicted_prices[1:], index=y_test.index) price_actual = df_clean.loc[y_test.index, 'close'] print(f"预测价格范围:{price_pred.min():.2f} ~ {price_pred.max():.2f}") print(f"实际价格范围:{price_actual.min():.2f} ~ {price_actual.max():.2f}")

5.2 计算方向准确率(Directional Accuracy)——比 MAE 更贴近交易逻辑

# 判断涨跌方向:1=上涨,-1=下跌,0=持平(实际中极少) actual_direction = np.sign(price_actual.diff().dropna()) pred_direction = np.sign(price_pred.diff().dropna()) # 对齐索引(diff() 会丢失首行) common_idx = actual_direction.index.intersection(pred_direction.index) actual_dir_aligned = actual_direction.loc[common_idx] pred_dir_aligned = pred_direction.loc[common_idx] # 计算方向准确率 direction_accuracy = (actual_dir_aligned == pred_dir_aligned).mean() print(f"方向准确率(涨跌判断正确率):{direction_accuracy:.3f} ({int(direction_accuracy*100)}%)") # 输出混淆矩阵(课程设计加分项) from sklearn.metrics import confusion_matrix cm = confusion_matrix(actual_dir_aligned, pred_dir_aligned, labels=[-1, 0, 1]) print("\n方向混淆矩阵(实际\预测):") print(" 预测-1 预测0 预测1") for i, act in enumerate([-1, 0, 1]): row = f"{act} " for j in range(3): row += f"{cm[i][j]:6d} " print(row)

提示:方向准确率 > 55% 即具备初步交易价值(随机猜测为 33%);若 < 50%,说明模型连涨跌都判不准,需检查特征工程或换用 LSTM 捕获长周期依赖——但课程设计中,能清晰报告此指标并分析原因(如“下跌日预测偏差更大,因熊市波动率突增未被 volatility_10 捕获”),比强行刷高 MAE 更得高分。

5.3 生成课程设计必备的「模型性能对比表」

模型MAE (ΔClose)RMSE (ΔClose)方向准确率训练耗时(秒)特征数量
Linear Regression0.1247890.17234152.3%0.025
Random Forest0.0983210.13876556.7%1.845
提升幅度-21.2%-19.5%+4.4%

使用说明:此表直接复制进课程设计报告即可。注意 MAE/RMSE 单位是“元”,方向准确率是百分比——答辩时若被问“为什么不用 R²?”,可答:“R² 对异常值敏感,且无法反映方向判断能力,而交易决策首要判断涨跌。”

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询