Pandas Series数据结构

带标签的一维数组

一、概述

Pandas 是 Python 生态中最核心的数据分析库之一,而 Series 是 Pandas 的两种基本数据结构之一(另一种是 DataFrame)。可以将 Series 理解为一个带标签的一维数组——它既有传统数组(如 NumPy ndarray)的数值计算能力,又具备类似字典的键值对访问特性。

具体来说,Series 由两个紧密耦合的部分组成:

这种"数据 + 标签"的双重结构,使得 Series 在数据分析和处理中既保留了数组的高效计算能力,又提供了灵活的标签化数据访问方式。在深入使用之前,需要确保 Pandas 库已正确安装:

安装命令
pip install pandas numpy

然后在 Python 脚本或 Jupyter Notebook 中导入:

导入 Pandas
import pandas as pd import numpy as np

导入后即可开始创建和操作 Series 对象。以下从最常见的创建方式开始逐步展开。

二、Series的创建

Pandas 提供了多种创建 Series 的方式,以适应不同的数据来源场景。核心构造函数是 pd.Series(data, index=index),其中 data 可以是列表(list)、字典(dict)、标量值或 NumPy 的 ndarray。下面逐一演示这四种创建方式。

2.1 从列表(list)创建

将一个 Python 列表传入 Series 构造函数,Pandas 会自动为每个元素分配默认的整数索引(RangeIndex)。

示例:从列表创建 Series
# 从列表创建,使用默认 RangeIndex(0, 1, 2, ...) data = [10, 20, 30, 40, 50] s = pd.Series(data) print(s)
输出
0 10 1 20 2 30 3 40 4 50 dtype: int64

可以看到输出分为两列:左侧是索引标签(0, 1, 2, 3, 4),右侧是对应的数据值。底部显示的 dtype: int64 表示 Pandas 自动推断出了整数数据类型。

2.2 从字典(dict)创建

将 Python 字典传入时,字典的键自动成为索引标签值成为数据。这是最自然地表达"标签-值"对应关系的方式。

示例:从字典创建 Series
# 从字典创建:键 -> 索引,值 -> 数据 population = { "北京": 2189, "上海": 2475, "广州": 1868, "深圳": 1756 } s_pop = pd.Series(population) print(s_pop)
输出
北京 2189 上海 2475 广州 1868 深圳 1756 dtype: int64

此时,索引标签即为城市名称,数据为对应的人口数量(单位:万人)。这种方式在数据本身具有明确的键名映射关系时非常便捷。

重要细节:索引顺序

从字典创建 Series 时,索引标签的顺序默认遵循字典的插入顺序(Python 3.7+ 中字典保持插入顺序)。如果需要自定义索引顺序,可以通过 index 参数指定。

2.3 从标量(scalar)创建

传入一个单一的标量值,必须同时指定 index 参数来确认 Series 的长度,Pandas 会将标量值重复填充到每个索引位置。

示例:从标量创建 Series
# 从标量值创建:指定 index 确定长度 s_scalar = pd.Series(0, index=["a", "b", "c", "d", "e"]) print(s_scalar)
输出
a 0 b 0 c 0 d 0 e 0 dtype: int64

这在需要初始化一个固定长度的默认值序列时非常实用,例如为多个城市初始化相同的起始统计数据。

2.4 从 ndarray 创建

NumPy 的 ndarray 是 Series 的底层数据载体,因此直接从 ndarray 创建 Series 是最高效的方式之一。

示例:从 ndarray 创建 Series
# 从 NumPy ndarray 创建 arr = np.array([3.14, 2.718, 1.618, 0.577]) s_arr = pd.Series(arr, index=["pi", "e", "golden", "euler"]) print(s_arr)
输出
pi 3.140 e 2.718 golden 1.618 euler 0.577 dtype: float64

这里我们创建了一个包含常用数学常数的 Series。注意当数据为浮点数时,dtype 自动推断为 float64

三、索引标签

索引标签是 Series 区别于普通一维数组的核心特性。它允许我们用有意义的名称而非数字位置来引用数据,极大地提高了代码的可读性和可维护性。

3.1 index 参数

pd.Series() 的第二个主要参数是 index,用于显式指定索引标签。索引标签可以是字符串、整数、浮点数或任何可哈希类型,且不要求标签一定唯一(尽管唯一索引更为常见和高效)。

