目錄
一、案例分析
假設我們要創建一個智能手機應用程序,從智能手機拍攝的照片中自動識別花的種類。 我們需創建一個演示機器學習模型,測量花的萼片長度 (sepal length),萼片寬度 (sepal width),花瓣長度 (petal length) 和花瓣寬度 (petal width) 四個變量,並根據這些測量識別物種。
如圖,花的萼片和花瓣。(萼片是花的最外一環)
三種類型的鳶尾花,如圖所示:
根據當地研究人員測量的每種鳶尾花的四個數據 (萼片長/寬和花瓣長/寬),我們最終目的是想正確的分類這三種花。
二、數據處理
2.1 回答問題
任何數據分析項目的第一步就是提出想要解決的問題,併爲成功解決該問題而定義一個度量 。一些常見的問題如下:
(1)在查看數據之前是否瞭解數據分析問題的類型,是迴歸,分類還是聚類問題?(明晰問題本質)
這是個根據萼片長度,萼片寬度,花瓣長度和花瓣寬度四個測量指標的分類問題
(2)是否在一開始就定義了成功的度量?(設定量化指標)
因爲是分類問題,所以可以使用查準率,即正確分類花的百分比,來量化模型的表現。我們的數據主管告訴我們應該實現 90% 的準確性。
(3)現有數據是否解決分類問題?(瞭解數據侷限性)
我們目前的數據集只有三種類型的鳶尾花。從這個數據集建立的模型將只適用於那些鳶尾花,未來創建一個通用的花分類器需要更多的數據。
(注意:思考這些問題執行有效數據分析的重要一步,不可忽略哦。)
2.2 檢查數據
在花費太多時間分析數據之前,提早檢查並修正這些數據錯誤能節省大量時間。一般來說,我們希望回答以下問題:
- 數據格式有什麼問題嗎?
- 數據數值有什麼問題嗎?
- 數據需要修復或刪除嗎?
首先引進 python 裏面的幾個包,numpy 是爲了做數學運算,pandas 是爲了處理數據,matplotlib 是爲了畫圖,seaborn 是爲了畫高級圖。代碼如下:
import numpy as np # 用來做數學運算
import pandas as pd # 用來處理數據表
import seaborn as sns # 用來畫高級統計圖
# 將所有圖都在 Notebook 裏顯示
%matplotlib inline
import matplotlib.pyplot as plt # 用來畫圖
from sklearn.model_selection import train_test_split # 做交叉驗證,劃分訓練集和測試集
from sklearn.tree import DecisionTreeClassifier # 用決策樹來分類
檢查點 1. 數據格式 (format)
首先用 pandas 讀取 csv 文件並將數據存成數據表 (data frame) 格式。
iris_data = pd.read_csv('data/iris-data.csv', na_values=['NA']) # 從名爲iris_data的csv文件讀數據存成數據表
#第二個參數用來把 csv 裏面空白處用 NaN 代替
iris_data.head(5).append(iris_data.tail()) # 展示數據表前5個和後5個數據
sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | class | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
145 | 6.7 | 3.0 | 5.2 | 2.3 | Iris-virginica |
146 | 6.3 | 2.5 | 5.0 | 2.3 | Iris-virginica |
147 | 6.5 | 3.0 | 5.2 | 2.0 | Iris-virginica |
148 | 6.2 | 3.4 | 5.4 | 2.3 | Iris-virginica |
149 | 5.9 | 3.0 | 5.1 | 1.8 | Iris-virginica |
檢查點 2. 數據統計 (statistics)
接下來,檢查數據的分佈可以識別異常值。我們從數據集的彙總統計數據開始。
iris_data.describe() # 檢查四列數據的個數,平均數,標準差,最小值,最大值和25,50,75的百分位數
sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | |
---|---|---|---|---|
count | 150.000000 | 150.000000 | 150.000000 | 145.000000 |
mean | 5.644627 | 3.054667 | 3.758667 | 1.236552 |
std | 1.312781 | 0.433123 | 1.764420 | 0.755058 |
min | 0.055000 | 2.000000 | 1.000000 | 0.100000 |
25% | 5.100000 | 2.800000 | 1.600000 | 0.400000 |
50% | 5.700000 | 3.000000 | 4.350000 | 1.300000 |
75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
從該表中看到幾個有用的值。 例如,我們看到缺少 5 條花瓣寬度的數據(表裏 count 那一行的萼片長度,萼片寬度和花瓣長度的個數都是 150 個,唯獨花瓣寬度是 145 個)。此外,這樣的表給不了太多有用信息,除非我們知道數據應該在一個特定的範圍 (如萼片長度的最小值是 0.055,和它其他指標如均值和幾個百分位數都不是一個數量級的,很有可能是測量錯誤)。
比起一串枯燥的數值,我們可能更喜歡絢爛的繪圖。接下來可視化數據,它能使異常值立即脫穎而出。
sns.pairplot(iris_data.dropna(), hue='class') # 畫散點矩陣圖
- 第一個參數 iris_data.dropna() 就是除去 NaN 的數據表,這麼做原因很簡單,圖裏不可能顯示的出 NaN 值的;
- 第二個參數 hue = 'class' 就是根據類 (class) 下不同的值賦予不同的顏色 (hue 就是色彩的意思) 。
散點矩陣圖繪製前四列變量(萼片長/寬和花瓣長/寬)的相關係數圖,而且用不同顏色區分不同的類下面的這四個變量。 從上圖可知,橫軸縱軸都有四個變量,那麼總共可以畫出 16 (4*4) 張小圖。
- 對角線上的 4 張都是某個變量和自己本身的關係,由於自己和自己的相關係數永遠是 1,畫出相關係數圖意義不大。
- 非對角線的 12 張就是某個變量和另一個變量的關係。比如第一行第二列的圖描述的就是萼片長度 (看縱軸第一個 sepal_length_cm 字樣) 和萼片寬度 (看橫軸第二個 sepal_width_cm 字樣)。
從散點矩陣圖中,我們可以迅速看出數據集的一些問題:
(1)圖的右側標註這五個類 (Iris-setosa, Iris-setossa, Iris-versicolor, versicolor, Iris-virginica),但原本要分類的花只有三類 (Iris-setosa, Iris-versicolor, Iris-virginica)。這意味着在記錄數據時可能會犯下一些錯誤。
(2)在測量中有一些明顯的異常值可能是錯誤的。
- 例如第一行後三張小圖,對於 Iris-setosa (山鳶尾花,藍點),一個萼片寬度值落在其正常範圍之外;
- 例如第二行第一,三,四張小圖,對於 Iris-versicolor (變色鳶尾花,紅點) ,幾個萼片長度值都接近零。
下一步我們的任務是要處理錯誤的數據。
2.3 清理數據
修正 1. 數據類(class)
問題:按理應該只有三個類,圖中卻顯示五個。
原因是:標記數據時忘記在 Iris-versicolor 之前添加 Iris-。另一個類 Iris-setossa 他們只是多打了一個 s。讓我們使用代碼來修復這些錯誤。
iris_data['class'].unique() # 查看數據表的類的不重複值,有5個,但按理說只有3個
array(['Iris-setosa', 'Iris-setossa', 'Iris-versicolor', 'versicolor', 'Iris-virginica'], dtype=object)
iris_data.loc[iris_data['class'] == 'versicolor', 'class'] = 'Iris-versicolor' # 將 versicolor 改爲 Iris-versicolor
iris_data.loc[iris_data['class'] == 'Iris-setossa', 'class'] = 'Iris-setosa' # 將 Iris-setossa 改爲 Iris-setosa
iris_data['class'].unique() # 再查看數據表的類的不重複值,現在只有3個
array(['Iris-setosa', 'Iris-versicolor', 'Iris-virginica'], dtype=object)
更新後的散點矩陣圖如下:
現在只有三個類而分別是 Iris-setosa, Iris-versicolor 和 Iris-virginica。
修正點 2. 異常數值 (outliers)
修復異常值是一件棘手的事情。因爲我們很難判斷異常值是否由測量誤差引起,或者是不正確的單位記錄數據,或者是真正的異常。如果我們決定排除任何數據,需要記錄排除的數據並提供排除該數據的充分理由。由上節所知,我們有兩種類型的異常值。
問題:山鳶尾花的一個萼片寬度值落在其正常範圍之外 (黑色圓框)。
我們發現,山鳶尾花 (Iris-setosa) 的萼片寬度 (sepal_width_cm) 不可能低於 2.5 釐米。顯然,這個記錄是錯誤的,這種情況下最有效的方法是刪除它而不是花時間查找原因。但是,我們仍需要知道有多少個類似這樣的錯誤數據,如果很少刪除它沒有問題,如果很多我們需要查明原因。
# 查看 Iris-setosa 裏萼片寬度小於2.5釐米的數據,只有一個
iris_data.loc[(iris_data['class'] == 'Iris-setosa') & (iris_data['sepal_width_cm'] < 2.5)]
sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | class | |
---|---|---|---|---|---|
41 | 4.5 | 2.3 | 1.3 | 0.3 | Iris-setosa |
iris_data.loc[iris_data['class'] == 'Iris-setosa', 'sepal_width_cm'].hist()
第一行代碼是用數據表裏的 loc[] 函數來找到類值爲 Iris-setoa (因爲是藍點) 並且 sepal width 小於 2.5 的所有行。最後發現只有一個這樣的數據,而上圖的條形圖也確認了這樣的異常值只有一個。因此可以直接刪除此數據。
# 去掉 Iris-setosa 裏萼片寬度大於2.5釐米的數據,然後畫出其條形圖
iris_data = iris_data.loc[(iris_data['class'] != 'Iris-setosa') | (iris_data['sepal_width_cm'] >= 2.5)]
iris_data.loc[iris_data['class'] == 'Iris-setosa', 'sepal_width_cm'].hist()
第一行代碼將類值爲 Iris-setosa 並且 sepal width 大於 2.5 的所有數據都新存到 iris_data 中。從上面條形圖也看到了再沒有這個異常值。現在所有的山鳶尾花的萼片寬度都大於 2.5 釐米。
問題:變色鳶尾花的幾個萼片長度值接近與零 (黑色橢圓框)。
我們發現,所有這些接近零的 sepal_length_cm 似乎錯位了兩個數量級。
# 查看 Iris-versicolor 裏的萼片長度接近於零的所有數據
iris_data.loc[(iris_data['class'] == 'Iris-versicolor') & (iris_data['sepal_length_cm'] < 1.0)]
sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | class | |
---|---|---|---|---|---|
77 | 0.067 | 3.0 | 5.0 | 1.7 | Iris-versicolor |
78 | 0.060 | 2.9 | 4.5 | 1.5 | Iris-versicolor |
79 | 0.057 | 2.6 | 3.5 | 1.0 | Iris-versicolor |
80 | 0.055 | 2.4 | 3.8 | 1.1 | Iris-versicolor |
81 | 0.055 | 2.4 | 3.7 | 1.0 | Iris-versicolor |
# 畫出其條形圖
iris_data.loc[iris_data['class'] == 'Iris-versicolor', 'sepal_length_cm'].hist()
第一行代碼是用數據表裏的 loc[] 函數來找到類值爲 Iris-versicolor (因爲是紅點) 並且 sepal length 接近零的所有行,發現有五個數據,而條形圖最左邊顯示的數據個數也確認了是五個。
# 將萼片長度乘以100倍,從單位米換成單位釐米
iris_data.loc[(iris_data['class'] == 'Iris-versicolor') &
(iris_data['sepal_length_cm'] < 1.0),
'sepal_length_cm'] *= 100.0
iris_data.loc[iris_data['class'] == 'Iris-versicolor', 'sepal_length_cm'].hist()
修正點 3. 缺失數值 (missing value)
我們還有些 NaN 數據。通常我們有兩種方式來處理這類數據。
- 刪除 (deletion)
- 插補 (imputation)
在本例中刪除不是理想的做法,特別是考慮到它們都在 Iris-setosa 下,如圖
所有缺失的值都屬於 Iris-setosa類,直接刪除可能會對日後數據分析帶來偏差。此外,可以用插補方法,其最常見的方法平均插補 (mean imputation)。其做法就是“假設知道測量的值落在一定範圍內,就可以用該測量的平均值填充空值”。
# 查看所有有NaN值的行數據,發現只有花瓣寬度 (petal_width_cm) 列下才有
iris_data.loc[(iris_data['sepal_length_cm'].isnull()) |
(iris_data['sepal_width_cm'].isnull()) |
(iris_data['petal_length_cm'].isnull()) |
(iris_data['petal_width_cm'].isnull())]
sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | class | |
---|---|---|---|---|---|
7 | 5.0 | 3.4 | 1.5 | NaN | Iris-setosa |
8 | 4.4 | 2.9 | 1.4 | NaN | Iris-setosa |
9 | 4.9 | 3.1 | 1.5 | NaN | Iris-setosa |
10 | 5.4 | 3.7 | 1.5 | NaN | Iris-setosa |
11 | 4.8 | 3.4 | 1.6 | NaN | Iris-setosa |
# 畫出其條形圖
iris_data.loc[iris_data['class'] == 'Iris-setosa', 'petal_width_cm'].hist()
接下來用 hist() 函數畫出 Iris-setosa 花瓣寬度的條形圖,可以清楚看到大多數寬度在 0.25 左右。
# 用平均值來代替NaN值
average_petal_width = iris_data.loc[iris_data['class'] == 'Iris-setosa', 'petal_width_cm'].mean()
iris_data.loc[(iris_data['class'] == 'Iris-setosa') &
(iris_data['petal_width_cm'].isnull()),
'petal_width_cm'] = average_petal_width
iris_data.loc[(iris_data['class'] == 'Iris-setosa') &
(iris_data['petal_width_cm'] == average_petal_width)]
sepal_length_cm | sepal_width_cm | petal_length_cm | petal_width_cm | class | |
---|---|---|---|---|---|
7 | 5.0 | 3.4 | 1.5 | 0.25 | Iris-setosa |
8 | 4.4 | 2.9 | 1.4 | 0.25 | Iris-setosa |
9 | 4.9 | 3.1 | 1.5 | 0.25 | Iris-setosa |
10 | 5.4 | 3.7 | 1.5 | 0.25 | Iris-setosa |
11 | 4.8 | 3.4 | 1.6 | 0.25 | Iris-setosa |
然後用 mean() 準確求出其寬度的平均值,將其 NaN 值全部用平均值代替,最後打出那 5 行插補後的數據表。
# 確保所有NaN值都已被更新
iris_data.loc[(iris_data['sepal_length_cm'].isnull()) |
(iris_data['sepal_width_cm'].isnull()) |
(iris_data['petal_length_cm'].isnull()) |
(iris_data['petal_width_cm'].isnull())]
爲了確保所有 NaN 值已被替換,再次用 iris_data[A].isnull() 語句來查看,出來的結果是一個只有列標題的空數據表。這表示表內已經沒有 NaN 值了。
2.4 測試數據
1. 存儲數據(save data)
iris_data.to_csv('data/iris-data-clean.csv', index = False) # 將整理好的數據寫到一個名叫iris-data-clean的csv文件裏,沒有行標
iris_data_clean = pd.read_csv('data/iris-data-clean.csv') # 重新從iris-data-clean的csv文件裏讀數據存成iris_data_clean數據表
讓我們再看看基於乾淨數據畫的散點矩陣圖吧。
sns.pairplot(iris_data_clean, hue='class') # 查看散點矩陣圖
從上圖可看到:
- 五個類變成三個類;
- 異常值全部刪除或修正了。
2. 聲明數據(assert data)
爲了防止一些數據問題沒有解決,我們可以用 assert 語句來做聲明。該語句好處是,在運行時如果聲明語句爲真,沒有任何事發生,反之會報錯而警告我們有哪些錯誤數據需要注意且修正。
# 聲明花只有三種類型
assert len(iris_data_clean['class'].unique()) == 3
# 聲明變色鳶尾花的萼片長度應該大於 2.5 釐米
assert iris_data_clean.loc[iris_data_clean['class'] == 'Iris-versicolor', 'sepal_length_cm'].min() >= 2.5
# 數據不應該有缺失
assert len(iris_data_clean.loc[(iris_data_clean['sepal_length_cm'].isnull()) |
(iris_data_clean['sepal_width_cm'].isnull()) |
(iris_data_clean['petal_length_cm'].isnull()) |
(iris_data_clean['petal_width_cm'].isnull())]) == 0
如果任何聲明被違反,我們應該立即停止分析,而回到整理階段。
三、用 scikit-learn 來預測數據
3.1 選出特徵 (輸入變量) 和標記 (輸出變量)
# 讀數據
iris_data_clean = pd.read_csv('data/iris-data-clean.csv')
# 選出特徵
all_inputs = iris_data_clean[['sepal_length_cm', 'sepal_width_cm',
'petal_length_cm', 'petal_width_cm']].values
# 選出標記
all_classes = iris_data_clean['class'].values
3.2 劃分訓練集和測試集
(training_inputs, test_inputs, training_classes,
test_classes) = train_test_split(all_inputs, all_classes, train_size=0.75, random_state=1)
用 75% 和 25% 的比例來劃分訓練集和測試集,設一個 random_state 是爲了在機器學習中每次出的結果一樣,便於找問題。
3.3 用模型來學習
# 創建決策樹分類器
decision_tree_classifier = DecisionTreeClassifier()
# 訓練數據
decision_tree_classifier.fit(training_inputs, training_classes)
# 分類精度
decision_tree_classifier.score(test_inputs, test_classes)
0.9736842105263158
再一次感嘆 scikit-learn 的強大和好用。
四、思考題
這時候有人會問了:
- 這個訓練集和測試集劃分是隨機的,一次不足以說明你查準率高;
- 這數據太少沒代表性,換套數據模型可能過擬合;
- 特徵選擇有很大學問,憑什麼就選萼片長度,萼片寬度,花瓣長度和花瓣寬度這四個變量?
有待學習,思考哦。