第一部分:Pandas 核心基石 —— Series 与 DataFrame 深度剖析
Pandas 是 Python 数据分析生态系统的核心库,它构建在 NumPy 之上,提供了高性能、易用的数据结构和数据分析工具。理解 Pandas 的核心数据结构——Series
和 DataFrame
——的内部机制、创建方式、基本操作以及它们与 NumPy 的关系,是掌握 Pandas 的第一步,也是至关重要的一步。
1.1 Series
:一维带标签数组的威力
Series
是 Pandas 中最基本的一维数据结构,可以看作是一个带标签的 NumPy 数组。它由两部分组成:
- 数据 (values):通常是一个 NumPy 数组,存储实际的数据。
- 索引 (index):一个与数据相关联的标签序列,用于访问和标识数据。索引可以是整数、字符串、日期时间等。
1.1.1 Series
的创建与基本属性
a. 从不同数据源创建 Series
Pandas 提供了多种创建 Series
对象的方式:
import pandas as pd
import numpy as np
# 1. 从 Python 列表创建 Series
# 默认情况下,索引是 RangeIndex(0, 1, 2, ...)
data_list = [10, 20, 30, 40, 50] # 定义一个Python列表
s_from_list = pd.Series(data_list) # 从列表创建Series
print("--- Series from Python List (default index) ---")
print(s_from_list)
# 输出:
# 0 10
# 1 20
# 2 30
# 3 40
# 4 50
# dtype: int64
# 指定自定义索引
custom_index_list = ['a', 'b', 'c', 'd', 'e'] # 定义自定义索引列表
s_from_list_custom_index = pd.Series(data_list, index=custom_index_list) # 创建带有自定义索引的Series
print("\n--- Series from Python List (custom index) ---")
print(s_from_list_custom_index)
# 输出:
# a 10
# b 20
# c 30
# d 40
# e 50
# dtype: int64
# 2. 从 NumPy 数组创建 Series
# 这是非常常见的方式,因为 Pandas 底层大量依赖 NumPy
np_array = np.array([1.1, 2.2, 3.3, 4.4, 5.5]) # 定义一个NumPy数组
s_from_numpy = pd.Series(np_array) # 从NumPy数组创建Series (默认索引)
print("\n--- Series from NumPy Array (default index) ---")
print(s_from_numpy)
# 输出:
# 0 1.1
# 1 2.2
# 2 3.3
# 3 4.4
# 4 5.5
# dtype: float64
# 指定索引和名称 (name 属性用于标识 Series 本身)
s_from_numpy_named = pd.Series(np_array, index=['row1', 'row2', 'row3', 'row4', 'row5'], name='MyFloatSeries') # 创建带有索引和名称的Series
print("\n--- Series from NumPy Array (custom index and name) ---")
print(s_from_numpy_named)
# 输出:
# row1 1.1
# row2 2.2
# row3 3.3
# row4 4.4
# row5 5.5
# Name: MyFloatSeries, dtype: float64
# 3. 从 Python 字典创建 Series
# 字典的键 (keys) 默认成为 Series 的索引。
# 字典的值 (values) 成为 Series 的数据。
# Series 中元素的顺序将遵循字典键的插入顺序 (Python 3.7+,或排序后的顺序)。
data_dict = {
'apple': 100, 'banana': 200, 'cherry': 150, 'date': 300} # 定义一个Python字典
s_from_dict = pd.Series(data_dict) # 从字典创建Series
print("\n--- Series from Python Dictionary ---")
print(s_from_dict)
# 输出 (顺序可能因Python版本而异,但通常是插入顺序或排序后):
# apple 100
# banana 200
# cherry 150
# date 300
# dtype: int64
# 如果在创建时额外指定了 index 参数,Pandas 会根据这个指定的 index 来构建 Series:
# - 如果指定 index 中的标签在字典的键中存在,则取对应的值。
# - 如果指定 index 中的标签在字典的键中不存在,则对应的值为 NaN (Not a Number)。
# - 字典中存在但未在指定 index 中出现的键将被忽略。
explicit_index_for_dict = ['banana', 'date', 'elderberry', 'apple'] # 定义显式索引
s_from_dict_explicit_index = pd.Series(data_dict, index=explicit_index_for_dict) # 使用显式索引从字典创建Series
print("\n--- Series from Python Dictionary (with explicit index) ---")
print(s_from_dict_explicit_index)
# 输出:
# banana 200.0 (存在于字典,取值)
# date 300.0 (存在于字典,取值)
# elderberry NaN (不在字典中,值为NaN)
# apple 100.0 (存在于字典,取值)
# dtype: float64 (因为引入了NaN,整数类型会自动提升为浮点数)
# 4. 从标量值创建 Series
# 如果数据是一个标量值,则必须提供索引。该标量值会被重复填充到与索引长度相同。
scalar_value = 7 # 定义一个标量值
scalar_index = ['x', 'y', 'z'] # 定义索引
s_from_scalar = pd.Series(scalar_value, index=scalar_index) # 从标量创建Series
print("\n--- Series from Scalar Value ---")
print(s_from_scalar)
# 输出:
# x 7
# y 7
# z 7
# dtype: int64
# 也可以不提供数据,只提供索引和 dtype,创建一个空的或特定类型的 Series
empty_s_with_index = pd.Series(index=['id1', 'id2'], dtype='object') # 创建一个指定索引和类型的空Series
print("\n--- Empty Series with specified index and dtype ---")
print(empty_s_with_index)
# 输出:
# id1 NaN
# id2 NaN
# dtype: object
这段代码全面展示了创建Pandas Series
的各种常用方法:
- 从Python列表创建:可以直接将列表传递给
pd.Series()
。如果不指定index
参数,Pandas会自动创建一个从0开始的整数范围索引 (RangeIndex
)。如果提供了index
参数(一个与数据等长的列表或类数组对象),则会使用这个自定义索引。 - 从NumPy数组创建:这是非常自然和高效的方式,因为
Series
的底层数据存储通常就是NumPy数组。同样可以指定自定义索引和name
属性。name
属性可以给Series
本身一个描述性名称,这在后续将Series
组合成DataFrame
或进行绘图时很有用。 - 从Python字典创建:
- 当直接将字典传递给
pd.Series()
时,字典的键会成为Series
的索引,字典的值成为Series
的数据。在Python 3.7+版本中,Series
中元素的顺序会保持字典键的插入顺序;在更早的版本中,或者如果字典本身是无序的,Series
的索引可能会被排序。 - 如果在从字典创建
Series
时显式地提供了index
参数,Pandas的行为会有所不同:它会严格按照提供的index
来构建Series
。如果index
中的某个标签存在于字典的键中,则取该键对应的值;如果不存在,则对应的值被设为NaN
(Not a Number)。任何存在于字典中但未出现在显式index
里的键值对都将被忽略。值得注意的是,如果因为引入NaN
而导致原始数据类型(如整数)无法表示NaN
,Pandas会自动将该Series
的数据类型(dtype
)提升为可以容纳NaN
的类型(通常是float64
)。
- 当直接将字典传递给
- 从标量值创建:如果传递给
pd.Series()
的数据是一个单一的标量值(如一个数字或字符串),那么必须同时提供index
参数。Pandas会将这个标量值重复广播,以匹配所提供索引的长度。 - 创建空或特定类型的Series:可以不传递数据参数(或传递
None
),只提供index
和可选的dtype
来创建一个所有值都为NaN
(或相应类型的“空”值) 的Series
。这在预先定义数据结构框架时可能有用。
理解这些创建方式是灵活使用 Series
的基础。根据数据来源和期望的结构选择合适的创建方法非常重要。
b. Series
的核心属性
Series
对象有很多有用的属性,可以帮助我们了解其结构和内容:
import pandas as pd
import numpy as np
# 创建一个示例 Series 用于演示属性
data = np.array([10., 20., np.nan, 40., 50.]) # 包含NaN的浮点数数据
idx = pd.Index(['alpha', 'beta', 'gamma', 'delta', 'epsilon'], name='MyIndex') # 创建一个带名称的Index对象
s_example = pd.Series(data, index=idx, name='SampleSeries') # 创建示例Series
print("--- Example Series for Attribute Demonstration ---")
print(s_example)
# 1. values: 返回 Series 中的实际数据,通常是一个 NumPy 数组。
series_values = s_example.values # 获取Series的值 (NumPy数组)
print(f"\n1. s_example.values: {
series_values}, 类型: {
type(series_values)}")
# 输出: [10. 20. nan 40. 50.], 类型: <class 'numpy.ndarray'>
# 注意:从 Pandas 1.0 开始,对于特定扩展类型 (如可空整数、分类类型等),
# .values 可能返回一个 ExtensionArray 而不是 NumPy 数组。
# 更推荐使用 .array 或 .to_numpy() 来获取底层数组。
# 2. array: 返回存储在 Series 中的底层数组对象。
# 对于标准数据类型,这通常是 NumPy ndarray。
# 对于扩展数据类型,这是 ExtensionArray。
series_array = s_example.array # 获取Series的底层数组对象
print(f"2. s_example.array: {
series_array}, 类型: {
type(series_array)}")
# 输出: <PandasArray>
# [10.0, 20.0, nan, 40.0, 50.0]
# Length: 5, dtype: float64
# 对于标准 float64,.array 返回的是一个 PandasArray 包装器,其底层仍是 NumPy 数组。
# 3. to_numpy(): 明确返回一个 NumPy ndarray。
# 可以指定 dtype 和 na_value。
series_numpy_array = s_example.to_numpy(dtype=float, na_value=np.nan) # 转换为NumPy数组
print(f"3. s_example.to_numpy(): {
series_numpy_array}, 类型: {
type(series_numpy_array)}")
# 输出: [10. 20. nan 40. 50.], 类型: <class 'numpy.ndarray'>
# 4. index: 返回 Series 的索引对象 (pd.Index)。
series_index = s_example.index # 获取Series的索引对象
print(f"\n4. s_example.index: {
series_index}")
# 输出: Index(['alpha', 'beta', 'gamma', 'delta', 'epsilon'], dtype='object', name='MyIndex')
print(f" 索引的类型: {
type(series_index)}") # <class 'pandas.core.indexes.base.Index'>
print(f" 索引的名称: {
series_index.name}") # MyIndex
# 5. dtype: 返回 Series 中数据值的 NumPy 数据类型 (dtype)。
series_dtype = s_example.dtype # 获取Series的数据类型
print(f"\n5. s_example.dtype: {
series_dtype}") # float64
# 6. shape: 返回一个表示 Series 形状的元组 (只有一个元素,即其长度)。
series_shape = s_example.shape # 获取Series的形状
print(f"\n6. s_example.shape: {
series_shape}") # (5,)
# 7. ndim: 返回 Series 的维度数量 (总是1)。
series_ndim = s_example.ndim # 获取Series的维度数量
print(f"\n7. s_example.ndim: {
series_ndim}") # 1
# 8. size: 返回 Series 中元素的总数 (包括 NaN)。
series_size = s_example.size # 获取Series的元素总数
print(f"\n8. s_example.size: {
series_size}") # 5
# 9. name: 返回 Series 的名称 (如果已设置)。
series_name = s_example.name # 获取Series的名称
print(f"\n9. s_example.name: {
series_name}") # SampleSeries
# 可以修改 name 属性
s_example.name = 'UpdatedSampleSeries' # 修改Series的名称
print(f" 修改后的 s_example.name: {
s_example.name}") # UpdatedSampleSeries
# 10. hasnans: 返回一个布尔值,指示 Series 是否包含任何 NaN 值。
series_hasnans = s_example.hasnans # 检查Series是否包含NaN
print(f"\n10. s_example.hasnans: {
series_hasnans}") # True (因为我们包含了一个np.nan)
# 11. empty: 返回一个布尔值,指示 Series 是否为空 (即长度为0)。
s_empty = pd.Series(dtype=float) # 创建一个空的Series
print(f"\n11. s_empty.empty: {
s_empty.empty}") # True
print(f" s_example.empty: {
s_example.empty}") # False
# 12. T / transpose(): 对于 Series,转置操作返回其自身 (因为是一维的)。
series_transposed = s_example.T # 获取Series的转置 (返回自身)
print(f"\n12. s_example.T (转置,对于Series返回自身):")
print(series_transposed)
# (与 s_example 相同)
# 13. memory_usage(deep=False, index=True): 返回 Series 对象占用的内存字节数。
# deep=True 会深入检查 'object' 类型数据内部元素的内存占用。
# index=True (默认) 会包含索引的内存占用。
memory_bytes = s_example.memory_usage(deep=True, index=True) # 计算Series的内存占用
print(f"\n13. s_example.memory_usage(deep=True, index=True): {
memory_bytes} 字节")
# 输出可能类似: 400 (具体值取决于索引类型和数据)
# 解释:
# - 索引 ('alpha'...'epsilon') 是对象类型,deep=True会计算字符串实际占用的内存。
# - 数据 (5个float64) 是 5 * 8 = 40 字节。
# - 索引对象本身也有开销。
# 14. is_unique: 检查Series中的所有值是否唯一。
s_unique_check = pd.Series([1, 2, 3, 2, 4]) # 包含重复值的Series
print(f"\n14. s_unique_check.is_unique: {
s_unique_check.is_unique}") # False
s_all_unique = pd.Series([10, 20, 30]) # 值全部唯一的Series
print(f" s_all_unique.is_unique: {
s_all_unique.is_unique}") # True
# 15. is_monotonic_increasing / is_monotonic_decreasing: 检查值是否单调。
# 在Pandas 1.2.0中,is_monotonic 被弃用,拆分为 is_monotonic_increasing 和 is_monotonic_decreasing
s_monotonic = pd.Series([1, 2, 2, 3, 5]) # 单调递增的Series
print(f"\n15. Monotonicity checks for s_monotonic = {
s_monotonic.to_list()}:")
print(f" is_monotonic_increasing: {
s_monotonic.is_monotonic_increasing}") # True
print(f" is_monotonic_decreasing: {
s_monotonic.is_monotonic_decreasing}") # False
s_monotonic_strict = pd.Series([1, 2, 3, 5]) # 严格单调递增的Series
print(f" s_monotonic_strict ({
s_monotonic_strict.to_list()}) is_monotonic_increasing: {
s_monotonic_strict.is_monotonic_increasing}") # True
s_non_monotonic = pd.Series([1, 3, 2, 4]) # 非单调的Series
print(f" s_non_monotonic ({
s_non_monotonic.to_list()}) is_monotonic_increasing: {
s_non_monotonic.is_monotonic_increasing}") # False
# 对于索引也有这些单调性检查
print(f" s_example.index.is_monotonic_increasing: {
s_example.index.is_monotonic_increasing}") # True (因为 'alpha'...'epsilon' 是有序的)
这段代码详细地演示和解释了 Series
对象的一些核心属性:
.values
: 返回Series
中的数据,通常是NumPyndarray
。但对于Pandas的扩展数据类型(如可空整数Int64Dtype
,分类CategoricalDtype
),它可能返回一个ExtensionArray
。.array
: 这是更现代的访问底层数据的方式,它总是返回一个ExtensionArray
实例(对于标准NumPy dtypes,返回的是PandasArray
,它是对NumPy数组的轻量级包装;对于Pandas特定的扩展类型,返回的是相应的ExtensionArray
)。这个属性旨在提供一个更一致的接口来处理不同类型的数据存储。.to_numpy()
: 这是一个方法,明确地将Series
的数据转换为NumPyndarray
。你可以指定转换后的dtype
以及如何处理NaN
值(通过na_value
参数)。这是获取纯NumPy数组的推荐方式,特别是当你需要与期望NumPy数组的库交互时。.index
: 返回Series
的索引对象,它本身是一个pd.Index
(或其子类,如RangeIndex
,DatetimeIndex
,CategoricalIndex
等)的实例。索引对象有很多自己的属性和方法,例如.name
(索引的名称),.is_unique
(索引值是否唯一),.is_monotonic_increasing
(索引是否单调递增) 等。.dtype
: 返回Series
中数据值的NumPy数据类型 (numpy.dtype
对象)。这告诉你数据是如何存储的(例如,int64
,float64
,object
,bool
,datetime64[ns]
,category
等)。.shape
: 对于Series
(一维),它返回一个只包含一个元素的元组(length,)
,表示Series
的长度。.ndim
:Series
的维度数量,总是1。.size
:Series
中元素的总数,等同于其长度len(series)
。.name
:Series
的名称,是一个字符串。可以在创建时指定,也可以后续赋值修改。当Series
作为DataFrame
的一列时,其name
通常就是列名。.hasnans
: 一个布尔值,如果Series
中至少包含一个NaN
(Not a Number) 或NaT
(Not a Time,用于日期时间类型) 或None
(在对象类型中),则为True
,否则为False
。这是一个快速检查是否存在缺失值的方法。.empty
: 一个布尔值,如果Series
的长度为0,则为True
。.T
(或方法.transpose()
)**: 对于Series
,转置操作返回其自身,因为一维数组的转置没有改变其结构。这与DataFrame
的转置行为不同。.memory_usage(deep=False, index=True)
: 这是一个方法,用于估算Series
对象在内存中占用的字节数。deep=False
(默认):只计算数据缓冲区本身(如NumPy数组)和索引对象的基本内存占用,不深入检查object
类型数据内部元素(如字符串)的实际内存。deep=True
:如果Series
的dtype
是object
,它会尝试递归地计算对象内部元素(如每个字符串)的真实内存占用,这会更准确但计算也更慢。index=True
(默认):计算结果中包含索引对象的内存占用。设为False
则不包含。
这个方法对于理解和优化大规模数据的内存消耗非常有用。
.is_unique
: 布尔属性,如果Series
中的所有值都是唯一的(没有重复项),则为True
。.is_monotonic_increasing
/.is_monotonic_decreasing
: 布尔属性,检查Series
中的值是否是单调递增(允许相等)或单调递减的。对于已排序或期望排序的数据,这是一个有用的验证。索引对象 (series.index
) 也有这些单调性检查属性。
这些属性为我们提供了关于 Series
结构、内容、类型和状态的丰富信息,是在进行数据探索、清洗和转换时的重要参考。
1.1.2 Series
的核心功能:索引与选择
Series
强大的功能很大程度上源于其灵活的索引和选择机制。它结合了NumPy数组的位置(整数)索引和类似Python字典的标签索引。
a. 基于标签的索引 (.loc
)
.loc
属性主要用于基于标签的索引和切片。
s.loc[label]
: 选择单个标签对应的值。s.loc[[label1, label2, ...]]
: 选择多个标签对应的值,返回一个新的Series
。s.loc[start_label:end_label]
: 进行基于标签的切片。重要的是,对于标签切片,结束标签end_label
是包含在内的 (inclusive),这与Python列表或NumPy数组的位置切片(不包含结束位置)不同。s.loc[boolean_series]
: 使用一个与s
索引对齐的布尔Series
进行选择,返回s
中对应True
的元素。
b. 基于整数位置的索引 (.iloc
)
.iloc
属性主要用于基于整数位置 (0-based) 的索引和切片,其行为非常类似于NumPy数组和Python列表。
s.iloc[position]
: 选择单个整数位置对应的值。s.iloc[[pos1, pos2, ...]]
: 选择多个整数位置对应的值。s.iloc[start_pos:end_pos]
: 进行基于整数位置的切片。结束位置end_pos
是不包含在内的 (exclusive)。s.iloc[boolean_numpy_array]
: 可以使用一个与s
等长的NumPy布尔数组进行选择。
c.直接索引 ([]
) 的行为与歧义
直接使用方括号 []
进行索引(如 s[key]
)时,Pandas会尝试“智能地”判断 key
是标签还是整数位置,这有时会导致行为不明确或意外。
- 如果
Series
的索引是整数类型:- 如果
key
是一个整数,Pandas 通常会将其解释为标签索引(如果该整数标签存在于索引中)。如果该整数标签不存在,但它在有效的整数位置范围内 (0 到len(s)-1
),则可能会引发KeyError
(如果想用标签但标签不存在) 或在某些旧版本/情况下回退到位置索引(不推荐依赖这种回退)。 - 如果
key
是一个整数切片 (如s[0:3]
),Pandas 通常会将其解释为位置切片。
- 如果
- 如果
Series
的索引是非整数类型(如字符串、日期时间):- 如果
key
是索引的标签类型 (如字符串),则执行标签索引。 - 如果
key
是一个整数或整数切片,则执行位置索引。
- 如果
- 最佳实践:为了代码的清晰性和避免歧义,强烈建议在需要明确基于标签索引时使用
.loc
,在需要明确基于整数位置索引时使用.iloc
。直接使用[]
应该谨慎,尤其是在索引类型可能变化或包含整数标签的情况下。
d. 条件选择 (Boolean Indexing)
这是 Series
(和 DataFrame
) 中非常强大的一种选择方式。可以通过一个布尔 Series
(通常由对原 Series
进行条件比较运算得到) 来选择满足条件的元素。
import pandas as pd
import numpy as np
# 创建一个更复杂的 Series 用于索引和选择演示
# 包含整数标签、字符串标签、以及重复的索引
data_values = np.arange(10, 80, 10) # [10, 20, 30, 40, 50, 60, 70]
complex_index_labels = ['a', 'b', 'c', 'a', 'd', 'e', 'c'] # 包含重复标签
s_complex = pd.Series(data_values, index=complex_index_labels, name='ComplexSeries')
print("--- Series for Indexing/Selection Demonstration (s_complex) ---")
print(s_complex)
# 输出:
# a 10
# b 20
# c 30
# a 40 (注意:标签'a'和'c'是重复的)
# d 50
# e 60
# c 70
# Name: ComplexSeries, dtype: int64
# 1. 基于标签的索引 (.loc)
print("\n--- 1. Label-based Indexing (.loc) ---")
# a) 选择单个标签
print(f"s_complex.loc['b']: {
s_complex.loc['b']}") # 输出: 20
# b) 选择单个重复标签 (如 'a')
# 如果标签是唯一的,s.loc[label] 返回标量值。
# 如果标签是重复的,s.loc[label] 返回一个新的 Series,包含所有匹配该标签的元素。
print(f"s_complex.loc['a']:\n{
s_complex.loc['a']}")
# 输出:
# a 10
# a 40
# Name: ComplexSeries, dtype: int64
# c) 选择多个标签 (使用列表)
selected_labels = ['e', 'c', 'missing_label'] # 包含一个不存在的标签
try:
print(f"s_complex.loc[['e', 'c']]:\n{
s_complex.loc[['e', 'c']]}")
# 输出 (顺序与列表一致,'c'的结果会包含所有匹配项):
# e 60
# c 30
# c 70
# Name: ComplexSeries, dtype: int64
# 如果选择的标签列表中包含不存在的标签,.loc 会引发 KeyError
# print(s_complex.loc[selected_labels]) # 这会引发 KeyError: "['missing_label'] not found in axis"
except KeyError as e: # 捕获KeyError
print(f"Error with s_complex.loc[{
selected_labels}]: {
e}")
# d) 标签切片 (结束标签包含在内)
# 注意:标签切片要求索引是已排序的,或者至少对于切片范围内的标签是明确有序的。
# 如果索引未排序且有重复,标签切片的行为可能不直观或引发错误,取决于Pandas版本。
# 对于 s_complex,其索引不是严格单调的。
# 如果索引是单调的,例如:
s_sorted_index = pd.Series([1,2,3,4], index=['w','x','y','z']) # 创建一个索引排序的Series
print(f"s_sorted_index:\n{
s_sorted_index}")
print(f"s_sorted_index.loc['x':'z'] (inclusive end):\n{
s_sorted_index.loc['x':'z']}")
# 输出:
# x 2
# y 3
# z 4
# dtype: int64
# 对于 s_complex,如果尝试对其未排序的重复索引进行标签切片,行为可能依赖于Pandas版本
# 并且通常不推荐。例如 s_complex.loc['b':'d'] 可能会基于标签出现的第一个位置进行切片。
# 为了安全和可预测性,最好在标签切片前确保索引是有意义的(如已排序或唯一)。
try:
print(f"s_complex.loc['b':'d'] (on non-monotonic index with duplicates):")
# 行为可能取决于Pandas版本。通常它会尝试找到'b'的第一个出现和'd'的第一个出现之间的所有元素
#(包括边界)。
print(s_complex.loc['b':'d'])
# 可能输出:
# b 20
# c 30
# a 40
# d 50
# Name: ComplexSeries, dtype: int64
except Exception as e: # 捕获可能的异常
print(f"Error with s_complex.loc['b':'d']: {
e}")
# 2. 基于整数位置的索引 (.iloc)
print("\n--- 2. Integer Position-based Indexing (.iloc) ---")
# a) 选择单个位置
print(f"s_complex.iloc[0]: {
s_complex.iloc[0]}") # 第一个元素 (对应标签 'a', 值为 10)
print(f"s_complex.iloc[-1]: {
s_complex.iloc[-1]}") # 最后一个元素 (对应标签 'c', 值为 70)
# b) 选择多个位置 (使用列表)
print(f"s_complex.iloc[[1, 3, 5]]:\n{
s_complex.iloc[[1, 3, 5]]}")
# 输出 (对应位置1,3,5的元素):
# b 20
# a 40
# e 60
# Name: ComplexSeries, dtype: int64
# c) 位置切片 (结束位置不包含在内)
print(f"s_complex.iloc[1:4] (exclusive end, positions 1, 2, 3):\n{
s_complex.iloc[1:4]}")
# 输出:
# b 20
# c 30
# a 40
# Name: ComplexSeries, dtype: int64
print(f"s_complex.iloc[:3] (first 3 elements):\n{
s_complex.iloc[:3]}") # 前3个元素
print(f"s_complex.iloc[-3:] (last 3 elements):\n{
s_complex.iloc[-3:]}") # 后3个元素
# 3. 直接索引 `[]` 的行为
print("\n--- 3. Direct Indexing `[]` ---")
# 对于 s_complex,其索引是字符串。
# a) 使用标签 (如果标签存在)
print(f"s_complex['b']: {
s_complex['b']}") # 标签索引,输出 20
print(f"s_complex['a'] (direct, duplicate label):\n{
s_complex['a']}") # 标签索引,返回Series
# b) 使用整数 (因为索引非整数,所以整数被解释为位置)
print(f"s_complex[0] (direct, int on non-int index -> positional): {
s_complex[0]}") # 位置索引,输出 10
# 但是,如果索引本身就是整数,直接用整数索引会优先尝试匹配标签!
s_int_index = pd.Series([100, 200, 300], index=[1, 5, 10]) # 创建一个整数索引的Series
print(f"\nSeries with integer index (s_int_index):\n{
s_int_index}")
print(f"s_int_index[1] (direct, int on int index -> label): {
s_int_index[1]}") # 匹配标签1,输出100
# print(s_int_index[0]) # 这会引发 KeyError,因为标签0不存在,且Pandas不会回退到位置0
try:
print(s_int_index[0]) # 尝试用整数0进行直接索引
except KeyError as e: # 捕获KeyError
print(f"Error with s_int_index[0]: {
e} (label 0 not found)")
# c) 使用切片 (通常被解释为位置切片,即使索引是标签)
print(f"s_complex[1:4] (direct slice on non-int index -> positional slice):\n{
s_complex[1:4]}")
# 输出 (与 s_complex.iloc[1:4] 相同):
# b 20
# c 30
# a 40
# Name: ComplexSeries, dtype: int64
# 对于整数索引的 Series,整数切片也是位置切片
print(f"s_int_index[0:2] (direct slice on int index -> positional slice):\n{
s_int_index[0:2]}")
# 输出 (位置0和1的元素):
# 1 100
# 5 200
# dtype: int64
# **强烈建议使用 .loc 和 .iloc 来避免 `[]` 的歧义!**
# 4. 条件选择 (Boolean Indexing)
print("\n--- 4. Conditional Selection (Boolean Indexing) ---")
# a) 创建一个布尔 Series 作为掩码
mask_gt_30 = s_complex > 30 # 创建一个布尔Series,标记s_complex中值大于30的元素
print(f"Boolean mask (s_complex > 30):\n{
mask_gt_30}")
# 输出:
# a False
# b False
# c False
# a True
# d True
# e True
# c True
# Name: ComplexSeries, dtype: bool
# b) 使用布尔 Series (掩码) 进行选择
selected_by_mask = s_complex[mask_gt_30] # 使用布尔掩码进行索引
# 这等效于 s_complex.loc[mask_gt_30] (通常更推荐 .loc)
print(f"s_complex[s_complex > 30]:\n{
selected_by_mask}")
# 输出 (所有值大于30的元素):
# a 40
# d 50
# e 60
# c 70
# Name: ComplexSeries, dtype: int64
# c) 组合条件
# 选择值大于20且其标签为 'a' 或 'c' 的元素
mask_combined = (s_complex > 20) & (s_complex.index.isin(['a', 'c'])) # 创建组合条件的布尔掩码
# s_complex.index.isin(['a', 'c']) 返回一个与 s_complex 等长的布尔数组,
# 标记索引是否在 ['a', 'c'] 中。
print(f"\nCombined mask ((s_complex > 20) & (index is 'a' or 'c')):\n{
mask_combined}")
print(f"s_complex with combined condition:\n{
s_complex[mask_combined]}")
# 输出:
# c 30 (value=30, index='c')
# a 40 (value=40, index='a')
# c 70 (value=70, index='c')
# Name: ComplexSeries, dtype: int64
# d) 条件选择与赋值
s_copy_for_assignment = s_complex.copy() # 创建一个副本用于赋值操作
print(f"\nOriginal s_copy_for_assignment (first 'a'): {
s_copy_for_assignment.loc['a'].iloc[0]}") # 打印第一个'a'的值
# 将所有标签为 'a' 且值小于 20 的元素的值更新为 -1
s_copy_for_assignment.loc[(s_copy_for_assignment.index == 'a') & (s_copy_for_assignment < 20)] = -1 # 使用组合条件进行赋值
print(f"After assigning -1 to ('a' & <20), s_copy_for_assignment['a']:\n{
s_copy_for_assignment.loc['a']}")
# 输出:
# a -1 (原值为10,被修改)
# a 40 (原值为40,未被修改)
# Name: ComplexSeries, dtype: int64
# 5. `.get()` 方法:类似字典的访问,带默认值
print("\n--- 5. .get() method for label access ---")
print(f"s_complex.get('b'): {
s_complex.get('b')}") # 输出: 20
print(f"s_complex.get('x'): {
s_complex.get('x')}") # 标签'x'不存在,默认返回 None
print(f"s_complex.get('x', default=-999): {
s_complex.get('x', default=-999)}") # 指定默认值 -999
# 如果标签重复,.get() 的行为与 .loc[] 类似,会返回一个 Series
print(f"s_complex.get('a'):\n{
s_complex.get('a')}")
# 输出 (与 s_complex.loc['a'] 相同):
# a 10
# a 40
# Name: ComplexSeries, dtype: int64
这段代码详细演示了 Series
对象的各种索引和选择方法:
.loc
(基于标签):- 选择单个标签:如果标签唯一,返回标量值;如果标签重复(如示例中的
'a'
和'c'
),则返回一个新的Series
,包含所有具有该标签的条目。 - 选择多个标签(通过列表):返回一个新的
Series
。如果列表中包含不存在于索引中的标签,.loc
会引发KeyError
。 - 标签切片 (
start_label:end_label
):结束标签是包含在内的。这要求索引至少在切片范围内是有序的。如果索引完全无序或有复杂的重复模式,标签切片的行为可能不直观或引发错误,因此建议在进行标签切片前确保索引的有序性或唯一性(如果适用)。
- 选择单个标签:如果标签唯一,返回标量值;如果标签重复(如示例中的
.iloc
(基于整数位置):- 其行为与Python列表和NumPy数组的整数索引/切片完全一致。
- 选择单个位置、多个位置(通过整数列表)、或进行位置切片(如
1:4
,不包含结束位置4
)。支持负数索引(如-1
表示最后一个元素)。
- 直接索引
[]
:- 其行为具有“智能”判断的特性,试图根据键的类型和索引的类型来决定是执行标签索引还是位置索引。
- 主要规则:如果索引是非整数类型(如字符串),则整数键被视为位置,非整数键被视为标签。如果索引是整数类型,则整数键优先被视为标签;如果该整数标签不存在,通常会引发
KeyError
(现代Pandas版本一般不会轻易回退到位置索引来避免歧义)。 - 切片:无论索引类型如何,直接使用
[]
进行切片(如s[0:3]
)通常被解释为位置切片。 - 强烈建议:为了代码的清晰性和可维护性,应优先使用
.loc
进行显式的标签索引,使用.iloc
进行显式的位置索引,以避免由[]
的歧义行为可能导致的错误或困惑。
- 条件选择 (Boolean Indexing):
- 这是Pandas中一种极其强大的数据选择方式。
- 首先,创建一个与原
Series
索引对齐的布尔Series
(通常通过对原Series
应用比较运算符如>
、<
、==
,或逻辑运算符如&
(AND),|
(OR),~
(NOT) 来组合多个条件)。 - 然后,将这个布尔
Series
用作索引(通常通过.loc
或直接[]
)来从原Series
中选择所有对应布尔值为True
的元素。 - 示例中演示了如何创建简单条件掩码、组合条件掩码(例如,值大于20 且 索引标签是 ‘a’ 或 ‘c’),以及如何使用布尔掩码进行条件赋值。
.get()
方法:- 提供了类似Python字典的
.get(key, default=None)
访问方式。 - 如果
key
(标签) 存在于索引中,返回对应的值(如果标签重复,返回一个Series
)。 - 如果
key
不存在,它不会引发KeyError
,而是返回None
(或者通过default
参数指定的默认值)。这在不确定某个标签是否一定存在时非常有用。
- 提供了类似Python字典的
掌握这些索引和选择技术是高效使用Pandas Series
的关键。它们允许我们以各种灵活的方式精确地访问、过滤和修改数据,是进行数据清洗、转换和分析的基础操作。特别地,条件选择(布尔索引)在实际数据处理中应用极为广泛。
1.1.3 Series
的基本运算与对齐
Series
对象支持广泛的算术运算、逻辑运算和函数应用。一个核心特性是,当对两个 Series
对象进行运算时,Pandas 会自动按索引对齐数据。
a. 算术运算与数据对齐
当你对两个 Series
执行算术运算(如 +
, -
, *
, /
)时:
- Pandas会查找两个
Series
中共同的索引标签。 - 对于共同的标签,执行相应的运算。
- 对于只存在于一个
Series
中的标签,结果中该标签对应的值将是NaN
(因为无法找到匹配项进行运算)。 - 结果
Series
的索引将是两个原始Series
索引的并集,并排序。
import pandas as pd
import numpy as np
s1_data = {
'a': 10, 'b': 20, 'c': 30, 'd': 40} # 第一个Series的数据
s1 = pd.Series(s1_data, name='Series1') # 创建第一个Series
s2_data = {
'c': 300, 'd': 400, 'e': 500, 'f': 600} # 第二个Series的数据,索引部分重叠
s2 = pd.Series(s2_data, name='Series2') # 创建第二个Series
print("--- Series for Arithmetic Operations ---")
print("s1:\n", s1)
# a 10
# b 20
# c 30
# d 40
# Name: Series1, dtype: int64
print("\ns2:\n", s2)
# c 300
# d 400
# e 500
# f 600
# Name: Series2, dtype: int64
# 1. Series 相加 (自动按索引对齐)
s_add = s1 + s2 # 对两个Series进行加法运算
print("\n--- s1 + s2 (Addition with Alignment) ---")
print(s_add)
# 预期结果:
# a NaN (s2中无'a')
# b NaN (s2中无'b')
# c 330.0 (30 + 300)
# d 440.0 (40 + 400)
# e NaN (s1中无'e')
# f NaN (s1中无'f')
# dtype: float64 (因为引入了NaN,可能从int提升为float)
# 2. 使用算术方法并提供 fill_value
# 当你希望对于不匹配的索引使用一个默认值而不是NaN时,可以使用算术运算的方法版本
# (如 `.add()`, `.sub()`, `.mul()`, `.div()`) 并指定 `fill_value` 参数。
# `fill_value` 会在对齐过程中替换缺失的索引对应的值。
# s1.add(s2, fill_value=0):
# - 对于 s1 中有但 s2 中没有的索引 (a, b),s2 中对应位置被视为0。
# - 对于 s2 中有但 s1