diff --git a/02/02.ipynb b/02/02.ipynb
new file mode 100644
index 0000000..4252949
--- /dev/null
+++ b/02/02.ipynb
@@ -0,0 +1,1410 @@
+{
+ "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
+}