Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备

本人以简书作者 SeanCheney 系列专题文章并结合原书为学习资源,记录个人笔记,仅作为知识记录及后期复习所用,原作者地址查看 简书 SeanCheney,如有错误,还望批评指教。——ZJ

原作者:SeanCheney | 《利用 Python 进行数据分析·第2版》第7章 数据清洗和准备 | 來源:简书

Github:wesm | Github:中文 BrambleXu|
简书:利用 Python 进行数据分析·第2版

环境: Python 3.6


Chapter 7 Data Cleaning and Preparation

  • 在数据分析和建模的过程中,相当多的时间要用在数据准备上:加载、清理、转换以及重塑。这些工作会占到分析师时间的 80% 或更多。有时,存储在文件和数据库中的数据的格式不适合某个特定的任务。
  • pandas 和内置的 Python 标准库提供了一组高级的、灵活的、快速的工具,可以让你轻松地将数据规变为想要的格式。
  • 在本章中,我会讨论处理缺失数据、重复数据、字符串操作和其它分析数据转换的工具。下一章,我会关注于用多种方法合并、重塑数据集。

7.1 处理缺失数据

在许多数据分析工作中,缺失数据是经常发生的。 pandas 的目标之一就是尽量轻松地处理缺失数据。例如, pandas 对象的所有描述性统计默认都不包括缺失数据。

缺失数据在 pandas 中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据, pandas 使用浮点值 NaN(Not a Number)表示缺失数据。我们称其为哨兵值,可以方便的检测出来:

In [1]: import numpy as np

In [2]: import pandas as pd

In [3]: string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])

In [4]: string_data
Out[4]:
0     aardvark
1    artichoke
2          NaN
3      avocado
dtype: object

In [5]: string_data.isnull()
Out[5]:
0    False
1    False
2     True
3    False
dtype: bool

  • 在 pandas 中,我们采用了 R 语言中的惯用法,即将缺失值表示为 NA ,它表示不可用 not available。
  • 在统计应用中, NA 数据可能是不存在的数据或者虽然存在,但是没有观察到(例如,数据采集中发生了问题)。
  • 当进行数据清洗以进行分析时,最好直接对缺失数据进行分析,以判断数据采集的问题或缺失数据可能导致的偏差。

Python 内置的 None 值在对象数组中也可以作为 NA:

In [6]: type(string_data)
Out[6]: pandas.core.series.Series

In [7]: string_data[0] = None

In [8]: string_data.isnull()
Out[8]:
0     True
1    False
2     True
3    False
dtype: bool

pandas 项目中还在不断优化内部细节以更好处理缺失数据,像用户API功能,例如 pandas .isnull,去除了许多恼人的细节。表7-1列出了一些关于缺失数据处理的函数。

Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备_第1张图片

滤除缺失数据

  • 过滤掉缺失数据的办法有很多种。你可以通过 pandas.isnull或布尔索引的手工方法,但 dropna 可能会更实用一些。
  • 对于一个 Series , dropna 返回一个仅含非空数据和索引值的 Series
In [9]: from numpy import nan as NA

In [10]: data = pd.Series([1, NA, 3.5, NA, 7])

In [11]: data.dropna()
Out[11]:
0    1.0
2    3.5
4    7.0
dtype: float64

In [12]: data
Out[12]:
0    1.0
1    NaN
2    3.5
3    NaN
4    7.0
dtype: float64

这等价于:

In [14]: data[data.notnull()]
Out[14]:
0    1.0
2    3.5
4    7.0
dtype: float64
  • 而对于 DataFrame 对象,事情就有点复杂了。你可能希望丢弃全 NA 或含有 NA 的行或列。
  • dropna 默认丢弃任何含有缺失值的行:
