1. 程式人生 > >第七篇 資料清洗和準備

第七篇 資料清洗和準備

在資料分析和建模的過程中,要花很多時間在資料準備上:載入、清理、轉換以及重塑。這些⼯作會佔到分析師時間的80%或更多。有時,儲存在⽂件和資料庫中的資料的格式不適合某個特定的任務。pandas和內建的Python標準庫提供了⼀組⾼級的、靈活的、快速的⼯具,可以讓你輕鬆地將資料規變為想要的格式。接下來會討論處理缺失資料、重複資料、字串操作和其它分析資料轉換的⼯具。

一、處理缺失資料


pandas的⽬標之⼀就是儘量輕鬆地處理缺失資料。例如,pandas物件的所有描述性統計預設都不包括缺失資料。

缺失資料在pandas中呈現的⽅式有些不完美,但對於⼤多數⽤戶可以保證功能正常。對於數值資料,pandas使⽤浮點值NaN(Not a Number)表示缺失資料。我們稱其為哨兵值,可以⽅便的檢測出來:


string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
string_data   # 輸出如下:
0    aardvark
1    artichoke
2    NaN
3    avocado
dtype: object
string_data.isnull()  # 輸出如下:
0    False
1    False
2    True
3    False
dtype: bool

在pandas中,將缺失值表示為NA,它表示不可⽤not available。在統計應⽤中,NA資料可能是不存在的資料或者雖然存在,但是沒有觀察到(例如,資料採集中發⽣了問題)。當進⾏資料清洗以進⾏分析時,最好直接對缺失資料進⾏分析,以判斷資料採集的問題或缺失資料可能導致的偏差。Python內建的None

值在物件陣列中也可以作為NA
string_data[0] = None
string_data.isnull()    # 輸出如下:
0    True
1    False
2    True
3    False
dtype: bool
pandas項⽬中還在不斷優化內部細節以更好處理缺失資料,像⽤戶API功能,例如pandas.isnull,去除了許多惱⼈的細節。表7-1列出了⼀些關於缺失資料處理的函式。

表7-1 NA處理⽅法

 

1、濾除缺失資料
過濾掉缺失資料的辦法有很多種。可以通過pandas.isnull或布林索引的⼿⼯⽅法,但dropna可能會更實⽤⼀些。對於⼀個Series,dropna返回⼀個僅含⾮空資料和索引值的Series:


from numpy import nan as NA
data = pd.Series([1, NA, 3.5, NA, 7])
data.dropna()    # 輸出如下:
0    1.0
2    3.5
4    7.0
dtype: float64
這等價於:
data[data.notnull()]    # 輸出如下:
0    1.0
2    3.5
4    7.0
dtype: float64

⽽對於DataFrame物件,事情就有點複雜了。你可能希望丟棄全NA或含有NA的⾏或列
dropna預設丟棄任何含有缺失值的⾏:
data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA], [NA, NA, NA], [NA, 6.5, 3.]])
cleaned = data.dropna()   # 刪除有缺失值的行
data      # data原始資料輸出如下:
    0     1       2
0    1.0    6.5    3.0
1    1.0    NaN       NaN
2    NaN     NaN    NaN
3    NaN     6.5    3.0
cleaned    # 刪除全部有缺失值的行後輸出如下:
    0       1       2
0    1.0    6.5    3.0
傳⼊how='all'將只丟棄全為NA的那些⾏:
data.dropna(how='all')     # 刪除全為NA的行
    0       1       2
0    1.0    6.5    3.0
1    1.0    NaN    NaN
3    NaN    6.5    3.0
⽤這種⽅式丟棄全為NA的列,只需傳⼊axis=1即可:
data[4] = NA    # 在data陣列中新增第4列全為NA
data      # 輸出如下:
    0       1         2       4
0    1.0    6.5      3.0   NaN
1    1.0    NaN    NaN      NaN
2    NaN    NaN    NaN      NaN
3    NaN    6.5      3.0        NaN
data.dropna(axis=1, how='all')    # 注意引數的組合使用,輸出如下:
    0     1     2
0    1.0    6.5      3.0
1    1.0    NaN    NaN
2    NaN    NaN    NaN
3    NaN    6.5      3.0

另⼀個濾除DataFrame⾏的問題涉及時間序列資料。假設你只想留下⼀部分觀測資料,可以⽤thresh引數實現此⽬的:
df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = NA    # 前面4行的第2列設定為NA
df.iloc[:2, 2] = NA    # 前面2行的第3列設定為NA
df             # 輸出如下:
         0        1          2
