【Python】一、Python 数据类型:从 List 到 NumPy array 到 pandas Series 的数据结构演进

从 Python list 和原生数组出发,逐步梳理 NumPy array、pandas Series 与 DatetimeIndex 如何补足批量计算、标签索引和时间语义能力。

全文共计5289字 次阅读

最近因为接触了一些异常数据检测的概念,需要用到一些 Python 代码,我便整理了一些可以放到一起理解的概念:

从 List / 数组 → NumPy array → pandas Series → DatetimeIndex

一、引言

这四个概念可以按"为了解决上一层的局限性,逐步叠加新能力"来理解,但这是一条通过联想加深认知的学习方式,他们彼此之间并没有严格的类型继承链:

  • Python list / C#·Java 原生数组 —— 最基础的"容器",解决"批量存放数据"的问题。

  • NumPy array —— 在数组基础上解决"大批量数值如何高效计算"的问题。

  • pandas Series —— 在 NumPy array 基础上解决"数据如何带上业务含义的标签"的问题。

  • pandas DatetimeIndex —— 作为 Index 的时间类型特化,可用作 Series/DataFrame 的索引,解决"时间维度的数据如何做时间专属操作"的问题。

一句话记忆:

list 是通用容器(可以混装不同类型,但没有原生的逐元素数值运算);

NumPy array 是数组计算工具(单一 dtype、形状与步长、向量化);

Series = NumPy array + 标签(既能像数组一样批量计算,又能像 Map 一样按业务标签取值和自动对齐);

DatetimeIndex 是专门为"时间"标签提供时间语义的 Index 子类。


二、Python list 与 C#/Java 原生数组

2.1 定义

Python 的 list 是可变长度、可混合存放不同类型对象的序列。在常用的 CPython 实现中,list 内部维护的是一组对象引用(这是 CPython 的实现细节,而不是 Python 语言规范对所有解释器的内存布局要求)

C#/Java 的数组(如 int[])则是定长、元素类型固定的容器,声明时就固定了元素类型

2.2 原生数组的局限性——为什么还需要 NumPy

  • Python list 没有定义逐元素乘法等数组数值语义,通常需要通过循环、列表推导式或其他迭代工具逐个处理。在 Python 中,对象引用间接访问和解释器循环开销会使它不适合大规模数值密集型计算。
1
2
3
4
5
6
py_list = [1, 2, 3, 4]

# 想让每个元素乘 2,没有原生的批量语法,只能:
result = [x * 2 for x in py_list]

print(result)  # [2, 4, 6, 8]
  • 即便是 C#/Java 的强类型数组也需要显式循环或数值计算库,虽然较Python在存储解决了内存连续的问题,提高了CPU 缓存命中率,但也没有"向量化运算"和"广播机制"这类高阶数值计算能力。
  • NumPy 的优势主要是直接提供了高层数组运算、广播和丰富的数值算法。

三、NumPy array

NumPy array 则为 Python 提供了高效、统一的批量数值计算模型。

3.1 定义

numpy.ndarray 是具有单一 dtype(如 int64float64)、形状(shape)和步长(strides)的多维数组。数据通常位于连续缓冲区中,但切片、转置等操作可以产生非连续视图,因此不能笼统地说每个 ndarray 都连续存储。它是 SciPy、pandas 等 Python 科学计算工具的重要基础。

3.2 相较原生数组 / list,新增了什么能力

  • 类型强制统一(dtype):牺牲了 Python list 的"随意混装"自由度,换来了和 C#/Java 数组类似的连续内存布局。数组使用一个 dtype 解释元素,适合采用紧凑、规则的数值表示;若使用 object dtype,元素仍然可以是 Python 对象,但此时通常无法获得纯数值 dtype 的性能优势。
  • 向量化运算(Vectorization)np.array * 2np.sin(arr) 等操作直接在底层用编译好的 C/SIMD 指令(Single Instruction, Multiple Data单指令多数据流的缩写)批量执行,避免 Python 层逐项调度。
  • 广播机制(Broadcasting):形状不同的数组之间可以按规则自动"扩展"后做逐元素运算,例如二维数组直接加一维数组,无需手写嵌套循环。
  • 基于 shape/strides 的多维视图reshapetranspose(转置/换轴)等操作只是改变了对同一块内存的"解读方式",不复制底层数据,这一点比 C#/Java 的多维数组更高效。
    • transpose 通常返回共享底层数据的视图;
    • reshape 在步长允许时返回视图,否则可能复制数据。

