A place for study and research

Python 100 Days Day72 Introduction to Pandas-3

|

author: jackfrued

Pandas的應用-3

DataFrame的應用

數據清洗

通常,我們從 Excel、CSV 或數據庫中獲取到的數據並不是非常完美的,里面可能因為系統或人為的原因混入了重覆值或異常值,也可能在某些字段上存在缺失值;再者,DataFrame中的數據也可能存在格式不統一、量綱不統一等各種問題。因此,在開始數據分析之前,對數據進行清洗就顯得特別重要。

缺失值

可以使用DataFrame對象的isnullisna方法來找出數據表中的缺失值,如下所示。

emp_df.isnull()

或者

emp_df.isna()

輸出:

        ename   job	    mgr     sal     comm    dno
eno						
1359	False	False	False	False	False	False
2056	False	False	False	False	False	False
3088	False	False	False	False	False	False
3211	False	False	False	False	True	False
3233	False	False	False	False	True	False
3244	False	False	False	False	True	False
3251	False	False	False	False	True	False
3344	False	False	False	False	False	False
3577	False	False	False	False	True	False
3588	False	False	False	False	True	False
4466	False	False	False	False	True	False
5234	False	False	False	False	True	False
5566	False	False	False	False	False	False
7800	False	False	True	False	False	False

相對應的,notnullnotna方法可以將非空的值標記為True。如果想刪除這些缺失值,可以使用DataFrame對象的dropna方法,該方法的axis參數可以指定沿著0軸還是1軸刪除,也就是說當遇到空值時,是刪除整行還是刪除整列,默認是沿0軸進行刪除的,代碼如下所示。

emp_df.dropna()

輸出:

        ename   job      mgr	 sal    comm     dno
eno						
1359	胡一刀  銷售員	3344.0	1800   200.0	30
2056	喬峰    架構師	 7800.0	 5000	1500.0	 20
3088	李莫愁  設計師	2056.0	3500   800.0	20
3344	黃蓉    銷售主管	7800.0	3000   800.0	30
5566	宋遠橋  會計師	7800.0	4000   1000.0	10

如果要沿著1軸進行刪除,可以使用下面的代碼。

emp_df.dropna(axis=1)

輸出:

        ename    job      sal    dno
eno				
1359	胡一刀   銷售員    1800	30
2056	喬峰     架構師	  5000	 20
3088	李莫愁   設計師    3500	20
3211	張無忌   程序員    3200	20
3233	丘處機   程序員    3400	20
3244	歐陽鋒   程序員    3200	20
3251	張翠山   程序員    4000	20
3344	黃蓉     銷售主管  3000	30
3577	楊過     會計	   2200	  10
3588	朱九真   會計	  2500	 10
4466	苗人鳳   銷售員	 2500   30
5234	郭靖     出納      2000   10
5566	宋遠橋   會計師    4000   10
7800	張三豐   總裁      9000   20

注意DataFrame對象的很多方法都有一個名為inplace的參數,該參數的默認值為False,表示我們的操作不會修改原來的DataFrame對象,而是將處理後的結果通過一個新的DataFrame對象返回。如果將該參數的值設置為True,那麽我們的操作就會在原來的DataFrame上面直接修改,方法的返回值為None。簡單的說,上面的操作並沒有修改emp_df,而是返回了一個新的DataFrame對象。

在某些特定的場景下,我們可以對空值進行填充,對應的方法是fillna,填充空值時可以使用指定的值(通過value參數進行指定),也可以用表格中前一個單元格(通過設置參數method=ffill)或後一個單元格(通過設置參數method=bfill)的值進行填充,當代碼如下所示。

emp_df.fillna(value=0)

注意:填充的值如何選擇也是一個值得探討的話題,實際工作中,可能會使用某種統計量(如:均值、眾數等)進行填充,或者使用某種插值法(如:隨機插值法、拉格朗日插值法等)進行填充,甚至有可能通過回歸模型、貝葉斯模型等對缺失數據進行填充。

輸出:

        ename    job        mgr      sal     comm    dno