In [15]:  data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],
    ...:                          [NA, NA, NA], [NA, 6.5, 3.]], index=['one', 't
    ...: wo', 'three', 'four'], columns=['first', 'second', 'third'])
    ...:
    ...:

In [16]: data
Out[16]:
       first  second  third
one      1.0     6.5    3.0
two      1.0     NaN    NaN
three    NaN     NaN    NaN
four     NaN     6.5    3.0

In [17]: cleaned  = data.dropna()

In [18]: cleaned
Out[18]:
     first  second  third
one    1.0     6.5    3.0

传入how='all'将只丢弃全为 NA 的那些行

In [19]: data.dropna(how='all')
Out[19]:
      first  second  third
one     1.0     6.5    3.0
two     1.0     NaN    NaN
four    NaN     6.5    3.0

用这种方式丢弃列(全是 NA 的列),只需传入axis=1即可:

In [20]: data['forth'] = NA

In [21]: data
Out[21]:
       first  second  third  forth
one      1.0     6.5    3.0    NaN
two      1.0     NaN    NaN    NaN
three    NaN     NaN    NaN    NaN
four     NaN     6.5    3.0    NaN

In [22]: data.dropna(axis=1, how='all')
Out[22]:
       first  second  third
one      1.0     6.5    3.0
two      1.0     NaN    NaN
three    NaN     NaN    NaN
four     NaN     6.5    3.0

另一个滤除 DataFrame 行的问题涉及时间序列数据。假设你只想留下一部分观测数据,可以用 thresh 参数实现此目的:

In [23]: df = pd.DataFrame(np.random.randn(7,3))

In [24]: df.iloc[:4, 1] = NA

In [25]: df
Out[25]:
          0         1         2
0 -0.052880       NaN  0.192669
1  0.440543       NaN -0.058121
2  0.297282       NaN -0.808425
3 -0.429874       NaN -0.965913
4  0.132290  0.251065  0.853049
5  1.190240 -1.118041 -0.075022
6  0.530970  0.033641 -0.473945

In [26]: df.iloc[:2, 2] = NA

In [27]: df
Out[27]:
          0         1         2
0 -0.052880       NaN       NaN
1  0.440543       NaN       NaN
2  0.297282       NaN -0.808425
3 -0.429874       NaN -0.965913
4  0.132290  0.251065  0.853049
5  1.190240 -1.118041 -0.075022
6  0.530970  0.033641 -0.473945

In [28]: df.dropna()
Out[28]:
         0         1         2
4  0.13229  0.251065  0.853049
5  1.19024 -1.118041 -0.075022
6  0.53097  0.033641 -0.473945

In [29]: df.dropna(thresh=2) # 索引 0 1 行 含有 NA 的去掉了
Out[29]:
          0         1         2
2  0.297282       NaN -0.808425
3 -0.429874       NaN -0.965913
4  0.132290  0.251065  0.853049
5  1.190240 -1.118041 -0.075022
6  0.530970  0.033641 -0.473945

填充缺失数据

  • 你可能不想滤除缺失数据(有可能会丢弃跟它有关的其他数据),而是希望通过其他方式填补那些“空洞”。
  • 对于大多数情况而言, fillna 方法是最主要的函数。通过一个常数调用 fillna 就会将缺失值替换为那个常数值:
In [30]: df.fillna(0)
Out[30]:
          0         1         2
0 -0.052880  0.000000  0.000000
1  0.440543  0.000000  0.000000
2  0.297282  0.000000 -0.808425
3 -0.429874  0.000000 -0.965913
4  0.132290  0.251065  0.853049
5  1.190240 -1.118041 -0.075022
6  0.530970  0.033641 -0.473945

若是通过一个字典调用 fillna ,就可以实现对不同的列填充不同的值:

In [31]: df.fillna({1: 0.5, 2: 0})
Out[31]:
          0         1         2
0 -0.052880  0.500000  0.000000
1  0.440543  0.500000  0.000000
2  0.297282  0.500000 -0.808425
3 -0.429874  0.500000 -0.965913
4  0.132290  0.251065  0.853049
5  1.190240 -1.118041 -0.075022
6  0.530970  0.033641 -0.473945

fillna 默认会返回新对象,但也可以对现有对象进行就地修改:

In [32]: _ = df.fillna(0, inplace= True)

In [33]: df
Out[33]:
          0         1         2
0 -0.052880  0.000000  0.000000
1  0.440543  0.000000  0.000000
2  0.297282  0.000000 -0.808425
3 -0.429874  0.000000 -0.965913
4  0.132290  0.251065  0.853049
5  1.190240 -1.118041 -0.075022
6  0.530970  0.033641 -0.473945

对 reindexing 有效的那些插值方法也可用于 fillna :

In [34]: df = pd.DataFrame(np.random.randn(6,3))

In [35]: df.iloc[2:, 1] = NA

In [36]: df
Out[36]:
          0         1         2
0 -0.775110  0.000504  1.445061
1 -0.469458  0.727227 -0.166666
2  0.019312       NaN -0.915137
3 -1.477259       NaN  0.423064
4  1.620944       NaN  1.165360
5  0.388970       NaN  0.230785

In [37]: df.iloc[4:, 2] = NA

In [38]: df
Out[38]:
          0         1         2
0 -0.775110  0.000504  1.445061
1 -0.469458  0.727227 -0.166666
2  0.019312       NaN -0.915137
3 -1.477259       NaN  0.423064
4  1.620944       NaN       NaN
5  0.388970       NaN       NaN

In [39]: df.fillna(method='ffill')
Out[39]:
          0         1         2
0 -0.775110  0.000504  1.445061
1 -0.469458  0.727227 -0.166666
2  0.019312  0.727227 -0.915137
3 -1.477259  0.727227  0.423064
4  1.620944  0.727227  0.423064
5  0.388970  0.727227  0.423064

In [40]: df.fillna(method='ffill', limit=2)
Out[40]:
          0         1         2
0 -0.775110  0.000504  1.445061
1 -0.469458  0.727227 -0.166666
2  0.019312  0.727227 -0.915137
3 -1.477259  0.727227  0.423064
4  1.620944       NaN  0.423064
5  0.388970       NaN  0.423064

只要有些创新,你就可以利用 fillna 实现许多别的功能。比如说,你可以传入 Series 的平均值或中位数:

In [41]: data = pd.Series([1., NA, 3.5, NA, 7])

In [42]: data.fillna(data.mean())
Out[42]:
0    1.000000
1    3.833333
2    3.500000
3    3.833333
4    7.000000
dtype: float64


表7-2列出了 fillna 的参考。

这里写图片描述

Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备_第2张图片

7.2 数据转换

本章到目前为止介绍的都是数据的重排。另一类重要操作则是过滤、清理以及其他的转换工作。

移除重复数据

DataFrame 中出现重复行有多种原因。下面就是一个例子:

In [59]: data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
    ...:    ....:                      'k2': [1, 1, 2, 3, 3, 4, 4]})
    ...:

In [60]: data
Out[60]:
    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

DataFrame 的duplicated方法返回一个布尔型 Series ,表示各行是否是重复行(前面出现过的行):

In [62]: data.duplicated()
Out[62]:
0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool

还有一个与此相关的drop_duplicates方法,它会返回一个 DataFrame ,重复的数组会标为 False(哪里标 False 了 ):

In [63]: data.drop_duplicates()
Out[63]:
    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4

In [64]: type(data.drop_duplicates())
Out[64]: pandas.core.frame.DataFrame

# 5  two   4
# 6  two   4
# 原来这两行 是相同的,所以重复 去掉最后一个

这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断。假设我们还有一列值,且只希望根据 k1 列过滤重复项:

In [65]: data['v1'] = range(7)

In [66]: data
Out[66]:
    k1  k2  v1
0  one   1   0
1  two   1   1
2  one   2   2
3  two   3   3
4  one   3   4
5  two   4   5
6  two   4   6

