Skrub in Python: Automated Feature Engineering for Messy Tabular Data (2026)

Skrub 0.9 turns messy pandas or Polars DataFrames into sklearn-ready features with one call. Covers TableVectorizer, tabular_pipeline, encoders, fuzzy joins, and DataOps.

Skrub Python: Tabular ML Guide (2026)

Updated: July 29, 2026

Skrub is an open-source Python library that turns messy pandas or Polars DataFrames into scikit-learn ready feature matrices with a single call, handling dirty strings, high-cardinality categoricals, datetimes, and even fuzzy multi-table joins. Version 0.9 (released May 2026) ships with TableVectorizer, the tabular_pipeline baseline builder, and the new declarative DataOps API. That's enough to replace hundreds of lines of manual preprocessing glue on typical tabular problems. If you've ever written a 200-line ColumnTransformer for a Kaggle dataset, skrub was built for you.

  • Skrub 0.9 (May 2026) is maintained by the INRIA / Probabl team behind scikit-learn and is marked Production/Stable on PyPI.
  • TableVectorizer inspects every column's dtype and dispatches to the right encoder, so you never write a ColumnTransformer for tabular baselines again.
  • tabular_pipeline(estimator) returns a strong end-to-end sklearn pipeline in one line (vectorizer, imputer, scaler, and model), wired for the estimator you passed.
  • For high-cardinality string columns, StringEncoder (default since 0.6), GapEncoder, and MinHashEncoder beat one-hot on typo-heavy, free-text fields.
  • Joiner, AggJoiner, and InterpolationJoiner perform fuzzy multi-table joins on inconsistent keys, a workflow that pandas alone cannot express cleanly.
  • The new DataOps API lets you build multi-input transformation graphs and export them as a SkrubLearner that ships like any sklearn estimator.

What is skrub and why it exists

Skrub sits between your raw DataFrame and your scikit-learn estimator. In academic terms, it operationalises what the literature calls automated feature engineering for heterogeneous tabular data. That's the class of problems where you have a mix of numeric, categorical, datetime, and free-text columns, missing values, typos, and often several tables that need to be joined before modelling. Cerda and Varoquaux described the underlying encoders in their 2022 IEEE TKDE paper "Encoding High-Cardinality String Categorical Variables". Skrub is the direct engineering descendant of that research, now maintained by the INRIA Soda team and the Probabl spin-off.

Honestly, in my experience shipping tabular ML into production, roughly 60–70% of the code in a first working pipeline is boilerplate: dtype inspection, imputation strategy selection, one-hot for low-cardinality columns, target- or hash-encoding for high-cardinality, datetime component extraction, and finally a ColumnTransformer that ties it all together. Skrub encodes those defaults as one function call. You don't lose the ability to override anything (TableVectorizer exposes per-dtype encoder slots), but you no longer have to write the plumbing just to get a first honest cross-validation score.

The library also fills a specific gap that pandas leaves open: fuzzy joins. Real-world enrichment datasets rarely share exact keys ("United States" vs "USA" vs "U.S."). Skrub's Joiner uses vectorised approximate string matching so you can enrich a training table without writing a bespoke reconciliation pipeline.

How is skrub different from pandas and scikit-learn?

Pandas is a data container with expression semantics; scikit-learn is a modelling framework that expects a clean numeric matrix. Skrub is the bridge, doing the same thing you'd do with a hand-tuned Pipeline and ColumnTransformer, but deriving the configuration from the data itself.

Concretely, when you call TableVectorizer().fit_transform(df) on a DataFrame of mixed columns, skrub inspects each column, decides it's numeric / low-cardinality categorical / high-cardinality string / datetime, and picks the appropriate transformer. The output is a NumPy or Polars matrix of dtype float32 that any sklearn estimator will accept. If you had done this by hand you'd have written something like:

from sklearn.compose import make_column_transformer, make_column_selector
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
# ... plus datetime extraction, plus a strategy for high-cardinality strings,
# plus glue for every new dataset shape. Easily 40+ lines.

Skrub also differs from pandas' own preprocessing utilities (get_dummies, factorize) in that its transformers are stateful sklearn objects: they learn parameters at fit time and re-apply them at transform time, which is what makes them safe under cross-validation and inside serialised pipelines. If you want a deeper primer on why that matters, our feature engineering with scikit-learn pipelines guide covers the fit/transform contract in detail.

Installing skrub 0.9 and a first look at TableReport

