Varför feature engineering avgör modellens framgång
Det finns ett gammalt talesätt inom maskininlärning: "bättre features slår bättre algoritmer". Och ärligt talat — det stämmer oftare än man tror. Du kan sitta med världens mest sofistikerade modell, men om dina features är dåligt bearbetade kommer resultaten vara mediokra. Punkt.
Feature engineering handlar i grunden om att omvandla rådata till numeriska representationer som faktiskt hjälper modellen hitta mönster. Det låter enkelt, men i praktiken är det här steget som skiljer ett genomsnittligt ML-projekt från ett riktigt bra.
I den här guiden går vi igenom hela processen — från grunderna i feature engineering till avancerade pipelines med scikit-learn 1.8. Vi bygger kompletta, reproducerbara arbetsflöden med Pipeline, ColumnTransformer och de senaste transformerarna. Om du redan läst vår guide till scikit-learn 1.8 är det här den naturliga fortsättningen där vi applicerar teorin i praktiken.
Grundläggande begrepp inom feature engineering
Vad är features och varför behöver de bearbetas?
En feature (eller variabel, om man föredrar det) är en enskild mätbar egenskap hos datan du analyserar. I ett bostadsdataset kan features vara saker som antal rum, area i kvadratmeter, postnummer och byggnadsår. Problemet? Maskininlärningsmodeller kräver numerisk input — och verkligheten levererar sällan data i det formatet.
Feature engineering omfattar flera typer av transformationer:
- Hantering av saknade värden — imputering med medelvärde, median eller mer avancerade metoder
- Kodning av kategoriska variabler — one-hot encoding, ordinal encoding, target encoding
- Skalning av numeriska variabler — standardisering, normalisering, robust skalning
- Skapande av nya features — polynomiska features, interaktionstermer, binning
- Feature-selektion — att välja ut de mest informativa variablerna
Varför pipelines är avgörande
Utan pipelines ser feature engineering-kod ofta ut som en lång sekvens av ad hoc-transformationer. Du vet hur det blir — svårt att underhålla, lätt att råka ut för dataläckage, och nästan omöjligt att reproducera konsekvent mellan träning och produktion.
Scikit-learns Pipeline löser detta genom att kapsla in hela transformationskedjan i ett enda objekt. Jag har själv jobbat med projekt där avsaknaden av pipelines ledde till timmar av debugging — tro mig, det är värt att lära sig från start.
Scikit-learn Pipeline: Grunderna
Så fungerar Pipeline-klassen
En Pipeline är en sekvens av steg där varje steg (utom det sista) måste vara en transformer — alltså ett objekt med fit() och transform()-metoder. Det sista steget kan vara en transformer, en prediktor eller en klustringsalgoritm.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
# En enkel pipeline med tre steg
pipe = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
("classifier", LogisticRegression(max_iter=1000))
])
# Träna hela kedjan i ett anrop
pipe.fit(X_train, y_train)
# Prediktera — imputering och skalning sker automatiskt
predictions = pipe.predict(X_test)
Det fina med detta? Hela kedjan — imputering, skalning och klassificering — hanteras som ett enda objekt. Du kan spara det med joblib, använda det i korsvalidering och skicka det till produktion utan risk för inkonsistenta transformationer. Ganska smidigt.
Undvik dataläckage med pipelines
Det här är ett av de vanligaste misstagen inom maskininlärning, och det smyger sig in lättare än man tror: att anpassa transformerare på hela datasetet innan man delar upp det i träning och test. Om du till exempel beräknar medelvärdet för imputering på hela datasetet läcker information från testdatan in i träningen.
Pipelines förhindrar detta automatiskt — fit() körs bara på träningsdata, och transform() appliceras konsekvent på all data.
from sklearn.model_selection import cross_val_score
# Korsvalidering med pipeline — inget dataläckage
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
print(f"Medelträffsäkerhet: {scores.mean():.3f} (+/- {scores.std():.3f})")
ColumnTransformer: Hantera blandade datatyper
Varför du behöver ColumnTransformer
I verkliga dataset har du nästan alltid en blandning av numeriska och kategoriska kolumner — och de kräver helt olika transformationer. Det går inte att köra StandardScaler på stadsnamn, liksom. ColumnTransformer löser detta genom att låta dig applicera olika pipelines på olika kolumner och sedan sammanfoga resultaten till en enhetlig feature-matris.
Bygga en ColumnTransformer steg för steg
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
# Exempeldata
data = pd.DataFrame({
"alder": [25, 30, None, 45, 35],
"inkomst": [350000, 420000, 510000, None, 380000],
"stad": ["Stockholm", "Göteborg", "Malmö", "Stockholm", "Uppsala"],
"utbildning": ["kandidat", "master", "kandidat", "doktor", "master"]
})
# Definiera kolumngrupper
numeriska_kolumner = ["alder", "inkomst"]
kategoriska_kolumner = ["stad", "utbildning"]
# Pipeline för numeriska features
numerisk_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
# Pipeline för kategoriska features
kategorisk_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
# Kombinera med ColumnTransformer
preprocessor = ColumnTransformer(transformers=[
("num", numerisk_pipeline, numeriska_kolumner),
("cat", kategorisk_pipeline, kategoriska_kolumner)
])
Automatisk kolumnidentifiering med make_column_selector
Istället för att hårdkoda kolumnnamn kan du använda make_column_selector för att automatiskt identifiera kolumner baserat på datatyp. Särskilt praktiskt med stora dataset där kolumnerna kan ändras över tid.
from sklearn.compose import make_column_selector
preprocessor_auto = ColumnTransformer(transformers=[
("num", numerisk_pipeline, make_column_selector(dtype_include="number")),
("cat", kategorisk_pipeline, make_column_selector(dtype_include="object"))
])
Avancerade transformerare i scikit-learn 1.8
TargetEncoder för kategoriska variabler med hög kardinalitet
One-hot encoding fungerar utmärkt för kategoriska variabler med få unika värden, men det blir opraktiskt (för att inte säga kaotiskt) när kardinaliteten är hög — tänk stadsnamn, produktkoder eller postnummer. TargetEncoder, som är stabil i scikit-learn sedan version 1.4, löser detta genom att ersätta varje kategori med medelvärdet av målvariabeln för den kategorin. Den har dessutom inbyggd regularisering för att undvika överanpassning.
from sklearn.preprocessing import TargetEncoder
# TargetEncoder med intern korsvalidering för regularisering
target_enc = TargetEncoder(smooth="auto", cv=5)
# Användning i en pipeline
kategorisk_target_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", target_enc)
])
Parametern smooth="auto" balanserar automatiskt mellan kategorins medelvärde och det globala medelvärdet baserat på antalet observationer per kategori. Färre observationer ger mer regularisering — logiskt och smidigt.
KBinsDiscretizer för binning av numeriska variabler
Ibland förbättras modellprestanda genom att dela in kontinuerliga variabler i diskreta intervall. Det kanske inte alltid är intuitivt, men binning kan hjälpa modellen fånga icke-linjära samband utan att behöva polynomiska features.
KBinsDiscretizer erbjuder tre strategier: uniform (lika breda intervall), quantile (lika många observationer per intervall) och kmeans (klusterbaserade intervall).
from sklearn.preprocessing import KBinsDiscretizer
# Diskretisera ålder i 5 kvantilbaserade bins
binner = KBinsDiscretizer(
n_bins=5,
encode="ordinal",
strategy="quantile"
)
# Kombinera med ColumnTransformer
preprocessor_binned = ColumnTransformer(transformers=[
("alder_bins", binner, ["alder"]),
("num", numerisk_pipeline, ["inkomst"]),
("cat", kategorisk_pipeline, kategoriska_kolumner)
])
PolynomialFeatures för icke-linjära samband
Linjära modeller kan bara fånga linjära relationer — om det verkliga sambandet är icke-linjärt behöver du ge modellen lite hjälp. PolynomialFeatures skapar automatiskt polynomtermer och interaktionstermer från dina befintliga features.
from sklearn.preprocessing import PolynomialFeatures
# Skapa grad 2-polynomer med interaktionstermer
poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False)
# Pipeline: imputera -> skala -> polynomisera
numerisk_poly_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
("poly", poly)
])
En varning dock: Med många features och hög grad exploderar antalet kolumner snabbt. Med 10 features och degree=3 får du hela 285 kolumner. Använd interaction_only=True för att begränsa, eller kombinera med feature-selektion efteråt. Jag har lärt mig den läxan den hårda vägen.
Komplett pipeline: Från rådata till prediktion
Okej, nu sätter vi ihop allt i ett realistiskt exempel. Vi använder ett simulerat bostadsdataset och bygger en komplett pipeline som hanterar alla steg från rådata till färdig modell.
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (
StandardScaler, OneHotEncoder, TargetEncoder,
PolynomialFeatures
)
from sklearn.impute import SimpleImputer
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import cross_val_score
# Simulerat bostadsdataset
np.random.seed(42)
n = 500
df = pd.DataFrame({
"area_kvm": np.random.normal(85, 25, n).clip(25, 200),
"antal_rum": np.random.choice([1, 2, 3, 4, 5], n),
"byggar": np.random.randint(1950, 2025, n),
"stad": np.random.choice(
["Stockholm", "Göteborg", "Malmö", "Uppsala", "Linköping",
"Örebro", "Västerås", "Helsingborg", "Norrköping", "Jönköping"],
n
),
"bostadstyp": np.random.choice(
["lägenhet", "villa", "radhus", "kedjehus"], n
),
"har_balkong": np.random.choice([True, False], n),
"pris_mkr": None # Fylls i nedan
})
# Generera pris baserat på features (för demonstration)
df["pris_mkr"] = (
df["area_kvm"] * 0.04
+ df["antal_rum"] * 0.3
+ (2025 - df["byggar"]) * -0.01
+ np.random.normal(0, 0.5, n)
).clip(0.5, 15)
# Separera features och målvariabel
X = df.drop(columns=["pris_mkr"])
y = df["pris_mkr"]
# Definiera kolumngrupper
numeriska = ["area_kvm", "antal_rum", "byggar"]
kategoriska_lag = ["bostadstyp"] # Låg kardinalitet
kategoriska_hog = ["stad"] # Hög kardinalitet
booleska = ["har_balkong"]
# Sub-pipelines
num_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
cat_low_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
cat_high_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", TargetEncoder(smooth="auto", cv=5))
])
bool_pipeline = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent"))
])
# Kombinera alla transformationer
preprocessor = ColumnTransformer(transformers=[
("num", num_pipeline, numeriska),
("cat_low", cat_low_pipeline, kategoriska_lag),
("cat_high", cat_high_pipeline, kategoriska_hog),
("bool", bool_pipeline, booleska)
])
# Fullständig pipeline med modell
full_pipeline = Pipeline([
("preprocessor", preprocessor),
("model", GradientBoostingRegressor(
n_estimators=200,
max_depth=4,
learning_rate=0.1,
random_state=42
))
])
# Korsvalidering
scores = cross_val_score(
full_pipeline, X, y,
cv=5,
scoring="neg_mean_absolute_error"
)
print(f"MAE: {-scores.mean():.3f} (+/- {scores.std():.3f})")
Nytt i scikit-learn 1.8: Pipeline-förbättringar
Scikit-learn 1.8 (december 2025) kom med flera förbättringar som är direkt relevanta för feature engineering-pipelines. Här är de viktigaste:
- Parametern
transform_input— gör det möjligt att transformera metadata-parametrar genom pipelinens steg innan de skickas vidare. Kräver att metadata-routing är aktiverat viasklearn.set_config(enable_metadata_routing=True). - Polars-stöd i ColumnTransformer — en välkommen buggfix som gör att
ColumnTransformernu korrekt hanterar data som skickas in sompolars.DataFrameäven när transformerare producerar gles (sparse) output. - Array API och GPU-stöd — fler estimerare stödjer nu Array API-kompatibla inputs (PyTorch-tensorer, CuPy-arrayer), vilket öppnar för GPU-accelererad feature engineering.
- FeatureUnion-validering —
FeatureUnionvaliderar nu att alla transformerare returnerar 2D-output och kastar ett informativt fel om 1D-output detekteras. Inga fler tysta fel. - Fri-trådad Python-stöd — experimentella wheels för fri-trådad CPython (Python 3.14, utan GIL) finns tillgängliga, vilket kan snabba upp parallella pipeline-steg avsevärt.
Feature-selektion inom pipelines
Feature engineering skapar ofta fler features än vad modellen egentligen behöver. Det är lätt att bli överentusiastisk (jag vet av erfarenhet). Feature-selektion hjälper dig identifiera och behålla de mest informativa variablerna, vilket minskar överanpassning och kan förbättra prestanda.
from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.pipeline import Pipeline
# Pipeline med preprocessning, feature-selektion och modell
pipeline_med_selektion = Pipeline([
("preprocessor", preprocessor),
("selector", SelectKBest(score_func=f_regression, k=10)),
("model", GradientBoostingRegressor(random_state=42))
])
Andra alternativ för feature-selektion inkluderar VarianceThreshold (tar bort features med låg varians), SelectFromModel (väljer features baserat på vikter från en tränad modell) och SequentialFeatureSelector (framåt- eller bakåtselektion).
Hyperparameteroptimering av hela pipelinen
En av de stora fördelarna med att kapsla in allt i en pipeline är att du kan optimera alla hyperparametrar samtidigt — både preprocessningens och modellens. Scikit-learns namnkonvention med dubbla understreck gör det möjligt, och det är ärligt talat ganska elegant.
from sklearn.model_selection import GridSearchCV
# Definiera sökrymden — notera dubbla understreck
param_grid = {
"preprocessor__num__imputer__strategy": ["mean", "median"],
"model__n_estimators": [100, 200, 300],
"model__max_depth": [3, 4, 5],
"model__learning_rate": [0.05, 0.1, 0.2]
}
# GridSearchCV med hela pipelinen
search = GridSearchCV(
full_pipeline,
param_grid,
cv=5,
scoring="neg_mean_absolute_error",
n_jobs=-1,
verbose=1
)
search.fit(X, y)
print(f"Bästa MAE: {-search.best_score_:.3f}")
print(f"Bästa parametrar: {search.best_params_}")
Spara och ladda pipelines för produktion
När du väl hittat den optimala pipelinen behöver du kunna spara den för användning i produktion. Det fina är att hela pipelinen — inklusive alla inlärda parametrar — kan serialiseras med joblib i ett enda anrop.
import joblib
# Spara den bästa pipelinen
joblib.dump(search.best_estimator_, "bostadspris_pipeline.joblib")
# Ladda och använd i produktion
loaded_pipeline = joblib.load("bostadspris_pipeline.joblib")
# Prediktera på ny data — all preprocessing sker automatiskt
ny_bostad = pd.DataFrame({
"area_kvm": [72],
"antal_rum": [3],
"byggar": [2018],
"stad": ["Stockholm"],
"bostadstyp": ["lägenhet"],
"har_balkong": [True]
})
pris = loaded_pipeline.predict(ny_bostad)
print(f"Uppskattat pris: {pris[0]:.2f} Mkr")
Feature-engine: Ett Pandas-vänligt alternativ
Scikit-learns transformerare returnerar NumPy-arrayer som standard, vilket innebär att du förlorar kolumnnamn och index. Det kan vara frustrerande under utveckling och felsökning. Biblioteket Feature-engine löser detta genom att erbjuda transformerare som alltid returnerar Pandas DataFrames — samtidigt som de är fullt kompatibla med scikit-learn-pipelines.
# pip install feature-engine
from feature_engine.imputation import MeanMedianImputer
from feature_engine.encoding import OneHotEncoder as FEOneHotEncoder
from feature_engine.selection import DropCorrelatedFeatures
# Feature-engine pipeline som bevarar DataFrame-struktur
fe_pipeline = Pipeline([
("imputer", MeanMedianImputer(
imputation_method="median",
variables=numeriska
)),
("encoder", FEOneHotEncoder(
variables=kategoriska_lag,
drop_last=True
)),
("drop_corr", DropCorrelatedFeatures(threshold=0.95)),
])
Feature-engine kan automatiskt identifiera numeriska, kategoriska och datetime-variabler, och erbjuder specialiserade transformerare som RareLabelEncoder (grupperar sällsynta kategorier), DecisionTreeEncoder och WoEEncoder (Weight of Evidence). Värt att testa om du tycker att scikit-learns API ibland känns lite stelbent.
Vanliga misstag och hur du undviker dem
Efter att ha jobbat med feature engineering i en hel del projekt har jag sett samma misstag dyka upp gång på gång. Här är de viktigaste att hålla koll på:
- Dataläckage vid imputering — Beräkna aldrig statistik (medelvärde, median) på hela datasetet före uppdelning. Använd alltid en pipeline som anpassas på träningsdata.
- One-hot encoding av hög-kardinalitetsvariabler — Med 1000 unika stadsnamn får du 1000 nya kolumner. Använd
TargetEncoderellerOrdinalEncoderistället. - Glömma
handle_unknown="ignore"— Om testdatan innehåller kategorier som inte fanns i träningsdatan krascharOneHotEncoderutan denna parameter. Klassisk miss. - Skalning av trädbaserade modeller — Decision trees, random forests och gradient boosting behöver inte skalade features. Skippa skalningssteget för dessa modeller — det sparar beräkningstid utan att påverka resultaten.
- Feature explosion med PolynomialFeatures — Kombinera alltid med feature-selektion eller regularisering om du använder polynomiska features av grad 2 eller högre.
Vanliga frågor om feature engineering i Python
Vad är skillnaden mellan Pipeline och ColumnTransformer?
En Pipeline kedjar ihop steg sekventiellt — varje steg transformerar outputen från föregående steg. En ColumnTransformer applicerar olika transformerare på olika kolumner parallellt och sammanfogar resultaten. I praktiken nästlar man nästan alltid en ColumnTransformer inuti en Pipeline för att hantera hela arbetsflödet.
Hur undviker jag dataläckage vid feature engineering?
Dela alltid upp datan i träning och test innan du gör några transformationer. Använd scikit-learns Pipeline som automatiskt ser till att fit() bara körs på träningsdata. Undvik att beräkna statistik (medelvärden, standardavvikelser) på hela datasetet — det ska alltid komma från träningsdatan. Det här misstaget är vanligare än man tror, även bland erfarna utvecklare.
När ska jag använda TargetEncoder istället för OneHotEncoder?
TargetEncoder är bäst lämpad för kategoriska variabler med hög kardinalitet (många unika värden), som postnummer eller stadsnamn. OneHotEncoder fungerar bra för variabler med få kategorier — under cirka 10–15 unika värden. Använder du one-hot encoding på tusentals kategorier riskerar du feature explosion och överanpassning.
Behöver jag skala features för alla modeller?
Nej, definitivt inte. Trädbaserade modeller (decision trees, random forests, gradient boosting) påverkas inte av skalning och fungerar lika bra med oskalade features. Däremot kräver avståndbaserade modeller (KNN, SVM) och linjära modeller (linjär regression, logistisk regression) att features skalas — annars domineras de av variabler med stora numeriska intervall.
Kan jag använda Polars med scikit-learn pipelines?
Ja! Sedan scikit-learn 1.8 har stödet för Polars-dataframes förbättrats rejält. En specifik buggfix gör att ColumnTransformer nu korrekt hanterar polars.DataFrame även när transformerare producerar gles output. Dock rekommenderas fortfarande Pandas om du vill ha bäst kompatibilitet med hela scikit-learn-ekosystemet.