53  Pandas模块的统计计算

53.1 引言统计计算是数据分析的核心

统计学是从数据中提取知识的科学。在金融领域,统计计算帮助我们: - 量化风险: 计算波动率、VaR(在险价值) - 评估收益: 计算收益率、夏普比率 - 发现规律: 识别相关性、趋势、周期性 - 做出决策: 基于统计显著性的投资决策

Pandas提供了丰富的统计计算方法,结合NumPy的向量化运算,可以高效处理大规模金融数据。

53.2 描述性统计

53.2.1 数学基础

给定数据集 \(X = \{x_1, x_2, \ldots, x_n\}\):

集中趋势: - 均值(Mean): \(\bar{x} = \frac{1}{n}\sum_{i=1}^n x_i\) - 中位数(Median): 排序后位于中间位置的值 - 众数(Mode): 出现频率最高的值

离散程度: - 方差(Variance): \(\sigma^2 = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2\) - 标准差(Standard Deviation): \(\sigma = \sqrt{\sigma^2}\) - 极差(Range): \(\max(X) - \min(X)\)

分布形状: - 偏度(Skewness): 衡量分布的对称性 - 峰度(Kurtosis): 衡量分布的尖峰/厚尾程度

53.2.2 基础统计量计算

平台任务解答代码

以下代码与教学平台任务要求完全一致:

列表 53.1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
import pandas as pd
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
value_QDII = value_QDII.dropna() #删除缺失值所在行
(value_QDII/value_QDII.iloc[0]).plot(figsize=(8,6),grid=True) #将基金净值按首个交易日进行归一处理并可视化
plt.savefig("1.png")  # 保存图形至文件

#任务二
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
print(value_QDII.max())  #找出每只基金净值的最大值
print(value_QDII.min())  #找出每只基金净值的最小值
print(value_QDII.idxmax()) #最大值所在的索引值
print(value_QDII.idxmin()) #最小值所在的索引值


#任务三
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
value_QDII_diff = value_QDII.diff()  # 计算基金每日净值的变动金额
print(value_QDII_diff.head())  #查看前五行数据
print(value_QDII_diff.tail())  #查看后五行数据

#任务四
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII_pctchangel = value_QDII.pct_change() #直接使用函数pct_change计算基金每日净值百分比变动

value_QDII_pctchangel.head()  # 查看value_QDII_pctchangel前5行数据
value_QDII_pctchangel.tail()  # 查看value_QDII_pctchangel后5行数据
value_QDII_diff = value_QDII.diff()  # 计算差分值
value_QDII_pctchange2 = value_QDII_diff/value_QDII.shift(1) #运用任务三的结果计算基金每日净值百分比变动
print(value_QDII_pctchange2.head())  # 输出前几行数据
print(value_QDII_pctchange2.tail())  # 输出最后几行数据
列表 53.2
import pandas as pd
import numpy as np

# 创建股票收益率数据
np.random.seed(42)
returns_data = {
    '贵州茅台': np.random.normal(0.001, 0.02, 100),  # 日收益率
    '五粮液': np.random.normal(0.0008, 0.025, 100),
    '招商银行': np.random.normal(0.0005, 0.015, 100),
    '中国平安': np.random.normal(0.0006, 0.018, 100)
}

df_returns = pd.DataFrame(returns_data)

print('收益率数据(前10行):')
print(df_returns.head(10))

# 计算各项统计量
print('\n基础统计量:')
print(f'均值:\n{df_returns.mean()}')
print(f'\n中位数:\n{df_returns.median()}')
print(f'\n标准差:\n{df_returns.std()}')
print(f'\n方差:\n{df_returns.var()}')
print(f'\n最小值:\n{df_returns.min()}')
print(f'\n最大值:\n{df_returns.max()}')

# 一次性获取所有描述性统计
print('\n完整描述性统计:')
desc_stats = df_returns.describe()
print(desc_stats)

53.2.3 describe()方法详解

