Scikit-learn Pipeline és ColumnTransformer: Teljes útmutató Pythonban

Ismerd meg a scikit-learn Pipeline és ColumnTransformer használatát Pythonban. Gyakorlati útmutató kódpéldákkal az adatelőfeldolgozástól a hiperparaméter-hangolásig, cross-validation és production deploy tippekkel.

Miért van szükséged scikit-learn Pipeline-okra?

Ha dolgoztál már gépi tanulási projekteken Pythonban, biztosan ismerős a helyzet: külön skálázod a numerikus oszlopokat, külön kódolod a kategorikusakat, aztán valahogy összerakod az egészet – és reméled, hogy a teszt adatokra is pontosan ugyanazokat a lépéseket alkalmaztad. Na, általában nem. És ilyenkor történik az, amit adatszivárgásnak (data leakage) hívunk. Ez az egyik leggyakoribb, mégis legkevésbé észrevett hiba a gépi tanulásban, és őszintén szólva, én is belefutottam párszor a pályafutásom elején.

A scikit-learn Pipeline és ColumnTransformer osztályai pont erre a problémára adnak megoldást. Egyetlen objektumba csomagolják az adatelőfeldolgozás és modellépítés teljes folyamatát – ami nemcsak tisztább kódot eredményez, hanem megbízhatóbb modelleket is.

Ebben az útmutatóban a scikit-learn 1.8 verziót használjuk, amely 2025 végén jelent meg és számos teljesítménybeli javítást hozott.

Mi az a Pipeline és hogyan működik?

A Pipeline a scikit-learn egyik legfontosabb eszköze. Lényegében adattranszformátorok sorozata, amelynek végén opcionálisan egy prediktor (modell) áll. Az egész a split-apply-combine elvén működik, csak lineáris feldolgozási lánc formájában.

Gondolj rá úgy, mint egy futószalagra: az adatod bemegy az egyik végén, végighalad az összes feldolgozási lépésen, és a másik végén kijön a predikció. A lényeg az, hogy minden lépés automatikusan alkalmazódik – tréning és predikció során egyaránt. Nem kell kézzel figyelned rá, hogy melyik transzformátor mit csinált. Komolyan, ez rengeteg fejfájástól kímél meg.

A Pipeline három alapszabálya

  • A közbenső lépéseknek transzformátoroknak kell lenniük (vagyis implementálják a fit() és transform() metódusokat)
  • Az utolsó lépés lehet bármi – transzformátor, osztályozó vagy regresszor
  • A fit() híváskor az összes lépés egymás után hajtódik végre; az utolsó lépés csak fit()-et kap

Első Pipeline: egyszerű példa

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# Adatok betöltése
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Pipeline létrehozása
pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", LogisticRegression(max_iter=200))
])

# Tréning és kiértékelés
pipe.fit(X_train, y_train)
pontossag = pipe.score(X_test, y_test)
print(f"Pontosság: {pontossag:.4f}")
# Kimenet: Pontosság: 1.0000

Ez a pár sor azt csinálja, amit korábban 10-15 sorban írtál meg. De ami ennél is fontosabb: a StandardScaler a tréning adatokból tanulja meg a paramétereit (átlag, szórás), és pontosan ugyanazokat alkalmazza a teszt adatokra. Nincs adatszivárgás.

ColumnTransformer: különböző előfeldolgozás különböző oszlopokra

A valós adathalmazok szinte mindig vegyes típusú oszlopokat tartalmaznak – vannak numerikus és kategorikus mezők, néha szöveges adatok is. A ColumnTransformer lehetővé teszi, hogy oszloponként más-más transzformációt alkalmazz, majd az eredményeket egyetlen jellemzőmátrixba fűzze össze.

Na, itt lesz igazán érdekes a dolog.

Gyakorlati példa: Titanic-szerű adathalmaz

import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# Minta adathalmaz létrehozása
np.random.seed(42)
n = 500
df = pd.DataFrame({
    "kor": np.random.randint(18, 75, n).astype(float),
    "jovedelem": np.random.normal(450000, 150000, n),
    "varos": np.random.choice(["Budapest", "Debrecen", "Szeged", "Pécs", "Győr"], n),
    "vegzettseg": np.random.choice(["alapfokú", "középfokú", "felsőfokú"], n),
    "hitel_elfogadva": np.random.choice([0, 1], n, p=[0.4, 0.6])
})

# Hiányzó értékek szimulálása
df.loc[np.random.choice(n, 30, replace=False), "kor"] = np.nan
df.loc[np.random.choice(n, 20, replace=False), "jovedelem"] = np.nan