Skrub 0.9 requires Python ≥ 3.10 and is BSD-3 licensed. Install it with pip or uv:

# pip
pip install "skrub==0.9.*"

# uv (faster, recommended in 2026)
uv add "skrub==0.9.*"

The very first thing worth trying on any new dataset is TableReport, an interactive HTML profile that shows distributions, cardinality, missingness, and pairwise associations. It renders inline in Jupyter and Marimo, or you can open it in a browser tab:

import pandas as pd
from skrub import TableReport
from skrub.datasets import fetch_employee_salaries

data = fetch_employee_salaries()
df = data.X.assign(current_annual_salary=data.y)

TableReport(df).open()   # opens a full HTML report in your browser

If you've already read our comparison of automated EDA tools in Python, think of TableReport as the lightweight, scikit-learn-native alternative. It won't replace ydata-profiling for a 40-page audit, but it's a much faster first look and it doesn't add a heavyweight dependency to your ML repo.

TableVectorizer: automatic column-wise preprocessing

TableVectorizer is the workhorse of skrub. It's an sklearn transformer that dispatches each column to a type-appropriate sub-transformer. As of 0.9, the defaults are:

  • Numeric → passthrough (cast to float32).
  • Low-cardinality categorical (≤ 40 unique values by default) → OneHotEncoder.
  • High-cardinality stringStringEncoder (tf-idf on character n-grams + truncated SVD).
  • DatetimeDatetimeEncoder (year, month, day, hour, day-of-week, plus optional cyclical features).

Here's what that looks like end-to-end on the Employee Salaries dataset, which has a genuinely dirty free-text employee_position_title column with 385 unique values:

from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from skrub import TableVectorizer
from skrub.datasets import fetch_employee_salaries

data = fetch_employee_salaries()
X, y = data.X, data.y

pipe = make_pipeline(
    TableVectorizer(),
    HistGradientBoostingRegressor(random_state=0),
)

r2 = cross_val_score(pipe, X, y, scoring="r2", cv=5).mean()
print(f"CV R2 = {r2:.3f}")

That's a complete tabular ML baseline in six lines. You can override any default:

from skrub import TableVectorizer, MinHashEncoder, DatetimeEncoder

vec = TableVectorizer(
    high_cardinality=MinHashEncoder(n_components=30),
    datetime=DatetimeEncoder(add_weekday=True),
    cardinality_threshold=20,
)

Under the hood, TableVectorizer keeps a mapping from input column names to output feature names. Call vec.get_feature_names_out() after fitting to inspect it. That mapping is what makes skrub compatible with model-interpretation tools; if you feed the transformed matrix into SHAP, the feature names propagate cleanly (see our SHAP values guide for how to consume them).

tabular_pipeline: a strong baseline in one function

If TableVectorizer is one abstraction level above ColumnTransformer, then tabular_pipeline is one level above that. It returns a fully wired sklearn Pipeline (vectorizer, optional imputer, optional scaler, and an estimator) with the intermediate steps chosen to match the estimator's needs. Tree-based models get no scaler; linear and neural models get a StandardScaler. Missing-value handling is added or skipped depending on whether the estimator supports NaNs natively.

The 0.9 release renamed the old tabular_learner to tabular_pipeline for consistency (the old name still works with a deprecation warning). You can pass either a string like "regressor" or "classifier", or an explicit estimator:

from skrub import tabular_pipeline
from sklearn.linear_model import RidgeCV
from sklearn.ensemble import HistGradientBoostingClassifier

# Auto-pick a good default estimator + preprocessing
model = tabular_pipeline("classifier")

# Or bring your own estimator; skrub configures the preprocessing around it
model = tabular_pipeline(HistGradientBoostingClassifier())
model = tabular_pipeline(RidgeCV())   # this variant will include a StandardScaler

Two subtle statistical points worth calling out. First, tabular_pipeline deliberately avoids target encoding by default, because target encoders can leak signal under naive cross-validation and require nested CV to score honestly. Skrub prefers the leak-safe StringEncoder/MinHashEncoder family. Second, the pipeline is cross-validation safe by construction: every transformer is fit only on the training fold, so you can pass the whole thing to cross_val_score or GridSearchCV without a "target leak" audit.

Which encoders does skrub provide for categorical data?