示例:自定义索引
scores = pd.Series( [85, 92, 78, 96], index=["张三", "李四", "王五", "赵六"] ) print(scores)
输出
张三 85 李四 92 王五 78 赵六 96 dtype: int64

3.2 默认 RangeIndex

当不指定 index 参数时,Pandas 会自动创建一个 RangeIndex,其值从 0 开始到 n-1(n 为数据长度)。这在不需要语义化标签的快速计算中非常实用。

RangeIndex 特性
s = pd.Series([100, 200, 300]) print("索引对象:", s.index) print("索引类型:", type(s.index)) print("索引值列表:", s.index.tolist())
输出
索引对象: RangeIndex(start=0, stop=3, step=1) 索引类型: <class 'pandas.core.indexes.range.RangeIndex'> 索引值列表: [0, 1, 2]

RangeIndex 是一种延迟计算的索引类型,它只记录 start、stop、step 三个参数,而不实际存储所有索引值,因此在大数据量时内存开销极低。

3.3 自定义索引的实际应用

自定义索引在真实数据分析中价值巨大。例如处理时序数据时,直接将时间作为索引:

示例:时序数据的索引
dates = pd.date_range(start="2026-01-01", periods=5, freq="D") print("生成的日期索引:") print(dates) print() temperature = pd.Series( [12.5, 13.1, 11.8, 14.2, 15.0], index=dates ) print("气温序列:") print(temperature)
输出
生成的日期索引: DatetimeIndex(['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04', '2026-01-05'], dtype='datetime64[ns]', freq='D') 气温序列: 2026-01-01 12.5 2026-01-02 13.1 2026-01-03 11.8 2026-01-04 14.2 2026-01-05 15.0 Freq: D, dtype: float64

将日期时间作为索引后,后续进行按时间切片、重采样、滚动计算等操作将变得十分方便。

四、核心属性

Series 对象暴露了多个实用的属性,用于快速获取数据的基本信息。掌握这些属性是高效使用 Series 的基础。

属性 类型 说明
.values ndarray 底层 NumPy 数组,存储所有数据值
.index Index 索引标签对象
.dtype dtype 数据的类型(如 int64、float64、object)
.name str 或 None Series 的名称,在 DataFrame 中会用作列名
.shape tuple 形状,一维 Series 返回 (n,)
.size int 元素的总数量
.nbytes int 底层数据占用的内存字节数
.empty bool Series 是否为空
.hasnans bool 是否包含 NaN 缺失值
示例:查看核心属性
s = pd.Series([10, 20, 30, 40, 50], name="scores") print("values :", s.values) print("index :", s.index) print("dtype :", s.dtype) print("name :", s.name) print("shape :", s.shape) print("size :", s.size) print("nbytes :", s.nbytes) print("empty :", s.empty) print("hasnans :", s.hasnans)
输出
values : [10 20 30 40 50] index : RangeIndex(start=0, stop=5, step=1) dtype : int64 name : scores shape : (5,) size : 5 nbytes : 40 empty : False hasnans : False

从中可以看到:.values 返回一个 NumPy 数组;.shape 返回的 (5,) 明确这是一维结构(只有一个维度);.nbytes 为 40 字节(5 个 int64 元素,每个 8 字节)。这些属性在数据探索和内存优化时非常有用。

五、数据访问

Series 提供了三种主要的数据访问方式:loc 标签索引iloc 位置索引布尔索引。选择哪种方式取决于你是根据标签、位置还是条件来获取数据。

5.1 loc —— 标签索引

.loc[] 使用索引标签来访问数据,是 Series 最独特也最强大的特性之一。语法为 s.loc[标签值],语义清晰:"给我标签等于某某的数据"

示例:loc 标签索引
s = pd.Series([85, 92, 78, 96], index=["张三", "李四", "王五", "赵六"]) # 通过标签访问单个元素 print("李四的成绩:", s.loc["李四"]) # 通过标签列表访问多个元素 print("张三和赵六的成绩:") print(s.loc[["张三", "赵六"]]) # 标签切片(包含两端) print("李四到赵六的成绩:") print(s.loc["李四":"赵六"])
输出
李四的成绩: 92 张三和赵六的成绩: 张三 85 赵六 96 dtype: int64 李四到赵六的成绩: 李四 92 王五 78 赵六 96 dtype: int64

