4  pandas入门

4.1 引言与学习目标

学习目标

通过本章学习,你应该能够:

  • 理论目标
    • 深刻理解pandas的Series和DataFrame数据结构的数学表示
    • 掌握数据对齐的数学原理和索引机制
    • 理解缺失值处理的数学基础
    • 掌握相关性和协方差的数学定义及其金融应用
  • 实践目标
    • 熟练使用pandas读取和处理本地金融数据(股票、财务报表)
    • 运用loc和iloc进行精确的数据选择和操作
    • 使用groupby进行数据分组分析和聚合计算
    • 实现投资组合基本计算(收益、风险、夏普比率)
  • 应用目标
    • 能够从本地数据源读取并处理中国上市公司数据
    • 实现基础的技术分析指标(移动平均、收益率)
    • 运用pandas进行投资组合风险分析和绩效评估

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

列表 4.1
import numpy as np
import pandas as pd

因此,每当你在代码中看到pd.时,它都指向pandas。由于SeriesDataFrame使用得非常频繁,将它们导入到本地命名空间可能会更方便:

列表 4.2
from pandas import Series, DataFrame

4.2 pandas数据结构介绍

4.2.1 理论基础:数据对齐的数学原理

索引的数学定义

从数学角度,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,你需要熟悉它的两个核心数据结构:SeriesDataFrame。虽然它们并非所有问题的通用解决方案,但它们为各种数据任务提供了坚实、灵活的基础。

4.2.2 Series

Series是一个一维的、类似数组的对象,它包含一个值序列(其类型与NumPy类型相似)和一个相关的数据标签数组,称为其索引 (index)。最简单的Series仅由一个数据数组构成:

series_data = pd.Series([4, 7, -5, 3])
series_data
0    4
1    7
2   -5
3    3
dtype: int64

在交互式环境中显示的Series的字符串表示形式,左侧是索引,右侧是值。由于我们没有为数据指定索引,系统会自动创建一个由0到N-1(其中N是数据长度)的整数组成的默认索引。你可以通过arrayindex属性分别获取Series的数组表示和索引对象:

series_data.array
<NumpyExtensionArray>
[4, 7, -5, 3]
Length: 4, dtype: int64
series_data.index
RangeIndex(start=0, stop=4, step=1)

.array属性的结果是一个PandasArray,它通常包装一个NumPy数组,但也可以包含特殊的扩展数组类型。

通常,你会希望创建一个带有索引的Series,用标签来标识每个数据点:

labeled_series = pd.Series([4, 7, -5, 3], index=['d', 'b', 'a', 'c'])
labeled_series
d    4
b    7
a   -5
c    3
dtype: int64
labeled_series.index
Index(['d', 'b', 'a', 'c'], dtype='object')

与NumPy数组相比,你可以在选择单个或一组值时使用索引中的标签:

labeled_series['a']
np.int64(-5)
labeled_series['d'] = 6
labeled_series[['c', 'a', 'd']]
c    3
a   -5
d    6
dtype: int64

在这里,['c', 'a', 'd']被解释为一个索引列表,即使它包含的是字符串而不是整数。

使用NumPy函数或类似NumPy的操作,例如用布尔数组进行过滤、标量乘法或应用数学函数,都会保留索引与值之间的链接:

labeled_series[labeled_series > 0]
d    6
b    7
c    3
dtype: int64
labeled_series * 2
d    12
b    14
a   -10
c     6
dtype: int64
np.exp(labeled_series)
d     403.428793
b    1096.633158
a       0.006738
c      20.085537
dtype: float64

另一种理解Series的方式是,可以把它看作一个固定长度的、有序的字典,因为它是索引值到数据值的映射。它可以在许多你可能使用字典的场景中使用:

'b' in labeled_series
True
'e' in labeled_series
False

如果你有一个包含在Python字典中的数据,你可以通过传递这个字典来创建一个Series。在这里,我们用中国几个省市2020年的常住人口(单位:万人)作为例子:

sdata = {'北京': 2189, '上海': 2487, '广东': 12601, '浙江': 6456}
pop_series = pd.Series(sdata)
pop_series
北京     2189
上海     2487
广东    12601
浙江     6456
dtype: int64

一个Series可以通过其to_dict方法转换回字典:

pop_series.to_dict()
{'北京': 2189, '上海': 2487, '广东': 12601, '浙江': 6456}

当你只传递一个字典时,生成的Series中的索引将遵循字典keys方法的顺序,这取决于键的插入顺序。你可以通过传递一个带有你期望顺序的字典键的索引来覆盖这个默认行为:

技术细节:Python 字典顺序的一致性保障

在 Python 3.7+ 版本中,标准 dict 类型已保证了键的插入顺序。因此,在通过本地金融字典快速构建 Pandas Series 时,其索引将默认遵循键的添加顺序。但在早期版本或某些非官方 Python 构建中,字典是无序的。在金融科研领域,为了确保实验的可复现性,显式传递 index 参数指定顺序是专业量化分析师的“标准动作”。

provinces = ['浙江', '广东', '江苏', '上海']
pop_series_sorted = pd.Series(sdata, index=provinces)
pop_series_sorted
浙江     6456.0
广东    12601.0
江苏        NaN
上海     2487.0
dtype: float64

在这里,sdata中找到的三个值被放在了相应的位置,但由于没有找到'江苏'的值,它显示为NaN(Not a Number),在pandas中用来标记缺失或NA值。由于'北京'没有包含在provinces列表中,它被从结果对象中排除了。

我将交替使用“缺失(missing)”、“NA”或“空值(null)”来指代缺失数据。应使用pandas中的isnanotna函数来检测缺失数据:

核心规则:NaN 的传播特性与级联失效风险

NaN(Not a Number)在 IEEE 754 标准中被定义。在金融数据清洗过程中,它扮演着重要的角色:它具有高度的“传染性”。任何包含 NaN 的算术运算,其结果都会坍缩为 NaN

\[ x + \text{NaN} = \text{NaN}, \quad x \times \text{NaN} = \text{NaN} \]

这一设计的深层意义在于:量化系统中不允许“静默失败”。如果资产的某天收盘价缺失,计算累计收益率时如果不报错且不处理,系统会迫使整个序列失效,从而提示分析师去修补源数据,而不是给出一个看似正确但毫无意义的虚假结果。

pd.isna(pop_series_sorted)
浙江    False
广东    False
江苏     True
上海    False
dtype: bool
pd.notna(pop_series_sorted)
浙江     True
广东     True
江苏    False
上海     True
dtype: bool

Series本身也拥有这些作为实例方法:

pop_series_sorted.isna()
浙江    False
广东    False
江苏     True
上海    False
dtype: bool

对于许多应用来说,Series的一个有用特性是它在算术运算中会根据索引标签自动对齐:

pop_series
北京     2189
上海     2487
广东    12601
浙江     6456
dtype: int64
pop_series_sorted
浙江     6456.0
广东    12601.0
江苏        NaN
上海     2487.0
dtype: float64
pop_series + pop_series_sorted
上海     4974.0
北京        NaN
广东    25202.0
江苏        NaN
浙江    12912.0
dtype: float64

数据对齐的特性将在后面更详细地讨论。如果你有数据库经验,可以将其视为类似于join(连接)操作。