0   -0.662826      NaN       NaN
1   -0.020137      NaN       NaN
2   -0.442894      NaN     0.732485
3    0.820674      NaN     0.292754
4    1.498702    0.643710    0.320940
5   -0.742214   -0.941152    0.895269
6   -1.220743    0.567435    0.490752
df.dropna()      # 刪除全部有NA值的行,輸出如下:
        0         1          2
4    1.498702    0.643710    0.320940
5   -0.742214   -0.941152    0.895269
6   -1.220743    0.567435    0.490752
df.dropna(thresh=2)    # 刪除2列都有NA值的行,輸出如下:
        0         1        2
2   -0.442894      NaN      0.732485
3    0.820674      NaN      0.292754
4    1.498702    0.643710     0.320940
5   -0.742214   -0.941152    0.895269
6   -1.220743   0.567435     0.490752

2、填充缺失資料

不刪除缺失資料,因為可能丟棄跟它有關的其他資料。而是採用填充那缺失資料。fillna⽅法是最主要的函式。通過⼀個常數調⽤fillna就會將缺失值替換為那個常數值:
df.fillna(0)      # 輸出如下:缺失值填充0
         0           1          2
0   -0.662826    0.000000    0.000000
1   -0.020137    0.000000    0.000000
2   -0.442894    0.000000    0.732485
3    0.820674    0.000000    0.292754
4    1.498702    0.643710    0.320940
5   -0.742214   -0.941152    0.895269
6   -1.220743    0.567435    0.490752
若是通過⼀個字典調⽤fillna,就可以實現對不同的列填充不同的值:
df.fillna({1: 0.5, 2: 0})      # 第2列的NA值填充為0.5,第3列的NA值填充為0。輸出如下:
         0         1         2
0   -0.662826    0.500000    0.000000
1   -0.020137    0.500000    0.000000
2   -0.442894    0.500000    0.732485
3    0.820674    0.500000    0.292754
4    1.498702    0.643710    0.320940
5   -0.742214   -0.941152    0.895269
6   -1.220743    0.567435    0.490752
fillna預設會返回新物件,但也可以對現有物件進⾏就地修改(引數inplace=True):
_ = df.fillna(0, inplace=True)   # 就地修改,引數inplace=True
df      # 輸出如下:
         0         1         2
0   -0.662826    0.500000    0.000000
1   -0.020137    0.500000    0.000000
2   -0.442894    0.500000    0.732485
3    0.820674    0.500000    0.292754
4    1.498702    0.643710    0.320940
5   -0.742214   -0.941152    0.895269
6   -1.220743    0.567435    0.490752

reindexing有效的那些插值⽅法也可⽤於fillna:
df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA
df      # 輸出如下:
         0         1            2
0    0.565130    0.430439     0.431819
1   -1.712032   -0.492103   -0.997918
2    0.241762      NaN     0.639279
3   -1.636311      NaN    -0.106550
4   -0.892301      NaN      NaN
5    0.116147      NaN      NaN
df.fillna(method='ffill')   # 輸出如下:根據列中NA值前的一個數字填充後面的NA值
         0            1           2
0    0.565130     0.430439   0.431819
1   -1.712032   -0.492103   -0.997918
2    0.241762   -0.492103    0.639279
3   -1.636311   -0.492103   -0.106550
4   -0.892301   -0.492103   -0.106550
5    0.116147   -0.492103   -0.106550
df.fillna(method='ffill', limit=2)      # 限制填充2個NA值
         0            1           2
0    0.565130     0.430439   0.431819
1   -1.712032   -0.492103   -0.997918
2    0.241762   -0.492103    0.639279
3   -1.636311   -0.492103   -0.106550
4   -0.892301      NaN   -0.106550
5    0.116147       NaN   -0.106550
只要做一些創新,你就可以利⽤fillna實現許多別的功能。⽐如,可以傳⼊Series的平均值或中位數:
data.fillna(data.mean())   # 用平均值填充NA值,輸出如下:
0    1.000000
1    3.833333
2    3.500000
3    3.833333
4    7.000000
dtype: float64

表7-2列出了fillna的引數

二、資料轉換
資料的另一類重要操作是過濾、清理以及其他的轉換⼯作。

1、移除重複資料
DataFrame中出現重複⾏有多種原因。下⾯就是⼀個例⼦:
data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'], 'k2': [1, 1, 2, 3, 3, 4, 4]})
data    # 輸出如下:
    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,表示各⾏是否是重複⾏(前⾯出現過的⾏):