eno
1359	胡一刀    銷售員	   3344.0	1800	200.0	30
2056	喬峰	    分析師	    7800.0	 5000	 1500.0	 20
3088	李莫愁	   設計師	   2056.0	3500	800.0	20
3211	張無忌	   程序員	   2056.0	3200	0.0     20
3233	丘處機	   程序員	   2056.0	3400	0.0	    20
3244	歐陽鋒	   程序員	   3088.0	3200	0.0     20
3251	張翠山	   程序員	   2056.0	4000	0.0	    20
3344	黃蓉	    銷售主管   7800.0	3000	800.0	30
3577	楊過	    會計	     5566.0	  2200	  0.0	  10
3588	朱九真	   會計	    5566.0	 2500	 0.0	 10
4466	苗人鳳	   銷售員	   3344.0	2500	0.0	    30
5234	郭靖	    出納	     5566.0	  2000	  0.0	  10
5566	宋遠橋	   會計師	   7800.0	4000	1000.0	10
7800	張三豐	   總裁	    0.0      9000	 1200.0	 20
重覆值

接下來,我們先給之前的部門表添加兩行數據,讓部門表中名為“研發部”和“銷售部”的部門各有兩個。

dept_df.loc[50] = {'dname': '研發部', 'dloc': '上海'}
dept_df.loc[60] = {'dname': '銷售部', 'dloc': '長沙'}
dept_df

輸出:

    dname  dloc
dno		
10	會計部	北京
20	研發部	成都
30	銷售部	重慶
40	運維部	天津
50	研發部	上海
60	銷售部	長沙

現在,我們的數據表中有重覆數據了,我們可以通過DataFrame對象的duplicated方法判斷是否存在重覆值,該方法在不指定參數時默認判斷行索引是否重覆,我們也可以指定根據部門名稱dname判斷部門是否重覆,代碼如下所示。

dept_df.duplicated('dname')

輸出:

dno
10    False
20    False
30    False
40    False
50     True
60     True
dtype: bool

從上面的輸出可以看到,5060兩個部門從部門名稱上來看是重覆的,如果要刪除重覆值,可以使用drop_duplicates方法,該方法的keep參數可以控制在遇到重覆值時,保留第一項還是保留最後一項,或者多個重覆項一個都不用保留,全部刪除掉。

dept_df.drop_duplicates('dname')

輸出:

	dname	dloc
dno		
10	會計部	北京
20	研發部	成都
30	銷售部	重慶
40	運維部	天津

keep參數的值修改為last

dept_df.drop_duplicates('dname', keep='last')

輸出:

	dname	dloc
dno		
10	會計部	北京
40	運維部	天津
50	研發部	上海
60	銷售部	長沙
異常值

異常值在統計學上的全稱是疑似異常值,也稱作離群點(outlier),異常值的分析也稱作離群點分析。異常值是指樣本中出現的“極端值”,數據值看起來異常大或異常小,其分布明顯偏離其余的觀測值。實際工作中,有些異常值可能是由系統或人為原因造成的,但有些異常值卻不是,它們能夠重覆且穩定的出現,屬於正常的極端值,例如很多遊戲產品中頭部玩家的數據往往都是離群的極端值。所以,我們既不能忽視異常值的存在,也不能簡單地把異常值從數據分析中剔除。重視異常值的出現,分析其產生的原因,常常成為發現問題進而改進決策的契機。

異常值的檢測有Z-score 方法、IQR 方法、DBScan 聚類、孤立森林等,這里我們對前兩種方法做一個簡單的介紹。

如果數據服從正態分布,依據3σ法則,異常值被定義與平均值的偏差超過三倍標準差的值。在正態分布下,距離平均值3σ之外的值出現的概率為$ P(|x-\mu|>3\sigma)<0.003 $,屬於小概率事件。如果數據不服從正態分布,那麽可以用遠離平均值的多少倍的標準差來描述,這里的倍數就是Z-score。Z-score以標準差為單位去度量某一原始分數偏離平均值的距離,公式如下所示。 \(z = \frac {X - \mu} {\sigma}\) Z-score需要根據經驗和實際情況來決定,通常把遠離標準差3倍距離以上的數據點視為離群點,下面的代給出了如何通過Z-score方法檢測異常值。

import numpy as np


def detect_outliers_zscore(data, threshold=3):
    avg_value = np.mean(data)
    std_value = np.std(data)
    z_score = np.abs((data - avg_value) / std_value)
    return data[z_score > threshold]

IQR 方法中的IQR(Inter-Quartile Range)代表四分位距離,即上四分位數(Q3)和下四分位數(Q1)的差值。通常情況下,可以認為小於 $ Q1 - 1.5 \times IQR $ 或大於 $ Q3 + 1.5 \times IQR $ 的就是異常值,而這種檢測異常值的方法也是箱線圖(後面會講到)默認使用的方法。下面的代給出了如何通過 IQR 方法檢測異常值。