print(df.head())
print(f"\nHiányzó értékek:\n{df.isnull().sum()}")

Az előfeldolgozó pipeline felépítése

# Oszlopok definiálása típus szerint
numerikus_oszlopok = ["kor", "jovedelem"]
kategorikus_oszlopok = ["varos", "vegzettseg"]

# Numerikus előfeldolgozó: hiánypótlás + skálázás
numerikus_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

# Kategorikus előfeldolgozó: hiánypótlás + one-hot kódolás
kategorikus_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])

# ColumnTransformer: összefogja a különböző transzformációkat
elofeldolgozo = ColumnTransformer(transformers=[
    ("num", numerikus_transformer, numerikus_oszlopok),
    ("cat", kategorikus_transformer, kategorikus_oszlopok)
])

A handle_unknown="ignore" paraméter itt kritikus szerepet játszik. Ha a teszt adatban olyan kategória jelenik meg, ami nem volt a tréning halmazban, a kódoló egyszerűen nullákat ad vissza – ahelyett, hogy hibát dobna. Éles környezetben ez szó szerint életmentő tud lenni.

A teljes pipeline összerakása

# Teljes pipeline: előfeldolgozás + modell
teljes_pipeline = Pipeline(steps=[
    ("elofeldolgozo", elofeldolgozo),
    ("classifier", RandomForestClassifier(n_estimators=100, random_state=42))
])