data.duplicated()   # 後面的一行與前面的一行相同則是重複行。輸出如下:
0    False
1    False
2    False
3    False
4    False
5    False
6    True
dtype: bool
還有⼀個與此相關的drop_duplicates⽅法,它會返回⼀個DataFrame,重複的陣列會標為False:
data.drop_duplicates()   # 刪除重複的行(後面的一行與前面的一行相同則是重複行)。輸出如下:
    k1    k2
0     one    1
1     two    1
2       one    2
3    two    3
4    one    3
5    two    4


這兩個⽅法預設會判斷全部列,你也可以指定部分列進⾏重複項判斷。假設我們還有⼀列值,且只希望根據k1列過濾重複項:
data['v1'] = range(7)    # 在data中新增v1列
data.drop_duplicates(['k1'])   # 輸出如下:
    k1    k2     v1
0    one    1    0
1    two    1    1
duplicated和drop_duplicates預設保留的是第⼀個出現的值組合。傳⼊keep='last'則保留最後⼀個
data.drop_duplicates(['k1', 'k2'], keep='last')   # 輸出如下:(注意沒有index=5的行)
    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

2、利⽤函式或對映進⾏資料轉換
對於許多資料集,可能希望根據陣列、Series或DataFrame列中的值來實現轉換⼯作。我們來看看下⾯這組有關⾁類的資料:
data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon',
              'Pastrami', 'corned beef', 'Bacon',
              'pastrami', 'honey ham', 'nova lox'],
          'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
data      # 原始資料如下所示:
        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
假設你想要新增⼀列表示該⾁類⻝物來源的動物型別。我們先編寫⼀個不同⾁類到動物的對映:
meat_to_animal = {
      'bacon': 'pig',
      'pulled pork': 'pig',
      'pastrami': 'cow',
      'corned beef': 'cow',
      'honey ham': 'pig',
      'nova lox': 'salmon'
      }
Series的map⽅法可以接受⼀個函式或含有對映關係的字典型物件,但是這⾥有⼀個⼩問題,即有些⾁類的⾸字⺟⼤寫了,⽽另⼀些則沒有。因此,我們還需要使⽤Series的str.lower⽅法,將各個值轉換為⼩寫:
data['animal'] = lowercased.map(meat_to_animal)   # 注意對映使用方法
data      # 輸出如下:
        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
我們也可以傳⼊⼀個能夠完成全部這些⼯作的函式:
data['food'].map(lambda x: meat_to_animal[x.lower()])    # 相當於:map(lambda x: meat_to_animal[x.lower()], data['food'])
0    pig
1    pig
2    pig
3    cow
4    cow
5    pig
6    cow
7    pig
8    salmon
Name: food, dtype: object
使⽤map是⼀種實現元素級轉換以及其他資料清理⼯作的便捷⽅式。

3、替換值
利⽤fillna⽅法填充缺失資料可以看做值替換的⼀種特殊情況。前⾯已經看到,map可⽤於修改物件的資料⼦集,⽽replace則提供了⼀種實現該功能的更簡單、更靈活的⽅式。我們來看看下⾯這個Series:
data = pd.Series([1., -999., 2., -999., -1000., 3.])
data      # 輸出如下:
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):
data.replace(-999, np.nan)   # 輸出如下:
0    1.0
1    NaN
2    2.0
3    NaN
4    -1000.0
5    3.0
dtype: float64
如果你希望⼀次性替換多個值,可以傳⼊⼀個由待替換值組成的列表以及⼀個替換值
data.replace([-999, -1000], np.nan)   # 輸出如下:
0    1.0
1    NaN
2    2.0
3    NaN
4    NaN
5    3.0
dtype: float64
要讓每個值有不同的替換值,可以傳遞⼀個替換列表
data.replace([-999, -1000], [np.nan, 0])   # 輸出如下:
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64
傳⼊的引數也可以是字典
data.replace({-999: np.nan, -1000: 0})   # 輸出如下:
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64
注意:data.replace⽅法與data.str.replace不同,後者做的是字串的元素級替換。

4、重新命名軸索引

跟Series中的值⼀樣,軸標籤也可以通過函式或對映進⾏轉換,從⽽得到⼀個新的不同標籤的物件。軸還可以被就地修改,⽽⽆需新建⼀個數據結構。接下來看看下⾯這個簡單的例⼦:
data = pd.DataFrame(np.arange(12).reshape((3, 4)),
      index=['Ohio', 'Colorado', 'New York'],
      columns=['one', 'two', 'three', 'four'])