import numpy as np


def detect_outliers_iqr(data, whis=1.5):
    q1, q3 = np.quantile(data, [0.25, 0.75])
    iqr = q3 - q1
    lower, upper = q1 - whis * iqr, q3 + whis * iqr
    return data[(data < lower) | (data > upper)]

如果要刪除異常值,可以使用DataFrame對象的drop方法,該方法可以根據行索引或列索引刪除指定的行或列。例如我們認為月薪低於2000或高於8000的是員工表中的異常值,可以用下面的代碼刪除對應的記錄。

emp_df.drop(emp_df[(emp_df.sal > 8000) | (emp_df.sal < 2000)].index)

如果要替換掉異常值,可以通過給單元格賦值的方式來實現,也可以使用replace方法將指定的值替換掉。例如我們要將月薪為18009000的替換為月薪的平均值,補貼為800的替換為1000,代碼如下所示。

avg_sal = np.mean(emp_df.sal).astype(int)
emp_df.replace({'sal': [1800, 9000], 'comm': 800}, {'sal': avg_sal, 'comm': 1000})
預處理

對數據進行預處理也是一個很大的話題,它包含了對數據的拆解、變換、歸約、離散化等操作。我們先來看看數據的拆解。如果數據表中的數據是一個時間日期,我們通常都需要從年、季度、月、日、星期、小時、分鐘等維度對其進行拆解,如果時間日期是用字符串表示的,可以先通過pandasto_datetime函數將其處理成時間日期。

在下面的例子中,我們先讀取 Excel 文件,獲取到一組銷售數據,其中第一列就是銷售日期,我們將其拆解為“月份”、“季度”和“星期”,代碼如下所示。

sales_df = pd.read_excel(
    '2020年銷售數據.xlsx',
    usecols=['銷售日期', '銷售區域', '銷售渠道', '品牌', '銷售額']
)
sales_df.info()

說明:如果需要上面例子中的 Excel 文件,可以通過下面的百度雲盤地址進行獲取,數據在《從零開始學數據分析》目錄中。鏈接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g,提取碼:e7b4。

輸出:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1945 entries, 0 to 1944
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   銷售日期    1945 non-null   datetime64[ns]
 1   銷售區域    1945 non-null   object        
 2   銷售渠道    1945 non-null   object        
 3   品牌        1945 non-null   object        
 4   銷售額      1945 non-null   int64         
dtypes: datetime64[ns](1), int64(1), object(3)
memory usage: 76.1+ KB
sales_df['月份'] = sales_df['銷售日期'].dt.month
sales_df['季度'] = sales_df['銷售日期'].dt.quarter
sales_df['星期'] = sales_df['銷售日期'].dt.weekday
sales_df

輸出:

	    銷售日期	 銷售區域	銷售渠道	品牌	  銷售額	月份	季度	星期
0	    2020-01-01	上海	     拼多多	 八匹馬   8217	    1	 1	   2
1	    2020-01-01	上海	     抖音	      八匹馬	6351	 1	  1	    2
2	    2020-01-01	上海	     天貓	      八匹馬	14365	 1	  1	    2
3	    2020-01-01	上海	     天貓       八匹馬	2366	 1	  1     2
4	    2020-01-01	上海	     天貓 	  皮皮蝦	15189	 1	  1     2
...     ...         ...        ...       ...      ...     ...  ...   ...
1940    2020-12-30	北京	     京東	      花花姑娘 6994     12	 4	   2
1941    2020-12-30	福建	     實體	      八匹馬	7663	 12	  4	    2
1942    2020-12-31	福建	     實體	      花花姑娘 14795    12	 4	   3
1943    2020-12-31	福建	     抖音	      八匹馬	3481	 12	  4	    3
1944    2020-12-31	福建	     天貓	      八匹馬	2673	 12	  4	    3

在上面的代碼中,通過日期時間類型的Series對象的dt 屬性,獲得一個訪問日期時間的對象,通過該對象的yearmonthquarterhour等屬性,就可以獲取到年、月、季度、小時等時間信息,獲取到的仍然是一個Series對象,它包含了一組時間信息,所以我們通常也將這個dt屬性稱為“日期時間向量”。

我們再來說一說字符串類型的數據的處理,我們先從指定的 Excel 文件中讀取某招聘網站的招聘數據。

jobs_df = pd.read_csv(
    '某招聘網站招聘數據.csv',
    usecols=['city', 'companyFullName', 'positionName', 'salary']
)
jobs_df.info()

