Dane odgrywają kluczową rolę w podejmowaniu decyzji i ważne jest, abyśmy rozumieli znaczenie czystości naszych danych treningowych i testowych. Kontaminacja danych treningowych i testowych to subtelny, ale potencjalnie destrukcyjny problem, który może prowadzić do błędnych wniosków i wyników modeli.
Czym jest kontaminacja danych treningowych i testowych?
Przypomnijmy, że walidacja ma na celu pomiar, jak model radzi sobie na danych, które dotąd nie były brane pod uwagę. Kontaminacja danych treningowych i testowych występuje, gdy dane, które są przeznaczone do testowania i oceny modelu, wpływają na proces przygotowywania danych i uczenia modelu. To oznacza, że dane, które powinny być odseparowane od procesu uczenia modelu, w jakikolwiek sposób wpływają na ten proces.
Dlaczego to jest problem?
Kontaminacja danych treningowych i testowych jest problemem, ponieważ może prowadzić do nadmiernego optymizmu co do jakości modelu. Model, który działa dobrze na danych, które zostały wykorzystane do uczenia i testowania, może działać bardzo źle na zupełnie nowych danych, które nie były używane w procesie modelowania. To może mieć poważne konsekwencje w przypadku podejmowania rzeczywistych decyzji opartych na tych modelach.
Przykład 1:
Rozważmy przykład, w którym mamy zbiór danych dotyczący klasyfikacji chorób serca. Załóżmy, że w naszych danych mamy kolumnę “ciśnienie krwi”. Jeśli podzielimy dane na zbiór treningowy i testowy po zastosowaniu jakiegokolwiek preprocessingu i normalizacji, to znaczy, że statystyki dotyczące “ciśnienia krwi” w naszych danych testowych wpłyną na sposób, w jaki przeprowadzimy normalizację w zbiorze treningowym. To może doprowadzić do tego, że model będzie działać dobrze na danych testowych, ale źle na nowych danych pacjentów.
import pandas as pd from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split # Tworzenie przykładowego DataFrame z wynikami ciśnienia krwi data = { 'ciśnienie_krwi': [120, 130, 140, 150, 160, 170, 180, 190], 'czy_chory': [0, 1, 1, 1, 1, 0, 0, 1] # (0 - zdrowy, 1 - chory) } df = pd.DataFrame(data) # Podział danych na zbiór treningowy i testowy X = df[['ciśnienie_krwi']] y = df['czy_chory'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) # Skalowanie Min-Max na całym zbiorze danych scaler_all_data = MinMaxScaler() X_scaled_all_data = scaler_all_data.fit_transform(X) # Skalowanie Min-Max osobno dla zbioru treningowego i testowego scaler_train = MinMaxScaler() X_train_scaled = scaler_train.fit_transform(X_train) scaler_test = MinMaxScaler() X_test_scaled = scaler_test.fit_transform(X_test) # Wyświetlanie wyników print("Skalowanie Min-Max na całym zbiorze danych:") print(X_scaled_all_data) print("\nSkalowanie Min-Max osobno dla zbioru treningowego i testowego:") print("Zbiór treningowy:") print(X_train_scaled) print("Zbiór testowy:") print(X_test_scaled)
Skalowanie Min-Max na całym zbiorze danych: [[0. ] [0.14285714] [0.28571429] [0.42857143] [0.57142857] [0.71428571] [0.85714286] [1. ]] Skalowanie Min-Max osobno dla zbioru treningowego i testowego: Zbiór treningowy: [[0. ] [1. ] [0.28571429] [0.57142857] [0.42857143] [0.85714286]] Zbiór testowy: [[0.] [1.]]
W tym przykładzie dane testowe wpływają na sposób normalizacji danych treningowych, co może prowadzić do nadmiernego optymizmu co do wyników modelu.
Przykład 2:
Załóżmy, że mamy zbiór danych zawierający informacje o wynikach egzaminów matematycznych i fizycznych uczniów. Jednym z atrybutów w zbiorze danych jest “wynik_egzaminu_fizyka”, ale w niektórych przypadkach brakuje danych. Chcemy przewidzieć, czy uczeń zdał egzamin matematyczny na podstawie wyniku egzaminu fizycznego.
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score # Tworzenie przykładowego DataFrame data = { 'wynik_egzaminu_matematyka': [80, 90, 70, 60, 85, 75, 78, 92], 'wynik_egzaminu_fizyka': [75, 85, None, 55, None, 70, 80, None], 'zdał_egzamin_matematyka': [1, 1, 0, 0, 1, 0, 1, 1] } df = pd.DataFrame(data) # Uzupełnienie brakujących wartości średnią na całym zbiorze imputer = SimpleImputer(strategy='mean') df_imputed = imputer.fit_transform(df[['wynik_egzaminu_fizyka']]) # Podział danych na zbiór treningowy i testowy X = df_imputed y = df['zdał_egzamin_matematyka'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) # Uczenie modelu model = LogisticRegression() model.fit(X_train, y_train) # Ocena modelu y_pred = model.predict(X_test) accuracy = accuracy_score(y_test, y_pred) print(f'Dokładność modelu po imputacji na całym zbiorze: {accuracy}') # Podział danych na zbiór treningowy i testowy X = df[['wynik_egzaminu_fizyka']] y = df['zdał_egzamin_matematyka'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) # Uzupełnienie brakujących wartości średnią imputer = SimpleImputer(strategy='mean') X_train_imputed = imputer.fit_transform(X_train) X_test_imputed = imputer.transform(X_test) # Uczenie modelu model = LogisticRegression() model.fit(X_train_imputed, y_train) # Ocena modelu y_pred = model.predict(X_test_imputed) accuracy = accuracy_score(y_test, y_pred) print(f'Dokładność modelu po imputacji osobno dla zbioru treningowego i testowego: {accuracy}')
Dokładność modelu po imputacji na całym zbiorze: 1.0 Dokładność modelu po imputacji osobno dla zbioru treningowego i testowego: 0.5
W tym przykładzie brakujące wartości w kolumnie “wynik_egzaminu_fizyka” są uzupełniane średnią wartością tej kolumny. Jednakże, uzupełnianie brakujących danych przed podziałem na zbiory treningowy i testowy może prowadzić do kontaminacji danych. Jeśli np. średnia jest obliczana na całym zbiorze danych, to w momencie trenowania modelu dane testowe mają dostęp do informacji ze zbioru treningowego, co może prowadzić do nadmiernego optymizmu w ocenie modelu. Aby uniknąć tego rodzaju kontaminacji, uzupełnianie brakujących danych powinno być wykonywane osobno na zbiorze treningowym i testowym przed procesem uczenia modelu.
Jak unikać kontaminacji danych treningowych i testowych?
- Podział danych przed preprocessingiem: Dane powinny być podzielone na zbiór treningowy i (walidacyjny) testowy przed przeprowadzeniem jakiejkolwiek obróbki danych, włączając w to normalizację, imputację brakujących wartości itp.
- Używanie potoków (pipelines): Potoki w bibliotece scikit-learn pozwalają na określenie sekwencji kroków przetwarzania danych, które będą przeprowadzane niezależnie na zbiorze treningowym i testowym.