跟Series⼀樣,軸索引也有⼀個map⽅法:
transform = lambda x: x[:4].upper()   # 定義規則
data.index.map(transform)        # 輸出:Index(['OHIO', 'COLO', 'NEW '], dtype='object')

你可以將其賦值給index,這樣就可以對DataFrame進⾏就地修改:
data.index = data.index.map(transform)
data      # 輸出如下:
     one   two   three   four
OHIO     0    1    2    3
COLO    4    5    6    7
NEW      8    9     10    11

如果想要建立資料集的轉換版(⽽不是修改原始資料),⽐較實⽤的⽅法是rename
data.rename(index=str.title, columns=str.upper)   # 輸出如下:注意傳的引數是函式名
    ONE   TWO   THREE  FOUR
Ohio    0    1    2    3
Colo    4    5    6    7
New    8    9    10    11
特別說明⼀下,rename可以結合字典型物件實現對部分軸標籤的更新:
data.rename(index={'OHIO': 'INDIANA'}, columns={'three': 'peekaboo'})   # 輸出如下:
       one     two  peekaboo  four
INDIANA     0    1    2    3
COLO      4    5    6    7
NEW        8    9   10   11
rename可以實現複製DataFrame並對其索引和列標籤進⾏賦值。如果希望就地修改某個資料集,傳⼊inplace=True即可:
data.rename(index={'COLO': 'CHENGDU'}, inplace=True)
data    # 輸出如下:
       one      two    three     four
OHIO      0    1    2    3
CHENGDU    4    5    6    7
NEW      8    9   10   11

5、離散化和⾯元劃分
為了便於分析,連續資料常常被離散化或拆分為“⾯元”(bin)。假設有⼀組⼈員資料,⽽你希望將它們劃分為不同的年齡組:
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
接下來將這些資料劃分為“18到25”、“26到35”、“35到60”以及“60以上”⼏個⾯元。要實現該功能,你需要使⽤pandas的cut函式:
bins = [18, 25, 35, 60, 100]  # 首先要指定範圍
cats = pd.cut(ages, bins)
cats      # 輸出如下:將每個年齡進行區間劃分
[(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屬性中的年齡資料的標籤:
cats.codes      # 輸出:array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
cats.categories    # 輸出如下:(區間劃分資訊)
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
      closed='right',
      dtype='interval[int64]')
pd.value_counts(cats)   # 輸出如下:
(18, 25]    5
(35, 60]    3
(25, 35]    3
(60, 100]    1
dtype: int64
pd.value_counts(cats)是pandas.cut結果的⾯元計數。

跟“區間”的數學符號⼀樣,圓括號表示開端,⽽⽅括號則表示閉端(包括)。哪邊是閉端可以通過right=False進⾏修改:
pd.cut(ages, [18, 26, 36, 61, 100], right=False)   # 輸出如下:
[[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,設定⾃⼰的⾯元名稱
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
pd.cut(ages, bins, labels=group_names)      # 輸出如下:
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]

如果向cut傳⼊的是⾯元的數量⽽不是確切的⾯元邊界,則它會根據資料的最⼩值和最⼤值計算等⻓⾯元。下⾯這個例⼦中,我們將⼀些均勻分佈的資料分成四組:
data = np.random.rand(20)
pd.cut(data, 4, precision=2)    # 輸出如下:平均分為4組
[(0.74, 0.98], (0.0089, 0.25], (0.49, 0.74], (0.0089, 0.25], (0.74, 0.98], ..., (0.0089, 0.25], (0.49, 0.74], (0.25, 0.49], (0.49, 0.74], (0.74, 0.98]]
Length: 20
Categories (4, interval[float64]): [(0.0089, 0.25] < (0.25, 0.49] < (0.49, 0.74] < (0.74, 0.98]]
選項precision=2,限定⼩數只有兩位。

qcut是⼀個⾮常類似於cut的函式,它可以根據樣本分位數對資料進⾏⾯元劃分。根據資料的分佈情況,cut可能⽆法使各個⾯元中含有相同數量的資料點。⽽qcut由於使⽤的是樣本分位數,因此可以得到⼤⼩基本相等的⾯元:
data = np.random.randn(1000)    # Normally distributed(正態分佈)
cats = pd.qcut(data, 4)           # Cut into quartiles
cats      # 輸出如下:
[(0.031, 0.66], (0.031, 0.66], (-2.58, -0.604], (-0.604, 0.031], (-0.604, 0.031], ..., (-2.58, -0.604], (-2.58, -0.604], (-2.58, -0.604], (0.031, 0.66], (0.66, 2.965]]
Length: 1000
Categories (4, interval[float64]): [(-2.58, -0.604] < (-0.604, 0.031] < (0.031, 0.66] < (0.66, 2.965]]
pd.value_counts(cats)   # 輸出如下:
(0.66, 2.965]       250
(0.031, 0.66]       250
(-0.604, 0.031]    250
(-2.58, -0.604]    250
dtype: int64
與cut類似,你也可以傳遞⾃定義的分位數(0到1之間的數值,包含端點):
pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])   # 輸出如下:
[(0.031, 1.352], (0.031, 1.352], (-2.58, -1.227], (-1.227, 0.031], (-1.227, 0.031], ..., (-2.58, -1.227], (-2.58, -1.227], (-2.58, -1.227], (0.031, 1.352], (0.031, 1.352]]
Length: 1000
Categories (4, interval[float64]): [(-2.58, -1.227] < (-1.227, 0.031] < (0.031, 1.352] < (1.352, 2.965]]
cut和qcut兩個離散化函式對分位和分組分析⾮常重要。聚合和分組運算時會再次⽤到。

