十種數(shù)據(jù)預(yù)處理中的數(shù)據(jù)泄露模式解析:識別與避免策略
在機器學(xué)習(xí)教學(xué)實踐中,我們常會遇到這樣一個問題:"模型表現(xiàn)非常出色,準確率超過90%!但當將其提交到隱藏數(shù)據(jù)集進行測試時,效果卻大打折扣。問題出在哪里?"這種情況幾乎總是與數(shù)據(jù)泄露有關(guān)。
當測試數(shù)據(jù)在數(shù)據(jù)準備階段無意中泄露(滲透)到訓(xùn)練數(shù)據(jù)時,就會發(fā)生數(shù)據(jù)泄露。這種情況經(jīng)常出現(xiàn)在常規(guī)數(shù)據(jù)處理任務(wù)中,而你可能并未察覺。當泄露發(fā)生時,模型會從本不應(yīng)看到的測試數(shù)據(jù)中學(xué)習(xí),導(dǎo)致測試結(jié)果失真。
數(shù)據(jù)泄露的定義
數(shù)據(jù)泄露是機器學(xué)習(xí)中的一個常見問題,發(fā)生在不應(yīng)被模型看到的數(shù)據(jù)(如測試數(shù)據(jù)或未來數(shù)據(jù))意外地被用于訓(xùn)練模型時。這可能導(dǎo)致模型過擬合,并在新的、未見數(shù)據(jù)上表現(xiàn)不佳。
我們將聚焦以下數(shù)據(jù)預(yù)處理步驟中的數(shù)據(jù)泄露問題。并將結(jié)合scikit-learn中的具體預(yù)處理方法,并在文章末尾給出代碼示例。
缺失值填充
在處理真實數(shù)據(jù)時,經(jīng)常會遇到缺失值。與其刪除這些不完整的數(shù)據(jù)點,不如用合理的估計值填充它們。這有助于我們保留更多的數(shù)據(jù)用于分析。
填充缺失值的簡單方法包括:
- 使用SimpleImputer(strategy='mean')或SimpleImputer(strategy='median')將缺失值填充為該列的平均值或中位數(shù)
- 使用KNNImputer()查看相似的數(shù)據(jù)點并使用它們的值
- 使用SimpleImputer(strategy='ffill')或SimpleImputer(strategy='bfill')將缺失值填充為數(shù)據(jù)中前一個或后一個值
- 使用SimpleImputer(strategy='constant', fill_value=value)將所有缺失值替換為相同的數(shù)字或文本
這個過程被稱為填充,雖然很有用,但我們需要謹慎計算這些替換值,以避免數(shù)據(jù)泄露。
數(shù)據(jù)泄露案例:簡單填充(平均值)
當你使用所有數(shù)據(jù)的平均值填充缺失值時,平均值本身包含了訓(xùn)練集和測試集的信息。這個合并的平均值與你僅使用訓(xùn)練數(shù)據(jù)計算所得不同。由于這個不同的平均值會進入你的訓(xùn)練數(shù)據(jù),你的模型實際上從本不應(yīng)看到的測試數(shù)據(jù)信息中學(xué)習(xí)。
- 問題所在使用完整數(shù)據(jù)集計算平均值
- 錯誤做法使用訓(xùn)練集和測試集的統(tǒng)計數(shù)據(jù)計算填充值
- 后果訓(xùn)練數(shù)據(jù)包含受測試數(shù)據(jù)影響的平均值
當使用所有數(shù)據(jù)行計算的平均值(4)填充缺失值,而非正確地僅使用訓(xùn)練數(shù)據(jù)的平均值(3)時,就會發(fā)生平均值填充泄露,導(dǎo)致錯誤的填充值。
數(shù)據(jù)泄露案例:KNN填充
當在所有數(shù)據(jù)上使用KNN填充缺失值時,該算法會從訓(xùn)練集和測試集中找到相似的數(shù)據(jù)點。它創(chuàng)建的替換值基于這些鄰近點,這意味著測試集值直接影響了訓(xùn)練數(shù)據(jù)。由于KNN查看實際的鄰近值,相比簡單的平均值填充,這種訓(xùn)練和測試信息的混合更加直接。
- 問題所在在完整數(shù)據(jù)集中尋找鄰居
- 錯誤做法 使用測試集樣本作為填充的潛在鄰居
- 后果使用直接的測試集信息填充缺失值
當使用訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)找到最近鄰(得到值3.5和4.5)時,就會發(fā)生KNN填充泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)模式填充缺失值(得到值6和6)。
分類編碼
有些數(shù)據(jù)以類別而非數(shù)字的形式呈現(xiàn),如顏色、名稱或類型。由于模型只能處理數(shù)字,我們需要將這些類別轉(zhuǎn)換為數(shù)值。
常見的類別轉(zhuǎn)換方法包括:
- 使用OneHotEncoder()為每個類別創(chuàng)建單獨的由1和0組成的列(也稱為虛擬變量)
- 使用OrdinalEncoder()或LabelEncoder()為每個類別分配一個數(shù)字(如1、2、3)
- 使用OrdinalEncoder(categories=[ordered_list])自定義類別順序,以反映自然層次結(jié)構(gòu)(如small=1, medium=2, large=3)
- 使用TargetEncoder()根據(jù)類別與我們試圖預(yù)測的目標變量之間的關(guān)系將類別轉(zhuǎn)換為數(shù)字
轉(zhuǎn)換類別的方式會影響模型的學(xué)習(xí)效果,在此過程中需要注意不要使用來自測試數(shù)據(jù)的信息。
數(shù)據(jù)泄露案例:目標編碼
當使用所有數(shù)據(jù)的目標編碼轉(zhuǎn)換分類值時,編碼值是使用來自訓(xùn)練集和測試集的目標信息計算的。替換每個類別的數(shù)字是目標值的平均值,其中包括測試數(shù)據(jù)。這意味著訓(xùn)練數(shù)據(jù)被分配的值已經(jīng)包含了本不應(yīng)知道的測試集目標值信息。
- 問題所在使用完整數(shù)據(jù)集計算類別平均值
- 錯誤做法使用所有目標值計算類別替換
- 后果訓(xùn)練特征包含未來目標信息
當使用所有數(shù)據(jù)替換類別的平均目標值(A=3, B=4, C=2)時,就會發(fā)生目標編碼泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)的平均值(A=2, B=5, C=1),否則會導(dǎo)致錯誤的類別值。
數(shù)據(jù)泄露案例:One-Hot編碼
當使用所有數(shù)據(jù)將類別轉(zhuǎn)換為二進制列,然后選擇要保留的列時,選擇是基于在訓(xùn)練集和測試集中發(fā)現(xiàn)的模式。保留或刪除某些二進制列的決定受到它們在測試數(shù)據(jù)中預(yù)測目標效果的影響,而不僅僅是訓(xùn)練數(shù)據(jù)。這意味著選擇的列部分取決于本不應(yīng)使用的測試集關(guān)系。
- 問題所在從完整數(shù)據(jù)集確定類別
- 錯誤做法基于所有唯一值創(chuàng)建二進制列
- 后果 特征選擇受測試集模式影響
當使用完整數(shù)據(jù)集的所有唯一值(A,B,C,D)創(chuàng)建類別列時,就會發(fā)生One-Hot編碼泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)中存在的類別(A,B,C),否則會導(dǎo)致錯誤的編碼模式。
數(shù)據(jù)縮放
數(shù)據(jù)中不同特征的取值范圍差異通常很大,有些可能是幾千,有些則是微小的小數(shù)。調(diào)整這些范圍,使所有特征具有相似的尺度,以幫助模型更好地工作。
常見的尺度調(diào)整方法包括:
- 使用StandardScaler()使值以0為中心,大多數(shù)值落在-1和1之間(均值=0,方差=1)
- 使用MinMaxScaler()將所有值壓縮在0和1之間,或使用MinMaxScaler(feature_range=(min, max))自定義范圍
- 使用FunctionTransformer(np.log1p)或PowerTransformer(method='box-cox')處理非常大的數(shù)字,使分布更正態(tài)
- 使用RobustScaler()采用不受異常值影響的統(tǒng)計數(shù)據(jù)調(diào)整尺度(使用四分位數(shù)而非均值/方差)
雖然縮放有助于模型公平地比較不同特征,但我們需要僅使用訓(xùn)練數(shù)據(jù)計算這些調(diào)整,以避免泄露。
數(shù)據(jù)泄露案例:標準縮放
當使用所有數(shù)據(jù)對特征進行標準化時,計算中使用的平均值和分布值來自訓(xùn)練集和測試集。這些值與僅使用訓(xùn)練數(shù)據(jù)所得不同。這意味著訓(xùn)練數(shù)據(jù)中的每個標準化值都使用了測試集中值的分布信息進行了調(diào)整。
- 問題所在 使用完整數(shù)據(jù)集計算統(tǒng)計數(shù)據(jù)
- 錯誤做法使用所有值計算均值和標準差
- 后果使用測試集分布縮放訓(xùn)練特征
當使用完整數(shù)據(jù)集的平均值(μ=0)和分布(σ=3)對數(shù)據(jù)進行歸一化時,就會發(fā)生標準縮放泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)的統(tǒng)計數(shù)據(jù)(μ=2,σ=2),否則會導(dǎo)致錯誤的標準化值。
數(shù)據(jù)泄露案例:最小-最大縮放
當使用所有數(shù)據(jù)的最小值和最大值縮放特征時,這些邊界值可能來自測試集。訓(xùn)練數(shù)據(jù)中的縮放值是使用這些邊界計算的,這可能與僅使用訓(xùn)練數(shù)據(jù)所得結(jié)果不同。這意味著你訓(xùn)練數(shù)據(jù)中的每個縮放值都使用了測試集中值的完整范圍進行了調(diào)整。
- 問題所在使用完整數(shù)據(jù)集找到邊界
- 錯誤做法從所有數(shù)據(jù)點確定最小/最大值
- 后果使用測試集范圍歸一化訓(xùn)練特征
當使用完整數(shù)據(jù)集的最小值(-5)和最大值(5)縮放數(shù)據(jù)時,就會發(fā)生最小-最大縮放泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)的范圍(最小值=-1,最大值=5),否則會導(dǎo)致值的錯誤縮放。
離散化
有時將數(shù)字分組為類別比使用精確值更有利。這有助于機器學(xué)習(xí)模型更輕松地處理和分析數(shù)據(jù)。
創(chuàng)建這些組的常見方法包括:
- 使用KBinsDiscretizer(strategy='uniform')使每個組覆蓋相同大小范圍的值
- 使用KBinsDiscretizer(strategy='quantile')使每個組包含相同數(shù)量的數(shù)據(jù)點
- 使用KBinsDiscretizer(strategy='kmeans')通過聚類找到數(shù)據(jù)中的自然分組
- 使用QuantileTransformer(n_quantiles=n, output_distributinotallow='uniform')根據(jù)數(shù)據(jù)中的百分位數(shù)創(chuàng)建組
雖然對值進行分組可以幫助模型更好地找到模式,但我們決定組邊界的方式需要僅使用訓(xùn)練數(shù)據(jù),以避免泄露。
數(shù)據(jù)泄露案例:等頻分箱
當使用所有數(shù)據(jù)創(chuàng)建具有相等數(shù)量數(shù)據(jù)點的箱時,箱之間的切割點是使用訓(xùn)練集和測試集確定的。這些切割值與僅使用訓(xùn)練數(shù)據(jù)所得結(jié)果不同。這意味著當你將訓(xùn)練數(shù)據(jù)中的數(shù)據(jù)點分配到箱中時,使用的分割點受到了測試集值的影響。
- 問題所在使用完整數(shù)據(jù)集設(shè)置閾值
- 錯誤做法使用所有數(shù)據(jù)點確定箱邊界
- 后果使用測試集分布對訓(xùn)練數(shù)據(jù)分箱
當使用所有數(shù)據(jù)設(shè)置箱切割點(-0.5,2.5)時,就會發(fā)生等頻分箱泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)設(shè)置邊界(-0.5,2.0),否則會導(dǎo)致值的錯誤分組。
數(shù)據(jù)泄露案例:等寬分箱
當使用所有數(shù)據(jù)創(chuàng)建相等大小的箱時,用于確定箱寬度的范圍來自訓(xùn)練集和測試集。這個總范圍可能比僅使用訓(xùn)練數(shù)據(jù)所得范圍更寬或更窄。這意味著當你將訓(xùn)練數(shù)據(jù)中的數(shù)據(jù)點分配到箱中時,你使用的是基于測試集值的完整分布計算得到的箱邊界。
- 問題所在 使用完整數(shù)據(jù)集計算范圍
- 錯誤做法基于完整數(shù)據(jù)分布設(shè)置箱寬度
- 后果使用測試集邊界對訓(xùn)練數(shù)據(jù)分箱
圖片
當使用完整數(shù)據(jù)集的范圍(-3到6)將數(shù)據(jù)拆分為大小相等的組時,就會發(fā)生等寬分箱泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)的范圍(-3到3),否則會導(dǎo)致錯誤的分組。
重采樣
當數(shù)據(jù)中某些類別的樣本數(shù)量遠多于其他類別時,我們可以使用imblearn中的重采樣技術(shù)通過創(chuàng)建新樣本或移除現(xiàn)有樣本來平衡它們。這有助于模型公平地學(xué)習(xí)所有類別。
添加樣本的常見方法(過采樣):
- 使用RandomOverSampler()復(fù)制較小類別中的現(xiàn)有樣本
- 使用SMOTE()使用插值為較小類別創(chuàng)建新的合成樣本
- 使用ADASYN()在模型最難處理的區(qū)域創(chuàng)建更多樣本,重點關(guān)注決策邊界 移除樣本的常見方法(欠采樣):
- 使用RandomUnderSampler()從較大類別中隨機移除樣本
- 使用NearMiss(versinotallow=1)或NearMiss(versinotallow=2)根據(jù)它們與較小類別的距離從較大類別中移除樣本
- 使用TomekLinks()或EditedNearestNeighbours()根據(jù)它們與其他類別的相似性仔細選擇要移除的樣本
雖然平衡數(shù)據(jù)有助于模型學(xué)習(xí),但創(chuàng)建或移除樣本的過程應(yīng)僅使用訓(xùn)練數(shù)據(jù)的信息,以避免泄露。
數(shù)據(jù)泄露案例:過采樣(SMOTE)
當使用所有數(shù)據(jù)上的SMOTE創(chuàng)建合成數(shù)據(jù)點時,該算法會從訓(xùn)練集和測試集中選取附近的點來創(chuàng)建新樣本。這些新點是通過將測試集樣本的值與訓(xùn)練數(shù)據(jù)混合創(chuàng)建的。這意味著你的訓(xùn)練數(shù)據(jù)獲得了直接使用測試集值信息創(chuàng)建的新樣本。
- 問題所在使用完整數(shù)據(jù)集生成樣本
- 錯誤做法使用測試集鄰居創(chuàng)建合成點
- 后果 訓(xùn)練數(shù)據(jù)被測試集影響的樣本增強
當根據(jù)整個數(shù)據(jù)集的類別計數(shù)復(fù)制數(shù)據(jù)點(A×4, B×3, C×2)時,就會發(fā)生過采樣泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)(A×1, B×2, C×2)來決定每個類別要復(fù)制的次數(shù)。
數(shù)據(jù)泄露案例:欠采樣(TomekLinks)
當使用所有數(shù)據(jù)上的Tomek Links移除數(shù)據(jù)點時,該算法會從訓(xùn)練集和測試集中找到最接近但標簽不同的點對。從訓(xùn)練數(shù)據(jù)中移除點的決定基于它們與測試集點的接近程度。這意味著你的最終訓(xùn)練數(shù)據(jù)是由其與測試集值的關(guān)系塑造的。
- 問題所在使用完整數(shù)據(jù)集移除樣本
- 錯誤做法使用測試集關(guān)系識別點對
- 后果 基于測試集模式減少訓(xùn)練數(shù)據(jù)
當根據(jù)整個數(shù)據(jù)集的類別比例移除數(shù)據(jù)點(A×4, B×3, C×2)時,就會發(fā)生欠采樣泄露;而正確的做法是僅使用訓(xùn)練數(shù)據(jù)(A×1, B×2, C×2)來決定每個類別要保留的樣本數(shù)量。
最后總結(jié)
在預(yù)處理數(shù)據(jù)時,需要將訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)完全分開。任何時候使用來自所有數(shù)據(jù)的信息來轉(zhuǎn)換值-無論是填充缺失值,將類別轉(zhuǎn)換為數(shù)字,縮放特征,分箱還是平衡類-都有可能將測試數(shù)據(jù)信息混合到訓(xùn)練數(shù)據(jù)中。這使得模型的測試結(jié)果不可靠,因為模型已經(jīng)從它不應(yīng)該看到的模式中學(xué)習(xí)了。
解決方案很簡單:始終首先轉(zhuǎn)換訓(xùn)練數(shù)據(jù),保存這些計算,然后將其應(yīng)用于測試數(shù)據(jù)。
數(shù)據(jù)預(yù)處理+分類(帶泄漏)代碼
讓我們看看在預(yù)測一個簡單的高爾夫比賽數(shù)據(jù)集時,泄漏是如何發(fā)生的。
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OrdinalEncoder, KBinsDiscretizer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
X, y = df.drop('Play', axis=1), df['Play']
# Preprocess AND apply SMOTE to ALL data first (causing leakage)
preprocessor = ColumnTransformer(transformers=[
('temp_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Temperature']),
('humid_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Humidity']),
('outlook_transform', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1),
['Outlook']),
('wind_transform', Pipeline([
('imputer', SimpleImputer(strategy='constant', fill_value=False)),
('scaler', StandardScaler())
]), ['Wind'])
])
# Transform all data and apply SMOTE before splitting (leakage!)
X_transformed = preprocessor.fit_transform(X)
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_transformed, y)
# Split the already transformed and resampled data
X_train, X_test, y_train, y_test = train_test_split(X_resampled, y_resampled, test_size=0.5, shuffle=False)
# Train a classifier
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)
print(f"Testing Accuracy (with leakage): {accuracy_score(y_test, clf.predict(X_test)):.2%}")
上面的代碼使用了ColumnTransformer,這是scikit-learn中的一個很好用的功能,允許我們對數(shù)據(jù)集中的不同列應(yīng)用不同的預(yù)處理步驟。
代碼演示了數(shù)據(jù)泄漏,因為所有轉(zhuǎn)換在擬合期間都會看到整個數(shù)據(jù)集,這在真實的機器學(xué)習(xí)場景中是不合適的,因為我們需要將測試數(shù)據(jù)與訓(xùn)練過程完全分開。
這種方法也可能顯示出人為的更高的測試精度,因為測試數(shù)據(jù)特征是在預(yù)處理步驟中使用的!
數(shù)據(jù)預(yù)處理+分類(無泄漏)代碼
以下是沒有數(shù)據(jù)泄露的版本:
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OrdinalEncoder, KBinsDiscretizer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
X, y = df.drop('Play', axis=1), df['Play']
# Split first (before any processing)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, shuffle=False)
# Create pipeline with preprocessing, SMOTE, and classifier
pipeline = Pipeline([
('preprocessor', ColumnTransformer(transformers=[
('temp_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Temperature']),
('humid_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Humidity']),
('outlook_transform', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1),
['Outlook']),
('wind_transform', Pipeline([
('imputer', SimpleImputer(strategy='constant', fill_value=False)),
('scaler', StandardScaler())
]), ['Wind'])
])),
('smote', SMOTE(random_state=42)),
('classifier', DecisionTreeClassifier(random_state=42))
])
# Fit pipeline on training data only
pipeline.fit(X_train, y_train)
print(f"Training Accuracy: {accuracy_score(y_train, pipeline.predict(X_train)):.2%}")
print(f"Testing Accuracy: {accuracy_score(y_test, pipeline.predict(X_test)):.2%}")
與泄漏版本的關(guān)鍵區(qū)別在于:
在進行任何處理之前,先拆分數(shù)據(jù)所有轉(zhuǎn)換(預(yù)處理、SMOTE),預(yù)處理僅從訓(xùn)練數(shù)據(jù)中學(xué)習(xí)的參數(shù),SMOTE僅適用于訓(xùn)練數(shù)據(jù)。在預(yù)測之前,測試數(shù)據(jù)完全不可見
這種方法提供了更現(xiàn)實的性能估計,因為它在訓(xùn)練和測試數(shù)據(jù)之間保持了適當?shù)姆蛛x。