Series对象本身及其索引都有一个name属性,这与其他pandas功能集成在一起:

pop_series_sorted.name = 'population'
pop_series_sorted.index.name = 'province'
pop_series_sorted
province
浙江     6456.0
广东    12601.0
江苏        NaN
上海     2487.0
Name: population, dtype: float64

Series的索引可以通过赋值来原地修改:

series_data
0    4
1    7
2   -5
3    3
dtype: int64
series_data.index = ['张三', '李四', '王五', '赵六']
series_data
张三    4
李四    7
王五   -5
赵六    3
dtype: int64

4.2.3 DataFrame

DataFrame表示一个矩形的数据表格,它包含一个有序的、命名的列集合,每一列都可以是不同的值类型(数值、字符串、布尔值等)。DataFrame既有行索引也有列索引;可以把它看作是一个共享相同索引的Series的字典。

构建DataFrame有多种方式,但最常用的一种是从一个等长列表或NumPy数组组成的字典来构建:

列表 4.3
province_data = {'province': ['广东', '广东', '广东', '浙江', '浙江', '浙江'],
        'year': [2010, 2015, 2020, 2015, 2020, 2022],
        'gdp': [4.6, 7.3, 11.07, 4.3, 6.46, 7.77]}
province_df = pd.DataFrame(province_data)

生成的DataFrame的索引会自动分配,就像Series一样,并且列会根据data中键的顺序来排列(这取决于它们在字典中的插入顺序):

province_df
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环境中渲染的pandas DataFrame

图 4.1 所示,如果你正在使用Jupyter Notebook或Quarto,pandasDataFrame对象将会以一个更适合浏览器的HTML表格形式显示。

对于大型DataFramehead方法只选择前五行:

province_df.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_df.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的列将按照那个顺序排列:

pd.DataFrame(province_data, columns=['year', 'province', 'gdp'])
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

如果你传递一个字典中不包含的列,它将在结果中以缺失值的形式出现:

province_df_with_debt = pd.DataFrame(province_data, columns=['year', 'province', 'gdp', 'debt'])
province_df_with_debt
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
province_df_with_debt.columns
Index(['year', 'province', 'gdp', 'debt'], dtype='object')

DataFrame中的一列可以像字典那样通过键来检索,也可以通过点属性表示法来检索,其结果是一个Series

province_df_with_debt['province']
0    广东
1    广东
2    广东
3    浙江
4    浙江
5    浙江
Name: province, dtype: object
province_df_with_debt.year
0    2010
1    2015
2    2020
3    2015
4    2020
5    2022
Name: year, dtype: int64

属性式访问(例如 province_df_with_debt.year)和在IPython/Jupyter中对列名的Tab补全是为了方便而提供的。province_df_with_debt[column]适用于任何列名,但province_df_with_debt.column仅在列名是有效的Python变量名且不与DataFrame的任何方法名冲突时才有效。例如,如果列名包含空格或下划线以外的符号,就不能使用点属性方法访问。

行也可以通过ilocloc这两个特殊的属性按位置或名称来检索:

province_df_with_debt.loc[1]
year        2015
province      广东
gdp          7.3
debt         NaN
Name: 1, dtype: object
province_df_with_debt.iloc[2]
year         2020
province       广东
gdp         11.07
debt          NaN
Name: 2, dtype: object

列可以通过赋值来修改。例如,空的debt列可以被赋一个标量值或一个值数组:

province_df_with_debt['debt'] = 16.5
province_df_with_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
province_df_with_debt['debt'] = np.arange(6.)
province_df_with_debt
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的索引重新对齐,任何不存在的索引值处都会插入缺失值:

val = pd.Series([-1.2, -1.5, -1.7], index=[0, 2, 5])
province_df_with_debt['debt'] = val
province_df_with_debt
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关键字可以像删除字典键一样删除列。作为例子,我首先添加一个新列,其布尔值表示该省份是否为沿海经济发达地区:

province_df_with_debt['coastal'] = province_df_with_debt['province'].isin(['广东', '浙江'])
province_df_with_debt
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关键字来移除这一列:

del province_df_with_debt['coastal']
province_df_with_debt.columns
Index(['year', 'province', 'gdp', 'debt'], dtype='object')

核心挑战:视图(View)与副本(Copy)的内存逻辑

在处理如 financial_statement.parquet 的千万级财务矩阵时,理解切片的底层操作至关重要。

  • 视图 (View): 指向原内存块的特定偏移量。修改视图会直接改变原数据。
  • 副本 (Copy): 重新分配内存并复制数据。修改副本不影响原矩阵。

严禁行为:链式索引赋值 类似 df_with_debt['debt'][2] = 5 的操作在 Pandas 中是不确定的,可能触发 SettingWithCopyWarning。在量化回测框架中,这种不确定性可能导致“未来函数”或逻辑错误。 最佳实践: 始终使用 .loc 进行显式、原子化的赋值: df_with_debt.loc[2, 'debt'] = 5

另一种常见的数据形式是嵌套的字典:

populations = {'广东': {2010: 10430, 2015: 10849, 2020: 12601},
               '浙江': {2010: 5442, 2015: 5539, 2020: 6456}}

如果将嵌套字典传递给DataFramepandas会将外层字典的键解释为列,内层字典的键解释为行索引:

pop_df = pd.DataFrame(populations)
pop_df
广东 浙江
2010 10430 5442
2015 10849 5539
2020 12601 6456

你可以使用与NumPy数组相似的语法来转置DataFrame(交换行和列):

pop_df.T
2010 2015 2020
广东 10430 10849 12601
浙江 5442 5539 6456

内层字典的键被合并起来形成结果中的索引。如果指定了显式索引,则情况不同:

pd.DataFrame(populations, index=[2015, 2020, 2025])
广东 浙江
2015 10849.0 5539.0
2020 12601.0 6456.0
2025 NaN NaN

Series组成的字典处理方式与此非常相似:

pdata = {'广东': pop_df['广东'][:-1],
         '浙江': pop_df['浙江'][:2]}
pd.DataFrame(pdata)
广东 浙江
2010 10430 5442
2015 10849 5539

关于可以传递给DataFrame构造函数的多种数据类型,请参见 表 4.1

表 4.1: 可传入DataFrame构造函数的数据类型
类型 说明
二维ndarray 数据矩阵,可选择传入行和列的标签
数组、列表或元组的字典 每个序列成为DataFrame的一列;所有序列必须等长
NumPy结构化/记录数组 被视为“数组的字典”情况
Series的字典 每个Series成为一列;如果没有显式传递索引,则每个Series的索引被合并以形成结果的行索引
字典的字典 每个内层字典成为一列;键被合并以形成行索引,同“Series的字典”情况
字典或Series的列表 每一项成为DataFrame的一行;字典键或Series索引的并集成为DataFrame的列标签
列表的列表或元组的列表 被视为“二维ndarray”情况
另一个DataFrame 除非传递了不同的索引,否则使用该DataFrame的索引
NumPy掩码数组 类似于“二维ndarray”情况,但掩码值在DataFrame结果中为缺失值

如果一个DataFrameindexcolumnsname属性被设置了,它们也会被显示出来:

pop_df.index.name = 'year'
pop_df.columns.name = 'province'
pop_df
province 广东 浙江
year
2010 10430 5442
2015 10849 5539
2020 12601 6456

Series不同,DataFrame没有name属性。DataFrameto_numpy方法返回其包含的数据,作为一个二维ndarray

pop_df.to_numpy()
array([[10430,  5442],
       [10849,  5539],
       [12601,  6456]])

如果DataFrame的列是不同的数据类型,返回数组的数据类型将会选择一个能够兼容所有列的类型:

province_df_with_debt.to_numpy()
array([[2010, '广东', 4.6, -1.2],
       [2015, '广东', 7.3, nan],
       [2020, '广东', 11.07, -1.5],
       [2015, '浙江', 4.3, nan],
       [2020, '浙江', 6.46, nan],
       [2022, '浙江', 7.77, -1.7]], dtype=object)

4.2.4 索引对象 (Index Objects)

pandasIndex对象负责持有轴标签(包括DataFrame的列名)和其他元数据(比如轴的名称)。你在构建SeriesDataFrame时使用的任何数组或其他标签序列,都会在内部被转换为一个Index

series_with_index = pd.Series(np.arange(3), index=['a', 'b', 'c'])
index = series_with_index.index
index
Index(['a', 'b', 'c'], dtype='object')
index[1:]
Index(['b', 'c'], dtype='object')

索引对象是不可变的 (immutable),因此不能被用户修改:

# 这行代码会引发TypeError
# index[1] = 'd'

不可变性使得在数据结构之间共享Index对象更加安全:

labels = pd.Index(np.arange(3))
labels
Index([0, 1, 2], dtype='int64')
series_with_int_labels = pd.Series([1.5, -2.5, 0], index=labels)
series_with_int_labels
0    1.5
1   -2.5
2    0.0
dtype: float64
series_with_int_labels.index is labels
True

除了类似数组,Index的行为也像一个固定大小的集合:

pop_df
province 广东 浙江
year
2010 10430 5442
2015 10849 5539
2020 12601 6456
pop_df.columns
Index(['广东', '浙江'], dtype='object', name='province')
'广东' in pop_df.columns
True
2025 in pop_df.index
False

与Python的集合不同,pandasIndex可以包含重复的标签:

pd.Index(['foo', 'foo', 'bar', 'bar'])
Index(['foo', 'foo', 'bar', 'bar'], dtype='object')

使用重复标签进行选择时,会选中该标签的所有出现。

每个Index都有许多用于集合逻辑的方法和属性,它们可以回答关于其所含数据的其他常见问题。一些有用的方法和属性总结在 表 4.2 中。

表 4.2: 一些Index的方法和属性
方法/属性 描述
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中唯一值的数组

4.3 核心功能

本节将引导你了解与SeriesDataFrame中数据进行交互的基本机制。在接下来的章节中,我们将使用pandas更深入地探讨数据分析和处理的主题。

4.3.1 重建索引 (Reindexing)

pandas对象的一个重要方法是reindex,它意味着创建一个新对象,其值根据新的索引重新排列。看一个例子:

price_series = pd.Series([4.5, 7.2, -5.3, 3.6], index=['d', 'b', 'a', 'c'])
price_series
d    4.5
b    7.2
a   -5.3
c    3.6
dtype: float64

对这个Series调用reindex会根据新的索引重新排列数据,如果某个索引值之前不存在,则引入缺失值:

reindexed_prices = price_series.reindex(['a', 'b', 'c', 'd', 'e'])
reindexed_prices
a   -5.3
b    7.2
c    3.6
d    4.5
e    NaN
dtype: float64

对于像时间序列这样的有序数据,你可能希望在重建索引时进行一些插值或填充。method选项允许我们这样做,例如使用ffill方法,它会向前填充值:

color_series = pd.Series(['blue', 'purple', 'yellow'], index=[0, 2, 4])
color_series
0      blue
2    purple
4    yellow
dtype: object
color_series.reindex(np.arange(6), method='ffill')
0      blue
1      blue
2    purple
3    purple
4    yellow
5    yellow
dtype: object

对于DataFramereindex可以改变行索引、列索引,或两者都改变。当只传递一个序列时,它会对结果的行进行重建索引:

region_df = pd.DataFrame(np.arange(9).reshape((3, 3)),
                     index=['a', 'c', 'd'],
                     columns=['北京', '上海', '广东'])
region_df
北京 上海 广东
a 0 1 2
c 3 4 5
d 6 7 8
reindexed_region_df = region_df.reindex(index=['a', 'b', 'c', 'd'])
reindexed_region_df
北京 上海 广东
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关键字进行重建索引:

provinces = ['上海', '浙江', '广东']
region_df.reindex(columns=provinces)
上海 浙江 广东
a 1 NaN 2
c 4 NaN 5
d 7 NaN 8

因为'北京'不在provinces中,所以该列的数据从结果中被删除了。

你也可以使用loc操作符进行重建索引,前提是所有新的索引标签都已存在于DataFrame中。

关于reindex的参数的更多信息,请参见 表 4.3

表 4.3: reindex函数参数
参数 描述
labels 用作索引的新序列。可以是Index实例或任何其他类似序列的Python数据结构。
index, columns 分别使用传入的序列作为新的行索引或列标签。
axis 要重建索引的轴,'index'(行)或'columns'(列)。默认为'index'
method 插值(填充)方法;'ffill'向前填充,'bfill'向后填充。
fill_value 在重建索引引入缺失数据时使用的替代值。
limit 在向前或向后填充时,要填充的最大间隙大小(以元素数量计)。
tolerance 在向前或向后填充时,对于非精确匹配要填充的最大间隙大小(以绝对数值距离计)。
level 在MultiIndex的指定层级上匹配简单索引;否则选择子集。
copy 如果为True,即使新索引与旧索引相同,也总是复制底层数据;如果为False,当索引相同时不复制数据。

4.3.2 删除指定轴上的条目 (Dropping Entries from an Axis)

如果你已经有一个不包含那些条目的索引数组或列表,那么从一个轴上删除一个或多个条目就很简单。drop方法将返回一个新对象,其中指定的值已从一个轴上被删除:

series_obj = pd.Series(np.arange(5.), index=['a', 'b', 'c', 'd', 'e'])
series_obj
a    0.0
b    1.0
c    2.0
d    3.0
e    4.0
dtype: float64
filtered_series = series_obj.drop('c')
filtered_series
a    0.0
b    1.0
d    3.0
e    4.0
dtype: float64
series_obj.drop(['d', 'c'])
a    0.0
b    1.0
e    4.0
dtype: float64

对于DataFrame,可以从任一轴删除索引值。为了说明这一点,我们首先创建一个示例DataFrame:

matrix_df = pd.DataFrame(np.arange(16).reshape((4, 4)),
                    index=['北京', '上海', '广东', '浙江'],
                    columns=['one', 'two', 'three', 'four'])
matrix_df
one two three four
北京 0 1 2 3
上海 4 5 6 7
广东 8 9 10 11
浙江 12 13 14 15

用一个标签序列调用drop将从行标签(轴0)中删除值:

matrix_df.drop(index=['上海', '北京'])
one two three four
广东 8 9 10 11
浙江 12 13 14 15

要从列中删除标签,可以使用columns关键字:

matrix_df.drop(columns=['two'])
one three four
北京 0 2 3
上海 4 6 7
广东 8 10 11
浙江 12 14 15

你也可以通过传递axis=1axis='columns'来从列中删除值:

matrix_df.drop('two', axis=1)
one three four
北京 0 2 3
上海 4 6 7
广东 8 10 11
浙江 12 14 15
matrix_df.drop(['two', 'four'], axis='columns')
one three
北京 0 2
上海 4 6
广东 8 10
浙江 12 14

4.3.3 索引、选择与过滤 (Indexing, Selection, and Filtering)

Series的索引(obj[...])工作方式类似于NumPy数组的索引,不同之处在于你可以使用Series的索引值而不仅仅是整数。

series_index = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
series_index
a    0.0
b    1.0
c    2.0
d    3.0
dtype: float64
series_index['b']
np.float64(1.0)
series_index[1]
np.float64(1.0)
series_index[2:4]
c    2.0
d    3.0
dtype: float64
series_index[['b', 'a', 'd']]
b    1.0
a    0.0
d    3.0
dtype: float64
series_index[[1, 3]]
b    1.0
d    3.0
dtype: float64
series_index[series_index < 2]
a    0.0
b    1.0
dtype: float64

虽然你可以通过标签来选择数据,但选择索引值的首选方法是使用特殊的loc操作符:

series_index.loc[['b', 'a', 'd']]
b    1.0
a    0.0
d    3.0
dtype: float64

决策依据:整数索引的歧义性与标准化消除方案

对于初学者,整数索引是引发 Bug 的重灾区。Pandas 对此提供了严谨的区分:

  • 属性 .loc: 解析为语义标签。如果索引是 2, 0, 1,那么 loc[0] 寻找标签为 0 的那一行。
  • 属性 .iloc: 解析为物理位置。无论标签是什么,iloc[0] 永远返回内存中的第一行。

在金融数据管道(Pipeline)中,这种区分至关重要。例如,当你在处理某省份的年份序列时,2020 既是标签(年份)也可能是位置偏移(如果数据正好有两千多行)。 最佳实践: 永远弃用隐式 [] 索引进行行选择,强制使用 .loc.iloc 来明晰你的业务逻辑或计算机逻辑。

例如,考虑一个带有整数索引的Series

yield_series = pd.Series([1, 2, 3], index=[2, 0, 1])
stock_series = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
yield_series
2    1
0    2
1    3
dtype: int64
stock_series
a    1
b    2
c    3
dtype: int64

yield_series上使用[]索引将使用标签,而不是位置:

yield_series[[0, 1, 2]]
0    2
1    3
2    1
dtype: int64

stock_series上使用loc和整数会失败,因为它的索引不是基于整数的:

# 这会引发一个KeyError
# stock_series.loc[[0, 1]]

由于loc操作符完全按标签索引,因此还有一个iloc操作符,它完全按整数索引,无论索引是否包含整数,其工作方式都保持一致:

yield_series.iloc[[0, 1, 2]]
2    1
0    2
1    3
dtype: int64
stock_series.iloc[[0, 1, 2]]
a    1
b    2
c    3
dtype: int64

你也可以使用loc进行标签切片,但它的工作方式与普通的Python切片不同,即终点是包含在内的:

stock_series.loc['b':'c']
b    2
c    3
dtype: int64

使用这些方法赋值会修改Series的相应部分:

stock_series.loc['b':'c'] = 5
stock_series
a    1
b    5
c    5
dtype: int64

DataFrame进行索引会检索一个或多个列,可以使用单个值或序列:

matrix_df = pd.DataFrame(np.arange(16).reshape((4, 4)),
                    index=['北京', '上海', '广东', '浙江'],
                    columns=['one', 'two', 'three', 'four'])
matrix_df
one two three four
北京 0 1 2 3
上海 4 5 6 7
广东 8 9 10 11
浙江 12 13 14 15
matrix_df['two']
北京     1
上海     5
广东     9
浙江    13
Name: two, dtype: int64
matrix_df[['three', 'one']]
three one
北京 2 0
上海 6 4
广东 10 8
浙江 14 12

这样的索引有几个特殊情况。第一种是切片或用布尔数组选择数据:

matrix_df[:2]
one two three four
北京 0 1 2 3
上海 4 5 6 7
matrix_df[matrix_df['three'] > 5]
one two three four
上海 4 5 6 7
广东 8 9 10 11
浙江 12 13 14 15

行选择语法matrix_df[:2]是为了方便而提供的。将单个元素或列表传递给[]操作符会选择列。

另一个用例是使用布尔DataFrame进行索引,例如由标量比较产生的DataFrame

matrix_df < 5
one two three four
北京 True True True True
上海 True False False False
广东 False False False False
浙江 False False False False

我们可以使用这个DataFrame将值为True的每个位置都赋为0:

matrix_df[matrix_df < 5] = 0
matrix_df
one two three four
北京 0 0 0 0
上海 0 5 6 7
广东 8 9 10 11
浙江 12 13 14 15

4.3.3.1 使用loc和iloc在DataFrame上进行选择

Series一样,DataFrame也具有用于基于标签和基于整数索引的特殊属性lociloc。作为第一个例子,让我们按标签选择单行:

matrix_df.loc['上海']
one      0
two      5
three    6
four     7
Name: 上海, dtype: int64

要选择多行,传递一个标签序列:

matrix_df.loc[['上海', '浙江']]
one two three four
上海 0 5 6 7
浙江 12 13 14 15

你可以在loc中组合行和列的选择,用逗号分隔:

matrix_df.loc['上海', ['two', 'three']]
two      5
three    6
Name: 上海, dtype: int64

接下来我们用iloc进行一些类似的整数选择:

matrix_df.iloc[2]
one       8
two       9
three    10
four     11
Name: 广东, dtype: int64
matrix_df.iloc[[2, 1]]
one two three four
广东 8 9 10 11
上海 0 5 6 7
matrix_df.iloc[2, [3, 0, 1]]
four    11
one      8
two      9
Name: 广东, dtype: int64
matrix_df.iloc[[1, 2], [3, 0, 1]]
four one two
上海 7 0 5
广东 11 8 9

两个索引函数都支持切片,以及单个标签或标签列表:

matrix_df.loc[:'广东', 'two']
北京    0
上海    5
广东    9
Name: two, dtype: int64
matrix_df.iloc[:, :3][matrix_df.three > 5]
one two three
上海 0 5 6
广东 8 9 10
浙江 12 13 14

布尔数组可以与loc一起使用,但不能与iloc一起使用:

matrix_df.loc[matrix_df.three >= 2]
one two three four
上海 0 5 6 7
广东 8 9 10 11
浙江 12 13 14 15

表 4.4 提供了DataFrame索引选项的简短摘要。

表 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 方法 按标签选择行或列

4.3.3.2 整数索引的陷阱

处理由整数索引的pandas对象可能会成为一个障碍。例如,你可能不会预料到以下代码会产生错误:

ser = pd.Series(np.arange(3.))
ser
0    0.0
1    1.0
2    2.0
dtype: float64
# 这会引发一个KeyError
# ser[-1]

