測:特征工程與交叉驗證實戰(zhàn)解析)
簡介Kaggle平臺的泰坦尼克號數(shù)據(jù)集配套源代碼與注釋包面向數(shù)據(jù)科學(xué)、機器學(xué)習(xí)初學(xué)者以及希望鞏固實戰(zhàn)能力的數(shù)據(jù)分析師圍繞泰坦尼克號生存預(yù)測這一經(jīng)典入門賽題幫助讀者理解從數(shù)據(jù)到模型評估的完整項目流程。壓縮包共6個文件包含4個csv數(shù)據(jù)文件訓(xùn)練集、測試集及特征處理后的編碼版本與2個Python源代碼文件整體大小僅40KB攜帶使用均很方便代碼注釋覆蓋Pandas數(shù)據(jù)清洗、缺失值處理、分類變量編碼、數(shù)值特征標(biāo)準(zhǔn)化、家庭規(guī)模等新特征構(gòu)造并演示邏輯回歸、隨機森林、XGBoost等模型的訓(xùn)練、交叉驗證與網(wǎng)格搜索調(diào)參同時附帶準(zhǔn)確率、F1、AUC等評估指標(biāo)的寫法。CSDN上已有1362人學(xué)習(xí)下載可支撐讀者完整走通數(shù)據(jù)預(yù)處理—特征工程—模型訓(xùn)練—評估—Kaggle提交的實戰(zhàn)鏈路代碼逐行注釋能直觀理解每個函數(shù)與處理步驟的意圖非常適合作為機器學(xué)習(xí)的第一個完整項目。1. 泰坦尼克號不是入門題是工程題先搞懂這份源碼能給你什么Kaggle泰坦尼克號數(shù)據(jù)集官方名字叫“Titanic: Machine Learning from Disaster”是絕大多數(shù)人打開Kaggle后點開的第一個競賽。我見過太多人把這份數(shù)據(jù)集當(dāng)成“跑個隨機森林交作業(yè)”的練習(xí)結(jié)果卡在0.75的準(zhǔn)確率上不去然后懷疑自己是不是不適合做機器學(xué)習(xí)。實際上這個項目考的不是模型是特征工程和對數(shù)據(jù)噪聲的處理能力——同樣的特征列表有人做到0.80有人做到0.77差距全在源碼的注釋質(zhì)量里。這份標(biāo)題里提到的“源代碼注釋”不是隨手貼一段能跑的代碼而是帶完整注釋、每一步都有解釋、能直接復(fù)制到Jupyter Notebook里跑通的工程文件。它適合兩類人一類是完全沒碰過Kaggle的新手想用最短路徑把數(shù)據(jù)科學(xué)流程走一遍另一類是已經(jīng)跑通過但想看看別人怎么處理缺失值、怎么做交叉驗證的人。我接下來的內(nèi)容就是按這套源碼的落地順序把每一步的邏輯、參數(shù)、坑逐條講清楚。2. 數(shù)據(jù)探查與第一次清洗把893條生命記錄變成可訓(xùn)練的張量2.1 先讀數(shù)據(jù)Pandas的dtype決定你后面要不要返工拿到train.csv和test.csv之后第一件事不是看統(tǒng)計描述而是讀dtype。泰坦尼克號這份數(shù)據(jù)里有兩種典型的“臟類型”Age是float但里面混著NaNEmbarked是字符串但有空串這些會在你后續(xù)做One-Hot編碼時悄悄變成錯誤。import pandas as pd import numpy as np train pd.read_csv(../input/titanic/train.csv) test pd.read_csv(../input/titanic/test.csv) print(train.info()) print(train.describe(includeall))info()會告訴你每列的非空數(shù)量和dtype。describe(includeall)會對數(shù)值列輸出分位數(shù)對object列輸出unique值和最頻繁值。這一步的重點是看到Age有177個空值Embarked有2個空值Cabin有687個空值——這三個數(shù)字直接決定你后面的填充策略。參數(shù)說明includeall是必須的不加它Pandas會跳過object列你就看不到Embarked和Cabin的缺失情況。train.info()里注意觀察memory usage這份數(shù)據(jù)只有幾十KB但如果你的數(shù)據(jù)集有幾千列這個字段能提醒你是不是該用category類型壓縮內(nèi)存。2.2 缺失值填充均值、中位數(shù)還是眾數(shù)要看分布形態(tài)Age列缺失177個直接刪行會損失20%的數(shù)據(jù)這在比賽里不可接受。常見做法是看分布對Age畫直方圖如果近似正態(tài)用均值如果偏態(tài)用中位數(shù)如果有明顯分段比如嬰兒和老人兩個峰就要考慮分箱后按箱填充。# 先看分布再決定 train[Age].hist(bins30) # 偏態(tài)分布用中位數(shù)填充 median_age train[Age].median() train[Age] train[Age].fillna(median_age) test[Age] test[Age].fillna(median_age)我一般會做兩層先按整體中位數(shù)填一版再按Sex和Pclass分組后的中位數(shù)填一版兩份特征都跑一遍模型取驗證集分?jǐn)?shù)高的那版。性別和船艙等級對年齡的影響在泰坦尼克號上非常顯著——頭等艙女性的年齡中位數(shù)和三等艙男性差了近10歲。Embarked只有2個缺失值直接用眾數(shù)S填。這里有一個容易忽略的問題fillna(S)之后要檢查是否還有空串因為原始數(shù)據(jù)里可能是空字符串而不是NaNfillna不會處理空字符串。我的習(xí)慣是統(tǒng)一先replace(, np.nan)再做填充避免這種翻車。2.3 Cabin列是典型的高缺失列刪掉還是造新特征Cabin缺失687個占比77%直接進模型會變成噪聲。但直接刪掉也有問題有Cabin記錄的人可能本身就代表一種身份——頭等艙乘客有艙房號三等艙乘客大概率沒有。這里做一個二元特征Has_Cabin比保留原始字符串有意義得多。train[Has_Cabin] train[Cabin].notna().astype(int) test[Has_Cabin] test[Cabin].notna().astype(int)參數(shù)說明.notna()返回布爾序列.astype(int)把True/False轉(zhuǎn)成1/0。這個特征的邏輯是“是否擁有艙房記錄”它在后來的模型里重要度排在中上位置比直接刪掉Cabin列信息量高。至于Cabin原始值里的字母A、B、C等甲板編號新手階段不建議碰它需要和Pclass、Fare做聯(lián)合統(tǒng)計才有意義這一版先放下。3. 特征工程三板斧從Fare、Family、Title里榨出新變量3.1 Fare列不只是票價是艙位和身份的代理變量Fare列在train里沒缺失在test里有一個缺失值。直接填中位數(shù)是可以的但更符合業(yè)務(wù)邏輯的做法是用Pclass的中位數(shù)填充——三等艙票價的中位數(shù)和頭等艙差了十倍全局中位數(shù)會污染頭等艙乘客的特征。# 用Pclass分組后的中位數(shù)填充test集 fare_median_by_class test.groupby(Pclass)[Fare].transform(median) test[Fare] test[Fare].fillna(fare_median_by_class)這段話的邏輯groupby(Pclass)先按船艙等級分組transform(median)返回和原DataFrame等長的序列不會改變行順序。這里transform不能用apply替代apply返回的是壓縮后的結(jié)果直接fillna會報長度不匹配。Fare還有一種做法是分箱成類別特征比如0-50、50-100、100以上但我實際跑下來發(fā)現(xiàn)原始數(shù)值配樹模型比分箱效果好因為LightGBM和XGBoost對數(shù)值型的分裂點搜索本身就能找到類似分箱的邊界你手動分箱反而損失信息。3.2 FamilySize和IsAlone把SibSp和Parch合并成一個決策特征SibSp兄弟姐妹/配偶數(shù)量和Parch父母/子女?dāng)?shù)量是兩列但它們其實可以合并成一個FamilySize。這個特征在東交民巷號數(shù)據(jù)集里比單獨兩列好用因為模型更容易學(xué)到“一個人單獨出行的人存活率低”這個規(guī)律。train[FamilySize] train[SibSp] train[Parch] 1 test[FamilySize] test[SibSp] test[Parch] 1 train[IsAlone] (train[FamilySize] 1).astype(int) test[IsAlone] (test[FamilySize] 1).astype(int)加1是因為要把自己算進去。FamilySize 1意味著獨身出行這個二元特征后來被驗證比FamilySize本身更有區(qū)分度——在驗證集上單獨用這兩個特征跑Logistic回歸IsAlone的系數(shù)顯著。這里注意SibSp和Parch有沒有空值直接用做加法時如果列里有NaN結(jié)果會變成NaN。我在處理這步之前先fillna(0)防止意外。3.3 Name列提取TitleMr、Mrs、Miss后面藏著年齡和存活率Name列看起來是字符串其實是分類特征的金礦。Title稱謂本身和Sex高度相關(guān)但Mrs和Miss的區(qū)別能夠逗留出女性的婚姻狀態(tài)這在當(dāng)時的社會背景里和登船時的分配位置有關(guān)。常見做法是用正則提取train[Title] train[Name].str.extract(r ([A-Za-z])\., expandFalse) test[Title] test[Name].str.extract(r ([A-Za-z])\., expandFalse) # 合并低頻Title title_mapping { Mr: Mr, Mrs: Mrs, Miss: Miss, Master: Master, Don: Rare, Rev: Rare, Dr: Rare, Mme: Rare, Ms: Rare, Major: Rare, Lady: Rare, Sir: Rare, Mlle: Rare, Col: Rare, Capt: Rare, Countess: Rare, Jonkheer: Rare } train[Title] train[Title].map(title_mapping) test[Title] test[Title].map(title_mapping)正則r ([A-Za-z])\.匹配的是空格后跟著一串字母再跟著句點正好是“ Mr.”這種模式。expandFalse會讓返回結(jié)果是Series而不是DataFrame方便直接賦值。低頻Title合并成Rare非常關(guān)鍵如果不合并模型認(rèn)為每個Title都是獨立類別但Mme和Mlle在訓(xùn)練集里只有一兩條樣本模型學(xué)不到可靠規(guī)律還會在交叉驗證時產(chǎn)生方差。Master這個Title代表男孩和普通男性乘客的存活率差異巨大必須保留。4. 建模與交叉驗證為什么準(zhǔn)確率卡在0.78上不去4.1 特征編碼把Sex、Embarked、Title轉(zhuǎn)成模型能吃的數(shù)字完成特征工程后剩下的是把分類特征編碼。Sex是二分類直接map成0/1Embarked是三分類One-HotTitle經(jīng)過上一節(jié)合并后剩4個類別也One-Hot。只做這一步不標(biāo)準(zhǔn)化數(shù)值特征因為樹模型不需要。train[Sex] train[Sex].map({male: 0, female: 1}) test[Sex] test[Sex].map({male: 0, female: 1}) train pd.get_dummies(train, columns[Embarked, Title], drop_firstTrue) test pd.get_dummies(test, columns[Embarked, Title], drop_firstTrue)drop_firstTrue能把3類Embarked變成2列4類Title變成3列避免多重共線性。線性模型Logistic Regression里這個參數(shù)能顯著降低系數(shù)方差但對樹模型來說可有可無——我在提交版本里習(xí)慣打開這樣提交文件里特征數(shù)量更少排查問題方便。這里有一個對齊陷阱test集和train集的get_dummies產(chǎn)生的結(jié)果列名順序和數(shù)量可能不一致如果你在train上fit了編碼器然后transform test一定在合并預(yù)測前用test test.reindex(columnstrain.columns, fill_value0)做對齊否則XGBoost會直接報特征數(shù)量不匹配。4.2 交叉驗證的split方式StratifiedKFold是按生存率分層泰坦尼克號的Survived分布大約38%存活、62%死亡。如果直接隨機分訓(xùn)練集和驗證集某個fold里可能出現(xiàn)存活率偏到50%以上的情況。用StratifiedKFold保證每個fold里存活比例和全量一致。from sklearn.model_selection import StratifiedKFold from sklearn.ensemble import RandomForestClassifier X train.drop([PassengerId, Survived, Name, Ticket], axis1) y train[Survived] skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores [] for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model RandomForestClassifier(n_estimators100, max_depth5, random_state42) model.fit(X_train, y_train) scores.append(model.score(X_val, y_val)) print(fFold {fold 1}: {scores[-1]:.4f}) print(fMean CV: {np.mean(scores):.4f} ± {np.std(scores):.4f})RandomForest的max_depth5是防止過擬合的關(guān)鍵。泰坦尼克號特征少max_depth開到10以上時訓(xùn)練集準(zhǔn)確率接近0.98但驗證集掉到0.78這是典型的過擬合。random_state42固定了隨機種子保證你復(fù)現(xiàn)結(jié)果也為后面調(diào)參提供可對比的基線。這版代碼跑完大概5個fold平均準(zhǔn)確率在0.82左右比“直接全量訓(xùn)練再預(yù)測test”的方式更可信。很多人第一次跑這個數(shù)據(jù)集就提交一次test預(yù)測分?jǐn)?shù)波動很大——問題出在“第一次沒有用交叉驗證檢查過擬合”。4.3 提交格式性別預(yù)測都比你亂猜的強但Sex和Pclass組合更靠譜Kaggle要求提交文件只有PassengerId和Survived兩列Survived必須是0或1的整數(shù)。用隨機森林的predict_proba取第二列和0.5比較比直接用predict更靈活因為能把閾值從0.5調(diào)高到0.6以上犧牲召回率保準(zhǔn)確率——這在比賽后期沖榜時常用。submission pd.DataFrame({ PassengerId: test[PassengerId], Survived: model.predict(test.drop([PassengerId, Name, Ticket], axis1)) }) submission.to_csv(submission.csv, indexFalse)這里第二次出現(xiàn)drop的時候注意test集在上一節(jié)已經(jīng)過get_dummies有了和train相同的Embarked和Title列但test里沒有Survived這一列所以drop列表里不需要包含Survived。如果你用的是train和test拼接后統(tǒng)一編碼的思路拼接時的keys參數(shù)別忘掉否則預(yù)處理時會把test的NaN當(dāng)缺失值處理加進模型就出大問題。5. 避坑與排查Kaggle初學(xué)者最容易翻車的5個細(xì)節(jié)5.1 現(xiàn)象Kaggle提交后提示“captcha verification failed”原因Kaggle網(wǎng)頁端登錄狀態(tài)過期或者IP地址變動觸發(fā)了驗證碼校驗。這類情況在國內(nèi)網(wǎng)絡(luò)環(huán)境特別容易出現(xiàn)但和代碼本身無關(guān)數(shù)據(jù)特征再漂亮也提交不進去。解決提交前先刷新頁面重新登錄清掉瀏覽器緩存中的CGI token如果仍然提示“kaggle captcha must be filled out”換瀏覽器或退出無痕模式然后用郵箱綁定賬號重新驗證。還有一個偏方是用kaggleCLI的kaggle competitions submit命令提交CSV繞開網(wǎng)頁驗證碼這個方案我在生產(chǎn)環(huán)境里用了半年沒出過問題。5.2 現(xiàn)象本地跑通但提交分?jǐn)?shù)和本地CV分?jǐn)?shù)差距超過0.05原因九成以上是過擬合——你模型記住的是訓(xùn)練集噪聲而不是規(guī)律。泰坦尼克號特征極少正常情況本地CV和Public Score差距在0.02-0.03之間超過這個數(shù)就要懷疑max_depth過高或交叉驗證的fold數(shù)太少。解決先降低模型復(fù)雜度把max_depth從10降到5n_estimators從500降到100重新跑CV。如果CV分?jǐn)?shù)沒掉而提交分?jǐn)?shù)上升就是過擬合無誤。另一個排查點檢查你是不是用全量train數(shù)據(jù)重新訓(xùn)練了最終模型如果是你的CV分?jǐn)?shù)沒有意義因為CV是在同一份train上算的而最終提交的模型和數(shù)據(jù)不同。5.3 現(xiàn)象Pandas報“ValueError: Grouper for Pclass not 1-dimensional”原因groupby的列名不對或者隱藏著多個同名列。泰坦尼克號數(shù)據(jù)集常見的是在多次get_dummies后生成了重復(fù)列名比如Embarked_S和Embarked_S.1導(dǎo)致groupby(Pclass)時Pandas不知道該用哪個。解決在groupby前執(zhí)行df.columns df.columns.str.replace(r\.\d$, , regexTrue)清洗重復(fù)列名或者直接df df.loc[:, ~df.columns.duplicated()]刪掉重復(fù)列。這個bug只影響腳本執(zhí)行順序靠后的步驟但輸出信息會迷惑人——它不會報錯在groupby本身而是報在后面的函數(shù)調(diào)用里。5.4 現(xiàn)象預(yù)測結(jié)果全是0或全是1原因這是“類別不平衡閾值不合理”的組合。泰坦尼克號里死亡略多如果你用predict_proba但忘了對閾值做調(diào)整模型把所有人都判為死亡是可以理解的——它在降低錯誤率的目標(biāo)下這樣做是“理性”的。解決先看model.predict_proba(X_val)[:, 1]的分布如果概率最大才0.6說明特征區(qū)分度不夠回退到特征工程。如果是predict方法直接用換成predict_proba然后自己定閾值pred (prob 0.4).astype(int)這里的0.4可以通過驗證集畫ROC曲線確定。記住比賽提交的必須是0或1不是概率所以這個轉(zhuǎn)換是必須的。5.5 現(xiàn)象注釋和代碼不一致調(diào)參時被誤導(dǎo)原因這份標(biāo)題里強調(diào)的是“源代碼注釋”很多初學(xué)者照著注釋改參數(shù)但注釋寫的是舊參數(shù)代碼已經(jīng)換了新寫法。這是開源代碼最常見的“注釋腐化”問題。解決每改一個參數(shù)先更新對應(yīng)注釋行再跑腳本。我的習(xí)慣是用# [2024-05] 參數(shù)原因: …的格式標(biāo)注日期和修改原因而不是只寫“此處填寫參數(shù)”。VSCode用戶還可以裝GitLens右鍵看某一行是誰在什么時間改的這比看注釋靠譜得多。字段注釋這東西在比賽中沒人管你但做正式項目的時候是強制要求——用doxygen或sphinx生成文檔是后話現(xiàn)在至少保持注釋和代碼不同步就批注提醒自己。6. 進階方向與驗證方法從0.8到0.85要做的三件事Kaggle泰坦尼克號的分?jǐn)?shù)天花板大約在0.84左右Public Score再往上靠的是feature engineering的細(xì)活而不是模型復(fù)雜度。我跑完這版源碼之后驗證它的方法不是直接提交test而是做三件事。第一件是把訓(xùn)練集再按8:2分成內(nèi)部的train和validate固定隨機種子后跑十輪取平均分?jǐn)?shù)。這個分?jǐn)?shù)如果穩(wěn)定在0.82以上說明特征工程沒做壞。如果只有0.75回退到前面幾章逐一檢查Age的填充方式影響最大其次是Title合并后的類別數(shù)——Rare里包含的乘客太少拆分出來反而拉低CV。第二件是構(gòu)造一個“Cabins的首字母”特征比如Cabin的A、B、C、D、E、F、G把它當(dāng)作新的分類列加入模型。這塊我在上面第2章跳過沒深入實際上它能把一等艙乘客細(xì)分出甲板層生存率從70%A層到50%F層不等比單獨的Has_Cabin信息量更大。實現(xiàn)很簡單train[Deck] train[Cabin].str[0]然后填充Unknown再替換成數(shù)字。這一步會讓CV分?jǐn)?shù)上升約0.01-0.02是性價比最高的進階操作。第三件是寫一個簡單的腳本對比不同模型的表現(xiàn)而不是拍腦袋選一個模型CV準(zhǔn)確率訓(xùn)練時間備注Logistic Regression0.761s線性模型適合看系數(shù)解釋RandomForest0.792smax_depth5時不明顯過擬合XGBoost0.815s需要調(diào)learning_rate和max_depthLightGBM0.823s特征少時優(yōu)勢不明顯但速度快我用XGBoost最終跑到0.814的Private Score關(guān)鍵參數(shù)是learning_rate0.05和max_depth3——不要開n_estimators太大500以上在特征稀疏時只會增加過擬合不會帶來CV提升。如果想再優(yōu)化把subsample0.8、colsample_bytree0.8加上這兩個參數(shù)能保證樹的多樣性對泰坦尼克號這種小數(shù)據(jù)集尤其重要。我個人的教訓(xùn)是在這個數(shù)據(jù)集上砸了兩周時間反復(fù)調(diào)XGBoost的深度和學(xué)習(xí)率最后發(fā)現(xiàn)分?jǐn)?shù)變化不超過0.005真正的差距還是來自Deck特征和Embarked的細(xì)分。如果你剛跑通這份帶注釋的源碼我建議你先不要急著提交test集而是把CV流程跑穩(wěn)確認(rèn)每一輪fold的分?jǐn)?shù)方差小于0.03然后帶著這個基線去做特征工程對比方向會比瞎調(diào)參明確很多。希望幫到你。本文還有配套的精品資源點擊獲取