In [67]: data.drop_duplicates(['k1'])
Out[67]:
    k1  k2  v1
0  one   1   0
1  two   1   1

# 只判断 k1 中重复的,不判断 k2, v2

duplicateddrop_duplicates 默认保留的是第一个出现的值组合。传入keep='last'则保留最后一个:

In [68]: data.drop_duplicates(['k1', 'k2'], keep='last')
Out[68]:
    k1  k2  v1
0  one   1   0
1  two   1   1
2  one   2   2
3  two   3   3
4  one   3   4
6  two   4   6

# 5  two   4   5
# 6  two   4   6
# 这两行 因为只判断  'k1', 'k2' , keep='last' 保留最后一个,所以保留 索引 6 去掉索引 5 

利用函数或映射进行数据转换

对于许多数据集,你可能希望根据数组、 Series 或 DataFrame 列中的值来实现转换工作。我们来看看下面这组有关肉类的数据:

In [70]: data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon',
    ...:    ....:                               'Pastrami', 'corned beef', 'Baco
    ...: n',
    ...:    ....:                               'pastrami', 'honey ham', 'nova l
    ...: ox'],
    ...:    ....:                      'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]}
    ...: )
    ...:

In [71]: data
Out[71]:
          food  ounces
0        bacon     4.0
1  pulled pork     3.0
2        bacon    12.0
3     Pastrami     6.0
4  corned beef     7.5
5        Bacon     8.0
6     pastrami     3.0
7    honey ham     5.0
8     nova lox     6.0

假设你想要添加一列表示该肉类食物来源的动物类型。我们先编写一个不同肉类到动物的映射:

In [72]: meat_to_animal = {
    ...:   'bacon': 'pig',
    ...:   'pulled pork': 'pig',
    ...:   'pastrami': 'cow',
    ...:   'corned beef': 'cow',
    ...:   'honey ham': 'pig',
    ...:   'nova lox': 'salmon'
    ...: }
  • Series 的 map 方法可以接受一个函数或含有映射关系的字典型对象,但是这里有一个小问题,即有些肉类的首字母大写了,而另一些则没有。
  • 因此,我们还需要使用 Series 的 str.lower方法,将各个值转换为小写:
In [73]: lowercased = data['food'].str.lower()

In [74]: lowercased
Out[74]:
0          bacon
1    pulled pork
2          bacon
3       pastrami
4    corned beef
5          bacon
6       pastrami
7      honey ham
8       nova lox
Name: food, dtype: object

# data 中添加 animal 一列,这列中的数据,是 lowercased (food 列 转化为小写后的)作为 key  # 从 meat_to_animal  这个字典中取出对应的 value

In [75]: data['animal'] = lowercased.map(meat_to_animal)

In [76]: data
Out[76]:
          food  ounces  animal
0        bacon     4.0     pig
1  pulled pork     3.0     pig
2        bacon    12.0     pig
3     Pastrami     6.0     cow
4  corned beef     7.5     cow
5        Bacon     8.0     pig
6     pastrami     3.0     cow
7    honey ham     5.0     pig
8     nova lox     6.0  salmon

我们也可以传入一个能够完成全部这些工作的函数:

In [77]: data['food'].map(lambda x: meat_to_animal[x.lower()])
Out[77]:
0       pig
1       pig
2       pig
3       cow
4       cow
5       pig
6       cow
7       pig
8    salmon
Name: food, dtype: object

使用 map 是一种实现元素级转换以及其他数据清理工作的便捷方式。

替换值

  • 利用 fillna 方法填充缺失数据可以看做值替换的一种特殊情况。前面已经看到,map可用于修改对象的数据子集,
  • 而 replace 则提供了一种实现该功能的更简单、更灵活的方式。我们来看看下面这个 Series :
In [78]: data = pd.Series([1., -999., 2., -999., -1000., 3.])

In [79]: data
Out[79]:
0       1.0
1    -999.0
2       2.0
3    -999.0
4   -1000.0
5       3.0
dtype: float64

-999 这个值可能是一个表示缺失数据的标记值。要将其替换为 pandas 能够理解的 NA 值,我们可以利用 replace 来产生一个新的 Series (除非传入inplace=True):

In [84]: data.replace(-999.0, np.nan)
Out[84]:
0       1.0
1       NaN
2       2.0
3       NaN
4   -1000.0
5       3.0
dtype: float64

如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值::

In [85]: data.replace([-999, -1000], NA)
Out[85]:
0    1.0
1    NaN
2    2.0
3    NaN
4    NaN
5    3.0
dtype: float64

要让每个值有不同的替换值,可以传递一个替换列表:

In [86]: data.replace([-999, -1000], [np.nan, 0])
Out[86]:
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

传入的参数也可以是字典:

In [87]: data.replace({-999: np.nan, -1000: 0})
Out[87]:
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

  • 笔记:data.replace 方法与 data.str.replace 不同,后者做的是字符串的元素级替换。我们会在后面学习 Series 的字符串方法。

重命名轴索引

跟 Series 中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象。轴还可以被就地修改,而无需新建一个数据结构。接下来看看下面这个简单的例子:

In [88]: data = pd.DataFrame(np.arange(12).reshape((3,4)), 
                index=['Ohio', 'Colorado', 'New York'],
    ...:          columns=['one', 'two', 'three', 'four'])
    ...:

In [89]: data
Out[89]:
          one  two  three  four
Ohio        0    1      2     3
Colorado    4    5      6     7
New York    8    9     10    11

跟 Series 一样,轴索引也有一个 map 方法:

In [90]: transform = lambda x: x[:].upper()

In [91]: data.index.map(transform)
Out[91]: Index(['OHIO', 'COLORADO', 'NEW YORK'], dtype='object')

你可以将其赋值给 index,这样就可以对 DataFrame 进行就地修改:

In [92]: data.index
Out[92]: Index(['Ohio', 'Colorado', 'New York'], dtype='object')

In [93]: data.index = data.index.map(transform)

In [94]: data
Out[94]:
          one  two  three  four
OHIO        0    1      2     3
COLORADO    4    5      6     7
NEW YORK    8    9     10    11

如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是 rename:

In [95]: data.rename(index=str.title, columns=str.upper)
Out[95]:
          ONE  TWO  THREE  FOUR
Ohio        0    1      2     3
Colorado    4    5      6     7
New York    8    9     10    11

特别说明一下,rename 可以结合字典型对象实现对部分轴标签的更新:

In [96]: data.rename(index={'OHIO': 'INDIANA'},
    ...:    ....:             columns={'three': 'peekaboo'})
    ...:
Out[96]:
          one  two  peekaboo  four
INDIANA     0    1         2     3
COLORADO    4    5         6     7
NEW YORK    8    9        10    11

rename 可以实现复制 DataFrame 并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True 即可:

In [102]: data.rename(index={'OHIO': 'INDIANA'}, inplace=True)

In [103]: data
Out[103]:
          one  two  three  four
INDIANA     0    1      2     3
COLORADO    4    5      6     7
NEW YORK    8    9     10    11

离散化和面元划分

为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:

In [98]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用 pandas 的cut函数:

In [99]: bins = [18, 25, 35, 60, 100]

In [100]: cats = pd.cut(ages, bins)

In [101]: cats
Out[101]:
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35,
 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

pandas 返回的是一个特殊的 Categorical 对象。结果展示了 pandas.cut 划分的面元。你可以将其看做一组表示面元名称的字符串。它的底层含有一个表示不同分类名称的类型数组,以及一个 codes属性中的年龄数据的标签:

In [104]: cats.codes
Out[104]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)

In [105]: cats.categories
Out[105]:
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
              closed='right',
              dtype='interval[int64]')

In [106]: pd.value_counts(cats)
Out[106]:
(18, 25]     5
(35, 60]     3
(25, 35]     3
(60, 100]    1
dtype: int64

pd.value_counts(cats)pandas.cut结果的面元计数。

跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:

In [107]: pd.cut(ages, [18, 26, 36, 61, 100], right=False)
Out[107]:
[[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36,
 61), [36, 61), [26, 36)]
Length: 12
Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]

你可以通过传递一个列表或数组到 labels,设置自己的面元名称:

In [108]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']

In [109]: pd.cut(ages, bins, labels=group_names)
Out[109]:
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, Mid
dleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]

如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。下面这个例子中,我们将一些均匀分布的数据分成四组:

In [110]: data = np.random.rand(20)

In [111]: pd.cut(data, 4, precision=2)
Out[111]:
[(0.73, 0.96], (0.028, 0.26], (0.26, 0.49], (0.49, 0.73], (0.73, 0.96], ..., (0.2
6, 0.49], (0.26, 0.49], (0.26, 0.49], (0.49, 0.73], (0.73, 0.96]]
Length: 20
Categories (4, interval[float64]): [(0.028, 0.26] < (0.26, 0.49] < (0.49, 0.73] <
 (0.73, 0.96]]

选项precision=2,限定小数只有两位。

  • qcut 是一个非常类似于 cut 的函数,它可以根据样本分位数对数据进行面元划分。
  • 根据数据的分布情况,cut 可能无法使各个面元中含有相同数量的数据点。而 qcut 由于使用的是样本分位数,因此可以得到大小基本相等的面元:
In [112]: data = np.random.randn(1000)

In [113]: cats = pd.qcut(data, 4)

In [114]: cats
Out[114]:
[(0.625, 2.842], (0.625, 2.842], (-3.054, -0.78], (0.625, 2.842], (-3.054, -0.78]
, ..., (-3.054, -0.78], (-3.054, -0.78], (-3.054, -0.78], (0.625, 2.842], (-3.054
, -0.78]]
Length: 1000
Categories (4, interval[float64]): [(-3.054, -0.78] < (-0.78, -0.0355] < (-0.0355
, 0.625] <
                                    (0.625, 2.842]]

In [115]: pd.value_counts(cats)
Out[115]:
(0.625, 2.842]      250
(-0.0355, 0.625]    250
(-0.78, -0.0355]    250
(-3.054, -0.78]     250
dtype: int64

与 cut 类似,你也可以传递自定义的分位数(0到1之间的数值,包含端点):

