Pandas:表格数据瑞士军刀 (Pandas Basics)


章节概述

量化分析处理的是表格化的金融数据:日线行情、财务报表、持仓记录。Pandas 是 Python 生态中处理二维表格数据的标准库。本章从 C 程序员的视角介绍 DataFrame 和 Series 的数据结构、CSV/Excel 读写、过滤/分组/合并操作,以及 .loc/.iloc 的索引哲学。最后简要介绍 Polars —— 用 Rust 重写的极速替代品。

核心理念:如果说 NumPy 是”带 stride 的 C 数组”,那 Pandas 就是”带标签的 NumPy 列向量集合”。DataFrame 底层每一列都是一个 NumPy 数组,而行索引和列标签则提供了比数字下标更强大的数据访问方式。你仍然可以用 C 程序员的直觉来理解它:内存布局、类型一致性、向量化操作——这些核心概念不变。


第一节:Series 与 DataFrame —— 两种核心数据结构

1.1 Series:带标签的一维数组

python -c "
import pandas as pd
import numpy as np
 
# Series = index + values(底层的 NumPy 数组)
s = pd.Series([1.0, 2.5, 3.8, 5.2], index=['a', 'b', 'c', 'd'])
print(s)
print()
print('values type:', type(s.values)) # numpy.ndarray
print('dtype:', s.dtype) # float64
"

Series 可以看作两个数组的组合:

  • s.values —— 底层的 NumPy 数组(和 C 数组一样连续存储)
  • s.index —— 标签索引(Python 对象数组)
python -c "
import pandas as pd
 
s = pd.Series([10, 20, 30, 40], index=['AAPL', 'GOOG', 'MSFT', 'TSLA'])
print('by label:', s['AAPL']) # 用标签访问
print('by position:', s.iloc[0]) # 用位置访问(和 C 数组相同)
print(s[['AAPL', 'TSLA']]) # 花式索引
"

1.2 DataFrame:带标签的二维表格

DataFrame 底层是多个 Series 的列对齐组合。每一列是一个独立的 NumPy 数组:

python -c "
import pandas as pd
import numpy as np
 
df = pd.DataFrame({
 'symbol': ['AAPL', 'GOOG', 'MSFT'],
 'price': [150.0, 2800.0, 330.0],
 'volume': [80000000, 1200000, 25000000]
})
print(df)
print()
print('dtypes:')
print(df.dtypes)
# price 列底层是 float64 的 NumPy 数组
# volume 列底层是 int64 的 NumPy 数组
# symbol 列底层是 object 数组(Python 字符串)
"

数据结构类比

概念C 语言NumPyPandas
一维数组double arr[100]np.array (ndarray)pd.Series
二维表格double mat[100][10]np.array (2D ndarray)pd.DataFrame
元素访问arr[i]arr[i]s.iloc[i](位置) / s.loc[label](标签)
类型信息doubledtypedtype(列级别)

1.3 DataFrame 的内存真相

python -c "
import pandas as pd
import numpy as np
 
df = pd.DataFrame({
 'a': np.random.randn(1000000),
 'b': np.random.randn(1000000),
 'c': np.random.randn(1000000)
})
print('memory usage (MB):')
print(df.memory_usage(deep=True) / 1024 / 1024)
# 每列约 8MB(100万 * 8字节 float64)
# 三列总计约 24MB — 和 C 中 double matrix[3][1000000] 一样!
"

第二节:数据读写 —— CSV、Excel 与更多

2.1 CSV:量化数据的默认格式

python -c "
import pandas as pd
 
# 写入示例数据
df = pd.DataFrame({
 'date': ['2024-01-02', '2024-01-03', '2024-01-04'],
 'open': [150.0, 151.5, 149.8],
 'high': [152.0, 153.0, 151.0],
 'low': [149.0, 150.0, 148.5],
 'close':[151.5, 150.8, 150.2],
 'volume':[80000000, 75000000, 82000000]
})
df.to_csv('/tmp/sample_ohlcv.csv', index=False)
print('written to /tmp/sample_ohlcv.csv')
"
python -c "
import pandas as pd
 
