End-to-end example
pretab is most useful as the feature layer in front of a model. This walkthrough builds the same small regression task twice: once with plain scaling and once with pretab, using the same linear model both times. The only thing that changes is how the raw columns are turned into features, which makes the effect of pretab easy to see.
The dataset
We simulate a tabular dataset with three numeric columns and one categorical column.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
rng = np.random.default_rng(0)
n = 4000
age = rng.uniform(18, 70, n)
income = rng.normal(60_000, 15_000, n)
tenure = rng.uniform(0, 40, n)
city = rng.choice(["Berlin", "Munich", "Hamburg", "Cologne"], n)
# The target depends on each feature in a *nonlinear* way.
city_effect = pd.Series(city).map(
{"Berlin": 5.0, "Munich": 8.0, "Hamburg": 3.0, "Cologne": 6.0}
).to_numpy()
target = (
12 * np.sin(age / 8) # wave in age
+ 0.00004 * (income - 60_000) ** 2 / 1000 # quadratic in income
+ np.sqrt(tenure) * 3 # diminishing returns on tenure
+ city_effect # per-city offset
+ rng.normal(0, 2, n) # noise
)
df = pd.DataFrame({"age": age, "income": income, "tenure": tenure, "city": city})
X_train, X_test, y_train, y_test = train_test_split(
df, target, test_size=0.25, random_state=42
)
df.head()
age income tenure city
0 51.122008 38220.981430 31.495899 Munich
1 32.028909 61219.946532 18.424959 Cologne
2 20.130623 49018.509912 28.795777 Munich
3 18.859437 42292.103104 22.590428 Munich
4 60.290052 40930.830263 39.569339 Cologne
The target curves with age, bends quadratically with income, and flattens out with
tenure. A plain linear model only sees a single straight-line term per column, so it has
no way to represent these shapes. That is exactly the gap pretab fills.
Warning
Fit every transformer on the training split only, then apply it to the test split with
transform. Supervised expansions such as PLE and RBF read y while fitting, so fitting
them on the full dataset would leak test information and inflate your scores.
Baseline: scaling + Ridge
First, a conventional pipeline: scale the numeric columns, one-hot the categorical one, and
fit a Ridge regressor.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
from sklearn.linear_model import Ridge
from sklearn.metrics import r2_score, mean_absolute_error
baseline = ColumnTransformer([
("num", MinMaxScaler(), ["age", "income", "tenure"]),
("cat", OneHotEncoder(handle_unknown="ignore"), ["city"]),
])
X_tr = baseline.fit_transform(X_train)
X_te = baseline.transform(X_test)
model = Ridge(alpha=1.0).fit(X_tr, y_train)
pred = model.predict(X_te)
print(f"features: {X_tr.shape[1]}")
print(f"R2: {r2_score(y_test, pred):.3f}")
print(f"MAE: {mean_absolute_error(y_test, pred):.2f}")
features: 7
R2: 0.124
MAE: 11.20
With one straight-line term per numeric column, Ridge can only fit a global slope. It
misses every curve in the target, and the \(R^2\) of 0.124 is barely better than predicting
the mean.
With pretab
Now swap the scaler for a Preprocessor that gives each column an expressive basis. It uses
a B-spline for age, piecewise-linear encoding (PLE) for income, radial basis functions
for tenure, and one-hot for city. Everything else stays the same.
from pretab import Preprocessor
pre = Preprocessor(
feature_preprocessing={
"age": "bspline",
"income": "ple",
"tenure": "rbf",
"city": "one-hot",
},
task="regression",
output_dim=12,
)
X_tr = pre.fit_transform(X_train, y_train, return_array=True)
X_te = pre.transform(X_test, return_array=True)
model = Ridge(alpha=1.0).fit(X_tr, y_train)
pred = model.predict(X_te)
print(f"features: {X_tr.shape[1]}")
print(f"R2: {r2_score(y_test, pred):.3f}")
print(f"MAE: {mean_absolute_error(y_test, pred):.2f}")
features: 41
R2: 0.968
MAE: 2.16
The data and the Ridge model are unchanged, but the expressive features let it capture the
nonlinear structure. The \(R^2\) jumps from 0.124 to 0.968 and the mean absolute error
drops from 11.20 to 2.16.
Tip
Preprocessor.transform returns a dict of feature blocks by default. When you feed a
plain estimator, call it yourself with return_array=True to get a single stacked matrix,
then hand the arrays to the model. If you would rather compose everything inside one
sklearn Pipeline, use the standalone transformers instead. See the
sklearn Pipeline tutorial.
What actually changed
The Preprocessor expands four raw columns into 41 features. Inspect the resolved layout
with get_feature_info:
pre.get_feature_info()
feature kind pipeline dim cats
----------------------------------------------------------------
age numerical imputer -> minmax -> bspline 13 -
income numerical imputer -> minmax -> ple 12 -
tenure numerical imputer -> minmax -> rbf 12 -
city categorical imputer -> onehot -> to_float 4 4
Each numeric column is imputed, scaled, then expanded into a basis that a linear model can
weight independently: 13 spline coefficients for age, 12 PLE bins for income, and 12 RBF
bumps for tenure, while city becomes four one-hot columns. The model is unchanged, and
only the representation improved.
Next steps
Do the same for a classifier in the classification tutorial.
Wire pretab transformers into a full
sklearnPipelinewith cross-validation and grid search in the sklearn Pipeline tutorial.Review every strategy string in the User Guide.