Pandas 是在 NumPy 基础上建立的新程序库,提供了一种高效的 DataFrame数据结构。 DataFrame 本质上是一种带行标签和列标签、支持相同类型数据和缺失值的多维数组。

NumPy 的 ndarray 数据结构为数值计算任务中常见的干净整齐、组织良好的数据提供了许多不可或缺的功能。虽然它在这方面做得很好,但是当我们需要处理更灵活的数据任务(如为数据添加标签、处理缺失值等),或者需要做一些不是对每个元素都进行广播映射的计算(如分组、透视表等)时, NumPy 的限制就非常明显了,而这些都是分析各种非结构化数据时很重要的一部分。建立在 NumPy 数组结构上的Pandas,尤其是它的 Series 和 DataFrame 对象,为数据科学家们处理那些消耗大量时间的“数据清理”(data munging)任务提供了捷径。

和之前导入 NumPy 并使用别名 np 一样,我们将导入 Pandas 并使用别名 pd:

  1. import numpy as np
  2. import pandas as pd

Pandas 有三个基本数据结构: SeriesDataFrame 和 Index。

Pandas的Series对象

Pandas 的 Series 对象是一个带索引数据构成的一维数组。可以用一个数组创建 Series 对象,如下所示:

  1. In[2]: data = pd.Series([0.25, 0.5, 0.75, 1.0])
  2. data
  3. Out[2]: 0 0.25
  4. 1 0.50
  5. 2 0.75
  6. 3 1.00
  7. dtype: float64

从上面的结果中,你会发现 Series 对象将一组数据和一组索引绑定在一起,我们可以通过values 属性和 index 属性获取数据。 values 属性返回的结果与 NumPy 数组类似:

  1. In[3]: data.values
  2. Out[3]: array([ 0.25, 0.5 , 0.75, 1. ])

index 属性返回的结果是一个类型为 pd.Index 的类数组对象,我们将在后面的内容里详细介绍它:

  1. In[4]: data.index
  2. Out[4]: RangeIndex(start=0, stop=4, step=1)

和 NumPy 数组一样,数据可以通过 Python 的中括号索引标签获取:

  1. In[5]: data[1]
  2. Out[5]: 0.5
  3. In[6]: data[1:3]
  4. Out[6]: 1 0.50
  5. 2 0.75
  6. dtype: float64

1. Serise是通用的NumPy数组
到目前为止,我们可能觉得 Series 对象和一维 NumPy 数组基本可以等价交换,但两者间的本质差异其实是索引: NumPy 数组通过隐式定义的整数索引获取数值,而 Pandas 的Series 对象用一种显式定义的索引与数值关联。
显式索引的定义让 Series 对象拥有了更强的能力。例如,索引不再仅仅是整数,还可以是任意想要的类型。如果需要,完全可以用字符串定义索引:

  1. In[7]: data = pd.Series([0.25, 0.5, 0.75, 1.0],
  2. index=['a', 'b', 'c', 'd'])
  3. data
  4. Out[7]: a 0.25
  5. b 0.50
  6. c 0.75
  7. d 1.00
  8. dtype: float64

获取数值的方式与之前一样:

  1. In[8]: data['b']
  2. Out[8]: 0.5

也可以使用不连续或不按顺序的索引:

  1. In[9]: data = pd.Series([0.25, 0.5, 0.75, 1.0],
  2. index=[2, 5, 3, 7])
  3. data
  4. Out[9]: 2 0.25
  5. 5 0.50
  6. 3 0.75
  7. 7 1.00
  8. dtype: float64
  9. In[10]: data[5]
  10. Out[10]: 0.5

2. Series是特殊的字典
可以把 Pandas 的 Series 对象看成一种特殊的 Python 字典。字典是一种将任意键映射到一组任意值的数据结构,而 Series 对象其实是一种将类型键映射到一组类型值的数据结构。类型至关重要:就像 NumPy 数组背后特定类型的经过编译的代码使得它在某些操作上比普通的 Python 列表更加高效一样, Pandas Series 的类型信息使得它在某些操作上比Python 的字典更高效。
我们可以直接用 Python 的字典创建一个 Series 对象,让 Series 对象与字典的类比更加清晰:

  1. In[11]: population_dict = {'California': 38332521,
  2. 'Texas': 26448193,
  3. 'New York': 19651127,
  4. 'Florida': 19552860,
  5. 'Illinois': 12882135}
  6. population = pd.Series(population_dict)
  7. population
  8. Out[11]:California 38332521
  9. Florida 19552860
  10. Illinois 12882135
  11. New York 19651127
  12. Texas 26448193
  13. dtype: int64