In [117]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
Out[117]:
[(-0.0355, 1.241], (1.241, 2.842], (-1.359, -0.0355], (1.241, 2.842], (-3.054, -1
.359], ..., (-3.054, -1.359], (-3.054, -1.359], (-1.359, -0.0355], (-0.0355, 1.24
1], (-1.359, -0.0355]]
Length: 1000
Categories (4, interval[float64]): [(-3.054, -1.359] < (-1.359, -0.0355] < (-0.03
55, 1.241] <
                                    (1.241, 2.842]
  • 本章稍后在讲解聚合和分组运算时会再次用到 cut 和 qcut,因为这两个离散化函数对分位和分组分析非常重要。

检测和过滤异常值

过滤或变换异常值(outlier)在很大程度上就是运用数组运算。来看一个含有正态分布数据的 DataFrame:

In [118]: data = pd.DataFrame(np.random.randn(1000, 4))

In [119]: data.describe()
Out[119]:
                 0            1            2            3
count  1000.000000  1000.000000  1000.000000  1000.000000
mean     -0.024284     0.000331    -0.033790    -0.005900
std       1.002445     1.030087     1.010808     1.027110
min      -3.253857    -3.951871    -2.916422    -3.060088
25%      -0.705493    -0.681622    -0.707278    -0.682978
50%      -0.006853     0.011623    -0.016127     0.009688
75%       0.643390     0.688746     0.671249     0.653106
max       3.096767     3.574762     3.291177     3.513211

假设你想要找出某列中绝对值大小超过 3 的值:

In [121]: col = data[2]

In [122]: col[np.abs(col) > 3]
Out[122]:
583    3.291177
Name: 2, dtype: float64

要选出全部含有“超过 3 或 -3的值”的行,你可以在布尔型 DataFrame 中使用any方法:

In [123]: data[(np.abs(data) > 3).any(1)]
Out[123]:
            0         1         2         3
583 -0.328060  0.512835  3.291177  0.649597
653 -0.388316  3.574762 -0.379249 -0.429550
706 -1.049828  0.397178 -0.011549 -3.060088
727 -3.082909 -0.310682  0.375530  0.242903
802 -3.253857  0.940367  0.040317  2.556581
856  0.779090 -3.239075  0.596105 -0.218275
901 -0.116137 -0.810662  0.290593  3.513211
903  0.125236 -3.951871 -0.210753 -1.271565
911  3.096767  1.309234  2.114700 -1.847553

根据这些条件,就可以对值进行设置。下面的代码可以将值限制在区间 -3 到 3 以内:

In [127]: data[(np.abs(data) > 3)] = np.sign(data) * 3

In [128]: data.describe()
Out[128]:
                 0            1            2            3
count  1000.000000  1000.000000  1000.000000  1000.000000
mean     -0.024044     0.000947    -0.034082    -0.006353
std       1.001111     1.024294     1.009890     1.025300
min      -3.000000    -3.000000    -2.916422    -3.000000
25%      -0.705493    -0.681622    -0.707278    -0.682978
50%      -0.006853     0.011623    -0.016127     0.009688
75%       0.643390     0.688746     0.671249     0.653106
max       3.000000     3.000000     3.000000     3.000000

根据数据的值是正还是负,np.sign(data)可以生成 1 和 -1:

In [129]: np.sign(data).head()
Out[129]:
     0    1    2    3
0  1.0  1.0 -1.0 -1.0
1  1.0  1.0  1.0 -1.0
2 -1.0 -1.0 -1.0 -1.0
3  1.0 -1.0  1.0  1.0
4 -1.0 -1.0 -1.0 -1.0

排列和随机采样

利用numpy.random.permutation函数可以轻松实现对 Series 或 DataFrame 的列的排列工作(permuting,随机重排序)。通过需要排列的轴的长度调用permutation,可产生一个表示新顺序的整数数组:

In [130]: df = pd.DataFrame(np.arange(5*4).reshape((5, 4)))

In [131]: df
Out[131]:
    0   1   2   3
0   0   1   2   3
1   4   5   6   7
2   8   9  10  11
3  12  13  14  15
4  16  17  18  19

In [132]: sampler = np.random.permutation(5)

In [133]: sampler
Out[133]: array([2, 0, 4, 1, 3])

In [134]: sampler1 = np.random.permutation(5)

In [135]: sampler1
Out[135]: array([0, 1, 3, 2, 4])

然后就可以在基于iloc的索引操作或take函数中使用该数组了:

In [136]: df
Out[136]:
    0   1   2   3
0   0   1   2   3
1   4   5   6   7
2   8   9  10  11
3  12  13  14  15
4  16  17  18  19

In [138]: df.take(sampler)
Out[138]:
    0   1   2   3
2   8   9  10  11
0   0   1   2   3
4  16  17  18  19
1   4   5   6   7
3  12  13  14  15

In [139]: df.take(sampler1)
Out[139]:
    0   1   2   3
0   0   1   2   3
1   4   5   6   7
3  12  13  14  15
2   8   9  10  11

如果不想用替换的方式选取随机子集,可以在 Series 和 DataFrame 上使用 sample 方法:

In [140]: df.sample(n=3)
Out[140]:
   0  1   2   3
1  4  5   6   7
0  0  1   2   3
2  8  9  10  11

要通过替换的方式产生样本(允许重复选择),可以传递 replace =True到sample:

In [141]: choices = pd.Series([5, 7, -1, 6, 4])

In [142]: draws = choices.sample(n=10, replace=True)

In [143]: draws
Out[143]:
2   -1
1    7
0    5
0    5
4    4
3    6
0    5
2   -1
0    5
4    4
dtype: int64

计算指标/哑变量

  • 另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。

  • 如果 DataFrame 的某一列中含有 k 个不同的值,则可以派生出一个 k 列矩阵或 DataFrame (其值全为 1 和 0)。 pandas 有一个 get_dummies 函数可以实现该功能(其实自己动手做一个也不难)。使用之前的一个 DataFrame 例子:

In [144]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
     ...:    .....:                    'data1': range(6)})
     ...:

In [145]: pd.get_dummies(df['key'])
Out[145]:
   a  b  c
0  0  1  0
1  0  1  0
2  1  0  0
3  0  0  1
4  1  0  0
5  0  1  0

In [146]: df
Out[146]:
   data1 key
0      0   b
1      1   b
2      2   a
3      3   c
4      4   a
5      5   b

有时候,你可能想给指标 DataFrame 的列加上一个前缀,以便能够跟其他数据进行合并。 get_dummiesprefix参数可以实现该功能:

In [147]: dummies = pd.get_dummies(df['key'], prefix='key')

In [148]: df_with_dummy = df[['data1']].join(dummies)

In [149]: df_with_dummy
Out[149]:
   data1  key_a  key_b  key_c
0      0      0      1      0
1      1      0      1      0
2      2      1      0      0
3      3      0      0      1
4      4      1      0      0
5      5      0      1      0


In [151]: df
Out[151]:
   data1 key
0      0   b
1      1   b
2      2   a
3      3   c
4      4   a
5      5   b

In [152]: df_with_dummy1 = df[['key']].join(dummies)

