一、引言
到此,我们已探讨完 Python 核心且重要的知识点,涵盖数据结构、流程控制、函数用法、类与对象,以及 Numpy、Pandas 等常用库。而在本次 “Python 进阶 2” 的内容中,我们将进一步介绍 Python 的其他高级用法与科学计算库 —— 不仅会讲解 Python 风格(Pythonic)及各类高级编程技巧,还会重点介绍量化领域常用的 SciPy 与 Statsmodels 科学计算库,为后续量化策略开发与数据分析夯实基础。
二、Python 高级用法
Pythonic
Pythonic 是一种编程风格,也是 Python 社区所推崇的编码哲学,强调代码应当 简洁、可读、自然且具有功能性。它并非语言本身的强制规范,而是一套约定俗成的最佳实践与编码理念,旨在让代码更加优雅、易于理解和维护。
简单来说,Pythonic 的代码就是符合 Python 风格与哲学的代码 —— 它不仅能够完成任务,还能以一种清晰、高效且“地道”的方式来实现。虽然遵循 Pythonic 并非绝对必要,但它能让你的代码更加自然、更具可读性,也更易于团队协作与长期维护。
下面为你展示使用Pythonic 风格示例:
除此之外,你可以参考下方的参考资料,进一步了解 Pythonic 风格的相关内容。
https://github.com/chenyz1984/FluentPython2ndCN
Assignment Expression
Python 的 赋值表达式(assignment expression,俗称海象运算符 :=) 完全符合 Pythonic 的理念 —— 它能让代码更简洁、可读性更强,还能避免重复计算。
我刚开始自学 Python 时,从未使用过海象运算符,这不仅导致我看他人代码时,在参数设定部分理解起来十分费劲,也让别人阅读我写的代码时面临同样的困惑。直到接触并学会使用海象表达式后,我阅读代码的效率显著提高,自己写出的代码可读性也得到了明显提升。
与传统赋值语句的核心区别
| 特性 | 赋值表达式 (:=) |
传统赋值语句 (=) |
|---|---|---|
| 使用场景 | 嵌套在表达式中(如条件、循环、推导式) | 作为独立语句使用 |
| 返回值 | 返回赋值的值 | 无返回值(语句不能作为表达式) |
| 绑定优先级 | 低(需括号辅助明确范围) | 最低(仅作为语句分隔) |
其他高级实用技术
除此之外,Python 中还有一系列支撑量化开发、数据处理等场景的实用技术与特性值得深入掌握:比如用于数据标准化呈现的格式化操作、实现文本高效匹配与提取的正则表达式、助力结构化数据解析的解析化工具、能节省内存空间的生成器、支持高效迭代遍历的迭代器、可简化代码逻辑并增强功能复用的装饰器、能自动完成资源(如文件、数据库连接)创建与释放的上下文管理器,以及提升程序运行效率的多进程 / 多线程技术,此外,用于批量获取外部数据(如金融市场公开信息)的爬虫技术,也在量化分析中扮演着重要角色,这些技术共同构成了 Python 进阶应用的模块。
但这些技术在实际代码中的具体运用,不会在这里展开详细解析;相关的免费学习资料可在网络上检索获取,大家可以自行参考学习。
Python学习资料:
https://python3-cookbook.readthedocs.io/zh-cn/latest/preface.html
https://funhacks.gitbooks.io/explore-python/content/
三、科学计算库
接下来我会介绍 SciPy 与 Statsmodels—— 这两个科学计算库在量化分析场景中应用十分广泛。
SciPy
SciPy 是 Scientific Python 的缩写,从其全称可看出它是 Python 里处理科学计算 (scientific computing) 的工具包,其官网地址是 https://www.scipy.org/。
在 SciPy中,通常人们更习惯给“SciPy + 子工具包”一起赋予个别名来专门处理一类问题,比如:
import scipy.interpolate as spi
import scipy.integrate as sci
import scipy.optimize as spo
import scipy.sparse as sp
总的来说,SciPy 的核心模块包括插值模块(interpolate)、积分模块(integrate)、优化模块(optimize)及稀疏矩阵模块(sparse)。这些模块分别用于数据插值估计、数值积分计算、优化问题求解与偏微分方程(PDE)相关计算,在金融工程与量化分析领域应用广泛。
SciPy 按功能划分为多个子模块:
| 子模块 | 核心功能 | 关键函数/方法/格式 | 金融应用场景及案例 |
|---|---|---|---|
scipy.interpolate |
通过已知数据点构建函数关系,实现一维/多维数据的内插(未知点在已知范围)与外推(未知点在已知范围外) | 1. 样条插值:splrep()(生成样条对象 tck)+ splev()(基于 tck 计算插值结果)2. 专用插值: interp1d()(一维插值,支持线性、三次样条等类型)、interp2d()(二维插值,适用于二维数据场景) |
1. 收益率曲线插值:补全非标准期限的利率数据 2. 远期利率计算:基于零息曲线的折现因子/零息利率,插出远期起始日与终止日的关键数据,代入公式推导远期利率 |
scipy.integrate |
数值方法求解函数定积分,解决无法通过解析法计算积分的问题 | 1. 关键方法及特点: - quad:自适应积分,精度高,适用于一般函数积分- fixed_quad:固定节点高斯积分,适用于平滑函数- romberg:龙贝格法,收敛效率高,适用于高精度需求场景 |
欧式期权定价:在 Black-Scholes 模型下,将期权价值表示为收益函数的积分形式,通过数值积分求解;同时可对比数值解与解析解的一致性,验证结果有效性 |
scipy.optimize |
求解函数最小化/最大化问题,支持无约束优化与带约束(等式/不等式)优化 | 1. 通用优化:minimize()(支持 SLSQP、BFGS 等多种算法,可设置约束条件)2. 全局搜索: brute()(通过网格遍历寻找全局最优解)3. 方程求解: root()(求解非线性方程) |
1. 风险平价模型:优化资产权重,使组合中各资产的风险贡献均衡,降低单一资产风险集中风险 2. 资产配置:基于预设风险预算,通过优化确定各类资产的配置权重 |
scipy.sparse |
高效存储和处理大型稀疏矩阵(减少内存占用),配合有限差分法求解偏微分方程(PDE) | 1. 稀疏矩阵格式: - CSR/CSC:压缩行/列格式,适合矩阵计算- DIA:对角线存储格式,适用于对角线元素非零的矩阵- LIL:列表嵌套格式,支持灵活增量构建矩阵 |
障碍期权定价:通过有限差分法离散 Black-Scholes 偏微分方程,结合稀疏矩阵高效求解;如双边敲出期权定价,需结合障碍处的边界条件(PaH 即时返还、PaE 到期返还)计算期权价值 |
1、处理插值
在 SciPy 里有个子工具包 scipy.interpolate 是用来插值的,给定一组 (𝑥𝑖,𝑦𝑖),其中 𝑖 =1,2,⋯,𝑛,而且 𝑥𝑖 是有序的,称为标准点。插值就是对于任何新点 𝑥_new,计算出对应的y_new。换句话来说,插值就是在标准点之间建立分段函数 (piecewise function),然后再把它们连起来。这样给定任意连续 𝑥 值,带入分段函数里就能计算出任意连续 𝑦 值。函数 splrep 和 splev 是一对的,前者将 𝑥,𝑦 和插值方式转换成「样条对象」tck,后者利用它在 𝑥_new 上生成 𝑦_new。
假设2025-10-10 这一天的人民币折现曲线如下,第一个点的日期是 2025-10-13,称为即期日,从第二个点开始的日期分别从即期日往后推 1W, 1M, 2M, 3M, 6M, 9M,1Y 和 2Y 得到的:
import pandas as pd
# 定义表格数据
data = {
"日期": [
"2025-10-13", "2025-10-20", "2025-11-13",
"2025-12-13", "2026-01-13", "2026-04-13",
"2026-07-13", "2026-10-13", "2027-10-13"
],
"折现因子": [
0.999905, 0.999565, 0.998324,
0.996434, 0.993978, 0.985531,
0.978066, 0.970684, 0.942104
],
"零息利率(%)": [
1.726744, 1.764049, 1.913496,
2.069661, 2.170469, 2.475450,
2.922324, 3.051187, 3.269799
]
}
# 创建 DataFrame
df = pd.DataFrame(data)
df["日期"] = pd.to_datetime(df["日期"])
# 显示 DataFrame
print(df)
远期利率 (forward rate) 是指从未来的某一时点到另一时点的利率。假设起息日是 2025-11-05,终止日是2026-11-05,远期利率的公式如下,
# 计算曲线上标准日期到估值日之间的天数差
from datetime import datetime
today = datetime.strptime('2025-10-10','%Y-%m-%d')
daydiff = df['日期'] - today
ddf = daydiff.dt.days.values
print(ddf) # [ 3 10 34 64 95 185 276 368 733]
# 计算起始日和终止日到估值日之间的天数差,以及远期利率年限
import scipy.interpolate as spi
import numpy as np
start = datetime.strptime('2025-11-05','%Y-%m-%d')
end = datetime.strptime('2026-11-05','%Y-%m-%d')
d_s = (start - today) .days
d_e = (end- today) .days
tau = (d_e - d_s) /360
print( d_s, d_e, tau )
# 在零息曲线上线性插值,再计算折现因子
tck = spi.splrep( ddf, df['零息利率(%)'], k=1 )
r_s = spi.splev( d_s, tck )
r_e = spi.splev( d_e, tck )
DF_s = np.exp(-d_s/365 * r_s/100)
DF_e = np.exp(-d_e/365 * r_e/100)
F = (1/tau) * (DF_s/DF_e -1) * 100
print( DF_s, DF_e, F )
# 26 391 1.0138888888888888
# 0.9986733276946635 0.9677002618532516 3.156842926727915
最终计算结果如下:起始日对应的折现因子为 0.9987,终止日对应的折现因子为 0.9677,据此求得的远期利率为 3.1568%。
2、数值积分
在 SciPy 库中,有一个专门用于数值积分的子模块 scipy.integrate;该子模块包含多个数值积分函数,下面以常用的 quad 函数为例进行说明。