在这种情况下,pandas可以“回退”到整数索引,但在不引入细微错误的情况下,这在通常很难做到。这里我们有一个包含0、1和2的索引,但pandas不想猜测用户想要的是什么(基于标签的索引还是基于位置的)。

另一方面,对于非整数索引,就没有这样的歧义:

ser2 = pd.Series(np.arange(3.), index=['a', 'b', 'c'])
ser2[-1]
np.float64(2.0)

如果你的轴索引包含整数,数据选择将始终是面向标签的。如前所述,如果你使用loc(用于标签)或iloc(用于整数),你将得到你想要的结果:

ser.iloc[-1]
np.float64(2.0)

另一方面,用整数切片总是面向位置的:

ser[:2]
0    0.0
1    1.0
dtype: float64

4.3.3.3 链式索引的陷阱

这些索引属性也可以用来原地修改DataFrame对象,但这需要一些小心。

matrix_df.loc[:, 'one'] = 1
matrix_df
one two three four
北京 1 0 0 0
上海 1 5 6 7
广东 1 9 10 11
浙江 1 13 14 15
matrix_df.iloc[2] = 5
matrix_df
one two three four
北京 1 0 0 0
上海 1 5 6 7
广东 5 5 5 5
浙江 1 13 14 15
matrix_df.loc[matrix_df['four'] > 5] = 3
matrix_df
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操作:

matrix_df.loc[matrix_df.three == 5, 'three'] = 6
matrix_df
one two three four
北京 1 0 0 0
上海 3 3 3 3
广东 5 5 6 5
浙江 3 3 3 3

4.3.4 算术与数据对齐

当你将对象相加时,如果任何索引对不相同,结果中的相应索引将是索引对的并集。

vol_series1 = pd.Series([7.3, -2.5, 3.4, 1.5], index=['a', 'c', 'd', 'e'])
vol_series2 = pd.Series([-2.1, 3.6, -1.5, 4, 3.1], index=['a', 'c', 'e', 'f', 'g'])
vol_series1
a    7.3
c   -2.5
d    3.4
e    1.5
dtype: float64
vol_series2
a   -2.1
c    3.6
e   -1.5
f    4.0
g    3.1
dtype: float64
vol_series1 + vol_series2
a    5.2
c    1.1
d    NaN
e    0.0
f    NaN
g    NaN
dtype: float64

内部数据对齐在不重叠的标签位置引入了缺失值。缺失值随后会在进一步的算术计算中传播。

对于DataFrame,对齐是在行和列上都进行的:

df_a = pd.DataFrame(np.arange(9.).reshape((3, 3)), columns=list('bcd'),
                   index=['北京', '上海', '广东'])
df_b = pd.DataFrame(np.arange(12.).reshape((4, 3)), columns=list('bde'),
                   index=['浙江', '北京', '上海', '江苏'])
df_a
b c d
北京 0.0 1.0 2.0
上海 3.0 4.0 5.0
广东 6.0 7.0 8.0
df_b
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,其索引和列是每个DataFrame中索引和列的并集:

df_a + df_b
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对象相加,结果将全部是空值。

4.3.4.1 带有填充值的算术方法

在不同索引对象之间的算术运算中,你可能希望当一个轴标签在一个对象中找到而另一个对象中没有时,用一个特殊值(如0)来填充。

df_a = pd.DataFrame(np.arange(12.).reshape((3, 4)), columns=list('abcd'))
df_b = pd.DataFrame(np.arange(20.).reshape((4, 5)), columns=list('abcde'))
df_b.loc[1, 'b'] = np.nan
df_a
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
df_b
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

将它们相加会导致在不重叠的位置出现缺失值:

df_a + df_b
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_aadd方法,我们传递df_b和一个fill_value参数:

df_a.add(df_b, fill_value=0)
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 列出了SeriesDataFrame的算术方法。每个方法都有一个以字母r开头的对应方法,其参数是反转的。

1 / df_a
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
df_a.rdiv(1)
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

相关地,在对SeriesDataFrame进行重建索引时,你也可以指定一个不同的填充值:

df_a.reindex(columns=df_b.columns, fill_value=0)
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
表 4.5: 灵活的算术方法
方法 描述
add, radd 加法 (+) 的方法
sub, rsub 减法 (-) 的方法
div, rdiv 除法 (/) 的方法
floordiv, rfloordiv 整除 (//) 的方法
mul, rmul 乘法 (*) 的方法
pow, rpow 幂运算 (**) 的方法

4.3.4.2 DataFrame与Series之间的运算

DataFrameSeries之间的算术运算也是有定义的。首先,考虑一个二维数组和它的一行之间的差异:

matrix_data = np.arange(12.).reshape((3, 4))
matrix_data
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.],
       [ 8.,  9., 10., 11.]])
matrix_data[0]
array([0., 1., 2., 3.])
matrix_data - matrix_data[0]
array([[0., 0., 0., 0.],
       [4., 4., 4., 4.],
       [8., 8., 8., 8.]])

当我们从matrix_data中减去matrix_data[0]时,这个减法操作会对每一行都执行一次。这被称为广播 (broadcasting)DataFrameSeries之间的运算是相似的。

核心机制:广播(Broadcasting)的维度匹配逻辑

广播是 Pandas 能够处理多资产异构计算的关键。其底层遵循维度对齐原则:

  1. 逐维兼容:从后向前比较,维度必须相等或其中一个为 1。
  2. 自动扩展:形状为 (n,)Series 会被拉伸至匹配 DataFrame(m, n)

在量化金融中,这种机制常用于: - 去均值化: 从价格矩阵减去均值向量。 - 权重分配: 将权重向量广播至全样本时间序列。

通过显式指定 axis='index' (或 0),你可以将横向(列)对齐切换为纵向(行)对齐。这在计算单个资产对全市场的对冲比率时非常有用。

sample_df = pd.DataFrame(np.arange(12.).reshape((4, 3)),
                     columns=list('bde'),
                     index=['浙江', '北京', '上海', '江苏'])
row_series = sample_df.iloc[0]
sample_df
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
row_series
b    0.0
d    1.0
e    2.0
Name: 浙江, dtype: float64

默认情况下,DataFrameSeries之间的算术运算会将Series的索引与DataFrame的列进行匹配,并向下广播行:

sample_df - row_series
b d e
浙江 0.0 0.0 0.0
北京 3.0 3.0 3.0
上海 6.0 6.0 6.0
江苏 9.0 9.0 9.0

如果在DataFrame的列或Series的索引中找不到某个索引值,对象将被重新索引以形成并集:

new_index_series = pd.Series(np.arange(3), index=['b', 'e', 'f'])
sample_df + new_index_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'):

col_series = sample_df['d']
sample_df
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
col_series
浙江     1.0
北京     4.0
上海     7.0
江苏    10.0
Name: d, dtype: float64
sample_df.sub(col_series, axis='index')
b d e
浙江 -1.0 0.0 1.0
北京 -1.0 0.0 1.0
上海 -1.0 0.0 1.0
江苏 -1.0 0.0 1.0

4.3.5 函数应用和映射 (Function Application and Mapping)

NumPy的通用函数(ufuncs,即逐元素的数组方法)也适用于pandas对象:

random_df = pd.DataFrame(np.random.standard_normal((4, 3)),
                     columns=list('bde'),
                     index=['浙江', '北京', '上海', '江苏'])