In [153]: df_with_dummy1
Out[153]:
  key  key_a  key_b  key_c
0   b      0      1      0
1   b      0      1      0
2   a      1      0      0
3   c      0      0      1
4   a      1      0      0
5   b      0      1      0

In [154]: df_with_dummy2 = df[['data1']].join(df[['key']].join(dummies))

In [155]: df_with_dummy2
Out[155]:
   data1 key  key_a  key_b  key_c
0      0   b      0      1      0
1      1   b      0      1      0
2      2   a      1      0      0
3      3   c      0      0      1
4      4   a      1      0      0
5      5   b      0      1      0

如果 DataFrame 中的某行同属于多个分类,则事情就会有点复杂。看一下 MovieLens 1M 数据集,14章会更深入地研究它:

In [156]: mnames = ['movie_id', 'title', 'genres']
     ...:

In [158]: movies[:10]
Out[158]:
   movie_id                               title                        genres
0         1                    Toy Story (1995)   Animation|Children's|Comedy
1         2                      Jumanji (1995)  Adventure|Children's|Fantasy
2         3             Grumpier Old Men (1995)                Comedy|Romance
3         4            Waiting to Exhale (1995)                  Comedy|Drama
4         5  Father of the Bride Part II (1995)                        Comedy
5         6                         Heat (1995)         Action|Crime|Thriller
6         7                      Sabrina (1995)                Comedy|Romance
7         8                 Tom and Huck (1995)          Adventure|Children's
8         9                 Sudden Death (1995)                        Action
9        10                    GoldenEye (1995)     Action|Adventure|Thriller

要为每个 genre 添加指标变量就需要做一些数据规整操作。首先,我们从数据集中抽取出不同的genre 值:

In [159]: all_genres = []

In [160]: for x in movies.genres:
     ...:     all_genres.extend(x.split('|')) # x.split('|') 对 Action|Adventure|Thriller 切割分割
     ...:

In [161]: genres = pd.unique(all_genres)

In [162]: genres
Out[162]:
array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy',
       'Romance', 'Drama', 'Action', 'Crime', 'Thriller', 'Horror',
       'Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir',
       'Western'], dtype=object)

构建指标 DataFrame 的方法之一是从一个全零 DataFrame 开始:

In [164]: zero_matrix = np.zeros((len(movies), len(genres)))

In [165]: dummies = pd.DataFrame(zero_matrix, columns=genres)

In [166]: zero_matrix
Out[166]:
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.]])

In [167]: dummies
Out[167]:
      Animation  Children's  Comedy  Adventure  Fantasy  Romance  Drama  \
0           0.0         0.0     0.0        0.0      0.0      0.0    0.0
1           0.0         0.0     0.0        0.0      0.0      0.0    0.0
2           0.0         0.0     0.0        0.0      0.0      0.0    0.0
3           0.0         0.0     0.0        0.0      0.0      0.0    0.0
4           0.0         0.0     0.0        0.0      0.0      0.0    0.0
5           0.0         0.0     0.0        0.0      0.0      0.0    

...         ...         ...     ...        ...      ...      ...    ...

3873        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3874        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3875        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3876        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3877        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3878        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3879        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3880        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3881        0.0         0.0     0.0        0.0      0.0      0.0    0.0
3882        0.0         0.0     0.0        0.0      0.0      0.0    0.0

      Action  Crime  Thriller  Horror  Sci-Fi  Documentary  War  Musical  \
0        0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
1        0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
2        0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3        0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
4        0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
5        0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0

...      ...    ...       ...     ...     ...          ...  ...      ...

3874     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3875     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3876     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3877     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3878     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3879     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3880     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3881     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0
3882     0.0    0.0       0.0     0.0     0.0          0.0  0.0      0.0

      Mystery  Film-Noir  Western
0         0.0        0.0      0.0
1         0.0        0.0      0.0
2         0.0        0.0      0.0
3         0.0        0.0      0.0
4         0.0        0.0      0.0
5         0.0        0.0      0.0
6         0.0        0.0      0.0
7         0.0        0.0      0.0
8         0.0        0.0      0.0
9         0.0        0.0      0.0

...       ...        ...      ...

3873      0.0        0.0      0.0
3874      0.0        0.0      0.0
3875      0.0        0.0      0.0
3876      0.0        0.0      0.0
3877      0.0        0.0      0.0
3878      0.0        0.0      0.0
3879      0.0        0.0      0.0
3880      0.0        0.0      0.0
3881      0.0        0.0      0.0
3882      0.0        0.0      0.0

[3883 rows x 18 columns]

现在,迭代每一部电影,并将 dummies 各行的条目设为1。要这么做,我们使用dummies.columns来计算每个类型的列索引:

In [168]: gen = movies.genres[0]

In [169]: gen.split('|')
Out[169]: ['Animation', "Children's", 'Comedy']

In [170]: dummies.columns.get_indexer(gen.split('|'))
Out[170]: array([0, 1, 2], dtype=int64)

然后,根据索引,使用.iloc设定值:

In [171]: for i, gen in enumerate(movies.genres):
     ...:     indices = dummies.columns.get_indexer(gen.split('|'))
     ...:     dummies.iloc[i, indices] = 1

然后,和以前一样,再将其与movies合并起来:

In [173]: movies_windic = movies.join(dummies.add_prefix('Genre_'))