用字典创建 Series 对象时,其索引默认按照顺序排列。典型的字典数值获取方式仍然有效:

  1. In[12]: population['California']
  2. Out[12]:38332521

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

  1. In[13]: population['California':'Illinois']
  2. Out[13]:California 38332521
  3. Florida 19552860
  4. Illinois 12882135
  5. dtype: int64

3. 创建Series对象
我们已经见过几种创建 Pandas 的 Series 对象的方法,都是像这样的形式:

  1. >>> pd.Series(data, index=index)

其中, index 是一个可选参数, data 参数支持多种数据类型。
例如, data 可以是列表或 NumPy 数组,这时 index 默认值为整数序列:

  1. In[14]: pd.Series([2, 4, 6])
  2. Out[14]:0 2
  3. 1 4
  4. 2 6
  5. dtype: int64

data 也可以是一个标量,创建 Series 对象时会重复填充到每个索引上:

  1. In[15]: pd.Series(5, index=[100, 200, 300])
  2. Out[15]:100 5
  3. 200 5
  4. 300 5
  5. dtype: int64

data 还可以是一个字典, index 默认是排序的字典键:

  1. In[16]: pd.Series({2:'a', 1:'b', 3:'c'})
  2. Out[16]:1 b
  3. 2 a
  4. 3 c
  5. dtype: object

每一种形式都可以通过显式指定索引筛选需要的结果:

  1. In[17]: pd.Series({2:'a', 1:'b', 3:'c'}, index=[3, 2])
  2. Out[17]:3 c
  3. 2 a
  4. dtype: object

这里需要注意的是, Series 对象只会保留显式定义的键值对。


Pandas的DataFrame对象

Pandas 的另一个基础数据结构是 DataFrame。和上一节介绍的 Series 对象一样, DataFrame 既可以作为一个通用型 NumPy 数组,也可以看作特殊的 Python 字典。

1. DataFrame是通用的NumPy数组
如果将 Series 类比为带灵活索引的一维数组,那么 DataFrame 就可以看作是一种既有灵活的行索引,又有灵活列名的二维数组。就像你可以把二维数组看成是有序排列的一维数组一样,你也可以把 DataFrame 看成是有序排列的若干 Series 对象。这里的“排列”指的是它们拥有共同的索引。
下面用上一节中美国五个州面积的数据创建一个新的 Series 来进行演示:

  1. In[18]:
  2. area_dict = {'California': 423967, 'Texas': 695662, 'New York': 141297,
  3. 'Florida': 170312, 'Illinois': 149995}
  4. area = pd.Series(area_dict)
  5. area
  6. Out[18]:California 423967
  7. Florida 170312
  8. Illinois 149995
  9. New York 141297
  10. Texas 695662
  11. dtype: int64

再结合之前创建的 population 的 Series 对象,用一个字典创建一个包含这些信息的二维对象:

  1. In[19]: states = pd.DataFrame({'population': population,
  2. 'area': area})
  3. states
  4. Out[19]: area population
  5. California 423967 38332521
  6. Florida 170312 19552860
  7. Illinois 149995 12882135
  8. New York 141297 19651127
  9. Texas 695662 26448193

和 Series 对象一样, DataFrame 也有一个 index 属性可以获取索引标签:

  1. In[20]: states.index
  2. Out[20]:
  3. Index(['California', 'Florida', 'Illinois', 'New York', 'Texas'], dtype='object')

另外, DataFrame 还有一个 columns 属性,是存放列标签的 Index 对象:

  1. In[21]: states.columns
  2. Out[21]: Index(['area', 'population'], dtype='object')

因此 DataFrame 可以看作一种通用的 NumPy 二维数组,它的行与列都可以通过索引获取。

2. DataFrame是特殊的字典
与 Series 类似,我们也可以把 DataFrame 看成一种特殊的字典。字典是一个键映射一个值,而 DataFrame 是一列映射一个 Series 的数据。例如,通过 ‘area’ 的列属性可以返回包含面积数据的 Series 对象:

  1. In[22]: states['area']
  2. Out[22]:California 423967
  3. Florida 170312
  4. Illinois 149995
  5. New York 141297
  6. Texas 695662
  7. Name: area, dtype: int64

这里需要注意的是,在 NumPy 的二维数组里, data[0] 返回第一行;而在 DataFrame 中,data[‘col0’] 返回第一列。因此,最好把 DataFrame 看成一种通用字典,而不是通用数组,即使这两种看法在不同情况下都是有用的。
3. 创建DataFrame对象
Pandas 的 DataFrame 对象可以通过许多方式创建,这里举几个常用的例子。
(1) 通过单个 Series 对象创建。 DataFrame 是一组 Series 对象的集合,可以用单个 Series创建一个单列的DataFrame:

  1. In[23]: pd.DataFrame(population, columns=['population'])
  2. Out[23]: population
  3. California 38332521
  4. Florida 19552860
  5. Illinois 12882135
  6. New York 19651127
  7. Texas 26448193


