import numpy as np
import pandas as pd
pd.__version__
'1.3.5'

1、Pandas 对象介绍

  • 先看看Pandas的三个基本数据结构:SeriesDataFrameIndex

1.1、Pandas 的 Series 对象

  • Pandas的Series对象是一个带索引的数据构成的一维数组。可以用一个数组创建Series对象
  • Series 对象将一组数据和一组索引绑定在一起,我们可以通过 values 属性和 index 属性获取数据
    • values 属性返回的结果与 Numpy 数组类似
    • index 属性返回的结果是一个类型为 pd.Index 的类数组对象,我们将在后面的内容里详细介绍它
data = pd.Series([0.25, 0.5, 0.75, 1.0])
data
0    0.25
1    0.50
2    0.75
3    1.00
dtype: float64
data.values
array([0.25, 0.5 , 0.75, 1.  ])
data.index
RangeIndex(start=0, stop=4, step=1)
data[0]
0.25
data[1:3]
1    0.50
2    0.75
dtype: float64

1.1.1、Serise是通用的NumPy数组

  • Numpy 数组通过 隐式定义 的整数索引获取数值
  • Serise 对象是一种 显式定义 的索引与数值关联
    • 显式索引的定义让Serise对象拥有了更强的能力。例如,索引不再仅仅是整数,还可以是任意想要的类型
    • 如果需要,完全可以用字符串定义索引
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
data = pd.Series([0.25, 0.5, 0.75, 1.0], index=[2, 5, 3, 7])
data
2    0.25
5    0.50
3    0.75
7    1.00
dtype: float64
data[5]
0.5

1.1.2、Serise是特殊的字典

  • 可以把 Serise 对象看成一种特殊的 Python 字典。
  • 字典是一种将任意键映射到一族任意值的数据结构,而 Serise 对象其实就是将类型键映射到一组类型值的数据结构
  • 我们可以直接用Python的 字典 创建一个 Serise 对象,让 Serise 对象与字典的类比更加清晰
    • Serise 对象的排序和输入的字典排序相同
population_dict = {'California': 38332521,
                   'Texas': 26448193,
                   'New York': 19651127,
                   'Florida': 19552860,
                   'Illinois': 12882135}
population = pd.Series(population_dict)
population
California    38332521
Texas         26448193
New York      19651127
Florida       19552860
Illinois      12882135
dtype: int64
population['California']
38332521
population['Texas':'Florida']
Texas       26448193
New York    19651127
Florida     19552860
dtype: int64

1.1.3、创建Serise对象

  • 前面已经见了几种创建 Pandas的Serise对象 的方法,都是像这样的形式
    • >>> pd.Serise(data, index=index)
    • 其中,index 是一个可选参数,data 参数支持多种数据类型
    • data 可以是 列表NumPy数组标量字典
  • 这里需要注意的是,Serise对象只会保留显示定义的键值对。
pd.Series([2, 4, 6])
0    2
1    4
2    6
dtype: int64
pd.Series(5, index=[100, 200, 300])
100    5
200    5
300    5
dtype: int64
pd.Series({2:'a', 1:'b', 3:'c'}, index=[3,2])
3    c
2    a
dtype: object

1.2、Pandas 的 DataFrame 对象

1.2.1、DataFrame是通用的NumPy数组

  • 如果将 Serise 类比为带灵活索引一维数组,那么 DataFrame 就可以看做是一种既有灵活的行索引,又有灵活列名二维数组
  • 也可以把 DataFrame 看成是有序排列的若干 Serise 对象。这里的 “排列” 指的是他们拥有共同的索引
area_dict = {'California': 423967, 
             'Texas': 695662, 
             'New York': 141297,
             'Florida': 170312, 
             'Illinois': 149995}
area = pd.Series(area_dict)
area
California    423967
Texas         695662
New York      141297
Florida       170312
Illinois      149995
dtype: int64

使用两个 Serise 对象,用一个字典创建一个包含这些信息的二维对象

states = pd.DataFrame({'populatin': population,
                       'area': area})
states
populatin area
California 38332521 423967
Texas 26448193 695662
New York 19651127 141297
Florida 19552860 170312
Illinois 12882135 149995
states.index
Index(['California', 'Texas', 'New York', 'Florida', 'Illinois'], dtype='object')
states.columns
Index(['populatin', 'area'], dtype='object')

1.2.2、DataFrame是特殊的字典

这里使用索引返回的是 一列数据(即一列 Serise 对象),不是 一行数据

states['area']
California    423967
Texas         695662
New York      141297
Florida       170312
Illinois      149995
Name: area, dtype: int64
states['California']
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

~/opt/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2645             try:
-> 2646                 return self._engine.get_loc(key)
   2647             except KeyError:


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()


pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()


pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()


KeyError: 'California'

During handling of the above exception, another exception occurred:


KeyError                                  Traceback (most recent call last)

<ipython-input-28-e9f6ea277cfb> in <module>
----> 1 states['California']


~/opt/anaconda3/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2798             if self.columns.nlevels > 1:
   2799                 return self._getitem_multilevel(key)
-> 2800             indexer = self.columns.get_loc(key)
   2801             if is_integer(indexer):
   2802                 indexer = [indexer]


~/opt/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2646                 return self._engine.get_loc(key)
   2647             except KeyError:
-> 2648                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2649         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2650         if indexer.ndim > 1 or indexer.size > 1:


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()


pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()


pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()


pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()


KeyError: 'California'

1.2.3、创建DataFrame对象

  1. 通过单个 Series 对象创建,DataFrame 是一组 Series 对象的集合,keyi单个 Series 创建一个单列的 DataFrame;
  2. 通过字典列表创建。任何元素是字典的列表都可以变成 DataFrame;
  3. 通过 Series 对象字典创建;
  4. 通过 NumPy 二维数组创建。假如有一个二维数组,就可以创建一个可以指定行列索引值的 DataFrame。如果不指定索引,那么就是默认索引;
  5. 通过 NumPy 结构化数组创建。由于 Pandas 的 DataFrame 与 结构化数组 十分相似,因此可以。
pd.DataFrame(population, columns=['population'])
population
California 38332521
Texas 26448193
New York 19651127
Florida 19552860
Illinois 12882135
data = [{'a': i, 'b': 2 * i} for i in range(3)]
pd.DataFrame(data)
a b
0 0 0
1 1 2
2 2 4
pd.DataFrame([{'a': 1, 'b': 2}, {'b': 3, 'c': 4}])
a b c
0 1.0 2 NaN
1 NaN 3 4.0
pd.DataFrame({'population': population, 
              'area': area})
population area
California 38332521 423967
Texas 26448193 695662
New York 19651127 141297
Florida 19552860 170312
Illinois 12882135 149995
pd.DataFrame(np.random.rand(3, 2), 
             columns=['foo', 'bar'], 
             index=['a', 'b', 'c'])
foo bar
a 0.665127 0.048725
b 0.579417 0.124578
c 0.029122 0.583950
A = np.zeros(3, dtype=[('A', 'i8'), ('B', 'f8')])
A
array([(0, 0.), (0, 0.), (0, 0.)], dtype=[('A', '<i8'), ('B', '<f8')])
pd.DataFrame(A)
A B
0 0 0.0
1 0 0.0
2 0 0.0