loc 切片 vs Python 切片

.loc 切片的结束位置包含在内(inclusive),这与 Python 原生的左闭右开规则不同。这是因为标签背后是语义化的名称,没有"右开"的数学意义。

5.2 iloc —— 位置索引

.iloc[] 使用整数位置(从 0 开始)来访问数据,行为与 Python 列表和 NumPy 数组的索引方式一致。

示例:iloc 位置索引
s = pd.Series([85, 92, 78, 96], index=["张三", "李四", "王五", "赵六"]) # 按位置索引 print("第一个元素:", s.iloc[0]) print("最后一个元素:", s.iloc[-1]) # 位置切片(左闭右开) print("前三个元素:") print(s.iloc[:3]) # 步长索引 print("偶数位元素:") print(s.iloc[::2])
输出
第一个元素: 85 最后一个元素: 96 前三个元素: 张三 85 李四 92 王五 78 dtype: int64 偶数位元素: 张三 85 王五 78 dtype: int64

loc vs iloc 核心区别

loc 通过标签访问,切片结束位置包含在内;iloc 通过整数位置访问,切片遵循左闭右开规则。当索引标签本身就是整数时,两者的行为差异尤其需要注意,切勿混淆。

5.3 布尔索引

布尔索引是按条件筛选数据的核心手段。通过将一个布尔条件表达式传给 Series 的索引操作符([]),只保留条件为 True 的行。

示例:布尔索引
s = pd.Series([85, 92, 78, 96, 88], index=["张三", "李四", "王五", "赵六", "孙七"]) # 筛选成绩大于等于 90 的学生 high_scores = s[s >= 90] print("成绩 >= 90:") print(high_scores) # 使用复合条件 between = s[(s >= 80) & (s < 95)] print("\n成绩在 80~95 之间:") print(between)
输出
成绩 >= 90: 李四 92 赵六 96 dtype: int64 成绩在 80~95 之间: 张三 85 李四 92 dtype: int64

复合条件注意事项

在 Pandas 中使用复合条件时,必须使用 &(与)、|(或)、~(非)这三个位运算符,而不是 Python 原生的 andornot。并且每个条件必须用括号括起,因为位运算符的优先级高于比较运算符。

六、运算

Series 的运算机制是其区别于普通数组的又一个重要特性。当对两个 Series 进行算术运算时,Pandas 会按照索引标签自动对齐再进行计算,而不是简单地按位置配对。

6.1 对齐运算

对齐运算是指当两个 Series 进行加减乘除时,Pandas 根据索引标签的匹配关系自动对齐数据,不匹配的标签产生 NaN(缺失值)。

示例:Series 对齐运算
s1 = pd.Series([1, 2, 3], index=["a", "b", "c"]) s2 = pd.Series([4, 5, 6, 7], index=["b", "c", "d", "e"]) print("s1:") print(s1) print("\ns2:") print(s2) # 标签对齐加法 result = s1 + s2 print("\ns1 + s2(按标签对齐):") print(result)
输出
s1: a 1 b 2 c 3 dtype: int64 s2: b 4 c 5 d 6 e 7 dtype: int64 s1 + s2(按标签对齐): a NaN b 6.0 c 8.0 d NaN e NaN dtype: float64

这里 "b""c" 在两个 Series 中都存在,因此正常相加; "a" 只在 s1 中存在,"d""e" 只在 s2 中存在,它们在对方的 Series 中没有对应的标签,所以结果为 NaN。同时,因为有 NaN 出现,结果的 dtype 从 int 自动提升为 float64。

6.2 缺失值处理

Pandas 提供了丰富的缺失值处理方法,包括检测、填充和删除:

示例:缺失值处理
s = pd.Series([1, None, 3, np.nan, 5]) print("原始数据:") print(s) # 检测缺失值 print("\nisnull() 检测结果:") print(s.isnull()) # 删除缺失值 print("\ndropna() 删除缺失值:") print(s.dropna()) # 填充缺失值 print("\nfillna(0) 填充缺失值:") print(s.fillna(0)) # 向前填充 print("\nffill() 向前填充:") print(s.ffill())
输出
原始数据: 0 1.0 1 NaN 2 3.0 3 NaN 4 5.0 dtype: float64 isnull() 检测结果: 0 False 1 True 2 False 3 True 4 False dtype: bool dropna() 删除缺失值: 0 1.0 2 3.0 4 5.0 dtype: float64 fillna(0) 填充缺失值: 0 1.0 1 0.0 2 3.0 3 0.0 4 5.0 dtype: float64 ffill() 向前填充: 0 1.0 1 1.0 2 3.0 3 3.0 4 5.0 dtype: float64

dropna() —— 删除缺失

直接丢弃包含 NaN 的行。适用于数据量大、缺失值占比较小的场景。操作简单但会损失数据量。

fillna() —— 填充缺失

使用指定值(0、均值、中位数等)或方法(ffill/bfill)填充。适用于数据珍贵或对长度敏感的场景。

6.3 广播运算

广播(broadcasting)是指 Series 与标量或其他序列进行算术运算时,Pandas 自动将标量应用到每个元素的过程。

示例:广播运算
prices = pd.Series({ "苹果": 6.5, "香蕉": 3.2, "葡萄": 12.0, "草莓": 25.0 }) # 广播:所有价格打八折 discounted = prices * 0.8 print("八折价格:") print(discounted) # 广播:满 10 减 2 promotion = np.where(prices >= 10, prices - 2, prices) promotion_series = pd.Series(promotion, index=prices.index) print("\n满10减2后:") print(promotion_series)
输出
八折价格: 苹果 5.2 香蕉 2.6 葡萄 9.6 草莓 20.0 dtype: float64 满10减2后: 苹果 6.5 香蕉 3.2 葡萄 10.0 草莓 23.0 dtype: float64

广播运算在数据预处理中极为常见,无论是单位转换(斤→公斤)、折扣计算,还是标准化、归一化等操作,都依赖该机制。

七、name属性与rename

每个 Series 对象都有一个可选的 .name 属性。这个名称在 Series 独立存在时看似无关紧要,但在 Series 被组合成 DataFrame 时,.name 会直接成为 DataFrame 的列名。

7.1 设置与查看 name

示例:name 属性
# 创建时指定 name s = pd.Series([1, 2, 3], name="my_series") print("name:", s.name) # 创建后修改 name s.name = "renamed_series" print("修改后 name:", s.name) # name 为 None s2 = pd.Series([1, 2, 3]) print("默认 name:", s2.name)
输出
name: my_series 修改后 name: renamed_series 默认 name: None

7.2 rename 方法

.rename() 方法可以原地或非原地地修改 Series 的索引标签和名称,是数据清洗中的常用工具。

示例:rename 重命名
s = pd.Series([85, 92, 78], index=["zhang", "li", "wang"], name="raw_scores") # 重命名索引 —— 传入映射字典 s_renamed = s.rename(index={ "zhang": "张三", "li": "李四", "wang": "王五" }) print("索引重命名后:") print(s_renamed) # 重命名 Series 本身 s_final = s_renamed.rename("chinese_scores") print("\nSeries 重命名后 name:", s_final.name) # inplace 原地修改 s.rename(index={"zhang": "Zhang San"}, inplace=True) print("\n原地修改后:") print(s)
输出
索引重命名后: 张三 85 李四 92 王五 78 Name: raw_scores, dtype: int64 Series 重命名后 name: chinese_scores 原地修改后: Zhang San 85 li 92 wang 78 Name: raw_scores, dtype: int64

rename 不修改原数据(默认)

.rename() 默认返回一个新的 Series,原对象保持不变。只有当显式设置 inplace=True 时才会修改原对象。推荐默认使用非原地操作,保持不可变性以避免意外副作用。

八、向量化方法

Series 内置了大量向量化方法,这些方法底层的实现往往依赖于 NumPy 的高度优化 C 代码,在性能上远优于 Python 级别的循环(for-loop)。以下从统计分析、数值计算、排序三个维度进行介绍。

8.1 描述性统计