代码示例:向量化运算 vs 手写循环

1
2
3
4
import numpy as np

arr = np.arange(1_000_000)
result = arr * 2          # 向量化:一次性对整块内存做运算

代码示例:广播机制

1
2
3
4
5
6
a = np.array([[1, 2, 3], [4, 5, 6]])   # shape (2, 3)
b = np.array([10, 20, 30])              # shape (3,)

print(a + b)
# [[11 22 33]
#  [14 25 36]]   ← b 被自动"广播"到每一行

使用场景

在生产场景里,比如生成一段带日周期性的模拟指标数据(用于测试异常检测算法),就是典型的 NumPy 向量化用法:

1
2
3
4
5
6
7
8
rng = np.random.default_rng(42)
hours = np.arange(24 * 14)                      # 336 个小时刻度
values = (
    20
    + 2 * np.sin(2 * np.pi * hours / 24)          # 日周期波动,向量化计算
    + rng.normal(0, 0.35, len(hours))              # 批量生成高斯噪声
)
# 336 个数值一次性算出,没有一行 Python 级别的 for 循环

四、pandas Series

pandas Series 则解决了 NumPy array “数据如何带上业务标签"的问题。

4.1 定义

pd.Series 是带索引的一维数据结构,由一维数据容器和等长的 Index 组成。数据容器常见为 NumPy ndarray,也可能是 pandas ExtensionArray(例如可空整数、分类类型或 Arrow 支持的类型)。位置(第几个元素)是隐式存在的顺序,不需要单独存储。

4.2 为什么需要它——NumPy array 的局限

  • 普通 ndarray 支持整数位置、切片、整数数组和布尔数组等索引方式(如arr[0]),但没有独立的业务标签轴,无法用日期、用户 ID、股票代码等标签直接取值。
  • 如果想让"第 3 个数"表示"2026-01-01 的销售额”,用纯 NumPy 只能自己维护一份平行的位置映射表,很繁琐。

代码示例:普通 ndarray 不支持标签索引

1
2
3
4
5
import numpy as np

arr = np.array([10, 20, 30])
arr[0]        # 10  (按位置访问)
arr['x']      # 报错:IndexError: only integers, slices... are valid indices

4.3 相较 NumPy array,新增了什么能力

  • 标签索引(index):可以用任意类型的标签(日期、字符串、整数等)来标记每个数据点,语义上更贴近业务。
  • 双访问器.loc 按"标签值"查找,.iloc 按"位置序号"查找——两者是读取同一份底层数据的两种方式,而不是索引本身由两部分构成。
  • 自动对齐运算:两个 Series 相加时,会按标签做"并集对齐",任一方缺失该标签,结果即为 NaN;这不同于典型 Dict 合并只处理交集的写法。
  • 批量运算与 NumPy 相互操作:Series 实现了逐元素运算,并能与许多 NumPy ufunc(如 np.sin())协作(这是接口互操作,不是说 Series 继承自 ndarray)

注意:pandas Series 依然保留 NumPy 的向量化能力,可以直接做 *np.sin() 等批量运算

代码示例:.loc 与 .iloc

.loc.iloc 不是索引的组成部分,而是两种不同的访问器(accessor),它们定位的是同一个 Series 中的数据:

  • Series 维护一维数据容器和与其等长的 Index,二者按位置一一对应。

  • 位置(position) 不需要单独存储,它就是数据序列中的隐式顺序,任何数组结构天然自带位置概念(就像 Dict 底层的哈希桶顺序,只是通常不暴露)。

  • .loc['x'] 是"按标签值查找",.iloc[0] 是"按位置查找";它们是两套访问方式,并不意味着索引由"标签索引 + 位置索引"两部分组成。