import scipy.integrate as sci
f = lambda x: np.sin(x) + 0.5*x
(-np.cos(9.5) + 0.25 * 9.5**2) - (-np.cos(0.5) + 0.25 * 0.5**2)
a, b = 0.5, 9.5
sci.quad(f, a, b)[0]
# 24.374754718086752
3、运筹规划
优化问题可分为无约束优化(unconstrained optimization)和有约束优化(constrained optimization)两类。在 SciPy 库中,有一个专门用于解决优化问题的子模块 scipy.optimize。
我们以金融领域的案例为例进行说明:在股票、债券、信贷三类资产构成的投资组合中,求解风险平价(RP)模型下该组合的最优资产权重。
import scipy.optimize as spo
def risk_parity( sigma, rho ):
"""
基于scipy.optimize实现风险平价资产配置模型,求解使各资产风险贡献相等的最优权重
核心逻辑:通过最小化"资产实际风险贡献占比与目标占比的平方和",满足权重约束条件
对应文件章节:第二章 用SciPy玩转统筹规划 - 案例分析:资产配置(风险平价)
Parameters:
----------
sigma : 1维numpy数组
各类资产的波动率(如股票、债券、信贷的波动率),文件案例中曾用[0.2, 0.1, 0.15](股票20%、债券10%、信贷15%)
rho : 2维numpy数组
资产间的相关系数矩阵,维度为(n,n)(n为资产数量),文件案例中曾用3×3矩阵描述股票-债券-信贷的相关性
Returns:
-------
result : scipy.optimize.OptimizeResult对象
优化结果对象,包含最优权重(result.x)、目标函数值(result.fun)、优化状态(result.success)等信息
"""
# 1. 计算资产数量n(等于波动率数组的长度)
n = len(sigma)
# 2. 计算资产的协方差矩阵(VCV)
# np.outer(sigma, sigma):计算波动率的外积,得到(n,n)的波动率乘积矩阵
# 乘以相关系数矩阵rho:根据"协方差=波动率1×波动率2×相关系数",得到最终协方差矩阵
# 对应文件逻辑:风险平价模型中需用协方差矩阵衡量资产间风险关联
VCV = np.outer(sigma,sigma) * rho
# 3. 设定目标风险贡献占比s
# 风险平价核心:每类资产对组合风险的贡献占比相等,故s为全1数组除以资产数量n(如n=3时s=[1/3,1/3,1/3])
s = np.ones(n) / n
# 4. 设定优化初始权重w0
# 初始值设为均等权重(每类资产权重=1/n),文件案例中常用此初始值启动优化
w0 = np.ones(n) / n
# 5. 定义优化目标函数obj(匿名函数形式)
# 目标:最小化"各资产实际风险贡献占比与目标占比s的平方和",对应文件中风险平价的优化核心
# 公式拆解:
# np.dot(VCV, x):计算权重x下的资产边际风险贡献(MRC)相关项
# x*np.dot(VCV, x):计算各资产的总风险贡献(TRC = 权重x × 边际风险贡献MRC)
# np.dot(np.dot(x*sigma, rho), x*sigma):等价于x.T @ VCV @ x,计算组合方差(σ_p²)
# 整体分式:TRC / σ_p(组合风险)= 资产风险贡献占比,与目标s作差后平方求和,即需最小化的目标函数
obj = lambda x: np.sum( ( x*np.dot(VCV,x) / np.dot(np.dot(x*sigma,rho), x*sigma) - s)**2 )
# 6. 设定优化约束条件cons(元组形式,包含等式约束和不等式约束)
# 约束1:等式约束(type='eq')- 权重和为1
# 逻辑:资产配置中所有资产权重之和必须等于1(满仓配置,无现金留存),对应文件中"w^T1=1"的约束
# 约束2:不等式约束(type='ineq')- 权重非负
# 逻辑:不允许做空资产,权重x≥0,对应文件中"0≤w≤1"的约束(因权重和为1,单个权重最大为1)
cons = ( {'type': 'eq', 'fun': lambda x: np.sum(x) - 1.0},
{'type': 'ineq', 'fun': lambda x: x} )
# 7. 调用scipy.optimize.minimize求解带约束优化问题
# fun=obj:待最小化的目标函数
# x0=w0:初始权重(均等权重)
# method='SLSQP':优化算法,文件案例指定用序列二次规划(SLSQP),适合带约束的非线性优化
# constraints=cons:传入上述权重和为1、权重非负的约束
# tol=1e-10:收敛容忍度,控制优化精度(文件中常用1e-10确保结果稳定性)
result = spo.minimize( obj, w0, method='SLSQP', constraints=cons, tol=1e-10 )
# 8. 返回优化结果对象
# 结果中result.x为最优权重,对应文件案例中"股票19.12%、债券51.47%、信贷29.41%"的输出形式
return result
mu = np.array([0.1, 0.05, 0.1]) # 股票、债券、信贷的预期超额回报
sigma = np.array([0.2, 0.1, 0.15]) # 股票、债券、信贷波动率
rho = np.array([[1, -0.1, 0.3],
[-0.1, 1, -0.3],
[0.3, -0.3, 1]]) # 相关系数矩阵
result = risk_parity( sigma, rho )
result.x
# array([0.19117648, 0.5147059 , 0.29411762])
从上面的结果可知:股票、债券和信贷的最优权重为[19.12%, 51.47%, 29.41%]。
4、有限差分
在 SciPy 库中,有一个专门用于处理稀疏矩阵的子模块 scipy.sparse。与稠密矩阵(dense matrix)相比,稀疏矩阵(sparse matrix)的最大优势在于能节省用于存储大量零元素的内存空间 —— 稀疏矩阵本质上仍是矩阵,只是其绝大部分元素为零。
稀疏矩阵的存储机制有多种,下面以 “内嵌列表格式(List of Lists,简称 LIL)” 为例进行说明。
import scipy.sparse as sp
data = [[8,0,1,0,-1],[0,8,2,0,0],
[0,0,3,0,0],[-2,0,4,8,-2],
[0,0,5,0,8],[0,0,6,0,0]]
A = sp.lil_matrix(data)
A.shape # (6, 5)
A.toarray()
# array([[ 8, 0, 1, 0, -1],
# [ 0, 8, 2, 0, 0],
# [ 0, 0, 3, 0, 0],
# [-2, 0, 4, 8, -2],
# [ 0, 0, 5, 0, 8],
# [ 0, 0, 6, 0, 0]])
import numpy as np
import scipy.sparse as sp
from scipy.sparse.linalg import spsolve
def finite_difference_volatility():
# 场景:用有限差分法求解股票价格波动率的稳态扩散方程
# 简化模型:σ²(S) 满足 0.5*S²*σ''(S) + r*S*σ'(S) = 0(无风险利率r)
# 1. 参数设置
S_min = 10.0 # 最小股票价格
S_max = 100.0 # 最大股票价格
N = 20 # 空间网格点数
r = 0.03 # 无风险利率
dS = (S_max - S_min) / (N - 1) # 价格步长
S = np.linspace(S_min, S_max, N) # 价格网格
# 2. 初始化LIL格式稀疏矩阵(适合构建有限差分矩阵)
A = sp.lil_matrix((N, N), dtype=np.float64)
b = np.zeros(N) # 右端项
# 3. 边界条件(假设已知两端波动率)
sigma_min = 0.2 # S_min处波动率
sigma_max = 0.4 # S_max处波动率
A[0, 0] = 1.0
b[0] = sigma_min
A[-1, -1] = 1.0
b[-1] = sigma_max
# 4. 内部点的有限差分格式(二阶中心差分)
for i in range(1, N-1):
S_i = S[i]
# 系数计算(来自方程离散化)
a = 0.5 * (S_i**2) / (dS**2) - 0.5 * r * S_i / dS # 下对角线
b_i = - (S_i**2) / (dS**2) - r # 主对角线
c = 0.5 * (S_i**2) / (dS**2) + 0.5 * r * S_i / dS # 上对角线
A[i, i-1] = a
A[i, i] = b_i
A[i, i+1] = c
b[i] = 0.0 # 方程右端项为0
# 5. 转换为CSR格式并求解
A_csr = A.tocsr()
sigma = spsolve(A_csr, b)
# 6. 输出结果
print("股票价格 | 波动率")
print("-" * 20)
for s, sig in zip(S[::2], sigma[::2]): # 每隔一个点输出
print(f"{s:.1f} | {sig:.4f}")
# 运行示例
finite_difference_volatility()
Statsmodels
SciPy 库中包含大量可用于科学计算的函数,但在统计计算领域,statsmodels 模块可作为 SciPy 的延伸工具。该工具包最核心的功能涵盖统计检验与统计建模,具体包括线性回归、逻辑回归以及时间序列分析。
应用场景:股票收益归因、风险因子暴露分析、因子中性化处理
statsmodels 库在回归分析与因子模型构建中应用广泛,下面我以普通最小二乘法(OLS)为例,说明其在因子中性化处理中的具体用法。
import pandas as pd
import numpy as np
# 定义基础数据维度
dates = pd.date_range(start="2024-01-02", periods=3, freq="B") # 3个交易日
stock_codes = ["000001.SZ", "000002.SZ", "000004.SZ", "000005.SZ", "000006.SZ",
"600000.SH", "600004.SH", "600006.SH", "600007.SH", "600008.SH"] # 10个真实风格股票代码
# 生成多索引(日期+股票代码)的DataFrame
multi_index = pd.MultiIndex.from_product([dates, stock_codes], names=["日期", "股票代码"])
df_factor_market = pd.DataFrame(index=multi_index)
# 生成合理的因子值(模拟估值类因子,范围[-2, 2])
np.random.seed(2025) # 固定随机种子,结果可复现
df_factor_market["factor"] = np.random.normal(loc=0, scale=0.8, size=len(multi_index))
# 生成合理的市值(单位:亿元,范围[50, 1000],区分大盘/小盘股)
market_cap = np.random.lognormal(mean=np.log(200), sigma=0.8, size=len(multi_index))
df_factor_market["market_cap"] = np.round(market_cap, 2)
# 重置索引(便于查看,后续中性化函数可再转为多索引)
df_factor_market = df_factor_market.reset_index()
print("股票因子与市值数据(df_factor_market):")
print(df_factor_market.head()) # 打印前12行(3个日期的前4只股票)
# 定义股票与行业的映射关系(4个行业,每个行业2-3只股票)
industry_mapping = {
"股票代码": stock_codes,
"行业": [
"金融", "消费", "工业", "科技", "消费", # 前5只股票
"金融", "工业", "消费", "科技", "工业" # 后5只股票
]
}
df_industry = pd.DataFrame(industry_mapping)
# 按行业排序(便于查看分类)
df_industry = df_industry.sort_values("行业").reset_index(drop=True)
print("\n股票-行业映射数据(df_industry):")
print(df_industry)
import pandas as pd
import statsmodels.api as sm
from tqdm import tqdm
def neutralization(df, df_industry):
"""
对因子进行行业和市值中性化处理(OLS回归取残差)
参数:
df: DataFrame,包含字段['日期', '股票代码', 'factor', 'market_cap']
df_industry: DataFrame,包含字段['股票代码', '行业']
返回:
df_result: DataFrame,中性化后的因子值(行:日期,列:股票代码,值:残差)
"""
# 1. 数据预处理:统一索引并合并行业信息
# 将因子数据转为多索引(日期+股票代码)
df = df.set_index(['日期', '股票代码']).sort_index()
# 行业数据转为股票代码索引,生成行业虚拟变量(one-hot编码)
df_hy = pd.get_dummies(df_industry.set_index('股票代码'), prefix='行业')
# 2. 合并市值与行业数据(构造回归的自变量X)
# 按股票代码合并因子数据与行业虚拟变量
df_combined = df.join(df_hy, on='股票代码', how='left')
# 确保无缺失值(避免回归报错)
df_combined = df_combined.dropna()
# 3. 提取核心参数(日期、股票代码列表)
datetime_period = df_combined.index.get_level_values('日期').unique().sort_values()
order_book_ids = df_combined.index.get_level_values('股票代码').unique().sort_values()
# 4. 按日期循环进行OLS回归(取残差作为中性化因子)
df_result = pd.DataFrame(columns=order_book_ids, index=datetime_period)
for date in tqdm(datetime_period, desc="因子中性化处理"):
try:
# 提取当日数据
df_day = df_combined.loc[date]
# 自变量X:市值(market_cap)+ 行业虚拟变量(排除因子列和索引)
x_cols = ['market_cap'] + [col for col in df_hy.columns if col.startswith('行业_')]
X = df_day[x_cols].astype(float)
# 因变量y:原始因子值
y = df_day['factor'].astype(float)
# OLS回归(无截距项,已通过行业虚拟变量处理)
model = sm.OLS(y, X, hasconst=False, missing='drop')
residuals = model.fit().resid
# 将残差写入结果表(按股票代码匹配)
df_result.loc[date, residuals.index] = residuals
except Exception as e:
print(f"日期 {date} 处理失败: {str(e)}")
continue # 跳过异常日期
# 5. 结果整理
df_result = df_result.sort_index(axis=1) # 按股票代码排序
df_result = df_result.dropna(how='all', axis=1) # 删除全为空的股票列
df_result.index.name = 'datetime' # 统一索引名称
return df_result
# 假设已生成df_factor_market和df_industry(如前序代码)
neutralized_factor = neutralization(df_factor_market, df_industry)
print("中性化后的因子结果(部分):")
print(neutralized_factor.head()) # 打印前5行日期的结果
四、总结
我们已经系统学习了 Python 基础内容,以及此前两次分享的进阶知识。相信大家在消化这些内容后,已具备正式编写量化策略的能力。至此,“编程篇” 的核心内容就暂告一段落,后续若有需要补充的知识点,我会再通过新的分享同步给大家。下一次分享,我们将正式进入 “数理篇” 的学习。