# Adatok szétválasztása
X = df.drop("hitel_elfogadva", axis=1)
y = df["hitel_elfogadva"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Tréning és kiértékelés
teljes_pipeline.fit(X_train, y_train)
pontossag = teljes_pipeline.score(X_test, y_test)
print(f"Pontosság: {pontossag:.4f}")

Automatikus oszlopválasztás típus alapján

Van egy jó hírem: a scikit-learn make_column_selector függvénye megkíméli attól, hogy kézzel sorold fel az oszlopneveket. Az adattípusok alapján automatikusan kiválasztja a megfelelő oszlopokat.

from sklearn.compose import make_column_selector

elofeldolgozo_auto = ColumnTransformer(transformers=[
    ("num", numerikus_transformer, make_column_selector(dtype_include="number")),
    ("cat", kategorikus_transformer, make_column_selector(dtype_include="object"))
])

# Ez ugyanúgy működik, de rugalmasabb
auto_pipeline = Pipeline(steps=[
    ("elofeldolgozo", elofeldolgozo_auto),
    ("classifier", RandomForestClassifier(n_estimators=100, random_state=42))
])

auto_pipeline.fit(X_train, y_train)
print(f"Auto pipeline pontosság: {auto_pipeline.score(X_test, y_test):.4f}")

Ez különösen akkor hasznos, ha az adathalmazod sok oszlopot tartalmaz, vagy ha a fejlesztés során gyakran változik az oszlopok száma. Nekem a legtöbb projektben ez az alapértelmezett megközelítés.

Keresztvalidáció Pipeline-nal

A Pipeline igazi ereje a keresztvalidációval kombinálva mutatkozik meg. Mivel a teljes előfeldolgozás a pipeline része, a cross_val_score minden fold-ban újra illeszti a transzformátorokat – kizárólag a tréning adatokra. Így garantált, hogy nincs adatszivárgás.

from sklearn.model_selection import cross_val_score

# 5-szörös keresztvalidáció
cv_scores = cross_val_score(teljes_pipeline, X, y, cv=5, scoring="accuracy")
print(f"CV pontosságok: {cv_scores}")
print(f"Átlagos pontosság: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")

Ha a pipeline-t nem használnád, és előtte kézzel transzformálnád az adatokat, a skálázó az összes adatból számolná ki a statisztikákat – beleértve azokat is, amiket épp tesztelésre használnál. Ez finoman, de biztosan torzítaná az eredményeidet.

Hiperparaméter-hangolás GridSearchCV-vel

Na, most jön a kedvenc részem. A Pipeline és a GridSearchCV kombinációja az, ahol igazán összeáll a kép. A pipeline lépéseinek paramétereit a lépésnév__paraméternév szintaxissal éred el:

from sklearn.model_selection import GridSearchCV

# Paraméterháló definiálása
param_grid = {
    "elofeldolgozo__num__imputer__strategy": ["mean", "median"],
    "classifier__n_estimators": [50, 100, 200],
    "classifier__max_depth": [5, 10, None],
    "classifier__min_samples_split": [2, 5]
}

# Grid search
grid_search = GridSearchCV(
    teljes_pipeline,
    param_grid,
    cv=5,
    scoring="accuracy",
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train, y_train)

print(f"Legjobb paraméterek: {grid_search.best_params_}")
print(f"Legjobb CV pontosság: {grid_search.best_score_:.4f}")
print(f"Teszt pontosság: {grid_search.score(X_test, y_test):.4f}")

Figyeld meg a dupla aláhúzás (__) szintaxist: az elofeldolgozo__num__imputer__strategy azt jelenti, hogy az elofeldolgozo lépésen belül a num transzformátor imputer lépésének strategy paraméterét hangolod. Elsőre kicsit bonyolultnak tűnhet, de ha egyszer megérted a struktúrát, nagyon intuitívvá válik. Ígérem.

Hatékonyabb hangolás: RandomizedSearchCV

Ha a paramétertér nagy (és általában az), a GridSearchCV exponenciálisan lassulhat. Ilyenkor érdemes a RandomizedSearchCV-t használni, amely véletlenszerűen mintáz a paramétertérből:

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

param_distributions = {
    "classifier__n_estimators": randint(50, 300),
    "classifier__max_depth": [3, 5, 10, 15, None],
    "classifier__min_samples_split": randint(2, 20),
    "classifier__min_samples_leaf": randint(1, 10)
}

random_search = RandomizedSearchCV(
    teljes_pipeline,
    param_distributions,
    n_iter=50,
    cv=5,
    scoring="accuracy",
    random_state=42,
    n_jobs=-1
)

random_search.fit(X_train, y_train)
print(f"Legjobb paraméterek: {random_search.best_params_}")
print(f"Legjobb CV pontosság: {random_search.best_score_:.4f}")

Tapasztalatom szerint a RandomizedSearchCV 50 iterációval gyakran majdnem ugyanolyan jó eredményt ad, mint a teljes grid search – de a futásidő a töredéke. Nagyobb projekteknél ez hatalmas különbség.

Pipeline mentése és betöltése

Az egyik legnagyobb előny, ami miatt én szinte mindig pipeline-t használok: a teljes pipeline – az összes transzformátorral és a tanított modellel együtt – egyetlen fájlba menthető.

import joblib

# Pipeline mentése
joblib.dump(teljes_pipeline, "hitel_pipeline.joblib")

# Pipeline betöltése
betoltott_pipeline = joblib.load("hitel_pipeline.joblib")

# Predikció új adatokra
uj_adat = pd.DataFrame({
    "kor": [35],
    "jovedelem": [520000],
    "varos": ["Budapest"],
    "vegzettseg": ["felsőfokú"]
})

eredmeny = betoltott_pipeline.predict(uj_adat)
valoszinuseg = betoltott_pipeline.predict_proba(uj_adat)
print(f"Predikció: {eredmeny[0]}")
print(f"Valószínűségek: {valoszinuseg[0]}")

Ez azt jelenti, hogy production környezetben nem kell külön kezelned az előfeldolgozást és a modellt. Egyetlen objektum mindent tartalmaz, ami az előrejelzéshez kell. Egyszerűbb deployt nehéz elképzelni.

Haladó technika: egyedi transzformátor írása

Néha a beépített transzformátorok nem elegendőek – és ez teljesen rendben van. Ilyenkor saját transzformátort írhatsz a BaseEstimator és TransformerMixin osztályok felhasználásával:

from sklearn.base import BaseEstimator, TransformerMixin

class KorCsoportTransformer(BaseEstimator, TransformerMixin):
    """A kor oszlopot korcsoportokra bontja."""

    def __init__(self, hatarok=None):
        self.hatarok = hatarok or [0, 25, 35, 50, 65, 100]

    def fit(self, X, y=None):
        return self

    def transform(self, X):
        X_masolat = X.copy()
        cimkek = [f"{self.hatarok[i]}-{self.hatarok[i+1]}"
                  for i in range(len(self.hatarok) - 1)]
        X_masolat["korcsoport"] = pd.cut(
            X_masolat["kor"],
            bins=self.hatarok,
            labels=cimkek,
            include_lowest=True
        )
        return X_masolat

# Használat a pipeline-ban
bovitett_pipeline = Pipeline(steps=[
    ("korcsoport", KorCsoportTransformer()),
    ("elofeldolgozo", ColumnTransformer(transformers=[
        ("num", numerikus_transformer, ["kor", "jovedelem"]),
        ("cat", kategorikus_transformer, ["varos", "vegzettseg", "korcsoport"])
    ])),
    ("classifier", RandomForestClassifier(n_estimators=100, random_state=42))
])

bovitett_pipeline.fit(X_train, y_train)
print(f"Bővített pipeline pontosság: {bovitett_pipeline.score(X_test, y_test):.4f}")

Az egyedi transzformátor írásánál a legfontosabb szabály: a fit() metódusban tanulj, a transform()-ban alakítsd az adatot, és mindig térj vissza self-fel a fit()-ből. Ha ezeket betartod, az egyedi transzformátorod tökéletesen fog működni a pipeline-ban – a keresztvalidáció és a grid search során is.

Pipeline vizualizáció és hibakeresés

A scikit-learn 1.8-ban a pipeline-ok vizuálisan is megjeleníthetők Jupyter Notebook-ban, ami a hibakeresést jelentősen megkönnyíti:

from sklearn import set_config

# HTML megjelenítés bekapcsolása
set_config(display="diagram")

# A pipeline megjelenítése – Jupyter-ben interaktív HTML diagramot kapsz
teljes_pipeline

Ez egy interaktív HTML diagramot generál, ahol kattintással megvizsgálhatod az egyes lépések paramétereit. Amikor valami nem stimmel a modelleddel, ez az első hely, ahová érdemes benézni.

Közbenső eredmények elérése

# Transzformált adatok megtekintése (a modell nélkül)
from sklearn.pipeline import make_pipeline

# Csak az előfeldolgozó lépés futtatása
X_transzformalt = teljes_pipeline.named_steps["elofeldolgozo"].transform(X_test)
print(f"Transzformált alakzat: {X_transzformalt.shape}")

# Jellemzőnevek lekérdezése (scikit-learn 1.8+)
feature_names = teljes_pipeline.named_steps["elofeldolgozo"].get_feature_names_out()
print(f"Jellemzőnevek: {feature_names}")

Tipikus hibák és elkerülésük

Pár csapda rendszeresen előkerül a pipeline-ok használata közben. Jobb, ha ezekről tudsz, mielőtt belefutnál.

1. Adatszivárgás a pipeline-on kívül

# ROSSZ – a scaler az összes adatból tanul
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # Az ÖSSZES adaton fit
X_train_s, X_test_s, y_train, y_test = train_test_split(X_scaled, y)

# JÓ – a scaler a pipeline-ban van
pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression())
])
pipe.fit(X_train, y_train)  # A scaler CSAK a tréning adatból tanul