打个类比:这就像一本书既可以按"页码"(位置,类似 iloc)翻到某页,也可以按"目录标题"(标签,类似 loc)跳转到某章——但"页码"和"标题"并不是这本书内容本身的两个组成部分,它们只是两种定位/访问同一份内容的方式

1
2
3
s = pd.Series([100, 200, 300], index=['x', 'y', 'z'])
s.loc['x']   # 100   按标签取值
s.iloc[0]    # 100   按位置取值,效果一样,但语义不同

代码示例:自动对齐(并集 + NaN)

1
2
3
4
5
6
7
s1 = pd.Series({'a': 1, 'b': 2, 'c': 3})
s2 = pd.Series({'b': 10, 'c': 20, 'd': 30})
print(s1 + s2)
# a     NaN   ← s2 没有 'a',结果直接是 NaN
# b    12.0
# c    23.0
# d     NaN   ← s1 没有 'd'

Python dict 没有定义两个字典之间的逐值加法;字典合并关注键值覆盖,Series 加法关注按标签对齐后的数值运算,两者不是同一种操作。

4.4 与 Python dict 的关系与本质区别

把 Series 暂时理解为"数组 + 字典式标签访问"有助于入门,但它与 Python dict 有几个关键区别:

  • Key 唯一性:dict 的 key 强制唯一;Series 的 index 允许重复标签共存。不过,重复标签会让重建索引等操作受限,实际建模时仍要判断标签是否应该唯一。
  • 计算语义:dict 用 key 查找和管理离散对象;Series 以一维数据和 Index 为核心,提供逐元素计算、缺失值处理和按标签对齐。

代码示例:Series 索引允许重复,Dict 的 key 不允许

1
2
3
4
5
s = pd.Series([1, 2, 3], index=['a', 'a', 'b'])
print(s)
# a    1
# a    2   ← 同一个 'a' 出现两次,这在 dict 里是不可能的
# b    3

使用场景

在生产监控场景中,把一段时间序列指标(比如每小时数据量)存成以 DatetimeIndex 为索引的 Series,既能做均值、标准差等批量统计,又能用 s.loc['2026-01-05'] 按日期取值、比对基线(Baseline),这正是带标签数据结构相比裸 NumPy array 的价值所在。


五、pandas DatetimeIndex —— 提供时间语义的索引

5.1 定义

pd.DatetimeIndexIndex 的一个子类,用来表示时间点,可使用不同分辨率的 datetime64 dtype,也支持带时区的日期时间。它常被用作 Series/DataFrame 的时间轴,但自身只保存索引标签,不保存观测值。

5.2 为什么需要专门的时间索引——普通 index 不够用在哪

  • 字符串可以写成日期的样子,但普通字符串 Index 不具备日历偏移、时区、时间频率和按日期解析等语义。
  • ADTK 的 validate_series 明确要求 Series/DataFrame 使用 DatetimeIndex,否则抛出 TypeError。但这项校验并不意味着输入一定等间隔,也不代表所有不规则序列都会被拒绝。

5.3 相较普通 Index,新增了什么能力

  • 频率元数据freq 可以记录固定频率(如每小时 'h'、每天 'D')。规律索引的 freq 也可能是 None,此时可查看 inferred_freqfreq 缺失并不会直接禁止 resampleasfreq
  • 时间切片:可以直接用日期字符串做区间筛选,例如 series['2026-01-05':'2026-01-10'],普通 Index 无法这样按"语义区间"切片。
  • 支持重采样与频率转换resample()asfreq() 是 Series/DataFrame 的方法;当对象使用 DatetimeIndex 时,它们可以按日历频率分组或重建时间轴,并且也能处理 freq=None 的不规则输入。
  • 支持时间窗口滚动:整数窗口(如 rolling(window=24))不要求 DatetimeIndex;时间偏移窗口(如 rolling('24h'))才依赖 DatetimeIndex 等可按时间计算的索引。

代码示例:生成规律的 DatetimeIndex 并做时间切片/重采样