random_df
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
np.abs(random_df)
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

另外一个常见的操作是将一个作用于一维数组的函数应用到每一列或每一行。DataFrameapply方法正是为此而生:

def range_func(x):
    return x.max() - x.min()

random_df.apply(range_func)
b    1.782252
d    2.461181
e    2.168171
dtype: float64

如果你向apply传递axis='columns',函数将对每一行调用一次。

random_df.apply(range_func, axis='columns')
浙江    0.579338
北京    0.165869
上海    1.293959
江苏    2.408491
dtype: float64

传递给apply的函数不一定返回一个标量值;它也可以返回一个包含多个值的Series

def stats_func(x):
    return pd.Series([x.min(), x.max()], index=['min', 'max'])
random_df.apply(stats_func)
b d e
min -1.022566 -1.111296 -1.058606
max 0.759686 1.349885 1.109565

逐元素的Python函数也可以使用。假设你想从random_df中的每个浮点值计算一个格式化的字符串。你可以用applymap来做到这一点:

def format_func(x):
    return f'{x:.2f}'

random_df.applymap(format_func)
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方法,用于应用一个逐元素的函数:

random_df['e'].map(format_func)
浙江     0.31
北京    -0.95
上海     1.11
江苏    -1.06
Name: e, dtype: object

4.3.6 排序和排名 (Sorting and Ranking)

要按行或列标签进行字典序排序,使用sort_index方法,它返回一个新的、已排序的对象:

series_to_sort = pd.Series(np.arange(4), index=['d', 'a', 'b', 'c'])
series_to_sort.sort_index()
a    1
b    2
c    3
d    0
dtype: int64

对于DataFrame,你可以按任一轴的索引进行排序:

df_to_sort = pd.DataFrame(np.arange(8).reshape((2, 4)),
                     index=['three', 'one'],
                     columns=['d', 'a', 'b', 'c'])
df_to_sort.sort_index()
d a b c
one 4 5 6 7
three 0 1 2 3
df_to_sort.sort_index(axis='columns')
a b c d
three 1 2 3 0
one 5 6 7 4

数据默认按升序排序,但也可以按降序排序:

df_to_sort.sort_index(axis='columns', ascending=False)
d c b a
three 0 3 2 1
one 4 7 6 5

要按值对Series进行排序,使用其sort_values方法:

series_to_sort = pd.Series([4, 7, -3, 2])
series_to_sort.sort_values()
2   -3
3    2
0    4
1    7
dtype: int64

任何缺失值默认都会被排到Series的末尾:

series_to_sort = pd.Series([4, np.nan, 7, np.nan, -3, 2])
series_to_sort.sort_values()
4   -3.0
5    2.0
0    4.0
2    7.0
1    NaN
3    NaN
dtype: float64

通过使用na_position选项,缺失值可以被排到开头:

series_to_sort.sort_values(na_position='first')
1    NaN
3    NaN
4   -3.0
5    2.0
0    4.0
2    7.0
dtype: float64

在对DataFrame进行排序时,你可以使用一列或多列中的数据作为排序键。

df_to_sort = pd.DataFrame({'b': [4, 7, -3, 2], 'a': [0, 1, 0, 1]})
df_to_sort
b a
0 4 0
1 7 1
2 -3 0
3 2 1
df_to_sort.sort_values('b')
b a
2 -3 0
3 2 1
0 4 0
1 7 1

要按多列排序,传递一个列名的列表:

df_to_sort.sort_values(['a', 'b'])
b a
2 -3 0
0 4 0
3 2 1
1 7 1

排名(Ranking)会为数组中的有效数据点分配从1到N的排名。默认情况下,rank通过为每个组分配平均排名来处理平级关系:

series_to_sort = pd.Series([7, -5, 7, 4, 2, 0, 4])
series_to_sort.rank()
0    6.5
1    1.0
2    6.5
3    4.5
4    3.0
5    2.0
6    4.5
dtype: float64

排名也可以根据它们在数据中出现的顺序来分配:

series_to_sort.rank(method='first')
0    6.0
1    1.0
2    7.0
3    4.0
4    3.0
5    2.0
6    5.0
dtype: float64

你也可以按降序排名:

series_to_sort.rank(ascending=False)
0    1.5
1    7.0
2    1.5
3    3.5
4    5.0
5    6.0
6    3.5
dtype: float64

DataFrame可以在行或列上计算排名。表 4.6 列出了排名的平级处理方法。

df_to_sort = pd.DataFrame({'b': [4.3, 7, -3, 2], 'a': [0, 1, 0, 1],
                      'c': [-2, 5, 8, -2.5]})
df_to_sort
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
df_to_sort.rank(axis='columns')
b a c
0 3.0 2.0 1.0
1 3.0 1.0 2.0
2 1.0 2.0 3.0
3 3.0 2.0 1.0
表 4.6: rank的平级处理方法
方法 描述
'average' 默认:为平级组中的每个条目分配平均排名
'min' 为整个组使用最小排名
'max' 为整个组使用最大排名
'first' 按值在数据中出现的顺序分配排名
'dense' 类似于method='min',但组与组之间的排名总是增加1

4.3.7 带有重复标签的轴索引 (Axis Indexes with Duplicate Labels)

虽然许多pandas函数(如reindex)要求标签是唯一的,但这并不是强制性的。

dup_series = pd.Series(np.arange(5), index=['a', 'a', 'b', 'b', 'c'])
dup_series
a    0
a    1
b    2
b    3
c    4
dtype: int64

索引的is_unique属性可以告诉你其标签是否唯一:

dup_series.index.is_unique
False

索引一个有多个条目的标签会返回一个Series,而单个条目则返回一个标量值:

dup_series['a']
a    0
a    1
dtype: int64
dup_series['c']
np.int64(4)

同样的逻辑也适用于在DataFrame中索引行:

dup_df = pd.DataFrame(np.random.standard_normal((5, 3)),
                  index=['a', 'a', 'b', 'b', 'c'])
dup_df
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
dup_df.loc['b']
0 1 2
b -1.272501 0.081308 -1.368653
b 0.217563 -1.410481 -1.273685
dup_df.loc['c']
0    1.466898
1    0.431466
2   -0.346921
Name: c, dtype: float64

4.4 汇总与描述性统计

pandas对象配备了一套常见的数学和统计方法。这些方法大多数属于归约 (reductions)汇总统计 (summary statistics) 的范畴。它们内置了对缺失数据的处理。

financial_na_df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5],
                   [np.nan, np.nan], [0.75, -1.3]],
                  index=['a', 'b', 'c', 'd'],
                  columns=['one', 'two'])
financial_na_df
one two
a 1.40 NaN
b 7.10 -4.5
c NaN NaN
d 0.75 -1.3

调用DataFramesum方法会返回一个包含列总和的Series

financial_na_df.sum()
one    9.25
two   -5.80
dtype: float64

传递axis='columns'则会对行进行求和:

financial_na_df.sum(axis='columns')
a    1.40
b    2.60
c    0.00
d   -0.55
dtype: float64

当整行或整列都包含NA值时,总和为0。这可以通过skipna选项(默认为True)来禁用,此时总和为NA