說明:如果需要上面例子中的 Excel 文件,可以通過下面的百度雲盤地址進行獲取,數據在《從零開始學數據分析》目錄中。鏈接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g,提取碼:e7b4。

輸出:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3140 entries, 0 to 3139
Data columns (total 4 columns):
 #   Column           Non-Null Count  Dtype 
---  ------           --------------  ----- 
 0   city             3140 non-null   object
 1   companyFullName  3140 non-null   object
 2   positionName     3140 non-null   object
 3   salary           3140 non-null   object
dtypes: object(4)
memory usage: 98.2+ KB

查看前5條數據。

jobs_df.head()

輸出:

    city    companyFullName              positionName    salary
0   北京	  達疆網絡科技(上海)有限公司    數據分析崗       15k-30k
1   北京	  北京音娛時光科技有限公司        數據分析        10k-18k
2   北京	  北京千喜鶴餐飲管理有限公司	     數據分析        20k-30k
3   北京	  吉林省海生電子商務有限公司	     數據分析        33k-50k
4   北京	  韋博網訊科技(北京)有限公司	數據分析        10k-15k

上面的數據表一共有3140條數據,但並非所有的職位都是“數據分析”的崗位,如果要篩選出數據分析的崗位,可以通過檢查positionName字段是否包含“數據分析”這個關鍵詞,這里需要模糊匹配,應該如何實現呢?我們可以先獲取positionName列,因為這個Series對象的dtype是字符串,所以可以通過str屬性獲取對應的字符串向量,然後就可以利用我們熟悉的字符串的方法來對其進行操作,代碼如下所示。

jobs_df = jobs_df[jobs_df.positionName.str.contains('數據分析')]
jobs_df.shape

輸出:

(1515, 4)

可以看出,篩選後的數據還有1515條。接下來,我們還需要對salary字段進行處理,如果我們希望統計所有崗位的平均工資或每個城市的平均工資,首先需要將用範圍表示的工資處理成其中間值,代碼如下所示。

jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?')

說明:上面的代碼通過正則表達式捕獲組從字符串中抽取出兩組數字,分別對應工資的下限和上限,對正則表達式不熟悉的讀者,可以閱讀我的知乎專欄“從零開始學Python”中的《正則表達式的應用》一文。

輸出:

        0     1
0	    15    30
1	    10	  18
2       20    30
3       33    50
4       10    15
...     ...   ...
3065    8     10
3069    6     10
3070    2     4
3071    6     12
3088    8     12

需要提醒大家的是,抽取出來的兩列數據都是字符串類型的值,我們需要將其轉換成int類型,才能計算平均值,對應的方法是DataFrame對象的applymap方法,該方法的參數是一個函數,而該函數會作用於DataFrame中的每個元素。完成這一步之後,我們就可以使用apply方法將上面的DataFrame處理成中間值,apply方法的參數也是一個函數,可以通過指定axis參數使其作用於DataFrame 對象的行或列,代碼如下所示。

temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
temp_df.apply(np.mean, axis=1)

輸出:

0       22.5
1       14.0
2       25.0
3       41.5
4       12.5
        ... 
3065    9.0
3069    8.0
3070    3.0
3071    9.0
3088    10.0
Length: 1515, dtype: float64

接下來,我們可以用上面的結果替換掉原來的salary列或者增加一個新的列來表示職位對應的工資,完整的代碼如下所示。

temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
jobs_df['salary'] = temp_df.apply(np.mean, axis=1)
jobs_df.head()

輸出:

    city    companyFullName              positionName    salary
0   北京	  達疆網絡科技(上海)有限公司    數據分析崗       22.5
1   北京	  北京音娛時光科技有限公司        數據分析        14.0
2   北京	  北京千喜鶴餐飲管理有限公司	     數據分析        25.0
3   北京	  吉林省海生電子商務有限公司	     數據分析        41.5
4   北京	  韋博網訊科技(北京)有限公司	數據分析        12.5

applymapapply兩個方法在數據預處理的時候經常用到,Series對象也有apply方法,也是用於數據的預處理,但是DataFrame對象還有一個名為transform 的方法,也是通過傳入的函數對數據進行變換,類似Series對象的map方法。需要強調的是,apply方法具有歸約效果的,簡單的說就是能將較多的數據處理成較少的數據或一條數據;而transform方法沒有歸約效果,只能對數據進行變換,原來有多少條數據,處理後還是有多少條數據。