1.3、Pandas 的 Index 对象

我们已经发现,Series 和 DataFrame 对象都使用便于引用和调整的 显示索引 。Pandas 的 Index 对象是一个很有意思的数据结构,可以将它看做一个 不可变数组有序集合 (实际上是一个多集,因为 Index 对象可能会包含重复值)。这两种观点使得 Index 对象能呈现一些有趣的功能。

ind = pd.Index([2, 3, 5, 7, 11])
ind
Int64Index([2, 3, 5, 7, 11], dtype='int64')

1.3.1、将 Index 看做不可变数组

  • Index 对象的许多操作都像数组。例如,可以通过标准 Python 的取值方法获取数值,也可以通过切片获取数值
  • Index 对象还有许多与 NumPy 数组相似的属性
  • Index 对象与 NumPy 数组不同在于,Index 对象的索引是不可变的,也就是说不能通过通常的方式进行调整
    • Index 对象的不可变特征使得多个 DataFrame 和 数组 直接进行索引共享时更安全,尤其是可以避免因修改索引时粗心大意而导致的副作用
ind[1]
3
ind[::2]
Int64Index([2, 5, 11], dtype='int64')
print(ind.size, ind.shape, ind.ndim, ind.dtype)
5 (5,) 1 int64
ind[1] = 0
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-15-906a9fa1424c> in <module>
----> 1 ind[1] = 0


~/opt/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in __setitem__(self, key, value)
   3908 
   3909     def __setitem__(self, key, value):
-> 3910         raise TypeError("Index does not support mutable operations")
   3911 
   3912     def __getitem__(self, key):


TypeError: Index does not support mutable operations

1.3.2、将 Index 看做有序集合

  • Pandas 对象被设计用于实现许多操作,如连接(join)数据集,其中会涉及许多集合操作。
  • Index 对象遵循 Python 标准库集合(set)数据结构的许多习惯用法,包括并集、交集、差集等
indA = pd.Index([1, 3, 5, 7, 9])
indB = pd.Index([2, 3, 5, 7, 11])
indA & indB # 交集
Int64Index([3, 5, 7], dtype='int64')
indA | indB # 并集
Int64Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')
indA ^ indB # 异或
Int64Index([1, 2, 9, 11], dtype='int64')

2、数据取值与选择

  • 下面介绍 Pandas 的 Series 和 DataFrame 对象相似的数据获取和调整操作。
  • 和 NumPy 的操作模式类似

2.1、Series 数据选择方法

2.1.1、将 Series 看做字典

和字典一样,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
'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)]
data['e'] = 1.25
data
a    0.25
b    0.50
c    0.75
d    1.00
e    1.25
dtype: float64

2.1.2、将 Series 看做一维数组

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

data['a':'c'] # 显式索引切片,包含最后一个索引 ['a', 'c']
a    0.25
b    0.50
c    0.75
dtype: float64
data[0:2] # 隐式索引切片,不包含最后一个索引 [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

2.1.3、索引器:loc、iloc 和 ix

这些切片和取值的习惯用法经常会造成混乱

  • 如果Series 是显式整数索引,那么 data[1] 这样的取值操作会使用显式索引
  • 而 data[1:3] 这样的切片操作却会使用隐式索引

Python的设计原则之一是 “显式优于隐式”,优先使用 loc、iloc。

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 提供了一些 索引器(indexer) 属性来作为取值的方法。它们不是 Series 对象的函数方法,而是暴露切片接口的属性。

2.1.3.1、 loc 属性
  • 表示取值和切片都是 显式 的:
data.loc[1]
'a'
data.loc[1:3]
1    a
3    b
dtype: object
2.1.3.2、 iloc 属性
  • 表示去取和索引都是 Python 形式的 隐式 索引
data.iloc[1]
'b'
data.iloc[1:3]
3    b
5    c
dtype: object
2.1.3.3、ix 属性
  • 它是前两种索引器的混合形式,在 Series 对象中 ix 等价于标准的 [ ](Python列表)取值方式。
  • ix索引器主要用于 DataFrame 对象,后面会介绍

2.2、DataFrame 数据选择方法

2.2.1、将 DataFrame 看做字典

  • 第一种类比是把 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
Texas 695662 26448193
New York 141297 19651127
Florida 170312 19552860
Illinois 149995 12882135
data['area']
California    423967
Texas         695662
New York      141297
Florida       170312
Illinois      149995
Name: area, dtype: int64
data.area
California    423967
Texas         695662
New York      141297
Florida       170312
Illinois      149995
Name: area, dtype: int64
data.area is data['area']
True

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

data.pop is data['pop']
False
data['density'] = data['pop'] / data['area']
data
area pop density
California 423967 38332521 90.413926
Texas 695662 26448193 38.018740
New York 141297 19651127 139.076746
Florida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763

2.2.2、将 DataFrame 看做二维数组

data.values
array([[4.23967000e+05, 3.83325210e+07, 9.04139261e+01],
       [6.95662000e+05, 2.64481930e+07, 3.80187404e+01],
       [1.41297000e+05, 1.96511270e+07, 1.39076746e+02],
       [1.70312000e+05, 1.95528600e+07, 1.14806121e+02],
       [1.49995000e+05, 1.28821350e+07, 8.58837628e+01]])
data.T
California Texas New York Florida Illinois
area 4.239670e+05 6.956620e+05 1.412970e+05 1.703120e+05 1.499950e+05
pop 3.833252e+07 2.644819e+07 1.965113e+07 1.955286e+07 1.288214e+07
density 9.041393e+01 3.801874e+01 1.390767e+02 1.148061e+02 8.588376e+01
data.values[0]
array([4.23967000e+05, 3.83325210e+07, 9.04139261e+01])
data['area']
California    423967
Texas         695662
New York      141297
Florida       170312
Illinois      149995
Name: area, dtype: int64

因此,在进行数组形式的取值时,我就可以使用 locilocix(后期被删除了)

data.iloc[:3, :2]
area pop
California 423967 38332521
Texas 695662 26448193
New York 141297 19651127
data.loc[:'New York', :'pop']
area pop
California 423967 38332521
Texas 695662 26448193
New York 141297 19651127
# 在pandas的1.0.0版本开始,移除了Series.ix and DataFrame.ix 方法。
data.ix[:3, :'pop']
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-37-9dfb8ab8cf3a> in <module>
      1 # ix 可以混用
----> 2 data.ix[:3, :'pop']


~/opt/anaconda3/lib/python3.7/site-packages/pandas/core/generic.py in __getattr__(self, name)
   5272             if self._info_axis._can_hold_identifiers_and_holds_name(name):
   5273                 return self[name]
-> 5274             return object.__getattribute__(self, name)
   5275 
   5276     def __setattr__(self, name: str, value) -> None:


AttributeError: 'DataFrame' object has no attribute 'ix'
data.loc[data.density > 100, ['pop', 'density']]
pop density
New York 19651127 139.076746
Florida 19552860 114.806121
data.iloc[0, 2] = 90
data
area pop density
California 423967 38332521 90.000000
Texas 695662 26448193 38.018740
New York 141297 19651127 139.076746
Florida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763

2.2.3、其他取值方法

