器學(xué)習(xí)的二手車價(jià)格預(yù)測系統(tǒng):從數(shù)據(jù)清洗到LightGBM模型部署實(shí)踐)
簡介面向天池二手車價(jià)格預(yù)測競賽的完整項(xiàng)目代碼包適合機(jī)器學(xué)習(xí)初學(xué)者以及準(zhǔn)備參加數(shù)據(jù)挖掘競賽的開發(fā)者。資源以Python源碼為核心圍繞超40萬條交易記錄、31個(gè)變量展開完整覆蓋了從數(shù)據(jù)探索、缺失值處理、異常值清洗、特征工程到CatBoostRegressor與LGBMRegressor建模調(diào)參、模型加權(quán)融合的全流程其中數(shù)據(jù)探索部分細(xì)致檢查了缺失值、分布和特征相關(guān)性建模階段采用5折交叉驗(yàn)證最終可復(fù)現(xiàn)線上422分的預(yù)測效果。壓縮包共12個(gè)文件除主腳本外還包含特征相關(guān)性與價(jià)格分布可視化圖、CatBoost訓(xùn)練日志、CSV格式預(yù)測結(jié)果以及依賴環(huán)境說明整體僅143KB結(jié)構(gòu)緊湊、便于閱讀。目前已有71人學(xué)習(xí)下載。通過該資源可直觀學(xué)習(xí)匿名特征交叉、平均數(shù)編碼、5折交叉驗(yàn)證和融合調(diào)參等關(guān)鍵技術(shù)訓(xùn)練日志還能幫助復(fù)盤每一步的模型表現(xiàn)是一份可直接擴(kuò)展與二次開發(fā)的實(shí)戰(zhàn)參考。 這兩年陸陸續(xù)續(xù)幫身邊朋友估過不少二手車我發(fā)現(xiàn)一個(gè)很有意思的現(xiàn)象同樣一臺(tái)車掛在個(gè)人手里和放在車商展廳里報(bào)價(jià)能差出兩三萬。而真正讓人頭疼的是網(wǎng)上各種估值平臺(tái)給出的價(jià)格要么高得離譜要么低到讓你懷疑人生。二手車定價(jià)這件事信息差和主觀判斷的成分太大了。所以當(dāng)我自己動(dòng)手做一個(gè)二手車價(jià)格預(yù)測項(xiàng)目的時(shí)候核心目標(biāo)就一個(gè)盡量用數(shù)據(jù)把“感覺”變成“計(jì)算”讓價(jià)格預(yù)測結(jié)果可解釋、可復(fù)用。這個(gè)項(xiàng)目的完整形態(tài)是一套基于SSM框架的Web系統(tǒng)后臺(tái)接入了機(jī)器學(xué)習(xí)模型前端提供查詢和預(yù)測入口。說白了就是用戶輸入品牌、車齡、公里數(shù)、排量、車況描述這些信息系統(tǒng)返回一個(gè)合理的價(jià)格區(qū)間同時(shí)展示這個(gè)結(jié)果是怎么算出來的。這篇文章我會(huì)從數(shù)據(jù)處理、特征工程、模型選型和代碼工程化這幾個(gè)角度把我實(shí)際踩過的坑和最終跑通的方案完整拆開來講。適合正在做畢業(yè)設(shè)計(jì)、剛接觸數(shù)據(jù)挖掘或者想把自己手頭的數(shù)據(jù)集變成一個(gè)小型預(yù)測服務(wù)的同學(xué)參考。1. 為什么二手車價(jià)格預(yù)測比想象中難得多先潑一盆冷水二手車價(jià)格預(yù)測不是單純跑一個(gè)回歸模型就完事的活兒。它和房價(jià)預(yù)測、股票預(yù)測這類問題有明顯的區(qū)別數(shù)據(jù)的“臟”和“亂”是貫穿整個(gè)項(xiàng)目的主線。第一車輛本身是非標(biāo)準(zhǔn)化的商品。同樣是“2018款寶馬3系”不同車況、不同配置、不同過戶次數(shù)實(shí)際成交價(jià)可能差出好幾萬。而二手交易平臺(tái)上能拿到的結(jié)構(gòu)化字段非常有限大量關(guān)鍵信息都藏在自由文本的描述里比如“右前門有噴漆”“底盤輕微異響”“實(shí)表8萬公里”這類描述模型沒法直接讀取。第二價(jià)格和時(shí)間的非線性關(guān)系很明顯。新車一落地就折價(jià)前三年貶值最快之后趨于平緩。但不同品牌、不同車型的貶值曲線差異巨大某些熱門車型甚至?xí)霈F(xiàn)開了兩年還比新車指導(dǎo)價(jià)貴的倒掛現(xiàn)象。這意味著簡單地用“車齡線性下降”去擬合必然出問題。第三區(qū)域差異不可忽視。同一個(gè)城市的不同區(qū)域、不同城市的消費(fèi)能力和排放標(biāo)準(zhǔn)限制都會(huì)影響二手車的流通價(jià)格。比如國五標(biāo)準(zhǔn)的車在一些城市還能正常過戶在另一些城市幾乎沒人接盤。我一開始用的是別人整理好的標(biāo)準(zhǔn)數(shù)據(jù)集字段干凈、缺失值少模型跑出來R2能有0.9以上當(dāng)時(shí)還挺得意。后來一接真實(shí)爬蟲數(shù)據(jù)就傻眼了車型名稱五花八門“寶馬320Li”“寶馬三系”“BMW 320”混在一起公里數(shù)有的寫“8萬公里”有的寫“0.8萬公里”實(shí)際是8000公里還有的直接不填。那一刻我才意識(shí)到這個(gè)項(xiàng)目百分之七十的功夫都在數(shù)據(jù)整理上模型反而是最省心的一環(huán)。2. 數(shù)據(jù)獲取與打標(biāo)整個(gè)項(xiàng)目的成敗都在這一環(huán)2.1 采集哪些字段直接決定了模型的天花板先明確一點(diǎn)模型能學(xué)到什么程度完全取決于你喂給它什么。我最終采用的字段集分為三組基礎(chǔ)屬性組品牌、車系、車型年款、變速箱類型AT/MT/CVT/DCT、排放標(biāo)準(zhǔn)、燃油類型、座位數(shù)、新車指導(dǎo)價(jià)。狀態(tài)描述組上牌時(shí)間、表顯里程、過戶次數(shù)、維保記錄有無/是否完整、事故記錄無/小剮蹭/重大事故、車身顏色。文本特征組車商或車主填寫的車況描述包括是否原版原漆、有無改裝、輪胎磨損程度、內(nèi)飾成色等。這里最容易被忽略的是新車指導(dǎo)價(jià)。二手價(jià)格本質(zhì)上是在新車價(jià)格基礎(chǔ)上做折舊有了這個(gè)錨點(diǎn)模型才能區(qū)分“一臺(tái)15萬的新車開了3年”和“一臺(tái)50萬的新車開了3年”的絕對價(jià)格差異。沒有這個(gè)字段模型只能靠品牌和車系的組合間接推斷精度會(huì)明顯下降。2.2 清洗過程里最花時(shí)間的幾個(gè)細(xì)節(jié)爬下來的數(shù)據(jù)大概一萬多條清洗時(shí)發(fā)現(xiàn)的問題可以說是千奇百怪公里數(shù)單位不統(tǒng)一。有人寫“萬公里”有人寫“km”還有人直接寫“160000”。我的處理方式是把所有值統(tǒng)一換算成“公里”為單位的整數(shù)同時(shí)過濾掉那些超過合理范圍的異常值比如家用車一年跑10萬公里以上但車齡又很短的數(shù)據(jù)基本可以判定為營運(yùn)車輛或填寫錯(cuò)誤。上牌時(shí)間格式混亂。有的精確到日有的只寫到年份。預(yù)測價(jià)格對具體日期不敏感精確到月就足夠了但要注意計(jì)算車齡時(shí)的基準(zhǔn)日我用的是數(shù)據(jù)采集日期而不是當(dāng)前日期避免后續(xù)使用時(shí)的偏差。車況描述的文本挖掘。這一塊我單獨(dú)做了個(gè)評分函數(shù)判斷描述中是否包含“原版原漆”“全程4S店保養(yǎng)”“女士一手車”這類正向詞以及“事故”“泡水”“火燒”“調(diào)表”這類負(fù)向詞然后加權(quán)得到一個(gè)0到1之間的車況分。清洗完之后有效樣本大約剩下七千多條。雖然數(shù)量不算多但勝在字段相對完整尤其是包含真實(shí)維保和事故信息的樣本占到了六成以上這對模型的穩(wěn)定性幫助很大。3. 特征工程決定預(yù)測精度的關(guān)鍵不是模型而是特征3.1 讓“品牌保值率”變成一個(gè)可計(jì)算的數(shù)品牌對二手車價(jià)格的影響極大但直接把品牌名做獨(dú)熱編碼會(huì)讓特征維度爆炸而且學(xué)習(xí)不到“豐田比納智捷保值”這種跨品牌的規(guī)律。我的做法是計(jì)算每個(gè)品牌的平均保值率指數(shù)保值率指數(shù) 該品牌車型的二手車成交均價(jià) / 該品牌車型的新車指導(dǎo)價(jià)均值然后用這個(gè)指數(shù)替換品牌字段。這樣模型看到的是一串有序的數(shù)字豐田可能接近0.75某些冷門品牌可能只有0.45排序關(guān)系一目了然。測試下來僅這一個(gè)特征就比直接用品牌獨(dú)熱編碼在測試集上的R2提升了將近0.03。3.2 車齡衰減曲線比單純的“年齡”更好用二手車圈有個(gè)說法“三年內(nèi)的車虧最多五年以上的車價(jià)格開始穩(wěn)”。為了把這個(gè)經(jīng)驗(yàn)轉(zhuǎn)化成模型能理解的特征我沒有直接用“車齡當(dāng)前年份-上牌年份”這個(gè)單值而是構(gòu)造了一組分段衰減特征車齡小于1年次新車折舊比例高但幅度有限。1到3年快速折舊段每年折舊8%到12%。3到6年平穩(wěn)折舊段每年折舊5%到8%。6年以上進(jìn)入低價(jià)區(qū)間絕對金額變化變小但相對比例開始波動(dòng)。這組分段特征輸入模型后預(yù)測曲線能夠明顯看出“陡降-平滑-再緩降”的實(shí)際價(jià)格走勢而不是簡單的一條直線。這在處理車齡跨度較大的樣本時(shí)尤為重要。3.3 里程的分段處理與車齡聯(lián)動(dòng)里程和車齡是高度相關(guān)的但又不是簡單的線性關(guān)系。一年跑1萬公里和一年跑3萬公里的車車況差異巨大市場價(jià)格也會(huì)不同。我把里程和車齡的比值年均行駛里程作為一個(gè)新特征并進(jìn)一步分箱處理年均0.8萬公里以下標(biāo)記為“低使用強(qiáng)度”0.8到2萬為“正常”2萬以上為“高使用強(qiáng)度”。實(shí)測下來這個(gè)特征對高價(jià)車型30萬以上的預(yù)測精度提升明顯因?yàn)檫@類車對車況更加敏感。而對10萬以下的代步車影響相對較小模型也確實(shí)學(xué)到了這種區(qū)別對待。3.4 車況文本挖掘的自動(dòng)評分車況描述的文本處理我采用了比較輕量的方案。沒有上復(fù)雜的BERT之類的預(yù)訓(xùn)練模型而是構(gòu)建了一個(gè)正負(fù)向關(guān)鍵詞詞典配合簡單的權(quán)重規(guī)則打分正向關(guān)鍵詞“原版”“原漆”“全程店保”“準(zhǔn)新車”“剛做完保養(yǎng)”“實(shí)表”——出現(xiàn)則加分。負(fù)向關(guān)鍵詞“事故”“更換過”“修復(fù)”“泡水”“調(diào)表”“異響”“故障”——出現(xiàn)則減分。額外處理“女士一手車”和“個(gè)人一手”這類描述對部分車型尤其是入門豪華車有肉眼可見的溢價(jià)效果作為加權(quán)正向詞處理。這里要提醒一點(diǎn)文本匹配的時(shí)候一定要做同義詞擴(kuò)展比如“原版原漆”和“全車原漆”意思相同但寫法不同。不處理的話同一個(gè)車況可能因?yàn)楸硎霾町惐辉u出完全不同的分?jǐn)?shù)。4. 模型選型與調(diào)參實(shí)錄為什么最后選了LightGBM4.1 從線性回歸到樹模型的進(jìn)階項(xiàng)目初期我按照慣性先跑了線性回歸和嶺回歸作為基準(zhǔn)模型。結(jié)果很穩(wěn)定但很平庸測試集R2只有0.78左右殘差分析顯示價(jià)格在20萬以上的樣本誤差偏大而且高估了低里程車的價(jià)格。原因是二手車價(jià)格和特征之間確實(shí)存在大量非線性關(guān)系線性模型表達(dá)力不夠。隨后換成隨機(jī)森林R2到了0.85提升明顯。但隨機(jī)森林的預(yù)測值在某些區(qū)間會(huì)呈現(xiàn)階梯狀分布因?yàn)樗谋举|(zhì)是對多棵樹的結(jié)論做平均遇到特征分布稀疏的區(qū)域輸出粒度會(huì)比較粗糙。后來又試了XGBoost和LightGBM兩者在精度上接近但LightGBM訓(xùn)練速度快得多而且對缺失值的處理更加友好適合我這種數(shù)據(jù)集不算大但需要頻繁迭代調(diào)參的場景。4.2 LightGBM的關(guān)鍵參數(shù)與實(shí)際調(diào)參思路這里分享一組最終跑通的參數(shù)以及每個(gè)參數(shù)背后的思考n_estimators控制在800左右配合早停early stopping使用。設(shè)置得太高不僅容易過擬合訓(xùn)練時(shí)間也會(huì)白白浪費(fèi)。learning_rate0.03。學(xué)習(xí)率調(diào)低之后需要更多棵樹但精度確實(shí)更穩(wěn)。我先用0.1快跑一輪確定特征有效性再降到0.03做最終訓(xùn)練。num_leaves設(shè)置成31。LightGBM的葉子節(jié)點(diǎn)數(shù)比樹的深度更影響模型復(fù)雜度這個(gè)值不宜過大否則極易過擬合。min_data_in_leaf20。這個(gè)參數(shù)可以防止模型在小型葉子節(jié)點(diǎn)上學(xué)到噪音對樣本量不大的項(xiàng)目尤其重要。feature_fraction和bagging_fraction都設(shè)為0.8相當(dāng)于每次訓(xùn)練隨機(jī)抽一部分特征和樣本間接實(shí)現(xiàn)正則化。我還試過對價(jià)格這個(gè)目標(biāo)值取對數(shù)后再訓(xùn)練也就是所謂的log變換。因?yàn)閮r(jià)格服從長尾分布個(gè)別豪車樣本會(huì)把損失函數(shù)拉偏log變換能讓模型更關(guān)注中低價(jià)位樣本的相對誤差。但從實(shí)際業(yè)務(wù)角度出發(fā)我們更關(guān)心絕對金額的誤差所以最后還是直接回歸原始價(jià)格只是在評估指標(biāo)上用MAE平均絕對誤差和MAPE平均絕對百分比誤差一起看。4.3 模型效果與業(yè)務(wù)可解釋性最終在測試集上的表現(xiàn)是R2約0.89MAE約1.2萬元。對于一臺(tái)均價(jià)15萬左右的車來說這個(gè)誤差范圍是可以接受的畢竟人工估價(jià)上下浮動(dòng)一兩萬都很正常。更重要的是LightGBM可以輸出特征重要性我看了排序之后發(fā)現(xiàn)和行業(yè)直覺高度吻合新車指導(dǎo)價(jià)、車齡分段、品牌保值率穩(wěn)居前三車況評分和過戶次數(shù)也進(jìn)了前十。這讓我在對業(yè)務(wù)方解釋模型結(jié)論的時(shí)候有了數(shù)據(jù)支撐。5. SSM項(xiàng)目集成把模型跑成Web服務(wù)才是完整的項(xiàng)目5.1 模型落地的通用做法訓(xùn)練好的模型是.txt格式的LightGBM原生模型文件如果只是在Jupyter Notebook里做實(shí)驗(yàn)?zāi)堑缴弦徊骄退憬Y(jié)束了。但既然是“二手車價(jià)格預(yù)測方案”的完整項(xiàng)目代碼就必須把模型部署成可調(diào)用的服務(wù)。我的架構(gòu)選的是SSMSpring SpringMVC MyBatis作為后端框架。選擇SSM倒不是說它一定比Spring Boot先進(jìn)而是這個(gè)項(xiàng)目本身有數(shù)據(jù)庫交互需求保存用戶查詢記錄、維護(hù)車輛信息SSM在這類傳統(tǒng)企業(yè)級(jí)項(xiàng)目中依然是面試和畢設(shè)的高頻要求。在Spring配置文件里加載模型只有一行代碼的事把.txt文件放到resources目錄下定義Bean讓容器啟動(dòng)時(shí)完成加載單例常駐內(nèi)存避免了每次請求都重新讀文件的性能損耗。5.2 接口設(shè)計(jì)要考慮的不只是模型預(yù)測后端接口我設(shè)計(jì)了兩個(gè)核心頁面價(jià)格預(yù)測頁面用戶選擇品牌、車系、年款輸入上牌時(shí)間、公里數(shù)、過戶次數(shù)下拉選擇車況描述中的關(guān)鍵詞例如“原版原漆”“有小剮蹭”“有噴漆”等提交后調(diào)用模型接口返回預(yù)測價(jià)格和區(qū)間。歷史記錄頁面保存每一次查詢記錄用戶可以對比不同車型的估值結(jié)果。Controller層接收參數(shù)后先做數(shù)據(jù)合法性校驗(yàn)比如公里數(shù)不能為負(fù)數(shù)上牌時(shí)間不能晚于當(dāng)前時(shí)間然后組裝特征向量。特征工程部分專門抽了一個(gè)FeatureBuilder類把文本關(guān)鍵詞轉(zhuǎn)成車況評分把年款和上牌時(shí)間轉(zhuǎn)換成車齡分段特征再調(diào)用模型預(yù)測。這里要特別強(qiáng)調(diào)訓(xùn)練時(shí)的特征預(yù)處理邏輯必須和線上服務(wù)完全一致否則模型輸入的分布一旦偏移輸出的結(jié)果會(huì)莫名其妙。我踩過一次坑訓(xùn)練時(shí)里程用“公里”做單位上線時(shí)前端傳過來的是“萬公里”結(jié)果有一段時(shí)間所有通過頁面查詢的估值結(jié)果都明顯偏高排查了半天才發(fā)現(xiàn)單位問題。5.3 預(yù)測區(qū)間的輸出邏輯光給一個(gè)點(diǎn)預(yù)測值并不夠用戶對“這臺(tái)車到底值多少錢”的信任感往往來自一個(gè)合理的區(qū)間。我在實(shí)現(xiàn)時(shí)采用了簡單的殘差分位數(shù)方法跑訓(xùn)練集時(shí)把每條樣本的真實(shí)值和預(yù)測值的殘差算出來取80%置信區(qū)間對應(yīng)的殘差邊界上線時(shí)用“預(yù)測值殘差下界”和“預(yù)測值殘差上界”作為返回區(qū)間。這個(gè)方法原理簡單計(jì)算開銷幾乎為零而且效果比固定±10%的方式要合理得多因?yàn)樗荏w現(xiàn)不同價(jià)位車型的波動(dòng)差異。6. 實(shí)際跑項(xiàng)目代碼時(shí)的排錯(cuò)記錄與優(yōu)化細(xì)節(jié)6.1 一次典型的特征對齊事故有段時(shí)間我發(fā)現(xiàn)同一個(gè)車型、相近車齡的預(yù)測值突然變得不對勁反復(fù)檢查數(shù)據(jù)和代碼都沒看出問題。后來用一條訓(xùn)練集里的樣本走了一遍線上特征構(gòu)建流程對比之后才發(fā)現(xiàn)訓(xùn)練代碼里我對“品牌保值率”的特征順序是按品牌字母排的線上代碼里卻是按數(shù)據(jù)集里出現(xiàn)順序排的。雖然最終模型的特征名是對應(yīng)正確的但有一處特征拼接邏輯里出現(xiàn)了索引錯(cuò)位導(dǎo)致模型讀到的“保值率”實(shí)際上對應(yīng)的是另一個(gè)特征值。這個(gè)問題暴露出的經(jīng)驗(yàn)是不要在管道里隱式依賴字段順序每次組裝特征向量時(shí)都要顯式指定特征名線上和線下共用同一個(gè)特征構(gòu)建函數(shù)不要各寫一套。后來我把特征構(gòu)建代碼抽成了一個(gè)獨(dú)立的common模塊訓(xùn)練腳本和SSM服務(wù)都引用同一份代碼這類問題再?zèng)]出現(xiàn)過。6.2 數(shù)據(jù)庫存儲(chǔ)與查詢性能權(quán)衡歷史記錄表如果每條查詢都存原文和全部參數(shù)表會(huì)膨脹得很快。我的做法是只保存用戶ID、查詢參數(shù)JSON、預(yù)測結(jié)果和創(chuàng)建時(shí)間。列表頁展示時(shí)直接用JSON字段解析回顯不需要額外關(guān)聯(lián)車輛字典表查詢速度能控制在幾十毫秒級(jí)別。當(dāng)然如果查詢量真的大到一定程度還是建議拆表或者移除明細(xì)展示但對于課時(shí)設(shè)計(jì)或者小型內(nèi)部系統(tǒng)JSON方案屬于性價(jià)比最高的選擇。6.3 模型更新策略和緩存注意事項(xiàng)二手車市場是有時(shí)效性的半年前訓(xùn)練的模型放到今天可能已經(jīng)不具備參考價(jià)值。我的做法是設(shè)計(jì)了一個(gè)簡單的模型版本號(hào)字段存在數(shù)據(jù)庫配置表里。前端每次發(fā)起預(yù)測請求時(shí)后端會(huì)帶上當(dāng)前版本號(hào)如果預(yù)測結(jié)果頁面上發(fā)現(xiàn)版本已經(jīng)落后會(huì)提示“估價(jià)結(jié)果僅供參考建議結(jié)合市場最新行情”。同時(shí)預(yù)測結(jié)果可以做短時(shí)緩存同一個(gè)參數(shù)組合在10分鐘內(nèi)重復(fù)查詢直接返回緩存值降低模型并發(fā)調(diào)用的壓力。7. 折騰三輪之后的關(guān)鍵心得數(shù)據(jù)質(zhì)量決定一切。我前后換過三批數(shù)據(jù)最初一批來自公開數(shù)據(jù)集干凈但特征單一第二批來自爬蟲采集特征豐富但噪音極大第三批是重新清洗和打標(biāo)后的結(jié)果。每一次模型的精度提升本質(zhì)上都來源于數(shù)據(jù)質(zhì)量的改善而不是模型換得多花哨。如果你準(zhǔn)備復(fù)現(xiàn)這個(gè)項(xiàng)目我建議從一開始就重視字段設(shè)計(jì)和清洗流程不要急于跑模型。第二個(gè)心得是特征工程要尊重行業(yè)常識(shí)。二手車定價(jià)在這行已經(jīng)有了一整套不成文的經(jīng)驗(yàn)體系新車指導(dǎo)價(jià)是錨點(diǎn)、車齡是折舊主軸、品牌保值率是修正系數(shù)、車況和里程是調(diào)節(jié)項(xiàng)。把這套邏輯映射成數(shù)值特征模型自然能學(xué)出效果相反如果只靠原始字段硬跑哪怕模型再先進(jìn)也未必能學(xué)到這些顯性規(guī)律。最后一個(gè)小建議做這類項(xiàng)目時(shí)把你的特征構(gòu)建代碼當(dāng)成核心資產(chǎn)來維護(hù)因?yàn)樗沁B接數(shù)據(jù)處理、模型訓(xùn)練和線上服務(wù)的關(guān)鍵通道。我每次重構(gòu)都在提醒自己寧可花時(shí)間把特征模塊寫得清晰一些也不要為了省事在Jupyter里東一塊西一塊地寫腳本。項(xiàng)目走到最后真正能沉淀下來的不是某個(gè)模型文件或者某次調(diào)參記錄而是一套數(shù)據(jù)從原始到特征、再到服務(wù)和預(yù)測的標(biāo)準(zhǔn)流程。值錢的從來不是那個(gè)“能預(yù)測價(jià)格”的結(jié)論而是這個(gè)結(jié)論為什么可信、怎么持續(xù)保持可信的整套機(jī)制。本文還有配套的精品資源點(diǎn)擊獲取