series_data = pd.Series([4, 7, -5, 3])
series_data0 4
1 7
2 -5
3 3
dtype: int64
学习目标
通过本章学习,你应该能够:
pandas 是我们后续学习中的一个核心工具。它包含了强大的数据结构和数据处理工具,旨在使Python中的数据清洗和分析工作变得快速和便捷。
核心概念:量化计算在金融分析中的本质
在处理数以亿计的金融高频数据或面板数据时,传统的 Python for 循环由于其解释执行的特性,往往会成为性能瓶颈。在计算经济学中,我们倾向于使用“向量化”(Vectorization)技术。
其核心优势体现在两个层面: 1. 分摊解释器开销:Python 的动态类型检查在循环执行时会产生巨大的额外负担。向量化通过一次性将整组数据传递给底层由 C 或 Fortran 编写的高性能内核,将这种开销平摊到数百万个元素上。 2. 指令级并行 (SIMD):现代 CPU 具备“单指令多数据”(Single Instruction, Multiple Data)能力。Pandas 调用的底层库能够利用这些指令,在单次计算周期内处理多个浮点数,这对于计算波动率矩阵或大型投资组合的协方差具有决定性的加速作用。
在量化金融实务中,遵循“永远不要显式编写循环”这一信条,能够让你的分析程序在处理 A 股全市场历史行情时依然保持秒级反馈。
尽管pandas借鉴了NumPy的许多编程习惯,但两者最大的区别在于pandas是为处理表格型或异构数据而设计的。相比之下,NumPy更适合处理同质化的数值数组数据。
自2010年成为开源项目以来,pandas已发展成为一个相当庞大的库,适用于广泛的现实世界用例。其开发者社区已增长到超过2500名独立的贡献者,他们在解决日常数据问题的过程中,不断地为这个项目添砖加瓦。充满活力的pandas开发者和用户社区是其成功的关键部分。
在本书的其余部分,我将使用以下导入约定来引入NumPy和pandas:
因此,每当你在代码中看到pd.时,它都指向pandas。由于Series和DataFrame使用得非常频繁,将它们导入到本地命名空间可能会更方便:
索引的数学定义
从数学角度,pandas的索引是标签集合到值集合的映射。
给定: - 标签集合 \(L = \{l_1, l_2, ..., l_n\}\) - 值集合 \(V = \{v_1, v_2, ..., v_n\}\) - 值函数 \(f: L \to V\)
索引映射定义为: \[ \text{Index}: L \to V \] \[ l_i \mapsto v_i = f(l_i) \]
数据对齐 (Data Alignment)
给定两个Series: - \(S_1: L_1 \to V_1\)(标签到值的映射) - \(S_2: L_2 \to V_2\)
对齐运算定义为标签并集上的操作: \[ L = L_1 \cup L_2 \] \[ S_{\text{aligned}} = \{(l, v_1(l), v_2(l)) \mid l \in L \land v_1(l), v_2(l) \text{ are defined}\} \]
对于算术运算,缺失标签处的值被定义为\(\text{NaN}\)。
缺失值的数学表示
设 \(V\) 为值域(如实数集 \(\mathbb{R}\)),则扩展值域为: \[ V' = V \cup \{\text{NaN}\} \]
NaN具有以下性质: - \(\forall x \in V: x + \text{NaN} = \text{NaN}\) - \(\forall x \in V: x \times \text{NaN} = \text{NaN}\) - \(\text{NaN} \neq \text{NaN}\) (NaN不等于自身)
要开始使用pandas,你需要熟悉它的两个核心数据结构:Series和DataFrame。虽然它们并非所有问题的通用解决方案,但它们为各种数据任务提供了坚实、灵活的基础。
Series是一个一维的、类似数组的对象,它包含一个值序列(其类型与NumPy类型相似)和一个相关的数据标签数组,称为其索引 (index)。最简单的Series仅由一个数据数组构成:
在交互式环境中显示的Series的字符串表示形式,左侧是索引,右侧是值。由于我们没有为数据指定索引,系统会自动创建一个由0到N-1(其中N是数据长度)的整数组成的默认索引。你可以通过array和index属性分别获取Series的数组表示和索引对象:
.array属性的结果是一个PandasArray,它通常包装一个NumPy数组,但也可以包含特殊的扩展数组类型。
通常,你会希望创建一个带有索引的Series,用标签来标识每个数据点:
d 4
b 7
a -5
c 3
dtype: int64
与NumPy数组相比,你可以在选择单个或一组值时使用索引中的标签:
在这里,['c', 'a', 'd']被解释为一个索引列表,即使它包含的是字符串而不是整数。
使用NumPy函数或类似NumPy的操作,例如用布尔数组进行过滤、标量乘法或应用数学函数,都会保留索引与值之间的链接:
另一种理解Series的方式是,可以把它看作一个固定长度的、有序的字典,因为它是索引值到数据值的映射。它可以在许多你可能使用字典的场景中使用:
如果你有一个包含在Python字典中的数据,你可以通过传递这个字典来创建一个Series。在这里,我们用中国几个省市2020年的常住人口(单位:万人)作为例子:
北京 2189
上海 2487
广东 12601
浙江 6456
dtype: int64
一个Series可以通过其to_dict方法转换回字典:
当你只传递一个字典时,生成的Series中的索引将遵循字典keys方法的顺序,这取决于键的插入顺序。你可以通过传递一个带有你期望顺序的字典键的索引来覆盖这个默认行为:
技术细节:Python 字典顺序的一致性保障
在 Python 3.7+ 版本中,标准 dict 类型已保证了键的插入顺序。因此,在通过本地金融字典快速构建 Pandas Series 时,其索引将默认遵循键的添加顺序。但在早期版本或某些非官方 Python 构建中,字典是无序的。在金融科研领域,为了确保实验的可复现性,显式传递 index 参数指定顺序是专业量化分析师的“标准动作”。
浙江 6456.0
广东 12601.0
江苏 NaN
上海 2487.0
dtype: float64
在这里,sdata中找到的三个值被放在了相应的位置,但由于没有找到'江苏'的值,它显示为NaN(Not a Number),在pandas中用来标记缺失或NA值。由于'北京'没有包含在provinces列表中,它被从结果对象中排除了。
我将交替使用“缺失(missing)”、“NA”或“空值(null)”来指代缺失数据。应使用pandas中的isna和notna函数来检测缺失数据:
核心规则:NaN 的传播特性与级联失效风险
NaN(Not a Number)在 IEEE 754 标准中被定义。在金融数据清洗过程中,它扮演着重要的角色:它具有高度的“传染性”。任何包含 NaN 的算术运算,其结果都会坍缩为 NaN。
\[ x + \text{NaN} = \text{NaN}, \quad x \times \text{NaN} = \text{NaN} \]
这一设计的深层意义在于:量化系统中不允许“静默失败”。如果资产的某天收盘价缺失,计算累计收益率时如果不报错且不处理,系统会迫使整个序列失效,从而提示分析师去修补源数据,而不是给出一个看似正确但毫无意义的虚假结果。
Series本身也拥有这些作为实例方法:
对于许多应用来说,Series的一个有用特性是它在算术运算中会根据索引标签自动对齐:
数据对齐的特性将在后面更详细地讨论。如果你有数据库经验,可以将其视为类似于join(连接)操作。
Series对象本身及其索引都有一个name属性,这与其他pandas功能集成在一起:
province
浙江 6456.0
广东 12601.0
江苏 NaN
上海 2487.0
Name: population, dtype: float64
Series的索引可以通过赋值来原地修改:
DataFrame表示一个矩形的数据表格,它包含一个有序的、命名的列集合,每一列都可以是不同的值类型(数值、字符串、布尔值等)。DataFrame既有行索引也有列索引;可以把它看作是一个共享相同索引的Series的字典。
构建DataFrame有多种方式,但最常用的一种是从一个等长列表或NumPy数组组成的字典来构建:
生成的DataFrame的索引会自动分配,就像Series一样,并且列会根据data中键的顺序来排列(这取决于它们在字典中的插入顺序):
| province | year | gdp | |
|---|---|---|---|
| 0 | 广东 | 2010 | 4.60 |
| 1 | 广东 | 2015 | 7.30 |
| 2 | 广东 | 2020 | 11.07 |
| 3 | 浙江 | 2015 | 4.30 |
| 4 | 浙江 | 2020 | 6.46 |
| 5 | 浙江 | 2022 | 7.77 |
如 图 4.1 所示,如果你正在使用Jupyter Notebook或Quarto,pandas的DataFrame对象将会以一个更适合浏览器的HTML表格形式显示。
对于大型DataFrame,head方法只选择前五行:
| province | year | gdp | |
|---|---|---|---|
| 0 | 广东 | 2010 | 4.60 |
| 1 | 广东 | 2015 | 7.30 |
| 2 | 广东 | 2020 | 11.07 |
| 3 | 浙江 | 2015 | 4.30 |
| 4 | 浙江 | 2020 | 6.46 |
类似地,tail返回最后五行:
| province | year | gdp | |
|---|---|---|---|
| 1 | 广东 | 2015 | 7.30 |
| 2 | 广东 | 2020 | 11.07 |
| 3 | 浙江 | 2015 | 4.30 |
| 4 | 浙江 | 2020 | 6.46 |
| 5 | 浙江 | 2022 | 7.77 |
如果你指定一个列的序列,DataFrame的列将按照那个顺序排列:
| year | province | gdp | |
|---|---|---|---|
| 0 | 2010 | 广东 | 4.60 |
| 1 | 2015 | 广东 | 7.30 |
| 2 | 2020 | 广东 | 11.07 |
| 3 | 2015 | 浙江 | 4.30 |
| 4 | 2020 | 浙江 | 6.46 |
| 5 | 2022 | 浙江 | 7.77 |
如果你传递一个字典中不包含的列,它将在结果中以缺失值的形式出现:
| year | province | gdp | debt | |
|---|---|---|---|---|
| 0 | 2010 | 广东 | 4.60 | NaN |
| 1 | 2015 | 广东 | 7.30 | NaN |
| 2 | 2020 | 广东 | 11.07 | NaN |
| 3 | 2015 | 浙江 | 4.30 | NaN |
| 4 | 2020 | 浙江 | 6.46 | NaN |
| 5 | 2022 | 浙江 | 7.77 | NaN |
DataFrame中的一列可以像字典那样通过键来检索,也可以通过点属性表示法来检索,其结果是一个Series:
属性式访问(例如 province_df_with_debt.year)和在IPython/Jupyter中对列名的Tab补全是为了方便而提供的。province_df_with_debt[column]适用于任何列名,但province_df_with_debt.column仅在列名是有效的Python变量名且不与DataFrame的任何方法名冲突时才有效。例如,如果列名包含空格或下划线以外的符号,就不能使用点属性方法访问。
行也可以通过iloc和loc这两个特殊的属性按位置或名称来检索:
列可以通过赋值来修改。例如,空的debt列可以被赋一个标量值或一个值数组:
| year | province | gdp | debt | |
|---|---|---|---|---|
| 0 | 2010 | 广东 | 4.60 | 16.5 |
| 1 | 2015 | 广东 | 7.30 | 16.5 |
| 2 | 2020 | 广东 | 11.07 | 16.5 |
| 3 | 2015 | 浙江 | 4.30 | 16.5 |
| 4 | 2020 | 浙江 | 6.46 | 16.5 |
| 5 | 2022 | 浙江 | 7.77 | 16.5 |
| year | province | gdp | debt | |
|---|---|---|---|---|
| 0 | 2010 | 广东 | 4.60 | 0.0 |
| 1 | 2015 | 广东 | 7.30 | 1.0 |
| 2 | 2020 | 广东 | 11.07 | 2.0 |
| 3 | 2015 | 浙江 | 4.30 | 3.0 |
| 4 | 2020 | 浙江 | 6.46 | 4.0 |
| 5 | 2022 | 浙江 | 7.77 | 5.0 |
当你给一列赋列表或数组时,值的长度必须与DataFrame的长度相匹配。如果你赋一个Series,它的标签将精确地与DataFrame的索引重新对齐,任何不存在的索引值处都会插入缺失值:
| year | province | gdp | debt | |
|---|---|---|---|---|
| 0 | 2010 | 广东 | 4.60 | -1.2 |
| 1 | 2015 | 广东 | 7.30 | NaN |
| 2 | 2020 | 广东 | 11.07 | -1.5 |
| 3 | 2015 | 浙江 | 4.30 | NaN |
| 4 | 2020 | 浙江 | 6.46 | NaN |
| 5 | 2022 | 浙江 | 7.77 | -1.7 |
为一个不存在的列赋值将会创建一个新列。del关键字可以像删除字典键一样删除列。作为例子,我首先添加一个新列,其布尔值表示该省份是否为沿海经济发达地区:
| year | province | gdp | debt | coastal | |
|---|---|---|---|---|---|
| 0 | 2010 | 广东 | 4.60 | -1.2 | True |
| 1 | 2015 | 广东 | 7.30 | NaN | True |
| 2 | 2020 | 广东 | 11.07 | -1.5 | True |
| 3 | 2015 | 浙江 | 4.30 | NaN | True |
| 4 | 2020 | 浙江 | 6.46 | NaN | True |
| 5 | 2022 | 浙江 | 7.77 | -1.7 | True |
注意,不能使用 province_df_with_debt.coastal 这样的点属性表示法来创建新列。
然后可以使用del关键字来移除这一列:
Index(['year', 'province', 'gdp', 'debt'], dtype='object')
核心挑战:视图(View)与副本(Copy)的内存逻辑
在处理如 financial_statement.parquet 的千万级财务矩阵时,理解切片的底层操作至关重要。
严禁行为:链式索引赋值 类似 df_with_debt['debt'][2] = 5 的操作在 Pandas 中是不确定的,可能触发 SettingWithCopyWarning。在量化回测框架中,这种不确定性可能导致“未来函数”或逻辑错误。 最佳实践: 始终使用 .loc 进行显式、原子化的赋值: df_with_debt.loc[2, 'debt'] = 5
另一种常见的数据形式是嵌套的字典:
如果将嵌套字典传递给DataFrame,pandas会将外层字典的键解释为列,内层字典的键解释为行索引:
你可以使用与NumPy数组相似的语法来转置DataFrame(交换行和列):
内层字典的键被合并起来形成结果中的索引。如果指定了显式索引,则情况不同:
| 广东 | 浙江 | |
|---|---|---|
| 2015 | 10849.0 | 5539.0 |
| 2020 | 12601.0 | 6456.0 |
| 2025 | NaN | NaN |
Series组成的字典处理方式与此非常相似:
| 广东 | 浙江 | |
|---|---|---|
| 2010 | 10430 | 5442 |
| 2015 | 10849 | 5539 |
关于可以传递给DataFrame构造函数的多种数据类型,请参见 表 4.1。
| 类型 | 说明 |
|---|---|
| 二维ndarray | 数据矩阵,可选择传入行和列的标签 |
| 数组、列表或元组的字典 | 每个序列成为DataFrame的一列;所有序列必须等长 |
| NumPy结构化/记录数组 | 被视为“数组的字典”情况 |
| Series的字典 | 每个Series成为一列;如果没有显式传递索引,则每个Series的索引被合并以形成结果的行索引 |
| 字典的字典 | 每个内层字典成为一列;键被合并以形成行索引,同“Series的字典”情况 |
| 字典或Series的列表 | 每一项成为DataFrame的一行;字典键或Series索引的并集成为DataFrame的列标签 |
| 列表的列表或元组的列表 | 被视为“二维ndarray”情况 |
| 另一个DataFrame | 除非传递了不同的索引,否则使用该DataFrame的索引 |
| NumPy掩码数组 | 类似于“二维ndarray”情况,但掩码值在DataFrame结果中为缺失值 |
如果一个DataFrame的index和columns的name属性被设置了,它们也会被显示出来:
| province | 广东 | 浙江 |
|---|---|---|
| year | ||
| 2010 | 10430 | 5442 |
| 2015 | 10849 | 5539 |
| 2020 | 12601 | 6456 |
与Series不同,DataFrame没有name属性。DataFrame的to_numpy方法返回其包含的数据,作为一个二维ndarray:
如果DataFrame的列是不同的数据类型,返回数组的数据类型将会选择一个能够兼容所有列的类型:
pandas的Index对象负责持有轴标签(包括DataFrame的列名)和其他元数据(比如轴的名称)。你在构建Series或DataFrame时使用的任何数组或其他标签序列,都会在内部被转换为一个Index:
Index(['a', 'b', 'c'], dtype='object')
索引对象是不可变的 (immutable),因此不能被用户修改:
不可变性使得在数据结构之间共享Index对象更加安全:
0 1.5
1 -2.5
2 0.0
dtype: float64
除了类似数组,Index的行为也像一个固定大小的集合:
与Python的集合不同,pandas的Index可以包含重复的标签:
使用重复标签进行选择时,会选中该标签的所有出现。
每个Index都有许多用于集合逻辑的方法和属性,它们可以回答关于其所含数据的其他常见问题。一些有用的方法和属性总结在 表 4.2 中。
| 方法/属性 | 描述 |
|---|---|
append() |
与其他Index对象连接,生成一个新的Index |
difference() |
计算集合的差集,结果为一个Index |
intersection() |
计算集合的交集 |
union() |
计算集合的并集 |
isin() |
计算一个布尔数组,指示每个值是否包含在传入的集合中 |
delete() |
计算一个删除了位置i处元素的新Index |
drop() |
通过删除传入的值来计算一个新Index |
insert() |
通过在位置i插入元素来计算一个新Index |
is_monotonic_increasing |
如果每个元素都大于或等于前一个元素,则返回True |
is_unique |
如果Index没有重复值,则返回True |
unique() |
计算Index中唯一值的数组 |
本节将引导你了解与Series或DataFrame中数据进行交互的基本机制。在接下来的章节中,我们将使用pandas更深入地探讨数据分析和处理的主题。
pandas对象的一个重要方法是reindex,它意味着创建一个新对象,其值根据新的索引重新排列。看一个例子:
d 4.5
b 7.2
a -5.3
c 3.6
dtype: float64
对这个Series调用reindex会根据新的索引重新排列数据,如果某个索引值之前不存在,则引入缺失值:
a -5.3
b 7.2
c 3.6
d 4.5
e NaN
dtype: float64
对于像时间序列这样的有序数据,你可能希望在重建索引时进行一些插值或填充。method选项允许我们这样做,例如使用ffill方法,它会向前填充值:
0 blue
2 purple
4 yellow
dtype: object
0 blue
1 blue
2 purple
3 purple
4 yellow
5 yellow
dtype: object
对于DataFrame,reindex可以改变行索引、列索引,或两者都改变。当只传递一个序列时,它会对结果的行进行重建索引:
| 北京 | 上海 | 广东 | |
|---|---|---|---|
| a | 0 | 1 | 2 |
| c | 3 | 4 | 5 |
| d | 6 | 7 | 8 |
| 北京 | 上海 | 广东 | |
|---|---|---|---|
| a | 0.0 | 1.0 | 2.0 |
| b | NaN | NaN | NaN |
| c | 3.0 | 4.0 | 5.0 |
| d | 6.0 | 7.0 | 8.0 |
列可以通过columns关键字进行重建索引:
| 上海 | 浙江 | 广东 | |
|---|---|---|---|
| a | 1 | NaN | 2 |
| c | 4 | NaN | 5 |
| d | 7 | NaN | 8 |
因为'北京'不在provinces中,所以该列的数据从结果中被删除了。
你也可以使用loc操作符进行重建索引,前提是所有新的索引标签都已存在于DataFrame中。
关于reindex的参数的更多信息,请参见 表 4.3。
reindex函数参数
| 参数 | 描述 |
|---|---|
labels |
用作索引的新序列。可以是Index实例或任何其他类似序列的Python数据结构。 |
index, columns |
分别使用传入的序列作为新的行索引或列标签。 |
axis |
要重建索引的轴,'index'(行)或'columns'(列)。默认为'index'。 |
method |
插值(填充)方法;'ffill'向前填充,'bfill'向后填充。 |
fill_value |
在重建索引引入缺失数据时使用的替代值。 |
limit |
在向前或向后填充时,要填充的最大间隙大小(以元素数量计)。 |
tolerance |
在向前或向后填充时,对于非精确匹配要填充的最大间隙大小(以绝对数值距离计)。 |
level |
在MultiIndex的指定层级上匹配简单索引;否则选择子集。 |
copy |
如果为True,即使新索引与旧索引相同,也总是复制底层数据;如果为False,当索引相同时不复制数据。 |
如果你已经有一个不包含那些条目的索引数组或列表,那么从一个轴上删除一个或多个条目就很简单。drop方法将返回一个新对象,其中指定的值已从一个轴上被删除:
a 0.0
b 1.0
c 2.0
d 3.0
e 4.0
dtype: float64
对于DataFrame,可以从任一轴删除索引值。为了说明这一点,我们首先创建一个示例DataFrame:
| one | two | three | four | |
|---|---|---|---|---|
| 北京 | 0 | 1 | 2 | 3 |
| 上海 | 4 | 5 | 6 | 7 |
| 广东 | 8 | 9 | 10 | 11 |
| 浙江 | 12 | 13 | 14 | 15 |
用一个标签序列调用drop将从行标签(轴0)中删除值:
要从列中删除标签,可以使用columns关键字:
你也可以通过传递axis=1或axis='columns'来从列中删除值:
Series的索引(obj[...])工作方式类似于NumPy数组的索引,不同之处在于你可以使用Series的索引值而不仅仅是整数。
a 0.0
b 1.0
c 2.0
d 3.0
dtype: float64
虽然你可以通过标签来选择数据,但选择索引值的首选方法是使用特殊的loc操作符:
决策依据:整数索引的歧义性与标准化消除方案
对于初学者,整数索引是引发 Bug 的重灾区。Pandas 对此提供了严谨的区分:
.loc: 解析为语义标签。如果索引是 2, 0, 1,那么 loc[0] 寻找标签为 0 的那一行。.iloc: 解析为物理位置。无论标签是什么,iloc[0] 永远返回内存中的第一行。在金融数据管道(Pipeline)中,这种区分至关重要。例如,当你在处理某省份的年份序列时,2020 既是标签(年份)也可能是位置偏移(如果数据正好有两千多行)。 最佳实践: 永远弃用隐式 [] 索引进行行选择,强制使用 .loc 或 .iloc 来明晰你的业务逻辑或计算机逻辑。
例如,考虑一个带有整数索引的Series:
2 1
0 2
1 3
dtype: int64
在yield_series上使用[]索引将使用标签,而不是位置:
在stock_series上使用loc和整数会失败,因为它的索引不是基于整数的:
由于loc操作符完全按标签索引,因此还有一个iloc操作符,它完全按整数索引,无论索引是否包含整数,其工作方式都保持一致:
你也可以使用loc进行标签切片,但它的工作方式与普通的Python切片不同,即终点是包含在内的:
使用这些方法赋值会修改Series的相应部分:
对DataFrame进行索引会检索一个或多个列,可以使用单个值或序列:
| one | two | three | four | |
|---|---|---|---|---|
| 北京 | 0 | 1 | 2 | 3 |
| 上海 | 4 | 5 | 6 | 7 |
| 广东 | 8 | 9 | 10 | 11 |
| 浙江 | 12 | 13 | 14 | 15 |
这样的索引有几个特殊情况。第一种是切片或用布尔数组选择数据:
行选择语法matrix_df[:2]是为了方便而提供的。将单个元素或列表传递给[]操作符会选择列。
另一个用例是使用布尔DataFrame进行索引,例如由标量比较产生的DataFrame:
| one | two | three | four | |
|---|---|---|---|---|
| 北京 | True | True | True | True |
| 上海 | True | False | False | False |
| 广东 | False | False | False | False |
| 浙江 | False | False | False | False |
我们可以使用这个DataFrame将值为True的每个位置都赋为0:
| one | two | three | four | |
|---|---|---|---|---|
| 北京 | 0 | 0 | 0 | 0 |
| 上海 | 0 | 5 | 6 | 7 |
| 广东 | 8 | 9 | 10 | 11 |
| 浙江 | 12 | 13 | 14 | 15 |
与Series一样,DataFrame也具有用于基于标签和基于整数索引的特殊属性loc和iloc。作为第一个例子,让我们按标签选择单行:
要选择多行,传递一个标签序列:
你可以在loc中组合行和列的选择,用逗号分隔:
接下来我们用iloc进行一些类似的整数选择:
两个索引函数都支持切片,以及单个标签或标签列表:
布尔数组可以与loc一起使用,但不能与iloc一起使用:
表 4.4 提供了DataFrame索引选项的简短摘要。
| 类型 | 说明 |
|---|---|
df[column] |
从DataFrame中选择单列或列序列;特殊便利用法:布尔数组(过滤行)、切片(切片行)或布尔DataFrame(基于某些标准设置值) |
df.loc[rows] |
按标签从DataFrame中选择单行或行子集 |
df.loc[:, cols] |
按标签选择单列或列子集 |
df.loc[rows, cols] |
按标签同时选择行和列 |
df.iloc[rows] |
按整数位置从DataFrame中选择单行或行子集 |
df.iloc[:, cols] |
按整数位置选择单列或列子集 |
df.iloc[rows, cols] |
按整数位置同时选择行和列 |
df.at[row, col] |
按行和列标签选择单个标量值 |
df.iat[row, col] |
按行和列位置(整数)选择单个标量值 |
reindex 方法 |
按标签选择行或列 |
处理由整数索引的pandas对象可能会成为一个障碍。例如,你可能不会预料到以下代码会产生错误:
在这种情况下,pandas可以“回退”到整数索引,但在不引入细微错误的情况下,这在通常很难做到。这里我们有一个包含0、1和2的索引,但pandas不想猜测用户想要的是什么(基于标签的索引还是基于位置的)。
另一方面,对于非整数索引,就没有这样的歧义:
如果你的轴索引包含整数,数据选择将始终是面向标签的。如前所述,如果你使用loc(用于标签)或iloc(用于整数),你将得到你想要的结果:
另一方面,用整数切片总是面向位置的:
这些索引属性也可以用来原地修改DataFrame对象,但这需要一些小心。
| one | two | three | four | |
|---|---|---|---|---|
| 北京 | 1 | 0 | 0 | 0 |
| 上海 | 1 | 5 | 6 | 7 |
| 广东 | 1 | 9 | 10 | 11 |
| 浙江 | 1 | 13 | 14 | 15 |
| one | two | three | four | |
|---|---|---|---|---|
| 北京 | 1 | 0 | 0 | 0 |
| 上海 | 3 | 3 | 3 | 3 |
| 广东 | 5 | 5 | 5 | 5 |
| 浙江 | 3 | 3 | 3 | 3 |
pandas新手常犯的一个错误是在赋值时使用链式选择,像这样:data.loc[data.three == 5]['three'] = 6。
这可能会打印一个特殊的SettingWithCopyWarning警告,并且可能不会按预期工作。表达式data.loc[data.three == 5]可能返回一个视图或一个副本,所以你是在给一个临时对象赋值。解决方法是重写链式赋值,使用单个loc操作:
当你将对象相加时,如果任何索引对不相同,结果中的相应索引将是索引对的并集。
a 7.3
c -2.5
d 3.4
e 1.5
dtype: float64
内部数据对齐在不重叠的标签位置引入了缺失值。缺失值随后会在进一步的算术计算中传播。
对于DataFrame,对齐是在行和列上都进行的:
| b | c | d | |
|---|---|---|---|
| 北京 | 0.0 | 1.0 | 2.0 |
| 上海 | 3.0 | 4.0 | 5.0 |
| 广东 | 6.0 | 7.0 | 8.0 |
将它们相加返回一个DataFrame,其索引和列是每个DataFrame中索引和列的并集:
| b | c | d | e | |
|---|---|---|---|---|
| 上海 | 9.0 | NaN | 12.0 | NaN |
| 北京 | 3.0 | NaN | 6.0 | NaN |
| 广东 | NaN | NaN | NaN | NaN |
| 江苏 | NaN | NaN | NaN | NaN |
| 浙江 | NaN | NaN | NaN | NaN |
如果你将没有共同列或行标签的DataFrame对象相加,结果将全部是空值。
在不同索引对象之间的算术运算中,你可能希望当一个轴标签在一个对象中找到而另一个对象中没有时,用一个特殊值(如0)来填充。
| a | b | c | d | |
|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 2.0 | 3.0 |
| 1 | 4.0 | 5.0 | 6.0 | 7.0 |
| 2 | 8.0 | 9.0 | 10.0 | 11.0 |
| a | b | c | d | e | |
|---|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 |
| 1 | 5.0 | NaN | 7.0 | 8.0 | 9.0 |
| 2 | 10.0 | 11.0 | 12.0 | 13.0 | 14.0 |
| 3 | 15.0 | 16.0 | 17.0 | 18.0 | 19.0 |
将它们相加会导致在不重叠的位置出现缺失值:
| a | b | c | d | e | |
|---|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 4.0 | 6.0 | NaN |
| 1 | 9.0 | NaN | 13.0 | 15.0 | NaN |
| 2 | 18.0 | 20.0 | 22.0 | 24.0 | NaN |
| 3 | NaN | NaN | NaN | NaN | NaN |
使用df_a的add方法,我们传递df_b和一个fill_value参数:
| a | b | c | d | e | |
|---|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 4.0 | 6.0 | 4.0 |
| 1 | 9.0 | 5.0 | 13.0 | 15.0 | 9.0 |
| 2 | 18.0 | 20.0 | 22.0 | 24.0 | 14.0 |
| 3 | 15.0 | 16.0 | 17.0 | 18.0 | 19.0 |
表 4.5 列出了Series和DataFrame的算术方法。每个方法都有一个以字母r开头的对应方法,其参数是反转的。
| a | b | c | d | |
|---|---|---|---|---|
| 0 | inf | 1.000000 | 0.500000 | 0.333333 |
| 1 | 0.250 | 0.200000 | 0.166667 | 0.142857 |
| 2 | 0.125 | 0.111111 | 0.100000 | 0.090909 |
| a | b | c | d | |
|---|---|---|---|---|
| 0 | inf | 1.000000 | 0.500000 | 0.333333 |
| 1 | 0.250 | 0.200000 | 0.166667 | 0.142857 |
| 2 | 0.125 | 0.111111 | 0.100000 | 0.090909 |
相关地,在对Series或DataFrame进行重建索引时,你也可以指定一个不同的填充值:
| a | b | c | d | e | |
|---|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 2.0 | 3.0 | 0 |
| 1 | 4.0 | 5.0 | 6.0 | 7.0 | 0 |
| 2 | 8.0 | 9.0 | 10.0 | 11.0 | 0 |
| 方法 | 描述 |
|---|---|
add, radd |
加法 (+) 的方法 |
sub, rsub |
减法 (-) 的方法 |
div, rdiv |
除法 (/) 的方法 |
floordiv, rfloordiv |
整除 (//) 的方法 |
mul, rmul |
乘法 (*) 的方法 |
pow, rpow |
幂运算 (**) 的方法 |
DataFrame和Series之间的算术运算也是有定义的。首先,考虑一个二维数组和它的一行之间的差异:
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
当我们从matrix_data中减去matrix_data[0]时,这个减法操作会对每一行都执行一次。这被称为广播 (broadcasting)。DataFrame和Series之间的运算是相似的。
核心机制:广播(Broadcasting)的维度匹配逻辑
广播是 Pandas 能够处理多资产异构计算的关键。其底层遵循维度对齐原则:
(n,) 的 Series 会被拉伸至匹配 DataFrame 的 (m, n)。在量化金融中,这种机制常用于: - 去均值化: 从价格矩阵减去均值向量。 - 权重分配: 将权重向量广播至全样本时间序列。
通过显式指定 axis='index' (或 0),你可以将横向(列)对齐切换为纵向(行)对齐。这在计算单个资产对全市场的对冲比率时非常有用。
| b | d | e | |
|---|---|---|---|
| 浙江 | 0.0 | 1.0 | 2.0 |
| 北京 | 3.0 | 4.0 | 5.0 |
| 上海 | 6.0 | 7.0 | 8.0 |
| 江苏 | 9.0 | 10.0 | 11.0 |
默认情况下,DataFrame和Series之间的算术运算会将Series的索引与DataFrame的列进行匹配,并向下广播行:
如果在DataFrame的列或Series的索引中找不到某个索引值,对象将被重新索引以形成并集:
| b | d | e | f | |
|---|---|---|---|---|
| 浙江 | 0.0 | NaN | 3.0 | NaN |
| 北京 | 3.0 | NaN | 6.0 | NaN |
| 上海 | 6.0 | NaN | 9.0 | NaN |
| 江苏 | 9.0 | NaN | 12.0 | NaN |
如果你想在列上进行广播,匹配行,你必须使用其中一个算术方法并指定在索引上匹配(axis='index'):
| b | d | e | |
|---|---|---|---|
| 浙江 | 0.0 | 1.0 | 2.0 |
| 北京 | 3.0 | 4.0 | 5.0 |
| 上海 | 6.0 | 7.0 | 8.0 |
| 江苏 | 9.0 | 10.0 | 11.0 |
NumPy的通用函数(ufuncs,即逐元素的数组方法)也适用于pandas对象:
| b | d | e | |
|---|---|---|---|
| 浙江 | -0.184270 | 0.395068 | 0.313373 |
| 北京 | -1.022566 | -1.111296 | -0.945427 |
| 上海 | 0.759686 | -0.184394 | 1.109565 |
| 江苏 | -0.210289 | 1.349885 | -1.058606 |
| b | d | e | |
|---|---|---|---|
| 浙江 | 0.184270 | 0.395068 | 0.313373 |
| 北京 | 1.022566 | 1.111296 | 0.945427 |
| 上海 | 0.759686 | 0.184394 | 1.109565 |
| 江苏 | 0.210289 | 1.349885 | 1.058606 |
另外一个常见的操作是将一个作用于一维数组的函数应用到每一列或每一行。DataFrame的apply方法正是为此而生:
b 1.782252
d 2.461181
e 2.168171
dtype: float64
如果你向apply传递axis='columns',函数将对每一行调用一次。
浙江 0.579338
北京 0.165869
上海 1.293959
江苏 2.408491
dtype: float64
传递给apply的函数不一定返回一个标量值;它也可以返回一个包含多个值的Series:
| b | d | e | |
|---|---|---|---|
| min | -1.022566 | -1.111296 | -1.058606 |
| max | 0.759686 | 1.349885 | 1.109565 |
逐元素的Python函数也可以使用。假设你想从random_df中的每个浮点值计算一个格式化的字符串。你可以用applymap来做到这一点:
| b | d | e | |
|---|---|---|---|
| 浙江 | -0.18 | 0.40 | 0.31 |
| 北京 | -1.02 | -1.11 | -0.95 |
| 上海 | 0.76 | -0.18 | 1.11 |
| 江苏 | -0.21 | 1.35 | -1.06 |
applymap这个名字的原因是Series有一个map方法,用于应用一个逐元素的函数:
要按行或列标签进行字典序排序,使用sort_index方法,它返回一个新的、已排序的对象:
a 1
b 2
c 3
d 0
dtype: int64
对于DataFrame,你可以按任一轴的索引进行排序:
| d | a | b | c | |
|---|---|---|---|---|
| one | 4 | 5 | 6 | 7 |
| three | 0 | 1 | 2 | 3 |
数据默认按升序排序,但也可以按降序排序:
要按值对Series进行排序,使用其sort_values方法:
2 -3
3 2
0 4
1 7
dtype: int64
任何缺失值默认都会被排到Series的末尾:
4 -3.0
5 2.0
0 4.0
2 7.0
1 NaN
3 NaN
dtype: float64
通过使用na_position选项,缺失值可以被排到开头:
在对DataFrame进行排序时,你可以使用一列或多列中的数据作为排序键。
| b | a | |
|---|---|---|
| 0 | 4 | 0 |
| 1 | 7 | 1 |
| 2 | -3 | 0 |
| 3 | 2 | 1 |
要按多列排序,传递一个列名的列表:
排名(Ranking)会为数组中的有效数据点分配从1到N的排名。默认情况下,rank通过为每个组分配平均排名来处理平级关系:
0 6.5
1 1.0
2 6.5
3 4.5
4 3.0
5 2.0
6 4.5
dtype: float64
排名也可以根据它们在数据中出现的顺序来分配:
你也可以按降序排名:
DataFrame可以在行或列上计算排名。表 4.6 列出了排名的平级处理方法。
| b | a | c | |
|---|---|---|---|
| 0 | 4.3 | 0 | -2.0 |
| 1 | 7.0 | 1 | 5.0 |
| 2 | -3.0 | 0 | 8.0 |
| 3 | 2.0 | 1 | -2.5 |
rank的平级处理方法
| 方法 | 描述 |
|---|---|
'average' |
默认:为平级组中的每个条目分配平均排名 |
'min' |
为整个组使用最小排名 |
'max' |
为整个组使用最大排名 |
'first' |
按值在数据中出现的顺序分配排名 |
'dense' |
类似于method='min',但组与组之间的排名总是增加1 |
虽然许多pandas函数(如reindex)要求标签是唯一的,但这并不是强制性的。
a 0
a 1
b 2
b 3
c 4
dtype: int64
索引的is_unique属性可以告诉你其标签是否唯一:
索引一个有多个条目的标签会返回一个Series,而单个条目则返回一个标量值:
同样的逻辑也适用于在DataFrame中索引行:
| 0 | 1 | 2 | |
|---|---|---|---|
| a | 0.387104 | -0.153014 | -0.093033 |
| a | -0.261367 | 1.358692 | -0.181223 |
| b | -1.272501 | 0.081308 | -1.368653 |
| b | 0.217563 | -1.410481 | -1.273685 |
| c | 1.466898 | 0.431466 | -0.346921 |
pandas对象配备了一套常见的数学和统计方法。这些方法大多数属于归约 (reductions) 或汇总统计 (summary statistics) 的范畴。它们内置了对缺失数据的处理。
| one | two | |
|---|---|---|
| a | 1.40 | NaN |
| b | 7.10 | -4.5 |
| c | NaN | NaN |
| d | 0.75 | -1.3 |
调用DataFrame的sum方法会返回一个包含列总和的Series:
传递axis='columns'则会对行进行求和:
当整行或整列都包含NA值时,总和为0。这可以通过skipna选项(默认为True)来禁用,此时总和为NA:
一些聚合操作,如mean,至少需要一个非NA值才能产生有效结果:
表 4.7 列出了归约方法的常用选项。
| 方法 | 描述 |
|---|---|
axis |
进行归约的轴;'index'表示行,'columns'表示列 |
skipna |
排除缺失值;默认为True |
level |
如果轴是分层索引,则按层级进行分组归约 |
一些方法,如idxmin和idxmax,返回的是间接统计量,比如取得最小值或最大值的索引值:
其他方法是累积型的:
describe就是这样一个例子,它能一次性产生多个汇总统计量:
| one | two | |
|---|---|---|
| count | 3.000000 | 2.000000 |
| mean | 3.083333 | -2.900000 |
| std | 3.493685 | 2.262742 |
| min | 0.750000 | -4.500000 |
| 25% | 1.075000 | -3.700000 |
| 50% | 1.400000 | -2.900000 |
| 75% | 4.250000 | -2.100000 |
| max | 7.100000 | -1.300000 |
对于非数值数据,describe会产生另一种汇总统计量:
count 16
unique 3
top IT
freq 8
dtype: object
表 4.8 列出了一个完整的汇总统计和相关方法的集合。
| 方法 | 描述 |
|---|---|
count |
非NA值的数量 |
describe |
计算一组汇总统计量 |
min, max |
计算最小值和最大值 |
argmin, argmax |
计算取得最小值或最大值的索引位置(整数) |
idxmin, idxmax |
计算取得最小值或最大值的索引标签 |
quantile |
计算样本分位数,范围从0到1(默认:0.5) |
sum |
值的总和 |
mean |
值的平均值 |
median |
值的算术中位数(50%分位数) |
mad |
平均绝对离差 |
prod |
所有值的乘积 |
var |
样本方差 |
std |
样本标准差 |
skew |
样本偏度(三阶矩) |
kurt |
样本峰度(四阶矩) |
cumsum |
值的累积和 |
cummin, cummax |
值的累积最小值或最大值 |
cumprod |
值的累积积 |
diff |
计算一阶算术差分(对时间序列有用) |
pct_change |
计算百分比变化 |
皮尔逊相关系数的数学推导
在深入实践之前,让我们从数学角度理解皮尔逊相关系数的完整推导。
给定两个随机变量 \(X\) 和 \(Y\) 的 \(n\) 个观测值:\(\{(x_1, y_1), (x_2, y_2), ..., (x_n, y_n)\}\)
样本均值: \[ \bar{X} = \frac{1}{n}\sum_{i=1}^{n} x_i, \quad \bar{Y} = \frac{1}{n}\sum_{i=1}^{n} y_i \]
样本方差: \[ \text{Var}(X) = \frac{1}{n-1}\sum_{i=1}^{n}(x_i - \bar{X})^2 \] \[ \text{Var}(Y) = \frac{1}{n-1}\sum_{i=1}^{n}(y_i - \bar{Y})^2 \]
样本协方差: \[ \text{Cov}(X, Y) = \frac{1}{n-1}\sum_{i=1}^{n}(x_i - \bar{X})(y_i - \bar{Y}) \]
皮尔逊相关系数: \[ \rho_{X,Y} = \frac{\text{Cov}(X, Y)}{\sqrt{\text{Var}(X) \cdot \text{Var}(Y)}} \]
展开形式: \[ \rho_{X,Y} = \frac{\sum_{i=1}^{n}(x_i - \bar{X})(y_i - \bar{Y})}{\sqrt{\sum_{i=1}^{n}(x_i - \bar{X})^2 \cdot \sum_{i=1}^{n}(y_i - \bar{Y})^2}} \]
协方差矩阵的数学性质
对于 \(k\) 个随机向量 \(\mathbf{X} = (X_1, X_2, ..., X_k)^T\),协方差矩阵 \(\Sigma\) 定义为:
\[ \Sigma_{ij} = \text{Cov}(X_i, X_j) = E[(X_i - E[X_i])(X_j - E[X_j])] \]
协方差矩阵具有以下重要性质:
现在让我们使用来自本地 Parquet 数据仓库(基于 RQSDK 格式)的真实 A 股市场数据来计算这些统计量。我们将重点分析长三角地区的重要上市公司,包括宁波港 (601018) 和宁波银行 (002142),这些公司在区域经济发展中具有重要地位。
数学原理:协方差与相关系数的公理化定义
在量化投资中,理解资产间的互动逻辑是风险管理的基石。
1. 协方差 (Covariance) 衡量两个随机变量 \(R_i\) 和 \(R_j\) 如何共同变化。其样本形式为:
\[ \text{Cov}(R_i, R_j) = \frac{1}{n-1} \sum_{t=1}^{n} (R_{i,t} - \bar{R}_i)(R_{j,t} - \bar{R}_j) \]
2. 皮尔逊相关系数 (Pearson Correlation) 通过标准差对协方差进行归一化,将其约束在 \([-1, 1]\):
\[ \rho_{ij} = \frac{\text{Cov}(R_i, R_j)}{\sigma_i \sigma_j} \]
这一指标消除了量纲(如股价绝对值)的影响,使得我们可以直接比较贵州茅台与中证 1000 成分股的联动强度。在构建分散化投资组合时,寻找低相关性(\(\rho < 0.3\))甚至是负相关的资产是降低非系统性风险的核心手段。
import pandas as pd
import numpy as np
from pathlib import Path
# 读取已下载的股票数据
# 从本地 Parquet 文件读取数据代替 HDF5
ningbo_port = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '601018.XSHG')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
ningbo_bank = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '002142.XSHE')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
maotai = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '600519.XSHG')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
ping_an = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '601318.XSHG')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
# 提取收盘价并按交易日期对齐
def prepare_close_price(df, stock_name):
"""准备收盘价数据"""
temp = df[['trade_date', 'close']].copy()
temp.rename(columns={'close': stock_name}, inplace=True)
return temp
# 合并所有股票的收盘价数据
price_data = prepare_close_price(ningbo_port, '宁波港')
price_data = price_data.merge(
prepare_close_price(ningbo_bank, '宁波银行'),
on='trade_date', how='outer'
)
price_data = price_data.merge(
prepare_close_price(maotai, '贵州茅台'),
on='trade_date', how='outer'
)
price_data = price_data.merge(
prepare_close_price(ping_an, '中国平安'),
on='trade_date', how='outer'
)
# 按日期排序并删除包含缺失值的行
price_data = price_data.sort_values('trade_date').dropna()
# 设置日期为索引
price_data.set_index('trade_date', inplace=True)
price_data.head()| 宁波港 | 宁波银行 | 贵州茅台 | 中国平安 | |
|---|---|---|---|---|
| trade_date | ||||
| 2010-09-28 | 2.5505 | 4.9686 | 93.9212 | 16.8327 |
| 2010-09-29 | 2.4790 | 4.9515 | 91.9303 | 16.8558 |
| 2010-09-30 | 2.5362 | 5.1224 | 92.7938 | 17.4531 |
| 2010-10-08 | 2.5648 | 5.2463 | 95.4335 | 18.2022 |
| 2010-10-11 | 2.6576 | 5.4769 | 92.2823 | 19.2054 |
现在我们计算价格的百分比变化,这代表了每日收益率。在金融学中,收益率 \(R_t\) 通常定义为:
\[ R_t = \frac{P_t - P_{t-1}}{P_{t-1}} = \frac{P_t}{P_{t-1}} - 1 \]
其中 \(P_t\) 是第 \(t\) 期的价格。这种计算方法在时间序列分析中是一个基本操作。
| 宁波港 | 宁波银行 | 贵州茅台 | 中国平安 | |
|---|---|---|---|---|
| trade_date | ||||
| 2025-12-25 | 0.005450 | -0.002115 | 0.009472 | 0.025641 |
| 2025-12-26 | -0.005420 | -0.010950 | -0.000028 | 0.005085 |
| 2025-12-29 | -0.005450 | 0.002143 | -0.008578 | -0.017988 |
| 2025-12-30 | -0.005479 | 0.004277 | -0.008759 | -0.015455 |
| 2025-12-31 | 0.000000 | -0.003194 | -0.009023 | -0.005814 |
Series 的 corr 方法计算两个序列之间重叠的、非 NA 的、按索引对齐的值的相关性。类似地,cov 计算协方差。让我们计算宁波港与宁波银行之间的相关性和协方差:
实证分析:宁波港与宁波银行的联动解读
宁波港作为港口物流企业(周期性与贸易敏感型),宁波银行作为区域性商业银行(金融顺周期型),两者的正相关性反映了微观层面上实体经济活跃度与资金流动性的共震。在量化策略中,这种适度的相关性(通常在 0.3-0.6)意味着单一行业的冲击无法完全解释两者的波动,从而为区域配置策略提供了分散化空间。
DataFrame 的 corr 和 cov 方法分别返回一个完整的相关性矩阵或协方差矩阵,形式为 DataFrame。这个矩阵在现代投资组合理论中具有至关重要的地位,因为它是计算投资组合方差的基础。
投资组合方差的数学原理:
对于一个包含 \(n\) 个资产的投资组合,权重向量为 \(w = (w_1, w_2, ..., w_n)^T\),投资组合收益率为:
\[ R_p = \sum_{i=1}^{n} w_i R_i = w^T R \]
投资组合方差为:
\[ \sigma_p^2 = \text{Var}(R_p) = \text{Var}\left(\sum_{i=1}^{n} w_i R_i\right) = \sum_{i=1}^{n}\sum_{j=1}^{n} w_i w_j \text{Cov}(R_i, R_j) = w^T \Sigma w \]
其中 \(\Sigma\) 是 \(n \times n\) 的协方差矩阵。这正是我们需要计算协方差矩阵的原因!
相关性矩阵:
宁波港 宁波银行 贵州茅台 中国平安
宁波港 1.000000 0.313899 0.226149 0.312377
宁波银行 0.313899 1.000000 0.353712 0.625816
贵州茅台 0.226149 0.353712 1.000000 0.436055
中国平安 0.312377 0.625816 0.436055 1.000000
协方差矩阵:
宁波港 宁波银行 贵州茅台 中国平安
宁波港 0.000353 0.000124 0.000080 0.000111
宁波银行 0.000124 0.000442 0.000141 0.000250
贵州茅台 0.000080 0.000141 0.000358 0.000156
中国平安 0.000111 0.000250 0.000156 0.000360
金融工程应用:相关性矩阵的多重意义
我们的计算展示了长三角龙头企业与白马股(贵州茅台)的联动差异,这为构建 beta 中性或行业中性策略提供了基础。
使用 DataFrame 的 corrwith 方法,你可以计算一个 DataFrame 的列或行与另一个 Series 或 DataFrame 之间的成对相关性。例如,我们可以计算所有股票与贵州茅台收益率的相关性:
另一类相关方法用于提取关于一维Series中所含值的信息。
unique给你一个Series中唯一值的数组:
value_counts计算一个包含值频率的Series,并按降序排序:
isin执行一个向量化的集合成员资格检查:
0 True
1 False
2 False
3 False
4 False
5 True
6 True
7 True
8 True
dtype: bool
与isin相关的是Index.get_indexer方法,它给你一个从可能非唯一值的数组到另一个唯一值数组的索引数组:
array([0, 2, 1, 1, 0, 2])
表 4.9 提供了这些方法的参考。
| 方法 | 描述 |
|---|---|
isin |
计算一个布尔数组,指示每个值是否包含在传递的值序列中 |
get_indexer |
为一个数组中的每个值计算到另一个唯一值数组的整数索引;有助于数据对齐和连接类型的操作 |
unique |
计算Series中的唯一值数组,按观察到的顺序返回 |
value_counts |
返回一个Series,其索引为唯一值,值为频率,按计数降序排列 |
要在DataFrame中的多个相关列上计算直方图(例如处理问卷调查数据):
| 问题1 | 问题2 | 问题3 | |
|---|---|---|---|
| 0 | 1 | 2 | 1 |
| 1 | 3 | 3 | 5 |
| 2 | 4 | 1 | 2 |
| 3 | 3 | 2 | 4 |
| 4 | 4 | 3 | 4 |
要计算所有列的值计数,将pd.value_counts传递给apply方法:
| 问题1 | 问题2 | 问题3 | |
|---|---|---|---|
| 1 | 1.0 | 1.0 | 1.0 |
| 2 | 0.0 | 2.0 | 1.0 |
| 3 | 2.0 | 2.0 | 0.0 |
| 4 | 2.0 | 0.0 | 2.0 |
| 5 | 0.0 | 0.0 | 1.0 |
还有一个DataFrame.value_counts方法,但它将DataFrame的每一行视为一个元组来计算每个不同行的出现次数。
习题 5.1: Series 创建与索引
创建一个表示中国四个直辖市2023年GDP(单位:万亿元)的Series: - 北京:4.38 - 上海:4.72 - 天津:1.67 - 重庆:3.01
解答:
import pandas as pd
# (a) 创建Series
gdp_data = {
'北京': 4.38,
'上海': 4.72,
'天津': 1.67,
'重庆': 3.01
}
gdp_series = pd.Series(gdp_data)
print('(a) 创建的Series:')
print(gdp_series)
# (b) 添加description属性
gdp_series.description = '2023年GDP'
print(f'\n(b) description属性: {gdp_series.description}')
# (c) 转换为字典
gdp_dict = gdp_series.to_dict()
print(f'\n(c) 转换为字典: {gdp_dict}')
# (d) 筛选GDP大于3的城市
large_cities = gdp_series[gdp_series > 3.0]
print('\n(d) GDP大于3万亿元的城市:')
print(large_cities)(a) 创建的Series:
北京 4.38
上海 4.72
天津 1.67
重庆 3.01
dtype: float64
(b) description属性: 2023年GDP
(c) 转换为字典: {'北京': 4.38, '上海': 4.72, '天津': 1.67, '重庆': 3.01}
(d) GDP大于3万亿元的城市:
北京 4.38
上海 4.72
重庆 3.01
dtype: float64
习题 5.2: DataFrame 基础操作
创建一个DataFrame,包含以下信息:
| 省份 | 年份 | GDP(万亿元) | 人口(万人) |
|---|---|---|---|
| 广东 | 2020 | 11.07 | 12601 |
| 广东 | 2021 | 12.44 | 12684 |
| 浙江 | 2020 | 6.46 | 6456 |
| 浙江 | 2021 | 7.35 | 6540 |
解答:
import pandas as pd
# (a) 创建DataFrame
data = {
'省份': ['广东', '广东', '浙江', '浙江'],
'年份': [2020, 2021, 2020, 2021],
'GDP': [11.07, 12.44, 6.46, 7.35],
'人口': [12601, 12684, 6456, 6540]
}
df = pd.DataFrame(data)
print('(a) 创建的DataFrame:')
print(df)
# (b) 计算人均GDP(注意单位转换:GDP万亿元=10000亿元,人口万人)
df['人均GDP_万元'] = (df['GDP'] * 10000) / df['人口']
print('\n(b) 添加人均GDP列:')
print(df)
# (c) 按年份分组
avg_gdp_by_year = df.groupby('年份')['GDP'].mean()
print('\n(c) 按年份分组的平均GDP:')
print(avg_gdp_by_year)
# (d) 按省份分组
total_gdp_by_province = df.groupby('省份')['GDP'].sum()
print('\n(d) 按省份分组的GDP总和:')
print(total_gdp_by_province)(a) 创建的DataFrame:
省份 年份 GDP 人口
0 广东 2020 11.07 12601
1 广东 2021 12.44 12684
2 浙江 2020 6.46 6456
3 浙江 2021 7.35 6540
(b) 添加人均GDP列:
省份 年份 GDP 人口 人均GDP_万元
0 广东 2020 11.07 12601 8.785017
1 广东 2021 12.44 12684 9.807632
2 浙江 2020 6.46 6456 10.006196
3 浙江 2021 7.35 6540 11.238532
(c) 按年份分组的平均GDP:
年份
2020 8.765
2021 9.895
Name: GDP, dtype: float64
(d) 按省份分组的GDP总和:
省份
广东 23.51
浙江 13.81
Name: GDP, dtype: float64
习题 5.3: 数据对齐与缺失值
考虑以下两个Series:
解答:
import pandas as pd
import numpy as np
s1 = pd.Series([10, 20, 30], index=['a', 'b', 'c'])
s2 = pd.Series([5, 15, 25, 35], index=['a', 'c', 'd', 'e'])
# (a) 直接相加
result_a = s1 + s2
print('(a) s1 + s2:')
print(result_a)
print('说明:索引不匹配的值为NaN,这是pandas的数据对齐特性')
# (b) 使用fill_value
result_b = s1.add(s2, fill_value=0)
print('\n(b) 使用fill_value=0:')
print(result_b)
# (c) 重建索引并前向填充
result_c = s1.reindex(s2.index, method='ffill')
print('\n(c) s1重建索引(ffill):')
print(result_c)(a) s1 + s2:
a 15.0
b NaN
c 45.0
d NaN
e NaN
dtype: float64
说明:索引不匹配的值为NaN,这是pandas的数据对齐特性
(b) 使用fill_value=0:
a 15.0
b 20.0
c 45.0
d 25.0
e 35.0
dtype: float64
(c) s1重建索引(ffill):
a 10
c 30
d 30
e 30
dtype: int64
习题 5.4: 索引与选择
给定以下DataFrame:
解答:
import pandas as pd
import numpy as np
data = pd.DataFrame(
np.arange(16).reshape((4, 4)),
index=['北京', '上海', '广东', '浙江'],
columns=['one', 'two', 'three', 'four']
)
print('原始DataFrame:')
print(data)
# (a) 选择'two'列
col_two = data['two']
print('\n(a) two列:')
print(col_two)
# (b) 选择特定行
rows = data.loc[['上海', '浙江']]
print('\n(b) 上海和浙江行:')
print(rows)
# (c) 使用loc选择特定行和列
selected = data.loc[['上海', '浙江'], ['one', 'three']]
print('\n(c) 上海和浙江的one和three列:')
print(selected)
# (d) 使用iloc选择前两行前两列
subset = data.iloc[:2, :2]
print('\n(d) 前两行前两列:')
print(subset)
# (e) 条件选择
filtered = data[data['three'] > 5]
print('\n(e) three列大于5的行:')
print(filtered)原始DataFrame:
one two three four
北京 0 1 2 3
上海 4 5 6 7
广东 8 9 10 11
浙江 12 13 14 15
(a) two列:
北京 1
上海 5
广东 9
浙江 13
Name: two, dtype: int64
(b) 上海和浙江行:
one two three four
上海 4 5 6 7
浙江 12 13 14 15
(c) 上海和浙江的one和three列:
one three
上海 4 6
浙江 12 14
(d) 前两行前两列:
one two
北京 0 1
上海 4 5
(e) three列大于5的行:
one two three four
上海 4 5 6 7
广东 8 9 10 11
浙江 12 13 14 15
习题 5.5: 函数应用与映射
给定以下包含学生成绩的DataFrame:
解答:
import pandas as pd
import numpy as np
grades = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六'],
'数学': [85, 92, 78, 88],
'英语': [90, 85, 92, 80],
'物理': [82, 88, 85, 90]
})
print('原始成绩表:')
print(grades)
# (a) 计算总分和平均分
grades['总分'] = grades[['数学', '英语', '物理']].sum(axis=1)
grades['平均分'] = grades[['数学', '英语', '物理']].mean(axis=1)
print('\n(a) 添加总分和平均分:')
print(grades)
# (b) 计算每门课的统计量
subject_stats = grades[['数学', '英语', '物理']].agg(['mean', 'std'])
print('\n(b) 各科平均分和标准差:')
print(subject_stats)
# (c) 转换为等级
def score_to_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
else:
return 'D'
# 对所有成绩列应用转换
grade_columns = ['数学', '英语', '物理']
for col in grade_columns:
grades[f'{col}_等级'] = grades[col].apply(score_to_grade)
print('\n(c) 转换为等级:')
print(grades)
# (d) 找出每门课的最高分学生
print('\n(d) 各科最高分学生:')
for subject in grade_columns:
max_score = grades[subject].max()
top_student = grades[grades[subject] == max_score]['姓名'].values[0]
print(f'{subject}: {top_student} ({max_score}分)')原始成绩表:
姓名 数学 英语 物理
0 张三 85 90 82
1 李四 92 85 88
2 王五 78 92 85
3 赵六 88 80 90
(a) 添加总分和平均分:
姓名 数学 英语 物理 总分 平均分
0 张三 85 90 82 257 85.666667
1 李四 92 85 88 265 88.333333
2 王五 78 92 85 255 85.000000
3 赵六 88 80 90 258 86.000000
(b) 各科平均分和标准差:
数学 英语 物理
mean 85.750000 86.750000 86.25
std 5.909033 5.377422 3.50
(c) 转换为等级:
姓名 数学 英语 物理 总分 平均分 数学_等级 英语_等级 物理_等级
0 张三 85 90 82 257 85.666667 B A B
1 李四 92 85 88 265 88.333333 A B B
2 王五 78 92 85 255 85.000000 C A B
3 赵六 88 80 90 258 86.000000 B B A
(d) 各科最高分学生:
数学: 李四 (92分)
英语: 王五 (92分)
物理: 赵六 (90分)
习题 5.6: 股票收益率分析
假设你已经获取了宁波港(601018)和宁波银行(002142)的股票价格数据。
解答:
import pandas as pd
import numpy as np
from pathlib import Path
# 读取数据
# 从本地 Parquet 文件读取数据代替 HDF5
ningbo_port = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '601018.XSHG')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
ningbo_bank = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '002142.XSHE')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
# 准备数据
def prepare_stock_data(df, name):
"""准备股票数据"""
temp = df[['trade_date', 'close']].copy()
temp.rename(columns={'close': name}, inplace=True)
temp['trade_date'] = pd.to_datetime(temp['trade_date'], format='%Y%m%d')
return temp
port_data = prepare_stock_data(ningbo_port, '宁波港')
bank_data = prepare_stock_data(ningbo_bank, '宁波银行')
# 合并数据
price_data = port_data.merge(bank_data, on='trade_date', how='outer')
price_data = price_data.sort_values('trade_date').dropna()
price_data.set_index('trade_date', inplace=True)
# (a) 计算收益率
# 日收益率
daily_returns = price_data.pct_change()
# 月收益率(假设每月约21个交易日)
monthly_returns = price_data.resample('M').last().pct_change()
# 年收益率
annual_returns = price_data.resample('Y').last().pct_change()
print('(a) 收益率统计:')
print(f'\n宁波港日收益率均值: {daily_returns["宁波港"].mean():.4f}')
print(f'宁波银行日收益率均值: {daily_returns["宁波银行"].mean():.4f}')
print(f'\n宁波港月收益率均值: {monthly_returns["宁波港"].mean():.4f}')
print(f'宁波银行月收益率均值: {monthly_returns["宁波银行"].mean():.4f}')
# (b) 计算波动率(年化)
daily_vol_port = daily_returns['宁波港'].std()
daily_vol_bank = daily_returns['宁波银行'].std()
# 年化波动率(假设252个交易日)
annual_vol_port = daily_vol_port * np.sqrt(252)
annual_vol_bank = daily_vol_bank * np.sqrt(252)
print(f'\n(b) 年化波动率:')
print(f'宁波港: {annual_vol_port:.4f} ({annual_vol_port*100:.2f}%)')
print(f'宁波银行: {annual_vol_bank:.4f} ({annual_vol_bank*100:.2f}%)')
# (c) 计算累计收益
cumulative_returns = (1 + daily_returns).cumprod()
print(f'\n(c) 累计收益(最后一天):')
print(f'宁波港: {cumulative_returns["宁波港"].iloc[-1]:.4f}')
print(f'宁波银行: {cumulative_returns["宁波银行"].iloc[-1]:.4f}')
# (d) 计算夏普比率
# 年化收益率
annual_return_port = daily_returns['宁波港'].mean() * 252
annual_return_bank = daily_returns['宁波银行'].mean() * 252
# 无风险利率(年化3%)
risk_free_rate = 0.03
# 夏普比率 = (年化收益率 - 无风险利率) / 年化波动率
sharpe_port = (annual_return_port - risk_free_rate) / annual_vol_port
sharpe_bank = (annual_return_bank - risk_free_rate) / annual_vol_bank
print(f'\n(d) 夏普比率:')
print(f'宁波港: {sharpe_port:.4f}')
print(f'宁波银行: {sharpe_bank:.4f}')
print('\n说明:夏普比率衡量每承担一单位风险所获得的超额收益,')
print('数值越高表示风险调整后的收益越好。')(a) 收益率统计:
宁波港日收益率均值: 0.0003
宁波银行日收益率均值: 0.0007
宁波港月收益率均值: 0.0059
宁波银行月收益率均值: 0.0132
(b) 年化波动率:
宁波港: 0.2983 (29.83%)
宁波银行: 0.3338 (33.38%)
(c) 累计收益(最后一天):
宁波港: 1.4233
宁波银行: 5.6535
(d) 夏普比率:
宁波港: 0.1284
宁波银行: 0.4290
说明:夏普比率衡量每承担一单位风险所获得的超额收益,
数值越高表示风险调整后的收益越好。
理论升华:夏普比率的边界与局限
夏普比率(Sharpe Ratio)是马科维茨框架下衡量风险调整后收益的“黄金准则”:
\[ \text{Sharpe Ratio} = \frac{E[R_p] - R_f}{\sigma_p} \]
经济内涵: - 性价比指标:它描述了单位总风险(Total Risk)所交换到的超额收益。 - 评级标准:通常认为 Sharp > 1 是专业资管产品的入场券,而 Sharp > 2 则属于顶级量化策略。
批判性思考: 1. 分布假设:夏普比率建立在正态分布、二阶矩(方差)能够完整捕捉风险的假设上。对 A 股市场常见的“尖峰厚尾”及“极端崩盘”风险,夏普比率往往会低估真实风险。 2. 不区分性质:它对上行波动(盈利)和下行波动(亏损)一视同仁,但在实际心理感受上,投资者更倾向于使用只关注下行风险的索提诺比率(Sortino Ratio)。
习题 5.7: 数据对齐在投资组合中的应用
假设你有以下三个资产的历史收益率数据:
构建一个等权重投资组合(权重各为1/3),计算:
解答:
import pandas as pd
import numpy as np
# 创建资产收益率数据
asset_A = pd.Series([0.02, 0.03, -0.01, 0.04, 0.02],
index=pd.date_range('2023-01-01', periods=5))
asset_B = pd.Series([0.01, 0.02, 0.03, -0.02, 0.01],
index=pd.date_range('2023-01-02', periods=5))
asset_C = pd.Series([0.015, 0.025, 0.01, 0.03, 0.02],
index=pd.date_range('2023-01-01', periods=5))
# 合并为DataFrame(演示pandas的数据对齐功能)
returns_df = pd.DataFrame({
'Asset_A': asset_A,
'Asset_B': asset_B,
'Asset_C': asset_C
})
print('收益率数据(注意pandas自动对齐了不同的索引):')
print(returns_df)
# (a) 计算等权重投资组合的每日收益率
weights = np.array([1/3, 1/3, 1/3])
portfolio_returns = returns_df.mul(weights, axis=1).sum(axis=1)
print('\n(a) 投资组合每日收益率:')
print(portfolio_returns)
# (b) 计算累计收益
cumulative_returns = (1 + portfolio_returns).cumprod() - 1
print('\n(b) 投资组合累计收益率:')
print(cumulative_returns)
# (c) 计算波动率(年化,假设252个交易日)
volatility = portfolio_returns.std() * np.sqrt(252)
print(f'\n(c) 投资组合年化波动率: {volatility:.4f} ({volatility*100:.2f}%)')
# (d) 计算期末组合价值
initial_investment = 10000 # 每个资产
total_initial = 30000 # 总投资
final_value = total_initial * (1 + cumulative_returns.iloc[-1])
print(f'\n(d) 期末组合价值:')
print(f'初始投资总额: {total_initial:,.2f} 元')
print(f'期末价值: {final_value:,.2f} 元')
print(f'绝对收益: {final_value - total_initial:,.2f} 元')
print(f'收益率: {cumulative_returns.iloc[-1]*100:.2f}%')收益率数据(注意pandas自动对齐了不同的索引):
Asset_A Asset_B Asset_C
2023-01-01 0.02 NaN 0.015
2023-01-02 0.03 0.01 0.025
2023-01-03 -0.01 0.02 0.010
2023-01-04 0.04 0.03 0.030
2023-01-05 0.02 -0.02 0.020
2023-01-06 NaN 0.01 NaN
(a) 投资组合每日收益率:
2023-01-01 0.011667
2023-01-02 0.021667
2023-01-03 0.006667
2023-01-04 0.033333
2023-01-05 0.006667
2023-01-06 0.003333
Freq: D, dtype: float64
(b) 投资组合累计收益率:
2023-01-01 0.011667
2023-01-02 0.033586
2023-01-03 0.040477
2023-01-04 0.075159
2023-01-05 0.082327
2023-01-06 0.085935
Freq: D, dtype: float64
(c) 投资组合年化波动率: 0.1823 (18.23%)
(d) 期末组合价值:
初始投资总额: 30,000.00 元
期末价值: 32,578.04 元
绝对收益: 2,578.04 元
收益率: 8.59%
核心机制:数据对齐在资产组合中的数学逻辑
在习题 5.7 中,我们见证了 Pandas 数据对齐机制在多资产分析中的鲁棒性。
Series 时会自动寻找日期的并集。NaN 强制让计算后果可见,避免了将缺失收益率误计为 0 所导致的风险低估。习题 5.8: 实现一个简单的事件研究策略
事件研究 (Event Study) 是金融实证研究中的重要方法,用于评估特定事件(如财报发布、政策出台)对资产价格的影响。
假设你要研究宁波港(601018)在财报发布日前后的异常收益。你需要:
解答:
import pandas as pd
import numpy as np
from pathlib import Path
# 读取数据
# 从本地 Parquet 文件读取数据代替 HDF5
ningbo_port = pd.read_parquet(
'C:/qiufei/data/stock/stock_price_pre_adjusted.parquet',
filters=[('order_book_id', '==', '601018.XSHG')]
).reset_index().rename(columns={'date': 'trade_date', 'vol': 'volume'})
# 准备数据
ningbo_port['trade_date'] = pd.to_datetime(ningbo_port['trade_date'], format='%Y%m%d')
ningbo_port = ningbo_port[['trade_date', 'close']].copy()
ningbo_port.set_index('trade_date', inplace=True)
# 计算日收益率
ningbo_port['return'] = ningbo_port['close'].pct_change()
# 为了演示,我们假设有一个市场收益率序列
# 在实际研究中,你应该读取沪深300指数数据
np.random.seed(42)
market_returns = pd.Series(
np.random.normal(0.0008, 0.015, len(ningbo_port)),
index=ningbo_port.index
)
ningbo_port['market_return'] = market_returns
# (a) 使用市场模型估计正常收益率
# R_i,t = α_i + β_i * R_m,t + ε_i,t
# 正常收益率 = α_i + β_i * R_m,t
# 估计期(事件期之前的数据)
estimation_period = 120 # 使用前120天
# 获取估计期数据并剔除缺失值
estimation_data = ningbo_port.iloc[:estimation_period].dropna()
# OLS回归估计α和β
import statsmodels.api as sm
from scipy import stats
x = estimation_data['market_return'].values
y = estimation_data['return'].values
# 添加常数项
X = sm.add_constant(x)
model = sm.OLS(y, X).fit()
alpha = model.params[0]
beta = model.params[1]
print(f'(a) 市场模型估计结果:')
print(f'α (alpha): {alpha:.6f}')
print(f'β (beta): {beta:.6f}')
print(f'R-squared: {model.rsquared:.4f}')
# (b) 计算全期的正常收益率和异常收益率
ningbo_port['normal_return'] = alpha + beta * ningbo_port['market_return']
ningbo_port['abnormal_return'] = ningbo_port['return'] - ningbo_port['normal_return']
# 重新获取包含新列的估计期数据
estimation_data = ningbo_port.iloc[:estimation_period]
# 假设事件日是第150个交易日
event_day = 150
event_window = 5 # 事件窗口:前后5天
event_data = ningbo_port.iloc[event_day-event_window:event_day+event_window+1]
print(f'\n(b) 事件期异常收益率:')
print(event_data[['abnormal_return']])
# (c) 计算累计异常收益率 (CAR)
car = event_data['abnormal_return'].sum()
print(f'\n(c) 累计异常收益率 (CAR): {car:.4f} ({car*100:.2f}%)')
# (d) t检验
# H0: CAR = 0 (事件对股价无显著影响)
# H1: CAR ≠ 0 (事件对股价有显著影响)
# 计算标准误差(使用估计期的残差标准差)
residuals = y - (alpha + beta * x)
sigma_epsilon = residuals.std()
# CAR的标准误差
car_std = sigma_epsilon * np.sqrt(2 * event_window + 1)
# t统计量
t_stat = car / car_std
# 计算p值(双尾检验)
from scipy import stats
p_value = 2 * (1 - stats.t.cdf(abs(t_stat), df=estimation_period-2))
print(f'\n(d) 显著性检验:')
print(f'CAR标准误: {car_std:.6f}')
print(f't统计量: {t_stat:.4f}')
print(f'p值: {p_value:.4f}')
if p_value < 0.05:
print('\n结论:在5%显著性水平下,拒绝原假设。')
print('事件对股价有显著影响。')
else:
print('\n结论:在5%显著性水平下,不能拒绝原假设。')
print('事件对股价没有显著影响。')
# 可视化
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'Microsoft YaHei', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.figure(figsize=(12, 6))
# 子图1:事件期累计异常收益
plt.subplot(1, 2, 1)
event_car = event_data['abnormal_return'].cumsum()
plt.plot(range(-event_window, event_window+1), event_car.values, marker='o')
plt.axhline(y=0, color='r', linestyle='--', label='基准线')
plt.axvline(x=0, color='g', linestyle='--', label='事件日')
plt.xlabel('相对事件日')
plt.ylabel('累计异常收益率')
plt.title('事件期累计异常收益率')
plt.legend()
plt.grid(True, alpha=0.3)
# 子图2:异常收益率的分布
plt.subplot(1, 2, 2)
plt.hist(estimation_data['return'] - estimation_data['normal_return'],
bins=30, alpha=0.7, label='估计期残差')
plt.axvline(x=car/(2*event_window+1), color='r',
linestyle='--', linewidth=2, label='事件期平均AR')
plt.xlabel('异常收益率')
plt.ylabel('频数')
plt.title('异常收益率分布')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()(a) 市场模型估计结果:
α (alpha): -0.000331
β (beta): -0.007629
R-squared: 0.0000
(b) 事件期异常收益率:
abnormal_return
trade_date
2011-05-09 0.000427
2011-05-10 -0.002771
2011-05-11 0.009154
2011-05-12 -0.011482
2011-05-13 0.003398
2011-05-16 -0.014637
2011-05-17 0.006461
2011-05-18 0.000259
2011-05-19 -0.008686
2011-05-20 -0.002702
2011-05-23 -0.030308
(c) 累计异常收益率 (CAR): -0.0509 (-5.09%)
(d) 显著性检验:
CAR标准误: 0.063506
t统计量: -0.8013
p值: 0.4246
结论:在5%显著性水平下,不能拒绝原假设。
事件对股价没有显著影响。