In [174]: movies_windic.iloc[0]
Out[174]:
movie_id                                       1
title                           Toy Story (1995)
genres               Animation|Children's|Comedy
Genre_Animation                                1
Genre_Children's                               1
Genre_Comedy                                   1
Genre_Adventure                                0
Genre_Fantasy                                  0
Genre_Romance                                  0
Genre_Drama                                    0
Genre_Action                                   0
Genre_Crime                                    0
Genre_Thriller                                 0
Genre_Horror                                   0
Genre_Sci-Fi                                   0
Genre_Documentary                              0
Genre_War                                      0
Genre_Musical                                  0
Genre_Mystery                                  0
Genre_Film-Noir                                0
Genre_Western                                  0
Name: 0, dtype: object

  • 笔记:对于很大的数据,用这种方式构建多成员指标变量就会变得非常慢。最好使用更低级的函数,将其写入 NumPy 数组,然后结果包装在 DataFrame 中。

  • 一个对统计应用有用的秘诀是:结合 get_dummies 和诸如cut之类的离散化函数:

In [179]: np.random.seed(12345)

In [180]: values = np.random.rand(10)

In [181]: values
Out[181]:
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
       0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])

In [182]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]

In [183]: pd.get_dummies(pd.cut(values, bins))
Out[183]:
   (0.0, 0.2]  (0.2, 0.4]  (0.4, 0.6]  (0.6, 0.8]  (0.8, 1.0]
0           0           0           0           0           1
1           0           1           0           0           0
2           1           0           0           0           0
3           0           1           0           0           0
4           0           0           1           0           0
5           0           0           1           0           0
6           0           0           0           0           1
7           0           0           0           1           0
8           0           0           0           1           0
9           0           0           0           1           0

我们用numpy.random.seed,使这个例子具有确定性。本书后面会介绍 pandas.get_dummies

7.3 字符串操作

  • Python 能够成为流行的数据处理语言,部分原因是其简单易用的字符串和文本处理功能。大部分文本运算都直接做成了字符串对象的内置方法。
  • 对于更为复杂的模式匹配和文本操作,则可能需要用到正则表达式。 pandas 对此进行了加强,它使你能够对整组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。

字符串对象方法

对于许多字符串处理和脚本应用,内置的字符串方法已经能够满足要求了。例如,以逗号分隔的字符串可以用split拆分成数段:

In [184]: val = 'a,b,  guido'

In [185]: val.split(',')
Out[185]: ['a', 'b', '  guido']

split常常与 strip一起使用,以去除空白符(包括换行符):

n [187]: pieces = [x.strip() for x in val.split(',')]

In [188]: pieces
Out[188]: ['a', 'b', 'guido']

利用加法,可以将这些子字符串以双冒号分隔符的形式连接起来:

In [189]: first, second, third = pieces

In [190]: first + '::' + second + '::' + third
Out[190]: 'a::b::guido'

但这种方式并不是很实用。一种更快更符合 Python 风格的方式是,向字符串”::”的join方法传入一个列表或元组:

In [191]: '::'.join(pieces)
Out[191]: 'a::b::guido'

其它方法关注的是子串定位。检测子串的最佳方式是利用 Python 的 in 关键字,还可以使用indexfind

In [192]: 'guido' in val
Out[192]: True

In [193]: val.index(',')
Out[193]: 1

In [194]: val.find(':')
Out[194]: -1

注意 find 和 index 的区别:如果找不到字符串,index 将会引发一个异常(而不是返回-1):

In [195]: val.index(':')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
input-195-2c016e7367ac> in <module>()
----> 1 val.index(':')

ValueError: substring not found

与此相关, count 可以返回指定子串的出现次数:

In [196]: val.count(',')
Out[196]: 2

replace 用于将指定模式替换为另一个模式。通过传入空字符串,它也常常用于删除模式:

In [197]: val.replace(',', '::')
Out[197]: 'a::b::  guido'

In [198]: val.replace(',','')
Out[198]: 'ab  guido'

表7-3 列出了 Python 内置的字符串方法。

这些运算大部分都能使用正则表达式实现(马上就会看到)。

Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备_第3张图片

Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备_第4张图片

casefold 将字符转换为小写,并将任何特定区域的变量字符组合转换成一个通用的可比较形式。

正则表达式

正则表达式提供了一种灵活的在文本中搜索或匹配(通常比前者复杂)字符串模式的方式。正则表达式,常称作 regex ,是根据正则表达式语言编写的字符串。 Python 内置的 re 模块负责对字符串应用正则表达式。我将通过一些例子说明其使用方法。

  • 笔记:正则表达式的编写技巧可以自成一章,超出了本书的范围。从网上和其它书可以找到许多非常不错的教程和参考资料。

  • re 模块的函数可以分为三个大类:模式匹配、替换以及拆分。当然,它们之间是相辅相成的。一个 regex 描述了需要在文本中定位的一个模式,它可以用于许多目的。

  • 我们先来看一个简单的例子:假设我想要拆分一个字符串,分隔符为数量不定的一组空白符(制表符、空格、换行符等)。描述一个或多个空白符的 regex 是 \s+
In [1]: import re

In [2]: text = "foo   bar\t baz   \t qux"

In [3]: re.split('\s+', text)
Out[3]: ['foo', 'bar', 'baz', 'qux']

调用 re.split('\s+',text)时,正则表达式会先被编译,然后再在text上调用其split方法。你可以用re.compile自己编译 regex 以得到一个可重用的 regex 对象:

In [4]: regex = re.compile('\s+')

In [5]: regex.split(text)
Out[5]: ['foo', 'bar', 'baz', 'qux']

如果只希望得到匹配 regex 的所有模式,则可以使用findall方法:

In [6]: regex.findall(text)
Out[6]: ['    ', '\t ', '  \t']

笔记:如果想避免正则表达式中不需要的转义(\),则可以使用原始字符串字面量如r’C:\x’(也可以编写其等价式’C:\x’)。

如果打算对许多字符串应用同一条正则表达式,强烈建议通过 re.compile 创建 regex 对象。这样将可以节省大量的 CPU 时间。

match 和 search 跟 findall 功能类似。

  • findall 返回的是字符串中所有的匹配项,
  • 而 search 则只返回第一个匹配项。
  • match 更加严格,它只匹配字符串的首部。