1
2
3
4
5
6
7
8
import pandas as pd

index = pd.date_range('2026-01-01', periods=24 * 14, freq='h')
s = pd.Series(range(len(index)), index=index)

s['2026-01-05']                 # 时间切片:取某一天的全部小时数据
s.resample('D').mean()          # 重采样:按天聚合,求每天的均值
s.rolling('24h').mean()         # 时间窗口:过去 24 小时的滑动平均

使用场景:结合 ADTK 的 validate_series

在 ADTK 0.6.2 中,validate_series 会复制输入、按时间升序排序、删除重复时间戳(保留首次出现的值),并在 freq 缺失但 inferred_freq 可推断时调用 asfreq() 补上频率元数据。若索引不是 DatetimeIndex,则直接报错。

需要特别注意:如果时间点确实不规则,inferred_freq 也是 Nonevalidate_series 会原样返回这组不规则时间点,不会自动插入缺失时间点,也不会因此报错。是否重采样、如何填补缺失值,应由业务在校验前显式决定。

1
2
3
4
5
6
7
8
9
import numpy as np
import pandas as pd
from adtk.data import validate_series

index = pd.date_range('2026-01-01', periods=24 * 14, freq='h')
values = 20 + 2 * np.sin(2 * np.pi * np.arange(len(index)) / 24)
series = pd.Series(values, index=index, name='metric')

validated = validate_series(series)  # 返回校验后的副本;必要时排序、去重、补充可推断的 freq

六、能力组合总结:每一层补充了什么

层级新增的核心能力边界与代价
Python list灵活存放和增删 Python 对象没有原生逐元素数值运算,CPython 层循环开销较高
NumPy ndarray统一 dtype、多维形状、向量化和广播没有独立业务标签;视图不一定连续,部分变形操作可能复制数据
pandas Series一维标签索引、缺失值处理和按标签对齐需要维护 Index;数据容器可能是 ndarray,也可能是 ExtensionArray
DatetimeIndex(用作 Series 的索引)时间解析、日历偏移、时区、时间切片和频率元数据它只是索引而非新一层观测值容器;freq 可能缺失,不规则性需显式处理

七、综合对比表格

维度Python listNumPy ndarraypandas SeriesDatetimeIndex(作为索引)
元素表示可混合不同 Python 对象单一 dtype单一 dtype,可由 ndarray 或 ExtensionArray 承载datetime64 的不同分辨率,或带时区日期时间
核心结构动态序列;CPython 中保存对象引用数据缓冲区 + shape + strides一维数据容器 + Index日期时间数组 + 时间相关元数据
访问方式位置、切片位置、切片、整数数组、布尔数组等.iloc 按位置,.loc 按标签时间标签、日期字符串和时间区间
批量计算通过循环、推导式等显式迭代向量化、广播逐元素运算、按标签对齐、与许多 NumPy ufunc 互操作日期偏移和时间字段运算;自身不是观测值容器
标签唯一性无标签概念无独立标签轴允许重复标签,但部分操作要求唯一允许重复时间戳;ADTK 校验时会保留第一个并删除其余重复项
两对象运算对齐无自动对齐按 shape 执行或广播通常按标签对齐,缺失标签产生缺失值作为宿主对象的对齐标签
特有能力类型灵活、通用增删多维视图、reshape、transpose、广播缺失值处理、自动对齐、.loc/.iloc时区、日历偏移、时间切片、频率元数据;支持宿主对象按时间重采样和滚动
典型使用场景通用业务对象集合科学计算、批量数值处理带业务标签的一维数据监控指标、异常检测和按时间汇总的报表
  • list 提供灵活的通用序列;

  • NumPy ndarray 提供统一 dtype 的多维批量计算;

  • Series 在一维数据上增加 Index、缺失值和自动对齐语义;

  • DatetimeIndex 则是可用于 Series/DataFrame 的 Index 子类,为标签补充时间解析、日历和频率能力。它们适合按能力逐层理解,但不是一条简单的继承关系。

使用 Hugo 构建
主题 StackJimmy 设计
无法复制,本站文章内容受保护