describe() 方法返回的统计量:

统计量 含义 公式
count 非缺失值数量 \(n_{\text{valid}}\)
mean 均值 \(\bar{x} = \frac{1}{n}\sum x_i\)
std 标准差 \(\sqrt{\frac{1}{n-1}\sum(x_i-\bar{x})^2}\)
min 最小值 \(\min(X)\)
25% 第一四分位数 \(Q_1 = P_{25}\)
50% 第二四分位数(中位数) \(Q_2 = P_{50}\)
75% 第三四分位数 \(Q_3 = P_{75}\)
max 最大值 \(\max(X)\)

53.2.4 分位数计算

列表 53.3
# 计算常用分位数
quantiles = [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
print('自定义分位数:')
print(df_returns.quantile(quantiles))

# 四分位距(IQR)
Q1 = df_returns.quantile(0.25)
Q3 = df_returns.quantile(0.75)
IQR = Q3 - Q1
print('\n四分位距(IQR):')
print(IQR)

# 识别异常值(超出1.5*IQR)
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print('\n异常值边界:')
print(f'下界:\n{lower_bound}')
print(f'\n上界:\n{upper_bound}')

# 检测异常值
outliers = (df_returns < lower_bound) | (df_returns > upper_bound)
print(f'\n异常值数量:')
print(outliers.sum())

异常值检测的数学原理:

箱线图规则(Boxplot Rule): - 正常值: \([Q_1 - 1.5 \times IQR, Q_3 + 1.5 \times IQR]\) - 温和异常值(Mild Outlier): 距离箱体 \(1.5-3 \times IQR\) - 极端异常值(Extreme Outlier): 距离箱体 \(> 3 \times IQR\)

Z-Score方法: \[ Z_i = \frac{x_i - \bar{x}}{\sigma} \] 通常认为 \(|Z| > 3\) 为异常值。

53.3 偏度与峰度

53.3.1 数学定义

偏度(Skewness): \[ \gamma_1 = \frac{E[(X-\mu)^3]}{\sigma^3} = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^3}{\left[\sqrt{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2}\right]^3} \]

峰度(Kurtosis): \[ \gamma_2 = \frac{E[(X-\mu)^4]}{\sigma^4} - 3 = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^4}{\left[\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2\right]^2} - 3 \]

53.3.2 金融意义

列表 53.4
# 计算偏度和峰度
skewness = df_returns.skew()
kurtosis = df_returns.kurtosis()

print('偏度(Skewness):')
print(skewness)
print('\n峰度(Kurtosis):')
print(kurtosis)

# 解释
print('\n解释:')
for stock in df_returns.columns:
    skew_val = skewness[stock]
    kurt_val = kurtosis[stock]

    # 偏度解释
    if skew_val > 0.5:
        skew_interp = '右偏(正偏),有较长右尾,极端正收益更多'
    elif skew_val < -0.5:
        skew_interp = '左偏(负偏),有较长左尾,极端负收益更多'
    else:
        skew_interp = '近似对称'

    # 峰度解释
    if kurt_val > 1:
        kurt_interp = '尖峰分布,有较多极端值(厚尾)'
    elif kurt_val < -1:
        kurt_interp = '低峰分布,较为平坦'
    else:
        kurt_interp = '接近正态分布'

    print(f'\n{stock}:')
    print(f'  偏度={skew_val:.3f}{skew_interp}')
    print(f'  峰度={kurt_val:.3f}{kurt_interp}')

金融应用:

  1. 正偏度:
    • 大多数时间小幅下跌
    • 偶尔出现大幅上涨(如牛市中的股票)
    • 符合投资者偏好(有限亏损,无限收益)
  2. 负偏度:
    • 大多数时间小幅上涨
    • 偶尔出现暴跌(如高杠杆资产)
    • 风险较高(黑天鹅事件)
  3. 高峰度:
    • 厚尾(Fat Tails): 极端事件发生的概率高于正态分布
    • 金融市场的典型特征
    • 风险管理需要考虑极端情况