(2) 通过字典列表创建。任何元素是字典的列表都可以变成 DataFrame。用一个简单的列表
综合来创建一些数据:

  1. In[24]: data = [{'a': i, 'b': 2 * i}
  2. for i in range(3)]
  3. pd.DataFrame(data)
  4. Out[24]: a b
  5. 0 0 0
  6. 1 1 2
  7. 2 2 4

即使字典中有些键不存在, Pandas 也会用缺失值 NaN(不是数字, not a number)来表示:

  1. In[25]: pd.DataFrame([{'a': 1, 'b': 2}, {'b': 3, 'c': 4}])
  2. Out[25]: a b c
  3. 0 1.0 2 NaN
  4. 1 NaN 3 4.0

(3) 通过 Series 对象字典创建。就像之前见过的那样, DataFrame 也可以用一个由 Series对象构成的字典创建:

  1. In[26]: pd.DataFrame({'population': population,'area': area})
  2. Out[26]: area population
  3. California 423967 38332521
  4. Florida 170312 19552860
  5. Illinois 149995 12882135
  6. New York 141297 19651127
  7. Texas 695662 26448193

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

  1. In[27]: pd.DataFrame(np.random.rand(3, 2),
  2. columns=['foo', 'bar'],
  3. index=['a', 'b', 'c'])
  4. Out[27]: foo bar
  5. a 0.865257 0.213169
  6. b 0.442759 0.108267
  7. c 0.047110 0.905718

Series数据选择方法

如前所述, Series 对象与一维 NumPy 数组和标准 Python 字典在许多方面都一样。只要牢牢记住这两个类比,就可以帮助我们更好地理解 Series 对象的数据索引与选择模式。
将Series看作字典
和字典一样, Series 对象提供了键值对的映射:

  1. In[1]: import pandas as pd
  2. data = pd.Series([0.25, 0.5, 0.75, 1.0],
  3. index=['a', 'b', 'c', 'd'])
  4. data
  5. Out[1]: a 0.25
  6. b 0.50
  7. c 0.75
  8. d 1.00
  9. dtype: float64
  10. In[2]: data['b']
  11. Out[2]: 0.5

Series 对象还可以用字典语法调整数据。就像你可以通过增加新的键扩展字典一样,你也可以通过增加新的索引值扩展 Series:

  1. In[6]: data['e'] = 1.25
  2. data
  3. Out[6]: a 0.25
  4. b 0.50
  5. c 0.75
  6. d 1.00
  7. e 1.25
  8. dtype: float64

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

  1. In[7]: # 将显式索引作为切片
  2. data['a':'c']
  3. Out[7]: a 0.25
  4. b 0.50
  5. c 0.75
  6. dtype: float64
  7. In[8]: # 将隐式整数索引作为切片
  8. data[0:2]
  9. Out[8]: a 0.25
  10. b 0.50
  11. dtype: float64
  12. In[9]: # 掩码
  13. data[(data > 0.3) & (data < 0.8)]
  14. Out[9]: b 0.50
  15. c 0.75
  16. dtype: float64

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

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

  1. In[11]: data = pd.Series(['a', 'b', 'c'], index=[1, 3, 5])
  2. data
  3. Out[11]:1 a
  4. 3 b
  5. 5 c
  6. dtype: object
  7. In[12]: # 取值操作是显式索引
  8. data[1]
  9. Out[12]: 'a'
  10. In[13]: # 切片操作是隐式索引
  11. data[1:3]
  12. Out[13]:3 b
  13. 5 c
  14. dtype: object

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

  1. In[14]: data.loc[1]
  2. Out[14]: 'a'
  3. In[15]: data.loc[1:3]
  4. Out[15]:1 a
  5. 3 b
  6. dtype: object

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

  1. In[16]: data.iloc[1]
  2. Out[16]: 'b'
  3. In[17]: data.iloc[1:3]
  4. Out[17]:3 b
  5. 5 c
  6. dtype: object

DataFrame数据选择方法