如果要對數據進行深度的分析和挖掘,字符串、日期時間這樣的非數值類型都需要處理成數值,因為非數值類型沒有辦法計算相關性,也沒有辦法進行$\chi^2$檢驗等操作。對於字符串類型,通常可以其分為以下三類,再進行對應的處理。

  1. 有序變量(Ordinal Variable):字符串表示的數據有順序關系,那麽可以對字符串進行序號化處理。
  2. 分類變量(Categorical Variable)/ 名義變量(Nominal Variable):字符串表示的數據沒有大小關系和等級之分,那麽就可以使用獨熱編碼的方式處理成啞變量(虛擬變量)矩陣。
  3. 定距變量(Scale Variable):字符串本質上對應到一個有大小高低之分的數據,而且可以進行加減運算,那麽只需要將字符串處理成對應的數值即可。

對於第1類和第3類,我們可以用上面提到的applytransform方法來處理,也可以利用scikit-learn中的OrdinalEncoder處理第1類字符串,這個我們在後續的課程中會講到。對於第2類字符串,可以使用pandasget_dummies()函數來生成啞變量(虛擬變量)矩陣,代碼如下所示。

persons_df = pd.DataFrame(
    data={
        '姓名': ['關羽', '張飛', '趙雲', '馬超', '黃忠'],
        '職業': ['醫生', '醫生', '程序員', '畫家', '教師'],
        '學歷': ['研究生', '大專', '研究生', '高中', '本科']
    }
)
persons_df

輸出:

	姓名	職業	學歷
0	關羽	醫生	研究生
1	張飛	醫生	大專
2	趙雲	程序員	研究生
3	馬超	畫家	高中
4	黃忠	教師	本科

將職業處理成啞變量矩陣。

pd.get_dummies(persons_df['職業'])

輸出:

    醫生 教師  畫家  程序員
0	1    0    0    0
1	1    0    0    0
2	0    0    0    1
3	0    0    1    0
4	0    1    0    0

將學歷處理成大小不同的值。

def handle_education(x):
    edu_dict = {'高中': 1, '大專': 3, '本科': 5, '研究生': 10}
    return edu_dict.get(x, 0)


persons_df['學歷'].apply(handle_education)

輸出:

0    10
1     3
2    10
3     1
4     5
Name: 學歷, dtype: int64

我們再來說說數據離散化。離散化也叫分箱,如果變量的取值是連續值,那麽它的取值有無數種可能,在進行數據分組的時候就會非常的不方便,這個時候將連續變量離散化就顯得非常重要。之所以把離散化叫做分箱,是因為我們可以預先設置一些箱子,每個箱子代表了數據取值的範圍,這樣就可以將連續的值分配到不同的箱子中,從而實現離散化。下面的例子讀取了2018年北京積分落戶數據,我們可以根據落戶積分對數據進行分組,具體的做法如下所示。

luohu_df = pd.read_csv('data/2018年北京積分落戶數據.csv', index_col='id')
luohu_df.score.describe()

輸出:

count    6019.000000
mean       95.654552
std         4.354445
min        90.750000
25%        92.330000
50%        94.460000
75%        97.750000
max       122.590000
Name: score, dtype: float64

可以看出,落戶積分的最大值是122.59,最小值是90.75,那麽我們可以構造一個從90分到125分,每5分一組的7個箱子,pandascut函數可以幫助我們首先數據分箱,代碼如下所示。

bins = np.arange(90, 126, 5)
pd.cut(luohu_df.score, bins, right=False)

說明cut函數的right參數默認值為True,表示箱子左開右閉;修改為False可以讓箱子的右邊界為開區間,左邊界為閉區間,大家看看下面的輸出就明白了。

輸出:

id
1       [120, 125)
2       [120, 125)
3       [115, 120)
4       [115, 120)
5       [115, 120)
           ...    
6015      [90, 95)
6016      [90, 95)
6017      [90, 95)
6018      [90, 95)
6019      [90, 95)
Name: score, Length: 6019, dtype: category
Categories (7, interval[int64, left]): [[90, 95) < [95, 100) < [100, 105) < [105, 110) < [110, 115) < [115, 120) < [120, 125)]

我們可以根據分箱的結果對數據進行分組,然後使用聚合函數對每個組進行統計,這是數據分析中經常用到的操作,下一個章節會為大家介紹。除此之外,pandas還提供了一個名為qcut的函數,可以指定分位數對數據進行分箱,有興趣的讀者可以自行研究。

Comments