financial_na_df.sum(axis='index', skipna=False)
one   NaN
two   NaN
dtype: float64
financial_na_df.sum(axis='columns', skipna=False)
a     NaN
b    2.60
c     NaN
d   -0.55
dtype: float64

一些聚合操作,如mean,至少需要一个非NA值才能产生有效结果:

financial_na_df.mean(axis='columns')
a    1.400
b    1.300
c      NaN
d   -0.275
dtype: float64

表 4.7 列出了归约方法的常用选项。

表 4.7: 归约方法的选项
方法 描述
axis 进行归约的轴;'index'表示行,'columns'表示列
skipna 排除缺失值;默认为True
level 如果轴是分层索引,则按层级进行分组归约

一些方法,如idxminidxmax,返回的是间接统计量,比如取得最小值或最大值的索引值:

financial_na_df.idxmax()
one    b
two    d
dtype: object

其他方法是累积型的:

financial_na_df.cumsum()
one two
a 1.40 NaN
b 8.50 -4.5
c NaN NaN
d 9.25 -5.8

describe就是这样一个例子,它能一次性产生多个汇总统计量:

financial_na_df.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会产生另一种汇总统计量:

industry_cat_series = pd.Series(['IT', 'IT', 'Finance', 'Energy'] * 4)
industry_cat_series.describe()
count     16
unique     3
top       IT
freq       8
dtype: object

表 4.8 列出了一个完整的汇总统计和相关方法的集合。

表 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 计算百分比变化

4.4.1 相关性与协方差 (Correlation and Covariance)

皮尔逊相关系数的数学推导

在深入实践之前,让我们从数学角度理解皮尔逊相关系数的完整推导。

给定两个随机变量 \(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])] \]

协方差矩阵具有以下重要性质:

  1. 对称性\(\Sigma = \Sigma^T\)
  2. 半正定性:对于任意向量 \(\mathbf{w}\)\(\mathbf{w}^T \Sigma \mathbf{w} \geq 0\)
  3. 对角元素\(\Sigma_{ii} = \text{Var}(X_i)\)

现在让我们使用来自本地 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) \]

  • \(\text{Cov} > 0\): 资产趋向于同步运行。
  • \(\text{Cov} < 0\): 资产趋向于对冲运行。

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()
列表 4.4
宁波港 宁波银行 贵州茅台 中国平安
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\) 期的价格。这种计算方法在时间序列分析中是一个基本操作。

returns = price_data.pct_change()
returns.tail()
列表 4.5
宁波港 宁波银行 贵州茅台 中国平安
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

Seriescorr 方法计算两个序列之间重叠的、非 NA 的、按索引对齐的值的相关性。类似地,cov 计算协方差。让我们计算宁波港与宁波银行之间的相关性和协方差:

列表 4.6
# 计算宁波港和宁波银行收益率的相关性
corr_nb = returns['宁波港'].corr(returns['宁波银行'])
print(f'宁波港与宁波银行的相关系数: {corr_nb:.4f}')

# 计算宁波港和宁波银行收益率的协方差
cov_nb = returns['宁波港'].cov(returns['宁波银行'])
print(f'宁波港与宁波银行的协方差: {cov_nb:.6f}')
宁波港与宁波银行的相关系数: 0.3139
宁波港与宁波银行的协方差: 0.000124

实证分析:宁波港与宁波银行的联动解读

宁波港作为港口物流企业(周期性与贸易敏感型),宁波银行作为区域性商业银行(金融顺周期型),两者的正相关性反映了微观层面上实体经济活跃度与资金流动性的共震。在量化策略中,这种适度的相关性(通常在 0.3-0.6)意味着单一行业的冲击无法完全解释两者的波动,从而为区域配置策略提供了分散化空间。

DataFramecorrcov 方法分别返回一个完整的相关性矩阵或协方差矩阵,形式为 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\) 的协方差矩阵。这正是我们需要计算协方差矩阵的原因!

列表 4.7
# 计算所有股票的相关性矩阵
corr_matrix = returns.corr()
print('相关性矩阵:')
print(corr_matrix)

# 计算所有股票的协方差矩阵
cov_matrix = returns.cov()
print('\n协方差矩阵:')
print(cov_matrix)
相关性矩阵:
           宁波港      宁波银行      贵州茅台      中国平安
宁波港   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

金融工程应用:相关性矩阵的多重意义

  1. 分散化边界: 若 \(\rho \to 0\),根据 Markowitz 理论,投资组合的方差将显著低于资产方差的加权平均。
  2. 因子暴露控制: 极高的相关性(如 > 0.85)可能意味着两家公司属于同一细分行业或共同面临相同的宏观政策风险(如房地产调控)。
  3. 统计套利: 寻找历史上相关性极高但短期发生背离的配对(Pair Trading)。

我们的计算展示了长三角龙头企业与白马股(贵州茅台)的联动差异,这为构建 beta 中性或行业中性策略提供了基础。

使用 DataFramecorrwith 方法,你可以计算一个 DataFrame 的列或行与另一个 SeriesDataFrame 之间的成对相关性。例如,我们可以计算所有股票与贵州茅台收益率的相关性:

列表 4.8
# 计算所有股票与贵州茅台的相关性
corr_with_maotai = returns.corrwith(returns['贵州茅台'])
print('各股票与贵州茅台的相关性:')
print(corr_with_maotai)
各股票与贵州茅台的相关性:
宁波港     0.226149
宁波银行    0.353712
贵州茅台    1.000000
中国平安    0.436055
dtype: float64

4.4.2 唯一值、值计数和成员资格 (Unique Values, Value Counts, and Membership)

另一类相关方法用于提取关于一维Series中所含值的信息。

char_series = pd.Series(['c', 'a', 'd', 'a', 'a', 'b', 'b', 'c', 'c'])

unique给你一个Series中唯一值的数组:

uniques = char_series.unique()
uniques
array(['c', 'a', 'd', 'b'], dtype=object)

value_counts计算一个包含值频率的Series,并按降序排序:

char_series.value_counts()
c    3
a    3
b    2
d    1
Name: count, dtype: int64

isin执行一个向量化的集合成员资格检查:

mask = char_series.isin(['b', 'c'])
mask
0     True
1    False
2    False
3    False
4    False
5     True
6     True
7     True
8     True
dtype: bool
char_series[mask]
0    c
5    b
6    b
7    c
8    c
dtype: object

isin相关的是Index.get_indexer方法,它给你一个从可能非唯一值的数组到另一个唯一值数组的索引数组:

labels_to_match = pd.Series(['c', 'a', 'b', 'b', 'c', 'a'])
unique_labels = pd.Series(['c', 'b', 'a'])
indices = pd.Index(unique_labels).get_indexer(labels_to_match)
indices
array([0, 2, 1, 1, 0, 2])

表 4.9 提供了这些方法的参考。

表 4.9: 唯一值、值计数和集合成员资格方法
方法 描述
isin 计算一个布尔数组,指示每个值是否包含在传递的值序列中
get_indexer 为一个数组中的每个值计算到另一个唯一值数组的整数索引;有助于数据对齐和连接类型的操作
unique 计算Series中的唯一值数组,按观察到的顺序返回
value_counts 返回一个Series,其索引为唯一值,值为频率,按计数降序排列