示例:常用统计方法
data = pd.Series([23, 45, 56, 78, 32, 45, 67, 89, 12, 54], name="sample_data") print("count :", data.count()) # 非空值计数 print("mean :", data.mean()) # 均值 print("std :", data.std()) # 标准差 print("min :", data.min()) # 最小值 print("max :", data.max()) # 最大值 print("median:", data.median()) # 中位数 print("skew :", data.skew()) # 偏度 print("kurt :", data.kurt()) # 峰度 # describe 一次输出多个统计量 print("\ndescribe() 汇总:") print(data.describe())
输出
count : 10 mean : 50.1 std : 23.464 min : 12.0 max : 89.0 median: 49.5 skew : 0.084 kurt : -1.241 describe() 汇总: count 10.00000 mean 50.10000 std 23.46397 min 12.00000 25% 33.50000 50% 49.50000 75% 65.25000 max 89.00000 dtype: float64

.describe() 一次性输出计数、均值、标准差、最小值、四分位数和最大值,是"EDA(探索性数据分析)第一枪"的标配工具。对于分类数据,则输出频次、唯一值数量等不同指标。

8.2 数值计算与变换

示例:数值方法
s = pd.Series([1, 2, 3, 4, 5]) print("sum :", s.sum()) print("cumsum :") print(s.cumsum()) # 累加 print("\ndiff :") print(s.diff()) # 一阶差分 print("\npct_change :") print(s.pct_change()) # 百分比变化 print("\nclip(2, 4) :") print(s.clip(2, 4)) # 截断到 [2, 4] 区间
输出
sum : 15 cumsum : 0 1 1 3 2 6 3 10 4 15 dtype: int64 diff : 0 NaN 1 1.0 2 1.0 3 1.0 4 1.0 dtype: float64 pct_change : 0 NaN 1 1.000000 2 0.500000 3 0.333333 4 0.250000 dtype: float64 clip(2, 4) : 0 2 1 2 2 3 3 4 4 4 dtype: int64

8.3 排序

Series 提供按值排序和按索引排序两种方式:

示例:排序操作
s = pd.Series([88, 72, 95, 68, 83], index=["E", "B", "A", "D", "C"]) print("原始数据:") print(s) # 按值排序(默认升序) print("\nsort_values() 按值排序:") print(s.sort_values()) # 按索引排序 print("\nsort_index() 按索引排序:") print(s.sort_index()) # 降序 print("\nsort_values(ascending=False) 降序:") print(s.sort_values(ascending=False))
输出
原始数据: E 88 B 72 A 95 D 68 C 83 dtype: int64 sort_values() 按值排序: D 68 B 72 C 83 E 88 A 95 dtype: int64 sort_index() 按索引排序: A 95 B 72 C 83 D 68 E 88 dtype: int64 sort_values(ascending=False) 降序: A 95 E 88 C 83 B 72 D 68 dtype: int64

8.4 apply 与 map —— 自定义函数变换

当内置方法无法满足需求时,.apply().map() 允许将任意 Python 函数应用到 Series 的每个元素上。

示例:apply 与 map
temps_c = pd.Series([0, 10, 20, 30, 40], name="celsius") # apply: 摄氏度转华氏度 def c_to_f(c): return round(c * 9/5 + 32, 1) temps_f = temps_c.apply(c_to_f).rename("fahrenheit") # map: 类似于 apply,但常用于字典映射 grade_map = { 0: "freezing", 10: "cold", 20: "warm", 30: "hot", 40: "very hot" } descriptions = temps_c.map(grade_map).rename("description") result = pd.DataFrame({ "celsius": temps_c, "fahrenheit": temps_f, "description": descriptions }) print(result)
输出
celsius fahrenheit description 0 0 32.0 freezing 1 10 50.0 cold 2 20 68.0 warm 3 30 86.0 hot 4 40 104.0 very hot

apply vs map 区别

  • .map() 主要用于字典映射或接受单个参数的函数,常用于分类数据转换
  • .apply() 更通用,可接受任意函数,还可传递额外参数
  • 两者都是元素级操作,但 .apply() 的底层实现更灵活

九、综合案例:股票收益率分析

以下通过一个模拟股票数据的完整案例,串联 Series 的多种操作。

