Completarea valorilor lipsă
În seturile de date reale, este foarte comun să lipsească anumite valori. Poate că un utilizator nu a completat un câmp într-un formular, sau un senzor nu a înregistrat o măsurătoare. Aceste valori lipsă (în engleză, missing values) pot cauza probleme serioase pentru modelele de machine learning, care de obicei nu știu cum să le gestioneze.
Vestea bună: există mai multe strategii pentru a trata valorile lipsă, de la cele simple (înlocuire cu media) până la cele avansate (estimare pe baza celorlalte date).
Identificarea valorilor lipsă
Primul pas este să înțelegi cât de multe valori lipsesc și în ce coloane. Pandas folosește None și NaN (Not a Number) pentru a reprezenta valorile lipsă.
import pandas as pd
import numpy as np
# Încarcă datele
df = pd.read_csv("date.csv")
# Câte valori lipsesc în fiecare coloană?
print(df.isnull().sum())
# Ce procent din date lipsește?
print(df.isnull().sum() / len(df) * 100)
# Total valori lipsă în întreg setul
print(df.isnull().sum().sum())
Pentru seturi mari de date, o vizualizare ajută să înțelegi pattern-ul valorilor lipsă:
import matplotlib.pyplot as plt
import seaborn as sns
# Heatmap - fiecare linie verticală reprezintă o valoare lipsă
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=True, yticklabels=False)
plt.title("Valori lipsă în dataset")
plt.show()
# Grafic cu procentul valorilor lipsă per coloană
missing_pct = df.isnull().sum() / len(df) * 100
missing_pct = missing_pct[missing_pct > 0].sort_values(ascending=False)
plt.figure(figsize=(10, 6))
missing_pct.plot(kind='bar')
plt.ylabel('Procent lipsă (%)')
plt.title('Valori lipsă per coloană')
plt.show()
Strategia 1: Ștergerea datelor
Cea mai simplă abordare: dacă o linie sau coloană are valori lipsă, o eliminăm complet.
Ștergerea rândurilor
# Șterge toate rândurile care au cel puțin o valoare lipsă
df_clean = df.dropna()
# Șterge rândurile doar dacă TOATE valorile lipsesc
df_clean = df.dropna(how='all')
# Șterge rândurile doar dacă lipsesc valori în anumite coloane
df_clean = df.dropna(subset=['coloana1', 'coloana2'])
# Păstrează rândurile care au cel puțin 5 valori prezente
df_clean = df.dropna(thresh=5)
Ștergerea coloanelor
# Șterge coloanele care au valori lipsă
df_clean = df.dropna(axis=1)
# Șterge doar coloanele cu mai mult de 50% valori lipsă
threshold = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=threshold)
Când e o idee bună: Când valorile lipsă sunt puține (sub 5%) și par a fi aleatorii.
Problema: Pierzi informație. Dacă ștergi prea multe rânduri, s-ar putea să rămâi cu prea puține date pentru a antrena un model bun.
Strategia 2: Înlocuirea cu valori simple (Imputare)
În loc să ștergi rândurile cu valori lipsă, le poți înlocui (imputa) cu o valoare estimată.
Media - pentru date numerice
Dacă într-o coloană numerică lipsesc valori, le poți înlocui cu media celorlalte valori:
# Varianta manuală
medie = df['coloana'].mean()
df['coloana'].fillna(medie, inplace=True)
# Cu SimpleImputer din sklearn
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='mean')
df['coloana'] = imputer.fit_transform(df[['coloana']])
Mediana - când ai outlieri
Media poate fi influențată de valori extreme. De exemplu, dacă într-un grup de oameni cu salarii de 3000-5000 lei ai unul cu salariu de 100.000 lei, media va fi mult mai mare decât salariul tipic. Mediana (valoarea din mijloc când sortezi datele) este mai robustă în aceste cazuri:
mediana = df['coloana'].median()
df['coloana'].fillna(mediana, inplace=True)
# Cu SimpleImputer
imputer = SimpleImputer(strategy='median')
df['coloana'] = imputer.fit_transform(df[['coloana']])
Modul - pentru date categorice
Pentru date categorice (gen, oraș, categorie de produs), nu poți calcula media. În schimb, poți folosi modul - valoarea care apare cel mai frecvent:
mod = df['coloana'].mode()[0]
df['coloana'].fillna(mod, inplace=True)
# Cu SimpleImputer
imputer = SimpleImputer(strategy='most_frequent')
df['coloana'] = imputer.fit_transform(df[['coloana']])
Valoare constantă
Uneori vrei să înlocuiești cu o valoare specifică (0 pentru valori numerice, "Necunoscut" pentru text):
df['coloana'].fillna(0, inplace=True)
df['coloana'].fillna('Necunoscut', inplace=True)
# Cu SimpleImputer
imputer = SimpleImputer(strategy='constant', fill_value=0)
Aplicare pe mai multe coloane simultan
from sklearn.impute import SimpleImputer
import numpy as np
# Coloanele numerice - înlocuiesc cu media
numeric_cols = df.select_dtypes(include=[np.number]).columns
imputer_num = SimpleImputer(strategy='mean')
df[numeric_cols] = imputer_num.fit_transform(df[numeric_cols])
# Coloanele categorice - înlocuiesc cu modul
cat_cols = df.select_dtypes(include=['object']).columns
imputer_cat = SimpleImputer(strategy='most_frequent')
df[cat_cols] = imputer_cat.fit_transform(df[cat_cols])
Strategia 3: Înlocuirea bazată pe context
Uneori, valorile din jurul celei lipsă îți pot da indicii despre ce ar trebui să fie.
Forward Fill / Backward Fill
Pentru date care au o ordine naturală (serii temporale, date cronologice), poți completa cu valoarea anterioară sau următoare:
# Completează cu valoarea anterioară (forward fill)
df['coloana'].fillna(method='ffill', inplace=True)
# Completează cu valoarea următoare (backward fill)
df['coloana'].fillna(method='bfill', inplace=True)
# Combinație: întâi înainte, apoi înapoi (pentru cazurile de la margini)
df['coloana'].fillna(method='ffill', inplace=True)
df['coloana'].fillna(method='bfill', inplace=True)
Interpolarea
Pentru date numerice ordonate, interpolarea estimează valorile lipsă pe baza celor din jur:
# Interpolare liniară - trasează o linie între punctele cunoscute
df['coloana'] = df['coloana'].interpolate(method='linear')
# Interpolare polinomială - trasează o curbă
df['coloana'] = df['coloana'].interpolate(method='polynomial', order=2)
Imputare pe grupuri
Uneori, media generală nu e relevantă - media salariului per departament e mai utilă decât media pe toată compania:
# Înlocuiește salariul lipsă cu media salariului din același departament
df['salariu'] = df.groupby('departament')['salariu'].transform(
lambda x: x.fillna(x.mean())
)
Strategia 4: Imputare avansată
KNN Imputer
Ideea: valorile lipsă sunt estimate pe baza celor mai similare K rânduri din set. Dacă un client seamănă cu alți 5 clienți în toate celelalte atribute, probabil că și valoarea lipsă e similară cu a lor.
from sklearn.impute import KNNImputer
# Creează imputer-ul care folosește 5 vecini
imputer = KNNImputer(n_neighbors=5)
# Aplică doar pe coloanele numerice
df_numeric = df.select_dtypes(include=[np.number])
df_imputed = pd.DataFrame(
imputer.fit_transform(df_numeric),
columns=df_numeric.columns
)
Iterative Imputer (MICE)
O metodă și mai sofisticată: fiecare coloană cu valori lipsă este modelată ca o funcție de celelalte coloane. Procesul se repetă de mai multe ori (iterativ) până când valorile se stabilizează.
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer = IterativeImputer(max_iter=10, random_state=42)
df_imputed = pd.DataFrame(
imputer.fit_transform(df_numeric),
columns=df_numeric.columns
)
Strategia 5: Indicator pentru valori lipsă
Uneori, faptul că o valoare lipsește este în sine informativ. De exemplu, dacă oamenii cu venituri mari tind să nu răspundă la întrebări despre venit, absența răspunsului e un semnal.
Poți crea o coloană nouă care indică dacă valoarea era lipsă:
# Creează indicator (1 = lipsea, 0 = era prezentă)
df['coloana_lipsa'] = df['coloana'].isnull().astype(int)
# Apoi completează valoarea cu mediana
df['coloana'].fillna(df['coloana'].median(), inplace=True)
Acum modelul poate învăța atât din valoarea imputată, cât și din informația că aceasta lipsea inițial.
Ce metodă aleg?
| Metodă | Când o folosesc | Avantaje | Dezavantaje |
|---|---|---|---|
| Ștergere | Puține valori lipsă, aleatorii | Simplă | Pierzi date |
| Media/Mediana | Date numerice | Rapidă | Reduce variația |
| Modul | Date categorice | Simplă | Poate introduce bias |
| KNN | Relații între features | Consideră contextul | Lentă pe date mari |
| Iterative | Date complexe | Foarte precisă | Complexă, lentă |
Tipuri de date lipsă
Nu toate valorile lipsă sunt la fel. Înțelegerea cauzei te ajută să alegi strategia potrivită:
MCAR (Missing Completely At Random) - Lipsa e complet aleatorie, fără nicio legătură cu alte variabile. Exemplu: un senzor s-a defectat aleatoriu.
MAR (Missing At Random) - Lipsa depinde de alte variabile observate. Exemplu: tinerii completează mai rar câmpul de venit, dar lipsa nu depinde de venitul în sine.
MNAR (Missing Not At Random) - Lipsa depinde de valoarea însăși. Exemplu: oamenii cu venituri foarte mari sau foarte mici evită să răspundă la întrebări despre venit.
Pentru MCAR și MAR, majoritatea metodelor de imputare funcționează bine. Pentru MNAR, lucrurile sunt mai complicate și ar trebui să folosești metode speciale sau să interpretezi rezultatele cu grijă.
Exemplu complet
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer, KNNImputer
# Încarcă datele
df = pd.read_csv("date.csv")
print("Valori lipsă înainte:")
print(df.isnull().sum())
# Separă coloanele numerice de cele categorice
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
cat_cols = df.select_dtypes(include=['object']).columns.tolist()
# Elimină coloanele cu mai mult de 50% valori lipsă
for col in df.columns:
if df[col].isnull().sum() / len(df) > 0.5:
df.drop(col, axis=1, inplace=True)
if col in numeric_cols:
numeric_cols.remove(col)
if col in cat_cols:
cat_cols.remove(col)
# Imputare coloane numerice cu KNN
if numeric_cols:
knn_imputer = KNNImputer(n_neighbors=5)
df[numeric_cols] = knn_imputer.fit_transform(df[numeric_cols])
# Imputare coloane categorice cu modul
if cat_cols:
mode_imputer = SimpleImputer(strategy='most_frequent')
df[cat_cols] = mode_imputer.fit_transform(df[cat_cols])
print("\nValori lipsă după:")
print(df.isnull().sum())