{ "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
PrecloseOpenHighestLowestClose
Day
1990-12-1996.05099.98095.79099.980
1990-12-2099.98104.300104.39099.980104.390
1990-12-21104.39109.070109.130103.730109.130
1990-12-24109.13113.570114.550109.130114.550
1990-12-25114.55120.090120.250114.550120.250
..................
2026-09-243936.523925.3233930.4963888.3743888.374
2026-09-283888.3743878.4093878.4093806.6713823.621
2026-09-293823.6213816.1543843.8383810.8123830.451
2026-09-303830.4513839.2533851.2173833.0863842.195
2026-10-083842.1953838.9773864.1183795.3743811.904
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ClosePrecloseRaw_returnLog_return
Day
1995-01-03639.88647.87-0.012333-0.012409
1995-01-04653.81639.880.0217700.021536
1995-01-05646.89653.81-0.010584-0.010641
1995-01-06640.76646.89-0.009476-0.009521
1995-01-09626.00640.76-0.023035-0.023305
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
CloseWeekly_rawWeekly_log
Day
2020-01-033083.79NaN0.010978
2020-01-103092.290.0027560.002753
2020-01-173075.50-0.005430-0.005444
2020-01-242976.53-0.032180-0.032709
2020-01-31NaNNaNNaN
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
cumAcumBdiff
Day
2020-01-020.0115010.0115016.661338e-16
2020-12-310.1386670.138667-2.220446e-16
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
v
ANaN
B12.0
C23.0
DNaN
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
v
A1.0
B12.0
C23.0
D30.0
\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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Raw_returnl1l_1
Day
2020-01-020.011501NaN-0.000457
2020-01-03-0.0004570.011501-0.000123
2020-01-06-0.000123-0.0004570.006937
2020-01-070.006937-0.000123-0.012210
2020-01-08-0.0122100.0069370.009127
\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 }