Python 100 Days Day72 Introduction to Pandas-3
26 Sep 2022 | beginner pythonauthor: jackfrued
Pandas的應用-3
DataFrame的應用
數據清洗
通常,我們從 Excel、CSV 或數據庫中獲取到的數據並不是非常完美的,里面可能因為系統或人為的原因混入了重覆值或異常值,也可能在某些字段上存在缺失值;再者,DataFrame中的數據也可能存在格式不統一、量綱不統一等各種問題。因此,在開始數據分析之前,對數據進行清洗就顯得特別重要。
缺失值
可以使用DataFrame對象的isnull或isna方法來找出數據表中的缺失值,如下所示。
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
相對應的,notnull和notna方法可以將非空的值標記為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
從上面的輸出可以看到,50和60兩個部門從部門名稱上來看是重覆的,如果要刪除重覆值,可以使用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方法將指定的值替換掉。例如我們要將月薪為1800和9000的替換為月薪的平均值,補貼為800的替換為1000,代碼如下所示。
avg_sal = np.mean(emp_df.sal).astype(int)
emp_df.replace({'sal': [1800, 9000], 'comm': 800}, {'sal': avg_sal, 'comm': 1000})
預處理
對數據進行預處理也是一個很大的話題,它包含了對數據的拆解、變換、歸約、離散化等操作。我們先來看看數據的拆解。如果數據表中的數據是一個時間日期,我們通常都需要從年、季度、月、日、星期、小時、分鐘等維度對其進行拆解,如果時間日期是用字符串表示的,可以先通過pandas的to_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 屬性,獲得一個訪問日期時間的對象,通過該對象的year、month、quarter、hour等屬性,就可以獲取到年、月、季度、小時等時間信息,獲取到的仍然是一個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
applymap和apply兩個方法在數據預處理的時候經常用到,Series對象也有apply方法,也是用於數據的預處理,但是DataFrame對象還有一個名為transform 的方法,也是通過傳入的函數對數據進行變換,類似Series對象的map方法。需要強調的是,apply方法具有歸約效果的,簡單的說就是能將較多的數據處理成較少的數據或一條數據;而transform方法沒有歸約效果,只能對數據進行變換,原來有多少條數據,處理後還是有多少條數據。
如果要對數據進行深度的分析和挖掘,字符串、日期時間這樣的非數值類型都需要處理成數值,因為非數值類型沒有辦法計算相關性,也沒有辦法進行$\chi^2$檢驗等操作。對於字符串類型,通常可以其分為以下三類,再進行對應的處理。
- 有序變量(Ordinal Variable):字符串表示的數據有順序關系,那麽可以對字符串進行序號化處理。
- 分類變量(Categorical Variable)/ 名義變量(Nominal Variable):字符串表示的數據沒有大小關系和等級之分,那麽就可以使用獨熱編碼的方式處理成啞變量(虛擬變量)矩陣。
- 定距變量(Scale Variable):字符串本質上對應到一個有大小高低之分的數據,而且可以進行加減運算,那麽只需要將字符串處理成對應的數值即可。
對於第1類和第3類,我們可以用上面提到的apply或transform方法來處理,也可以利用scikit-learn中的OrdinalEncoder處理第1類字符串,這個我們在後續的課程中會講到。對於第2類字符串,可以使用pandas的get_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個箱子,pandas的cut函數可以幫助我們首先數據分箱,代碼如下所示。
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