Skrub's most distinctive contribution is its family of encoders for high-cardinality and dirty string columns. Classic one-hot encoding explodes to thousands of columns and can't generalise to unseen categories. Target encoders leak. Skrub's encoders are unsupervised (no leak) and produce a fixed-width numeric embedding that generalises to new strings.

  • StringEncoder (default since 0.6). Computes character n-gram tf-idf, then reduces to k dimensions with truncated SVD. Fast, tolerant of typos, and interpretable-enough for feature-importance analysis.
  • GapEncoder. A topic model over character n-grams. Each output dimension corresponds to a learned "topic" of similar substrings, retrievable via get_feature_names_out(). Best when you want interpretable features (e.g., a categorical column of job titles where you'd like the model to say "this row scored high on the 'manager' topic").
  • MinHashEncoder. Locality-sensitive hashing (Broder, 1997) on character shingles. The cheapest option for very high cardinality (millions of unique values). Less interpretable than GapEncoder but scales linearly and is trivially parallel.
  • DatetimeEncoder. Extracts year, month, day, hour, minute, second, weekday, and optionally cyclical (sin/cos) features. Handles mixed formats via pandas parsing.

Empirically, on the classic "employee_salaries" and "medical_charges" benchmarks used in the original Cerda–Varoquaux paper, GapEncoder matches or beats one-hot while producing 10–100× fewer features. That's the payoff: fewer features, better generalisation, and no target-leak risk.

Fuzzy joining multi-table data with Joiner and AggJoiner

Skrub's second unique contribution is a family of sklearn-compatible joiners. Pandas' merge requires exact key equality; skrub's Joiner uses vectorised nearest-neighbour matching on string embeddings, so it can join tables whose keys have typos or formatting differences. I hit this exact problem last year on a country-enrichment job where the fuzzy match saved me a full afternoon of regex hand-cleaning.

import pandas as pd
from skrub import Joiner

main = pd.DataFrame({"country": ["United States", "Federal Rep. of Germany", "U.K."]})
lookup = pd.DataFrame({
    "country_name": ["United States of America", "Germany", "United Kingdom"],
    "gdp_per_capita": [76399, 51203, 46125],
})

joined = Joiner(lookup, main_key="country", aux_key="country_name").fit_transform(main)
print(joined)

AggJoiner extends this to one-to-many enrichment: aggregate the auxiliary table (mean, sum, first, min, max, mode) and attach the aggregates. InterpolationJoiner handles the numerical case. Instead of nearest-neighbour on strings, it fits a per-column regressor to predict the auxiliary values for each main row, which is useful for enriching geographical or temporal data where an exact key match will never exist.

Because all three joiners are transformers, they slot into a Pipeline like any other step. That means you can put the enrichment inside cross-validation and get an honest score on the enriched features, a workflow that's essentially impossible with pandas' merge.

What are skrub DataOps?

DataOps is the headline feature of skrub 0.9. It graduates the experimental Recipe API into a stable, declarative graph of multi-input transformations that can be exported as a SkrubLearner, a dict-input sklearn-style estimator that supports hyperparameter tuning and pickle-based deployment.

The intuition: real ML systems rarely have a single input table. You have a fact table, a customer table, a product table, and a time-series feature store, and your training procedure joins them, engineers features, and hands the result to a model. DataOps lets you express that whole graph declaratively and then treat it as a single sklearn object:

import skrub
from sklearn.ensemble import HistGradientBoostingClassifier

# Declare inputs as variables in the DataOps graph
orders = skrub.var("orders")
customers = skrub.var("customers")
y = skrub.var("y")

# Compose the graph with familiar pandas-like syntax
enriched = orders.skb.join(customers, on="customer_id", how="left")
features = enriched.skb.apply(skrub.TableVectorizer())
predictions = features.skb.apply(HistGradientBoostingClassifier(), y=y)

# Turn the graph into a deployable learner
learner = predictions.skb.make_learner()
learner.fit({"orders": orders_df, "customers": customers_df, "y": y_train})
learner.predict({"orders": new_orders_df, "customers": customers_df})

The important property: the graph carries provenance. When you serialise a SkrubLearner with joblib, you also serialise the join keys, the vectorizer state, and the estimator parameters. Redeploying the model in production means calling predict() with a dict of the same variable names. No manual pipeline rebuilding.

Skrub vs feature-engine vs category_encoders

Skrub doesn't replace the entire tabular-preprocessing ecosystem; it occupies a specific niche. Here's how the three most common contenders compare on the dimensions that actually matter when you choose one:

Dimensionskrub 0.9feature-engine 1.8category_encoders 2.6
Primary abstractionAuto-configured pipelineGranular transformersEncoder catalog
Dirty / typo-heavy stringsExcellent (Gap / Min-Hash / String)Not addressedNot addressed
Classic encoders (Target, WoE, Helmert)Deliberately omittedSome (WoE, Ordinal, Mean)Full catalog
Multi-table fuzzy joinsYes (Joiner family)NoNo
Interactive EDAYes (TableReport)NoNo
Learning curveVery lowMedium (many classes)Low
Polars-nativeYes (0.7+)PartialNo

The pragmatic rule I use: reach for skrub first for any new tabular problem, especially if there are string columns or multiple tables. Add feature-engine when you need surgical control (outlier capping, discretisation, feature selection wrappers). Add category_encoders when a specific encoder (Target with cross-fitting, Weight of Evidence, Helmert) is genuinely the right choice for a linear or logistic model, which is a narrower situation than most tutorials suggest.

Is skrub production-ready?

Yes, with two caveats. On the "ready" side: skrub is marked Production/Stable on PyPI, it follows semantic versioning, its API has been remarkably stable across the 0.7 → 0.9 releases (mostly additions, no silent behaviour changes on the core transformers), and it's maintained by a team of full-time engineers at Probabl with backing from INRIA. It's currently a dependency in production pipelines at a growing number of European ML teams.

Now the caveats. First, the 1.0 milestone isn't yet released; expect one or two more small API tweaks before it lands. The recent tabular_learnertabular_pipeline rename is representative. Pin to a minor version ("skrub==0.9.*") and read the release notes when you upgrade. Second, DataOps is new; treat the graduation from experimental to stable as "reasonably safe" rather than "battle-tested", and validate on a real dataset before using it as the backbone of a training-serving skew audit.

For serving, the standard pattern still applies: pair skrub's SkrubLearner (or plain sklearn Pipeline) with a modern serving stack. Our overview of Python model serving frameworks covers the trade-offs; BentoML and Ray Serve both accept skrub-produced pipelines directly via their sklearn integrations.

For deeper reading, the official skrub documentation is well written and includes runnable examples, and the skrub GitHub releases page is the canonical source for changelog notes. The theoretical grounding for the string encoders is in Cerda & Varoquaux's 2020 IEEE TKDE paper on encoding high-cardinality string categorical variables, worth reading if you want to defend an encoder choice in a design review.

Frequently Asked Questions

What does skrub do in Python?

Skrub automates the preprocessing step between a messy pandas or Polars DataFrame and a scikit-learn estimator. It inspects each column, picks appropriate encoders for numeric, categorical, string, and datetime data, and can also perform fuzzy joins across tables, all as sklearn-compatible transformers safe under cross-validation.

How is skrub different from pandas?

Pandas is a data-manipulation library; skrub is a preprocessing and feature-engineering library that sits on top of it. Skrub's transformers are stateful sklearn objects that learn parameters at fit time and re-apply them at transform time, which pandas' one-shot functions like get_dummies can't do safely inside a cross-validation loop.

Is skrub production-ready?

Yes. Skrub 0.9 is marked Production/Stable on PyPI, follows semantic versioning, and is maintained by the INRIA / Probabl team behind scikit-learn. Pin to a minor version (skrub==0.9.*) and read release notes on upgrade. The API is stable but not yet frozen at 1.0.

Which encoders does skrub provide for categorical data?

Skrub ships StringEncoder (tf-idf n-grams plus truncated SVD, the default for high-cardinality columns since 0.6), GapEncoder (interpretable topic-model over n-grams), MinHashEncoder (fast locality-sensitive hashing for very high cardinality), and DatetimeEncoder. Low-cardinality columns still use scikit-learn's OneHotEncoder.

What is tabular_pipeline in skrub?

tabular_pipeline(estimator) returns a fully wired scikit-learn Pipeline combining a TableVectorizer, optional imputer and scaler, and the estimator you passed. The intermediate steps are chosen automatically to match the estimator's needs, so no scaler for tree models, standard scaling for linear ones. It replaces the older tabular_learner name from earlier releases.

What are skrub DataOps?

DataOps is a declarative API introduced in skrub 0.9 for building multi-input transformation graphs. You declare inputs as skrub.var objects, compose transformations with a pandas-like syntax, and then export the graph as a SkrubLearner, a dict-input, sklearn-style estimator that supports hyperparameter tuning and pickle-based deployment.

Dr. Elena Vasquez
About the Author Dr. Elena Vasquez

Data scientist with a PhD in computational statistics. Translates papers into pandas one notebook at a time.