:從數(shù)據(jù)清洗到誤差復盤的完整流程)
我第一次正兒八經(jīng)用機器學習做項目選的就是房屋銷售價格預測。當時心里想得很簡單找個數(shù)據(jù)集跑個回歸看個R2完事。結(jié)果真上手才發(fā)現(xiàn)光是把數(shù)據(jù)洗干凈就花了兩天后面調(diào)模型的時間反而不是最多的。這個項目之所以值得寫是因為它幾乎覆蓋了機器學習應用流程里所有關(guān)鍵環(huán)節(jié)數(shù)據(jù)理解、清洗、特征工程、模型選型、驗證策略、誤差復盤。而且房價預測的業(yè)務直覺人人都有——不用解釋什么叫成交價每個人都能對“哪些因素影響房價”說出幾句。但恰恰因為太熟悉很多細節(jié)容易被忽略。這篇文章適合三類人剛?cè)腴T機器學習、想找一個完整項目練手的同學已經(jīng)會調(diào)包但沒做過完整比賽的初學者以及工作中偶爾需要處理回歸問題、想看一套可復盤的工程思路的開發(fā)者。我會盡量把每一步的“為什么”也講清楚而不是只給代碼。1. 先估摸清楚數(shù)據(jù)成交價到底被哪些字段牽著走1.1 字段概覽與第一眼直覺哪些列天生就是“錢”我當時用的是一份公開的房屋銷售數(shù)據(jù)樣本量不大1460條左右輸入字段接近80個。這個規(guī)模放在今天的深度學習任務面前簡直不夠看但對樹模型和線性模型來說是一個很典型的“中等偏小”的表格數(shù)據(jù)集。拿到數(shù)據(jù)第一件事不是跑模型而是把所有字段按業(yè)務直覺分成幾類。我會在紙上列一下面積類、房間數(shù)量類、質(zhì)量評分類、地段類、時間類、外部設(shè)施類。面積類里面地上居住面積GrLivArea幾乎肯定是核心變量因為房子的物理大小直接決定價格。質(zhì)量評分類OverallQual也是1到10的整數(shù)分跟價格的條線關(guān)系肉眼可見。地段類在真實業(yè)務里通常是最重要的因子但這套數(shù)據(jù)里只有鄰居劃分Neighborhood沒有更精細的坐標信息后面得靠它來近似地段效應。分類之后我習慣馬上做一次相關(guān)性掃描。用pandas的corr()掃一遍最大的幾個相關(guān)性基本不會出人意料總體面積、質(zhì)量評分、地下室面積、車庫面積。這一步的意義不是發(fā)現(xiàn)什么新大陸而是建立“模型是否學歪了”的參照系。如果后面訓練出來的模型特征重要性排名跟業(yè)務常識嚴重沖突那就說明數(shù)據(jù)或流程出了問題而不是模型發(fā)現(xiàn)了什么人類不知道的規(guī)律。1.2 時間維度、地段缺失與樣本量三個容易埋雷的前提這份數(shù)據(jù)有個隱蔽的問題時間跨度。房屋的出售月份分布在一年內(nèi)的不同月份年份也可能跨了好幾年。如果不處理時間維度模型會默認“所有年份的房價水平一樣”這在市場波動大的時候會直接導致預測偏差。我當時沒有拿到精確的成交年份只有年份和月份的組合于是干脆用“銷售年份月份”拼成一個數(shù)值特征雖然粗糙但至少給模型一個捕捉時間趨勢的機會。更隱蔽的坑是缺失值。很多人一看到NaN就想到“填充”但房價數(shù)據(jù)里的缺失往往帶著業(yè)務語義。這點我會在下一節(jié)詳細講這里先提一句很多列的缺失在原始問卷里意味著“沒有這項設(shè)施”而不是“數(shù)據(jù)丟了”。用中位數(shù)或均值去填反而會把真正有價值的信息抹掉。樣本量1460條也決定了后面模型選型的天花板。樹模型在幾千條樣本上表現(xiàn)穩(wěn)定深度學習在這種規(guī)模下很容易過擬合除非做大量數(shù)據(jù)增強否則性價比極低。這一點在后面選型時是決定性因素。2. 數(shù)據(jù)清洗的心法缺失值不是填空題是判斷題2.1 各列缺失值的真實語義沒有泳池不等于泳池質(zhì)量差這套數(shù)據(jù)里有一堆列比如泳池質(zhì)量、地下室類型、車庫完成年份、門廊面積。它們的NaN含義完全不一樣。泳池質(zhì)量這個字段如果房子沒有泳池那確實該是缺失但填充成“無”或者“0質(zhì)量分”才是對的用中位數(shù)填一個5分進去等于憑空給沒有泳池的房子加了泳池。地下室也一樣很多房子沒有地下室那這類字段要么填“無”要么填0面積絕不能填均值。我的處理原則是先看每一列的缺失率再翻數(shù)據(jù)說明文檔判斷缺失是“結(jié)構(gòu)性缺失”還是“隨機缺失”。結(jié)構(gòu)性缺失比如沒有車庫導致車庫相關(guān)字段全空就統(tǒng)一填0或“None”。隨機缺失比如某個質(zhì)量評分為空但房子確實存在才考慮眾數(shù)或模型預測填充。實際操作里我把大量時間花在了區(qū)分這兩類情況上因為填錯一個就可能引入系統(tǒng)性偏差。還有一類字段很典型比如“裝修年份”缺失。這種情況往往意味著房子從未翻新這時候填0反而不對因為0會被模型當成“公元0年裝修”。更好的做法是填“等于建造年份”表示房子維持原樣。這一類細節(jié)如果沒人提醒新手很容易踩坑。2.2 偏態(tài)數(shù)值與類別編碼log變換和目標編碼的高危操作數(shù)值特征里面積、價格這類量級差異大的變量幾乎都呈右偏分布。右偏意味著大部分樣本集中在低值區(qū)少數(shù)樣本拖著一條長尾巴。這種分布對線性模型特別不友好因為極端值會拽著擬合線走。處理辦法是取對數(shù)或者做Box-Cox變換。比如地塊面積原始尺度下從幾千到幾萬不等取log之后就平滑很多。但要注意log變換不是所有數(shù)值特征都要做。計數(shù)類的特征比如衛(wèi)生間數(shù)量、臥室數(shù)量本身是離散的且取值少做log反而畫蛇添足。我一開始圖省事對所有數(shù)值列無腦log結(jié)果模型效果反而下降因為有些特征本來就接近正態(tài)變換后分布反而畸形了。類別特征的編碼也是重災區(qū)。Neighborhood這個字段有二十幾個取值直接LabelEncoder會強加一個虛假的排序關(guān)系模型會以為鄰居A比鄰居B“大”。我最后用的是目標編碼用該類別下目標變量的均值經(jīng)過平滑來替代原始類別。這個技巧對房價這類有強地段效應的數(shù)據(jù)特別有效。但目標編碼有一個致命風險——數(shù)據(jù)泄漏。如果先用全量數(shù)據(jù)計算均值再切分訓練集和驗證集模型就提前看到了驗證集里的地段價格水平本地驗證會虛高。正確做法是在交叉驗證的每一折內(nèi)單獨計算或者用保留均值的方式。我后面在第四節(jié)會具體講怎么在交叉驗證里安全地做目標編碼。2.3 手工特征組合總面積、房齡、翻新年限的加法與減法原始數(shù)據(jù)里有很多信息藏在不同列的“加法關(guān)系”里。最典型的是總居住面積一層面積、二層面積、地下室面積分開給但業(yè)務上人們更關(guān)心的是全屋總面積。于是我構(gòu)造了總地面面積和總建筑面積兩個組合特征。這類特征對樹模型來說尤其有效因為樹模型本質(zhì)上是在做條件分裂給它一個已經(jīng)加好的總面積相當于幫它省去了一次隱式組合的嘗試分裂效率更高。房齡也是一個必需的特征。原始數(shù)據(jù)里只有建造年份沒有“賣房時房子多少歲”這個直感變量。我用銷售年份減建造年份得到房齡再用“是否翻新過”作為開關(guān)如果翻新過就生成一個新特征“翻新后房齡”否則就等于原始房齡。這樣模型就能區(qū)分“老房子但剛翻新”和“老房子沒動過”——前者的價格往往比后者高出一截這個差異在純原始字段里很難被樹模型發(fā)現(xiàn)。組合特征不是越多越好。我試過把車庫面積和門廊面積相加結(jié)果模型根本沒用到這個特征因為兩份面積加在一起之后噪音占了多數(shù)。做組合特征的判斷標準很簡單這個組合在業(yè)務上是否站得住腳、直覺上是否直接影響買家的出價。沒有業(yè)務依據(jù)的組合加了反而是過擬合的來源。3. 先別上模型用基線和線性回歸探明“基準水位”3.1 均值模型和單變量回歸給自己一個可接受的底真正開始建模前我習慣先建立一個最傻的基線用訓練集目標變量的均值去預測所有樣本。這個模型不看任何特征它的誤差就是后續(xù)所有模型的“及格線”。在log變換后的價格上均值模型的RMSE大概在0.4左右。看到這個數(shù)字后續(xù)模型只要能明顯低于0.4就說明特征有信號否則說明數(shù)據(jù)出問題了?;€建立之后再做一次單變量線性回歸只看總體面積這一個特征。結(jié)果R2能到0.6左右log尺度RMSE降到0.25上下。這一步的意義是讓心里有數(shù)單個特征已經(jīng)能解釋一大半方差那后續(xù)所有復雜模型的上限應該還能顯著降低誤差。同時也可以順帶檢查這個單變量關(guān)系的殘差分布為后面的多變量模型做準備。3.2 線性回歸的失效現(xiàn)場殘差圖的喇叭口說明什么多變量線性回歸跑起來之后R2能做到0.8附近看起來挺像回事但畫殘差圖的時候問題暴露了。橫坐標是擬合值縱坐標是殘差圖上出現(xiàn)了一個明顯的“喇叭口”房價預測值越高殘差波動越大。這就是統(tǒng)計學里常說的異方差性。為什么會出現(xiàn)喇叭口因為房價的本質(zhì)是乘性效應而不是加性效應。一套100萬的房子漲10%是10萬一套20萬的房子漲10%只有2萬。如果用線性回歸直接擬合原始價格模型會被高價房的大額殘差牽著走同時低價房的相對誤差反而顯得小。也就是說線性模型在“高價位段”和“低價位段”之間被迫做了一次不公平的取舍。還有一個表現(xiàn)是有些特征的系數(shù)符號跟業(yè)務直覺矛盾。比如某個區(qū)域?qū)傩栽趩巫兞糠治隼锔鷥r格正相關(guān)放進多變量模型后系數(shù)變負。這往往是特征之間的共線性在作祟也可能是目標變量分布沒處理好。我當時的應對是先停掉追特征回頭處理目標變量。3.3 對成交價做log變換從“加法世界”切換到“乘法世界”把成交價取log之后殘差喇叭口明顯收窄模型的表現(xiàn)也變得穩(wěn)定很多。雖然我平時不太喜歡依賴經(jīng)驗技巧但“回歸問題里對強正偏的目標做log”這個操作確實是性價比最高的處理方式之一。背后的道理并不復雜。線性回歸假設(shè)的是“特征每變化一個單位目標值變化一個固定幅度”這是加法世界。但房價更接近“特征每變化一個單位價格變化一個百分比”這是乘法世界。取log之后乘法關(guān)系變成加法關(guān)系正好和線性回歸的假設(shè)對齊。用通俗的話說不是面積每多1平米房子貴2萬塊而是面積每多1%房價貴2%。log變換還有一個附帶的好處模型直接預測的是log價格在評估的時候要把它指數(shù)化還原成真實價格來計算誤差。我用的評估指標是RMSE和MAE分別在log尺度上算也在還原后的價格上算。后者更符合業(yè)務認知——買家要知道的是誤差幾萬塊而不是誤差0.1個log單位。4. 樹模型的實戰(zhàn)選擇隨機森林、XGBoost與LightGBM怎么取舍4.1 為什么結(jié)構(gòu)化數(shù)據(jù)里樹模型比深度學習更實在我見過很多新手一上來就想上神經(jīng)網(wǎng)絡理由也簡單“深度學習聽起來高級”。但在這個項目里1460條樣本、幾十個表格特征神經(jīng)網(wǎng)絡很難占到便宜。深度學習擅長的是從原始高維數(shù)據(jù)里自動提取特征比如圖像、文本、語音而表格數(shù)據(jù)里的特征已經(jīng)被人為整理過信息密度高但量小這時候樹模型對“特征與目標之間的非線性關(guān)系”的擬合能力反而更強而且訓練速度快、可解釋性好。這不是說神經(jīng)網(wǎng)絡不行而是說選型要看數(shù)據(jù)和問題性質(zhì)。表格數(shù)據(jù)回歸任務里隨機森林、梯度提升樹這類模型通常能打到80分甚至90分的表現(xiàn)而深度學習要花更多時間調(diào)網(wǎng)絡結(jié)構(gòu)、學習率、正則化最后可能還不如一棵調(diào)好參的LightGBM。我個人的原則是先用樹模型作為主力如果業(yè)務上明確需要端到端自動特征提取再考慮深度學習。4.2 三模型訓練配置與調(diào)參心得早停、K折和目標編碼我在這個項目里依次試了隨機森林、XGBoost和LightGBM。隨機森林作為起點很好因為它幾乎不挑參數(shù)默認配置就能拿到還不錯的分數(shù)。缺點是它對噪聲較敏感、對特征工程的依賴比較強而且預測結(jié)果不夠平滑。XGBoost和LightGBM同屬梯度提升但實現(xiàn)細節(jié)不同LightGBM用基于直方圖的算法訓練速度更快內(nèi)存占用更小對類別特征也有原生支持。參數(shù)調(diào)優(yōu)方面我踩過最大的坑是“n_estimators調(diào)太大卻不配合早?!薄L荻忍嵘P兔慷嘁豢脴渚褪窃诋斍皻埐钌侠^續(xù)擬合樹的數(shù)量太多就會過擬合到訓練集。正確的做法是設(shè)置一個較大的樹數(shù)量上限配合early_stopping在驗證集指標不再改善時提前終止。我當時給LightGBM設(shè)置了1000棵的上限實際跑了三四百棵就觸發(fā)了早停省下大量時間。這里給一段我當時配置LightGBM的核心代碼思路不涉及完整數(shù)據(jù)但邏輯可以直接照搬import lightgbm as lgb params { objective: regression, metric: rmse, learning_rate: 0.03, max_depth: 4, num_leaves: 15, subsample: 0.8, colsample_bytree: 0.8, random_state: 42 } # 每一折內(nèi)單獨做目標編碼避免數(shù)據(jù)泄漏 for train_idx, valid_idx in kfold.split(X, y): X_train_fold target_encode(X.iloc[train_idx], y.iloc[train_idx]) X_valid_fold target_encode(X.iloc[valid_idx], y.iloc[train_idx]) model lgb.train( params, lgb.Dataset(X_train_fold, labely.iloc[train_idx]), num_boost_round1000, valid_sets[lgb.Dataset(X_valid_fold, labely.iloc[valid_idx])], callbacks[lgb.early_stopping(50)] )學習率設(shè)低到0.03配合更多輪數(shù)效果通常比學習率0.1少輪數(shù)更好。max_depth和num_leaves要一起控制防止單棵樹過深導致過擬合。subsample和colsample_bytree是兩種正則化手段前者是行采樣、后者是列采樣能在訓練速度幾乎不變的情況下提升泛化能力。4.3 本地CV與線上分數(shù)的差異驗證策略優(yōu)先于模型技巧三個模型的本地表現(xiàn)大致是隨機森林log RMSE在0.115左右XGBoost能做到0.105上下LightGBM最好到0.098。加上目標編碼和特征工程之后LightGBM能繼續(xù)壓到0.088附近??吹竭@個趨勢我當時已經(jīng)比較滿意了。但這里有個經(jīng)驗值得單獨說本地分數(shù)和外部驗證分數(shù)如果差異大第一反應不要懷疑模型參數(shù)而是懷疑驗證策略。我用的是5折交叉驗證K折切分要保證每一折的房源分布大致一致。如果使用隨機切分可能某一折里全是高價房、另一折全是低價房這樣驗證出來的誤差波動會很大。我還發(fā)現(xiàn)一個常見問題如果提前對全量數(shù)據(jù)做目標編碼再切分本地CV分數(shù)會明顯虛高因為模型已經(jīng)在目標編碼階段看到了驗證集的價格均值。這也是為什么我在4.2的代碼里把目標編碼放進了每一折內(nèi)部。驗證策略永遠是先把數(shù)據(jù)泄漏的漏洞堵上再去調(diào)參數(shù)。否則后面的一切調(diào)優(yōu)都是往沙地上蓋樓。5. 誤差復盤與特征解釋模型不騙人但數(shù)據(jù)會撒謊5.1 SHAP值怎么看哪些特征真正決定一套房的估值模型訓練完成之后我習慣用一個模型解釋工具看特征重要性。SHAPSHapley Additive exPlanations比傳統(tǒng)的feature_importance更能說明“每個特征對每個樣本的具體影響方向和大小”。傳統(tǒng)重要性只告訴你哪個特征用得多SHAP能告訴你面積增加對預測值到底推高了多少、有沒有某些樣本里面積反而壓低價格。我的SHAP結(jié)果里總體面積、總體質(zhì)量評分、總建筑面積排在前三后面跟著地段標簽、房齡、裝修狀態(tài)。這個排名和業(yè)務直覺高度一致??吹竭@樣的結(jié)果我才敢說模型學到的規(guī)律是正常的。如果哪天SHAP結(jié)果里出現(xiàn)一個莫名其妙的特征排在第一名那大概率是清洗時出了bug比如把ID列當成數(shù)值特征喂進去了或者某個列被錯誤填充。SHAP還有一個好處單樣本解釋。我可以隨機挑一套被預測得很準的房和一套被預測偏很多的房逐個看哪些特征把預測值往上推、哪些往下壓。這個能力在做業(yè)務匯報時特別有用因為聽眾不關(guān)心RMSE但很關(guān)心“為什么這棟房子被估高了10萬”。5.2 預測最差的樣本畫像豪宅、小戶型與“處處都特殊”的房子誤差復盤是機器學習項目里最值得花時間的一步可惜很多人跳過了。我把驗證集里預測誤差最大的20個樣本全部列出來一個個看它們的特征結(jié)果發(fā)現(xiàn)了一個清晰的模式誤差最大的幾乎都是豪宅和極小戶型。豪宅的絕對誤差大原因很好理解價格基數(shù)高同比例誤差換算成金額就大。但有趣的是即使改成相對誤差豪宅的預測也普遍偏差。原因可能是數(shù)據(jù)里豪宅樣本太少模型很難學到這類房屋的組合規(guī)律又或許是豪宅的“獨特性”太強每套豪宅都有自己的特殊配置通用模型對它們束手無策。極小戶型的誤差則是另一個方向面積雖然小但單價可能極高比如核心地段的小戶型原始字段里沒有單價這個概念模型自然很難預測。我后來做了一次分組誤差分析把樣本按面積分成5檔分別計算每一檔的RMSE。結(jié)果證實了高階樣本的誤差顯著高于中段。這類分析的價值是幫助設(shè)定預期一個模型不可能在所有價位段都表現(xiàn)完美知道它在哪里弱業(yè)務上就可以針對性地做人工復核。5.3 還可以繼續(xù)做的方向空間特征與外部數(shù)據(jù)這個項目做到這里模型已經(jīng)收斂在一個比較穩(wěn)定的水平。如果再往下推進我會優(yōu)先考慮外部數(shù)據(jù)。比如給每個房源加上經(jīng)緯度然后從空間上計算周邊配套設(shè)施密度、通勤時間、學區(qū)質(zhì)量這類特征在真實房價預測里往往比面積更能解釋價格差異。地理空間特征是一種很自然的擴展距離最近的地鐵站、距離市中心的直線距離、周邊學校的評分均值這些都可以用公開數(shù)據(jù)拼出來。樹的特性決定了它很適合吃這類“地理距離”特征因為距離天然是非線性的影響——離地鐵站300米和500米的差距也許不大但3公里和5公里的差距可能就很顯著。但引入外部數(shù)據(jù)的代價是時間和不確定性。公開數(shù)據(jù)的口徑、覆蓋率、時效性都需要仔細排查一不留神就會引入新的偏差。在競賽或練手場景下我通常先把純數(shù)據(jù)內(nèi)部的特征工程和驗證做到位再考慮外部數(shù)據(jù)。因為這個項目的主線是學習完整流程而不是無限堆高分數(shù)。做完整套流程之后我個人最大的體會是模型本身的選擇隨機森林還是LightGBM對最終分數(shù)的影響其實小于數(shù)據(jù)處理和驗證策略的影響。很多初學者把精力全放在調(diào)參上忽略了數(shù)據(jù)清洗和特征工程才是決定模型上限的部分。如果你也想拿房價預測練手建議先把目標變量的分布、缺失值的語義、目標編碼的數(shù)據(jù)泄漏這三個環(huán)節(jié)吃透再討論超參數(shù)。這樣冒的險更少學到的也更貼近真實項目。