df = pd.read_csv('/tmp/sample_ohlcv.csv')
print(df)
print()
print(df.describe()) # 统计摘要
"

关键参数:

  • parse_dates=['date'] — 自动解析日期列
  • index_col='date' — 将某列设为行索引
  • dtype={'volume': 'int64'} — 指定列的数据类型
  • usecols=['date', 'close'] — 只读需要的列

2.2 Excel:财务报表的常用格式

python -c "
import pandas as pd
# 读取 Excel(需要 openpyxl 或 xlrd)
# df = pd.read_excel('financials.xlsx', sheet_name='Balance Sheet', skiprows=3)
# df.to_excel('output.xlsx', sheet_name='Summary', index=False)
print('Excel read/write requires: pip install openpyxl')
"

2.3 与 NumPy 的无缝互操作

# DataFrame 的某一列就是一个 NumPy 数组
close_prices = df['close'].values # 返回 np.ndarray
returns = np.diff(np.log(close_prices)) # 可以直接做 NumPy 运算
 
# NumPy 数组可以直接构造成 DataFrame
new_df = pd.DataFrame(np.random.randn(100, 4),
 columns=['A', 'B', 'C', 'D'])

向量化计算技巧详见 NumPy 向量化


第三节:数据筛选与索引 —— .loc 与 .iloc

3.1 .iloc:位置索引(C 程序员最熟悉的模式)

iloc 就是 C 数组的 [i][j]——纯粹的整数位置索引:

python -c "
import pandas as pd
import numpy as np
 
np.random.seed(42)
df = pd.DataFrame(np.random.randn(5, 4),
 columns=['A', 'B', 'C', 'D'])
print('original:')
print(df)
print()
 
# 位置索引 — 和 C 的 df[i][j] 一样
print('df.iloc[2, 1]:', df.iloc[2, 1]) # 第 3 行,第 2 列
print('df.iloc[1:3, 0:2]:') # 行切片 + 列切片
print(df.iloc[1:3, 0:2])
print('df.iloc[[0, 4], [1, 3]]:') # 花式索引
print(df.iloc[[0, 4], [1, 3]])
"

3.2 .loc:标签索引(Pandas 独门武器)

python -c "
import pandas as pd
 
# 行有标签,列也有标签
df = pd.DataFrame(
 {'price': [150, 2800, 330], 'pe': [28, 22, 35]},
 index=['AAPL', 'GOOG', 'MSFT']
)
print('AAPL row:')
print(df.loc['AAPL'])
print()
print('price column:', df.loc[:, 'price'].values)
print('boolean condition:', df.loc[df['pe'] < 30])
"

3.3 布尔索引:条件筛选的主力

python -c "
import pandas as pd
import numpy as np
 
np.random.seed(1)
df = pd.DataFrame({
 'returns': np.random.randn(1000) * 0.02,
 'volume': np.random.randint(1000000, 10000000, 1000)
})
 
# C 思维:遍历所有行检查条件
# Pandas 思维:一行布尔表达式
high_vol_up = df[(df['volume'] > 5000000) & (df['returns'] > 0)]
print(f'{len(high_vol_up)} days with high volume and positive returns')
print('condition speed: vectorized, not loop-based')
"

常用筛选模式:

# 等值筛选
df[df['symbol'] == 'AAPL']
 
# 范围筛选
df[(df['price'] >= 100) & (df['price'] <= 200)]
 
# 列表筛选
df[df['symbol'].isin(['AAPL', 'GOOG', 'MSFT'])]
 
# 字符串匹配
df[df['symbol'].str.startswith('A')]
 
# 空值筛选
df[df['price'].notna()]

第四节:分组、聚合与透视表

4.1 groupby:分类聚合

python -c "
import pandas as pd
import numpy as np
 
