7.5 数据索引和选择
原文:Data Indexing and Selection 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。
在第二章中,我们详细介绍了在 NumPy 数组中访问,设置和修改值的方法和工具。这些包括索引(例如,arr[2,1]
),切片(例如,arr[:, 1:5]
),掩码(例如,arr[arr > 0]
),花式索引(例如,arr[0, [1, 5]]
)及其组合(例如,arr[:, [1, 5]]
)。
在这里,我们将看看在 Pandas Series
和DataFrame
对象中,访问和修改值的类似方法。如果你使用过 NumPy 模式,Pandas 中的相应模式将会非常熟悉,尽管有一些需要注意的怪异之处。
我们将从一维Series
对象的简单情况开始,然后转向更复杂的二维DataFrame
对象。
序列中的数据选择
我们在上一节中看到,Series
对象在很多方面都像一维 NumPy 数组,并且在许多方面像标准的 Python 字典。如果我们记住这两个重叠的类比,它将帮助我们理解这些数组中的数据索引和选择的模式。
作为字典的序列
像字典一样,Series
对象提供从一组键到一组值的映射:
import pandas as pd
data = pd.Series([0.25, 0.5, 0.75, 1.0],
index=['a', 'b', 'c', 'd'])
data
'''
a 0.25
b 0.50
c 0.75
d 1.00
dtype: float64
'''
data['b']
# 0.5
我们还可以使用字典式的 Python 表达式和方法,来检查键/索引和值:
代码语言:javascript复制'a' in data
# True
data.keys()
# Index(['a', 'b', 'c', 'd'], dtype='object')
list(data.items())
# [('a', 0.25), ('b', 0.5), ('c', 0.75), ('d', 1.0)]
Series
对象甚至可以用类字典语法来修改。就像你可以通过为新键赋值来扩展字典,你可以通过为新索引赋值来扩展Series
:
data['e'] = 1.25
data
'''
a 0.25
b 0.50
c 0.75
d 1.00
e 1.25
dtype: float64
'''
对象的这种容易修改的特性,是一个方便的特性:在其背后,Pandas 正在决定可能需要执行的内存布局和数据复制;用户通常不需要担心这些问题。
作为一维数组的序列
Series
建立字典式接口上,并通过与 NumPy 数组相同的基本机制,提供数组式的项目选择,即切片,掩码和花式索引。这些例子如下:
# 按照显式下标来切片
data['a':'c']
'''
a 0.25
b 0.50
c 0.75
dtype: float64
'''
# 按照隐式下标来切片
data[0:2]
'''
a 0.25
b 0.50
dtype: float64
'''
# 掩码
data[(data > 0.3) & (data < 0.8)]
'''
b 0.50
c 0.75
dtype: float64
'''
# 花式索引
data[['a', 'e']]
'''
a 0.25
e 1.25
dtype: float64
'''
其中,切片可能是混乱的根源。注意,当使用显式索引进行切片时(即data['a':'c']
),切片中包含最终索引,而在使用隐式索引进行切片时(即data[0:2]
),最终索引从切片中排除。
索引器:loc
,iloc
,和ix
这些切片和索引惯例可能会引起混淆。例如,如果你的Series
拥有显式的整数索引,那么索引操作如data[1]
将使用显式索引,而切片操作如data[1:3]
将使用隐式的 Python 风格索引。
data = pd.Series(['a', 'b', 'c'], index=[1, 3, 5])
data
'''
1 a
3 b
5 c
dtype: object
'''
# 索引的时候是显式索引
data[1]
# 'a'
# 切片的时候是隐式索引
data[1:3]
'''
3 b
5 c
dtype: object
'''
由于在整数索引的情况下存在这种潜在的混淆,Pandas 提供了一些特殊的索引器属性,这些属性明确地提供了特定的索引方案。这些不是函数方法而是属性,它们将特定切片接口提供给Series
中的数组。
首先,loc
属性让索引和切片始终引用显式索引:
data.loc[1]
# 'a'
data.loc[1:3]
'''
1 a
3 b
dtype: object
'''
iloc
属性让索引和切片始终引用隐式的 Python 风格索引:
data.iloc[1]
# 'b'
data.iloc[1:3]
'''
3 b
5 c
dtype: object
'''
第三个索引属性ix
是两者的混合,对Series
对象来说,相当于标准的[]
风格的索引。在DataFrame
对象的上下文中,ix
索引器的目的将变得更加明显,我们将在稍后讨论。
Python 代码的一个指导原则是“显式优于隐式”。loc
和iloc
的显式特性,使它们在维护清晰可读的代码时非常有用;特别是在整数索引的情况下,我建议使用这两者,来使代码更容易阅读和理解,并防止由于混合索引/切片约定而导致的细微错误。
数据帧中的数据选择
回想一下,DataFrame
在很多方面都类似二维或结构化数组,在其它方面莱斯共享相同索引的Series
结构的字典。在我们探索此结构中的数据选择时,记住些类比是有帮助的。
作为字典的数据帧
我们将考虑的第一个类比是,DataFrame
作为相关Series
对象的字典。让我们回到我们的州人口和面积的例子:
area = pd.Series({'California': 423967, 'Texas': 695662,
'New York': 141297, 'Florida': 170312,
'Illinois': 149995})
pop = pd.Series({'California': 38332521, 'Texas': 26448193,
'New York': 19651127, 'Florida': 19552860,
'Illinois': 12882135})
data = pd.DataFrame({'area':area, 'pop':pop})
data
area | pop | |
---|---|---|
California | 423967 | 38332521 |
Florida | 170312 | 19552860 |
Illinois | 149995 | 12882135 |
New York | 141297 | 19651127 |
Texas | 695662 | 26448193 |
构成DataFrame
列的单个Series
,可以通过列名称的字典式索引来访问:
data['area']
'''
California 423967
Florida 170312
Illinois 149995
New York 141297
Texas 695662
Name: area, dtype: int64
'''
同样,我们可以使用列名的字符串和属性风格来访问:
代码语言:javascript复制data.area
'''
California 423967
Florida 170312
Illinois 149995
New York 141297
Texas 695662
Name: area, dtype: int64
'''
属性风格的列访问,与字典风格的访问,实际上访问了完全相同的对象:
代码语言:javascript复制data.area is data['area']
# True
虽然这是一个有用的简写,但请记住,它并不适用于所有情况!例如,如果列名不是字符串,或者列名与DataFrame
的方法冲突,则无法进行属性风格的访问。例如,DataFrame
有pop()
方法,所以data.pop
将指向它而不是pop
列:
data.pop is data['pop']
# False
特别是,你应该避免尝试通过属性对列赋值(即使用data['pop'] = z
而不是data.pop = z
)。
与前面讨论的Series
对象一样,这种字典式语法也可用于修改对象,在这里添加一个新列:
data['density'] = data['pop'] / data['area']
data
area | pop | density | |
---|---|---|---|
California | 423967 | 38332521 | 90.413926 |
Florida | 170312 | 19552860 | 114.806121 |
Illinois | 149995 | 12882135 | 85.883763 |
New York | 141297 | 19651127 | 139.076746 |
Texas | 695662 | 26448193 | 38.018740 |
这显示了Series
对象之间的逐元素算术的直接语法;我们将在“使用 Pandas 中的数据进行操作”中深入研究它。
作为二维数组的数据帧
如前所述,我们还可以将DataFrame
视为扩展的二维数组。我们可以使用values
属性检查原始底层数据数组:
data.values
'''
array([[ 4.23967000e 05, 3.83325210e 07, 9.04139261e 01],
[ 1.70312000e 05, 1.95528600e 07, 1.14806121e 02],
[ 1.49995000e 05, 1.28821350e 07, 8.58837628e 01],
[ 1.41297000e 05, 1.96511270e 07, 1.39076746e 02],
[ 6.95662000e 05, 2.64481930e 07, 3.80187404e 01]])
'''
考虑到这一点,许多熟悉的数组式观测,可以在DataFrame
本身上执行。例如,我们可以转置完整的DataFrame
来交换行和列:
data.T
California | Florida | Illinois | New York | Texas | |
---|---|---|---|---|---|
area | 4.239670e 05 | 1.703120e 05 | 1.499950e 05 | 1.412970e 05 | 6.956620e 05 |
pop | 3.833252e 07 | 1.955286e 07 | 1.288214e 07 | 1.965113e 07 | 2.644819e 07 |
density | 9.041393e 01 | 1.148061e 02 | 8.588376e 01 | 1.390767e 02 | 3.801874e 01 |
然而,当谈到DataFrame
对象的索引时,很明显列的字典式索引,让我们不能将其简单地视为 NumPy 数组。特别是,将单个索引传递给数组会访问一行:
data.values[0]
'''
array([ 4.23967000e 05, 3.83325210e 07, 9.04139261e 01])
'''
将单个“索引”传递给DataFrame
会访问一列:
data['area']
'''
California 423967
Florida 170312
Illinois 149995
New York 141297
Texas 695662
Name: area, dtype: int64
'''
因此,对于数组风格的索引,我们需要另一个惯例。在这里,Pandas 再次使用前面提到的loc
,iloc
和ix
索引器。使用iloc
索引器,我们可以索引底层数组,好像它是一个简单的 NumPy 数组(使用隐式的 Python 风格索引),但结果中保留了DataFrame
索引和列标签:
data.iloc[:3, :2]
area | pop | |
---|---|---|
California | 423967 | 38332521 |
Florida | 170312 | 19552860 |
Illinois | 149995 | 12882135 |
与之类似,使用loc
索引器,我们可以用数组风格索引底层数据,但是使用显式索引和列名称:
data.loc[:'Illinois', :'pop']
area | pop | |
---|---|---|
California | 423967 | 38332521 |
Florida | 170312 | 19552860 |
Illinois | 149995 | 12882135 |
ix
索引器是这两种方法的混合:
data.ix[:3, :'pop']
area | pop | |
---|---|---|
California | 423967 | 38332521 |
Florida | 170312 | 19552860 |
Illinois | 149995 | 12882135 |
请记住,对于整数索引,ix
索引器具有与整数索引的Series
对象相同的潜在混淆。
任何熟悉的 NumPy 风格的数据访问模式,都可以在这些索引器中使用。例如,在loc
索引器中,我们可以组合掩码和花式索引,如下所示:
data.loc[data.density > 100, ['pop', 'density']]
pop | density | |
---|---|---|
Florida | 19552860 | 114.806121 |
New York | 19651127 | 139.076746 |
任何这些索引惯例也可用于设置或修改值;你可能习惯使用 NumPy 的标准方式完成它们:
代码语言:javascript复制data.iloc[0, 2] = 90
data
area | pop | density | |
---|---|---|---|
California | 423967 | 38332521 | 90.000000 |
Florida | 170312 | 19552860 | 114.806121 |
Illinois | 149995 | 12882135 | 85.883763 |
New York | 141297 | 19651127 | 139.076746 |
Texas | 695662 | 26448193 | 38.018740 |
为了提高你对 Pandas 数据操作的流畅性,我建议花一些时间使用简单的DataFrame
,并探索各种索引方法所允许的索引,切片,掩码和花式索引。
额外的索引惯例
有一些额外的索引约定可能与前面的讨论不一致,但在实践中可能非常有用。首先,索引引用列,切片引用行:
代码语言:javascript复制data['Florida':'Illinois']
area | pop | density | |
---|---|---|---|
Florida | 170312 | 19552860 | 114.806121 |
Illinois | 149995 | 12882135 | 85.883763 |
这样的切片也可以通过数字而不是索引来引用行:
代码语言:javascript复制data[1:3]
area | pop | density | |
---|---|---|---|
Florida | 170312 | 19552860 | 114.806121 |
Illinois | 149995 | 12882135 | 85.883763 |
与之类似,直接掩码操作也是按行而不是按列解释的:
代码语言:javascript复制data[data.density > 100]
area | pop | density | |
---|---|---|---|
Florida | 170312 | 19552860 | 114.806121 |
New York | 141297 | 19651127 | 139.076746 |
这两个惯例在语法上类似于 NumPy 数组上的惯例,虽然这些惯例可能不完全符合 Pandas 惯例,但它们在实践中非常有用。