6、檢測和過濾異常值
過濾或變換異常值(outlier)在很⼤程度上就是運⽤陣列運算。來看⼀個含有正態分佈資料的DataFrame:
data.describe()   # 輸出如下:
           0            1             2            3
count    1000.000000    1000.000000    1000.000000    1000.000000
mean       0.016706       0.028248      0.036608      -0.005333
std         0.967088       1.016612      0.995248       1.026645
min       -2.949776     -3.408071      -3.077886     -3.309142
25%      -0.629107     -0.660412      -0.649365     -0.744179
50%       0.009834      0.058910        0.020661      0.018473
75%       0.668249      0.705827        0.739391      0.683511
max        2.650242     3.560653         3.053767     3.010718
假設想要找出某列中絕對值⼤⼩超過3的值
col = data[2]     # 第3列
col[np.abs(col) > 3]   # 輸出如下:(找出絕對值大於3的數)
52    -3.077886
277      3.012002
932      3.053767
Name: 2, dtype: float64

要選出全部含有“超過3或-3的值”的⾏,你可以在布林型DataFrame中使⽤any⽅法
data[(np.abs(data) > 3).any(1)]    # 輸出如下:
        0           1            2         3
3       -0.842118    1.394323    0.310644   -3.309142
52      0.185749   -0.620026   -3.077886    0.148803
81      0.026080   -1.311505   -0.041084   -3.245929
146    0.977894    0.120866    1.006821    3.010718
277   -0.331066   -0.471784    3.012002   -1.350731
288   -0.656026   -3.408071   -0.955631    0.358787
564   -1.093666    3.560653    0.351178     1.185690
743    0.148074   -3.001641    0.815807    -1.031880
747    0.489386   -3.077059   -0.503121   -1.291354
800   -0.631084    3.461719    1.664846     0.999467
932    1.764187    0.683385    3.053767    -0.110174
根據這些條件,就可以對值進⾏設定。下⾯的程式碼可以將值限制在區間-3到3以內:
data[np.abs(data) > 3] = np.sign(data) * 3
data.describe()      # 輸出如下:
          0           1             2            3
count    1000.000000    1000.000000    1000.000000    1000.000000
mean       0.016706          0.027712        0.036620        -0.004789
std         0.967088          1.011813      0.994809              1.024915
min       -2.949776         -3.000000       -3.000000            -3.000000
25%      -0.629107         -0.660412       -0.649365            -0.744179
50%       0.009834           0.058910       0.020661              0.018473
75%       0.668249           0.705827       0.739391               0.683511
max        2.650242          3.000000        3.000000              3.000000
根據資料的值是正還是負,np.sign(data)可以⽣成1和-1
np.sign(data).head() # 輸出如下:
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

7、排列和隨機取樣
利⽤numpy.random.permutation函式可以輕鬆實現對Series或DataFrame的列的排列⼯作(permuting,隨機重排序)。通過需要排列的軸的⻓度調⽤permutation,可產⽣⼀個表示新順序的整數陣列:
df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))
sampler = np.random.permutation(5)
sampler      # 輸出:array([1, 3, 2, 4, 0])
然後就可以在基於iloc的索引操作或take函式中使⽤該陣列了:
df    # 輸出如下:
   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