np.random.seed(0)
df = pd.DataFrame({
 'sector': ['Tech', 'Tech', 'Finance', 'Finance', 'Tech'],
 'symbol': ['AAPL', 'MSFT', 'JPM', 'GS', 'GOOG'],
 'returns': np.random.randn(5) * 0.02,
 'volume': np.random.randint(1e6, 1e7, 5)
})
print('By sector mean returns:')
print(df.groupby('sector')['returns'].mean())
print()
print('Multiple aggregations:')
print(df.groupby('sector').agg({
 'returns': ['mean', 'std', 'count'],
 'volume': 'sum'
}))
"
python -c "
import pandas as pd
 
# 更复杂的示例:多列分组 + transform
df = pd.DataFrame({
 'date': ['2024-01', '2024-01', '2024-01', '2024-02', '2024-02'],
 'symbol': ['AAPL', 'GOOG', 'MSFT', 'AAPL', 'GOOG'],
 'price': [150, 2800, 330, 155, 2850]
})
# 计算每个 symbol 的月度价格变化
df['prev_price'] = df.groupby('symbol')['price'].shift(1)
df['pct_change'] = (df['price'] / df['prev_price'] - 1) * 100
print(df)
"

4.2 merge:表连接

python -c "
import pandas as pd
 
prices = pd.DataFrame({
 'symbol': ['AAPL', 'GOOG', 'MSFT'],
 'price': [150, 2800, 330]
})
fundamentals = pd.DataFrame({
 'symbol': ['AAPL', 'GOOG', 'TSLA'],
 'pe': [28, 22, 45]
})
 
# 类似 SQL JOIN
inner = prices.merge(fundamentals, on='symbol', how='inner')
left = prices.merge(fundamentals, on='symbol', how='left')
outer = prices.merge(fundamentals, on='symbol', how='outer')
 
print('inner (only matching):', list(inner['symbol']))
print('left (all prices):', list(left['symbol']))
print('outer (all symbols):', list(outer['symbol']))
"

4.3 pivot_table:透视表

python -c "
import pandas as pd
import numpy as np
 
np.random.seed(1)
df = pd.DataFrame({
 'date': np.repeat(['2024-Q1', '2024-Q2', '2024-Q3', '2024-Q4'], 3),
 'sector': ['Tech', 'Finance', 'Energy'] * 4,
 'returns': np.random.randn(12) * 0.05
})
pivot = df.pivot_table(
 values='returns',
 index='date',
 columns='sector',
 aggfunc='mean'
)
print(pivot)
"

第五节:Polars —— 更快的 Pandas 替代品

Polars 用 Rust 重写了 DataFrame,提供零拷贝、惰性计算和 SIMD 优化:

python -c "
# pip install polars
import polars as pl
import numpy as np
 
# 类似 Pandas 但语法略有不同
df = pl.DataFrame({
 'symbol': ['AAPL', 'GOOG', 'MSFT'],
 'price': [150.0, 2800.0, 330.0],
 'volume': [80000000, 1200000, 25000000]
})
print(df)
print()
# Polars 的操作链式调用
result = df.filter(pl.col('price') > 200).select(['symbol', 'price'])
print(result)
"

Pandas vs Polars 关键差异:

特性PandasPolars
底层语言C/CythonRust
索引有行索引/列标签无行索引(更接近 NumPy)
并行计算部分操作几乎所有操作
惰性计算pl.LazyFrame
内存效率一般优秀(Apache Arrow 格式)
生态成熟度极成熟快速成长中

对于大数据量的量化回测,Polars 可以带来 5-10 倍的性能提升。但 Pandas 的生态(文档、教程、第三方集成)目前仍然远胜 Polars。



练习

以下题目用于验证本章所学内容:

题号题目链接涉及知识点
175组合两个表https://leetcode.cn/problems/combine-two-tables/表连接、数据合并
176第二高的薪水https://leetcode.cn/problems/second-highest-salary/排序去重、子查询
178分数排名https://leetcode.cn/problems/rank-scores/窗口函数、排名思想