正态分布检验:

如果数据完全服从正态分布: - 偏度 ≈ 0 - 峰度 ≈ 0(超额峰度) - Jarque-Bera检验: \(JB = \frac{n}{6}\left(\gamma_1^2 + \frac{\gamma_2^2}{4}\right)\)

53.4 累积统计量

列表 53.5
# 创建价格序列
prices = pd.DataFrame({
    '贵州茅台': [1850, 1860, 1855, 1870, 1865, 1880],
    '五粮液': [220, 218, 222, 225, 223, 226]
})

print('原始价格:')
print(prices)

# 累积和
print('\n累积和:')
print(prices.cumsum())

# 累积积
print('\n累积积:')
print(prices.cumprod())

# 累积最大值
print('\n累积最大值( expanding maximum):')
print(prices.cummax())

# 累积最小值
print('\n累积最小值( expanding minimum):')
print(prices.cummin())

# 金融应用:累计收益率
initial_prices = prices.iloc[0]
cum_returns = (prices / initial_prices - 1) * 100
print('\n累计收益率(%):')
print(cum_returns)

累积统计量的金融应用:

  1. cumsum: 累计收益、累计成交量
  2. cumprod: 复利增长(价格相对变化)
  3. cummax: 回撤分析(Drawdown)
  4. cummin: 历史最低价监控

53.4.1 回撤计算

列表 53.6
# 模拟净值曲线
np.random.seed(42)
nav = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=100),
    '净值': 1.0 + np.cumsum(np.random.normal(0.001, 0.02, 100))
})

# 计算历史最高点
nav['历史最高'] = nav['净值'].cummax()

# 计算回撤
nav['回撤'] = (nav['净值'] - nav['历史最高']) / nav['历史最高']

print('净值与回撤:')
print(nav.head(20))

# 最大回撤
max_drawdown = nav['回撤'].min()
print(f'\n最大回撤: {max_drawdown:.2%}')

# 可视化
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

# 净值曲线
ax1.plot(nav['日期'], nav['净值'], label='净值', linewidth=2)
ax1.plot(nav['日期'], nav['历史最高'], label='历史最高', linewidth=2, linestyle='--')
ax1.set_ylabel('净值')
ax1.set_title('净值曲线与回撤分析')
ax1.legend()
ax1.grid(True, alpha=0.3)

# 回撤曲线
ax2.fill_between(nav['日期'], nav['回撤'], 0, alpha=0.3, color='red')
ax2.plot(nav['日期'], nav['回撤'], color='red', linewidth=2)
ax2.set_ylabel('回撤率')
ax2.set_xlabel('日期')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

回撤的金融意义:

回撤(Drawdown):从历史最高点到当前点的下降幅度 \[ \text{Drawdown}_t = \frac{P_t - \max_{i \leq t} P_i}{\max_{i \leq t} P_i} \]

最大回撤(Maximum Drawdown, MDD): \[ \text{MDD} = \min_t \text{Drawdown}_t \]

最大回撤是衡量投资策略风险的关键指标: - MDD = -20%: 意味着历史上曾经从高点下跌20% - 心理影响: 投资者需要承受20%的亏损 - 恢复要求: 下跌20%需要上涨25%才能回本

53.5 窗口统计量滚动计算

53.5.1 数学原理

滚动窗口(Rolling Window):对于时间序列 \(\{x_t\}_{t=1}^T\),窗口大小为 \(w\):

\[ \text{RollingMean}_t = \frac{1}{w}\sum_{i=t-w+1}^t x_i \]

53.5.2 滚动统计量计算

列表 53.7
# 创建价格数据
dates = pd.date_range('2024-01-01', periods=100)
prices_ts = pd.DataFrame({
    '日期': dates,
    '收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 100))
})
prices_ts = prices_ts.set_index('日期')

print('原始价格:')
print(prices_ts.tail(10))

