測實戰(zhàn):數(shù)據(jù)清洗、特征工程與模型評估全流程解析)
簡介這是一份面向Python課程設(shè)計、期末大作業(yè)及畢業(yè)設(shè)計的二手車價格預(yù)測案例數(shù)據(jù)挖掘項目資源適合已掌握Python基礎(chǔ)語法、希望完整跑通數(shù)據(jù)探索與建模流程的學(xué)習(xí)者。資源以真實二手車交易數(shù)據(jù)為輸入覆蓋數(shù)據(jù)清洗、特征工程、模型訓(xùn)練與評估等核心環(huán)節(jié)源碼包含詳細(xì)中文注釋新手也能逐步讀懂并可直接復(fù)現(xiàn)或改造。壓縮包共27個文件包含py源碼、csv數(shù)據(jù)集、docx實驗報告、png結(jié)果圖表、xml工程配置與md說明文檔等類型整體約34.86MB目錄劃分清晰按“代碼、數(shù)據(jù)、圖片、文檔”分別存放便于按模塊學(xué)習(xí)與整理。目前已有148人學(xué)習(xí)下載。項目經(jīng)過嚴(yán)格調(diào)試運行穩(wěn)定操作界面簡潔作為課程設(shè)計/畢業(yè)設(shè)計的高分參考模板具備很強的實用價值與擴展空間。1. 一份能跑通的 Python 二手車價格預(yù)測項目先搞清楚它值不值得下選期末大作業(yè)題目時二手車價格預(yù)測幾乎是數(shù)據(jù)挖掘課的「大眾款」但真正能在答辯現(xiàn)場把每個環(huán)節(jié)講清楚、能把代碼從報錯里撈出來的人不多。這套壓縮包我拆過一遍里面是完整源碼、詳細(xì)注釋、實驗報告和數(shù)據(jù)集四件套走的是數(shù)據(jù)挖掘的標(biāo)準(zhǔn)流程加載數(shù)據(jù)、清洗、特征工程、建模、評估不是那種只給個model.fit就完事的半成品。它的技術(shù)棧是純 Python pandas scikit-learn適合課程設(shè)計、期末大作業(yè)和本科畢設(shè)新手照著注釋能看懂每一步在干嘛熟手可以拿它當(dāng)基線工程改出自己的版本。文檔和代碼對應(yīng)的環(huán)境不復(fù)雜裝好依賴就能復(fù)現(xiàn)。2. 動手前資源盤點這份壓縮包里到底裝了啥2.1 目錄結(jié)構(gòu)與文件職責(zé)壓縮包解壓后第一件事不是急著雙擊運行而是先把目錄結(jié)構(gòu)看明白。常見做法是先對著 README 梳理文件用途再決定先看哪份文檔。這份資源里的目錄形態(tài)一般是這樣的pythonProject ├── code-main │ ├── .idea │ ├── data # 原始數(shù)據(jù)集存放目錄 │ ├── src # 核心源碼目錄 │ ├── report # 實驗報告 │ ├── README.md # 項目說明 │ └── requirements.txt.idea是 PyCharm 的工程配置目錄它存在只代表作者用 PyCharm 打開過項目對你運行代碼沒有任何影響刪掉也不影響別被它嚇到。重點看src或者與code-main平級的代碼文件數(shù)據(jù)挖掘大作業(yè)的代碼通常按「數(shù)據(jù)讀取、數(shù)據(jù)清洗、建模、評估」拆成幾個.py文件或直接一個.ipynb文件打通全部流程。2.2 環(huán)境準(zhǔn)備與依賴安裝這份項目在 PyCharm 或者 VS Code 里都能跑我更推薦用 PyCharm因為大作業(yè)項目里帶著.idea目錄導(dǎo)入時它能保留作者的運行配置和虛擬環(huán)境引用。先把依賴裝齊python -m venv venv source venv/bin/activate # Windows 上用 venv\Scripts\activate pip install -r requirements.txt pip install pandas numpy scikit-learn matplotlib seabornvenv是 Python 3.3 以后內(nèi)置的虛擬環(huán)境模塊比直接裝在全局環(huán)境里干凈得多能避免不同項目之間包版本沖突。scikit-learn建議裝在 1.x 的穩(wěn)定版本上不要用 alpha 版否則 API 可能對不上。裝完以后可以先用python -c import pandas; print(pandas.__version__)做一個快速驗證。2.3 第一次跑通全流程代碼能不能跑通是判斷這個資源是否可用的第一步。如果項目入口是一個.py文件用命令行執(zhí)行如果是 Jupyter Notebook自己按順序執(zhí)行每個 cellcd code-main python src/train.py --data data/used_cars.csv --model output/model.pkl--data指定的是二手車數(shù)據(jù)集的 CSV 路徑--model是訓(xùn)練完成后模型的輸出路徑。很多大作業(yè)源碼會把路徑寫死在代碼里如果你執(zhí)行時發(fā)現(xiàn)文件路徑報錯優(yōu)先看當(dāng)前終端路徑和源碼里pd.read_csv()引用的相對路徑是否一致。這一步跑通之后你已經(jīng)確認(rèn)了項目的運行鏈路是通的接下來再逐行看數(shù)據(jù)流的細(xì)節(jié)。跑通不是終點后面還會踩到各種隱患。3. 數(shù)據(jù)讀入與探索性分析先看懂車價數(shù)據(jù)的脾氣3.1 字段字典與數(shù)據(jù)量確認(rèn)讀完數(shù)據(jù)第一件事是什么不是馬上describe()而是確認(rèn)行數(shù)和列名防止數(shù)據(jù)集被人動過手腳。這是一份二手車數(shù)據(jù)集里常見的字段字典字段名含義類型缺失情況說明brand品牌類別通常完整model車型類別可能有少量臟值year上牌年份數(shù)值可能含 0 或極值km_driven已行駛里程數(shù)值可能有單位混用engine_cc排量數(shù)值部分車缺失fuel_type燃油類型類別少量缺失transmission變速箱類別一般完整price二手售價數(shù)值目標(biāo)變量必須有import pandas as pd df pd.read_csv(data/used_cars.csv) print(df.shape) print(df.dtypes) print(df.isnull().sum())df.shape返回的元組里第一個是行數(shù)第二個是列數(shù)行數(shù)太少說明樣本量不足后面模型容易欠擬合行數(shù)過多則要注意內(nèi)存占用。df.isnull().sum()是處理缺失值前的摸底動作它告訴你每一列的缺失情況后續(xù)處理策略都由這張表決定。3.2 目標(biāo)變量與特征分布價格分析要一上來先看分布的形狀這不僅決定用什么模型還決定要不要對目標(biāo)變量做變換。真實場景里二手車的價格分布往往右偏嚴(yán)重低價的便宜車數(shù)量巨大高價豪車只有零星幾輛這完全是正常現(xiàn)象。import matplotlib.pyplot as plt import seaborn as sns sns.histplot(df[price], bins50, kdeTrue) plt.title(Price Distribution - Before Log Transform) plt.show() import numpy as np log_price np.log1p(df[price]) sns.histplot(log_price, bins50, kdeTrue) plt.title(Price Distribution - After Log Transform) plt.show()log1p是log(1x)的簡寫它的好處是當(dāng)價格為 0 時結(jié)果仍然有意義而直接取對數(shù)會因為 0 值報錯???log 之后的分布如果接近正態(tài)后續(xù)建模時可以基于log(price)做回歸最后預(yù)測值再用expm1還原成實際價格。右偏分布不處理模型對高價車的預(yù)測誤差會被均勻地「掩蓋」在低價區(qū)里這是數(shù)據(jù)挖掘課上一個很加分的處理細(xì)節(jié)。3.3 相關(guān)性分析與數(shù)據(jù)切分特征之間的相關(guān)性和特征與目標(biāo)的相關(guān)性要分開看。前者幫助你去冗余后者幫你做篩選但不能單靠相關(guān)系數(shù)決定一切特征去留某些相關(guān)性弱的特征是合法信號相關(guān)性強的特征則可能存在泄漏風(fēng)險。numeric_cols df.select_dtypes(include[np.number]).columns corr_matrix df[numeric_cols].corr() plt.figure(figsize(10, 8)) sns.heatmap(corr_matrix, annotTrue, cmapcoolwarm, fmt.2f) plt.show() from sklearn.model_selection import train_test_split X df.drop(price, axis1) y df[price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )annotTrue把相關(guān)系數(shù)值直接標(biāo)在熱力圖格子里方便的排查兩個特征相關(guān)性超過 0.9 的情況比如engine_cc和engine_type可能有強關(guān)聯(lián)重復(fù)建模等價于一個特征用了兩遍。random_state42的作用是固定隨機切分的種子保證每次運行得到同一份訓(xùn)練測試集大作業(yè)答辯時同一份代碼輸出穩(wěn)定結(jié)果是基本要求。4. 特征工程讓模型能聽懂的數(shù)據(jù)長什么樣4.1 缺失值與異常值處理缺失值不能統(tǒng)一的填 0 或填均值得看業(yè)務(wù)含義來決定。km_driven缺失可以考慮用品牌和年份組合的眾數(shù)填充其背后的理由是同一年份、同一品牌的二手車行駛里程中位數(shù)具備參照性總比全表填充一個有意義的數(shù)字更合理。df[km_driven] df.groupby([brand, year])[km_driven].transform( lambda s: s.fillna(s.median()) ) df[engine_cc].fillna(df[engine_cc].median(), inplaceTrue)transform和普通的apply區(qū)別在于transform返回的是和原 DataFrame 等長的對象可以直接填入原列不會改變索引結(jié)構(gòu)。groupby里的兩個鍵是填充的參照維度組合維度越細(xì)填充越準(zhǔn)但維度太細(xì)可能導(dǎo)致某些組樣本太少執(zhí)行時會看到警告不用太緊張。數(shù)值型缺失用中位數(shù)填充比均值更抗異常值這是數(shù)據(jù)挖掘課里容易在答辯時被老師追問的一個點。異常值處理要用箱線圖定位而不是人眼看圖。先用IQR方法定義異常值邊界q1 df[km_driven].quantile(0.25) q3 df[km_driven].quantile(0.75) iqr q3 - q1 lower_bound q1 - 1.5 * iqr upper_bound q3 1.5 * iqr outliers df[(df[km_driven] lower_bound) | (df[km_driven] upper_bound)] print(f異常值數(shù)量: {len(outliers)})IQR的計算邏輯是把落在(Q1 - 1.5IQR, Q3 1.5IQR)這個區(qū)間之外的點視為異常值這個 1.5 是經(jīng)驗系數(shù)沒有絕對正確性。在二手車場景中高里程車可能是真實存在的運營車輛不一定直接刪可以先看看這些異常值有多少占比小就刪占比大切不可一刀切。4.2 類別編碼與數(shù)值歸一化類別特征不能直接喂給 scikit-learn常見做法是先做 Label Encoding 再轉(zhuǎn)機器學(xué)習(xí)模型能用的格式但無序類別用 Label Encoding 等于強行給類別排了大小順序這個邏輯在很多模型里是不成立的。品牌屬于無序類別推薦 One-Hot 編碼。年份和里程屬于有序數(shù)值直接保留from sklearn.preprocessing import OneHotEncoder, StandardScaler import pandas as pd categorical_cols [brand, fuel_type, transmission] encoder OneHotEncoder(handle_unknownignore) encoded_arr encoder.fit_transform(df[categorical_cols]) numeric_cols [year, km_driven, engine_cc] scaler StandardScaler() scaled_arr scaler.fit_transform(df[numeric_cols])handle_unknownignore的意思是訓(xùn)練時沒見過的類別在預(yù)測時會整行轉(zhuǎn)成全 0 向量而不是直接報錯。這在實際部署時很有用新車品牌上市后模型不至于崩潰。StandardScaler把連續(xù)特征轉(zhuǎn)換成均值為 0、標(biāo)準(zhǔn)差為 1 的標(biāo)準(zhǔn)正態(tài)分布對線性模型和神經(jīng)網(wǎng)絡(luò)友好對決策樹系模型則意義不大因為樹模型不關(guān)心量綱。這里有一個需要留意的細(xì)節(jié)scaled_arr和encoded_arr需要拼接時不能直接用np.hstack把原來的 DataFrame 也拼進(jìn)去因為原 DataFrame 里還殘留著未編碼的原始字符串列拼進(jìn)去會導(dǎo)致 dtype 不一致而報錯。4.3 衍生特征與業(yè)務(wù)規(guī)則特征工程加分項常常來自對業(yè)務(wù)的拆解而不是更多的調(diào)參。二手車的車齡比年份更直接年份是絕對時間車齡是相對當(dāng)前時間模型更容易理解后者from datetime import datetime current_year datetime.now().year df[car_age] current_year - df[year] df[price_per_km] df[price] / (df[km_driven] 1) df[year_bucket] pd.cut( df[year], bins[1990, 2000, 2008, 2015, 2024], labels[90s, 00s, 10s_early, 10s_late] )car_age把「越新越貴」這個業(yè)務(wù)直覺直接變成了數(shù)值特征。price_per_km是個單位價格指標(biāo)注意千萬不能被用作建模輸入因為它由price除以里程構(gòu)造和預(yù)測目標(biāo)存在直接函數(shù)關(guān)系放進(jìn)特征就是典型泄漏。pd.cut把年份分箱讓模型可以從不同年代區(qū)間學(xué)習(xí)不同的價格衰減模式。衍生特征不是越多越好新增每個特征之前先問一句這個特征在預(yù)測時能拿到嗎比如「該車已售天數(shù)」如果預(yù)測時必須先知道結(jié)果那就是數(shù)據(jù)泄漏大作業(yè)答辯時這種問題一旦被提問會被扣掉不少分。5. 建模避坑與調(diào)參這些坑我替你先踩了5.1 坑 1把目標(biāo)變量標(biāo)準(zhǔn)化后忘了逆變換現(xiàn)象訓(xùn)練用標(biāo)準(zhǔn)化后的log(price)直接調(diào)model.predict(X_test)得到的預(yù)測值是一個標(biāo)準(zhǔn)化過的無量綱數(shù)字和人眼能理解的價格差著十萬八千里。原因數(shù)據(jù)挖掘流程里目標(biāo)變量進(jìn)模型前做了StandardScaler或LogTransform預(yù)測結(jié)果也要跟著做對應(yīng)的逆變換很多人只記住了特征要歸一化忘了目標(biāo)是同一套邏輯。解決預(yù)測后必須做逆變換。如果用的是np.log1p(y)還原就是np.expm1(pred)如果用了StandardScaler變換目標(biāo)就用scaler.inverse_transform(pred)還原??梢栽诖a里寫一個固定函數(shù)def inverse_transform_price(pred_log): return np.expm1(pred_log) pred_price inverse_transform_price(model.predict(X_test_scaled))5.2 坑 2訓(xùn)練集驗證集混在一起做編碼現(xiàn)象OneHotEncoder在整份 DataFrame 上fit_transform后再切訓(xùn)練集測試集測試集信息泄漏進(jìn)訓(xùn)練集然后測試集 AUC 虛高。答辯時老師問一句「你測試集怎么切的」如果回答不上來基本扣分。原因編碼器和標(biāo)準(zhǔn)化器必須在訓(xùn)練集上fit再分別transform訓(xùn)練集和測試集。整表fit_transform等于用全量數(shù)據(jù)的分布信息去變換訓(xùn)練集測試集的類別分布提前暴露給了模型。解決先切分再做編碼。正確順序是train_test_split在前特征編碼在后from sklearn.preprocessing import OneHotEncoder X_train, X_test, y_train, y_test train_test_split(...) encoder OneHotEncoder(handle_unknownignore) encoder.fit(X_train[[brand, fuel_type]]) X_train_encoded encoder.transform(X_train[[brand, fuel_type]]) X_test_encoded encoder.transform(X_test[[brand, fuel_type]])5.3 坑 3回歸問題用準(zhǔn)確率當(dāng)評判指標(biāo)現(xiàn)象在代碼里看到from sklearn.metrics import accuracy_score并且直接對price的預(yù)測結(jié)果調(diào)用輸出值是 0.0 或者一個毫無意義的小數(shù)點。原因回歸問題的預(yù)測值是連續(xù)實數(shù)準(zhǔn)確率只適合分類問題中類別完全相等的判斷。用準(zhǔn)確率評回歸任務(wù)邏輯上就不成立。解決回歸任務(wù)看mean_squared_error、mean_absolute_error或R2 score。價格跨度大的數(shù)據(jù)里MAE 比 MSE 更接近人理解的「平均差多少錢」from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae mean_absolute_error(y_test, pred_price) mse mean_squared_error(y_test, pred_price) r2 r2_score(y_test, pred_price) print(fMAE: {mae:.2f} | MSE: {mse:.2f} | R2: {r2:.4f})MAE 單位是「元」直接對應(yīng)人腦理解的平均差距。R2 接近 1 說明模型解釋了大部分方差但 R2 對異常值敏感實測時單看 R2 高有可能是運氣好要結(jié)合 MAE 一起看。碰到 R2 很高但 MAE 也高的怪象基本是少量極貴車的預(yù)測殘差拉高了誤差的平方項。5.4 坑 4亂調(diào)參之前不看交叉驗證的穩(wěn)定度現(xiàn)象手動改了一堆參數(shù)比如n_estimators500, max_depth20, learning_rate0.01換來換去測試集分?jǐn)?shù)忽高忽低不知道到底哪些參數(shù)真正有效。原因沒有做交叉驗證單次切分的測試集結(jié)果受隨機性影響很大同一組參數(shù)在不同隨機種子下表現(xiàn)可能差 3 到 5 個點。解決先跑一個固定KFold做基線確認(rèn)模型穩(wěn)定性再進(jìn)網(wǎng)格搜索from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor(n_estimators200, random_state42) scores cross_val_score(rf, X_train, y_train, cv5, scoringr2) print(fCV R2 - mean: {scores.mean():.4f}, std: {scores.std():.4f})cv5把訓(xùn)練集切成 5 份每輪用其中 4 份訓(xùn)練、1 份驗證最終分?jǐn)?shù)取 5 輪平均??磗td比看mean更重要std超過 0.05 說明模型對數(shù)據(jù)分布的局部變化太敏感優(yōu)先處理數(shù)據(jù)問題而不是繼續(xù)調(diào)參。6. 驗證與部署如何把模型從屏幕搬到大作業(yè)報告里要讓答辯現(xiàn)場的老師相信你的模型真的有用光有訓(xùn)練時那幾張圖不夠最好準(zhǔn)備一個「預(yù)測結(jié)果對照表」隨機選幾輛車把真實價格和預(yù)測價格并列再做一張殘差分布圖。這份資源里沒有專門的可視化界面但用 Flask 包一個本地預(yù)測接口并不費事手打代碼時牢記模型內(nèi)部保存的文件格式。模型訓(xùn)練完畢后用joblib把管線整體保存這是最省心的做法因為歸一化器和編碼器會連同訓(xùn)練好的模型一起打包import joblib joblib.dump(rf, output/model_pipeline.pkl)恢復(fù)使用時直接加載并predict不需要重新編碼特征順序省掉不少時間。驗證時除了 R2 和 MAE我建議加一個殘差圖檢查residuals y_test - pred_price plt.scatter(pred_price, residuals, alpha0.5) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted Price) plt.ylabel(Residuals) plt.show()殘差圖里如果散點圍繞紅色零線上下基本均勻分布說明模型預(yù)測誤差沒有明顯趨勢如果出現(xiàn)喇叭形狀——預(yù)測值越大殘差越散通常對應(yīng)高價區(qū)樣本量不足這時可以考慮對目標(biāo)值加 log 變換或者按品牌單獨建模。一般來說評價一個模型值不值得寫進(jìn)報告殘差圖比 R2 更有說服力。部署接口不用做得很復(fù)雜本地能跑通即可from flask import Flask, request, jsonify app Flask(__name__) app.route(/predict, methods[POST]) def predict(): payload request.get_json() features [[ payload[brand], payload[fuel_type], payload[transmission], payload[year], payload[km_driven], payload[engine_cc] ]] pred model.predict(features)[0] return jsonify({predicted_price: round(float(pred), 2)})接口數(shù)據(jù)格式是 JSONget_json()拿到的是一個字典前臺傳過來的字段名必須和訓(xùn)練時的列名完全一致否則模型構(gòu)造特征時順序錯位預(yù)測結(jié)果直接廢掉。部署這種模塊化接口課程設(shè)計足夠用跟老師講清楚「模型持久化、接口分離、可嵌入其他系統(tǒng)」比單獨交一個.py文件有明顯優(yōu)勢。如果這份資源里的預(yù)測結(jié)果和實際價格差距始終偏大優(yōu)先檢查數(shù)據(jù)里是否有品牌混入拼寫錯誤比如Toyota和Toyta被編碼成兩個類別測試集上一個從沒見過的拼法會直接變?nèi)?0 向量預(yù)測值被拉向均值。從那以后我每次拿到新數(shù)據(jù)會先跑一輪df[brand].value_counts()把詞頻過低且拼寫可疑的類別做歸一化處理再拆分?jǐn)?shù)據(jù)集已經(jīng)成了固定習(xí)慣。希望幫到你。本文還有配套的精品資源點擊獲取