df.take(sampler)     # 輸出如下:
   0     1      2       3
1    4     5      6      7
3     12     13    14    15
2    8     9    10    11
4     16   17    18    19
0    0     1       2     3
如果不想⽤替換的⽅式選取隨機⼦集,可以在Series和DataFrame上使⽤sample⽅法:
df.sample(3)    # 輸出如下:(隨機選取3行)
     0      1       2     3
3    12    13    14    15
2      8      9    10    11
4    16    17    18    19
要通過替換的⽅式產⽣樣本(允許重複選擇),可以傳遞replace=True到sample:
choices = pd.Series([5, 7, -1, 6, 4])
draws = choices.sample(n=10, replace=True)  # 對choices隨機選取10個數據,並允許重複
draws    # 輸出如下:
1    7
0    5
2    -1
4    4
4    4
3    6
2    -1
3    6
3    6
4    4
dtype: int64

8、計算指標/啞變數

另⼀種常⽤於統計建模或機器學習的轉換⽅式是:將分類變數(categorical variable)轉換為“啞變數”或“指標矩陣”
如果DataFrame的某⼀列中含有k個不同的值,則可以派⽣出⼀個k列矩陣或DataFrame(其值全為1和0)。pandas有⼀個get_dummies函式可以實現該功能(其實⾃⼰動⼿做⼀個也不難)。使⽤之前的⼀個DataFrame例⼦:
df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)})
pd.get_dummies(df['key'])   # 輸出如下:注意0和1出現的規律,1表示新的DataFrame列標籤在原DataFrame中的列出現位置
   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
有時候,你可能想給指標DataFrame的列加上⼀個字首,以便能夠跟其他資料進⾏合併get_dummies的prefix引數可以實現該功能:
dummies = pd.get_dummies(df['key'], prefix='key')
df_with_dummy = df[['data1']].join(dummies)
df_with_dummy    # 輸出如下:
  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

如果DataFrame中的某⾏同屬於多個分類,則事情就會有點複雜。看⼀下MovieLens 1M資料集,
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('datasets/movielens/movies.dat', sep='::', header=None, names=mnames)
movies[:10]     # 輸出如下:
  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值
all_genres = []
for x in movies.genres:
  all_genres.extend(x.split('|'))
genres = pd.unique(all_genres)    # 去重
現在有:
genres    # 輸出如下:
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開始
zero_matrix = np.zeros((len(movies), len(genres)))
dummies = pd.DataFrame(zero_matrix, columns=genres)  # 把zero_matrix轉換為DataFrame,列標籤是genres列表
現在,迭代每⼀部電影,並將dummies各⾏的條⽬設為1。要這麼做,我們使⽤dummies.columns來計算每個型別的列索引:
gen = movies.genres[0]
gen.split('|')     # 輸出:['Animation', "Children's", 'Comedy']
dummies.columns.get_indexer(gen.split('|'))   # 輸出:array([0, 1, 2], dtype=int64)
然後,根據索引,使⽤.iloc設定值:
for i, gen in enumerate(movies.genres):
  indices = dummies.columns.get_indexer(gen.split('|'))
  dummies.iloc[i, indices] = 1
然後,和以前⼀樣,再將其與movies合併起來:
movies_windic = movies.join(dummies.add_prefix('Genre'))
movies_windic.iloc[0]    # 輸出如下:
movie_id                    1
title                  Toy Story (1995)
genres        Animation|Children's|Comedy
GenreAnimation                 1
GenreChildren's                 1
    ...
GenreWar                     0
GenreMusical                    0
GenreMystery                 0
GenreFilm-Noir                  0
GenreWestern                   0
Name: 0, Length: 21, dtype: object

注意:對於很⼤的資料,⽤這種⽅式構建多成員指標變數就會變得⾮常慢。最好使⽤更低階的函式,將其寫⼊NumPy陣列,然後結果包裝在DataFrame中。

⼀個對統計應⽤有⽤的祕訣是:結合get_dummies和諸如cut之類的離散化函式:
np.random.seed(12345)
values = np.random.rand(10)
values      # 輸出如下:
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
     0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