# 5日滚动均值
prices_ts['MA5'] = prices_ts['收盘价'].rolling(window=5).mean()

# 20日滚动均值
prices_ts['MA20'] = prices_ts['收盘价'].rolling(window=20).mean()

# 5日滚动标准差
prices_ts['STD5'] = prices_ts['收盘价'].rolling(window=5).std()

# 5日滚动最大值
prices_ts['MAX5'] = prices_ts['收盘价'].rolling(window=5).max()

print('\n滚动统计量:')
print(prices_ts.tail(10))

# 计算布林带
prices_ts['布林带_上'] = prices_ts['MA20'] + 2 * prices_ts['收盘价'].rolling(window=20).std()
prices_ts['布林带_下'] = prices_ts['MA20'] - 2 * prices_ts['收盘价'].rolling(window=20).std()

print('\n布林带:')
print(prices_ts[['收盘价', 'MA20', '布林带_上', '布林带_下']].tail(10))

金融技术指标:

  1. 移动平均(Moving Average, MA):平滑价格波动,识别趋势
  2. 布林带(Bollinger Bands):
    • 中轨: \(MA_{20}\)
    • 上轨: \(MA_{20} + 2\sigma\)
    • 下轨: \(MA_{20} - 2\sigma\)
    • 交易信号:价格触及上轨可能超买,触及下轨可能超卖

53.6 expanding窗口

列表 53.8
# expanding:从起点到当前点的累积计算
prices_ts['累积均值'] = prices_ts['收盘价'].expanding().mean()
prices_ts['累积标准差'] = prices_ts['收盘价'].expanding().std()
prices_ts['累积最大值'] = prices_ts['收盘价'].expanding().max()

print('扩展窗口统计:')
print(prices_ts[['收盘价', '累积均值', '累积标准差', '累积最大值']].tail(10))

# 金融应用:累计波动率
prices_ts['累计波动率'] = prices_ts['收盘价'].pct_change().expanding().std() * np.sqrt(252)
print('\n年化累计波动率:')
print(prices_ts['累计波动率'].tail(10))

rolling vs expanding:

特性 rolling expanding
窗口大小 固定 不断增长
计算范围 \([t-w+1, t]\) \([1, t]\)
权重 等权重 等权重
应用 移动平均、短期波动 累计收益、长期风险

53.7 分组统计 groupby

53.7.1 数学原理

分组聚合(GroupBy Aggregation): \[ \text{GroupBy}(K, f, X) = \{(k, f(\{x | key(x) = k\})) | k \in K\} \]

其中: - \(K\): 键集合 - \(f\): 聚合函数 - \(X\): 数据集

53.7.2 基础分组操作

列表 53.9
# 创建行业数据
industry_data = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ', '601398.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安', '平安银行', '工商银行'],
    '行业': ['白酒', '白酒', '银行', '保险', '银行', '银行'],
    '市盈率': [45.2, 35.8, 8.5, 12.3, 9.2, 6.8],
    '市净率': [12.3, 8.9, 0.9, 1.5, 1.1, 0.7],
    'ROE': [0.28, 0.22, 0.15, 0.18, 0.13, 0.14],
    '股息率': [0.012, 0.018, 0.035, 0.028, 0.040, 0.045]
})

print('行业数据:')
print(industry_data)

# 按行业分组计算均值
industry_mean = industry_data.groupby('行业').mean(numeric_only=True)
print('\n行业均值:')
print(industry_mean)

# 按行业分组计算多个统计量
industry_stats = industry_data.groupby('行业').agg({
    '市盈率': ['mean', 'median', 'std'],
    '市净率': ['mean', 'min', 'max'],
    'ROE': 'mean',
    '股息率': 'mean'
})
print('\n行业详细统计:')
print(industry_stats.round(4))

# 分组计数
industry_count = industry_data.groupby('行业').size()
print('\n行业股票数量:')
print(industry_count)