data['Florida':'Illinois']
area pop density
Florida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763
data[1:3]
area pop density
Texas 695662 26448193 38.018740
New York 141297 19651127 139.076746
data[data.density > 100]
area pop density
New York 141297 19651127 139.076746
Florida 170312 19552860 114.806121

3、Pandas 数组运算方法

3.1、通用函数:保留索引

import numpy as np
import pandas as pd
rng = np.random.RandomState(42)
ser = pd.Series(rng.randint(0, 10, 4))
ser
0    6
1    3
2    7
3    4
dtype: int64
df = pd.DataFrame(rng.randint(0, 10, (3, 4)), columns=list('ABCD'))
df
A B C D
0 6 9 2 6
1 7 4 3 7
2 7 2 5 4

如果对这两个对象的一种一个使用 NumPy 通用函数,生成的结果是另一个 保留索引 的 Pandas 对象

np.exp(ser)
0     403.428793
1      20.085537
2    1096.633158
3      54.598150
dtype: float64
np.sin(df * np.pi / 4)
A B C D
0 -1.000000 7.071068e-01 1.000000 -1.000000e+00
1 -0.707107 1.224647e-16 0.707107 -7.071068e-01
2 -0.707107 1.000000e+00 -0.707107 1.224647e-16

3.2、通用函数:索引对齐

当在两个 Serise 或 DataFrame 对象上进行二元计算时,Pandas 会在计算过程中对齐两个对象的索引。当你处理不完整的数据时,这一点非常方便,我们将在后面的示例中看到

3.2.1、Serise 索引对齐

area       = pd.Series({'Alaska': 1723337, 
                        'Texas': 695662,
                        'California': 423967}, name='area')
population = pd.Series({'California': 38332521, 
                        'Texas': 26448193,
                        'New York': 19651127}, name='population')
population / area
Alaska              NaN
California    90.413926
New York            NaN
Texas         38.018740
dtype: float64

结果数组的索引是两个输入数组索引的并集