pd.get_dummies(pd.cut(values, bins))   # 輸出如下:
  (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。

 

三、字串操作
Python能夠成為流⾏的資料處理語⾔,部分原因是其簡單易⽤的字串和⽂本處理功能。⼤部分⽂本運算都直接做成了字串物件的內建⽅法。對於更為複雜的模式匹配和⽂本操作,則可能需要⽤到正則表示式。pandas對此進⾏了加強,它使你能夠對整組資料應⽤字串表示式和正則表示式,⽽且能處理煩⼈的缺失資料。

1、字串物件⽅法
對於許多字串處理和指令碼應⽤,內建的字串⽅法已經能夠滿⾜要求。例如,以逗號分隔的字串可以⽤split拆分成數段:
val = 'a,b, guido'
val.split(',')    # 輸出:['a', 'b', ' guido']
split常常與strip⼀起使⽤,以去除空⽩符(包括換⾏符):
pieces = [x.strip() for x in val.split(',')]
pieces      # 輸出:['a', 'b', 'guido']
利⽤加法,可以將這些⼦字串以雙冒號分隔符的形式連線起來:

first, second, third = pieces
first + '::' + second + '::' + third    # 輸出:'a::b::guido'

但這種⽅式並不是很實⽤。⼀種更快更符合Python⻛格的⽅式是,向字串"::"的join⽅法傳⼊⼀個列表或元組
'::'.join(pieces)   # 輸出:'a::b::guido'

其它⽅法關注的是⼦串定位。檢測⼦串的最佳⽅式是利⽤Python的in關鍵字,還可以使⽤index和find:
'guido' in val   # 輸出:True
val.index(',')   # 輸出: 1
val.find(':')     # 輸出:-1
注意find和index的區別:如果找不到字串,index將會引發⼀個異常(⽽不是返回-1):
val.index(':')   # 引發一個異常:ValueError: substring not found
與此相關,count可以返回指定⼦串的出現次數:
val.count(',')   # 輸出:2

replace⽤於將指定模式替換為另⼀個模式。通過傳⼊空字串,它也常常⽤於刪除模式:
val.replace(',', '::') # 輸出:'a::b:: guido'
val.replace(',', '') # 輸出:'ab guido'

表7-3列出了Python內建的字串⽅法。這些運算⼤部分都能使⽤正則表示式實現(⻢上就會看到)。


2、正則表示式
正則表示式提供了⼀種靈活的在⽂本中搜尋或匹配(通常⽐前者複雜)字串模式的⽅式。正則表示式,常稱作regex,是根據正則表示式語⾔編寫的字串。Python內建的re模組負責對字串應⽤正則表示式。下面通過⼀些例⼦說明其使⽤⽅法。

re模組的函式可以分為三個⼤類:模式匹配、替換以及拆分。當然,它們之間是相輔相成的。⼀個regex描述了需要在⽂本中定位的⼀個模式,它可以⽤於許多⽬的。先來看⼀個簡單的例⼦:假設我想要拆分⼀個字串,分隔符為數量不定的⼀組空⽩符(製表符、空格、換⾏符等)。
描述⼀個或多個空⽩符的regex是\s+
import re
text = "foo bar\t baz \tqux"
re.split('\s+', text)   # 輸出:['foo', 'bar', 'baz', 'qux']
調⽤re.split('\s+',text)時,正則表示式會先被編譯,然後再在text上調⽤其split⽅法。


你可以⽤re.compile⾃⼰編譯regex以得到⼀個可重⽤的regex物件:
regex = re.compile('\s+')
regex.split(text)   # 輸出:['foo', 'bar', 'baz', 'qux']

如果只希望得到匹配regex的所有模式,則可以使⽤findall⽅法
regex.findall(text) # 輸出:[' ', '\t ', ' \t']
注意:如果想避免正則表示式中不需要的轉義(\),則可以使⽤原始字串表示如r'C:\x'(也可以編寫其等價式'C:\x')。

如果打算對許多字串應⽤同⼀條正則表示式,強烈建議通過re.compile建立regex物件。這樣將可以節省⼤量的CPU時間。

match和search跟findall功能類似。findall返回的是字串中所有的匹配項,⽽search則只返回第⼀個匹配項match更加嚴格,它只匹配字串的⾸部。來看⼀個⼩例⼦,假設我們有⼀段⽂本以及⼀條能夠識別⼤部分電⼦郵件地址的正則表示式:
text = """Dave [email protected]
Steve [email protected]
Rob [email protected]
Ryan [email protected]
"""
pattern = r'[A-Z0-9._%+-][email protected][A-Z0-9.-]+\.[A-Z]{2,4}'
# re.IGNORECASE makes the regex case-insensitive,忽略正則表示式的大小寫
regex = re.compile(pattern, flags=re.IGNORECASE)   # 編譯regex得到⼀個可重⽤的regex物件
對text使⽤findall將得到⼀組電⼦郵件地址:
regex.findall(text)   # 輸出:['[email protected]', '[email protected]', '[email protected]', '[email protected]']

search返回的是⽂本中第⼀個電⼦郵件地址(以特殊的匹配項物件形式返回)。對於上⾯那個regex,匹配項物件只能告訴我們模式在原字串中的起始和結束位置:

m = regex.search(text)
m   # 輸出:<_sre.SRE_Match object; span=(5, 20), match='[email protected]'>
text[m.start():m.end()]   # 輸出:'[email protected]'

regex.match則將返回None,因為它只匹配出現在字串開頭的模式:
print(regex.match(text))   # 輸出:None

相關的,sub⽅法可以將匹配到的模式替換為指定字串,並返回所得到的新字串
print(regex.sub('REDACTED',text))   # 輸出如下:
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED

假設你不僅想要找出電⼦郵件地址,還想將各個地址分成3個部分:⽤戶名、域名以及域字尾。要實現此功能,只需將待分段的模式的各部分⽤圓括號包起來即可:
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
regex = re.compile(pattern, flags=re.IGNORECASE)
由這種修改過的正則表示式所產⽣的匹配項物件,可以通過其groups⽅法返回⼀個由模式各段組成的元組:
m = regex.match('[email protected]')
m.groups()   # 輸出:('michael', '163', 'com')
對於帶有分組功能的模式,findall會返回⼀個元組列表
regex.findall(text)   # 輸出如下:
[('dave', 'google', 'com'),
('steve', 'gmail', 'com'),
('rob', 'gmail', 'com'),
('ryan', 'yahoo', 'com')]

sub還能通過諸如\1、\2之類的特殊符號訪問各匹配項中的分組。符號\1對應第⼀個匹配的組,\2對應第⼆個匹配的組,以此類推:
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:正則表示式方法(簡要概括)

3、pandas的⽮量化字串函式
清理待分析的散亂資料時,常常需要做⼀些字串規整化⼯作。更為複雜的情況是,含有字串的列有時還含有缺失資料:
data = {'Dave': '[email protected]', 'Steve': '[email protected]', 'Rob': '[email protected]', 'Wes': np.nan}
data = pd.Series(data)
data    # 輸出如下:
Dave    [email protected]
Steve    [email protected]
Rob       [email protected]
Wes          NaN
dtype: object
data.isnull()   # 輸出如下:
Dave     False
Steve    False
Rob      False
Wes     True
dtype: bool
通過data.map,所有字串和正則表示式⽅法都能被應⽤於(傳⼊lambda表示式或其他函式)各個值,但是如果存在NA(null)就會報錯。為了解決這個問題,Series有⼀些能夠跳過NA值的⾯向陣列⽅法,進⾏字串操作。通過Series的str屬性即可訪問這些⽅法。例如,我們可以通過str.contains檢查各個電⼦郵件地址是否含有"gmail":
data.str.contains('gmail')   # 輸出如下:
Dave     False
Steve    True
Rob       True
Wes      NaN
dtype: object
也可以使⽤正則表示式,還可以加上任意re選項(如IGNORECASE):
pattern     # 有pattern表示式:'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
data.str.findall(pattern, flags=re.IGNORECASE)   # 輸出如下:
Dave     [(dave, google, com)]
Steve    [(steve, gmail, com)]
Rob      [(rob, gmail, com)]
Wes            NaN
dtype: object

有兩個辦法可以實現⽮量化的元素獲取操作:要麼使⽤str.get,要麼在str屬性上使⽤索引
matches = data.str.match(pattern, flags=re.IGNORECASE)
matches    # 輸出如下:
Dave     True
Steve    True
Rob      True
Wes      NaN
dtype: object
要訪問嵌⼊列表中的元素,我們可以傳遞索引到這兩個函式中:
matches.str.get(1)   # 輸出如下:使用str.get
Dave    NaN
Steve    NaN
Rob    NaN
Wes    NaN
dtype: float64
matches.str[0]   # 輸出如下:在str屬性上使用索引
Dave     NaN
Steve    NaN
Rob      NaN
Wes      NaN
dtype: float64
可以利⽤這種⽅法對字串進⾏擷取:
data.str[:5]   # 輸出如下:
Dave     [email protected]
Steve    steve
Rob      [email protected]
Wes      NaN
dtype: object

表7-5 部分⽮量化字串⽅法(更多的pandas字串⽅法)