{
"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": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Preclose | \n",
" Open | \n",
" Highest | \n",
" Lowest | \n",
" Close | \n",
"
\n",
" \n",
" | Day | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1990-12-19 | \n",
" | \n",
" 96.050 | \n",
" 99.980 | \n",
" 95.790 | \n",
" 99.980 | \n",
"
\n",
" \n",
" | 1990-12-20 | \n",
" 99.98 | \n",
" 104.300 | \n",
" 104.390 | \n",
" 99.980 | \n",
" 104.390 | \n",
"
\n",
" \n",
" | 1990-12-21 | \n",
" 104.39 | \n",
" 109.070 | \n",
" 109.130 | \n",
" 103.730 | \n",
" 109.130 | \n",
"
\n",
" \n",
" | 1990-12-24 | \n",
" 109.13 | \n",
" 113.570 | \n",
" 114.550 | \n",
" 109.130 | \n",
" 114.550 | \n",
"
\n",
" \n",
" | 1990-12-25 | \n",
" 114.55 | \n",
" 120.090 | \n",
" 120.250 | \n",
" 114.550 | \n",
" 120.250 | \n",
"
\n",
" \n",
" | ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
"
\n",
" \n",
" | 2026-09-24 | \n",
" 3936.52 | \n",
" 3925.323 | \n",
" 3930.496 | \n",
" 3888.374 | \n",
" 3888.374 | \n",
"
\n",
" \n",
" | 2026-09-28 | \n",
" 3888.374 | \n",
" 3878.409 | \n",
" 3878.409 | \n",
" 3806.671 | \n",
" 3823.621 | \n",
"
\n",
" \n",
" | 2026-09-29 | \n",
" 3823.621 | \n",
" 3816.154 | \n",
" 3843.838 | \n",
" 3810.812 | \n",
" 3830.451 | \n",
"
\n",
" \n",
" | 2026-09-30 | \n",
" 3830.451 | \n",
" 3839.253 | \n",
" 3851.217 | \n",
" 3833.086 | \n",
" 3842.195 | \n",
"
\n",
" \n",
" | 2026-10-08 | \n",
" 3842.195 | \n",
" 3838.977 | \n",
" 3864.118 | \n",
" 3795.374 | \n",
" 3811.904 | \n",
"
\n",
" \n",
"
\n",
"
8737 rows × 5 columns
\n",
"
"
],
"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": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Close | \n",
" Preclose | \n",
" Raw_return | \n",
" Log_return | \n",
"
\n",
" \n",
" | Day | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1995-01-03 | \n",
" 639.88 | \n",
" 647.87 | \n",
" -0.012333 | \n",
" -0.012409 | \n",
"
\n",
" \n",
" | 1995-01-04 | \n",
" 653.81 | \n",
" 639.88 | \n",
" 0.021770 | \n",
" 0.021536 | \n",
"
\n",
" \n",
" | 1995-01-05 | \n",
" 646.89 | \n",
" 653.81 | \n",
" -0.010584 | \n",
" -0.010641 | \n",
"
\n",
" \n",
" | 1995-01-06 | \n",
" 640.76 | \n",
" 646.89 | \n",
" -0.009476 | \n",
" -0.009521 | \n",
"
\n",
" \n",
" | 1995-01-09 | \n",
" 626.00 | \n",
" 640.76 | \n",
" -0.023035 | \n",
" -0.023305 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"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": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Close | \n",
" Weekly_raw | \n",
" Weekly_log | \n",
"
\n",
" \n",
" | Day | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 2020-01-03 | \n",
" 3083.79 | \n",
" NaN | \n",
" 0.010978 | \n",
"
\n",
" \n",
" | 2020-01-10 | \n",
" 3092.29 | \n",
" 0.002756 | \n",
" 0.002753 | \n",
"
\n",
" \n",
" | 2020-01-17 | \n",
" 3075.50 | \n",
" -0.005430 | \n",
" -0.005444 | \n",
"
\n",
" \n",
" | 2020-01-24 | \n",
" 2976.53 | \n",
" -0.032180 | \n",
" -0.032709 | \n",
"
\n",
" \n",
" | 2020-01-31 | \n",
" NaN | \n",
" NaN | \n",
" NaN | \n",
"
\n",
" \n",
"
\n",
"
"
],
"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": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" cumA | \n",
" cumB | \n",
" diff | \n",
"
\n",
" \n",
" | Day | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 2020-01-02 | \n",
" 0.011501 | \n",
" 0.011501 | \n",
" 6.661338e-16 | \n",
"
\n",
" \n",
" | 2020-12-31 | \n",
" 0.138667 | \n",
" 0.138667 | \n",
" -2.220446e-16 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"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": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" v | \n",
"
\n",
" \n",
" \n",
" \n",
" | A | \n",
" NaN | \n",
"
\n",
" \n",
" | B | \n",
" 12.0 | \n",
"
\n",
" \n",
" | C | \n",
" 23.0 | \n",
"
\n",
" \n",
" | D | \n",
" NaN | \n",
"
\n",
" \n",
"
\n",
"
"
],
"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": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" v | \n",
"
\n",
" \n",
" \n",
" \n",
" | A | \n",
" 1.0 | \n",
"
\n",
" \n",
" | B | \n",
" 12.0 | \n",
"
\n",
" \n",
" | C | \n",
" 23.0 | \n",
"
\n",
" \n",
" | D | \n",
" 30.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"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": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Raw_return | \n",
" l1 | \n",
" l_1 | \n",
"
\n",
" \n",
" | Day | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 2020-01-02 | \n",
" 0.011501 | \n",
" NaN | \n",
" -0.000457 | \n",
"
\n",
" \n",
" | 2020-01-03 | \n",
" -0.000457 | \n",
" 0.011501 | \n",
" -0.000123 | \n",
"
\n",
" \n",
" | 2020-01-06 | \n",
" -0.000123 | \n",
" -0.000457 | \n",
" 0.006937 | \n",
"
\n",
" \n",
" | 2020-01-07 | \n",
" 0.006937 | \n",
" -0.000123 | \n",
" -0.012210 | \n",
"
\n",
" \n",
" | 2020-01-08 | \n",
" -0.012210 | \n",
" 0.006937 | \n",
" 0.009127 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"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
}