综合案例
# ========== 1. 创建模拟股票价格数据 ========== import pandas as pd import numpy as np dates = pd.date_range("2026-04-01", periods=10, freq="D") prices = pd.Series( [100, 102, 101, 105, 108, 107, 110, 115, 113, 118], index=dates, name="close_price" ) print("=== 收盘价 ===") print(prices) # ========== 2. 计算日收益率 ========== daily_return = prices.pct_change() * 100 daily_return.name = "daily_return_pct" print("\n=== 日收益率 (%) ===") print(daily_return) # ========== 3. 筛选异常波动日 ========== volatile = daily_return[abs(daily_return) >= 2] print("\n=== 异常波动日 (收益率 >= 2%) ===") print(volatile) # ========== 4. 累计收益率 ========== cum_return = (1 + daily_return / 100).cumprod() - 1 cum_return.name = "cum_return" print("\n=== 累计收益率 ===") print(cum_return) # ========== 5. 描述性统计 ========== print("\n=== 收益率统计 ===") print(daily_return.describe()) # ========== 6. 缺失值处理 ========== # 第一天的 pct_change 为 NaN,用 0 填充 daily_return_clean = daily_return.fillna(0) print("\n=== 清理后的收益率 ===") print(daily_return_clean)
输出
=== 收盘价 === 2026-04-01 100 2026-04-02 102 2026-04-03 101 2026-04-04 105 2026-04-05 108 2026-04-06 107 2026-04-07 110 2026-04-08 115 2026-04-09 113 2026-04-10 118 Freq: D, Name: close_price, dtype: int64 === 日收益率 (%) === 2026-04-01 NaN 2026-04-02 2.000000 2026-04-03 -0.980392 2026-04-04 3.960396 2026-04-05 2.857143 2026-04-06 -0.925926 2026-04-07 2.803738 2026-04-08 4.545455 2026-04-09 -1.739130 2026-04-10 4.424779 Freq: D, Name: daily_return_pct, dtype: float64 === 异常波动日 (收益率 >= 2%) === 2026-04-02 2.000000 2026-04-04 3.960396 2026-04-05 2.857143 2026-04-07 2.803738 2026-04-08 4.545455 2026-04-10 4.424779 Name: daily_return_pct, dtype: float64 === 累计收益率 === 2026-04-01 0.000000 2026-04-02 0.020000 2026-04-03 0.009804 2026-04-04 0.050000 2026-04-05 0.080000 2026-04-06 0.069804 2026-04-07 0.100000 2026-04-08 0.150000 2026-04-09 0.130000 2026-04-10 0.180000 Freq: D, Name: cum_return, dtype: float64 === 收益率统计 === count 9.000000 mean 1.880718 std 2.475575 min -1.739130 25% -0.953159 50% 2.000000 75% 3.361067 max 4.545455 Name: daily_return_pct, dtype: float64 === 清理后的收益率 === 2026-04-01 0.000000 2026-04-02 2.000000 2026-04-03 -0.980392 2026-04-04 3.960396 2026-04-05 2.857143 2026-04-06 -0.925926 2026-04-07 2.803738 2026-04-08 4.545455 2026-04-09 -1.739130 2026-04-10 4.424779 Freq: D, Name: daily_return_pct, dtype: float64

这个综合案例展示了:创建带日期索引的 Series、使用向量化方法 pct_change()cumprod() 计算收益率、利用布尔索引筛选异常数据、通过 fillna() 清理缺失值、以及用 describe() 获取统计摘要——所有操作均在 Series 层面高效完成,无需编写任何显式循环。

十、核心要点总结

十一、进一步思考

Series 不仅是独立使用的数据结构,更是理解 DataFrame 的基石。DataFrame 本质上是由多个 Series 组成的列集合,每一列都是一个 Series。因此,掌握 Series 的所有操作——创建、索引、运算、变换——将直接迁移到 DataFrame 的使用中。建议在学习完 Series 之后,系统学习 DataFrame 的二维数据操作,包括分组聚合、透视表、多级索引等进阶内容。

练习建议

  1. 从 CSV 文件读取一列数据为 Series,检查其 dtype、shape、缺失值数量
  2. 创建一个包含 1000 个随机数的 Series,计算其移动平均线(窗口为 5)
  3. 将两个不同索引的 Series 相加,观察自动对齐和 NaN 的产生过程
  4. 使用 .apply() 实现一个自定义的文本清洗函数(去掉空格、统一大小写)
  5. 利用布尔索引筛选出 Series 中超出均值正负两倍标准差的数据(异常值检测)