2. ColumnTransformer oszlopsorrend-probléma

A ColumnTransformer újrarendezi az oszlopokat: először a numerikus transzformátor kimenetei jönnek, aztán a kategorikuséi. Ha egy későbbi lépés indexek alapján hivatkozik oszlopokra, az baj lehet. A megoldás egyszerű: mindig a get_feature_names_out() metódust használd az oszlopnevek lekérdezéséhez.

3. Elfelejtett remainder paraméter

# Ha vannak oszlopok, amikre nem definiáltál transzformátort,
# alapértelmezetten eldobódnak!
elofeldolgozo_biztonsagos = ColumnTransformer(
    transformers=[
        ("num", numerikus_transformer, numerikus_oszlopok),
        ("cat", kategorikus_transformer, kategorikus_oszlopok)
    ],
    remainder="passthrough"  # A többi oszlop változatlanul átmegy
)

Ez a hiba különösen alattomosan tud megjelenni. Nem kapsz hibaüzenetet – egyszerűen csak eltűnnek azok az oszlopok, amikre nem definiáltál transzformátort. Szóval mindig gondold végig, kell-e a remainder paraméter.

Teljes munkafolyamat: összefoglaló példa

Végül lássunk egy teljes, éles projektben is használható munkafolyamatot az elejétől a végéig. Ez az a sablon, amit én is kiindulópontként szoktam használni:

import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import (
    train_test_split, RandomizedSearchCV, cross_val_score
)
from sklearn.metrics import classification_report
from scipy.stats import randint
import joblib