来看一个小例子,假设我们有一段文本以及一条能够识别大部分电子邮件地址的正则表达式:

In [7]: text = """Dave [email protected]
   ...: Steve [email protected]
   ...: Rob [email protected]
   ...: Ryan [email protected]
   ...: """

In [8]: pattern = r'[A-Z0-9.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'

In [9]: # re.IGNORECASE makes the regex case-insensitive

In [10]: regex = re.compile(pattern, flags=re.IGNORECASE)


对 text 使用 findall 将得到一组电子邮件地址:


In [11]: regex.findall(text)
Out[11]: ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']

search 返回的是文本中第一个电子邮件地址(以特殊的匹配项对象形式返回)。对于上面那个 regex ,匹配项对象只能告诉我们模式在原字符串中的起始和结束位置:

In [12]: m = regex.search(text)

In [13]: m
Out[13]: <_sre.SRE_Match object; span=(5, 20), match='[email protected]'>

In [14]: text[m.start():m.end()]
Out[14]: '[email protected]'

regex .match则将返回 None ,因为它只匹配出现在字符串开头的模式:

In [15]: print(regex.match(text))
None

相关的,sub 方法可以将匹配到的模式替换为指定字符串,并返回所得到的新字符串:

In [16]: print(regex.sub('REDACTED', text))
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED

假设你不仅想要找出电子邮件地址,还想将各个地址分成3个部分:用户名、域名以及域后缀。要实现此功能,只需将待分段的模式的各部分用圆括号包起来即可:

In [23]: pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'

In [24]: regex = re.compile(pattern, flags=re.IGNORECASE)

由这种修改过的正则表达式所产生的匹配项对象,可以通过其 groups 方法返回一个由模式各段组成的元组:

In [25]: m = regex.match('[email protected]')

In [26]: m.groups()
Out[26]: ('wesm', 'bright', 'net')

对于带有分组功能的模式,findall 会返回一个元组列表:

In [6]:
In [27]: regex.findall(text)
Out[27]:
[('dave', 'google', 'com'),
 ('steve', 'gmail', 'com'),
 ('rob', 'gmail', 'com'),
 ('ryan', 'yahoo', 'com')]

sub还能通过诸如\1、\2之类的特殊符号访问各匹配项中的分组。符号\1对应第一个匹配的组,\2对应第二个匹配的组,以此类推:

In [28]: print(regex.sub(r'Username: \1, Domain: \2, Suffix: \3', text))
Dave Username: dave, Domain: google, Suffix: com
Steve Username: steve, Domain: gmail, Suffix: com
Rob Username: rob, Domain: gmail, Suffix: com
Ryan Username: ryan, Domain: yahoo, Suffix: com

Python 中还有许多的正则表达式,但大部分都超出了本书的范围。表7-4是一个简要概括。

Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备_第5张图片

pandas 的矢量化字符串函数

清理待分析的散乱数据时,常常需要做一些字符串规整化工作。更为复杂的情况是,含有字符串的列有时还含有缺失数据:

In [33]: import numpy as np

In [34]: import pandas as pd

In [35]:  data = {'Dave': '[email protected]', 'Steve': '[email protected]',
    ...:    .....:         'Rob': '[email protected]', 'Wes': np.nan}
    ...:

In [36]:

In [36]: data = pd.Series(data)

In [37]: data
Out[37]:
Dave     [email protected]
Rob        [email protected]
Steve    [email protected]
Wes                  NaN
dtype: object

In [38]: data.isnull()
Out[38]:
Dave     False
Rob      False
Steve    False
Wes       True
dtype: bool

通过data.map,所有字符串和正则表达式方法都能被应用于(传入lambda表达式或其他函数)各个值,但是如果存在 NA (null)就会报错。为了解决这个问题, Series 有一些能够跳过 NA 值的面向数组方法,进行字符串操作。通过 Series 的 str 属性即可访问这些方法。例如,我们可以通过 str .contains检查各个电子邮件地址是否含有”gmail”:

In [40]: data.str.contains('gmail')
Out[40]:
Dave     False
Rob       True
Steve     True
Wes        NaN
dtype: object

也可以使用正则表达式,还可以加上任意 re 选项(如 IGNORECASE):

In [41]: pattern
Out[41]: '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'

In [42]: data.str.findall(pattern, flags=re.IGNORECASE)
Out[42]:
Dave     [(dave, google, com)]
Rob        [(rob, gmail, com)]
Steve    [(steve, gmail, com)]
Wes                        NaN
dtype: object

有两个办法可以实现矢量化的元素获取操作:要么使用 str .get,要么在 str 属性上使用索引:

In [43]: matches = data.str.match(pattern, flags=re.IGNORECASE)

In [44]: matches
Out[44]:
Dave     True
Rob      True
Steve    True
Wes       NaN
dtype: object

要访问嵌入列表中的元素,我们可以传递索引到这两个函数中:

In [45]: matches.str.get(1)
Out[45]:
Dave    NaN
Rob     NaN
Steve   NaN
Wes     NaN
dtype: float64

In [46]: matches.str[0]
Out[46]:
Dave    NaN
Rob     NaN
Steve   NaN
Wes     NaN
dtype: float64

你可以利用这种方法对字符串进行截取:

In [47]: data.str[:5]
Out[47]:
Dave     dave@
Rob      rob@g
Steve    steve
Wes        NaN
dtype: object

表7-5介绍了更多的 pandas 字符串方法。

Python for Data Analysis v2 | Notes_ Chapter_7 数据清洗和准备_第6张图片

7.4 总结

高效的数据准备可以让你将更多的时间用于数据分析,花较少的时间用于准备工作,这样就可以极大地提高生产力。我们在本章中学习了许多工具,但覆盖并不全面。下一章,我们会学习 pandas 的聚合与分组。

你可能感兴趣的:(Python,学习笔记)