添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

2. pandas

NumPy 的 ndarray 数据处理要求数据类型一致,且不能缺失,不可为数据项添加额外标签等,为了解决 ndarray 的强类型限制,Panda 对 NumPy 的 ndarray 对象进行了扩展。

建立在 NumPy 数组结构上的 Pandas, 提供了 Series 和 DataFrame 对象,为极度繁琐和耗时的“数据清理”(data munging)任务提供了捷径。

笔者使用 Anaconda 提供的集成数据处理环境,查看 pandas 版本:

4
import pandas as pd
print(pd.__version__)
0.20.3

2.1. 基本数据结构

pandas 在 NumPy 的 ndarray 对象基础上封装了三个基本数据结构 Series、 DataFrame 和 Index。 Pandas 在这些基本数据结构上实现了许多功能和方法。

2.1.1. Series 对象

Series 对象是一个带索引标签(Labels)的一维数组,打印查看很像只有一列的表,可以看做向量。可以使用 list 作为参数,来生成对应 Series 对象,例如:

14
sdata = pd.Series([1, 2, 3.14])
print(sdata)
0    1.00  # 默认使用从 0 开始的整数作为索引
1    2.00
2    3.14
dtype: float64
print(type(sdata).__name__)
print(sdata.dtype) # dtype 属性记录成员的类型
Series
float64

可以使用索引访问 Series 对象成员,如果使用切片返回的是一个 Series 对象。

5
print(type(sdata[1]).__name__, sdata[1])
print(type(sdata[0:-1]).__name__)
float64 2
Series

如同查看列表长度一样,可以使用 len() 查看成员数目:

3
print(len(sdata))

2.1.1.1. Series 索引