实证方法:事件研究(Event Study)的现代金融逻辑
事件研究法是现代实证金融学的核心,旨在剥离市场噪音,提取特定事件带来的“超额价值”。
1. 资产均衡模型自律: 我们首先通过市场模型(Market Model)估计资产的系统性风险 \(\beta\): \[ R_{i,t} = \alpha_i + \beta_i R_{m,t} + \varepsilon_{i,t} \]
2. 异常收益(AR)的剥离: 异常收益 \(AR_{i,t}\) 捕捉的是市场整体无法解释的那部分波动: \[ AR_{i,t} = R_{i,t} - (\hat{\alpha}_i + \hat{\beta}_i R_{m,t}) \]
3. 累计影响 (CAR): 通过在事件窗口(Event Window)内对异常收益求和,我们可以判断信息泄露(前 window)、即时反应(事件日)以及价格漂移(后 window)。
在量化 A 股市场对财报预喜、监管政策或资产重组的反应时,这一分析范式能帮助我们验证有效市场假说 (EMH) 在中国市场的具体表现。
核心教材 1. 《Python for Data Analysis (3rd Edition)》by Wes McKinney - 第5章:pandas入门 - 第7章:数据清洗与准备 - 第8章:数据规整:合并、重塑和连接
专题深入 2. 《Effective Pandas》(第2版)by Kevin Markham - 涵盖pandas最佳实践、性能优化和高级技巧
统计分析 4. 《Practical Statistics for Data Scientists》by Peter Bruce & Andrew Bruce - 第3章:统计方法与pandas应用
可视化 5. 《Python for Finance (2nd Edition)》by Hilpisch & Yves Hilpisch - 第11章:时间序列数据可视化
量化金融 6. 《Algorithmic Trading with Python》(2024)by Stefan Jansen - 警告:量化交易涉及高风险,学习目的应仅限于教育和研究
经典教材 8. 《Event Studies in Finance》by Campbell, Lo, and MacKinlay - 事件研究方法的经典教材
官方文档 - pandas User Guide: https://pandas.pydata.org/docs/user_guide/ - pandas API Reference: https://pandas.pydata.org/docs/reference/
教程与课程 - Kaggle Learn: https://www.kaggle.com/learn/pandas - Real Python: https://realpython.com/pandas-python-tutorial/
在下一章中,我们将讨论使用pandas读取(或加载)和写入数据集的工具。之后,我们将更深入地使用pandas来研究数据清洗、整理、分析和可视化的工具。
通过本章的学习和习题练习,你应该已经掌握了:
这些技能将为你后续学习更高级的数据分析和机器学习技术打下坚实的基础。