:從數(shù)據(jù)診斷到特征工程準備)
1. 從零讀懂Task2為什么EDA是二手車價格預測的第一道分水嶺很多人拿到一個回歸預測任務(wù)第一反應是趕緊上模型、調(diào)參數(shù)、刷榜單。我見過太多這樣的操作數(shù)據(jù)讀進來簡單fillna一下直接XGBoost跑起來然后盯著線上分數(shù)反復調(diào)參調(diào)了三天發(fā)現(xiàn)還不如別人一個簡單的線性回歸。問題出在哪出在壓根沒搞明白數(shù)據(jù)長什么樣。DataWhale的Task2定位非常明確在Task1已經(jīng)完成數(shù)據(jù)加載和初步觀察的基礎(chǔ)上系統(tǒng)性地做探索性數(shù)據(jù)分析EDA。這個階段的核心目標不是建模而是回答幾個關(guān)鍵問題——目標變量長什么樣特征和目標之間是什么關(guān)系特征之間有沒有冗余缺失值和異常值該怎么處理這些問題的答案直接決定了后續(xù)特征工程的方向和模型選型的天花板。二手車交易價格預測這個場景尤其特殊。它的目標變量是價格而價格數(shù)據(jù)天然具有幾個特點右偏分布、存在極端值、受多因素非線性影響。如果你不做EDA直接建模模型很可能被少數(shù)高價車帶偏或者被大量缺失值搞得性能驟降。我個人的經(jīng)驗是在一個中等規(guī)模的結(jié)構(gòu)化數(shù)據(jù)競賽中EDA做得好不好直接決定了你最終成績是前30%還是前10%。這篇文章面向的是已經(jīng)完成Task1、正準備進入Task2的參賽者也適合任何想系統(tǒng)學習結(jié)構(gòu)化數(shù)據(jù)EDA方法論的人。我會把Task2涉及的每個環(huán)節(jié)拆開講透包括為什么要這么做、具體怎么操作、實測中會遇到什么問題、以及我踩過的那些坑。代碼以Python為主工具鏈是pandas matplotlib seaborn這些都是數(shù)據(jù)競賽的標配不需要額外折騰環(huán)境。2. 數(shù)據(jù)加載后的第一件事把數(shù)據(jù)摸清楚2.1 數(shù)據(jù)規(guī)模與字段類型的快速體檢拿到數(shù)據(jù)后別急著畫圖。先做一次基礎(chǔ)體檢搞清楚三件事數(shù)據(jù)有多少行多少列、每個字段是什么類型、內(nèi)存占用多大。這聽起來很簡單但我見過有人跑了半天代碼才發(fā)現(xiàn)數(shù)據(jù)讀進來時類型全錯了數(shù)值型被當成了字符串。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 讀取數(shù)據(jù) train pd.read_csv(used_car_train.csv) test pd.read_csv(used_car_test.csv) # 基礎(chǔ)體檢 print(f訓練集形狀: {train.shape}) print(f測試集形狀: {test.shape}) print(f內(nèi)存占用: {train.memory_usage(deepTrue).sum() / 1024**2:.2f} MB) print(\n字段類型分布:) print(train.dtypes.value_counts())這里有個細節(jié)值得注意二手車數(shù)據(jù)里經(jīng)常有power、kilometer這類數(shù)值字段但有時候因為缺失值的存在pandas會把它們讀成object類型。如果你不檢查后面做數(shù)值計算時就會報錯。我的習慣是讀完數(shù)據(jù)后立刻用train.info()看一眼確認沒有類型異常。另外訓練集和測試集的字段差異也要檢查。測試集通常會少一個目標列price但其他字段應該完全一致。如果發(fā)現(xiàn)字段不一致要么是數(shù)據(jù)版本有問題要么是讀取時出了錯。2.2 目標變量price的分布診斷目標變量的分布是EDA的重中之重。對于價格預測任務(wù)你需要搞清楚它是正態(tài)分布、右偏分布還是雙峰分布。這直接決定了后續(xù)要不要做對數(shù)變換。# 目標變量分布 fig, axes plt.subplots(1, 3, figsize(18, 5)) # 原始分布 sns.histplot(train[price], bins50, axaxes[0], kdeTrue) axes[0].set_title(Price原始分布) # 對數(shù)變換后 sns.histplot(np.log1p(train[price]), bins50, axaxes[1], kdeTrue) axes[1].set_title(Log(Price1)分布) # 箱線圖看異常值 sns.boxplot(xtrain[price], axaxes[2]) axes[2].set_title(Price箱線圖) plt.tight_layout() plt.show() # 統(tǒng)計量 print(train[price].describe()) print(f偏度: {train[price].skew():.4f}) print(f峰度: {train[price].kurtosis():.4f})實測下來二手車價格數(shù)據(jù)幾乎必然是右偏的偏度通常在2以上。這意味著大部分車價格集中在較低區(qū)間少數(shù)豪車拉高了尾部。如果你直接用原始price做回歸模型會過度關(guān)注那些高價樣本導致整體預測偏差。我的處理建議是先看對數(shù)變換后的分布是否接近正態(tài)。如果接近后續(xù)建模時對目標變量做log1p變換預測后再用expm1還原。這個操作在Kaggle的很多回歸競賽中都是標配效果立竿見影。但要注意如果評價指標是RMSE對數(shù)變換后還原會引入偏差這時候需要對預測結(jié)果做無偏修正或者直接用MAE作為評價指標。2.3 缺失值的全局掃描與分類缺失值處理是EDA中最容易被輕視的環(huán)節(jié)。很多人看到缺失值就無腦填均值或中位數(shù)這是大忌。正確的做法是先搞清楚缺失的模式是隨機缺失還是系統(tǒng)性缺失缺失比例是多少缺失本身是否攜帶信息# 缺失值統(tǒng)計 missing train.isnull().sum() missing_pct (missing / len(train)) * 100 missing_df pd.DataFrame({ 缺失數(shù)量: missing, 缺失比例(%): missing_pct }).sort_values(缺失比例(%), ascendingFalse) print(missing_df[missing_df[缺失數(shù)量] 0])在二手車數(shù)據(jù)中bodyType、fuelType、gearbox這幾個類別字段經(jīng)常有缺失而model、brand等字段缺失較少。這里有個關(guān)鍵判斷如果某個字段缺失比例超過40%填充的風險很大可以考慮直接構(gòu)造一個缺失類別讓模型自己學習缺失是否意味著某種特殊車況。我踩過的一個坑是對notRepairedDamage字段做填充時這個字段的值是-和0.0直接astype(float)會報錯。必須先replace(-, np.nan)再處理。這種臟數(shù)據(jù)在真實業(yè)務(wù)數(shù)據(jù)里非常常見EDA階段不發(fā)現(xiàn)建模時就會卡住。3. 單變量與多變量分析從每個字段里榨出信息3.1 數(shù)值型特征的分布與業(yè)務(wù)含義解讀數(shù)值型特征不能只看統(tǒng)計量要結(jié)合業(yè)務(wù)含義去理解。以kilometer為例它代表車輛行駛里程。如果你只看describe()可能覺得就是個普通數(shù)值。但結(jié)合業(yè)務(wù)想一想里程越高車況越差價格應該越低。如果EDA發(fā)現(xiàn)里程和價格沒有明顯負相關(guān)那要么是數(shù)據(jù)有問題要么是這個字段被噪聲污染了。# 數(shù)值特征分布 num_cols [power, kilometer, brand, model] fig, axes plt.subplots(2, 2, figsize(14, 10)) for idx, col in enumerate(num_cols): ax axes[idx // 2][idx % 2] sns.histplot(train[col].dropna(), bins50, axax, kdeTrue) ax.set_title(f{col}分布) plt.tight_layout() plt.show()power字段馬力通常也是右偏的大部分車在100-200馬力之間少數(shù)高性能車超過500。這個字段和價格的相關(guān)性往往很強但要注意非線性關(guān)系——馬力從100到200對價格的影響可能遠大于從400到500。brand和model雖然是數(shù)值編碼但本質(zhì)上是類別特征。直接當數(shù)值用會引入錯誤的序關(guān)系。比如brand1和brand2之間未必有大小關(guān)系。這類字段在EDA階段就要標記出來后續(xù)做類別編碼。3.2 類別型特征的頻次分析與目標編碼預判類別特征的分析重點是頻次分布和目標均值分布。頻次太低的小類別模型很難學到有效模式可以考慮合并。而目標均值分布則能告訴你這個特征是否有區(qū)分度。# 類別特征分析 cat_cols [brand, bodyType, fuelType, gearbox, notRepairedDamage] for col in cat_cols: print(f\n {col} ) print(f唯一值數(shù)量: {train[col].nunique()}) print(train[col].value_counts().head(10)) print(f目標均值:\n{train.groupby(col)[price].mean().head(10)})這里有個經(jīng)驗如果某個類別特征的唯一值超過50個比如model字段可能有上千個取值直接用One-Hot會導致維度爆炸。這時候目標編碼Target Encoding或者頻率編碼是更好的選擇。但目標編碼容易過擬合必須配合交叉驗證或者平滑處理。notRepairedDamage這個字段特別有意思。它表示車輛是否有未修復的損壞。從業(yè)務(wù)邏輯看有未修復損壞的車價格應該更低。如果EDA發(fā)現(xiàn)這個字段的缺失本身就和低價相關(guān)那缺失值就不能隨便填而要當作一個獨立類別。3.3 特征與目標的相關(guān)性矩陣與非線性關(guān)系識別相關(guān)性分析是EDA的常規(guī)操作但很多人只看皮爾遜相關(guān)系數(shù)這只能捕捉線性關(guān)系。對于二手車價格這種復雜場景非線性關(guān)系才是主流。# 相關(guān)性矩陣 corr_matrix train.corr() plt.figure(figsize(16, 12)) sns.heatmap(corr_matrix, cmapcoolwarm, center0, annotFalse) plt.title(特征相關(guān)性熱力圖) plt.show() # 與目標的相關(guān)性排序 target_corr corr_matrix[price].sort_values(ascendingFalse) print(target_corr)實測中和price相關(guān)性最高的往往是power、kilometer、brand這幾個。但相關(guān)系數(shù)低不代表特征沒用可能是非線性關(guān)系。比如kilometer和price可能是分段關(guān)系低里程區(qū)間價格下降快高里程區(qū)間價格下降慢。我的做法是對每個重要數(shù)值特征畫散點圖或者用分箱后的箱線圖看趨勢。如果發(fā)現(xiàn)明顯的非線性模式后續(xù)可以考慮多項式特征或者分箱離散化。4. 異常值與重復值的處理策略別讓臟數(shù)據(jù)毀掉模型4.1 異常值檢測IQR、Z-Score與業(yè)務(wù)規(guī)則三管齊下異常值處理沒有萬能公式必須結(jié)合統(tǒng)計方法和業(yè)務(wù)規(guī)則。統(tǒng)計方法能發(fā)現(xiàn)數(shù)值上的離群點業(yè)務(wù)規(guī)則能判斷這些離群點是否合理。# IQR方法檢測異常值 def detect_outliers_iqr(df, col): Q1 df[col].quantile(0.25) Q3 df[col].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR outliers df[(df[col] lower) | (df[col] upper)] return outliers, lower, upper # 對price檢測 outliers, lower, upper detect_outliers_iqr(train, price) print(fPrice異常值數(shù)量: {len(outliers)}) print(f正常范圍: [{lower:.2f}, {upper:.2f}])但要注意價格數(shù)據(jù)里的異常值未必是錯誤數(shù)據(jù)。一輛限量版豪車價格遠超普通車這是合理的業(yè)務(wù)現(xiàn)象不是數(shù)據(jù)錯誤。如果直接刪除模型就學不到高價車的模式。我的建議是對于價格預測任務(wù)不要輕易刪除高價樣本而是考慮用對數(shù)變換壓縮尺度或者用分位數(shù)回歸、Huber損失等對異常值魯棒的建模方法。power字段有時候會出現(xiàn)0或者異常大的值。0馬力顯然不合理可能是缺失值被填成了0。這種就要當作缺失值重新處理。4.2 重復值排查完全重復與業(yè)務(wù)重復的區(qū)別重復值分兩種完全重復所有字段都一樣和業(yè)務(wù)重復關(guān)鍵字段一樣但其他字段有差異。完全重復直接drop_duplicates即可但業(yè)務(wù)重復要小心。# 完全重復 print(f完全重復行數(shù): {train.duplicated().sum()}) # 業(yè)務(wù)重復同一輛車多次出現(xiàn) key_cols [brand, model, kilometer, power] biz_dup train.duplicated(subsetkey_cols, keepFalse) print(f業(yè)務(wù)重復行數(shù): {biz_dup.sum()})如果發(fā)現(xiàn)同一組關(guān)鍵字段對應多個不同的price可能是數(shù)據(jù)采集時的重復記錄也可能是不同交易時間的價格差異。這時候不能簡單去重要結(jié)合業(yè)務(wù)判斷。我一般會保留第一條或者取價格均值。4.3 異常值處理后的驗證分布對比與模型反饋處理完異常值后一定要做前后對比驗證。最直接的方法是畫處理前后的分布對比圖看分布形態(tài)是否更合理。# 處理前后對比 fig, axes plt.subplots(1, 2, figsize(14, 5)) sns.histplot(train[price], bins50, axaxes[0]) axes[0].set_title(處理前) sns.histplot(train_clean[price], bins50, axaxes[1]) axes[1].set_title(處理后) plt.show()更嚴謹?shù)淖龇ㄊ怯锰幚砗蟮臄?shù)據(jù)跑一個基線模型看交叉驗證分數(shù)是否提升。如果處理后分數(shù)反而下降說明你可能刪掉了有用信息。這個反饋循環(huán)是EDA和建模之間的橋梁很多人忽略了這一步導致EDA和建模脫節(jié)。5. 特征工程前的EDA準備為后續(xù)Task鋪路5.1 時間特征的拆解與業(yè)務(wù)周期識別二手車數(shù)據(jù)里通常有regDate注冊日期和creatDate數(shù)據(jù)創(chuàng)建日期。這兩個字段能衍生出車齡、使用年限等強特征。# 時間特征處理 train[regDate] pd.to_datetime(train[regDate], format%Y%m%d, errorscoerce) train[creatDate] pd.to_datetime(train[creatDate], format%Y%m%d, errorscoerce) train[car_age] (train[creatDate] - train[regDate]).dt.days / 365 # 車齡分布 sns.histplot(train[car_age].dropna(), bins50) plt.title(車齡分布) plt.show()車齡和價格的關(guān)系通常是非線性的3年內(nèi)的車貶值最快之后趨于平緩。EDA階段要確認這個模式是否存在如果存在后續(xù)可以對車齡做分箱或者樣條變換。另外creatDate本身可能攜帶時間信息。如果數(shù)據(jù)采集跨越了較長時間不同時間段的市場行情不同這個字段可能和價格有隱含關(guān)聯(lián)。但要注意如果測試集的creatDate分布和訓練集差異很大直接用這個字段可能導致過擬合。5.2 高基數(shù)類別特征的編碼預研model字段可能有上千個取值brand也有幾十個。這類高基數(shù)類別特征怎么編碼是EDA階段就要想清楚的問題。# 高基數(shù)特征統(tǒng)計 for col in [brand, model]: freq train[col].value_counts() print(f{col}: 總類別數(shù){len(freq)}, 出現(xiàn)次數(shù)10的類別數(shù){(freq10).sum()})如果大量類別出現(xiàn)次數(shù)極少可以考慮把低頻類別合并成其他?;蛘哂媚繕司幋a但必須做平滑encoding (count * mean prior * smoothing) / (count smoothing)。這個平滑參數(shù)需要在交叉驗證中調(diào)。我的經(jīng)驗是對于二手車這種數(shù)據(jù)brand用目標編碼效果通常不錯但model因為基數(shù)太高目標編碼容易過擬合可以考慮用頻率編碼或者嵌入Embedding的方式。5.3 特征交互的初步探索哪些組合值得深挖EDA的最后一步是探索特征交互。有些特征單獨看區(qū)分度一般但組合起來很強。比如brand和car_age的組合同樣是3年車齡豪華品牌和普通品牌的貶值曲線完全不同。# 特征交互探索 pivot train.pivot_table(valuesprice, indexbrand, columnscar_age_bin, aggfuncmean) sns.heatmap(pivot, cmapYlOrRd) plt.title(品牌與車齡的價格交互) plt.show()這種交互分析能幫你發(fā)現(xiàn)哪些組合特征值得構(gòu)造。但要注意EDA階段只是探索不要在這里花太多時間構(gòu)造特征那是Task3的事。EDA的目標是找到方向不是完成所有工作。6. 我在Task2實操中踩過的坑與總結(jié)的技巧第一個坑是數(shù)據(jù)類型陷阱。二手車數(shù)據(jù)里很多字段看起來是數(shù)值實際是字符串比如notRepairedDamage的-。如果不處理后面所有數(shù)值操作都會報錯。我的建議是讀完數(shù)據(jù)后立刻做一次df.apply(lambda x: x.dtype)檢查對object類型的字段逐個確認。第二個坑是缺失值填充的連鎖反應。我曾經(jīng)對bodyType用眾數(shù)填充結(jié)果發(fā)現(xiàn)填充后這個字段和目標變量的相關(guān)性反而下降了。后來才明白缺失本身可能意味著某種特殊車型填成眾數(shù)反而抹掉了這個信息。正確的做法是先構(gòu)造一個bodyType_isnull的指示特征再用模型預測填充或者保留缺失。第三個坑是異常值刪除過度。有一次我按IQR刪掉了price的異常值結(jié)果交叉驗證分數(shù)掉了0.02。原因是那些高價樣本雖然少但包含了重要的模式信息。后來我改用對數(shù)變換分數(shù)就回來了。第四個坑是忽略了訓練集和測試集的分布一致性。EDA不能只看訓練集測試集的分布也要對比。如果某個特征在測試集里的分布和訓練集差異很大說明可能存在數(shù)據(jù)漂移直接用這個特征風險很高。最后分享一個實用技巧EDA階段的所有發(fā)現(xiàn)都要記錄下來形成一個數(shù)據(jù)洞察清單。比如price右偏需要log變換、kilometer和price非線性、bodyType缺失可能攜帶信息。這個清單就是Task3特征工程的路線圖。沒有這個清單你做到Task3時會忘記EDA階段的發(fā)現(xiàn)又得重新看一遍數(shù)據(jù)。另外可視化不要追求花哨能說明問題就行。我見過有人花大量時間做交互式圖表結(jié)果對建模沒有任何幫助。EDA的可視化目標是快速驗證假設(shè)不是做數(shù)據(jù)報告。一張清晰的箱線圖比十張炫酷的3D圖更有價值。