53.7.3 多级分组

列表 53.10
# 创建多级数据
multi_level_data = pd.DataFrame({
    '行业': ['白酒', '白酒', '白酒', '银行', '银行', '银行', '保险', '保险'],
    '市值等级': ['大盘', '中盘', '大盘', '大盘', '小盘', '大盘', '大盘', '中盘'],
    '市盈率': [45.2, 35.8, 38.5, 8.5, 15.2, 6.8, 12.3, 18.5],
    '收益率': [0.05, 0.03, 0.06, 0.02, 0.04, 0.01, 0.03, 0.02]
})

df_multi = pd.DataFrame(multi_level_data)

print('多级分组统计:')
# 多级分组
multi_stats = df_multi.groupby(['行业', '市值等级']).agg({
    '市盈率': 'mean',
    '收益率': 'mean'
})
print(multi_stats)

# 按行业统计,并排名
df_multi['行业内PE排名'] = df_multi.groupby('行业')['市盈率'].rank()
print('\n行业内PE排名:')
print(df_multi)

53.7.4 自定义聚合函数

列表 53.11
# 定义自定义函数:计算市净率与市盈率的比率均值
def price_to_book_ratio(group):
    '''计算市净率/市盈率比率'''
    return (group['市净率'] / group['市盈率']).mean()  # 按组计算PB/PE均值

# 定义自定义函数:基于ROE计算类夏普比率
def roe_adjusted_ratio(group, benchmark_roe=0.10):
    '''计算ROE超额收益与波动的比率'''
    excess_roe = group['ROE'].mean() - benchmark_roe  # 超额ROE
    return excess_roe / group['ROE'].std() if group['ROE'].std() > 0 else 0  # 类夏普比率

# 应用自定义函数
print('自定义聚合函数:')  # 输出标题
print('\n市净率/市盈率比率:')  # 输出PB/PE比率标题
print(industry_data.groupby('行业').apply(price_to_book_ratio))  # 按行业计算PB/PE比率

# 使用agg配合lambda计算市盈率范围
print('\n市盈率范围(最大-最小):')  # 输出PE范围标题
print(industry_data.groupby('行业')['市盈率'].agg(lambda x: x.max() - x.min()))  # 计算各行业PE极差

53.8 相关性分析

53.8.1 协方差与相关系数

协方差(Covariance): \[ \text{Cov}(X, Y) = E[(X - \mu_X)(Y - \mu_Y)] = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) \]

相关系数(Correlation Coefficient): \[ \rho_{X,Y} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} = \frac{\sum(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum(x_i - \bar{x})^2}\sqrt{\sum(y_i - \bar{y})^2}} \]

53.8.2 相关性计算

列表 53.12
# 使用之前的收益率数据
df_returns_sample = df_returns

# 计算协方差矩阵
cov_matrix = df_returns_sample.cov()
print('协方差矩阵:')
print(cov_matrix)

# 计算相关系数矩阵
corr_matrix = df_returns_sample.corr()
print('\n相关系数矩阵:')
print(corr_matrix)

# 找出相关性最高的股票对
corr_unstack = corr_matrix.unstack()
corr_unstack = corr_unstack[corr_unstack != 1]  # 排除自相关
top_corr = corr_unstack.abs().sort_values(ascending=False).head(5)
print('\n相关性最高的股票对:')
print(top_corr)

相关性的金融意义:

相关系数 关系 金融含义 投资策略
\(\rho \approx 1\) 强正相关 同涨同跌 分散化效果差
\(\rho \approx 0\) 无相关 独立变动 有效分散化
\(\rho \approx -1\) 强负相关 此消彼长 对冲工具

现代投资组合理论(MPT): \[ \sigma_p^2 = \sum_{i=1}^n \sum_{j=1}^n w_i w_j \sigma_i \sigma_j \rho_{ij} \]

\(\rho_{ij} < 1\) 时,组合方差小于各资产方差的加权平均,实现风险分散