Chapter 02

Series 与 DataFrame
核心数据结构

索引系统、dtypes 体系、内存布局——从底层理解 Pandas 的两大基石

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',相加
ℹ️
索引对齐:优势与陷阱 索引对齐在处理时间序列等有天然对齐需求的数据时非常方便,但在不同索引的 Series 之间运算时容易产生意外的 NaN。如果你确定数据已对齐,可以直接用 .values 属性(得到 NumPy 数组)进行运算,绕过对齐机制。

DataFrame:带标签的二维表格

DataFrame 是 Pandas 最重要的数据结构,可以看作是:

# 从字典创建——最常见的方式
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 数组)。这意味着:

# 查看内存使用详情
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),可以理解为一个有序的、可能重复的、带有元数据的标签集合。

索引类型一览

# 不同索引类型示例

# 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, boolnumpy.int64传统类型,不支持原生 null
Pandas 扩展(Nullable)Int64, Float64, boolean, stringpd.Int64Dtype()支持 pd.NA,大写区分
Pandas 扩展(特殊)category, datetime, timedeltaCategoricalDtype优化特定场景
PyArrow 扩展int64[pyarrow], string[pyarrow]pd.ArrowDtypeArrow 内存格式,通常更快
# 查看和转换 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 对象。它的问题在于:

在 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 参数在 CoW 下的变化 在 Pandas 2.x CoW 模式下,inplace=True 的大多数操作仍然有效,但其语义发生了变化:它只是"尝试"原地修改。在 Pandas 3.0 中,inplace 参数可能被完全移除。建议养成使用链式返回值的习惯:df = df.rename(columns={'a': 'b'}),而不是 df.rename(columns={'a': 'b'}, inplace=True)

小结

Series 和 DataFrame 是 Pandas 的两大基石。理解它们的关键在于:

下一章将介绍如何从各种数据源导入数据到 Pandas,以及如何合理地指定数据类型以避免后续的清洗工作。