# 1. Adatok betöltése (itt szintetikus adatot használunk)
np.random.seed(42)
n = 1000
df = pd.DataFrame({
    "kor": np.random.randint(18, 70, n).astype(float),
    "jovedelem": np.random.normal(500000, 180000, n),
    "munkaviszony_ev": np.random.randint(0, 40, n).astype(float),
    "hitel_osszeg": np.random.normal(3000000, 1500000, n),
    "varos": np.random.choice(
        ["Budapest", "Debrecen", "Szeged", "Pécs", "Győr", "Miskolc"], n
    ),
    "vegzettseg": np.random.choice(["alapfokú", "középfokú", "felsőfokú"], n),
    "foglalkozas": np.random.choice(
        ["alkalmazott", "vállalkozó", "szabadúszó", "nyugdíjas"], n
    ),
    "elfogadva": np.random.choice([0, 1], n, p=[0.35, 0.65])
})

# Hiányzó értékek szimulálása
for col in ["kor", "jovedelem", "munkaviszony_ev"]:
    df.loc[np.random.choice(n, 40, replace=False), col] = np.nan

# 2. Előfeldolgozó pipeline
numerikus_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

kategorikus_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])

elofeldolgozo = ColumnTransformer([
    ("num", numerikus_pipeline, make_column_selector(dtype_include="number")),
    ("cat", kategorikus_pipeline, make_column_selector(dtype_include="object"))
])

# 3. Teljes pipeline
pipeline = Pipeline([
    ("preprocess", elofeldolgozo),
    ("model", GradientBoostingClassifier(random_state=42))
])

# 4. Adat szétválasztás
X = df.drop("elfogadva", axis=1)
y = df["elfogadva"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 5. Hiperparaméter-hangolás
param_dist = {
    "model__n_estimators": randint(50, 300),
    "model__max_depth": [3, 5, 7, 10],
    "model__learning_rate": [0.01, 0.05, 0.1, 0.2],
    "model__subsample": [0.8, 0.9, 1.0]
}

search = RandomizedSearchCV(
    pipeline, param_dist, n_iter=30, cv=5,
    scoring="accuracy", random_state=42, n_jobs=-1
)
search.fit(X_train, y_train)

# 6. Eredmények
print(f"Legjobb paraméterek: {search.best_params_}")
print(f"Legjobb CV pontosság: {search.best_score_:.4f}")
print(f"\nTeszt eredmények:")
print(classification_report(y_test, search.predict(X_test)))

# 7. Pipeline mentése production-re
joblib.dump(search.best_estimator_, "legjobb_hitel_pipeline.joblib")

Gyakran ismételt kérdések

Mi a különbség a Pipeline és a make_pipeline között?

A Pipeline explicit neveket vár minden lépéshez (pl. ("scaler", StandardScaler())), míg a make_pipeline automatikusan generálja a neveket az osztályok nevéből. Ha hiperparaméter-hangolást tervezel, a Pipeline-t érdemes használni, mert a nevek fontosak a paraméter-hivatkozásoknál.

Hogyan kezeljem a szöveges oszlopokat a Pipeline-ban?

A ColumnTransformer-ben egy harmadik ágat adhatsz hozzá szöveges oszlopokra, például TfidfVectorizer-rel. A szintaxis ugyanaz: ("text", TfidfVectorizer(), "szoveg_oszlop"). Fontos, hogy a szöveges transzformátor egyetlen oszlopot vár sztring formátumban.

Miért használjak Pipeline-t egyszerű projektekhez is?

Három fő ok van. Először is megelőzöd az adatszivárgást a keresztvalidáció során. Másodszor a teljes munkafolyamatot egyetlen fájlba mentheted. Harmadszor pedig a kódod azonnal production-ready lesz, mert a predikció során automatikusan ugyanazok az előfeldolgozási lépések futnak le.

Lehet-e GPU-t használni a scikit-learn Pipeline-ban?

A scikit-learn 1.8 vezette be a natív Array API támogatást, ami lehetővé teszi PyTorch és CuPy tömbök közvetlen használatát. Ez azt jelenti, hogy bizonyos műveletek (különösen lineáris modellek) GPU-n is futtathatók, bár a teljes GPU támogatás egyelőre még korlátozott. Nagyobb projekteknél érdemes a scikit-learn-intelex csomagot is megnézni, amely Intel hardveren jelentős gyorsulást nyújt.

Hogyan debuggoljam a Pipeline-t, ha rossz eredményt kapok?

Három bevált megközelítés van. Használd a set_config(display="diagram") beállítást a vizuális megjelenítéshez. A named_steps attribútummal közvetlenül elérheted bármelyik lépést és megvizsgálhatod a paramétereit. Végül a Pipeline memory paraméterével cache-elheted a közbenső eredményeket, ami gyorsabb iterációt tesz lehetővé fejlesztés közben.

A Szerzőről Editorial Team

Our team of expert writers and editors.