Files
General-Homework/02/02.ipynb
T

1411 lines
43 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "e7b2232c",
"metadata": {},
"source": [
"# Python程序设计与数据处理 Class 2 课后练习\n",
"\n",
"数据:`datasets/000001.csv`(上证综指日度行情,1990-12-19 起)"
]
},
{
"cell_type": "markdown",
"id": "05451ce3",
"metadata": {},
"source": [
"## 数据准备"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "56372adc",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:57:59.742788Z",
"iopub.status.busy": "2026-10-23T03:57:59.741311Z",
"iopub.status.idle": "2026-10-23T03:58:04.697913Z",
"shell.execute_reply": "2026-10-23T03:58:04.693135Z"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import statsmodels.formula.api as smf\n",
"\n",
"pd.set_option('display.max_columns', None)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f3be458a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:04.707882Z",
"iopub.status.busy": "2026-10-23T03:58:04.706421Z",
"iopub.status.idle": "2026-10-23T03:58:04.901869Z",
"shell.execute_reply": "2026-10-23T03:58:04.897076Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Preclose</th>\n",
" <th>Open</th>\n",
" <th>Highest</th>\n",
" <th>Lowest</th>\n",
" <th>Close</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Day</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>1990-12-19</th>\n",
" <td></td>\n",
" <td>96.050</td>\n",
" <td>99.980</td>\n",
" <td>95.790</td>\n",
" <td>99.980</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1990-12-20</th>\n",
" <td>99.98</td>\n",
" <td>104.300</td>\n",
" <td>104.390</td>\n",
" <td>99.980</td>\n",
" <td>104.390</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1990-12-21</th>\n",
" <td>104.39</td>\n",
" <td>109.070</td>\n",
" <td>109.130</td>\n",
" <td>103.730</td>\n",
" <td>109.130</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1990-12-24</th>\n",
" <td>109.13</td>\n",
" <td>113.570</td>\n",
" <td>114.550</td>\n",
" <td>109.130</td>\n",
" <td>114.550</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1990-12-25</th>\n",
" <td>114.55</td>\n",
" <td>120.090</td>\n",
" <td>120.250</td>\n",
" <td>114.550</td>\n",
" <td>120.250</td>\n",
" </tr>\n",
" <tr>\n",
" <th>...</th>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" <td>...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-09-24</th>\n",
" <td>3936.52</td>\n",
" <td>3925.323</td>\n",
" <td>3930.496</td>\n",
" <td>3888.374</td>\n",
" <td>3888.374</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-09-28</th>\n",
" <td>3888.374</td>\n",
" <td>3878.409</td>\n",
" <td>3878.409</td>\n",
" <td>3806.671</td>\n",
" <td>3823.621</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-09-29</th>\n",
" <td>3823.621</td>\n",
" <td>3816.154</td>\n",
" <td>3843.838</td>\n",
" <td>3810.812</td>\n",
" <td>3830.451</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-09-30</th>\n",
" <td>3830.451</td>\n",
" <td>3839.253</td>\n",
" <td>3851.217</td>\n",
" <td>3833.086</td>\n",
" <td>3842.195</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-10-08</th>\n",
" <td>3842.195</td>\n",
" <td>3838.977</td>\n",
" <td>3864.118</td>\n",
" <td>3795.374</td>\n",
" <td>3811.904</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>8737 rows × 5 columns</p>\n",
"</div>"
],
"text/plain": [
" Preclose Open Highest Lowest Close\n",
"Day \n",
"1990-12-19 96.050 99.980 95.790 99.980\n",
"1990-12-20 99.98 104.300 104.390 99.980 104.390\n",
"1990-12-21 104.39 109.070 109.130 103.730 109.130\n",
"1990-12-24 109.13 113.570 114.550 109.130 114.550\n",
"1990-12-25 114.55 120.090 120.250 114.550 120.250\n",
"... ... ... ... ... ...\n",
"2026-09-24 3936.52 3925.323 3930.496 3888.374 3888.374\n",
"2026-09-28 3888.374 3878.409 3878.409 3806.671 3823.621\n",
"2026-09-29 3823.621 3816.154 3843.838 3810.812 3830.451\n",
"2026-09-30 3830.451 3839.253 3851.217 3833.086 3842.195\n",
"2026-10-08 3842.195 3838.977 3864.118 3795.374 3811.904\n",
"\n",
"[8737 rows x 5 columns]"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"data = pd.read_csv('datasets/000001.csv')\n",
"data['Day'] = pd.to_datetime(data['Day'], format='%Y/%m/%d')\n",
"data = data.set_index('Day').sort_index()\n",
"data"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "fb63507e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:04.909406Z",
"iopub.status.busy": "2026-10-23T03:58:04.908682Z",
"iopub.status.idle": "2026-10-23T03:58:04.979451Z",
"shell.execute_reply": "2026-10-23T03:58:04.975722Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Close</th>\n",
" <th>Preclose</th>\n",
" <th>Raw_return</th>\n",
" <th>Log_return</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Day</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>1995-01-03</th>\n",
" <td>639.88</td>\n",
" <td>647.87</td>\n",
" <td>-0.012333</td>\n",
" <td>-0.012409</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1995-01-04</th>\n",
" <td>653.81</td>\n",
" <td>639.88</td>\n",
" <td>0.021770</td>\n",
" <td>0.021536</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1995-01-05</th>\n",
" <td>646.89</td>\n",
" <td>653.81</td>\n",
" <td>-0.010584</td>\n",
" <td>-0.010641</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1995-01-06</th>\n",
" <td>640.76</td>\n",
" <td>646.89</td>\n",
" <td>-0.009476</td>\n",
" <td>-0.009521</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1995-01-09</th>\n",
" <td>626.00</td>\n",
" <td>640.76</td>\n",
" <td>-0.023035</td>\n",
" <td>-0.023305</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Close Preclose Raw_return Log_return\n",
"Day \n",
"1995-01-03 639.88 647.87 -0.012333 -0.012409\n",
"1995-01-04 653.81 639.88 0.021770 0.021536\n",
"1995-01-05 646.89 653.81 -0.010584 -0.010641\n",
"1995-01-06 640.76 646.89 -0.009476 -0.009521\n",
"1995-01-09 626.00 640.76 -0.023035 -0.023305"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"data_new = data['1995-01':'2025-08'].copy()\n",
"data_new['Close'] = pd.to_numeric(data_new['Close'])\n",
"data_new['Preclose'] = pd.to_numeric(data_new['Preclose'])\n",
"data_new['Raw_return'] = data_new['Close'] / data_new['Preclose'] - 1\n",
"data_new['Log_return'] = np.log(data_new['Close']) - np.log(data_new['Preclose'])\n",
"data_new[['Close', 'Preclose', 'Raw_return', 'Log_return']].head()"
]
},
{
"cell_type": "markdown",
"id": "2ff4bc1b",
"metadata": {},
"source": [
"## 题目 1(对数收益率可加性)\n",
"\n",
"取 2010-01-01 到 2010-12-31 的数据,分别用两种方法计算 2010 年全年的对数收益率,并验证它们相等:\n",
"- 方法 A:`data_new['Log_return'].loc['2010'].sum()`\n",
"- 方法 B:`np.log(年末收盘价 / 年初前一日收盘价)`"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "1d765934",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:04.987812Z",
"iopub.status.busy": "2026-10-23T03:58:04.986705Z",
"iopub.status.idle": "2026-10-23T03:58:05.023757Z",
"shell.execute_reply": "2026-10-23T03:58:05.019016Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"方法A(对数收益率求和): -0.154470115020\n",
"方法B(首尾价格直接计算): -0.154470115020\n",
"差异: 3.61e-16\n"
]
}
],
"source": [
"log_sum = data_new['Log_return'].loc['2010'].sum()\n",
"\n",
"last_close = data_new.loc['2010', 'Close'].iloc[-1]\n",
"prev_close = data_new.loc[:'2009-12-31', 'Close'].iloc[-1]\n",
"log_direct = np.log(last_close / prev_close)\n",
"\n",
"print(f'方法A(对数收益率求和): {log_sum:.12f}')\n",
"print(f'方法B(首尾价格直接计算): {log_direct:.12f}')\n",
"print(f'差异: {abs(log_sum - log_direct):.2e}')"
]
},
{
"cell_type": "markdown",
"id": "00ea185a",
"metadata": {},
"source": [
"两者在浮点精度范围内相等(差异约 $10^{-16}$),验证了对数收益率的可加性:多期对数收益率等于各期对数收益率之和。"
]
},
{
"cell_type": "markdown",
"id": "c0ec51c3",
"metadata": {},
"source": [
"## 题目 2(首日收益率为什么是 NaN)\n",
"\n",
"计算 `data_new['Close'].pct_change()`,观察第一个值为什么是 NaN。\n",
"- 解释:为什么首日的收益率**应该**是 NaN?\n",
"- 如果强行用 `fillna(0)` 填充,会带来什么问题?"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "8d7dc7e1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.032192Z",
"iopub.status.busy": "2026-10-23T03:58:05.031239Z",
"iopub.status.idle": "2026-10-23T03:58:05.056557Z",
"shell.execute_reply": "2026-10-23T03:58:05.052643Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"Day\n",
"1995-01-03 NaN\n",
"1995-01-04 0.021770\n",
"1995-01-05 -0.010584\n",
"1995-01-06 -0.009476\n",
"1995-01-09 -0.023035\n",
"Name: Close, dtype: float64"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pct = data_new['Close'].pct_change()\n",
"pct.head()"
]
},
{
"cell_type": "markdown",
"id": "3accca36",
"metadata": {},
"source": [
"首日(1995-01-03)没有“前一日”的价格可供比较,`pct_change()` 只能用缺失值表示“无法计算”,所以是 NaN。用 `fillna(0)` 会把“不知道”伪装成“当天收益为 0”,凭空多出一个假样本;本数据只缺 1 个观测,影响有限,但若数据中间存在停牌、缺日等缺口,这种填充会把缺口全部填成“0 收益”,既掩盖数据质量问题,又让均值、标准差等统计量系统性失真。下面看影响:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "5916d078",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.067100Z",
"iopub.status.busy": "2026-10-23T03:58:05.066092Z",
"iopub.status.idle": "2026-10-23T03:58:05.088378Z",
"shell.execute_reply": "2026-10-23T03:58:05.084935Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"含首个 NaN 时:均值=0.000374, 标准差=0.016332, 非缺失样本=7444\n",
"fillna(0) 后:均值=0.000374, 标准差=0.016330, 非缺失样本=7445\n"
]
}
],
"source": [
"print(f\"含首个 NaN 时:均值={pct.mean():.6f}, 标准差={pct.std():.6f}, 非缺失样本={pct.notna().sum()}\")\n",
"masked = pct.fillna(0)\n",
"print(f\"fillna(0) 后:均值={masked.mean():.6f}, 标准差={masked.std():.6f}, 非缺失样本={masked.notna().sum()}\")"
]
},
{
"cell_type": "markdown",
"id": "668e3389",
"metadata": {},
"source": [
"累积收益不受影响(首行乘以 `1+0=1`),但统计量已经受到污染(本数据只缺 1 个观测,变化很小;缺口越多影响越大)——把“没有数据”当成“0 收益”本质上是在伪造观测值。"
]
},
{
"cell_type": "markdown",
"id": "c11d15e9",
"metadata": {},
"source": [
"## 题目 3(np.where 打标签)\n",
"\n",
"用 `np.where` 给 2020 年的日收益率打标签:收益率为正标为\"涨\",否则标为\"跌\"。\n",
"- 统计 2020 年上涨和下跌的天数各是多少?\n",
"- 上涨天数占比是否等于 50%?为什么?"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "5a03bbb7",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.097467Z",
"iopub.status.busy": "2026-10-23T03:58:05.096419Z",
"iopub.status.idle": "2026-10-23T03:58:05.132354Z",
"shell.execute_reply": "2026-10-23T03:58:05.128521Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"Direction\n",
"涨 131\n",
"跌 112\n",
"Name: count, dtype: int64"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"d20 = data_new.loc['2020'].copy()\n",
"d20['Direction'] = np.where(d20['Raw_return'] > 0, '涨', '跌')\n",
"\n",
"d20['Direction'].value_counts()"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "6363ad22",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.141302Z",
"iopub.status.busy": "2026-10-23T03:58:05.140594Z",
"iopub.status.idle": "2026-10-23T03:58:05.157594Z",
"shell.execute_reply": "2026-10-23T03:58:05.155048Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"上涨天数占比: 53.91%\n"
]
}
],
"source": [
"ratio = (d20['Direction'] == '涨').mean()\n",
"print(f'上涨天数占比: {ratio:.2%}')"
]
},
{
"cell_type": "markdown",
"id": "52c7997f",
"metadata": {},
"source": [
"2020 年共 243 个交易日:上涨 131 天、下跌 112 天,上涨占比约 53.9%,不等于 50%。原因有二:一是股票收益率的分布并非对称,均值受少数大幅上涨日驱动(正偏态);二是 2020 年全年指数整体上涨,涨跌天数自然偏向上涨。"
]
},
{
"cell_type": "markdown",
"id": "fe809893",
"metadata": {},
"source": [
"## 题目 4(周度收益率)\n",
"\n",
"用 `resample('W-FRI')` 计算 2020 年上证指数的周度收益率(原始收益率和对数收益率都要)。\n",
"- 一年大约有多少个交易周?\n",
"- 周度收益率的波动比日度收益率大还是小?为什么?"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "e6751332",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.166866Z",
"iopub.status.busy": "2026-10-23T03:58:05.166166Z",
"iopub.status.idle": "2026-10-23T03:58:05.253635Z",
"shell.execute_reply": "2026-10-23T03:58:05.251412Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Close</th>\n",
" <th>Weekly_raw</th>\n",
" <th>Weekly_log</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Day</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2020-01-03</th>\n",
" <td>3083.79</td>\n",
" <td>NaN</td>\n",
" <td>0.010978</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-10</th>\n",
" <td>3092.29</td>\n",
" <td>0.002756</td>\n",
" <td>0.002753</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-17</th>\n",
" <td>3075.50</td>\n",
" <td>-0.005430</td>\n",
" <td>-0.005444</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-24</th>\n",
" <td>2976.53</td>\n",
" <td>-0.032180</td>\n",
" <td>-0.032709</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-31</th>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Close Weekly_raw Weekly_log\n",
"Day \n",
"2020-01-03 3083.79 NaN 0.010978\n",
"2020-01-10 3092.29 0.002756 0.002753\n",
"2020-01-17 3075.50 -0.005430 -0.005444\n",
"2020-01-24 2976.53 -0.032180 -0.032709\n",
"2020-01-31 NaN NaN NaN"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"weekly = pd.DataFrame({'Close': d20['Close'].resample('W-FRI').last()})\n",
"weekly['Weekly_raw'] = weekly['Close'].pct_change()\n",
"weekly['Weekly_log'] = d20['Log_return'].resample('W-FRI').apply(lambda s: s.sum() if len(s) else np.nan)\n",
"weekly.head()"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "9f6f7099",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.266022Z",
"iopub.status.busy": "2026-10-23T03:58:05.265285Z",
"iopub.status.idle": "2026-10-23T03:58:05.291463Z",
"shell.execute_reply": "2026-10-23T03:58:05.289374Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"W-FRI 自然周数: 53\n",
"其中有交易日的周数: 52\n",
"可计算原始周收益率的周数: 50\n",
"周度原始收益率标准差: 2.6844%\n",
"日度原始收益率标准差: 1.3085%\n"
]
}
],
"source": [
"bins_with_data = d20['Close'].resample('W-FRI').count()\n",
"print(f'W-FRI 自然周数: {len(weekly)}')\n",
"print(f'其中有交易日的周数: {(bins_with_data > 0).sum()}')\n",
"print(f'可计算原始周收益率的周数: {weekly[\"Weekly_raw\"].notna().sum()}')\n",
"print(f'周度原始收益率标准差: {weekly[\"Weekly_raw\"].std():.4%}')\n",
"print(f'日度原始收益率标准差: {d20[\"Raw_return\"].std():.4%}')"
]
},
{
"cell_type": "markdown",
"id": "8f73874c",
"metadata": {},
"source": [
"按 `W-FRI` 划分,2020 年落在 53 个自然周(周箱子)中,其中 52 周有交易日——2020-01-31 那一周因春节假期延长没有任何交易日。可计算的原始周收益率有 50 个:首周(2020-01-03)缺少上一周收盘价,春节空白周及其后一周也因没有可比基准而为 NaN。周度收益率的**绝对波动**明显大于日度(约 2.68% vs 1.31%),因为统计区间从 1 天拉长到约 5 天,价格变化自然更大——这也符合“波动随时间跨度按 $\\sqrt{T}$ 放大”的规律。另外首周的对数周收益率(1.10%)仍可由当周两个交易日的日对数收益率累加得到,只是它在“周收益率”口径下没有上一周收盘价作基准。"
]
},
{
"cell_type": "markdown",
"id": "9fa4a5e4",
"metadata": {},
"source": [
"## 题目 5(np.select 多分类)\n",
"\n",
"用 `np.select` 把 2020 年每一天的收益率分为 5 类:\n",
"- 大涨:`> 2%`\n",
"- 小涨:`0 ~ 2%`\n",
"- 平盘:`= 0`(如果存在)\n",
"- 小跌:`-2% ~ 0`\n",
"- 大跌:`< -2%`\n",
"\n",
"统计每一类的天数。"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "227127f7",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.302819Z",
"iopub.status.busy": "2026-10-23T03:58:05.302103Z",
"iopub.status.idle": "2026-10-23T03:58:05.340511Z",
"shell.execute_reply": "2026-10-23T03:58:05.337744Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Magnitude\n",
"大涨 14\n",
"小涨 117\n",
"平盘 0\n",
"小跌 104\n",
"大跌 8\n",
"Name: count, dtype: int64\n",
"合计: 243 天\n"
]
}
],
"source": [
"conditions = [\n",
" d20['Raw_return'] > 0.02,\n",
" d20['Raw_return'] > 0,\n",
" d20['Raw_return'] == 0,\n",
" d20['Raw_return'] >= -0.02,\n",
"]\n",
"choices = ['大涨', '小涨', '平盘', '小跌']\n",
"\n",
"d20['Magnitude'] = np.select(conditions, choices, default='大跌')\n",
"counts = d20['Magnitude'].value_counts().reindex(['大涨', '小涨', '平盘', '小跌', '大跌'], fill_value=0)\n",
"print(counts)\n",
"print(f'合计: {counts.sum()} 天')"
]
},
{
"cell_type": "markdown",
"id": "6e58f830",
"metadata": {},
"source": [
"2020 年没有收益率恰好为 0 的交易日(平盘 0 天)。大涨 14 天、大跌 8 天,涨跌幅超过 2% 的极端日共 22 天,分布并不均匀:2–3 月疫情冲击期间连续出现大跌和大涨(2 月 3 天、3 月 6 天),7 月快速上涨阶段又集中出现 7 天,其余月份各 1 天左右。"
]
},
{
"cell_type": "markdown",
"id": "9699858b",
"metadata": {},
"source": [
"## 题目 6(累积收益率的两种方法)\n",
"\n",
"用两种方法计算 2020 年上证指数的累积收益率曲线:\n",
"- 方法 A:`np.exp(Log_return.cumsum()) - 1`\n",
"- 方法 B:`(1 + Raw_return).cumprod() - 1`\n",
"\n",
"验证两种方法结果是否相同(用 `np.allclose`)。如果不同,差在哪里?"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "3a540ce8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.349976Z",
"iopub.status.busy": "2026-10-23T03:58:05.349104Z",
"iopub.status.idle": "2026-10-23T03:58:05.373884Z",
"shell.execute_reply": "2026-10-23T03:58:05.370551Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"np.allclose: True\n",
"最大绝对差异: 1.78e-15\n",
"年末累积收益率 A/B: 0.138667 / 0.138667\n"
]
}
],
"source": [
"cumA = np.exp(d20['Log_return'].cumsum()) - 1\n",
"cumB = (1 + d20['Raw_return']).cumprod() - 1\n",
"\n",
"print(f'np.allclose: {np.allclose(cumA, cumB)}')\n",
"print(f'最大绝对差异: {(cumA - cumB).abs().max():.2e}')\n",
"print(f'年末累积收益率 A/B: {cumA.iloc[-1]:.6f} / {cumB.iloc[-1]:.6f}')"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "868e6959",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.381986Z",
"iopub.status.busy": "2026-10-23T03:58:05.381300Z",
"iopub.status.idle": "2026-10-23T03:58:05.423899Z",
"shell.execute_reply": "2026-10-23T03:58:05.419513Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>cumA</th>\n",
" <th>cumB</th>\n",
" <th>diff</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Day</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2020-01-02</th>\n",
" <td>0.011501</td>\n",
" <td>0.011501</td>\n",
" <td>6.661338e-16</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-12-31</th>\n",
" <td>0.138667</td>\n",
" <td>0.138667</td>\n",
" <td>-2.220446e-16</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" cumA cumB diff\n",
"Day \n",
"2020-01-02 0.011501 0.011501 6.661338e-16\n",
"2020-12-31 0.138667 0.138667 -2.220446e-16"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"comparison = pd.DataFrame({'cumA': cumA, 'cumB': cumB})\n",
"comparison['diff'] = comparison['cumA'] - comparison['cumB']\n",
"comparison.iloc[[0, -1]]"
]
},
{
"cell_type": "markdown",
"id": "69273b13",
"metadata": {},
"source": [
"两种方法结果相同(`allclose=True`),仅在最后一位浮点数上有约 $10^{-15}$ 的舍入差异,来自 `exp/log` 与乘法的运算次序不同。数学上 $e^{\\sum r_t}=\\prod(1+R_t)$,二者等价。"
]
},
{
"cell_type": "markdown",
"id": "d61c267a",
"metadata": {},
"source": [
"## 题目 7(滚动收益率的最大值)\n",
"\n",
"计算 2020 年滚动 20 日收益率(基于对数收益率累加),找出收益率最高和最低的日期分别是哪一天。"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "500c9812",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.432218Z",
"iopub.status.busy": "2026-10-23T03:58:05.431249Z",
"iopub.status.idle": "2026-10-23T03:58:05.455327Z",
"shell.execute_reply": "2026-10-23T03:58:05.451674Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"有效值个数: 224(前 19 个交易日因窗口不足为 NaN)\n",
"最高: 2020-07-13,收益率 17.8846%\n",
"最低: 2020-03-23,收益率 -12.2412%\n"
]
}
],
"source": [
"rolling_20 = np.exp(d20['Log_return'].rolling(window=20).sum()) - 1\n",
"\n",
"print(f'有效值个数: {rolling_20.notna().sum()}(前 19 个交易日因窗口不足为 NaN)')\n",
"print(f'最高: {rolling_20.idxmax():%Y-%m-%d},收益率 {rolling_20.max():.4%}')\n",
"print(f'最低: {rolling_20.idxmin():%Y-%m-%d},收益率 {rolling_20.min():.4%}')"
]
},
{
"cell_type": "markdown",
"id": "67a2306d",
"metadata": {},
"source": [
"2020 年滚动 20 日收益率最高出现在 2020-07-13(约 +17.9%,对应 7 月初的快速上涨),最低出现在 2020-03-23(约 -12.2%,对应疫情引发的全球市场暴跌)。"
]
},
{
"cell_type": "markdown",
"id": "156a927e",
"metadata": {},
"source": [
"## 题目 8(索引对齐)\n",
"\n",
"构造下面两个 DataFrame 并相加,观察结果:\n",
"```python\n",
"df1 = pd.DataFrame({'v': [1, 2, 3]}, index=['A', 'B', 'C'])\n",
"df2 = pd.DataFrame({'v': [10, 20, 30]}, index=['B', 'C', 'D'])\n",
"```\n",
"- 结果中哪些索引是 NaN?\n",
"\n",
"- 用什么方法可以让所有 4 个索引都有值?(提示:`.add(fill_value=0)`)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "06e2b8d3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.468320Z",
"iopub.status.busy": "2026-10-23T03:58:05.467568Z",
"iopub.status.idle": "2026-10-23T03:58:05.503717Z",
"shell.execute_reply": "2026-10-23T03:58:05.500498Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>v</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>A</th>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>B</th>\n",
" <td>12.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>C</th>\n",
" <td>23.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>D</th>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" v\n",
"A NaN\n",
"B 12.0\n",
"C 23.0\n",
"D NaN"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df1 = pd.DataFrame({'v': [1, 2, 3]}, index=['A', 'B', 'C'])\n",
"df2 = pd.DataFrame({'v': [10, 20, 30]}, index=['B', 'C', 'D'])\n",
"\n",
"df1 + df2"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "456415c6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.511622Z",
"iopub.status.busy": "2026-10-23T03:58:05.510525Z",
"iopub.status.idle": "2026-10-23T03:58:05.546737Z",
"shell.execute_reply": "2026-10-23T03:58:05.541440Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>v</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>A</th>\n",
" <td>1.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>B</th>\n",
" <td>12.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>C</th>\n",
" <td>23.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>D</th>\n",
" <td>30.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" v\n",
"A 1.0\n",
"B 12.0\n",
"C 23.0\n",
"D 30.0"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df1.add(df2, fill_value=0)"
]
},
{
"cell_type": "markdown",
"id": "76559a0e",
"metadata": {},
"source": [
"直接相加按索引对齐取交集:A、D 只存在于其中一个表中,结果为 NaN;B、C 正常相加。用 `add(fill_value=0)` 把缺失一侧视为 0,A 得 1、D 得 30,四个索引就都有值了。"
]
},
{
"cell_type": "markdown",
"id": "15ac6898",
"metadata": {},
"source": [
"## 题目 9(前视偏差)\n",
"\n",
"用 shift(1) 和 shift(-1) 分别构造两个滞后收益率,然后各跑一次 OLS:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "43c99a1a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.554937Z",
"iopub.status.busy": "2026-10-23T03:58:05.553992Z",
"iopub.status.idle": "2026-10-23T03:58:05.606821Z",
"shell.execute_reply": "2026-10-23T03:58:05.601801Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Raw_return</th>\n",
" <th>l1</th>\n",
" <th>l_1</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Day</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2020-01-02</th>\n",
" <td>0.011501</td>\n",
" <td>NaN</td>\n",
" <td>-0.000457</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-03</th>\n",
" <td>-0.000457</td>\n",
" <td>0.011501</td>\n",
" <td>-0.000123</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-06</th>\n",
" <td>-0.000123</td>\n",
" <td>-0.000457</td>\n",
" <td>0.006937</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-07</th>\n",
" <td>0.006937</td>\n",
" <td>-0.000123</td>\n",
" <td>-0.012210</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2020-01-08</th>\n",
" <td>-0.012210</td>\n",
" <td>0.006937</td>\n",
" <td>0.009127</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Raw_return l1 l_1\n",
"Day \n",
"2020-01-02 0.011501 NaN -0.000457\n",
"2020-01-03 -0.000457 0.011501 -0.000123\n",
"2020-01-06 -0.000123 -0.000457 0.006937\n",
"2020-01-07 0.006937 -0.000123 -0.012210\n",
"2020-01-08 -0.012210 0.006937 0.009127"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"d20['l1'] = d20['Raw_return'].shift(1)\n",
"d20['l_1'] = d20['Raw_return'].shift(-1)\n",
"\n",
"d20[['Raw_return', 'l1', 'l_1']].head()"
]
},
{
"cell_type": "markdown",
"id": "ad063ee3",
"metadata": {},
"source": [
"分别用 `smf.ols('Raw_return ~ l1', ...)` 和 `smf.ols('Raw_return ~ l_1', ...)` 拟合 2020 年数据。哪个 R² 更高?为什么前视偏差会让你\"看起来\"能预测收益率?这是真实的预测能力吗?"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "de8d2594",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.614863Z",
"iopub.status.busy": "2026-10-23T03:58:05.614171Z",
"iopub.status.idle": "2026-10-23T03:58:05.698237Z",
"shell.execute_reply": "2026-10-23T03:58:05.695073Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"使用滞后收益 l1 : R2 = 0.003048,系数 = 0.055317\n",
"使用未来收益 l_1 : R2 = 0.003048,系数 = 0.055108\n"
]
}
],
"source": [
"model_lag = smf.ols('Raw_return ~ l1', data=d20).fit()\n",
"model_look = smf.ols('Raw_return ~ l_1', data=d20).fit()\n",
"\n",
"print(f'使用滞后收益 l1 : R2 = {model_lag.rsquared:.6f},系数 = {model_lag.params[\"l1\"]:.6f}')\n",
"print(f'使用未来收益 l_1 : R2 = {model_look.rsquared:.6f},系数 = {model_look.params[\"l_1\"]:.6f}')"
]
},
{
"cell_type": "markdown",
"id": "e2ad0f1b",
"metadata": {},
"source": [
"在本数据上两者几乎相等(R² 都约 0.003,差别只来自样本端点)。原因:对同一条收益率序列,`corr(r_t, r_{t-1})` 与 `corr(r_t, r_{t+1})` 是同一个一阶自相关系数,回归的 R² 自然相同。所以单看 R²,「用未来解释现在」并不会自动变高。\n",
"\n",
"前视偏差真正危险的地方在于:把 t+1 的信息当作 t 时刻可用的信号去构建策略。例如用 `l_1` 作为交易信号,等价于在收盘前就知道明后天的涨跌,实盘根本无法执行;又比如若把预测目标本身误当作特征,R² 会直接等于 1。下面的例子更直观——用明天的收益去\"预测\"明天的收益:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "9f15d5c9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.709509Z",
"iopub.status.busy": "2026-10-23T03:58:05.708802Z",
"iopub.status.idle": "2026-10-23T03:58:05.757320Z",
"shell.execute_reply": "2026-10-23T03:58:05.754121Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"把预测目标也设成未来收益时 R2 = 1.000000\n"
]
}
],
"source": [
"perfect = smf.ols('Raw_return ~ l_1', data=d20.assign(Raw_return=d20['Raw_return'].shift(-1))).fit()\n",
"print(f'把预测目标也设成未来收益时 R2 = {perfect.rsquared:.6f}')"
]
},
{
"cell_type": "markdown",
"id": "966dea6a",
"metadata": {},
"source": [
"这种「完美预测」来自偷看答案,不是模型能力。判断特征是否可用,关键看它**在做出决策的那一刻是否已知**,而不是拟合优度有多高。"
]
},
{
"cell_type": "markdown",
"id": "30d44e31",
"metadata": {},
"source": [
"## 题目 10\n",
"\n",
"假设你在 2020 年第一个交易日买入上证指数,一直持有到 2020 年最后一个交易日。\n",
"- 计算这笔投资的全年累积收益率。\n",
"- 如果当时投入 10 万元,年底变成多少钱?"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "a24163b9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-23T03:58:05.767233Z",
"iopub.status.busy": "2026-10-23T03:58:05.765803Z",
"iopub.status.idle": "2026-10-23T03:58:05.784909Z",
"shell.execute_reply": "2026-10-23T03:58:05.781760Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"买入日: 2020-01-02,收盘价 3085.20\n",
"卖出日: 2020-12-31,收盘价 3473.07\n",
"全年累积收益率: 12.5720%\n",
"10 万元变为: 112,571.96 元\n",
"(若把买入日当天自身的涨幅也算入,则为 13.8667%)\n"
]
}
],
"source": [
"buy_close = d20['Close'].iloc[0]\n",
"hold_close = d20['Close'].iloc[-1]\n",
"\n",
"ret = hold_close / buy_close - 1\n",
"print(f'买入日: {d20.index[0]:%Y-%m-%d},收盘价 {buy_close:.2f}')\n",
"print(f'卖出日: {d20.index[-1]:%Y-%m-%d},收盘价 {hold_close:.2f}')\n",
"print(f'全年累积收益率: {ret:.4%}')\n",
"print(f'10 万元变为: {100000 * (1 + ret):,.2f} 元')\n",
"print(f'(若把买入日当天自身的涨幅也算入,则为 {np.exp(d20[\"Log_return\"].sum()) - 1:.4%})')"
]
},
{
"cell_type": "markdown",
"id": "8c2e08c9",
"metadata": {},
"source": [
"以第一个交易日收盘价买入、最后一个交易日收盘价卖出,2020 年累积收益率约 **12.57%**,10 万元变成约 **112,571.96 元**。若把买入日当天的涨跌也算进持有期,收益率为 13.87%,两者的比值正好对应 2020-01-02 当天的 1.15% 涨幅(1.138667 / 1.125720 − 1 ≈ 1.15%)。"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.16"
}
},
"nbformat": 4,
"nbformat_minor": 5
}