area.index | population.index
/Users/weiyi/opt/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:1: FutureWarning: Index.__or__ operating as a set operation is deprecated, in the future this will be a logical operation matching Series.__or__.  Use index.union(other) instead
  """Entry point for launching an IPython kernel.





Index(['Alaska', 'California', 'New York', 'Texas'], dtype='object')

对于缺失位置的数据,Pandas 活用 NaN 填充,表示 “此处无数”

A = pd.Series([2, 4, 6], index=[0, 1, 2])
B = pd.Series([1, 3, 5], index=[1, 2, 3])
A + B
0    NaN
1    5.0
2    9.0
3    NaN
dtype: float64

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

A.add(B, fill_value=0)
0    2.0
1    5.0
2    9.0
3    5.0
dtype: float64

3.2.2、DataFrame 索引对齐

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

A = pd.DataFrame(rng.randint(0, 20, (2, 2)),
                 columns=list("AB"))
A
A B
0 1 11
1 5 1
B = pd.DataFrame(rng.randint(0, 10, (3, 3)),
                 columns=list("BAC"))
B
B A C
0 4 0 9
1 5 8 0
2 9 2 6
A + B
A B C
0 1.0 15.0 NaN
1 13.0 6.0 NaN
2 NaN NaN NaN

你会发现,两个对象的行列索引可以是不同顺序的,结果的索引会自动排序。在Series中,我们可以通过运算符方法的fill_value参数自定义缺失值。这里,我们将用 A 中的所有值的均值来填充缺失值(计算 A 的均值需要用 stack 将二维码组压缩成一维数组):

fill = A.stack().mean()
A.add(B, fill_value=fill)
A B C
0 1.0 15.0 13.5
1 13.0 6.0 4.5
2 6.5 13.5 10.5

下面列举了 Python 运算符与 Pandas 对象方法的映射关系。

Python Operator Pandas Method(s)
+ add()
- sub(), subtract()
* mul(), multiply()
/ truediv(), div(), divide()
// floordiv()
% mod()
** pow()

3.3、通用函数:DataFrame 与 Series 的运算

我们经常需要对一个 DataFrame 和一个 Series 进行运算,行列对齐方式与之前类似。也就是说,DataFrame 和 Series 的运算规则,与 NumPy 中二维数组与一维数组的运算规则是一样的。来看一个常见的运算,让一个二维数组减去自身的一行数据:

A = rng.randint(0, 10, (3, 4))
A
array([[3, 8, 2, 4],
       [2, 6, 4, 8],
       [6, 1, 3, 8]])
A - A[0]
array([[ 0,  0,  0,  0],
       [-1, -2,  2,  4],
       [ 3, -7,  1,  4]])

根据 NumPy 的广播规则(见第二章),让二维数组减去自身的一行数据会按行计算。

在 Pandas 里默认也是按行运算的:

df = pd.DataFrame(A, columns=list("QRST"))
df - df.iloc[0]
Q R S T
0 0 0 0 0
1 -1 -2 2 4
2 3 -7 1 4

如果你想按列计算,那么就需要利用前面介绍过的运算符方法,通过axis参数设置:

df.subtract(df['R'], axis=0)
Q R S T
0 -5 0 -6 -4
1 -4 0 -2 2
2 5 0 2 7

你会发现 DataFrame/Series 的运算与前面介绍的运算一样,结果的索引都会自动对齐:

halfrow = df.iloc[0, ::2]
halfrow
Q    3
S    2
Name: 0, dtype: int64
df - halfrow
Q R S T
0 0.0 NaN 0.0 NaN
1 -1.0 NaN 2.0 NaN
2 3.0 NaN 1.0 NaN

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

4、处理缺失值

大多数教程里使用的数据与现实工作中的数据的区别在于后者很少是干净整齐的,许多目前流行的数据集都会有数据缺失的现象。更有甚者,处理不同数据源缺失值的方法还不同。

我们将在本节介绍一些处理缺失值的通用规则,Pandas 对缺失值的表现形式,并演示 Pandas 自带的几个处理缺失值的工具用法。本节以及全书涉及的缺失值主要有三种形式:nullNaNNA

4.1、选择处理缺失值的方法

在数据表或DataFrame中有很多识别缺失值的方法。一般情况下可以分为两种:

  • 一种方法是通过一个覆盖全局的掩码表示缺失值,
  • 另一种方法是用一个标签值(sentinel value)表示缺失值;

在掩码方法中,掩码可能是一个与原数组维度相同的完整布尔类型的数组,也可能是用一个比特(0或1)表示缺失值的局部状态。

在标签方法中,标签值可能是具体的数据(例如用-9999表示缺失的整数),也可能是些极少出现的形式。另外,标签值还可能是更全局的值,比如用NaN(不是一个数)表示缺失的浮点数,它是IEEE浮点数规范中指定的特殊字符。

使用这两种方法之前都需要先综合考量:

  • 使用单独的掩码数组会额外出现一个布尔类型数组,从而增加储存于计算负担;
  • 而标签值方法缩小了可以表示为有效值的范围,可能需要在CPU或GPU计算逻辑单元中增加额外的(往往不是最优的)计算逻辑。通常使用的NaN也不能表示所有数据类型。

大多数情况下,都不存在最佳选择,不同的编程语言与系统使用不同的方法。例如,R语言在每种数据类型中保留一个比特作为缺失数据的标签值,而 SciDB 系统会在每个单元后面加一个额外的字节表示 NA 状态。

4.2、Panda 的缺失值

Pandas 里处理缺失值的方式延续了 NumPy 程序包的方式,并没有为浮点数据类型提供内置的 NA 作为缺失值。

Pandas 原本也可以按照 R语言 采用的比特模式为每一种数据类型标注缺失值,但是这种方法非常笨拙。R语言包含4种基本数据类型,而 NumPy 支持的类型远超4种。另外,对于一些较小的数据类型(比如8位整型数据),牺牲一个比特作为缺失值标注的掩码还会导致其数据范围缩小。

当然,NumPy也支持掩码数据的,也就是说可以用一个布尔掩码数组为原数组标注“无缺失值”或“有缺失值”。Pandas 也集成了这个功能,但是在储存、计算和编码维护方面都需要耗费不必要的资源,因此这种方式并不可取。

综合考虑各种方法事务优缺点,Pandas 最终选择用标签方法表示缺失值,包括两种 Python 原有的缺失值:

  • 浮点数据类型的NaN 值
  • Python 的None 对象

后面我们将会发现,虽然这么做也会有一些副作用,但是在实际运用中的效果还是不错的。

4.2.1、None:Python 对象类型的缺失值

Pandas 可以使用的第一个缺失值标签是None,它是一个 Python 单体对象,经常在代码中表示缺失值。由于 None 是一个 Python 对象,所以不能作为任何 NumPy/Pandas 数组类型的缺失值,只能用于‘object’ 数组类型(即由 Python 对象构成的数组):

import numpy as np
import pandas as pd

vals1 = np.array([1, None, 3, 4])
vals1
array([1, None, 3, 4], dtype=object)

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

for dtype in ['object', 'int']:
    print("dtype =", dtype)
    %timeit np.arange(1E6, dtype=dtype).sum()
    print()
dtype = object
46.8 ms ± 603 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

dtype = int
1.59 ms ± 79.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

使用 Python 对象构成的数组就意味着如果你对一个包含 None 的数组进行累计操作,如 sum()或者min(),那么通常会出现类型错误:

vals1.sum()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

/var/folders/rx/27fc03s525nbs15jphd750hm0000gn/T/ipykernel_95399/1181914653.py in <module>
----> 1 vals1.sum()


~/opt/anaconda3/lib/python3.7/site-packages/numpy/core/_methods.py in _sum(a, axis, dtype, out, keepdims, initial, where)
     45 def _sum(a, axis=None, dtype=None, out=None, keepdims=False,
     46          initial=_NoValue, where=True):
---> 47     return umr_sum(a, axis, dtype, out, keepdims, initial, where)
     48 
     49 def _prod(a, axis=None, dtype=None, out=None, keepdims=False,


TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

也就是说,在 Python 中没有定义整数None之间的加法运算。

4.2.2、NaN:数值类型的缺失值

另一种缺失值的标签是NaN(全称Not a Number,不是一个数),是一种按照 IEEE 浮点数标准设计、在任何系统中都兼容的特殊浮点数

vals2 = np.array([1, np.nan, 3, 4])
print(vals2, vals2.dtype)
[ 1. nan  3.  4.] float64

请注意,NumPy 会为这个数组选择一个原生浮点类型,这意味着和之前的object类型数组不同,这个数组会被编译成C代码从而实现快速操作。你可以把 NaN 看做是一个数据类病毒它会将与它接触过的数据同化。无论和 NaN 进行何种操作,最终结果都是 NaN:

1 + np.nan
nan
0 * np.nan
nan

虽然这些累计操作的结果定义是合理的(既不会抛异常),但是并非总是有效的:

vals2.sum(), vals2.min(), vals2.max()
(nan, nan, nan)

NumPy 也提供了一些特殊的累计函数,它们可以忽略缺失值的影响:

np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
(8.0, 1.0, 4.0)

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

4.2.3、Pandas 中 NaN 与 None 的差异

虽然 NaN 和 None 各有各的用处,但是 Pandas 把它们看成可以等价交换的,在适当的时候会将两者进行替换:

pd.Series([1, np.nan, 2, None])
0    1.0
1    NaN
2    2.0
3    NaN
dtype: float64

Pandas 会将没有标签值的数据类型自动转换为NA。例如,当我们将整型数组中的一个值设置为np.nan时,这个值就会强制转换为浮点数缺失值NA

x = pd.Series(range(2), dtype=int)
x
0    0
1    1
dtype: int64
x[0] = None
x
0    NaN
1    1.0
dtype: float64

上面除了将整个整型数组的缺失值强制转换为浮点数,Pandas 还会自动将 None 转换为 NaN。

尽管这些仿佛会魔法的类型比 R语言等 专用统计语言的缺失值要复杂一些,但是 Pandas 的标签/转换方法在实践中的效果非常好。

Pandas 对不同类型缺失值的转换规则

类型 缺失值值转换规则 NA标签值
floating 无变化 np.nan
object 无变化 None or np.nan
integer 强制转换为 float64 np.nan
boolean 强制转换为 object None or np.nan

需要注意的是,Pandas 中字符串类型的数据通常是用object类型储存。

4.3、处理缺失值

我们已经知道,Pandas 基本上把 None 和 NaN 看成是可以等价交换的缺失值形式。为了完成这种交换过程,Pandas 提供了一些方法来发现、剔除、替换数据结构中的缺失值,主要包括以下几种:

  • isnull():创建一个布尔类型的掩码标签缺失值。
  • notnull():与isnull()操作相反。
  • dropna():返回一个剔除缺失值的数据。
  • fillna():返回一个填充了缺失值的数据

4.3.1、发现缺失值

Pandas 数据结构有两种有效的方法可以发现缺失值:isnull() 和 notnull()。每种方法都返回布尔类型的掩码数据,例如:

data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
0    False
1     True
2    False
3     True
dtype: bool

布尔类型掩码数组可以直接作为 Series 或 DataFrame 的索引使用:

data[data.notnull()]
0        1
2    hello
dtype: object

在 Series 中使用 isnull() 和 notnull() 同样适用于 DataFrame,产生的结果同样是布尔类型。

4.3.2、剔除缺失值

除了前面介绍的掩码方法,还有两种很好用的缺失值处理方法,分别是 dropna()(剔除缺失值)和 fillna()(填充缺失值)。在 Series 上使用这些方法非常简单:

data.dropna()
0        1
2    hello
dtype: object

而在 DataFrame 上使用它们时需要注意设置一些参数,例如下面的 DataFrame:

df = pd.DataFrame([[1,      np.nan, 2], 
                   [2,      3,      5], 
                   [np.nan, 4,      6]])
df
0 1 2
0 1.0 NaN 2
1 2.0 3.0 5
2 NaN 4.0 6

我们没法从 DataFrame 中单独剔除一个值,要么是剔除缺失值所在的整行,要么是整列。根据实际需求,有时你需要剔除整行,有事可能是整列,DataFrame 中的 dropna() 会有一些参数可以配置。

  • 默认请求下,dropna() 会剔除任何包含缺失值的整行数据
df.dropna()
0 1 2
1 2.0 3.0 5

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

df.dropna(axis=1)
2
0 2
1 5
2 6

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

默认设置是how='any',也就是说只要有缺失值就剔除整行或整列。你还可以设置how='all',这样就只会剔除全部是缺失值的行或列了:

df[3] = np.nan
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.dropna(axis=1, how='all')
0 1 2
0 1.0 NaN 2
1 2.0 3.0 5
2 NaN 4.0 6

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

下面示例中,第一行和第三行被剔除掉了,因为它们只包含两个非缺失值。

df.dropna(thresh=3)
0 1 2 3
1 2.0 3.0 5 NaN

4.3.3、填充缺失值

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

来用下面的 Series 演示:

data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))
data
a    1.0
b    NaN
c    2.0
d    NaN
e    3.0
dtype: float64

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

data.fillna(0)
a    1.0
b    0.0
c    2.0
d    0.0
e    3.0
dtype: float64

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

# `从前往后`填充
data.fillna(method='ffill')
a    1.0
b    1.0
c    2.0
d    2.0
e    3.0
dtype: float64
# `从后往前`填充
data.fillna(method='bfill')
a    1.0
b    2.0
c    2.0
d    3.0
e    3.0
dtype: float64

DataFrame 的操作也类似:

df = pd.DataFrame([[np.nan, 2,      np.nan, 0],
                   [3,      4,      np.nan, 1],
                   [np.nan, np.nan, np.nan, 5],
                   [np.nan, 3,      np.nan, 4]],
                  columns=list("ABCD"))
df
A B C D
0 NaN 2.0 NaN 0
1 3.0 4.0 NaN 1
2 NaN NaN NaN 5
3 NaN 3.0 NaN 4
# 用 0 替换所有 NaN 元素。
df.fillna(0)
A B C D
0 0.0 2.0 0.0 0
1 3.0 4.0 0.0 1
2 0.0 0.0 0.0 5
3 0.0 3.0 0.0 4
# 我们还可以向前或向后传播非空值,axis 设置坐标轴参开
df.fillna(method="bfill", axis=1)
A B C D
0 2.0 2.0 0.0 0.0
1 3.0 4.0 1.0 1.0
2 5.0 5.0 5.0 5.0
3 3.0 3.0 4.0 4.0
# 将“A”、“B”、“C”和“D”列中的所有 NaN 元素分别替换为 0、1、2 和 3。
values = {"A": 0, "B": 1, "C": 2, "D": 3}
df.fillna(value=values)
A B C D
0 0.0 2.0 2.0 0
1 3.0 4.0 2.0 1
2 0.0 1.0 2.0 5
3 0.0 3.0 2.0 4
# 仅替换第一个 NaN 元素。
df.fillna(value=values, limit=1)
A B C D
0 0.0 2.0 2.0 0
1 3.0 4.0 NaN 1
2 NaN 1.0 NaN 5
3 NaN 3.0 NaN 4
# 使用 DataFrame 填充时,替换发生在相同的列名和相同的索引上
df2 = pd.DataFrame(np.zeros((4, 4)), columns=list("ABCE"))  # 一个 4×4 全部值为零 的数组
df.fillna(df2)
A B C D
0 0.0 2.0 0.0 0
1 3.0 4.0 0.0 1
2 0.0 0.0 0.0 5
3 0.0 3.0 0.0 4

5、层级索引

到目前为止,我们接触的都是一维、二维的数据,用 Pandas 的 Series 和 DataFrame 对象就可以储存。但是我们也经常会遇到存储多维数据的需求,数据索引超过一两个键。因此,Pandas 提供了PanelPanel4D对象解决三维数据四维数据。而在实践中,更直观的形式是通过层级索引(hierarchical indexing,也称为多级索引,multi-indexing)配合多个有不同等级(level)的一级索引一起使用,这样就可以将高维数组转换成类似一维 Series 和二维 DataFrame 对象的形式。

这一节,我们将介绍创建MultiIndex对象的方法,多级索引数据的取值、切片和统计值的计算,以及普通索引与层级索引的转换方法。

5.1、多级索引 Series

让我们看看如果用一维的 Series 对象表示二维数据 – 用一系列包含特征与数值的数据点来简单演示。

5.1.1、笨方法

假设你想要分析美国各州在两个不同年份的数据。如果你用前面介绍的 Pandas 工具来处理,那么可能会用一个 Python元组 来表示索引:

index = [('California', 2000), ('California', 2010),
         ('New York', 2000), ('New York', 2010),
         ('Texas', 2000), ('Texas', 2010)]
populations = [33871648, 37253956,
               18976457, 19378102,
               20851820, 25145561]
pop = pd.Series(populations, index=index)
pop
(California, 2000)    33871648
(California, 2010)    37253956
(New York, 2000)      18976457
(New York, 2010)      19378102
(Texas, 2000)         20851820
(Texas, 2010)         25145561
dtype: int64

通过元组构成的多级索引,你可以直接在 Series 上取值或用切片查询数据:

pop[('California', 2010):('Texas', 2000)]
(California, 2010)    37253956
(New York, 2000)      18976457
(New York, 2010)      19378102
(Texas, 2000)         20851820
dtype: int64

但是这样做很不方便。假如你想要现在所有 2010 年的数据,那么就得用一些比较复杂的(可能也比较慢的)清理方法了:

pop[[i for i in pop.index if i[1] == 2010]]
(California, 2010)    37253956
(New York, 2010)      19378102
(Texas, 2010)         25145561
dtype: int64

这样做虽然也可以得到需要的结果,但是与 Pandas 令人爱不释手的切片语法相比,这种方法确实不够简洁(在处理较大的数据时也不够高效)。

5.1.2、好办法:Pandas 多级索引

好在 Pandas 提供了更好的解决方案。用元组表示索引其实是多级索引的基础,Pandas 的 MultiIndex 类型提供了更丰富的操作方法。我们可以用元组创建一个多级索引:

index = pd.MultiIndex.from_tuples(index)
index
MultiIndex([('California', 2000),
            ('California', 2010),
            (  'New York', 2000),
            (  'New York', 2010),
            (     'Texas', 2000),
            (     'Texas', 2010)],
           )
pop = pop.reindex(index)
pop
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
Texas       2000    20851820
            2010    25145561
dtype: int64

其中前两列表示 Series 的多级索引值,第三列是数据。你会发现有些行仿佛缺失了第一列数据 – 其实这是多级索引的表现形式,每个空格与上面的索引相同。

现在可以直接用第二个索引获取 2010 年的全部数据,与 Pandas 的切片查询用法一致:

pop[:, 2010]
California    37253956
New York      19378102
Texas         25145561
dtype: int64

结果是单索引的数组,正是我们需要的。与之前的元组索引相比,多级索引的语法更简洁。操作也更方便。

5.1.3、高维数据的多级索引

你可能注意到,我们其实完全可以用一个带行列索引的简单 DataFrame 代替前面的多级索引。其实 Pandas 已经实现了类似的功能。unstack() 方法可以快速将一个多级索引的 Series 转化为普通索引的 DataFrame:

pop_df = pop.unstack()
pop_df
2000 2010
California 33871648 37253956
New York 18976457 19378102
Texas 20851820 25145561

也有 stack() 方法实现相反的效果:

pop_df.stack()
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
Texas       2000    20851820
            2010    25145561
dtype: int64

你可能会纠结于为什么要费时间研究层级索引。其实理由很简单:

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

假如要增加一列显示每年各州的人口统计指标(比如18岁以下的人口),那么对于这种带有 MultiIndex 的对象,增加一列就像 DataFrame 的操作一样简单:

pop_df = pd.DataFrame({'人口中总数': pop,
                       '未满18岁': [9267089, 9284094,
                                   4687374, 4318033,
                                   5906301, 6879014]})
pop_df
total under18
California 2000 33871648 9267089
2010 37253956 9284094
New York 2000 18976457 4687374
2010 19378102 4318033
Texas 2000 20851820 5906301
2010 25145561 6879014

层级索引也同样适用于之前讲的通过函数和其他功能。比如我们可以计算18岁以下的人口占总人数的比例:

f_u18 = pop_df['未满18岁'] / pop_df['人口中总数']
f_u18.unstack()
2000 2010
California 0.273594 0.249211
New York 0.247010 0.222831
Texas 0.283251 0.273568

5.2、多级索引的创建方法

为 Series 或 DataFrame 创建的多级索引最直接的办法就是将 index 参数设置为至少二维的索引数组,如下所示:

df = pd.DataFrame(np.random.rand(4, 2),
                  index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                  columns=['数据1', '数据2'])
df
data1 data2
a 1 0.867703 0.473127
2 0.244040 0.119101
b 1 0.897737 0.971455
2 0.913016 0.206495

同理,如果你把将元组作为键的字典传递给 Pandas,Pandas 也会默认转换为 MultiIndex:

data = {('California', 2000): 33871648,
        ('California', 2010): 37253956,
        ('Texas', 2000): 20851820,
        ('Texas', 2010): 25145561,
        ('New York', 2000): 18976457,
        ('New York', 2010): 19378102}
pd.Series(data)
California  2000    33871648
            2010    37253956
Texas       2000    20851820
            2010    25145561
New York    2000    18976457
            2010    19378102
dtype: int64

但是有时候显式地创建 MultiIndex 也是很有用的,下面来介绍一些创建方法。

5.2.1、显式地创建多级索引

你可以用 pd.MultiIndex 中的类方法更加灵活地构建多级索引。

  • 通过一个有不同等级的若干简单的数组组成的列表来构建 MultiIndex
pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
MultiIndex([('a', 1),
            ('a', 2),
            ('b', 1),
            ('b', 2)],
           )
  • 通过包含多个索引值的元组构成的列表创建 MultiIndex
pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
MultiIndex([('a', 1),
            ('a', 2),
            ('b', 1),
            ('b', 2)],
           )
  • 通过两个索引的笛卡尔积创建 MultiIndex
pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
MultiIndex([('a', 1),
            ('a', 2),
            ('b', 1),
            ('b', 2)],
           )
  • 通过直接提供levels(包含每个等级的索引值列表的列表)和codes(包含每个索引值标签列表的列表)创建 MultiIndex
pd.MultiIndex(levels=[['a', 'b'], [1, 2]],
              codes=[[0, 0, 1, 1], [0, 1, 0, 1]])
MultiIndex([('a', 1),
            ('a', 2),
            ('b', 1),
            ('b', 2)],
           )

在创建 Series 或 DataFrame 时,可以将这些对象作为 index 参数,或者通过 reindex 方法更新 Series 或 DataFrame 的索引。

5.2.2、多级索引的等级名称

MultiIndex的等级加上名称会为一些操作提供便利。你可以面前任何一个MultiIndex构造器中通过names参数设置等级名称,也可以在创建之后通过索引的names属性来修改名称:

pop.index.names = ['州', '年份']
pop
州           年份  
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
Texas       2000    20851820
            2010    25145561
dtype: int64

在处理复杂的数据时,为等级设置名称是管理多个索引值的好方法。

5.2.3、多级列索引

每个DataFrame的行与列都是对称的,也就是说既然有多级行索引,那么同样可以有多级列索引。让我们通过一份医学报告的模拟数据来演示:

# 多级行列索引, from_product 笛卡尔积
index = pd.MultiIndex.from_product([[2013, 2014], [1, 2]], 
                                   names=['年份', '次数'])
columns = pd.MultiIndex.from_product([['Bob', 'Guido', 'Sue'], ['心率', '体温']], 
                                     names=['姓名', '项目'])
# 模拟数据
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data += 37

# 创建 DataFrame
health_data = pd.DataFrame(data, index=index, columns=columns)
health_data
姓名 Bob Guido Sue
项目 心率 体温 心率 体温 心率 体温
年份 次数
2013 1 45.0 37.0 39.0 37.0 36.0 36.4
2 15.0 38.3 41.0 36.0 48.0 35.6
2014 1 45.0 36.0 46.0 36.2 39.0 36.3
2 33.0 36.1 27.0 37.6 25.0 37.5

多级行列索引的创建非常简单。上面创建了一个简单的思维数据,四个维度分别为被检查人的姓名、检查项目、检查年份和检查次数。可以在列索引的第一级查询姓名,从而获取包含一个人全部检查的DataFrame

health_data['Guido']
项目 HR Temp
年份 次数
2013 1 41.0 35.6
2 35.0 35.2
2014 1 42.0 35.9
2 23.0 36.5

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

5.3、多级索引的取值与切片

MultiIndex的取值和qp操作很直观,你可以直接把索引看成额外增加的维度。我们想来介绍Series多级索引的取值和切片方法,在介绍DataFrame的用法:

5.3.1、Series 多级索引

看看下面由各州历年人口数量创建的多级索引Series

pop
州           年份  
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
Texas       2000    20851820
            2010    25145561
dtype: int64

可以用过对多个级别索引值获取单个元素:

pop['California', 2000]
33871648

MultiIndex也支持局部取值(partial indexing),即只取索引的某一个层级。假如只取最高级的索引,获得的结果是一个新的Series,未被选中的底层索引值会被保留:

pop['California']
2000    33871648
2010    37253956
dtype: int64

类似的还有局部切片,不过要求MultiIndex是按顺序排序的:

pop.loc['California':'New York']
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
dtype: int64

如果索引已经排序,那么可以用较低层级的索引取值,第一层级的索引可以用空切片:

pop[:, 2000]
California    33871648
New York      18976457
Texas         20851820
dtype: int64

其他取值与数据选择的方法也都起作用。下面的例子是通过布尔掩码选择数据:

pop[pop > 22000000]
California  2000    33871648
            2010    37253956
Texas       2010    25145561
dtype: int64

也可以用花哨的索引选择数据:

pop[['California', 'Texas']]
California  2000    33871648
            2010    37253956
Texas       2000    20851820
            2010    25145561
dtype: int64

5.3.2、DataFrame 多级索引

DataFrame 多级索引的用法与 Series 类似。还用之前的体检报告数据来演示:

health_data
姓名 Bob Guido Sue
项目 心率 体温 心率 体温 心率 体温
年份 次数
2013 1 45.0 37.0 39.0 37.0 36.0 36.4
2 15.0 38.3 41.0 36.0 48.0 35.6
2014 1 45.0 36.0 46.0 36.2 39.0 36.3
2 33.0 36.1 27.0 37.6 25.0 37.5
health_data['Guido', '心率']
年份    次数
2013  1     39.0
      2     41.0
2014  1     46.0
      2     27.0
Name: (Guido, 心率), dtype: float64
health_data.iloc[:2, :2]
姓名 Bob
项目 心率 体温
年份 次数
2013 1 45.0 37.0
2 15.0 38.3
# health_data.loc[:, ('Bob', 'Sue')]  这样也是可以的
health_data.loc[:, ['Bob', 'Sue']]
姓名 Bob Sue
项目 心率 体温 心率 体温
年份 次数
2013 1 45.0 37.0 36.0 36.4
2 15.0 38.3 48.0 35.6
2014 1 45.0 36.0 39.0 36.3
2 33.0 36.1 25.0 37.5

这种索引元组的方法不是很方便,如果在元组中使用切片还会导致语法错误:

# health_data.loc[(:, 1), (:, '心率')]  这样也是可以的
health_data.loc[[:, 1], [:, '心率']]
  File "/var/folders/rx/27fc03s525nbs15jphd750hm0000gn/T/ipykernel_95399/3276540353.py", line 2
    health_data.loc[[:, 1], [:, '心率']]
                     ^
SyntaxError: invalid syntax

虽然你可以用 Python 内置的silce()函数获取想要的切片,但是还有一种更好的办法,就是使用IndexSilce对象。Pandas 专门用它解决这类问题,例如:

idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, '心率']]
姓名 Bob Guido Sue
项目 心率 心率 心率
年份 次数
2013 1 45.0 39.0 36.0
2014 1 45.0 46.0 39.0

和带多级索引的SeriesDataFrame进行数据交互的方法有很多,但就像上面讲的诸多工具一样,若想要掌握它们,最好的办法就是使用它们。

5.4、多级索引行列转换

使用多级索引的关键是掌握有效数据转换的方法。Pandas 提供了许多操作,可以让数据在内容保持不变的同时,按照需求进行行列转换。之前我们用一个简短的例子演示过stack()unstack()的方法,但其实还有许多合理控制层级行列索引的方法,让我们来一探究。

5.4.1、有序的索引和无序的索引

在前面的内容里,我们曾经简单提过多级索引排序,这里需要详细介绍一下。如果 MultiIndex 不是有序的索引,那么大多数切片操作都会失败。让我们演示一下。

首先创建一个不按字典顺序(lexographically)排序的多级索引 Series:

index = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])
data = pd.Series(np.random.rand(6), index=index)
data.index.names = ['char', 'int']
data
char  int
a     1      0.436070
      2      0.805001
c     1      0.463607
      2      0.758502
b     1      0.528290
      2      0.426491
dtype: float64

如果相对索引使用局部切片,那么就会报错:

try:
    data['a':'b']
except KeyError as e:
    print(type(e))
    print(e)
<class 'pandas.errors.UnsortedIndexError'>
'Key length (1) was greater than MultiIndex lexsort depth (0)'

尽管上面的错误信息及看不懂也看不出具体细节,但问题是出现在MultiIndex无序排列上。局部切片和许多其他相似的操作都要求MultiIndex的各级索引是有序的(即按照字典顺序由A至Z)。为此,Pandas 提供了许多便捷的操作完成排序,如sort_index()sortlevel()方法,我们用最简单的soet_index()方法来演示:

data = data.sort_index()
data
char  int
a     1      0.436070
      2      0.805001
b     1      0.528290
      2      0.426491
c     1      0.463607
      2      0.758502
dtype: float64

索引排序之后,局部切片就可以正常使用了:

data['a':'b']
char  int
a     1      0.436070
      2      0.805001
b     1      0.528290
      2      0.426491
dtype: float64

5.4.2、索引 stack 与 unstack

前文曾提过,我们可以将一个多级索引数据集转换成简单的二维形式,可以通过level参数设置转换的索引层级:

pop.unstack(level=0)
California New York Texas
年份
2000 33871648 18976457 20851820
2010 37253956 19378102 25145561
pop.unstack(level=1)
年份 2000 2010
California 33871648 37253956
New York 18976457 19378102
Texas 20851820 25145561
# 两个方法互为逆操作,同时使用数据保持不变
pop.unstack().stack()
州           年份  
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
Texas       2000    20851820
            2010    25145561
dtype: int64

5.4.3、索引的设置与重置

层级数据维度转换的另一种方法是行列标签转换,可以通过reset_index()方法实现。如果上面的人口数据Series中使用该方法,则会生成一个标签中包含之前行索引标签年份的 DataFrame。也可以用数据的name属性为列设置名称:

pop_flat = pop.reset_index(name='人口')
pop_flat
年份 人口
0 California 2000 33871648
1 California 2010 37253956
2 New York 2000 18976457
3 New York 2010 19378102
4 Texas 2000 20851820
5 Texas 2010 25145561

在解决实际问题的时候,如果能将类似这样的原始输入数据的列直接转换成MultiIndex,通常将会大有裨益。其实可以通过 DataFrame 的set_index()方法实现,返回结果就会是一个带多级索引的 DataFrame:

pop_flat.set_index(['州', '年份'])
人口
年份
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561

在实践中,用这种重建索引的方法处理数据集非常好用。

5.5、多级索引的数据累计方法

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

再一次以体检数据为例:

health_data
姓名 Bob Guido Sue
项目 心率 体温 心率 体温 心率 体温
年份 次数
2013 1 45.0 37.0 39.0 37.0 36.0 36.4
2 15.0 38.3 41.0 36.0 48.0 35.6
2014 1 45.0 36.0 46.0 36.2 39.0 36.3
2 33.0 36.1 27.0 37.6 25.0 37.5

如果你需要计算每一年各项指标的平均值,那么可以将参数level设置为索引年份

# 不建议在 DataFrame 和 Series 聚合中使用级别关键字,并将在未来版本中删除。
# 改为使用 groupby:df.mean(level=1) 应该使用 df.groupby(level=1).mean()。
# data_mean = health_data.mean(level='年份') 
data_mean = health_data.groupby(level='年份').mean()
data_mean
姓名 Bob Guido Sue
项目 心率 体温 心率 体温 心率 体温
年份
2013 30.0 37.65 40.0 36.5 42.0 36.0
2014 39.0 36.05 36.5 36.9 32.0 36.9

如果在设置axis参数,就可以对列索引进行类似的累计操作了:

# data_mean.mean(axis=1, level='项目')
data_mean.groupby(axis=1, level='项目').mean()
项目 体温 心率
年份
2013 36.716667 37.333333
2014 36.616667 35.833333

通过这两行数据,我们就可以获取每一年所有人的平均心率和体温了。这种语法其实就是GroupBy功能的快进方式。

6、合并数据集:Concat 与 Append 操作

将不同的数据源进行合并是数据科学中最有趣的事情之一,这即包括将两个不同的数据集非常简单地拼凑在一起,也包括用数据库这样的连接(join)与合并(merge)操作处理有重叠字段的数据集。Series 与 DataFrame 都具有这类操作,Pandas 的函数与方法让数据合并变得快速简单。

先来用pd.concat函数演示一个 Series 与 DataFrame 的简单合并操作。之后,我们将介绍 Pandas 中更复杂的joinmerge内存数据合并操作。

先定一个能创建 DataFrame 某种形式的函数,后面将会用到:

class display(object):
    """显示多个对象的HTML表示"""
    template = """<div style="float: left; padding: 10px;">
    <p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1}
    </div>"""
    def __init__(self, *args):
        self.args = args
        
    def _repr_html_(self):
        return '\n'.join(self.template.format(a, eval(a)._repr_html_())
                         for a in self.args)
    
    def __repr__(self):
        return '\n\n'.join(a + '\n' + repr(eval(a))
                           for a in self.args)
    
def make_df(cols, ind):
    data = {c: [str(c) + str(i) for i in ind] for c in cols}
    return pd.DataFrame(data, ind)

# DataFrame 示例
make_df('ABC', range(3))
A B C
0 A0 B0 C0
1 A1 B1 C1
2 A2 B2 C2

6.1、【知识回顾】NumPy 数组的合并

合并 Series 与 DataFrame 与合并 NumPy 数组基本相同,后者通过第二章 NumPy 入门中介绍的np.concatenate函数即可完成。你可以用这个函数将两个或两个以上的数组合并成一个数组。

x = [1, 2 ,3]
y = [4, 5, 6]
z = [7, 8, 9]
np.concatenate([x, y, z])
array([1, 2, 3, 4, 5, 6, 7, 8, 9])

第一个参数是需要合并的数组列表或元组。还有一个axis参数可以设置合并的坐标轴方向:

x = [[1, 2], [3, 4]]
np.concatenate([x, x], axis=1)
array([[1, 2, 1, 2],
       [3, 4, 3, 4]])

6.2、通过 pd.concat 实现建议合并

Pandas 有一个pd.concat()函数与np.concatenate函数语法类似,但是配置参数更多,功能也更强大:

# Pandas 1.3.3版本中的 函数签名
pd.concat(objs: Iterable[NDFrame] | Mapping[Hashable, NDFrame],
          axis: Any = 0,
          join: Any = "outer",
          ignore_index: bool = False,
          keys: Any = None,
          levels: Any = None,
          names: Any = None,
          verify_integrity: bool = False,
          sort: bool = False,
          copy: bool = True) -> DataFrame | Series
ser1 = pd.Series(list('ABC'), index=[1, 2, 3])
ser2 = pd.Series(list('DEF'), index=[4, 5, 6])
pd.concat([ser1, ser2])
1    A
2    B
3    C
4    D
5    E
6    F
dtype: object

它也可以用来合并高维数据,例如下面的 DataFrame:

df1 = make_df('AB', [1, 2])
df2 = make_df('AB', [3, 4])
display('df1', 'df2', 'pd.concat([df1, df2])')

df1

A B
1 A1 B1
2 A2 B2

df2

A B
3 A3 B3
4 A4 B4

pd.concat([df1, df2])

A B
1 A1 B1
2 A2 B2
3 A3 B3
4 A4 B4

默认情况下,DataFrame 的合并都是逐行进行的(默认设置是 axis=0)。与np.concatenate()一样,pd.concat()也可以设置合并坐标轴,例如下面的示例:

df3 = make_df('AB', [0, 1])
df4 = make_df('CD', [0, 1])
display('df3', 'df4', "pd.concat([df3, df4], axis=1)")

df3

A B
0 A0 B0
1 A1 B1

df4

C D
0 C0 D0
1 C1 D1

pd.concat([df3, df4], axis=1)

A B C D
0 A0 B0 C0 D0
1 A1 B1 C1 D1

6.2.1、索引重复

np.concatenate()pd.concat()最主要的差异之一就是 Pandas 在合并是会保留索引,即使索引是重复的!例如下面的简单示例:

x = make_df('AB', [0, 1])
y = make_df('AB', [2, 3])
y.index = x.index
display('x', 'y', 'pd.concat([x, y])')

x

A B
0 A0 B0
1 A1 B1

y

A B
0 A2 B2
1 A3 B3

pd.concat([x, y])

A B
0 A0 B0
1 A1 B1
0 A2 B2
1 A3 B3

你会发现结果中的索引是重复的。虽然 DataFrame 允许这么做,但结果并不是我们想要的。pd.concat()提供了一些解决这个问题的方法。

(a)捕获索引重复的错a误

如果你想要检查pd.concat()合并的结果中是否出现了重复的索引触发异常。下面的示例可以让我们清晰地捕捉并打印错误信息:

try:
    pd.concat([x, y], verify_integrity=True)
except ValueError as e:
    print('ValueError:', e)
ValueError: Indexes have overlapping values: Int64Index([0, 1], dtype='int64')
(b)忽略索引

有时索引无关紧要,那么合并时就可以忽略它们,可以通过设置ignore_index参数来实现。如果将参数设置为 True,那么合并时将会创建一个新的整数索引:

display('x', 'y', 'pd.concat([x, y], ignore_index=True)')

x

A B
0 A0 B0
1 A1 B1

y

A B
0 A2 B2
1 A3 B3

pd.concat([x, y], ignore_index=True)

A B
0 A0 B0
1 A1 B1
2 A2 B2
3 A3 B3
(c)增加多级索引

另一种处理索引重复的方法是通过keys参数为数据源设置多级索引标签,这样的结果数据就会带上多级索引:

display('x', 'y', "pd.concat([x, y], keys=['x', 'y'])")

x

A B
0 A0 B0
1 A1 B1

y

A B
0 A2 B2
1 A3 B3

pd.concat([x, y], keys=['x', 'y'])

A B
x 0 A0 B0
1 A1 B1
y 0 A2 B2
1 A3 B3

6.2.2、类似 join 的合并

df5 = make_df('ABC', [1, 2])
df6 = make_df('BCD', [3, 4])
display('df5', 'df6', "pd.concat([df5, df6])")

df5

A B C
1 A1 B1 C1
2 A2 B2 C2

df6

B C D
3 B3 C3 D3
4 B4 C4 D4

pd.concat([df5, df6])

A B C D
1 A1 B1 C1 NaN
2 A2 B2 C2 NaN
3 NaN B3 C3 D3
4 NaN B4 C4 D4

默认情况下,某个位置上缺失的数据会用NaN表示。如果不想这样,可以用joinjoin_axes参数设置合并方式。默认的合并方式是对所有输入列进行并集合并(join=‘outer’),当然也可以用join='inner'实现对输入列的交集合并:

display('df5', 'df6', "pd.concat([df5, df6], join='inner')")

df5

A B C
1 A1 B1 C1
2 A2 B2 C2

df6

B C D
3 B3 C3 D3
4 B4 C4 D4

pd.concat([df5, df6], join='inner')

B C
1 B1 C1
2 B2 C2
3 B3 C3
4 B4 C4

6.2.3、append() 方法

因为直接进行数组合并的需求非常普遍,所以 Series 和 DataFrame 对象都支持append()方法,让你通过最少的代码实现合并功能。

  • df1.append(df2) == pd.concat([df1, df2])
display('df1', 'df2', 'df1.append(df2)')

df1

A B
1 A1 B1
2 A2 B2

df2

A B
3 A3 B3
4 A4 B4

df1.append(df2)

A B
1 A1 B1
2 A2 B2
3 A3 B3
4 A4 B4

这里需要注意的是,与 Python 列表中的append()extend()方法不同,Pandas 的append()不直接更新原油对象的值,而是为合并后的数据创建一个新的对象。因此,它不能被称之为一个非常高效的解决方案,因为每次合并都需要重新创建索引和数据缓存。总之,如果你需要进行多个append操作,还是建议先创建一个 DataFrame 列表,然后用concat()函数一次性解决所有合并问题。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