Series 对象和一维 NumPy 数组的本质差异在于索引:

  • NumPy 数组通过隐式定义的整数索引获取数值。
  • Pandas 的 Series 对象用显式定义的 RangeIndex 索引与数值关联。
  • 4
    # 打印 RangeIndex 类型
    print(sdata.index)
    RangeIndex(start=0, stop=3, step=1)
    

    显式索引让 Series 对象拥有了更具弹性的索引方式。 索引不再局限于整数,可以是任意想要的类型。例如用字符串作为索引:

    13
    sdata = pd.Series([1, 2, 3.14], index=['num1', 'num2', 'pi'])
    print(sdata)
    num1    1.00
    num2    2.00
    pi      3.14
    dtype: float64
    # 使用字符串作为索引
    print(sdata['pi'])
    

    Series 成员可以是其他任何对象,也可以是不同对象,这看起来很像字典,此时它的类型为 object:

    7
    sdata = pd.Series({'a': 1, 'b': 2, 'c': 'abc'})
    print(sdata)
    c    abc
    dtype: object
    

    2.1.1.2. Series 是特殊字典

    字典是一种将任意键映射到一组任意值或对象的数据结构,而 Series 对象是一种将类型键映射到一组类型值的数据结构。Pandas Series 的类型信息使得它在某些操作上比 Python 的字典更高效。

    可以直接用 Python 的字典创建一个 Series 对象:

    16
    id_dicts = {'John': 100,
                'Tom' : 101,
                'Bill': 102}
    ids = pd.Series(id_dicts)
    print(ids['Bill'])
    # 元素顺序按照索引字母大小进行排序
    print(ids)
    Bill    102
    John    100
    Tom     101
    dtype: int64
    

    和字典不同,Series 对象还支持数组形式的操作, 比如切片:

    7
    # 注意切片索引顺序不是按照字典中元素定义顺序,而是按照 Series 对象的索引顺序
    sub_ids = ids['Bill':'John']
    print(sub_ids)
    Bill    102
    John    100
    dtype: int64
    6
    subsdata = pd.Series({'a': 1, 'b': 2, 'c': 'abc'}, index=['a', 'c'])
    print(subsdata)
    c    abc
    dtype: object
    

    Series 对象只会保留显式定义的键值对。

    Series.index 属性获取所有行索引信息:

    4
    # 获取行索引信息
    print(subsdata.index)
    Index(['a', 'c'], dtype='object')
    

    2.1.2. DataFrame 对象

    如果将 Series 类比为带索引的一维数组(或者含有一列数据的带有行标签的单列表), 那么 DataFrame 就可以看作是一种既有行索引,又有列名的二维数组(或者含有行标签和列标签的表,每一列都是一个 Series 对象)。

    16
    id_dicts = {'John': 100,
                'Tom' : 101,
                'Bill': 102}
    age_dicts = {'John': 20,
                 'Tom' : 21,
                 'Bill': 19}
    studentd = pd.DataFrame({'id':  pd.Series(id_dicts),
                            'age': pd.Series(age_dicts)})
    print(studentd)
          age   id
    Bill   19  102
    John   20  100
    Tom    21  101
    

    从示例中可以看出 DataFrame 是一组 Series 的集合,每一列都是一个 Series 对象。

    2.1.2.1. DataFrame 索引

    在 NumPy 的二维数组里, data[0] 返回第一行;而在 DataFrame 中, data[‘col0’] 返回第一列。 因此,DataFrame 是一种通用字典,而不是通用数组。

    13
    # 使用列名字访问特定列
    print(studentd['age'])
    Bill    19
    John    20
    Tom     21
    Name: age, dtype: int64
    # 指定列名和行名
    print(studentd['age']['John'])
    # 通过 Series 对象字典创建
    studentd = pd.DataFrame({'id': ids})
    studentd = pd.DataFrame(ids, columns=['id'])
    

    通过字典列表创建: 任何元素是字典的列表都可以变成 DataFrame。

    14
    # 创建字典列表
    num = [{'num0': i, 'num*3': 3 * i} for i in range(3)]
    print(num)
    [{'num0': 0, 'num*3': 0}, {'num0': 1, 'num*3': 3}, {'num0': 2, 'num*3': 6}]
    # 创建 DataFrame 对象
    print(pd.DataFrame(num))
       num*3  num0
    0      0     0
    1      3     1
    2      6     2
    

    如果字典中有些键不存在,Pandas 会用 NaN(不是数字或此处无数,Not a number) 来表示:

    5
    numd = pd.DataFrame([{'a': 1, 'b': 2}, {'b': 3, 'c': 4}])
    print(numd)
         a  b    c
    0  1.0  2  NaN
    1  NaN  3  4.0
    

    通过 NumPy 二维数组创建。 假如有一个二维数组, 就可以创建一个可以指定行列索引值的 DataFrame。 如果不指定行列索引值,那么行列默认都是整数索引值:

    17
    narray = np.random.randint(3, size=(3, 2))
    print(narray)
    [[2 0]
     [2 2]
     [2 1]]
    df = pd.DataFrame(narray,
                      columns = ['foo', 'bar'],
                      index=['a', 'b', 'c'])
    print(df)
       foo  bar
    a    2    0
    b    2    2
    c    2    1
    

    查看行索引和列索引:

    5
    print(df.index)
    print(df.columns)
    Index(['a', 'b', 'c'], dtype='object')
    Index(['foo', 'bar'], dtype='object')
    

    通过 NumPy 结构化数组创建:

    12
    A = np.ones(3, dtype=[('A', 'i8'), ('B', 'f8')])
    print(A)
    [(1,  1.) (1,  1.) (1,  1.)]
    print(pd.DataFrame(A))
    0  1  1.0
    1  1  1.0
    2  1  1.0
    

    2.1.3.1. 通过属性更改

    可以通过 df.index 和 df.columns 查看行名和列名,同样可以通过这些属性更改行或列的名称,对于 Series 来说只有行名:

    23
    narray = np.random.randint(3, size=(3, 2))
    df = pd.DataFrame(narray,
                      columns = ['foo', 'bar'],
                      index=['a', 'b', 'c'])
    # 更新行索引标签
    df.index = [0, 1, 2]
    print(df.index)
    # 更新列索引标签
    df.columns = ['a', 'b']
    print(df.columns)
    Int64Index([0, 1, 2], dtype='int64')
    Index(['a', 'b'], dtype='object')
    print(df)
    0  0  0
    1  1  0
    2  0  2
    

    注意行或列的标签个数和 DataFrame 对象的行数或列数必须一致,否则会报错。

    2.1.4. Index 对象

    Pandas 的 Index 对象可以将它看作是一个不可变数组或有序集合, Index 对象可以包含重复值。

    16
    # 可以包含重复值
    ind = pd.Index([2, 3, 5, 7, 7, 11])
    print(type(ind).__name__)
    Int64Index
    # 索引访问元素
    print(ind[1])
    # 切片访问返回 Index 对象
    print(ind[::2])
    Int64Index([2, 5, 7], dtype='int64')
    

    Index 对象不支持对数据的修改:

    3
    ind[1] = 1
    TypeError: Index does not support mutable operations
    

    Index 对象还有许多与 NumPy 数组相似的属性:

    3
    print(ind.size, ind.shape, ind.ndim, ind.dtype)
    6 (6,) 1 int64
    

    2.1.4.1. 排序操作

    Index 对象支持对元素的排序:

    10
    ind = pd.Index([2, 4, 5, 1, 11])
    print(ind)
    Int64Index([2, 4, 5, 1, 11], dtype='int64')
    ind = ind.sort_values()
    print(ind)
    Int64Index([1, 2, 4, 5, 11], dtype='int64')
    

    2.1.4.2. 集合操作

    Pandas 对象被设计用于实现多种操作, 如连接(join) 数据集,其中会涉及许多集合操作。 Index 对象遵循 Python 标准库的集合(set) 数据结构的许多习惯用法, 包括并集、 交集、 差集等:

    19
    indA = pd.Index([1, 3, 5, 7, 9])
    indB = pd.Index([2, 3, 5, 7, 11])
    # 交集,等价于 indA.intersection(indB)
    print(indA & indB)
    Int64Index([3, 5, 7], dtype='int64')
    print(indA | indB)
    Int64Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')
    print(indA ^ indB)
    Int64Index([1, 2, 9, 11], dtype='int64')
    

    Index 对象进行集合操作的结果还是 Index 对象。它可以是一个空对象。

    7
    indA = pd.Index([1, 3, 5, 7, 9])
    indB = pd.Index([2])
    print(indA & indB)
    Int64Index([], dtype='int64')
    

    2.2. 数据选择和扩展

    NumPy 数组可以通过索引,切片,花式索引和掩码操作进行各类选择,Pandas 的 Series 和 DataFrame 对象具有相似的数据获取与调整操作。

    2.2.1. Series数据选择

    2.2.1.1. 访问数据

    将Series看作字典,和字典一样, Series 对象提供了键值对(索引)的映射:

    25
    # 使用 in 或者 not in 判断键是否存在
    sdata = pd.Series([1, 2, 3.14], index=['num1', 'num2', 'pi'])
    # 等价于 sdata.index
    print(sdata.keys())
    print('pi' in sdata) # 等价于 'pi' in sdata.keys()
    Index(['num1', 'num2', 'pi'], dtype='object')
    # 判断值是否存在,Series.values 是 ndarray 类型
    print(sdata.values, type(sdata.values).__name__)
    print(1 in sdata.values)
    [ 1.    2.    3.14] ndarray
    # Series.items() 返回 zip 类型,可以转换为 list
    print(sdata.items())
    print(list(sdata.items()))
    <zip object at 0x0000020B8A3DCF08>
    [('num1', 1.0), ('num2', 2.0), ('pi', 3.1400000000000001)]
    

    Series 不仅有着和字典一样的接口, 而且还具备和 NumPy 数组一样的数组数据选择功能,包括索引、掩码、花式索引等操作,例如:

    47
    # 将显式索引作为切片,结果包含最后一个索引
    subs = sdata['num1':'num2']
    print(subs)
    num1    1.0
    num2    2.0
    dtype: float64
    # 将隐式整数索引作为切片,结果不含最后一个索引
    print(sdata[0:2])
    print(sdata[-1:0:-1])
    num1    1.0
    num2    2.0
    dtype: float64
    pi      3.14
    num2    2.00
    dtype: float64
    # 掩码,返回 bool 类型的 Series 掩码对象
    print((sdata > 1) & (sdata < 4))
    num1    False
    num2     True
    pi       True
    dtype: bool
    # Series 掩码对象作为索引
    subs = sdata[(sdata > 1) & (sdata < 4)]
    print(subs)
    num2    2.00
    pi      3.14
    dtype: float64
    # 花式索引
    subs = sdata[['num1', 'pi']]
    print(subs)
    num1    1.00
    pi      3.14
    dtype: float64
    

    切片是绝大部分混乱之源。 需要注意的是,当使用显式索引(即 data[‘a’:’c’]) 作切片时, 结果包含最后一个索引; 而当使用隐式索引(即 data[0:2]) 作切片时, 结果不包含最后一个索引。

    索引标签默认是无序的,也即根据创建时标签声明的顺序来排列,我们可以对它进行排序,以方便切片操作:

    16
    sdata = pd.Series([1, 2, 3.14], index=['num2', 'num1', 'pi'])
    print(sdata)
    num2    1.00
    num1    3.14
    pi      2.00
    dtype: float64
    # 对索引进行排序
    sdata = sdata.sort_index()
    print(sdata['num1':'num2'])
    num1    2.0
    num2    1.0
    dtype: float64
    

    2.2.1.2. 索引器

    切片和取值的习惯用法经常会造成混乱。如果 Series 是显式整数索引,那么 data[1] 这样的取值操作会使用显式索引,而 data[1:3] 样的切片操作却会使用隐式索引。

    11
    sdata = pd.Series([1, 2, 3.14], index=[1, 2, 3])
    print(sdata[1]) # 显式索引,使用 sdata[0] 将报错
    print(sdata[0:2]) # 隐式索引,不含 sdata[2]
    1    1.0
    2    2.0
    dtype: float64
    

    由于整数索引很容易造成混淆,所以 Pandas 提供了一些索引器(indexer) 属性来作为取值的方法。它们不是 Series 对象的函数方法, 而是暴露切片接口的属性。

    第一种索引器是 loc 属性, 表示取值和切片都是显式的:

    11
    sdata = pd.Series([1, 2, 3.14], index=[1, 2, 3])
    print(sdata.loc[1])   # 显式索引
    print(sdata.loc[1:2]) # 显式索引
    1    1.0
    2    2.0
    dtype: float64
    

    第二种是 iloc 索引属性,表示取值和切片都是隐式索引(从 0 开始, 左闭右开区间):

    10
    sdata = pd.Series([1, 2, 3.14], index=[1, 2, 3])
    print(sdata.iloc[1])  # 隐式索引
    print(sdata.iloc[1:2])# 隐式索引
    2    2.0
    dtype: float64
    

    第三种取值属性是 ix,它是前两种索引器的混合形式,从 0.20.0 版本开始,ix 索引器不再被推荐使用。

    Python 代码的设计原则之一是“显式优于隐式”。 使用 loc 和 iloc 可以让代码更容易维护, 可读性更高。 特别是在处理整数索引的对象时, 我强烈推荐使用这两种索引器。 它们既可以让代码阅读和理解起来更容易, 也能避免因误用索引 / 切片而产生的小 bug。

    2.2.1.3. 扩展数据

    Series 对象还可以用字典语法调整数据。可以通过增加新的索引值扩展 Series:

    8
    sdata['e'] = 2.72
    print(sdata)
    num1    1.00
    num2    2.00
    pi      3.14
    e       2.72
    dtype: float64
    18
    studentd = pd.DataFrame({'id':  pd.Series(id_dicts),
                            'age': pd.Series(age_dicts)})
    print(studentd['id']['John']) # 字典键方式访问
    print(studentd['id']) # 列属性方式访问
    Bill    102
    John    100
    Tom     101
    Name: id, dtype: int64
    print(studentd['id']['John']) # 列属性和行属性方式访问
    

    虽然属性形式的数据选择方法很方便, 但是它并不是通用的。 如果列名不是纯字符串, 或者列名与 DataFrame 的方法同名, 那么就不能用属性索引。 例如, DataFrame 有一个 pop() 方法, 如果用data.pop 就不会获取 ‘pop’ 列, 而是显示为方法。

    另外, 还应该避免对用属性形式选择的列直接赋值(即可以用data[‘pop’] = z,但不要用 data.pop = z)防止覆盖方法名。

    和前面介绍的 Series 对象一样,还可以用字典形式的语法调整对象,如果要增加一列可以这样做:

    8
    # 等价于 studentd['newcol'] = studentd.id + studentd.age
    studentd['newcol'] = studentd['id'] + studentd['age']
    print(studentd)
          age   id  newcol
    Bill   19  102     121
    John   20  100     120
    Tom    21  101     122
    

    将DataFrame看作二维数组,用 values 属性按行查看数组数据:

    6
    print(studentd.values, '\n', type(studentd.values).__name__)
    [[ 19 102]
     [ 20 100]
     [ 21 101]]
     ndarray
    

    由于返回值是 ndarray 类型,所以可以对其进行任何矩阵操作:

    12
    # 获取行数据(获取一列数据要传递列索引)
    print(studentd.values[0])
    [ 19 102]
    print(studentd.values.T)
    [[ 19  20  21]
     [102 100 101]]
    print(studentd.keys())
    

    keys() 方法返回列名组成的索引类型 Index:

    Index(['age', 'id'], dtype='object')

    2.2.2.2. 使用索引器

    索引器的作用在于指明使用隐式索引还是显示索引。通过 iloc 索引器,可以像对待 NumPy 数组一样索引 Pandas 的底层数组(Python 的隐式索引),DataFrame 的行列标签会自动保留在结果中:

    4
    print(studentd.iloc[:1, :2])
          age   id
    Bill   19  102
    

    任何用于处理 NumPy 形式数据的方法都可以用于这些索引器。例如,可以在 loc 索引器中结合使用掩码与花式索引方法:

    6
    # 选择 age >= 20 的学生的 id 信息
    print(studentd.loc[studentd.age >= 20, ['id']])
    John  100
    Tom   102
    

    2.3. 数值运算

    NumPy 的基本能力之一是快速对每个元素进行运算,既包括基本算术运算(加、 减、 乘、 除) , 也包括更复杂的运算(三角函数、 指数函数和对数函数等),参考 算术运算。 Pandas 继承了 NumPy 的功能,也即这些函数同样可以作用在 Pandas 对象上。

    除此之外,Pandas 也实现了一些高效技巧:一元运算作用在 Pandas 对象上时会保留索引和列标签;而对于二元运算(如加法和乘法),Pandas 在传递通用函数时会自动对齐索引进行计算。这就意味着,保存数据内容与组合不同来源的数据——两处在NumPy 数组中都容易出错的地方在 Pandas 中很容易实现。

    2.3.1. 一元运算

    8
    sdata = pd.Series(np.arange(4))
    print(sdata * 2)
    dtype: int32
    

    可以发现 np 函数作用在 Pandas 对象上的返回值还是 Pandas 对象,会保留原标签。

    6
    df = pd.DataFrame(np.arange(4).reshape(2, 2), columns=['a', 'b'])
    print(np.sin(df / 4 * np.pi))
    0  0.0  0.707107
    1  1.0  0.707107
    

    2.3.2. 二元运算

    当在两个 Series 或 DataFrame 对象上进行二元计算时,Pandas 会在计算过程中对齐两个对象的索引。当处理不完整的数据时,这一点非常方便。

    12
    sdata0 = pd.Series(np.arange(3))
    sdata1 = pd.Series(np.arange(2))
    print(sdata0)
    print(sdata1)
    dtype: int32
    dtype: int32
    

    首先生成两个索引不同的 Series 对象,然后进行相加:

    6
    print(sdata0 + sdata1)
    0    0.0
    1    2.0
    2    NaN
    dtype: float64
    

    结果数组的索引是两个输入数组索引的并集。对于缺失位置的数据,Pandas 会用 NaN 填充,表示“此处无数”。这是 Pandas 表示缺失值的方法。

    如果用 NaN 值不是我们想要的结果, 那么可以用适当的对象方法代替运算符。 例如, A.add(B) 等价于 A + B, 也可以设置参数自定义 A 或 B 缺失的数据:

    7
    # sdata1 中缺失的索引 2 的值将使用 0 替代
    print(sdata0.add(sdata1, fill_value=0))
    0    0.0
    1    2.0
    2    2.0  # 0 + 2
    dtype: float64
    

    在计算两个 DataFrame 时,类似的索引对齐规则也同样会出现在共同(并集)列中:

    26
    df0 = pd.DataFrame(np.arange(4).reshape(2,2), columns=list('BA'))
    df1 = pd.DataFrame(np.arange(2).reshape(2,1), columns=list('A'))
    print(df0)
    print(df1)
    0  0  1
    1  2  3
    # 填充缺省值 NaN
    print(df0 + df1)
    0  1 NaN
    1  4 NaN
    # 指定缺省值
    print(df0.sub(df1, fill_value=0))
    0  1  0.0
    1  2  2.0
    

    两个对象的行列索引可以是不同顺序的,结果的索引会自动按顺序排列。

    Python运算符与Pandas方法的映射关系:

    29
    narray0 = np.array([2,2])
    narray1 = np.array([[1,1],[2,2]])
    print(narray0 + narray1)
    [[3 3]
     [4 4]]
    sdata = pd.Series(narray0, index=list('AB'))
    print(sdata)
    dtype: int32
    df = pd.DataFrame(narray1, columns=list('AB'))
    print(df)
    0  1  2
    1  1  2
    print(sdata + df)
    0  3  3
    1  4  4
    

    根据 NumPy 的广播规则,让二维数组减自身的一行数据会按行计算。如果想按列计算,就需要利用前面介绍过的运算符方法, 通过 axis 参数设置:

    14
    # 默认按行计算
    print(df + df.iloc[0])
    0  2  2
    1  3  3
    # 按列相加
    print(df.add(df['A'], axis=0))
    0  2  2
    1  4  4
    

    这些行列索引的保留与对齐方法说明 Pandas 在运算时会一直保存这些数据内容, 从而避免在处理数据类型有差异和 / 或维度不一致的 NumPy 数组时可能遇到的问题。

    2.4. 缺失值处理

    现实中采集的数据很少是干净整齐的,许多目前流行的数据集都会有数据缺失的现象。

    通常有两种方式表示缺失值:

  • 通过一个覆盖全局的掩码表示缺失值,例如 R 语言为每个元素保留 1 bit 用于标记缺失值。
  • 用一个标签值(sentinel value) 表示缺失值,比如用 NaN(不是一个数) 表示缺失的浮点数。
  • Pandas 选择用标签方法表示缺失值,包括两种 Python 原有的缺失值: 浮点数据类型的 NaN 值, 以及 Python 的 None 对象。

    2.4.1. None

    None 是一个 Python 内置对象,经常在代码中表示缺失值。

    3
    print(None, type(None).__name__)
    None NoneType
    

    由于 None 是一个 Python 对象,只能用于 ‘object’ 数组类型(即由 Python 对象构成的数组),不能用于其他类型的数组:

    10
    print(np.array([1, None, 3, 4], dtype=object))
    [1 None 3 4]
    # 如果不是 object 类型将报错
    print(np.array([1, None, 3, 4], dtype=int))
    TypeError: int() argument must be a string, a bytes-like
    object or a number, not 'NoneType'
    

    这里 dtype=object 表示 NumPy 认为由于这个数组是 Python 对象构成的,因此将其类型判断为 object。虽然这种类型在某些情景中非常有用,对数据的任何操作最终都会在 Python 层面完成,但是在进行常见的快速操作时,这种类型比其他原生类型数组要更耗时。

    由于 Python 没有对 None 对象定义加减等运算操作,所以在包含 None 的数组上执行这类操作均会报错。

    4
    narray = np.array([1, None, 3, 4], dtype=object)
    print(narray.sum())
    TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
    

    在 Pandas 中,None 被自动转化为 NaN 类型,由于 NaN 是特殊的浮点数,所以生成的对象类型默认为浮点型 float64:

    7
    ps = pd.Series([1, 2, None])
    print(ps)
    0    1.0
    1    2.0
    2    NaN
    dtype: float64
    

    当为一个整型对象的元素赋值为 None 时,类型自动转换为 float64:

    12
    ps = pd.Series([1, 2])
    print(ps.dtype)
    int64
    ps[0] = None
    print(ps)
    0    NaN
    1    2.0
    dtype: float64
    

    2.4.2. NaN

    NaN(全称 Not a Number,不是一个数字),是一种按照 IEEE 浮点数标准设计、在任何系统中都兼容的特殊浮点数。表示未定义或不可表示的值。

    IEEE 754-1985中,用指数部分全为1、小数部分非零表示NaN。以32位IEEE单精度浮点数的NaN为例,按位表示即:S111 1111 1AXX XXXX XXXX XXXX XXXX XXXX,S为符号位,符号位S的取值无关紧要;A是小数部分的最高位(the most significant bit of the significand),其取值表示了 NaN 的类型:X 不能全为0,并被称为 NaN 的payload。

    通常返回 NaN 的运算有如下三种:

  • 至少有一个参数是 NaN 的运算
  • 下列除法运算:0/0、∞/∞、∞/−∞、−∞/∞、−∞/−∞
  • 下列乘法运算:0×∞、0×−∞
  • 下列加法运算:∞ + (−∞)、(−∞) + ∞
  • 下列减法运算:∞ - ∞、(−∞) - (−∞)
  • 产生复数结果的实数运算。例如:
  • 对负数进行开偶次方的运算
  • 对负数进行对数运算
  • 对正弦或余弦到达域以外的数进行反正弦或反余弦运算
  • 由于 NaN 是特殊的浮点数,所以当数组成员包含 NaN 时,其类型为浮点型,默认为 float64。

    16
    narray = np.array([1, np.nan, 3, 4])
    print(narray.dtype)
    float64
    # 作用在 NaN 上的运算总是返回 NaN
    print(narray.sum())
    # 指定类型为 int 将报错
    narray = np.array([1, np.nan, 3, 4], dtype=int)
    ValueError: cannot convert float NaN to integer
    

    NumPy 同时提供了一类特殊的累计函数,参考 聚合统计,它们可以忽略缺失值的影响:

    8
    print(np.nansum(narray))
    print(np.nanmin(narray), np.nanmax(narray))
    1.0 4.0
    

    NaN 是一种特殊的浮点数, 不是整数、 字符串以及其他数据类型。

    np.nan 表示常量 NaN,如果在创建 Pandas 对象时,包含 np.nan 成员,则对象 dtype 自动转化为 float64 类型,同样赋值操作也会改变 dtype:

    18
    ps = pd.Series([1, 2, np.nan])
    print(ps.dtype)
    float64
    ps = pd.Series([1, 2])
    print(ps.dtype)
    int64
    ps[0] = np.nan
    print(ps)
    0    NaN
    1    2.0
    dtype: float64
    9
    df = pd.DataFrame([[1, np.nan, 2],
                        [2, 3, 5],
                        [np.nan, 4, 6]])
    print(df)
         0    1  2
    0  1.0  NaN  2
    1  2.0  3.0  5
    2  NaN  4.0  6
    

    无法从 DataFrame 中单独剔除一个值,要么是剔除缺失值所在的整行,要么是整列。根据实际需求,来剔除整行或整列,DataFrame 中的 dropna() 会有一些参数可以配置。 默认情况下, dropna() 会剔除任何包含缺失值的整行数据:

    4
    print(df.dropna())
         0    1  2
    1  2.0  3.0  5
    

    可以设置按不同的坐标轴剔除缺失值, 比如 axis=1(或 axis=’columns’) 会剔除任何包含缺失值的整列数据:

    6
    print(df.dropna(axis='columns'))
    

    这么做也会把非缺失值一并剔除,因为可能有时候只需要剔除全部是缺失值的行或列,或者绝大多数是缺失值的行或列。可以通过设置 how 或 thresh 参数来满足,它们可以设置剔除行或列缺失值的数量阈值。

    默认设置是 how=’any’, 也就是说只要有缺失值就剔除整行或整列(通过 axis 设置坐标轴)。还可以设置 how=’all’, 这样就只会剔除全部是缺失值的行或列了:

    16
    df[3] = np.nan
    print(df)
         0    1  2   3
    0  1.0  NaN  2 NaN
    1  2.0  3.0  5 NaN
    2  NaN  4.0  6 NaN
    df = df.dropna(axis='columns', how='all')
    print(df)
         0    1  2
    0  1.0  NaN  2
    1  2.0  3.0  5
    2  NaN  4.0  6
    

    还可以通过 thresh 参数设置行或列中非缺失值的最小数量,从而实现更加个性化的配置:

    5
    df = df.dropna(axis='rows', thresh=3)
    print(df)
         0    1  2   3
    1  2.0  3.0  5 NaN
    

    第 1 行与第 3 行被剔除了, 因为它们只包含两个非缺失值。

    2.4.4.3. 填充缺失值

    有时可能并不想移除缺失值,而是想把它们替换成有效的数值。 有效的值可能是像 0、 1、 2 那样单独的值,也可能是经过填充(imputation) 或转换(interpolation) 得到的。 虽然你可以通过isnull() 方法建立掩码来填充缺失值, 但是 Pandas 为此专门提供了一个 fillna() 方法, 它将返回填充了缺失值后的数组副本。

    8
    ps = pd.Series([1, np.nan, 2, None], index=list('abcd'))
    print(ps)
    a    1.0
    b    NaN
    c    2.0
    d    NaN
    dtype: float64
    

    我们将用一个单独的值来填充缺失值, 例如用 -1:

    7
    print(ps.fillna(-1))
    a    1.0
    b   -1.0
    c    2.0
    d   -1.0
    dtype: float64
    

    可以用缺失值前面的有效值来从前往后填充(forward-fill):

    7
    print(ps.fillna(method='ffill'))
    a    1.0
    b    1.0
    c    2.0
    d    2.0
    dtype: float64
    

    也可以用缺失值后面的有效值来从后往前填充(back-fill) :

    6
    print(ps.fillna(method='bfill'))
    a    1.0
    b    2.0
    c    2.0
    d    NaN
    dtype: float64
    

    无论是从前往后还是从后往前,NaN 之后或之前如果都是 NaN 则无法实现填充。

    DataFrame 的操作方法与 Series 类似, 只是在填充时需要设置坐标轴参数 axis:

    27
    df = pd.DataFrame([[1, np.nan, 2],
                       [2, 3, 5],
                       [np.nan, np.nan, np.nan]])
    print(df)
         0    1    2
    0  1.0  NaN  2.0
    1  2.0  3.0  5.0
    2  NaN  NaN  NaN
    # 从前向后填充行
    print(df.fillna(method='ffill', axis=1))
         0    1    2
    0  1.0  1.0  2.0
    1  2.0  3.0  5.0
    2  NaN  NaN  NaN
    # 从后向前填充行
    print(df.fillna(method='bfill', axis=1))
         0    1    2
    0  1.0  2.0  2.0
    1  2.0  3.0  5.0
    2  NaN  NaN  NaN
    

    需要注意的是,假如在从前往后填充时,需要填充的缺失值前面没有值,那么它就仍然是缺失值,这个机制是递归填充。

    16
    # 从前向后填充列
    print(df.fillna(method='ffill', axis=0))
         0    1    2
    0  1.0  NaN  2.0
    1  2.0  3.0  5.0
    2  2.0  3.0  5.0
    # 从后向前填充列
    print(df.fillna(method='bfill', axis=0))
         0    1    2
    0  1.0  3.0  2.0
    1  2.0  3.0  5.0
    2  NaN  NaN  NaN
    

    2.5.1. csv 文件数据

    CSV 是逗号分隔值(Comma-Separated Values有时也称为字符分隔值,因为分隔字符也可以不是逗号)的缩写,其文件以纯文本形式存储表格数据(数字和文本)。可以使用记事本直接打开它,或者使用 Excel 打开。

    名为 students.csv 的示例文件内容如下:

    3
    age,id,name
    20,100,John
    21,101,Tom
    19,102,Bill
    

    2.5.1.1. 读取数据

    9
    # 参数 header 默认值为 0,表示以第一行为列索引
    # 等价于 df = pd.read_csv('students.csv')
    df = pd.read_csv('students.csv', header=0)
    print(df)
       age   id  name
    0   20  100  John
    1   21  101   Tom
    2   19  102  Bill
    

    read_csv() 方法具有非常丰富的参数,常用参数说明如下:

  • sep:分隔符,默认是‘,’,CSV文件的分隔符
  • header:列名所在 csv 中的行(列索引),默认第一行为列名(默认header=0),header=None 说明第一行不是列名,它会生成新的整数列名。
  • names:当 csv 文件没有列名时候,可以用 names 加上要用的列名
  • index_col:要用的行名(index),int或sequence或False,默认为 None,即默认添加从 0 开始的 index,若要用第一列作为行索引则 index_col = 0。
  • 30
    # header 为 None,表示 csv 第一行数据作为普通数据
    df = pd.read_csv('students.csv', header=None)
    print(df)
         0    1     2
    0  age   id  name
    1   20  100  John
    2   21  101   Tom
    3   19  102  Bill
    # 使用 names 指定列名
    df = pd.read_csv('students.csv', header=None, names=['a', 'b', 'c'])
    print(df)
         a    b     c
    0  age   id  name
    1   20  100  John
    2   21  101   Tom
    3   19  102  Bill
    # 指定 csv 文件第一列为行名
    df = pd.read_csv('students.csv', header=0, index_col=0)
    print(df)
          id  name
    20   100  John
    21   101   Tom
    19   102  Bill
    

    2.5.1.2. 分块读取

    read_csv() 的 chunksize 参数支持指定每次读取的行数,返回的是一个可迭代的对象 TextFileReader,这对于读取超大文件特别有用:

    18
    # 每次读取两行
    tfr = pd.read_csv('students.csv', header=0, chunksize=2)
    print(type(tfr).__name__)
    TextFileReader
    for chunk in tfr:
        print('------------------')
        print(chunk)
    ------------------
       age   id  name
    0   20  100  John
    1   21  101   Tom
    ------------------
       age   id  name
    2   19  102  Bill
    

    可以看到每次从 TextFileReader 迭代对象读取时都会带上列名。

    2.5.1.3. 保存数据

    to_csv() 用于写出数据到文件,注意 index 参数指明是否写出行信息:

    3
    studentdf = pd.DataFrame({'id': [100,101,102],
                              'name':['John', 'Tom', 'Bill'],
                              'age': [20, 21, 19]})
    studentdf.to_csv('students.csv', index=False)
    

    2.6. 层级索引

    一级索引的 Series 看起来很像一维数组,且是单列数组。DataFrame 可以看做有两个索引的二维数组。

    通过层级索引(hierarchical indexing,也被称为多级索引,multi-indexing)配合多个有不同等级(level)的一级索引一起使用,这样就可以将高维数组转换成类似一维 Series 和二维 DataFrame 对象的形式。

    2.6.1. 多级索引的 Series

    多级索引的 Series,索引是一个二维数组,相当于多个索引决定一个值,类似于 DataFrame 的行索引和列索引:

    17
    ps = pd.Series([90, 80, 95, 91, 92, 88], index=[['John'] * 3 + ['Tom'] * 3,
                    ['Maths', 'English', 'Chemistry'] * 2])
    print(ps)
    John  Maths        90
          English      80
          Chemistry    95
    Tom   Maths        91
          English      92
          Chemistry    88
    dtype: int64
    print(ps.index)
    MultiIndex(levels=[['John', 'Tom'], ['Chemistry', 'English', 'Maths']],
             labels=[[0, 0, 0, 1, 1, 1], [2, 1, 0, 2, 1, 0]])
    

    此时的索引类型为 MultiIndex。MultiIndex 里面的 levels 属性表示索引的等级,可以看到 John 和 Tom 处在第一级,各科课程名称为第二级。

    labels 标签包含了各个索引等级对应的数据的整数索引。

    15
    # 使用一级索引查看数据
    print(ps['Tom'])
    Maths        91
    English      92
    Chemistry    88
    dtype: int64
    # 使用切片查看二级索引 Maths 数据
    print(ps[:, 'Maths'])
    John    90
    Tom     91
    dtype: int64
    

    2.6.1.1. 多级索引 Series 转 DataFrame

    unstack() 方法可以快速将一个多级索引的 Series 转化为普通索引的 DataFrame:

    11
    df = ps.unstack()
    print(type(df).__name__)
    DataFrame
    print(df)
          Chemistry  English  Maths
    John         95       80     90
    Tom          88       92     91
    

    stack() 方法实现相反的转换:

    9
    print(df.stack())
    John  Chemistry    95
          English      80
          Maths        90
    Tom   Chemistry    88
          English      92
          Maths        91
    dtype: int64
    

    2.6.1.2. 增加索引层级

    如果我们可以用含多级索引的一维 Series 数据表示二维数据,那么我们就可以用 Series 或 DataFrame 表示三维甚至更高维度的数据。 多级索引每增加一级,就表示数据增加一维, 利用这一特点就可以轻松表示任意维度的数据了。

    假如上面示例中的学生成绩是 2012 年数据,我们要添加 2013 年的数据,只需要增加一个新的索引层级即可:

    10
    newps = pd.DataFrame({'2012': ps, '2013': [98,87,93, 90,91,84]})
    print(newps)
                    2012  2013
    John Maths        90    98
         English      80    87
         Chemistry    95    93
    Tom  Maths        91    90
         English      92    91
         Chemistry    88    84
    

    当然我们可以使用 stack() 转化为 Series 类型:

    14
    print(newps.stack())
    John  Maths      2012    90
                     2013    98
          English    2012    80
                     2013    87
          Chemistry  2012    95
                     2013    93
    Tom   Maths      2012    91
                     2013    90
          English    2012    92
                     2013    91
          Chemistry  2012    88
                     2013    84
    

    这一实现效果令人惊喜。求取各科平均成绩非常简单:

    7
    # 求取两年各科平均成绩
    average = (newps['2013'] + newps['2012']) / 2
    print(average.unstack())
          Chemistry  English  Maths
    John       94.0     83.5   94.0
    Tom        86.0     91.5   90.5
    
  • MultiIndex.from_arrays 转换由 arrays 组成的 list 为 MultiIndex
  • MultiIndex.from_tuples 转换元组为 MultiIndex
  • MultiIndex.from_product 由迭代对象的笛卡尔积生成 MultiIndex
  • 2.6.2.1. array 转多级索引

    18
    # names 指明每个层级的名称
    arrays = [[1, 1, 2, 2], ['red', 'blue', 'red', 'blue']]
    pm = pd.MultiIndex.from_arrays(arrays, names=('number', 'color'))
    print(pm)
    MultiIndex(levels=[[1, 2], ['blue', 'red']],
               labels=[[0, 0, 1, 1], [1, 0, 1, 0]],
               names=['number', 'color'])
    # 查看多级索引的属性
    print(pm.levels)
    print(pm.labels)
    print(pm.names)
    [[1, 2], ['blue', 'red']]
    [[0, 0, 1, 1], [1, 0, 1, 0]]
    ['number', 'color']
    7
    tuples = [(1, 'red'), (1, 'blue'),(2, 'red'), (2, 'blue')]
    pm = pd.MultiIndex.from_tuples(tuples, names=('number', 'color'))
    print(pm)
    MultiIndex(levels=[[1, 2], ['blue', 'red']],
               labels=[[0, 0, 1, 1], [1, 0, 1, 0]],
               names=['number', 'color'])
    8
    numbers = [0, 1, 2]
    colors = ['green', 'purple']
    pm = pd.MultiIndex.from_product([numbers, colors], names=['number', 'color'])
    print(pm)
    MultiIndex(levels=[[0, 1, 2], ['green', 'purple']],
               labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]],
               names=['number', 'color'])
    

    使用笛卡尔积方式创建 MultiIndex 对象,层次是比较清晰的。

    11
    # 增加模拟数据,查看层级和层级名称
    pf = pd.DataFrame(np.arange(6), index=pm)
    print(pf)
    number color
    0      green   0
           purple  1
    1      green   2
           purple  3
    2      green   4
           purple  5
    23
    students = ['John', 'Tom']
    subjects = ['Maths', 'English', 'Chemistry']
    # 创建多级行索引
    index = pd.MultiIndex.from_product([students, subjects], names=['student', 'subject'])
    # 创建多级列索引
    columns = pd.MultiIndex.from_product([['2012', '2013'], ['first_half', 'latter_half']],
                                          names=['year', 'half'])
    # 模拟成绩数据,一共是 6 行 4 列
    pf = pd.DataFrame(99 - np.random.randint(20, size=(6, 4)), index=index, columns=columns)
    print(pf)
    year                    2012                   2013
    half              first_half latter_half first_half latter_half
    student subject
    John    Maths             82          81         86          81
            English           83          81         88          94
            Chemistry         81          85         85          81
    Tom     Maths             97          80         95          82
            English           89          99         94          92
            Chemistry         97          92         92          84
    

    有上例可以看出多级行列索引的创建非常简单。我们可以方便查看各级索引的数据:

    6
    # 查询 2012 年上半年成绩数据
    print(pf['2012','first_half'].unstack())
    subject  Chemistry  English  Maths
    student
    John            89       88     88
    Tom             83       84     99
    

    如果想获取包含多种标签的数据,需要通过对多个维度(姓名、科目等标签)的多次查询才能实现,这时使用多级行列索引进行查询会非常方便。

    2.6.3. 多级索引排序和切片

    2.6.3.1. Series多级索引排序

    和单级索引一样,多级索引顺序是按照声明顺序确定的,也即是无序的,如果按照索引字母顺序排序,将方便切片操作:

    15
    students = ['Tom', 'John']
    subjects = ['Maths', 'English', 'Chemistry']
    index = pd.MultiIndex.from_product([students, subjects], names=['student', 'subject'])
    ps = pd.Series(np.arange(6) + 90, index=index)
    print(ps)
    student  subject
    Tom      Maths        90
             English      91
             Chemistry    92
    John     Maths        93
             English      94
             Chemistry    95
    dtype: int32
    

    可以看到,默认的索引顺序和声明中索引顺序相同,但是使用切片 [start:end:step] 操作时,start 要小于 end,否则返回空对象,如果是乱序的,我们每次切片时都要记住声明的标签顺序,且声明顺序一旦更改,切片相关的代码就要更新,如果对索引进行排序,就不会再出现这类问题:

    12
    # 默认使用 level=0 排序
    ps = ps.sort_index()
    print(ps)
    student  subject
    John     Chemistry    95
             English      94
             Maths        93
    Tom      Chemistry    92
             English      91
             Maths        90
    dtype: int32
    

    经过排序后,可以发现第一级索引和第二季索引都被更新了。 可以使用 level 参数指定优先进行排序的索引层:

    11
    # 使用 subject 索引排序
    print(ps.sort_index(level=1))
    student  subject
    John     Chemistry    95
    Tom      Chemistry    92
    John     English      94
    Tom      English      91
    John     Maths        93
    Tom      Maths        90
    dtype: int32
    

    2.6.3.5. DataFrame 多级索引排序

    与 Series 对象类似,DataFrame 同样支持多级索引的排序,唯一不同点在于它有行索引和列索引,可以接受 axis 参数:

  • axis = 0,对行索引进行排序,Series 只有行索引,所以 axis 永远为 0.
  • axis = 1,对列索引进行排序。
  • 18
    students = ['Tom', 'John']
    subjects = ['Maths', 'English', 'Chemistry']
    columns = pd.MultiIndex.from_product([['2013', '2012'], ['first_half', 'latter_half']],
                                          names=['year', 'half'])
    # 使用固定的模拟成绩数据,以观察排序影响
    pf = pd.DataFrame(75 + np.arange(24).reshape(6, 4), index=index, columns=columns)
    print(pf)
    year                     2013                   2012
    half              latter_half first_half latter_half first_half
    student subject
    Tom     Maths              75         76          77         78
            English            79         80          81         82
            Chemistry          83         84          85         86
    John    Maths              87         88          89         90
            English            91         92          93         94
            Chemistry          95         96          97         98
    

    我们使用上面的示例数据,为了查看排序效果,我们把所有索引标签的顺序都颠倒了。

    12
    # 行标签排序
    print(pf.sort_index(axis=0))
    year                     2013                   2012
    half              latter_half first_half latter_half first_half
    student subject
    John    Chemistry          95         96          97         98
            English            91         92          93         94
            Maths              87         88          89         90
    Tom     Chemistry          83         84          85         86
            English            79         80          81         82
            Maths              75         76          77         78
    

    行标签排序后,对列标签顺序无影响,同样列标签排序对行标签顺序无影响:

    27
    # 列标签排序
    print(pf.sort_index(axis=1))
    year                    2012                   2013
    half              first_half latter_half first_half latter_half
    student subject
    Tom     Maths             78          77         76          75
            English           82          81         80          79
            Chemistry         86          85         84          83
    John    Maths             90          89         88          87
            English           94          93         92          91
            Chemistry         98          97         96          95
    # 同时对行和列排序
    sorted_pf = pf.sort_index(axis=0).sort_index(axis=1)
    print(sorted_pf)
    year                    2012                   2013
    half              first_half latter_half first_half latter_half
    student subject
    John    Chemistry         98          97         96          95
            English           94          93         92          91
            Maths             90          89         88          87
    Tom     Chemistry         86          85         84          83
            English           82          81         80          79
            Maths             78          77         76          75
    

    无论是行排序还是列排序,均对行或列的所有层级标签依次进行了排序,我们当然可以使用 level 指定优先排序的索引层:

    26
    # 优先使用 subject 排序
    print(pf.sort_index(axis=0, level=1))
    year                     2013                   2012
    half              latter_half first_half latter_half first_half
    student subject
    John    Chemistry          95         96          97         98
    Tom     Chemistry          83         84          85         86
    John    English            91         92          93         94
    Tom     English            79         80          81         82
    John    Maths              87         88          89         90
    Tom     Maths              75         76          77         78
    # # 优先使用半学期 half 排序
    print(pf.sort_index(axis=1, level=1))
    year                    2012       2013        2012        2013
    half              first_half first_half latter_half latter_half
    student subject
    Tom     Maths             78         76          77          75
            English           82         80          81          79
            Chemistry         86         84          85          83
    John    Maths             90         88          89          87
            English           94         92          93          91
            Chemistry         98         96          97          95
    half              latter_half first_half latter_half first_half
    student subject
    John    Chemistry          95         96          97         98
            English            91         92          93         94
            Maths              87         88          89         90
    Tom     Chemistry          83         84          85         86
            English            79         80          81         82
            Maths              75         76          77         78
    # 访问 Tom 的各科成绩
    print(pf.loc['Tom', :])
    year             2013                   2012
    half      latter_half first_half latter_half first_half
    subject
    Chemistry          83         84          85         86
    English            79         80          81         82
    Maths              75         76          77         78
    # 查看 Tom 的 2012 年各科成绩
    print(pf.loc['Tom', :]['2012'])
      half       latter_half  first_half
    subject
    Chemistry           85          86
    English             81          82
    Maths               77          78
    # 查看 Tom 的 2012 年下半年各科成绩
    # 等价于 print(pf.loc['Tom', '2012']['latter_half'])
    print(pf.loc['Tom', :]['2012']['latter_half'])
      subject
    Chemistry    85
    English      81
    Maths        77
    Name: latter_half, dtype: int32
    

    注意体会 DataFrame 的数组访问方式,第一维索引的形式有几种:

  • [‘Tom’, ‘2012’]:指定行索引和列索引
  • [‘Tom’, ‘Maths’]:均指定行索引
  • [‘Tom’, :]:均指定行索引
  • 显然第一维索引无法指定的列索引的第二层索引,就要通过增加第二维索引来访问,例如 [‘Tom’, ‘2012’][‘latter_half’]。

    2.6.4. 索引调整和重置

    我们可以通过对象的 index 和 columns 属性更新行或列标签。也可以调整索引的顺序。

    2.6.4.1. reindex

    reindex() 是 pandas 对象的一个重要方法,其作用是在当前对象基础上创建一个新索引的新对象。它通常和 set_index() 配合使用:

    28
    students = ['Tom', 'John']
    subjects = ['Maths', 'English', 'Chemistry']
    index = pd.MultiIndex.from_product([students, subjects], names=['student', 'subject'])
    ps = pd.Series(np.arange(6) + 90, index=index)
    # 重制索引
    flat_ps = ps.reset_index()
    print(flat_ps)
      student    subject   0
    0     Tom      Maths  90
    1     Tom    English  91
    2     Tom  Chemistry  92
    3    John      Maths  93
    4    John    English  94
    5    John  Chemistry  95
    # 逆向转换
    print(flat_ps.set_index(['student','subject']))
    student subject
    Tom     Maths      90
            English    91
            Chemistry  92
    John    Maths      93
            English    94
            Chemistry  95
    

    可以传入 drop = True 丢弃所有索引,此时变为 Series 对象,只保留数据:

    10
    flat_ps = ps.reset_index(drop = True)
    print(flat_ps)
    0    90
    1    91
    2    92
    3    93
    4    94
    5    95
    dtype: int32
    

    2.6.5. 多级索引数据统计

    前面已经介绍过一些 Pandas 自带的数据累计方法,比如 mean()、sum() 和 max()。而对于层级索引数据,可以设置参数 level 实现对数据子集的累计操作。

    首先我们准备如下用于统计的带有多级索引的数据:

    15
    # Series 类型的数据
    student  subject         # 行标签名
    Tom      Maths        90
             English      91
             Chemistry    92
    John     Maths        93
             English      94
             Chemistry    95
    dtype: int32
    print(ps.index)
    MultiIndex(levels=[['John', 'Tom'], ['Chemistry', 'English', 'Maths']],
               labels=[[1, 1, 1, 0, 0, 0], [2, 1, 0, 2, 1, 0]],
               names=['student', 'subject'])
    

    可以使用 level 指定要统计的行标签名或者整数索引,来进行统计:

    6
    print(ps.mean(level='student')) # 等价于 ps.mean(level=0)
    student
    John    94  # (93+94+95) / 3 = 94
    Tom     91  # (90+91+92) / 3 = 91
    dtype: int32
    

    结合 axis 参数, 就可以对 DataFrame 列索引进行类似的统计操作:

    40
    # 准备以下 DataFrame 数据
    year                     2013                   2012
    half              latter_half first_half latter_half first_half
    student subject
    John    Chemistry          95         96          97         98
            English            91         92          93         94
            Maths              87         88          89         90
    Tom     Chemistry          83         84          85         86
            English            79         80          81         82
            Maths              75         76          77         78
    # 查看多级索引信息
    print(pf.index)
    MultiIndex(levels=[['John', 'Tom'], ['Chemistry', 'English', 'Maths']],
               labels=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]],
               names=['student', 'subject'])
    # 获取每个学生的平均成绩,axis=0 指定对行统计
    print(pf.mean(level='student', axis=0))
    year           2013                   2012
    half    latter_half first_half latter_half first_half
    student
    John             91         92          93         94
    Tom              79         80          81         82
    # 获取年平均成绩,axis=1 指定对列统计
    print(pf.mean(level='year', axis=1))
    year               2012  2013
    student subject
    John    Chemistry  97.5  95.5
            English    93.5  91.5
            Maths      89.5  87.5
    Tom     Chemistry  85.5  83.5
            English    81.5  79.5
            Maths      77.5  75.5