1.入門
從外部看,資料科學似乎是一門龐大而模糊的學科。今天的資料科學專家並不是為了獲得資料科學學位而上大學的(儘管現在許多大學都提供這些課程)。
第一代專業資料科學家來自數學、統計學、計算機科學和物理學科。
資料科學的“科學”部分是提出問題、生成假設、檢驗,以及形成解釋證據的模型的工作。
這些技能是任何人都可以學習的,而且現在比以往任何時候都有更多的資源可以開始學習。
最好的資源之一是Kaggle。他們的資料科學競賽對任何人來說都是一個機會,讓他們能夠去練習專案。圍繞這些挑戰形成的社群也是向他人學習的好地方。
案例研究在本文中,我將使用經典挑戰“泰坦尼克號”來解釋如何處理資料科學問題並找到一個成功的解決方案:https://www.kaggle.com/c/titanic/overview 。
這項挑戰的目的是建立一個模型,根據乘客名單中已知的乘客資訊,預測乘客的存活率。這是基於歷史資料,我們知道很多乘客的姓名、年齡、性別、船艙等級和家庭資訊,以及他們是否在災難中倖存下來。
Kaggle提供訓練資料和測試資料。訓練資料有生存的“真實”標籤(是/否),但測試資料不包括基本真實標籤。Kaggle保留這些標籤,並使用它們為你的提交打分。測試資料預測取決於你的預測,預測的準確性將用於確定你在排行榜上的位置。
如何在泰坦尼克號挑戰賽中獲得滿分關於泰坦尼克號挑戰賽的旁註:如果你看看排行榜,你會看到很多完美的分數。這自然會讓你懷疑,“他們是怎麼做到的?”。
答案令人沮喪——他們作弊了。如果你在網上搜索一段時間,你會發現可以在網際網路上找到帶有基本事實標籤的完整測試資料。那些有完美分數的人只需提交真實的標籤,而不是機器學習模型的預測……然後得到一個完美的分數。
但他們沒有透過真正的挑戰——挑戰存在於掌握一門技術,而不是偷取高分。
工作環境在開始資料科學專案之前,我建議設定你的工作環境,以便:
新建專案資料夾,其中包含用於儲存資料的子資料夾;獨立的虛擬環境,安裝了標準的資料科學庫。對於虛擬環境,我建議使用conda來管理Python環境。我最喜歡的資料科學庫是numpy、pandas、matplotlib、seaborn和scikit-learn。根據問題的性質,其他庫(如scipy)可能是相關的。深度學習的挑戰包括安裝Tensorflow或PyTorch。
最後我們載入資料。假設你已經將挑戰的資料從Kaggle下載到你自己的機器上,並將其放入名為data的子資料夾中,並且你正在Jupyter Notebook中編寫程式碼。
或者,你可以直接在Kaggle平臺上建立一個數據科學Notebook。
import numpy as npimport matplotlib.pyplot as pltimport pandas as pdpd.set_option('display.max_rows', 200)import seaborn as sns# 應用預設主題sns.set()# 假設你已經將資料下載到自己的機器上# 它位於名為“data”的子資料夾中train_data = pd.read_csv('./data/train.csv')test_data = pd.read_csv('./data/test.csv')
2.探索性資料分析
不要跳過這第一步。
無論何時使用新資料,瞭解資料包含的內容、變數的含義、使用的單位和資料型別以及分佈的外觀都很重要。
這將有助於你對資料建立直覺,使其更容易產生假設,這也有望使解決方案更容易找到。
訓練資料的前幾行如下所示。“Survived”列表示生存,它是我們試圖預測的目標變數。
challenge網站很好地解釋了資料,並用表格解釋了每個變數:
其中大多數都是不言而喻的,但是sibsp和parch需要更多的資訊:
sibsp:資料集以這種方式定義家庭關係…
Sibling =兄弟姐妹
Spouse =丈夫、妻子
parch:資料集以這種方式定義家庭關係…
Parent =母親,父親
Child=女兒,兒子,繼女,繼子
有些孩子只和保姆一起旅行,因此對他們來說parch=0。
按年齡或性別分列的存活率seaborn庫擅長視覺化,因此在本節中,我將以幾種不同的方式檢查資料。
我很想知道我們關於婦女和兒童存活率的假設是否成立,所以我設計了以下的圖。
import numpy as npimport matplotlib.pyplot as pltimport pandas as pdimport seaborn as sns# 應用預設主題sns.set()# 載入訓練資料train_data = pd.read_csv('./data/train.csv')sns.catplot(data=train_data, kind="swarm", x="Sex", y="Age", hue="Survived")
用seaborn建立的圖顯示了基於性別和年齡的乘客存活率。
我們的假設成立得相當好,但值得注意的是,相當多的兒童沒有存活下來,相當多不同年齡的男性確實存活了下來。他們的生存可能與其他什麼有關嗎?
船艙等級也許船艙等級是生存的預測。讓我們再看一次資料,但這次是按船艙等級劃分的。
import numpy as npimport matplotlib.pyplot as pltimport pandas as pdimport seaborn as sns# 應用預設主題sns.set()# 載入訓練資料train_data = pd.read_csv('./data/train.csv')sns.catplot(data=train_data, kind="swarm", x="Sex", y="Age", col="Pclass", hue="Survived")
圖中顯示了基於年齡、性別和乘客對應船艙等級(1、2、3)的乘客存活率。
我們在這裡看到了一些內容。在倖存的成年男性中,一等艙乘客存活率較高;在二等艙和三等艙乘客中也有成年男性生還者,但相對於每組乘客總人數而言沒有一等艙那麼多。
在沒有幸存下來的女性中,大多數是三等艙乘客。
這告訴我們,性別、年齡和船艙等級都可能是生存率的預測因素,但在每一組中都有異常值。目前還不清楚這是隨機的,還是由於更微妙的因素。
登船最後,讓我們快速看看這些乘客在哪裡登船。
import numpy as npimport matplotlib.pyplot as pltimport pandas as pdimport seaborn as sns# 應用預設主題sns.set()# 載入訓練資料train_data = pd.read_csv('./data/train.csv')sns.catplot(data=train_data, kind="count", x="Survived", col="Embarked")
字母S、C和Q代表南安普頓、瑟堡和皇后鎮。大多數乘客在南安普頓上船。在瑟堡上船的乘客從他們的同齡人中看來生存的機會稍好一些,但港口和生存之間似乎沒有很強的相關性。
有了這些見解,我們已經開始形成關於資料的假設,我們將在稍後進行測試。如果沒有視覺化的資料,我們就不會有同樣的直覺。
3.清理資料即使在最好的情況下,資料也很少是“乾淨的”,這意味著資料中可能存在缺失值或錯誤。其他時候,資料將以需要轉換、過濾或其他方式處理的單位記錄,然後才能進行任何進一步的工作。
資料清理沒有單一的方法。這取決於你的資料:
X = train_datay = train_data['Survived']X.drop(columns = ['Survived'], inplace=True)for col in X.columns: if X[col].isna().any(): print('Column "{}" is missing data.'.format(col))
此程式碼生成輸出:
Column "Age" is missing data.Column "Cabin" is missing data.Column "Embarked" is missing data.
處理NAN這裡我使用pandas來用.isna()方法檢查空值。缺少資料或空值將生成“NA”。如果需要數字資料,它會產生一個“NaN”,意思是“不是一個數字”。
泰坦尼克號的資料中有很多缺失的值。有時我們不知道一個人的年齡,或者他們住的是哪個艙室(如果有的話),或者他們的出發港是什麼。
這給我們留下了幾個選擇:
在訓練機器學習模型時,資料越多越好。如果你有很多幹淨的資料,扔掉任何不完整的樣本可能是好的。但是如果表中的每一行都是寶貴的,那麼最好找到值來填補。
泰坦尼克號的資料集不是很大。我們的訓練裝置裡只有不到1000名乘客。我們可能需要進一步細分訓練資料來驗證模型,這樣我們的訓練例子就更少了。
重新對映類別資料機器學習模型需要數值資料,但很多泰坦尼克號的資料是離散資料。我們需要把這些資料轉換成數字。
“Sex”列只有兩個值,女性和男性。我們可以把它們重新對映到0和1。
train_data.Sex = train_data.Sex.map({‘female’: 0, ‘male’: 1})
下面將使用一種稱為“one-hot”的技術來處理具有兩種以上可能性的分類資料,例如出發港(有3個可能的值)。
填補缺失值透過一些合理的假設,我們實際上可以很好地填補資料。
年齡我們可以在這裡使用幾種策略,例如簡單地用所有乘客的平均年齡填充缺失值。
但我們可以做得更好。
我的策略是觀察每一個船艙等級的人的平均年齡。
for pclass, grp in X.groupby('Pclass'): print('Class:', pclass, '-- Median Age:', grp.Age.median())
結果是:
Class: 1 -- Median Age: 37.0Class: 2 -- Median Age: 29.0Class: 3 -- Median Age: 24.0
當你進入二等艙和三等艙時,我發現頭等艙的乘客往往年齡偏大,而且年齡有下降的趨勢,對此我並不感到驚訝。
對於所有缺失的年齡值,我根據船艙等級給他們分配了中值。
def impute_age(row): if row['Pclass'] == 1: age = 37.0 elif row['Pclass'] == 2: age = 29.0 elif row['Pclass'] == 3: age = 24.0 return agemissing_ages = X.Age.isna()X.loc[missing_ages, 'Age'] = X[missing_ages].apply(lambda row: impute_age(row), axis=1)
你可以透過觀察男女的平均年齡,然後根據這兩個變數填寫缺失的資料,從而進一步改進這項技術。
登船港這裡沒有多少缺失的值。最常見的登船港是南漢普頓,所以在其他條件相同的情況下,乘客很可能在那裡登船。所有級別的乘客都是這樣。
missing_embarked = X.Embarked.isna()X.loc[missing_embarked, 'Embarked'] = 'S'
從船艙到甲板
我們表中的許多行都包含一個艙位號。最初還不清楚如何利用這些資訊,但我們可以根據船艙號來確定甲板。例如,“C22”在甲板C上。
客艙大多在B到F甲板上。關於船舶佈局的一些資訊可以在這裡找到。同一頁還顯示了一等艙、二等艙和三等艙的位置。
對於已知艙位號的乘客,我用它來推斷甲板。
對於沒有艙位號的乘客,我用他們的船艙等級來推斷他們最有可能佔用的艙位。
def infer_deck(row): if type(row['Cabin']) == str: deck = str(row['Cabin'])[0] else: deck = 'Unknown' return deckX['Deck'] = X.apply(lambda row: infer_deck(row), axis=1)for pc, grp in X.groupby('Pclass'): print('\n Class:', pc) print(grp['Deck'].value_counts()) # 對於每個類,根據從中推斷出的甲板佈局來計算缺失的甲板# https://www.dummies.com/education/history/titanic-facts-the-layout-of-the-ship/# Pclass 1: 'C'# Pclass 2: 'E'# Pcasss 3: 'F'def infer_deck_v2(row): if row['Pclass'] == 1: deck = 'C' elif row['Pclass'] == 2: deck = 'E' else: deck = 'F' return deckunknown_decks = X['Deck'] == 'Unknown'X.loc[unknown_decks, 'Deck'] = X[unknown_decks].apply(lambda row: infer_deck_v2(row), axis=1)X.drop(['Cabin'],axis=1, inplace=True)
輸出為:
Class: 1C 59B 47Unknown 40D 29E 25A 15T 1Name: Deck, dtype: int64 Class: 2Unknown 168F 8D 4E 4Name: Deck, dtype: int64 Class: 3Unknown 479F 5G 4E 3Name: Deck, dtype: int64
我在這裡的策略是檢視甲板佈局,看看大部分一等艙、二等艙和三等艙的位置。它似乎分別是C、E和F甲板,儘管我可能錯了。
對於所有未知艙位的乘客,我根據乘客船艙等級將他們分配到一個艙位。
解釋車票號碼我花了大量時間研究可以從罰單列中的值中收集到哪些資訊。
你會注意到有些票有一個字首,比如“S.C./PARIS”,後面跟著一個數字。字首和數字都能告訴我們一些東西。我猜字首是指售票員。從車票號碼本身我們有時可以推斷出一群人一起旅行。
我對字首資料做了一系列深入的清理和消除歧義,但最後,我還是放棄了它,因為它似乎沒有帶來任何結果。
4.假設檢驗既然我們已經清理了資料,我們可以嘗試一些簡單的測試。我們可以分離出測試集。對於這個分離出來的測試資料,我們知道基本的真實標籤,所以可以測量我們預測的準確性。
from sklearn.preprocessing import StandardScalerfrom sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_scoreXX = X[['Age','Sex']]std_scaler = StandardScaler()XX = std_scaler.fit_transform(XX)X_train, X_test, y_train, y_test = train_test_split(XX, y, test_size=0.25, random_state=42)
年齡和性別我們知道泰坦尼克號的倖存者乘救生艇逃離,而這些救生艇(我們假設)將優先裝滿婦女和兒童。我們能從這兩個變數中準確預測生存率嗎?
我們將使用logistic迴歸進行測試:
clf = LogisticRegression()clf.fit(X_train, y_train)y_pred = clf.predict(X_test)print(accuracy_score(y_pred, y_test))
準確度:
0.7847533632286996
78%的準確率相當不錯!顯然,這兩個變數是高度重要的,正如預期的那樣。
船艙等級接下來,我們可以假設頭等艙的乘客,因為他們的身份或者他們的艙室靠近上層甲板,可能更可能是倖存者,所以讓我們來看看是否只有艙位是一個很好的預測因素。然後我們再看看結合年齡和性別是否能改善之前的結果。
clf = LogisticRegression()# 需要將Pclass拆分為3個單獨的二元列X = pd.concat([X, pd.get_dummies(X['Pclass'], prefix='Pclass')], axis=1)X.drop(['Pclass'],axis=1, inplace=True)# 只保留該些列XX = X[['Pclass_1', 'Pclass_2', 'Pclass_3']]std_scaler = StandardScaler()XX = std_scaler.fit_transform(XX)X_train, X_test, y_train, y_test = train_test_split(XX, y, test_size=0.25, random_state=42)clf.fit(X_train, y_train)y_pred = clf.predict(X_test)print('Using Pclass as the sole predictor, our accuracy:')print(accuracy_score(y_pred, y_test))XX = X[['Age', 'Sex', 'Pclass_1', 'Pclass_2', 'Pclass_3']]std_scaler = StandardScaler()XX = std_scaler.fit_transform(XX)X_train, X_test, y_train, y_test = train_test_split(XX, y, test_size=0.25, random_state=42)clf.fit(X_train, y_train)y_pred = clf.predict(X_test)print('\nUsing Pclass, age, and sex as predictors, our accuracy:')print(accuracy_score(y_pred, y_test))
我不得不對Pclass變數進行one-hot編碼。我解釋一下下面的one-hot編碼是什麼,為什麼它很重要。從這些測試中,我得到的結果是:
Using Pclass as the sole predictor, our accuracy:0.6995515695067265Using Pclass, age, and sex as predictors, our accuracy:0.7937219730941704
因此,使用船艙等級作為唯一的預測因子,我們的logistic分類器的準確率接近70%。結合年齡和性別,我們在之前的結果上略有改善:79%比78%。這種差別不大,可能是噪音。
前幾項實驗告訴我們的是,生存在很大程度上取決於年齡、性別和社會經濟地位。單憑這三個因素,我們就有可能對生存率有一個相當好的預測。
但要想彌補最後幾個百分點的準確性,還需要一些特徵工程。
5.特徵工程真正優秀的特徵工程往往是專家與新手資料科學家的區別。任何人都可以使用現成的軟體庫,用幾行Python訓練機器學習模型,並使用它進行預測。但資料科學不僅僅是模型選擇。你需要為該模型提供高質量的預測特徵。
設計新特徵特徵工程通常意味著建立新的特徵來幫助機器學習模型做出更好的預測。有一些工具可以使這一過程自動化,但最好首先深入思考資料,以及可能導致目標結果的其他因素。
在我們的泰坦尼克號例子中,我們有一些資訊家庭一起旅行。“sibsp”和“parch”列告訴我們乘客有多少兄弟姐妹、配偶、父母和孩子。我們可以建立一個名為“Family Size”的新變數,它是“sibsp”和“parch”的總和。
X['Family Size'] = X['SibSp'] + X['Parch']
許多kaggler還會建立一個名為“not_only”的變數,它只是一個二元識別符號,用來描述乘客是否獨自旅行。
one-hot編碼這個特定的資料集包含大量的分類資料。考慮一下出發港。有3個可能的值:瑟堡、皇后鎮和南安普頓。我們需要輸入一個數值模型,以便操作:
{'Cherbourg': 1, 'Queenstown': 2, 'Southampton': 3}
但是想想這對機器學習模型來說是怎樣的。南安普敦的價值是瑟堡的3倍嗎?不,那太荒謬了。每個港口都同等重要。
相反,我們對這些分類資料執行“one-hot編碼”,這將建立三個新列,每個港口一個,我們將使用數字0或1來指示乘客是否在特定港口登船。
我們也可以對其他分類變數做同樣的處理,比如deck。性別在我們的資料集中是一個分類變數,但是由於我們的資料集中只有“女性”和“男性”,所以我們只使用0/1來表示。不需要建立新列。
one-hot編碼的一個主要缺點是它可以建立許多新列。每列都被視為一個單獨的特徵。很多特徵並不總是一件好事。資料中的示例數量應該大大超過特徵的數量。這有助於防止過擬合。
裝箱一些kaggler發現為年齡或票價範圍建立單獨的“箱子”是有幫助的。考慮到,當救生艇裝滿水時,船員們可能不是問年齡,而是考慮諸如“嬰兒”、“兒童”、“年輕人”、“老年人”等年齡類別。你可以建立類似的容器,看看這是否有助於你的模型。我將保留年齡和票價變數不變。
6.選擇模型以上步驟實際上是最困難的部分,大約佔工作的80%到90%。
接下來的幾步通常更容易,也更有趣。我們可以嘗試不同的機器學習模型,看看它們的效能如何,並選擇一個有希望的模型來進一步最佳化。
由於我們只是試圖預測一個二元變數“生存”,任何一個二元分類器都可以工作。如果你使用scikit learn,你有很多選擇。
邏輯迴歸決策樹隨機森林AdaBoostXGBoost最後一個XGBoost,不是scikit learn的一部分,所以你必須單獨安裝它。
交叉驗證是一種技術,其中一小部分資料被排除在外,而模型則根據剩餘的資料進行訓練。然後根據遺漏的資料對模型的準確性進行測試。這個過程重複k次,每次隨機抽取遺漏的部分資料。
這項技術的重點是幫助避免過擬合。在你熱衷於以最高精度設計完美模型的過程中,你可能會無意中建立了一個模型,而該模型並不能泛化到樣本之外的資料。因此,你總是需要根據樣本之外的資料來測試模型。
在本文中,我不介紹每個分類器的機制,但是這些資訊很容易找到。
from sklearn.preprocessing import StandardScalerfrom sklearn.model_selection import cross_validatefrom sklearn.linear_model import LogisticRegressionfrom sklearn.tree import DecisionTreeClassifierfrom sklearn.ensemble import RandomForestClassifier, AdaBoostClassifierimport xgboost as xgbX.drop(columns=['PassengerId', 'Name'], inplace=True)std_scaler = StandardScaler()X = std_scaler.fit_transform(X)X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)def assess_model(y_test, y_pred): scores = cross_validate(clf, X, y, cv=5, scoring=('accuracy'), return_train_score=True) train_avg = np.mean(scores['train_score']) train_std = np.std(scores['train_score']) test_avg = np.mean(scores['test_score']) test_std =np.std(scores['test_score']) print('Average Train Accuracy: {:5.3f} ±{:4.2f}'.format(train_avg, train_std)) print('Average Test Accuracy: {:5.3f} ±{:4.2f}'.format(test_avg, test_std)) return clf = LogisticRegression(class_weight='balanced', max_iter=1000, solver='lbfgs')clf.fit(X_train, y_train)y_pred = clf.predict(X_test)assess_model(y_test, y_pred)clf = DecisionTreeClassifier(random_state=0)clf.fit(X_train, y_train)y_pred = clf.predict(X_test)assess_model(y_test, y_pred)clf = RandomForestClassifier(random_state=0)clf.fit(X_train, y_train)y_pred = clf.predict(X_test)assess_model(y_test, y_pred)clf = AdaBoostClassifier(random_state=0)clf.fit(X_train, y_train)y_pred = clf.predict(X_test)assess_model(y_test, y_pred)clf = xgb.XGBClassifier()clf.fit(X_train, y_train)y_pred = clf.predict(X_test)assess_model(y_test, y_pred)
我的assess_model函式使用5倍交叉驗證來測試訓練集和測試集上每個分類器的準確性。模型的真正價值在於它在測試集上的表現。每一個模型的準確度如下:
# 邏輯迴歸Average Train Accuracy: 0.802 ±0.01Average Test Accuracy: 0.780 ±0.02# 決策樹Average Train Accuracy: 0.974 ±0.00Average Test Accuracy: 0.781 ±0.02# 隨機森林Average Train Accuracy: 0.974 ±0.00Average Test Accuracy: 0.791 ±0.03# AdaBoostAverage Train Accuracy: 0.832 ±0.00Average Test Accuracy: 0.804 ±0.02# XGBoostAverage Train Accuracy: 0.964 ±0.00Average Test Accuracy: 0.820 ±0.03
需要注意的是,我們使用了這些分類器的“開箱即用”版本。它們對所有內部可調引數使用預設值。
注意分類器在訓練集上的準確率有時達到97%。那看起來很棒,但過擬合了。測試準確度較低,測試準確度是我們更關心的變數。
在這些分類器中,XGBoost的測試精度最高。這就是為什麼Kaggler喜歡XGBoost的原因之一
7.模型最佳化大多數機器學習模型都有可調引數,這些引數通常會影響模型的精度。對於每個問題,這些引數的最佳執行值都是不同的。
在ML中,這些引數通常被稱為“超引數”,調整它們既是科學又是藝術。
有一些工具可以幫助調整。TPOT就是一個例子。但是為了簡單起見,我們將執行一個簡單的網格搜尋,並手動測試一系列合理的超引數值,看看哪一個給了我們最好的結果。
scikit-learn有一個方便的工具來執行網格搜尋。
from sklearn.model_selection import GridSearchCVparam_grid = {'bootstrap': [True], 'max_depth': [2, 6, None], 'max_features': ['auto', 'log2'], 'min_samples_leaf': [1, 2, 3, 5], 'min_samples_split': [2, 4, 6], 'n_estimators': [100, 350] } forest_clf = RandomForestClassifier()forest_grid_search = GridSearchCV(forest_clf, param_grid, cv=5, scoring="accuracy", return_train_score=True, verbose=True, n_jobs=-1)forest_grid_search.fit(X_train, y_train)
這段程式碼需要一些時間來執行,因為它嘗試了超引數的所有不同組合。
可以修改引數網格以更改搜尋空間。記住,這種技術不會找到最佳的超引數值,只能從搜尋空間中找到最佳組合。
print(forest_grid_search.best_params_)print(forest_grid_search.best_estimator_)print(forest_grid_search.best_score_)
透過這種方法,我們可以得到:
{'bootstrap': True, 'max_depth': 6, 'max_features': 'auto', 'min_samples_leaf': 2, 'min_samples_split': 4, 'n_estimators': 100}RandomForestClassifier(max_depth=6, min_samples_leaf=2, min_samples_split=4)0.830804623499046
結論
如果不作弊,你很難在卡格爾泰坦尼克號挑戰賽的分數超過83%。我還提到了其他一些提高成績的方法,比如年齡或票價劃分,以及基於船艙等級和性別的缺失年齡值的插補。你可以試試這些,看看它們是否能提高之前度。
總之,解決資料科學問題是一個循序漸進的過程,從零開始,瞭解資料,清理資料,然後反覆測試不同的模型並新增更多的特徵,直到獲得良好的模型效能。從那裡,你可以最佳化你最好的模型,從中擠出更多的效能。
接下來會發生什麼?如果你是一個研究人員,你就公佈你的結果。如果一個企業家願意為你的模型付出金錢的話。如果你是一個Kaggler,你會提交你的測試預測來獲得榮譽(有時還包括金錢)。