要在DataFrame中的多个相关列上计算直方图(例如处理问卷调查数据):

survey_df = pd.DataFrame({'问题1': [1, 3, 4, 3, 4],
                     '问题2': [2, 3, 1, 2, 3],
                     '问题3': [1, 5, 2, 4, 4]})
survey_df
问题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方法:

result = survey_df.apply(pd.value_counts).fillna(0)
result
问题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的每一行视为一个元组来计算每个不同行的出现次数。

tuple_df = pd.DataFrame({'a': [1, 1, 1, 2, 2], 'b': [0, 0, 1, 0, 0]})
tuple_df.value_counts()
a  b
1  0    2
2  0    2
1  1    1
Name: count, dtype: int64

4.5 习题

4.5.1 基础习题

习题 5.1: Series 创建与索引

创建一个表示中国四个直辖市2023年GDP(单位:万亿元)的Series: - 北京:4.38 - 上海:4.72 - 天津:1.67 - 重庆:3.01

  1. 创建这个Series,并使用城市名作为索引
  2. 添加一个名为’description’的属性,值为’2023年GDP’
  3. 将Series转换为字典
  4. 筛选出GDP大于3万亿元的城市

解答:

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
  1. 创建这个DataFrame
  2. 计算人均GDP(单位:万元)
  3. 按年份分组,计算平均GDP
  4. 按省份分组,计算GDP总和

解答:

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:

s1 = pd.Series([10, 20, 30], index=['a', 'b', 'c'])
s2 = pd.Series([5, 15, 25, 35], index=['a', 'c', 'd', 'e'])
  1. 计算 s1 + s2,并解释结果
  2. 使用 fill_value=0 参数计算加法
  3. 对 s1 进行重建索引,使其包含 s2 的所有索引,缺失值用前向填充(ffill)

解答:

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

4.5.2 进阶习题

习题 5.4: 索引与选择

给定以下DataFrame:

import numpy as np
data = pd.DataFrame(
    np.arange(16).reshape((4, 4)),
    index=['北京', '上海', '广东', '浙江'],
    columns=['one', 'two', 'three', 'four']
)
  1. 选择 ‘two’ 列
  2. 选择行索引为 ‘上海’ 和 ‘浙江’ 的行
  3. 选择 ‘上海’ 和 ‘浙江’ 行的 ‘one’ 和 ‘three’ 列(使用 loc)
  4. 选择前两行和前两列(使用 iloc)
  5. 选择 ‘three’ 列值大于 5 的行

解答:

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:

grades = pd.DataFrame({
    '姓名': ['张三', '李四', '王五', '赵六'],
    '数学': [85, 92, 78, 88],
    '英语': [90, 85, 92, 80],
    '物理': [82, 88, 85, 90]
})
  1. 计算每个学生的总分和平均分
  2. 计算每门课的平均分和标准差
  3. 将分数转换为等级(A: 90-100, B: 80-89, C: 70-79, D: <70)
  4. 找出每门课的最高分学生

解答:

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分)

4.5.3 应用习题

习题 5.6: 股票收益率分析

假设你已经获取了宁波港(601018)和宁波银行(002142)的股票价格数据。

  1. 计算两只股票的日收益率、月收益率和年收益率
  2. 计算两只股票的波动率(标准差)
  3. 绘制两只股票的累计收益曲线
  4. 计算并比较夏普比率(假设无风险利率为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'})

# 准备数据
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: 数据对齐在投资组合中的应用

假设你有以下三个资产的历史收益率数据:

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))

构建一个等权重投资组合(权重各为1/3),计算:

  1. 投资组合的每日收益率序列
  2. 投资组合的累计收益
  3. 投资组合的波动率
  4. 如果每个资产的初始投资为10000元,计算期末组合价值

解答:

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 数据对齐机制在多资产分析中的鲁棒性。

  1. 非同步交易处理:不同资产(如 A 股、港股、美股)有不同的交易日历。Pandas 在合并 Series 时会自动寻找日期的并集。
  2. 算术安全性:在缺失数据的日期(如停牌),Pandas 通过 NaN 强制让计算后果可见,避免了将缺失收益率误计为 0 所导致的风险低估。
  3. 内联 Join 操作:这相当于在底层执行了一个逻辑上的“外连接”(Outer Join),确保了分析的时间维度完整性。

4.5.4 挑战习题

习题 5.8: 实现一个简单的事件研究策略

事件研究 (Event Study) 是金融实证研究中的重要方法,用于评估特定事件(如财报发布、政策出台)对资产价格的影响。

假设你要研究宁波港(601018)在财报发布日前后的异常收益。你需要:

  1. 计算正常收益率(使用市场模型,以沪深300指数为基准)
  2. 计算异常收益率(Actual Return - Normal Return)
  3. 计算累计异常收益率 (CAR)
  4. 进行t检验,判断异常收益是否显著

解答:

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) 在中国市场的具体表现。


4.6 延伸阅读

4.6.1 pandas 深入学习

核心教材 1. 《Python for Data Analysis (3rd Edition)》by Wes McKinney - 第5章:pandas入门 - 第7章:数据清洗与准备 - 第8章:数据规整:合并、重塑和连接

专题深入 2. 《Effective Pandas》(第2版)by Kevin Markham - 涵盖pandas最佳实践、性能优化和高级技巧

  1. 《Pandas in Action》by Boris Paskhaver
    • 通过实际案例学习pandas的各种功能

4.6.2 数据分析与可视化

统计分析 4. 《Practical Statistics for Data Scientists》by Peter Bruce & Andrew Bruce - 第3章:统计方法与pandas应用

可视化 5. 《Python for Finance (2nd Edition)》by Hilpisch & Yves Hilpisch - 第11章:时间序列数据可视化

4.6.3 金融数据分析

量化金融 6. 《Algorithmic Trading with Python》(2024)by Stefan Jansen - 警告:量化交易涉及高风险,学习目的应仅限于教育和研究

  1. 《Python for Finance》by Yves Hilpisch
    • 涵盖pandas在金融分析中的应用

4.6.4 事件研究与学术资源

经典教材 8. 《Event Studies in Finance》by Campbell, Lo, and MacKinlay - 事件研究方法的经典教材

  1. 《Empirical Asset Pricing》by Campbell, Lo, MacKinlay, and Whitelaw
    • 资产定价实证研究的经典著作

4.6.5 在线资源

官方文档 - 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/


4.7 结论

在下一章中,我们将讨论使用pandas读取(或加载)和写入数据集的工具。之后,我们将更深入地使用pandas来研究数据清洗、整理、分析和可视化的工具。

通过本章的学习和习题练习,你应该已经掌握了:

  1. pandas核心数据结构:Series和DataFrame的创建、索引和基本操作
  2. 数据对齐机制:pandas如何通过索引自动对齐数据,这是其强大之处
  3. 数据选择与过滤:使用.loc、.iloc和布尔索引灵活地访问数据
  4. 数据运算:包括算术运算、函数应用和自定义映射
  5. 数据汇总与描述统计:特别是相关性和协方差在金融中的应用
  6. 实战能力:通过习题,你已经能够处理真实的金融数据,进行基本的投资分析

这些技能将为你后续学习更高级的数据分析和机器学习技术打下坚实的基础。