Series:带标签的一维数组
Series 是 Pandas 最基本的数据结构,可以理解为"带标签的一维数组"。它由两部分组成:一个存储数据的 values(底层是 NumPy 数组或 Arrow ChunkedArray)和一个存储标签的 Index。这个索引是 Pandas 区别于纯 NumPy 的核心特性。
import pandas as pd
import numpy as np
# 从 Python 列表创建——自动生成 RangeIndex
s1 = pd.Series([10, 20, 30, 40])
print(s1)
# 0 10
# 1 20
# 2 30
# 3 40
# dtype: int64
# 从字典创建——键成为索引
s2 = pd.Series({'北京': 2185, '上海': 2487, '深圳': 1768})
print(s2['北京']) # 2185
# 从 NumPy 数组创建,指定自定义索引
arr = np.array([1.5, 2.3, 3.7])
s3 = pd.Series(arr, index=['a', 'b', 'c'], name='values')
# 关键属性
print(s3.values) # ndarray: [1.5 2.3 3.7]
print(s3.index) # Index(['a', 'b', 'c'], dtype='object')
print(s3.dtype) # float64
print(s3.name) # 'values'
print(s3.shape) # (3,)
Series 的向量化运算
Series 支持与 NumPy 相同的向量化操作,但额外提供了索引对齐(Index Alignment)特性:两个 Series 进行算术运算时,Pandas 会按索引标签自动对齐,而不是按位置对齐。
# 索引对齐的例子
a = pd.Series({'x': 1, 'y': 2, 'z': 3})
b = pd.Series({'y': 10, 'z': 20, 'w': 30})
result = a + b
print(result)
# w NaN ← 只有 b 有 'w',结果为 NaN
# x NaN ← 只有 a 有 'x',结果为 NaN
# y 12.0 ← 都有 'y',相加
# z 23.0 ← 都有 'z',相加
.values 属性(得到 NumPy 数组)进行运算,绕过对齐机制。
DataFrame:带标签的二维表格
DataFrame 是 Pandas 最重要的数据结构,可以看作是:
- 具有共同行索引的多个 Series 的集合
- 一个具有行列双轴标签的二维表格
- 一个有序的列字典,每列可以有不同的 dtype
# 从字典创建——最常见的方式
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Carol', 'Dave'],
'age': [25, 30, 27, 35],
'salary': [60000.0, 80000.0, 75000.0, 90000.0],
'dept': ['engineering', 'marketing', 'engineering', 'sales'],
})
# 基本属性
print(df.shape) # (4, 4)
print(df.dtypes) # 每列的数据类型
print(df.index) # RangeIndex(start=0, stop=4, step=1)
print(df.columns) # Index(['name', 'age', 'salary', 'dept'], dtype='object')
print(df.info()) # 结构摘要,包含内存使用
print(df.describe()) # 数值列的统计摘要
DataFrame 的内存模型
理解 DataFrame 的内存布局对性能优化至关重要。DataFrame 在内存中以列式存储为主:每一列是一个独立的连续内存块(NumPy 数组)。这意味着:
- 按列访问(
df['col'])非常快,因为数据连续 - 按行迭代相对慢,需要跨越不连续的内存区域
- 不同列可以有不同的 dtype,因为它们是独立的数组
# 查看内存使用详情
print(df.memory_usage(deep=True))
# Index 132 ← 行索引占用
# name 264 ← object 类型(字符串)占用较大
# age 32 ← int64:4行 × 8字节
# salary 32 ← float64:4行 × 8字节
# dept 264 ← object 类型
# 每列的值实际上就是一个 NumPy 数组
print(type(df['age'].values)) # <class 'numpy.ndarray'>
print(df['age'].values.dtype) # int64
索引系统深度解析
Pandas 的索引(Index)是其区别于其他数据处理工具最核心的特性。Index 对象是不可变的(immutable),可以理解为一个有序的、可能重复的、带有元数据的标签集合。
索引类型一览
- RangeIndex 最常见的默认索引,表示连续整数范围(0, 1, 2, ...)。内存高效——只存储起始值、终止值和步长,而不是完整的数组。
- Index 通用索引,可存储任意 Python 对象(字符串、整数、混合类型)。大多数情况下 dtype 为 object。
- Int64Index / Float64Index 数值类型的索引,支持高效的数值范围查询。在 Pandas 2.0 中统一为 Index(带数值 dtype)。
- DatetimeIndex 时间序列的专用索引,基于 numpy.datetime64 类型,支持时区感知操作、resample、滚动计算等。详见第9章。
- MultiIndex 多层次索引(层级索引),允许在一个轴上有多层标签,适合表达层次化数据结构。每一层称为一个 level。
- CategoricalIndex 分类索引,底层使用整数编码存储,适合有限取值集合(如性别、城市)的索引,节省内存并加速 groupby。
# 不同索引类型示例
# 1. 自定义字符串索引
df_custom = pd.DataFrame(
{'value': [10, 20, 30]},
index=['apple', 'banana', 'cherry']
)
print(df_custom.loc['banana']) # value=20
# 2. DatetimeIndex
dates = pd.date_range('2024-01-01', periods=5, freq='D')
ts = pd.Series(np.random.randn(5), index=dates)
print(ts['2024-01']) # 支持字符串切片
# 3. MultiIndex(层次化索引)
arrays = [
['北京', '北京', '上海', '上海'],
['2023', '2024', '2023', '2024']
]
multi_idx = pd.MultiIndex.from_arrays(arrays, names=['city', 'year'])
df_multi = pd.DataFrame({'gdp': [4.0, 4.5, 5.2, 5.8]}, index=multi_idx)
print(df_multi.loc['北京']) # 取所有北京的数据
print(df_multi.loc[('北京', '2024')]) # 取具体一行
Index 的重要方法
idx = pd.Index(['a', 'b', 'c', 'd'])
# 集合操作——索引支持类似集合的操作
idx2 = pd.Index(['c', 'd', 'e'])
print(idx.union(idx2)) # ['a', 'b', 'c', 'd', 'e']
print(idx.intersection(idx2)) # ['c', 'd']
print(idx.difference(idx2)) # ['a', 'b']
# 索引对象的不可变性
try:
idx[0] = 'x' # TypeError: Index does not support mutable operations
except TypeError as e:
print(e)
# 重新设置索引
df_reset = df_custom.reset_index() # 旧索引变为普通列
df_set = df.set_index('name') # 某列变为索引
数据类型(dtypes)完整体系
Pandas 的 dtype 系统经过 2.x 的扩展,现在包含三个层次:
| 层次 | 类型 | 代表值 | 说明 |
|---|---|---|---|
| NumPy 原生 | int64, float64, bool | numpy.int64 | 传统类型,不支持原生 null |
| Pandas 扩展(Nullable) | Int64, Float64, boolean, string | pd.Int64Dtype() | 支持 pd.NA,大写区分 |
| Pandas 扩展(特殊) | category, datetime, timedelta | CategoricalDtype | 优化特定场景 |
| PyArrow 扩展 | int64[pyarrow], string[pyarrow] | pd.ArrowDtype | Arrow 内存格式,通常更快 |
# 查看和转换 dtype
df = pd.DataFrame({
'a': [1, 2, 3], # int64(NumPy)
'b': [1.0, 2.5, 3.0], # float64(NumPy)
'c': ['x', 'y', 'z'], # object(字符串)
'd': [True, False, True], # bool
})
# 类型转换
df['a'] = df['a'].astype('Int64') # 转为 Nullable Int64
df['c'] = df['c'].astype('category') # 转为分类类型
df['d'] = df['d'].astype('boolean') # 转为 Nullable boolean
# 批量转换:推断更好的类型(Pandas 2.x 新功能)
df_better = df.convert_dtypes() # 自动推断最优 nullable 类型
print(df_better.dtypes)
object dtype 的问题
object dtype 是 Pandas 的历史遗留类型,通常用于存储字符串,但实际上可以存储任意 Python 对象。它的问题在于:
- 每个元素都是一个 Python 对象(指针),内存开销大
- 字符串操作无法向量化,必须逐元素处理,速度慢
- groupby、sort 等操作比数值类型慢数倍
在 Pandas 2.x 中,应该尽量将字符串列转换为 string(Nullable string)或 string[pyarrow](PyArrow string),以获得更好的性能和内存效率。
DataFrame 的创建方式汇总
import pandas as pd
import numpy as np
# 1. 从列表的字典(最常用)
df1 = pd.DataFrame({'A': [1,2,3], 'B': [4,5,6]})
# 2. 从字典的列表(每个字典是一行)
df2 = pd.DataFrame([
{'name': 'Alice', 'age': 25},
{'name': 'Bob', 'age': 30},
])
# 3. 从 NumPy 二维数组
arr = np.arange(12).reshape(3, 4)
df3 = pd.DataFrame(arr, columns=['W','X','Y','Z'])
# 4. 从 Series 的字典
df4 = pd.DataFrame({
'x': pd.Series([1,2], index=['a','b']),
'y': pd.Series([3,4,5], index=['a','b','c']),
})
# x 列在 'c' 行的值为 NaN(因为 Series x 没有 'c' 标签)
# 5. 从文件(最常见的实际场景,见第3章)
# df5 = pd.read_csv('data.csv')
常用基础操作速查
# ── 查看数据 ──
df.head(5) # 前5行
df.tail(5) # 后5行
df.sample(5) # 随机5行
df.info() # 结构信息(列名、非空数量、dtype)
df.describe() # 统计摘要(count/mean/std/min/max等)
df.describe(include='all') # 包含非数值列
df.value_counts() # 各列唯一值计数
# ── 列操作 ──
df['col'] # 选择单列,返回 Series
df[['col1', 'col2']] # 选择多列,返回 DataFrame
df.rename(columns={'old': 'new'}) # 重命名列
df.drop(columns=['col']) # 删除列
df['new_col'] = df['a'] + df['b'] # 新增列
df.assign(new_col=df['a'] * 2) # 函数式新增列(返回新 DataFrame)
# ── 行操作 ──
df.drop(index=[0, 1]) # 删除指定行
df.sort_values('col', ascending=False) # 按列排序
df.sort_index() # 按索引排序
inplace=True 的大多数操作仍然有效,但其语义发生了变化:它只是"尝试"原地修改。在 Pandas 3.0 中,inplace 参数可能被完全移除。建议养成使用链式返回值的习惯:df = df.rename(columns={'a': 'b'}),而不是 df.rename(columns={'a': 'b'}, inplace=True)。
小结
Series 和 DataFrame 是 Pandas 的两大基石。理解它们的关键在于:
- 索引是 Pandas 最核心的特性,提供了标签化访问和自动对齐能力
- dtype 体系在 2.x 中扩展为四个层次,应优先使用 Nullable 扩展类型或 PyArrow 类型
- 列式存储是 DataFrame 的内存布局,决定了按列操作快、按行迭代慢的性能特性
- 避免使用
objectdtype 存储字符串,改用string或string[pyarrow]
下一章将介绍如何从各种数据源导入数据到 Pandas,以及如何合理地指定数据类型以避免后续的清洗工作。