前面曾提到, DataFrame 在有些方面像二维或结构化数组,在有些方面又像一个共享索引的若干 Series 对象构成的字典。这两种类比可以帮助我们更好地掌握这种数据结构的数据选择方法。
将DataFrame看作字典
第一种类比是把 DataFrame 当作一个由若干 Series 对象构成的字典。让我们用之前的美国五州面积与人口数据来演示:

  1. In[18]: area = pd.Series({'California': 423967, 'Texas': 695662,
  2. 'New York': 141297, 'Florida': 170312,
  3. 'Illinois': 149995})
  4. pop = pd.Series({'California': 38332521, 'Texas': 26448193,
  5. 'New York': 19651127, 'Florida': 19552860,
  6. 'Illinois': 12882135})
  7. data = pd.DataFrame({'area':area, 'pop':pop})
  8. data
  9. Out[18]: area pop
  10. California 423967 38332521
  11. Florida 170312 19552860
  12. Illinois 149995 12882135
  13. New York 141297 19651127
  14. Texas 695662 26448193

两个 Series 分别构成 DataFrame 的一列,可以通过对列名进行字典形式(dictionary-style)的取值获取数据:

  1. In[19]: data['area']
  2. Out[19]:California 423967
  3. Florida 170312
  4. Illinois 149995
  5. New York 141297
  6. Texas 695662
  7. Name: area, dtype: int64

同样,也可以用属性形式(attribute-style)选择纯字符串列名的数据:

  1. In[20]: data.area
  2. Out[20]:California 423967
  3. Florida 170312
  4. Illinois 149995
  5. New York 141297
  6. Texas 695662
  7. Name: area, dtype: int64

对同一个对象进行属性形式与字典形式的列数据,结果是相同的:

  1. In[21]: data.area is data['area']
  2. Out[21]: True

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

  1. In[22]: data.pop is data['pop']
  2. Out[22]: False

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

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

  1. In[23]: data['density'] = data['pop'] / data['area']
  2. data
  3. Out[23]: area pop density
  4. California 423967 38332521 90.413926
  5. Florida 170312 19552860 114.806121
  6. Illinois 149995 12882135 85.883763
  7. New York 141297 19651127 139.076746
  8. Texas 695662 26448193 38.018740

将DataFrame看作二维数组
前面曾提到,可以把 DataFrame 看成是一个增强版的二维数组,用 values 属性按行查看数组数据:

  1. In[24]: data.values
  2. Out[24]: array([[ 4.23967000e+05, 3.83325210e+07, 9.04139261e+01],
  3. [ 1.70312000e+05, 1.95528600e+07, 1.14806121e+02],
  4. [ 1.49995000e+05, 1.28821350e+07, 8.58837628e+01],
  5. [ 1.41297000e+05, 1.96511270e+07, 1.39076746e+02],
  6. [ 6.95662000e+05, 2.64481930e+07, 3.80187404e+01]])

理解了这一点,就可以把许多数组操作方式用在 DataFrame 上。例如,可以对 DataFrame 进行行列转置:

  1. In[25]: data.T
  2. Out[25]:
  3. California Florida Illinois New York Texas
  4. area 4.239670e+05 1.703120e+05 1.499950e+05 1.412970e+05 6.956620e+05
  5. pop 3.833252e+07 1.955286e+07 1.288214e+07 1.965113e+07 2.644819e+07
  6. density 9.041393e+01 1.148061e+02 8.588376e+01 1.390767e+02 3.801874e+01

通过字典形式对列进行取值显然会限制我们把 DataFrame 作为 NumPy 数组可以获得的能力,尤其是当我们在 DataFrame 数组中使用单个行索引获取一行数据时:

  1. In[26]: data.values[0]
  2. Out[26]: array([ 4.23967000e+05, 3.83325210e+07, 9.04139261e+01])

而获取一列数据就需要向 DataFrame 传递单个列索引:

  1. In[27]: data['area']
  2. Out[27]:California 423967
  3. Florida 170312
  4. Illinois 149995
  5. New York 141297
  6. Texas 695662
  7. Name: area, dtype: int64

其他取值方法
还有一些取值方法和前面介绍过的方法不太一样。它们虽然看着有点奇怪,但是在实践中还是很好用的。首先,如果对单个标签取值就选择列,而对多个标签用切片就选择行:

  1. In[33]: data['Florida':'Illinois']
  2. Out[33]: area pop density
  3. Florida 170312 19552860 114.806121
  4. Illinois 149995 12882135 85.883763

切片也可以不用索引值,而直接用行数来实现:

  1. In[34]: data[1:3]
  2. Out[34]: area pop density
  3. Florida 170312 19552860 114.806121
  4. Illinois 149995 12882135 85.883763

与之类似,掩码操作也可以直接对每一行进行过滤,而不需要使用 loc 索引器:

  1. In[35]: data[data.density > 100]
  2. Out[35]: area pop density
  3. Florida 170312 19552860 114.806121
  4. New York 141297 19651127 139.076746

这两种操作方法其实与 NumPy 数组的语法类似,虽然它们与 Pandas 的操作习惯不太一致,但是在实践中非常好用。