據(jù)分析預(yù)測(cè)系統(tǒng)實(shí)戰(zhàn):從數(shù)據(jù)清洗到XGBoost模型部署全流程)
簡(jiǎn)介面向計(jì)算機(jī)專(zhuān)業(yè)正在做課程設(shè)計(jì)或期末大作業(yè)的學(xué)生以及需要項(xiàng)目實(shí)戰(zhàn)練習(xí)的學(xué)習(xí)者這份Python二手房數(shù)據(jù)分析預(yù)測(cè)系統(tǒng)資料提供完整可運(yùn)行的實(shí)戰(zhàn)項(xiàng)目。內(nèi)容涵蓋數(shù)據(jù)清洗、房?jī)r(jià)分析與預(yù)測(cè)建模的全流程源碼同時(shí)附帶文檔報(bào)告便于理解數(shù)據(jù)挖掘項(xiàng)目的整體結(jié)構(gòu)并快速參考復(fù)用。壓縮包共156個(gè)文件以18個(gè)Python腳本、18個(gè)CSV數(shù)據(jù)文件、15個(gè)HTML頁(yè)面、11個(gè)JavaScript文件為主體另有大量可視化圖表圖片與配置文件等包體大小約40.03MB各類(lèi)文件按用途歸類(lèi)清晰。目前已有831人學(xué)習(xí)使用項(xiàng)目經(jīng)過(guò)嚴(yán)格調(diào)試下載解壓后即可直接運(yùn)行。通過(guò)源碼與報(bào)告的對(duì)應(yīng)學(xué)習(xí)讀者能夠系統(tǒng)掌握數(shù)據(jù)預(yù)處理、特征工程、模型構(gòu)建與結(jié)果展示的核心思路尤其適合期末答辯、課程設(shè)計(jì)以及希望獲得高分范例的開(kāi)發(fā)者作為實(shí)踐模板。1. 二手房數(shù)據(jù)分析預(yù)測(cè)系統(tǒng)數(shù)據(jù)從業(yè)者繞不開(kāi)的完整實(shí)戰(zhàn)項(xiàng)目把一套二手房數(shù)據(jù)分析預(yù)測(cè)系統(tǒng)從零搭起來(lái)最反直覺(jué)的結(jié)論是模型挑得再好不如數(shù)據(jù)洗得干凈。我見(jiàn)過(guò)太多人花兩周調(diào) XGBoost 參數(shù)最后發(fā)現(xiàn)訓(xùn)練集 R2 高達(dá) 0.96、測(cè)試集只有 0.55原因只是特征里混進(jìn)了帶價(jià)格的字段。這篇文章不說(shuō)空泛理論只講一條能直接上手的完整流程房源數(shù)據(jù)采集清洗、特征工程、模型訓(xùn)練評(píng)估再到項(xiàng)目落地才會(huì)遇到的坑和部署路徑。適合想獨(dú)立完成數(shù)據(jù)項(xiàng)目、能對(duì)每個(gè)環(huán)節(jié)講清楚取舍的從業(yè)者。文中代碼都能直接改著跑。2. 數(shù)據(jù)從哪來(lái)二手房源采集與清洗的完整鏈路無(wú)論模型多先進(jìn)、特征多精巧采集和清洗決定了數(shù)據(jù)基本面。這一部分看起來(lái)沒(méi)有模型那么高級(jí)但恰恰是項(xiàng)目成敗的分水嶺。本節(jié)先解決數(shù)據(jù)從哪來(lái)的問(wèn)題再做臟數(shù)據(jù)清洗跑完這兩步你手里就有一份能進(jìn)模型的干凈表格。2.1 抓取房源信息requests BeautifulSoup 的最小采集方案采集是數(shù)據(jù)系統(tǒng)的源頭也是新手最容易卡住的環(huán)節(jié)。常見(jiàn)做法是用 requests 請(qǐng)求目標(biāo)站點(diǎn)的列表頁(yè)再用 BeautifulSoup 解析 HTML把每套房子的標(biāo)題、小區(qū)、戶(hù)型、面積、樓層、朝向、總價(jià)、單價(jià)、掛牌時(shí)間抽出來(lái)。別一上來(lái)就上 Scrapy——先跑通單頁(yè)采集確認(rèn)字段能取全再談并發(fā)和框架。import requests import time import random from bs4 import BeautifulSoup # 請(qǐng)求頭必須帶很多站點(diǎn)會(huì)根據(jù) UA 特征攔截腳本 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } def parse_list_page(html_text): 從列表頁(yè) HTML 中提取房源字段返回字典列表 soup BeautifulSoup(html_text, html.parser) items [] for card in soup.select(.listing-card): # 選擇器以實(shí)際頁(yè)面 DOM 為準(zhǔn) item { house_id: card.select_one(.house-id).get_text(stripTrue), title: card.select_one(.title).get_text(stripTrue), community: card.select_one(.community).get_text(stripTrue), layout: card.select_one(.layout).get_text(stripTrue), area: card.select_one(.area).get_text(stripTrue), total_price: card.select_one(.price).get_text(stripTrue), unit_price: card.select_one(.unit-price).get_text(stripTrue), } items.append(item) return items def crawl_pages(start_page, end_page): 翻頁(yè)采集每頁(yè)之間加隨機(jī)延時(shí) all_items [] for page in range(start_page, end_page 1): url fhttps://your-target-site.com/ershoufang/pg{page}/ resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f第{page}頁(yè)返回異常狀態(tài)碼: {resp.status_code}) continue page_items parse_list_page(resp.text) all_items.extend(page_items) time.sleep(random.uniform(2, 5)) # 隨機(jī)延時(shí)是續(xù)命關(guān)鍵 return all_items這段代碼的邏輯很直白parse_list_page 負(fù)責(zé)解析單頁(yè)crawl_pages 負(fù)責(zé)翻頁(yè)并匯總。核心有三個(gè)值得細(xì)說(shuō)。第一個(gè)是 headersUser-Agent 偽裝是讓請(qǐng)求看起來(lái)像真實(shí)瀏覽器的基礎(chǔ)操作建議直接用瀏覽器開(kāi)發(fā)者工具復(fù)制自己電腦上的 UA 字符串別用網(wǎng)上抄的通用值。第二個(gè)是 select 選擇器它依賴(lài)目標(biāo)頁(yè)面的 DOM 結(jié)構(gòu)不同站點(diǎn)的類(lèi)名差異很大寫(xiě)之前先在開(kāi)發(fā)者工具里確認(rèn)卡片節(jié)點(diǎn)的實(shí)際類(lèi)名再替換。第三個(gè)是 time.sleep(random.uniform(2, 5))固定延時(shí)容易被頻率檢測(cè)識(shí)別隨機(jī)延時(shí)能顯著降低被封風(fēng)險(xiǎn)。爬蟲(chóng)的目標(biāo)是用最少的代碼拿到可用的數(shù)據(jù)不是把頁(yè)面全部字段都抓下來(lái)。我一般先抓標(biāo)題、小區(qū)、戶(hù)型、面積、朝向、總價(jià)和單價(jià)這幾個(gè)核心字段跑通之后再按需擴(kuò)展。數(shù)據(jù)量控制在 5000 到 20000 條就夠起步太多的話(huà)清洗和訓(xùn)練都會(huì)變得遲鈍。頁(yè)面里有房屋 ID 的話(huà)務(wù)必抓下來(lái)這是之后去重的唯一可靠依據(jù)比用標(biāo)題去重穩(wěn)得多。2.2 數(shù)據(jù)清洗把爬下來(lái)的文本變成能建模的結(jié)構(gòu)化表格采集回來(lái)的數(shù)據(jù)一定是臟的。常見(jiàn)問(wèn)題包括價(jià)格字段帶「萬(wàn)」「元/㎡」等混用單位、面積是「89.5㎡」夾著中文符號(hào)、戶(hù)型寫(xiě)成「3室2廳1衛(wèi)」或「三室兩廳」不統(tǒng)一、缺失值以空字符串或「暫無(wú)」出現(xiàn)、同一房源被多個(gè)頁(yè)面重復(fù)抓取。這些不洗掉后面做特征工程全是錯(cuò)。import pandas as pd import re def clean_price(price_str): 把 488萬(wàn) 轉(zhuǎn)成 488.04880000元 轉(zhuǎn)成 488.0 if not isinstance(price_str, str): return None if 萬(wàn) in price_str: return float(re.search(r([\d.]), price_str).group(1)) match re.search(r([\d.])元, price_str) if match: return float(match.group(1)) / 10000 # 元 - 萬(wàn)元 return None def clean_area(area_str): 把 89.5㎡ 或 89.5平米 統(tǒng)一轉(zhuǎn)成 89.5 match re.search(r([\d.]), str(area_str).replace( , )) return float(match.group(1)) if match else None def clean_orientation(raw): 朝向文本歸一化南 北、南北 統(tǒng)一歸入枚舉 raw str(raw).replace( , ) if 南北 in raw: return south-north if 東南 in raw: return southeast if 西南 in raw: return southwest if 南 in raw: return south return raw df pd.DataFrame(raw_items) df[house_id] df[house_id].astype(str) df df.drop_duplicates(subset[house_id]) # 按房源 ID 去重 df[total_price] df[total_price].apply(clean_price) df[unit_price] df[unit_price].apply(clean_price) df[area] df[area_text].apply(clean_area) df[orientation] df[orientation_raw].apply(clean_orientation) # 總價(jià)或面積解析失敗的記錄無(wú)法參與建模直接刪除 df df.dropna(subset[total_price, area]) # 粗篩異常值統(tǒng)一成萬(wàn)元后總價(jià)低于 10 萬(wàn)基本是錄入錯(cuò)誤 df df[(df[total_price] 10) (df[area] 5)]清洗代碼的取舍邏輯比代碼本身更重要。去重必須用 house_id 而不是房源標(biāo)題因?yàn)橥瑯?biāo)題可能對(duì)應(yīng)不同掛牌批次。clean_price 的核心是把「萬(wàn)」和「元」統(tǒng)一成「萬(wàn)元」這是二手房數(shù)據(jù)里最隱蔽的坑——不同板塊、不同頁(yè)面顯示的價(jià)格單位可能不一致清洗時(shí)必須顯式統(tǒng)一否則后面算特征相關(guān)性全是錯(cuò)的。缺失值處理不建議一刀切刪除??們r(jià)、面積這種核心字段缺失就刪行朝向、裝修這種可選字段缺失用 unknown 填充。異常值篩選先靠 total_price 10 且 area 5 這種粗暴規(guī)則擋掉明顯錯(cuò)誤更精細(xì)的做法是畫(huà)箱線(xiàn)圖把超出上下邊緣 1.5 倍四分位距的記錄單獨(dú)拉出來(lái)人工確認(rèn)。清洗完后打印 df.info() 和 df.describe()看每列的缺失情況、數(shù)值范圍和均值是判斷清洗是否到位的快速手段。3. 特征工程與探索性分析把原始字段變成模型吃得下的數(shù)值特征數(shù)據(jù)清洗完只是第一步原始字段還不能直接進(jìn)模型。朝向是文本、小區(qū)名是高基數(shù) ID、面積和價(jià)格之間有復(fù)雜關(guān)系。這一章先做探索性分析確認(rèn)數(shù)據(jù)形態(tài)再做特征編碼把文本轉(zhuǎn)為模型能處理的數(shù)值結(jié)構(gòu)。3.1 先做探索性分析三張圖決定模型方向建模前做 EDA 的價(jià)值是用三張圖快速搞清楚房?jī)r(jià)分布形態(tài)、面積與總價(jià)的關(guān)系、以及城區(qū)樣本是否均衡。EDA 不是走形式它直接決定后面選模型和特征的方向如果總價(jià)分布嚴(yán)重右偏線(xiàn)性回歸的誤差會(huì)被高價(jià)房樣本主導(dǎo)更合理的做法是先對(duì)目標(biāo)變量做 log 變換。import math import matplotlib.pyplot as plt import seaborn as sns # 1. 總價(jià)分布右偏明顯就考慮 log1p 變換 fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(df[total_price], bins60, axaxes[0]) axes[0].set_title(total_price distribution) sns.histplot(df[total_price].apply(math.log1p), bins60, axaxes[1]) axes[1].set_title(log(total_price) distribution) plt.show() # 2. 面積 vs 總價(jià)判斷變量關(guān)系是否線(xiàn)性 plt.figure(figsize(6, 4)) sns.scatterplot(datadf, xarea, ytotal_price, alpha0.3) plt.axhline(df[total_price].median(), colorred, linestyle--) plt.show() # 3. 城區(qū)均價(jià)驗(yàn)證采集樣本是否覆蓋主要板塊 city_avg df.groupby(district)[unit_price].median().sort_values() city_avg.plot(kindbarh) plt.show()這三張圖對(duì)應(yīng)三種判斷。第一張分布圖告訴你 target 要不要做 log 變換肉眼看到長(zhǎng)尾直接選 log1p第二張散點(diǎn)圖看面積與總價(jià)的關(guān)系如果出現(xiàn)明顯非線(xiàn)性說(shuō)明后續(xù)可以加面積平方項(xiàng)或者直接交給樹(shù)模型自行切分第三張城區(qū)均價(jià)圖用來(lái)驗(yàn)證采集數(shù)據(jù)覆蓋度如果某個(gè)城區(qū)樣本極少它的統(tǒng)計(jì)數(shù)字帶不動(dòng)模型泛化要么補(bǔ)數(shù)據(jù)要么干脆把那部分樣本從訓(xùn)練集里拿掉。EDA 過(guò)程中要順手復(fù)查缺失值的性質(zhì)。我之前做過(guò)一個(gè)模擬項(xiàng)目X清洗完發(fā)現(xiàn)朝向字段 35% 缺失但這個(gè)字段不影響核心建模后來(lái)在特征工程里設(shè)了 unknown 分類(lèi)。最怕的是缺失不是隨機(jī)的——比如高價(jià)房源更不愛(ài)填裝修信息這種結(jié)構(gòu)性缺失直接刪掉等于讓模型在高價(jià)區(qū)間失去重要信號(hào)。應(yīng)對(duì)方法是對(duì)比缺失記錄與正常記錄的目標(biāo)均值差異差異明顯就要么補(bǔ)全要么顯式生成一個(gè)「是否缺失」標(biāo)志特征。3.2 特征編碼高基數(shù)類(lèi)別特征別直接 One-Hot特征工程這一步二手房系統(tǒng)里最常用的有兩類(lèi)低基數(shù)類(lèi)別特征朝向、裝修、戶(hù)型和高基數(shù)類(lèi)別特征小區(qū)名、行政區(qū)域。前者適合 One-Hot后者按小區(qū)做目標(biāo)編碼比 One-Hot 更適合因?yàn)樾^(qū)數(shù)量動(dòng)輒上千One-Hot 會(huì)瞬間把維度撐爆而且絕大多數(shù)小區(qū)樣本極少獨(dú)立成列既稀疏又容易過(guò)擬合。特征類(lèi)型推薦做法原因朝向、裝修、戶(hù)型One-Hot枚舉少可解釋性強(qiáng)行政區(qū)域目標(biāo)編碼類(lèi)別幾十種One-Hot 能做但編碼更穩(wěn)小區(qū)名目標(biāo)編碼上千類(lèi)別One-Hot 維度爆炸from sklearn.preprocessing import OneHotEncoder # 低基數(shù)特征One-Hotdrop_first 避免共線(xiàn)性 df pd.get_dummies(df, columns[orientation, decoration], drop_firstTrue) def target_encode(series, target, alpha10): 按類(lèi)別統(tǒng)計(jì) target 均值并用全局先驗(yàn)做平滑 global_mean target.mean() grp target.groupby(series).agg([sum, count]) encoded (grp[sum] alpha * global_mean) / (grp[count] alpha) return series.map(encoded) df[community_encoded] target_encode(df[community], df[total_price]) df[log_area] df[area].apply(math.log1p)target_encode 里有兩個(gè)硬核參數(shù)。alpha 是平滑系數(shù)它解決的是小樣本類(lèi)別統(tǒng)計(jì)不可靠的問(wèn)題某個(gè)小區(qū)只有 1 條記錄時(shí)直接用這條記錄均價(jià)編碼會(huì)嚴(yán)重過(guò)擬合alpha 越大編碼值越趨向全局均值。實(shí)際項(xiàng)目里 alpha 取 5 到 20做完建議再疊加 K 折外編碼即每一折只用訓(xùn)練部分統(tǒng)計(jì)編碼避免驗(yàn)證集信息泄漏到特征里。get_dummies 里的 drop_firstTrue 去掉一列冗余避免和截距項(xiàng)共線(xiàn)線(xiàn)性模型里這一步很重要。還有幾個(gè)特征容易被忽略但實(shí)際很有用樓齡當(dāng)前年份減建成年份、單價(jià)總價(jià)除以面積、樓層檔位低/中/高、近地鐵步行距離如果有坐標(biāo)。這些衍生特征計(jì)算成本低對(duì)線(xiàn)性模型和樹(shù)模型都有實(shí)際增益。做完特征工程后跑一遍 df.shape 確認(rèn)行數(shù)沒(méi)變、列數(shù)增加合理再用 df.isna().sum() 確認(rèn)沒(méi)有引入新的缺失值。4. 房?jī)r(jià)預(yù)測(cè)模型從線(xiàn)性基線(xiàn)到梯度提升樹(shù)的調(diào)參路徑特征就緒之后進(jìn)入建模環(huán)節(jié)。這一章先講為什么必須從線(xiàn)性回歸跑起再給 XGBoost 的完整參數(shù)配置和特征重要性分析方法。目標(biāo)不是把參數(shù)調(diào)到極致而是讓你拿到一套可以復(fù)制、可以解釋、可以排查問(wèn)題的建模流程。4.1 先跑通線(xiàn)性回歸基線(xiàn)模型是排查問(wèn)題的錨點(diǎn)我一般會(huì)先跑線(xiàn)性回歸不為多好的精度而是為了驗(yàn)證數(shù)據(jù)流程整體貫通。一個(gè)能跑的簡(jiǎn)單模型配合常規(guī)評(píng)估指標(biāo)能快速暴露數(shù)據(jù)泄露、異常值、單位不統(tǒng)一等問(wèn)題。如果線(xiàn)性回歸的 MAE 小到離譜那大概率是目標(biāo)變量被混進(jìn)了特征別急著高興。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score feature_cols [c for c in df.columns if c not in (total_price, house_id, title)] X df[feature_cols].select_dtypes(includenumber) y df[total_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(MAE:, mean_absolute_error(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred, squaredFalse)) print(R2:, r2_score(y_test, y_pred))這里有個(gè)細(xì)節(jié)feature_cols 先把明顯不該進(jìn)模型的對(duì)象列排除掉再用 select_dtypes(includenumber) 兜底只保留數(shù)值列。標(biāo)題和房屋 ID 不能進(jìn)模型前者是文本、后者是高基數(shù) ID直接喂給線(xiàn)性模型會(huì)導(dǎo)致維度災(zāi)難或無(wú)意義擬合?;€(xiàn)模型的 MAE 是后續(xù)對(duì)比的錨點(diǎn)如果后面把特征工程做扎實(shí)樹(shù)模型理應(yīng)比基線(xiàn)明顯下降要是沒(méi)下降說(shuō)明改進(jìn)方向偏了。評(píng)估指標(biāo)的選擇也有信息量。MAE 適合向業(yè)務(wù)方解釋平均預(yù)測(cè)差多少錢(qián)RMSE 對(duì)異常值更敏感會(huì)放大高價(jià)房預(yù)測(cè)偏離的問(wèn)題。我會(huì)額外看一眼分位數(shù)誤差——只算預(yù)測(cè)價(jià)格最低 10% 和最高 10% 樣本的 MAE如果低總價(jià)樣本誤差遠(yuǎn)小于高總價(jià)樣本說(shuō)明模型在高價(jià)段位擬合不足光調(diào)參救不回來(lái)要加特征或者對(duì)目標(biāo)變量做 log 變換。4.2 集成模型XGBoost 參數(shù)表與早停策略表格型回歸任務(wù)的首選不是深度學(xué)習(xí)而是梯度提升樹(shù)。隨機(jī)森林方差小但偏置高XGBoost 通過(guò)正則化和梯度提升把偏置打下來(lái)是這類(lèi)項(xiàng)目最穩(wěn)的主模型。我直接用 XGBoost配合早??刂频啍?shù)from xgboost import XGBRegressor xgb XGBRegressor( n_estimators1000, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42, early_stopping_rounds50, eval_metricmae, ) xgb.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) y_pred_xgb xgb.predict(X_test) print(XGBoost MAE:, mean_absolute_error(y_test, y_pred_xgb))關(guān)鍵參數(shù)速查表參數(shù)建議值說(shuō)明max_depth5數(shù)據(jù)特征不多時(shí) 4~7 是甜點(diǎn)位越大越容易過(guò)擬合learning_rate0.05越小越穩(wěn)但需要更多樹(shù)配合早停使用n_estimators1000不直接決定最終輪數(shù)早停會(huì)自動(dòng)截?cái)鄐ubsample / colsample_bytree0.8每棵樹(shù)隨機(jī)采樣比例噪聲大時(shí)可降到 0.7reg_alpha / reg_lambda0.1 / 1.0正則權(quán)重特征多時(shí)可加大early_stopping_rounds50驗(yàn)證集連續(xù) 50 輪無(wú)改善就停機(jī)返回最優(yōu)輪learning_rate 設(shè) 0.05 配合 n_estimators1000 靠早停自動(dòng)收斂比一次性設(shè) 100 棵樹(shù)效果好一個(gè)檔次。subsample 和 colsample_bytree 都設(shè)在 0.8給模型加一點(diǎn)隨機(jī)性減少方差reg_alpha 對(duì)這類(lèi)噪聲不小的數(shù)據(jù)有實(shí)際幫助early_stopping_rounds50 是防止過(guò)擬合最省心的招。如果環(huán)境里沒(méi)有 XGBoost用 LightGBM 或隨機(jī)森林邏輯一樣只是參數(shù)名略有差異。樹(shù)模型訓(xùn)練完我會(huì)把特征重要性打出來(lái)看一遍import pandas as pd importance pd.DataFrame({ feature: X_train.columns, importance: xgb.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(15))看特征重要性的目的不是挑刺而是找反向信號(hào)。如果面積、小區(qū)目標(biāo)編碼、城區(qū)排在前面說(shuō)明采集和特征工程做對(duì)了如果樓層、朝向這類(lèi)該有效的特征重要性接近 0可能是編碼方式有問(wèn)題。特征重要性排序也能幫你砍掉尾部無(wú)效特征讓最終模型更穩(wěn)。5. 常見(jiàn)問(wèn)題與避坑二手房預(yù)測(cè)系統(tǒng)的 5 個(gè)經(jīng)典翻車(chē)現(xiàn)場(chǎng)這一章是整套流程里最值錢(qián)的部分。以下 5 個(gè)問(wèn)題都是我在實(shí)際項(xiàng)目里見(jiàn)過(guò)、踩過(guò)、幫人排查過(guò)的按現(xiàn)象、原因、解決三步記錄。前兩個(gè)決定模型評(píng)估可信度后三個(gè)決定項(xiàng)目能否順利上線(xiàn)。5.1 數(shù)據(jù)泄露模型「太好」往往是壞消息現(xiàn)象訓(xùn)練集 R2 高達(dá) 0.96測(cè)試集卻只有 0.5或者兩個(gè)集合指標(biāo)都好得夸張導(dǎo)致你誤以為模型已經(jīng)完美。原因特征里有字段直接或間接包含目標(biāo)價(jià)格的信息。最常見(jiàn)的元兇是小區(qū)均價(jià)列和單價(jià)字段——單價(jià)等于總價(jià)除以面積只要面積誤差不大用單價(jià)預(yù)測(cè)總價(jià)就是作弊。解決訓(xùn)練之前人工過(guò)一遍特征名把所有名字里帶 price、均價(jià)、掛牌價(jià)的列全部剔除。再做一次保險(xiǎn)驗(yàn)算用留下的特征訓(xùn)練決策樹(shù)看特征重要性里有沒(méi)有哪個(gè)特征貢獻(xiàn)異常偏高。我做過(guò)一個(gè)模擬項(xiàng)目X排查到最后發(fā)現(xiàn)是「近 30 天小區(qū)成交均價(jià)」這個(gè)特征它幾乎就是目標(biāo)值的加噪聲版本刪掉之后測(cè)試集指標(biāo)才恢復(fù)正常。特征來(lái)源審查不需要很復(fù)雜把特征列和來(lái)源寫(xiě)進(jìn)一個(gè) md 文件寫(xiě)不出來(lái)的特征就該懷疑。5.2 隨機(jī)切分讓評(píng)估結(jié)果虛高現(xiàn)象模型在隨機(jī)切分的測(cè)試集上表現(xiàn)很好但上線(xiàn)后預(yù)測(cè)新掛牌房源誤差明顯變大業(yè)務(wù)方開(kāi)始質(zhì)疑模型的可靠性。原因二手房?jī)r(jià)格帶時(shí)間趨勢(shì)隨機(jī)切分讓訓(xùn)練集和測(cè)試集高度同分布模型相當(dāng)于偷看了未來(lái)數(shù)據(jù)的分布規(guī)律評(píng)估結(jié)果虛高。解決如果數(shù)據(jù)帶掛牌時(shí)間按時(shí)間做切分——前 80% 月份訓(xùn)練、后 20% 月份測(cè)試沒(méi)有時(shí)間字段時(shí)用 GroupKFold 按小區(qū)分組交叉驗(yàn)證避免同一小區(qū)不同房源同時(shí)出現(xiàn)在訓(xùn)練和測(cè)試?yán)?。注意時(shí)間切分的測(cè)試集指標(biāo)通常比隨機(jī)切分低一些這是正常的別因此懷疑模型壞了。5.3 高基數(shù)類(lèi)別字段 One-Hot 維度爆炸現(xiàn)象One-Hot 之后訓(xùn)練數(shù)據(jù)從 20 列漲到 2000 多列訓(xùn)練變慢且明顯過(guò)擬合模型在驗(yàn)證集上的表現(xiàn)反而不如簡(jiǎn)單模型。原因把小區(qū)名、開(kāi)發(fā)商這種高基數(shù)類(lèi)別字段也丟進(jìn)了 get_dummies。解決小區(qū)名、區(qū)域改目標(biāo)編碼或頻次編碼只有朝向、裝修、戶(hù)型這種枚舉數(shù)小于 10 的字段才 One-Hot。如果業(yè)務(wù)上必須保留小區(qū)名先做頻次過(guò)濾只保留樣本數(shù)超過(guò)一定閾值的小區(qū)剩下的歸為 other 類(lèi)。5.4 爬蟲(chóng)采集被識(shí)別固定延時(shí)是致命傷現(xiàn)象采集到第 50 頁(yè)后開(kāi)始持續(xù)返回 403 或驗(yàn)證碼頁(yè)面導(dǎo)致數(shù)據(jù)中斷。原因請(qǐng)求頭固定、請(qǐng)求間隔固定、訪(fǎng)問(wèn)節(jié)奏和真實(shí)用戶(hù)差異明顯。站點(diǎn)風(fēng)控看的是短時(shí)間內(nèi)的訪(fǎng)問(wèn)節(jié)奏不是單次請(qǐng)求。解決每次請(qǐng)求之間用 random.uniform(2, 5) 隨機(jī)延時(shí)請(qǐng)求頭補(bǔ)成真實(shí)瀏覽器的完整集合UA、Accept、Accept-Language。血的教訓(xùn)別為了趕進(jìn)度把延時(shí)壓到 1 秒以?xún)?nèi)被封一次等解封的時(shí)間遠(yuǎn)超省下的時(shí)間。采集代碼上線(xiàn)前再加一個(gè)字段完整性校驗(yàn)當(dāng)解析結(jié)果為空或字段缺失時(shí)自動(dòng)告警而不是悄悄產(chǎn)出臟數(shù)據(jù)。5.5 價(jià)格單位口徑不統(tǒng)一現(xiàn)象整體評(píng)估不錯(cuò)但預(yù)測(cè)結(jié)果偶爾出現(xiàn)離譜價(jià)格比如面積 80 平方米、總價(jià)預(yù)測(cè) 0.08 萬(wàn)元看不出是哪里出了問(wèn)題。原因同一列里部分記錄是「488 萬(wàn)」另一部分是「4880000 元」清洗時(shí)沒(méi)有統(tǒng)一換算模型被迫學(xué)習(xí)了不一致的目標(biāo)口徑。解決清洗階段最后一個(gè)步驟加斷言檢查總價(jià)取值范圍和中位數(shù)是否落在真實(shí)量級(jí)。把元統(tǒng)一轉(zhuǎn)成萬(wàn)元后跑 df[total_price].describe()看 25%、50%、75% 分位數(shù)是否符合常識(shí)。做轉(zhuǎn)換函數(shù)時(shí)優(yōu)先用正則提取數(shù)字不要用字符串切片單位字符長(zhǎng)度在不同平臺(tái)上不可靠。提示這 5 個(gè)坑里數(shù)據(jù)泄露和訓(xùn)練測(cè)試切分最容易影響模型評(píng)估可信度。建議每次訓(xùn)練前都做一次特征來(lái)源審查再對(duì)比訓(xùn)練集和測(cè)試集的關(guān)鍵字段分布確認(rèn)沒(méi)有意外的信息泄漏。6. 進(jìn)階把預(yù)測(cè)系統(tǒng)從筆記本變成可迭代的服務(wù)訓(xùn)練完模型只是項(xiàng)目的一半另一半是讓它能持續(xù)被調(diào)用、持續(xù)被驗(yàn)證。這一章用一個(gè)輕量接口解決模型部署問(wèn)題再聊定期重訓(xùn)練和項(xiàng)目文檔的沉淀方式。6.1 模型持久化與最小推理接口訓(xùn)練結(jié)束后下一步是把模型從 Jupyter 里搬出來(lái)。用 joblib 保存模型和特征列再用 Flask 寫(xiě)一個(gè)最小接口讓項(xiàng)目從「跑完結(jié)果在 notebook 里」變成「能給別人調(diào)用的服務(wù)」。import joblib from flask import Flask, request, jsonify # 把模型和特征列一起打包特征列順序是推理的關(guān)鍵 joblib.dump({model: xgb, feature_cols: feature_cols}, service.joblib) pkg joblib.load(service.joblib) model, feature_cols pkg[model], pkg[feature_cols] app Flask(__name__) app.route(/predict, methods[POST]) def predict(): data request.get_json() features [data.get(col, 0) for col in feature_cols] return jsonify({total_price_pred: float(model.predict([features])[0])}) app.run(host0.0.0.0, port5001)這段代碼里最容易被忽略的是 feature_cols 的一致性。推理時(shí)傳入服務(wù)的數(shù)據(jù)列順序必須與訓(xùn)練時(shí)完全一致我習(xí)慣把特征列名單和模型打包存在同一個(gè)文件里推理端只從包里讀列名不手寫(xiě)特征列表。接口用 POST JSON 比 GET 更穩(wěn)妥參數(shù)多的時(shí)候不會(huì)被 URL 長(zhǎng)度限制卡住也方便調(diào)用方傳嵌套結(jié)構(gòu)。6.2 定期重訓(xùn)練與項(xiàng)目文檔沉淀二手房市場(chǎng)受供需、地段成熟度和基礎(chǔ)設(shè)施變化影響模型上線(xiàn)一個(gè)月后精度就會(huì)開(kāi)始下降。常見(jiàn)做法是每周拉取一次新掛牌數(shù)據(jù)和舊數(shù)據(jù)合并重新訓(xùn)練做時(shí)間切分驗(yàn)證。指標(biāo)沒(méi)有明顯下降就繼續(xù)用舊模型有明顯下降就用新模型替換。這個(gè)流程里最容易出問(wèn)題的不是訓(xùn)練而是新數(shù)據(jù)清洗和特征工程必須完全復(fù)用訓(xùn)練時(shí)的代碼——稍有不一致模型在線(xiàn)上的表現(xiàn)就會(huì)靜默變差。最后聊項(xiàng)目文檔。完整交付通常包含采集腳本、清洗腳本、特征工程腳本、模型訓(xùn)練腳本、評(píng)估實(shí)驗(yàn)記錄和一份數(shù)據(jù)字典。文檔不用寫(xiě)長(zhǎng)但必須寫(xiě)清數(shù)據(jù)來(lái)源、字段口徑、模型選型理由和調(diào)參記錄——不是給領(lǐng)導(dǎo)看是給三個(gè)月后的自己看的。我早期做過(guò)一個(gè)跨平臺(tái)數(shù)據(jù)項(xiàng)目調(diào)完模型后沒(méi)留文檔三周后再去改特征連當(dāng)時(shí)為什么對(duì)朝向做四分類(lèi)都記不清了最后只能重跑一遍實(shí)驗(yàn)。從那以后每個(gè)項(xiàng)目我都先建一個(gè)數(shù)據(jù)字典文件邊做邊補(bǔ)踩過(guò)的坑也順手記進(jìn)去。希望我的這些經(jīng)驗(yàn)?zāi)軒偷侥闵僮咭欢螐澛?。本文還有配套的精品資源點(diǎn)擊獲取