:貸款違約預測的數(shù)據(jù)分析與特征工程)
上一次聊完了賽題理解和整體建模思路今天正式進入數(shù)據(jù)分析階段。天池這場金融風控比賽數(shù)據(jù)拿到手第一步不是急著訓練模型而是先把數(shù)據(jù)摸透。EDAExploratory Data Analysis在這個賽題里的地位相當關(guān)鍵——它直接決定你后續(xù)特征工程的方向、缺失值處理的策略甚至影響模型選型。我把這次完整的數(shù)據(jù)分析過程整理出來包含具體代碼、圖表解讀和踩坑記錄給準備做貸款違約預測或者類似信貸風控項目的人一個參考。這次的分析對象是某頭部金融平臺的貸款記錄數(shù)據(jù)訓練集約12萬條樣本、47個特征標簽是用戶是否違約。字段覆蓋了借款金額、利率、分期數(shù)、收入水平、負債率、征信歷史、居住地區(qū)等維度還包含十幾個匿名脫敏特征。整體來看這是一份非常典型的二分類風控數(shù)據(jù)集類別不平衡、缺失值不少、特征類型混雜正是考驗數(shù)據(jù)分析基本功的場景。1. 數(shù)據(jù)總覽與環(huán)境準備先把“體檢報告”打出來拿到數(shù)據(jù)別急著畫圖先把最基礎(chǔ)的結(jié)構(gòu)性信息看清楚。這一步我習慣用幾個熟悉的 pandas 方法快速完成基本三分鐘就能對數(shù)據(jù)形成初步認知。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False train pd.read_csv(train.csv) test pd.read_csv(test.csv) print(訓練集形狀:, train.shape) print(測試集形狀:, test.shape) print(訓練集列名:, train.columns.tolist())輸出結(jié)果里訓練集是 120000 行 47 列測試集是 120000 行 46 列比訓練集少一列標簽 isDefault。列名里既有 loanAmnt、interestRate 這種語義明確的字段也有 n0 到 n14 這種完全匿名的特征這類匿名特征在比賽數(shù)據(jù)里很常見大概率是脫敏后的第三方征信或行為數(shù)據(jù)處理時要格外小心。接著用 info() 看數(shù)據(jù)類型和內(nèi)存占用用 describe() 看數(shù)值列的統(tǒng)計量??吹降慕Y(jié)果里有幾個值得注意的點employmentTitle 和 employmentLength 缺失比較明顯title 字段缺失率接近 5%n 系列匿名特征整體完整但存在少量空值。數(shù)據(jù)類型上大部分是 int64 和 float64只有 term 這種分期數(shù)看起來是離散整數(shù)issueDate 是字符串類型后面要轉(zhuǎn)成日期格式。這里有一個我在實際項目中養(yǎng)成的習慣讀入數(shù)據(jù)后立刻檢查內(nèi)存。120 萬行數(shù)據(jù)其實不大但如果后面要做特征工程、內(nèi)存頻繁擴展初始就把數(shù)據(jù)類型壓縮好能讓整個分析流程快很多。def reduce_mem_usage(df): for col in df.columns: col_type df[col].dtype if col_type ! object: c_min df[col].min() c_max df[col].max() if str(col_type)[:3] int: if c_min np.iinfo(np.int8).min and c_max np.iinfo(np.int8).max: df[col] df[col].astype(np.int8) elif c_min np.iinfo(np.int16).min and c_max np.iinfo(np.int16).max: df[col] df[col].astype(np.int16) elif c_min np.iinfo(np.int32).min and c_max np.iinfo(np.int32).max: df[col] df[col].astype(np.int32) else: if c_min np.finfo(np.float16).min and c_max np.finfo(np.float16).max: df[col] df[col].astype(np.float16) elif c_min np.finfo(np.float32).min and c_max np.finfo(np.float32).max: df[col] df[col].astype(np.float32) return df train reduce_mem_usage(train)壓縮之后內(nèi)存從原來的 80MB 左右降到了 30MB 上下后面對數(shù)據(jù)進行 groupby、merge 都會舒服很多。這種操作在核心內(nèi)存只有 8G 的筆記本上尤其重要比賽后期特征數(shù)量變大時養(yǎng)成這個習慣能幫你避開不少內(nèi)存溢出的尷尬。1.1 快速識別缺失字段和異常字段缺失值分析是數(shù)據(jù)質(zhì)量探查的重頭戲。我一般會先算一列缺失比例再決定每個字段的后續(xù)處理方式。missing train.isnull().sum().sort_values(ascendingFalse) missing_pct (missing / len(train)).sort_values(ascendingFalse) missing_info pd.DataFrame({缺失數(shù)量: missing, 缺失比例: missing_pct}) print(missing_info[missing_info[缺失數(shù)量] 0])跑完的結(jié)果顯示缺失字段主要集中在 employmentTitle職位名稱缺失約 2.1%、employmentLength工作年限約 2.4%、postCode郵編約 0.4%、title借款標題約 4.8%還有 ficoRangeLow / ficoRangeHighFICO 評分區(qū)間上下界各約 5.2%。整體缺失率不算高但處理方式各不相同這個放到后面特征工程部分細說。異常值方面我重點檢查了 annualIncome年收入和 revolBal循環(huán)貸款余額這兩個連續(xù)變量。annualIncome 的最小值只有 4000最大值超過 600 萬明顯右偏且?guī)в虚L尾。revolUtil循環(huán)額度使用率出現(xiàn)了超過 100% 的值這在信貸場景里意味著用戶可能存在超額透支或數(shù)據(jù)報送口徑問題。這種異常值不能直接刪先標記出來后續(xù)可以考慮做截斷或單獨構(gòu)造“是否異常”的布爾特征。2. 單變量分布分析每個字段都在講什么故事清洗檢查做完下面進入真正的探索性分析。單變量分布看的是每個字段自身的形態(tài)這一步能幫你形成對數(shù)據(jù)的直覺。2.1 目標變量與核心數(shù)值特征的分布畫像先看標簽 isDefault 的分布。訓練集里違約樣本為 23914 條占比 19.93%非違約樣本占比 80.07%是一個接近 1:4 的不平衡二分類問題。對于金融風控場景這個違約率其實相當真實信用卡或消費貸產(chǎn)品的壞賬率通常在 2% 到 10% 之間部分下沉客群的壞賬率會更高。比賽數(shù)據(jù)直接給到 20% 的違約率說明業(yè)務(wù)方有意篩掉了一部分明顯好客戶讓模型需要真正學習風險特征。train[isDefault].value_counts(normalizeTrue).plot(kindbar) plt.title(目標變量分布) plt.xticks([0, 1], [非違約, 違約], rotation0) plt.show()畫出來的柱狀圖配合數(shù)值輸出能明顯感受到類別不平衡的壓迫感。這說明后續(xù)建模時要用 AUC 而不是準確率作為主要評估指標同時要考慮樣本不均衡的處理方法比如在 LightGBM 里設(shè)置 scale_pos_weight或者用負樣本下采樣的方式做訓練集構(gòu)建。數(shù)值特征分布我看的是幾個對違約預測最關(guān)鍵的特征loanAmnt借款金額、interestRate利率、annualIncome年收入、dti負債收入比、ficoRangeLowFICO 下界。loanAmnt 分布整體在 3000 到 35000 之間波動常見區(qū)間集中在 10000 到 20000峰值位置在 10000 和 20000 附近這符合消費金融產(chǎn)品的額度設(shè)計邏輯產(chǎn)品方預置了幾個固定額度檔位。interestRate 呈階梯狀分布在 6% 到 24% 區(qū)間內(nèi)出現(xiàn)多個尖峰。這是信貸產(chǎn)品的定價特征——不同風險等級對應(yīng)不同利率檔位本質(zhì)上利率已經(jīng)是風險等級的一個代理變量。annualIncome 右偏明顯大部分客戶年收入在 20 萬以下尾部延伸到數(shù)百萬。建模時如果不做處理極端值會對樹模型的分裂點產(chǎn)生干擾。dti 集中在 0 到 30 之間代表月負債占月收入的比例超過 40 的客戶明顯遞減超過 60 的幾乎沒有。信貸業(yè)務(wù)通常把 dti 大于 50 視為高風險這個邏輯在數(shù)據(jù)中也有體現(xiàn)。畫圖的時候建議用直方圖加核密度曲線的組合方式直方圖看柱狀形態(tài)核密度看平滑的分布趨勢兩個一起看對分布形態(tài)的判斷更準確。2.2 類別特征的取值結(jié)構(gòu)與編碼思路數(shù)據(jù)里的類別型字段不少term分期數(shù)、grade評級、subGrade子評級、employmentLength工作年限區(qū)間、homeOwnership房產(chǎn)所有權(quán)、verificationStatus收入驗證狀態(tài)、purpose借款用途、initialListStatus初始列表狀態(tài)、applicationType個人或聯(lián)合申請等。我逐個看了它們的取值數(shù)量。grade 從 A 到 G 共 7 個等級subGrade 從 A1 到 G5 共 35 個等級這兩個字段和利率高度相關(guān)屬于風控模型里必須重視的強特征。homeOwnership 取值為 0 到 50 代表租房1 代表按揭2 代表自有其他值代表其他情況。purpose 有 13 個類別以債務(wù)合并debt_consolidation為主占比超過一半。這里有一個重要的編碼建議對 subGrade 這種有序類別特征不要直接用 LabelEncoder 編碼成 1 到 35 的整數(shù)因為樹模型雖然能處理這種數(shù)值型編碼但會把類別間的順序關(guān)系當作線性關(guān)系處理容易產(chǎn)生誤導性分裂。更好的方式是保留原始字符串讓 LightGBM 的類別特征處理機制自動處理或者將其映射為等級對應(yīng)的風險評分。train[term].value_counts() train[grade].value_counts().sort_index() train[homeOwnership].value_counts()term 字段有 36 個月和 60 個月兩檔60 個月分期占比不到三成但違約率明顯更高。這一點后面做交叉分析時再看。employmentLength 雖然是字符串類型但本質(zhì)上是有序的從“ 1 年”到“10 年”共 11 檔可以直接映射為 0 到 10 的整數(shù)。3. 多變量與業(yè)務(wù)洞察誰在驅(qū)動違約風險單變量分析解決的是“每個字段長什么樣”的問題多變量分析解決的是“哪些字段和目標變量相關(guān)、如何相關(guān)”的問題。這是 EDA 的核心產(chǎn)出也是后續(xù)特征工程的主要依據(jù)。3.1 目標變量與關(guān)鍵特征的交叉分析對所有重要特征我做了“特征分箱后計算各箱違約率”的操作用違約率的走勢來判斷特征的方向性和單調(diào)性。以 interestRate 為例把利率從低到高等頻分成 10 箱計算每箱的違約率結(jié)果呈現(xiàn)出極強的正相關(guān)利率最低的 10% 人群違約率約 8%利率最高的 10% 人群違約率接近 42%。這個規(guī)律非常清晰說明利率本身就是風險定價的產(chǎn)物高利率對應(yīng)高風險模型必須充分利用這個信號。用同樣的方式看 annualIncome收入從低到高等頻分箱后違約率呈現(xiàn)明顯的負相關(guān)最低收入組違約率 32%最高收入組違約率只有 12%。收入低的人更需要借貸也更可能還不上錢這個邏輯在數(shù)據(jù)里完全站得住。還看了幾個征信相關(guān)的字段delinquency_2years兩年內(nèi)逾期次數(shù)大多數(shù)人為 0但逾期次數(shù)為 1 時違約率從 18% 跳到 29%逾期 2 次時跳到 36%邊際遞增效應(yīng)明顯。revolUtil循環(huán)額度使用率使用率低于 30% 的人群違約率約 15%高于 90% 的人群違約率突破 35%信用卡額度幾乎刷爆的人現(xiàn)金流壓力極大。這些交叉分析的代碼不復雜本質(zhì)就是 pandas 的 groupby 加 meantrain[int_rate_bin] pd.qcut(train[interestRate], 10, labelsFalse) bin_default_rate train.groupby(int_rate_bin)[isDefault].mean() print(bin_default_rate)3.2 相關(guān)性矩陣與共線性檢查數(shù)值特征的皮爾遜相關(guān)性矩陣也值得跑一遍但要注意皮爾遜相關(guān)系數(shù)只能捕捉線性關(guān)系對樹模型來說非線性關(guān)聯(lián)也很重要所以相關(guān)性矩陣只是輔助工具不能代替分組違約率的觀察。num_cols train.select_dtypes(include[np.number]).columns corr train[num_cols].corr() mask np.triu(np.ones(corr.shape), k1) sns.heatmap(corr, maskmask, cmapRdBu_r, annotFalse, vmin-1, vmax1) plt.show()從熱力圖里能看出幾個明顯聚集interestRate 與 grade、subGrade 高度相關(guān)相關(guān)系數(shù)接近 0.9這本身就是業(yè)務(wù)邏輯的體現(xiàn)——評級越差利率越高。installment月還款額與 loanAmnt 強相關(guān)相關(guān)系數(shù)超過 0.9因為月還款額就是根據(jù)借款金額、利率和期限計算出來的這是機械關(guān)系而非業(yè)務(wù)洞見做特征篩選時要注意區(qū)分。annualIncome 與 revolBal 呈弱負相關(guān)收入高的人循環(huán)貸款余額相對更低。共線性方面grade 和 subGrade 基本是冗余的subGrade 信息量更細保留 subGrade 就可以。ficoRangeLow 和 ficoRangeHigh 高度相關(guān)保留其中一個或者取均值即可。這些判斷直接服務(wù)于后續(xù)的特征篩選能幫你減少無意義的特征。3.3 時序維度下的數(shù)據(jù)分布觀察issueDate 是放款日期雖然比賽沒有明確說明訓練集和測試集按時間切分但觀察時間分布能幫你規(guī)避時序泄漏問題。train[issueDate] pd.to_datetime(train[issueDate]) train[issue_month] train[issueDate].dt.to_period(M) monthly_default train.groupby(issue_month)[isDefault].agg([mean, count])統(tǒng)計結(jié)果顯示訓練集的放款時間覆蓋了一年半左右每個月的樣本量在 4000 到 9000 之間波動違約率在時間維度上有小幅起伏但沒有明顯的單調(diào)趨勢。這個結(jié)論很重要——說明雖然存在時間效應(yīng)但短期內(nèi)沒有劇烈變化直接用分類模型處理不需要過度擔心趨勢漂移。不過謹慎起見我在后續(xù)交叉驗證設(shè)計時仍然采用了按時間順序劃分的時序 K 折方案而不是隨機 K 折。原因很簡單如果某個月的數(shù)據(jù)分布發(fā)生了變化隨機 K 折會讓模型在訓練時提前“看到”未來信息評估結(jié)果會偏樂觀。用時序切分能更真實地反映上線后模型的表現(xiàn)。4. 分析結(jié)論向特征工程的落地轉(zhuǎn)化EDA 做到這里腦子里已經(jīng)有了一張清晰的風險畫像。下面把分析結(jié)論整理成具體的特征工程動作這才是 EDA 的真正價值所在。4.1 缺失字段的處理策略針對每個有缺失的字段處理方式完全不一樣employmentLength 和 employmentTitle 缺失率只有 2% 左右量不大。employmentLength 缺失可以填充為“未知”或者單獨建一個“是否缺失”的標記employmentTitle 是高基數(shù)類別特征直接填充眾數(shù)意義不大建議以缺失直接填充為“未知”字符串。postCode 缺失率低但郵編本身是高基數(shù)類別建模時一般做頻次編碼或直接刪除。我選擇保留頻次編碼因為地區(qū)信貸風險差異是真實存在的。title 缺失率 4.8%這個字段是借款人在申請時自己填的標題和 purpose 含義接近但更嘈雜。缺失的可以直接填充為 purpose 字段的值或者干脆把 title 和 purpose 合并。ficoRangeLow 和 ficoRangeHigh 缺失率約 5%FICO 評分是強特征不能輕易丟掉。我用 dti、delinquency_2years、interestRate、subGrade 這幾個相關(guān)字段做了一個簡單的回歸模型來預測缺失值雖然沒有達到高精度但比直接填充均值的下游模型效果要好一些。匿名特征 n 系列的缺失數(shù)量很少統(tǒng)一填充 0 或者中位數(shù)即可。4.2 基于分布洞察構(gòu)造的新特征從 EDA 中能直接提煉出不少新特征想法收入與借款的匹配度annualIncome / loanAmnt比值衡量借款相對于收入的重負程度。這個特征從交叉分析看區(qū)分度很明顯收入低但借款高的人違約率顯著偏高。利率與評級的一致性偏差subGrade 已經(jīng)內(nèi)含利率水平但可以將實際利率減去該子評級下的平均利率得到一個“定價偏差”特征捕捉被高估或低估風險的用戶。FICO 評分區(qū)間的中值取 ficoRangeLow 和 ficoRangeHigh 的均值作為用戶的統(tǒng)一信用評分。歷史逾期記錄的綜合指標delinquency_2years、pubRec、pubRecBankruptcies 這三個字段可以加總形成“歷史負面記錄總數(shù)”。申請類型與負債水平的交互applicationType 如果是聯(lián)合申請要考慮兩個人的收入負債合并計算可以構(gòu)造 appType 與 annualIncome 的交叉特征。循環(huán)使用率極端標記revolUtil 超過 100% 時設(shè)為 1否則為 0捕捉超額透支的警告信號。4.3 基于 EDA 結(jié)果的建模選型建議從數(shù)據(jù)分布可以判斷大部分特征和目標變量之間是非線性關(guān)系而且存在大量類別特征和缺失值這種情況 LightGBM 或 XGBoost 這種基于直方圖的樹模型是首選。我計劃先跑一個 LightGBM 基線把默認參數(shù)跑通然后再用 EDA 階段發(fā)現(xiàn)的強特征組合做第二輪迭代。另外由于類別不平衡比例接近 1:4評估指標我堅持用 AUC 和 KS 統(tǒng)計量不參考準確率。在參數(shù)設(shè)置上LightGBM 的 is_unbalance 參數(shù)和 scale_pos_weight 都需要試驗但不要一開始就大力度調(diào)樣本權(quán)重先讓模型在原始分布下跑出基線再逐步加權(quán)重觀察驗證集 AUC 的變化。5. 踩坑記錄與排查心得數(shù)據(jù)分析階段看著簡單實際跑下來還是有不少坑整理成表格給后面做類似項目的人參考。問題現(xiàn)象根因分析解決方案讀入數(shù)據(jù)后內(nèi)存占用過大運行耗時默認 int64/float64 類型浪費內(nèi)存用 reduce_mem_usage 函數(shù)壓縮為 int32/int16/float32describe 里 annualIncome 最大值異常達到數(shù)百萬長尾分布存在高凈值客戶或異常錄入先保留后續(xù)特征工程做分位數(shù)截斷或取對數(shù)revolUtil 出現(xiàn)超過 100% 的數(shù)值報送口徑或超額透支導致不刪反手構(gòu)造“是否超額使用”布爾特征installment 與 loanAmnt 相關(guān)系數(shù)很高兩個字段存在機械計算關(guān)系不做強相關(guān)去重但在特征重要性解釋時注意干擾subGrade 直接 LabelEncoder 后樹模型特征重要性失真類別有序性被當作線性關(guān)系處理保留原始字符串讓 LightGBM 自動處理或映射為評分隨機 K 折驗證結(jié)果波動大數(shù)據(jù)存在時間效應(yīng)隨機劃分泄漏未來信息改用按時間順序的時序驗證切分title 缺失率 4.8%直接刪掉感覺可惜高基數(shù)類別缺失填充方式不好選結(jié)合 purpose 字段填充或合并到 purpose 中還有一個值得單獨說的心得相關(guān)性分析不要只看正負號還要看業(yè)務(wù)邏輯。比如 employmentTitle 和 annualIncome 天然相關(guān)但這不說明工作職位決定了收入而是職位類型本身和行業(yè)相關(guān)。發(fā)現(xiàn)強關(guān)聯(lián)特征時先回頭想想這個關(guān)聯(lián)在業(yè)務(wù)上是否成立不成立大概率是代理特征或數(shù)據(jù)噪聲需要謹慎處理。匿名特征 n0 到 n14 的分布基本都集中在幾個離散值上這類特征在樹模型里很容易被用來做細微切分但也容易造成過擬合。我的做法是先用 LightGBM 跑一遍全特征基線通過特征重要性判斷匿名特征是否有貢獻再決定是否全量保留。有些匿名特征明顯是和用戶行為相關(guān)的計數(shù)器這類特征在信貸場景里往往有強預測力不能因為“看不懂”就直接丟掉。6. 復盤與后續(xù)迭代思路這次數(shù)據(jù)分析給我最大的感受是EDA 不是一次性工作它是貫穿全程的。第一次分析做到這里我只能形成初步假設(shè)等到模型跑出結(jié)果哪些特征效果不好、哪些誤差集中在一個特定客群還需要回到數(shù)據(jù)分布里重新檢查。所以在項目初期我會有意識地把分析代碼組織成模塊化的函數(shù)讓后面可以隨時復用、隨時重跑。后續(xù)的建模迭代里我計劃優(yōu)先驗證三個從 EDA 中提煉的假設(shè)第一利率分箱后的違約率單調(diào)性是否能在 LightGBM 分裂中被充分利用如果不能考慮構(gòu)造利率排名特征第二收入和借款金額的交互特征能否顯著提升區(qū)分度第三歷史負面記錄總數(shù)能否替代三個單一征信特征。每一個假設(shè)都在 EDA 里有跡可循而不是憑空猜測這樣迭代起來才有方向。做金融風控數(shù)據(jù)分析有一個很深的體會數(shù)據(jù)探索的深度決定特征工程的上限。模型只是把你在數(shù)據(jù)里發(fā)現(xiàn)的規(guī)律編碼成數(shù)學表達式如果你自己都沒看清數(shù)據(jù)的全貌模型也學不到更多。這一篇的分析內(nèi)容就到這里下一步我會基于這些結(jié)論跑第一版 LightGBM 基線模型到時候再對比特征工程前后效果看看 EDA 階段花的功夫到底值多少錢。