BitBullet:Lessons — 04: Data Transformations¶
A Deep Dive into bitbullet.transform¶
Raw data does not feed models cleanly. A feature with extreme right skew pushes tree splits to the extremes. An unscaled column ranging [0, 1,000,000] next to one ranging [0, 1] biases distance-based algorithms. A categorical column with 50 unique values explodes into 50 one-hot columns unless you have a smarter strategy.
bitbullet.transform is built around one critical constraint: transformers must be fit only on training data and applied identically to test data. Violating this rule is one of the most common sources of optimistic, non-reproducible models in the industry.
Dataset: default_of_credit_card_clients.xls — 30,000 Taiwanese credit card clients, 23 features after cleaning, binary target: default_payment.
Source: UCI ML Repository — Default of Credit Card Clients
Context: Predict whether a client will default on next month's payment based on credit limit, repayment history, bill statements, and demographics.
Notebook Roadmap¶
| Section | Topic |
|---|---|
| 2 | Setup and imports |
| 3 | Load and clean data |
| 4 | EDA with generate_feature_stats |
| 5 | The pipeline mental model |
| 6 | Train/test split |
| 7a–7e | Numerical transforms: log/power, scaling, quantile, winsorize, binning |
| 8a–8f | Categorical transforms: label, one-hot, frequency, target, rare-bin, hash |
| 9 | Composing a production pipeline |
| 10 | fit_transform on train |
| 11 | Save, reload, transform test set |
| 12 | Pipeline management: list, disable, enable |
| 13 | Visualising transformations |
| 14 | Pipeline metadata inspection |
| 15 | Conclusion |
Let a guided workflow carry reviewed transformations through to training. BitBullet Platform centralises datasets, profiles, managed compute, storage, and modelling work in one place. Configure and review transformations with guided controls or AI-assisted drafting, compare completed results, and export the fitted preprocessing with the trained result, metadata, and generated inference code. This lesson provides the same transformation discipline directly in the BitBullet SDK.
2. Setup and Imports¶
import sys
import os
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
# When running this notebook from bitbullet/lessons in a local clone, prefer the local SDK source.
sdk_root = os.path.abspath("..")
if sdk_root not in sys.path:
sys.path.insert(0, sdk_root)
from bitbullet.transform import (
TransformPipeline,
BaseTransformer,
TransformConfig,
NumericalTransformer,
CategoricalTransformer,
DateTimeTransformer,
generate_feature_stats,
)
from bitbullet.transform.eda import plot_pipeline_transformations
import bitbullet
print(f"bitbullet version : {getattr(bitbullet, '__version__', 'dev')}")
print("All imports successful.")
plt.rcParams.update({
'figure.facecolor': 'white',
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'grid.alpha': 0.3,
'font.size': 11,
})
BLUE = '#2563EB'
ORANGE = '#F59E0B'
GREEN = '#10B981'
RED = '#EF4444'
GREY = '#9CA3AF'
bitbullet version : dev All imports successful.
3. Load and Clean the Dataset¶
Dataset: Default of Credit Card Clients Dataset
Source: UCI ML Repository — Default of Credit Card Clients
File: default_of_credit_card_clients.xls
Before running this cell: download
default_of_credit_card_clients.xlsfrom the link above and place it in the same directory as this notebook. If you store it elsewhere, updatedata_pathin the code cell below to match your chosen location.
# Update this path if you stored the file in a different location.
data_path = "default_of_credit_card_clients.xls"
# The XLS file has a descriptive first row — header is on row 1 (0-indexed)
df_raw = pd.read_excel(data_path, header=1)
print(f"Raw shape: {df_raw.shape}")
print(f"Columns: {list(df_raw.columns)}")
Raw shape: (30000, 25) Columns: ['ID', 'LIMIT_BAL', 'SEX', 'EDUCATION', 'MARRIAGE', 'AGE', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6', 'BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6', 'PAY_AMT1', 'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6', 'default payment next month']
# Drop the ID column — it is a row identifier with no predictive value
df = df_raw.drop(columns=['ID'])
# Rename target to a clean identifier
df = df.rename(columns={'default payment next month': 'default_payment'})
# Map integer-coded categorical columns to descriptive strings
df['SEX'] = df['SEX'].map({1: 'Male', 2: 'Female'})
df['EDUCATION'] = df['EDUCATION'].map({
1: 'Graduate', 2: 'University', 3: 'High_School', 4: 'Other',
5: 'Other', 6: 'Other', 0: 'Other' # values 0, 5, 6 are undocumented — treat as Other
})
df['MARRIAGE'] = df['MARRIAGE'].map({
1: 'Married', 2: 'Single', 3: 'Other', 0: 'Other'
})
TARGET = 'default_payment'
print(f"Dataset shape (after cleaning): {df.shape}")
print(f"\nTarget distribution:")
counts = df[TARGET].value_counts()
print(f" No default (0) : {counts[0]:,} ({counts[0]/len(df)*100:.1f}%)")
print(f" Default (1) : {counts[1]:,} ({counts[1]/len(df)*100:.1f}%)")
print()
display(df.head())
Dataset shape (after cleaning): (30000, 24) Target distribution: No default (0) : 23,364 (77.9%) Default (1) : 6,636 (22.1%)
| LIMIT_BAL | SEX | EDUCATION | MARRIAGE | AGE | PAY_0 | PAY_2 | PAY_3 | PAY_4 | PAY_5 | ... | BILL_AMT4 | BILL_AMT5 | BILL_AMT6 | PAY_AMT1 | PAY_AMT2 | PAY_AMT3 | PAY_AMT4 | PAY_AMT5 | PAY_AMT6 | default_payment | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20000 | Female | University | Married | 24 | 2 | 2 | -1 | -1 | -2 | ... | 0 | 0 | 0 | 0 | 689 | 0 | 0 | 0 | 0 | 1 |
| 1 | 120000 | Female | University | Single | 26 | -1 | 2 | 0 | 0 | 0 | ... | 3272 | 3455 | 3261 | 0 | 1000 | 1000 | 1000 | 0 | 2000 | 1 |
| 2 | 90000 | Female | University | Single | 34 | 0 | 0 | 0 | 0 | 0 | ... | 14331 | 14948 | 15549 | 1518 | 1500 | 1000 | 1000 | 1000 | 5000 | 0 |
| 3 | 50000 | Female | University | Married | 37 | 0 | 0 | 0 | 0 | 0 | ... | 28314 | 28959 | 29547 | 2000 | 2019 | 1200 | 1100 | 1069 | 1000 | 0 |
| 4 | 50000 | Male | University | Married | 57 | -1 | 0 | -1 | 0 | 0 | ... | 20940 | 19146 | 19131 | 2000 | 36681 | 10000 | 9000 | 689 | 679 | 0 |
5 rows × 24 columns
Column Glossary¶
| Column | Type | Description |
|---|---|---|
LIMIT_BAL |
numerical | Credit limit (NT dollar) |
SEX |
categorical | Male / Female |
EDUCATION |
categorical | Graduate / University / High_School / Other |
MARRIAGE |
categorical | Married / Single / Other |
AGE |
numerical | Age in years |
PAY_0–PAY_6 |
numerical | Repayment status for Sept–April (-2=no consumption, -1=paid duly, 0=revolving credit, 1–9=months delayed) |
BILL_AMT1–BILL_AMT6 |
numerical | Bill statement amount Sept–April (NT dollar) |
PAY_AMT1–PAY_AMT6 |
numerical | Previous payment amount Sept–April (NT dollar) |
default_payment |
target | 1 = defaulted next month, 0 = did not |
4. EDA with generate_feature_stats¶
X_full = df.drop(columns=[TARGET])
y_full = df[TARGET]
stats_df, numerical_cols, categorical_cols = generate_feature_stats(X_full)
print(f"Numerical ({len(numerical_cols)}): {numerical_cols}")
print(f"Categorical ({len(categorical_cols)}): {categorical_cols}")
print()
display(stats_df)
Numerical (20): ['LIMIT_BAL', 'AGE', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6', 'BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6', 'PAY_AMT1', 'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6'] Categorical (3): ['SEX', 'EDUCATION', 'MARRIAGE']
| dtype | missing_count | missing_percent | unique_values | zero_count | negative_count | mean | std | skewness | kurtosis | mean_percentile | min | 25% | 50% | 75% | max | top | freq | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LIMIT_BAL | int64 | 0 | 0.0 | 81 | 0 | 0 | 167484.322667 | 129747.661567 | 0.992867 | 0.536263 | 56.98 | 10000.0 | 50000.0 | 140000.0 | 240000.0 | 1000000.0 | - | - |
| SEX | object | 0 | 0.0 | 2 | 0 | 0 | - | - | - | - | - | - | - | - | - | - | Female | 18112 |
| EDUCATION | object | 0 | 0.0 | 4 | 0 | 0 | - | - | - | - | - | - | - | - | - | - | University | 14030 |
| MARRIAGE | object | 0 | 0.0 | 3 | 0 | 0 | - | - | - | - | - | - | - | - | - | - | Single | 15964 |
| AGE | int64 | 0 | 0.0 | 56 | 0 | 0 | 35.4855 | 9.217904 | 0.732246 | 0.044303 | 56.03 | 21.0 | 28.0 | 34.0 | 41.0 | 79.0 | - | - |
| PAY_0 | int64 | 0 | 0.0 | 11 | 14737 | 8445 | -0.0167 | 1.123802 | 0.731975 | 2.720715 | 28.15 | -2.0 | -1.0 | 0.0 | 0.0 | 8.0 | - | - |
| PAY_2 | int64 | 0 | 0.0 | 11 | 15730 | 9832 | -0.133767 | 1.197186 | 0.790565 | 1.570418 | 32.773333 | -2.0 | -1.0 | 0.0 | 0.0 | 8.0 | - | - |
| PAY_3 | int64 | 0 | 0.0 | 11 | 15764 | 10023 | -0.1662 | 1.196868 | 0.840682 | 2.084436 | 33.41 | -2.0 | -1.0 | 0.0 | 0.0 | 8.0 | - | - |
| PAY_4 | int64 | 0 | 0.0 | 11 | 16455 | 10035 | -0.220667 | 1.169139 | 0.999629 | 3.496983 | 33.45 | -2.0 | -1.0 | 0.0 | 0.0 | 8.0 | - | - |
| PAY_5 | int64 | 0 | 0.0 | 10 | 16947 | 10085 | -0.2662 | 1.133187 | 1.008197 | 3.989748 | 33.616667 | -2.0 | -1.0 | 0.0 | 0.0 | 8.0 | - | - |
| PAY_6 | int64 | 0 | 0.0 | 10 | 16286 | 10635 | -0.2911 | 1.149988 | 0.948029 | 3.426534 | 35.45 | -2.0 | -1.0 | 0.0 | 0.0 | 8.0 | - | - |
| BILL_AMT1 | int64 | 0 | 0.0 | 22723 | 2008 | 590 | 51223.3309 | 73635.860576 | 2.663861 | 9.806289 | 69.516667 | -165580.0 | 3558.75 | 22381.5 | 67091.0 | 964511.0 | - | - |
| BILL_AMT2 | int64 | 0 | 0.0 | 22346 | 2506 | 669 | 49179.075167 | 71173.768783 | 2.705221 | 10.302946 | 68.426667 | -69777.0 | 2984.75 | 21200.0 | 64006.25 | 983931.0 | - | - |
| BILL_AMT3 | int64 | 0 | 0.0 | 22026 | 2870 | 655 | 47013.1548 | 69349.387427 | 3.08783 | 19.783255 | 68.073333 | -157264.0 | 2666.25 | 20088.5 | 60164.75 | 1664089.0 | - | - |
| BILL_AMT4 | int64 | 0 | 0.0 | 21548 | 3195 | 675 | 43262.948967 | 64332.856134 | 2.821965 | 11.309325 | 68.856667 | -170000.0 | 2326.75 | 19052.0 | 54506.0 | 891586.0 | - | - |
| BILL_AMT5 | int64 | 0 | 0.0 | 21010 | 3506 | 655 | 40311.400967 | 60797.15577 | 2.87638 | 12.305881 | 69.696667 | -81334.0 | 1763.0 | 18104.5 | 50190.5 | 927171.0 | - | - |
| BILL_AMT6 | int64 | 0 | 0.0 | 20604 | 4020 | 688 | 38871.7604 | 59554.107537 | 2.846645 | 12.270705 | 69.846667 | -339603.0 | 1256.0 | 17071.0 | 49198.25 | 961664.0 | - | - |
| PAY_AMT1 | int64 | 0 | 0.0 | 7943 | 5249 | 0 | 5663.5805 | 16563.280354 | 14.668364 | 415.254743 | 77.59 | 0.0 | 1000.0 | 2100.0 | 5006.0 | 873552.0 | - | - |
| PAY_AMT2 | int64 | 0 | 0.0 | 7899 | 5396 | 0 | 5921.1635 | 23040.870402 | 30.453817 | 1641.631911 | 79.133333 | 0.0 | 833.0 | 2009.0 | 5000.0 | 1684259.0 | - | - |
| PAY_AMT3 | int64 | 0 | 0.0 | 7518 | 5968 | 0 | 5225.6815 | 17606.96147 | 17.216635 | 564.311229 | 79.873333 | 0.0 | 390.0 | 1800.0 | 4505.0 | 896040.0 | - | - |
| PAY_AMT4 | int64 | 0 | 0.0 | 6937 | 6408 | 0 | 4826.076867 | 15666.159744 | 12.904985 | 277.333768 | 77.683333 | 0.0 | 296.0 | 1500.0 | 4013.25 | 621000.0 | - | - |
| PAY_AMT5 | int64 | 0 | 0.0 | 6897 | 6703 | 0 | 4799.387633 | 15278.305679 | 11.127417 | 180.06394 | 77.5 | 0.0 | 252.5 | 1500.0 | 4031.5 | 426529.0 | - | - |
| PAY_AMT6 | int64 | 0 | 0.0 | 6939 | 7173 | 0 | 5215.502567 | 17777.465775 | 10.640727 | 167.16143 | 81.986667 | 0.0 | 117.75 | 1500.0 | 4000.0 | 528666.0 | - | - |
num_stats = stats_df.loc[numerical_cols, ['dtype', 'missing_percent', 'skewness', 'kurtosis',
'zero_count', 'negative_count', 'min', 'max']]
print("Numerical feature audit:")
display(num_stats)
print()
print("Categorical feature audit:")
cat_stats = stats_df.loc[categorical_cols, ['dtype', 'missing_percent', 'unique_values', 'top', 'freq']]
display(cat_stats)
Numerical feature audit:
| dtype | missing_percent | skewness | kurtosis | zero_count | negative_count | min | max | |
|---|---|---|---|---|---|---|---|---|
| LIMIT_BAL | int64 | 0.0 | 0.992867 | 0.536263 | 0 | 0 | 10000.0 | 1000000.0 |
| AGE | int64 | 0.0 | 0.732246 | 0.044303 | 0 | 0 | 21.0 | 79.0 |
| PAY_0 | int64 | 0.0 | 0.731975 | 2.720715 | 14737 | 8445 | -2.0 | 8.0 |
| PAY_2 | int64 | 0.0 | 0.790565 | 1.570418 | 15730 | 9832 | -2.0 | 8.0 |
| PAY_3 | int64 | 0.0 | 0.840682 | 2.084436 | 15764 | 10023 | -2.0 | 8.0 |
| PAY_4 | int64 | 0.0 | 0.999629 | 3.496983 | 16455 | 10035 | -2.0 | 8.0 |
| PAY_5 | int64 | 0.0 | 1.008197 | 3.989748 | 16947 | 10085 | -2.0 | 8.0 |
| PAY_6 | int64 | 0.0 | 0.948029 | 3.426534 | 16286 | 10635 | -2.0 | 8.0 |
| BILL_AMT1 | int64 | 0.0 | 2.663861 | 9.806289 | 2008 | 590 | -165580.0 | 964511.0 |
| BILL_AMT2 | int64 | 0.0 | 2.705221 | 10.302946 | 2506 | 669 | -69777.0 | 983931.0 |
| BILL_AMT3 | int64 | 0.0 | 3.08783 | 19.783255 | 2870 | 655 | -157264.0 | 1664089.0 |
| BILL_AMT4 | int64 | 0.0 | 2.821965 | 11.309325 | 3195 | 675 | -170000.0 | 891586.0 |
| BILL_AMT5 | int64 | 0.0 | 2.87638 | 12.305881 | 3506 | 655 | -81334.0 | 927171.0 |
| BILL_AMT6 | int64 | 0.0 | 2.846645 | 12.270705 | 4020 | 688 | -339603.0 | 961664.0 |
| PAY_AMT1 | int64 | 0.0 | 14.668364 | 415.254743 | 5249 | 0 | 0.0 | 873552.0 |
| PAY_AMT2 | int64 | 0.0 | 30.453817 | 1641.631911 | 5396 | 0 | 0.0 | 1684259.0 |
| PAY_AMT3 | int64 | 0.0 | 17.216635 | 564.311229 | 5968 | 0 | 0.0 | 896040.0 |
| PAY_AMT4 | int64 | 0.0 | 12.904985 | 277.333768 | 6408 | 0 | 0.0 | 621000.0 |
| PAY_AMT5 | int64 | 0.0 | 11.127417 | 180.06394 | 6703 | 0 | 0.0 | 426529.0 |
| PAY_AMT6 | int64 | 0.0 | 10.640727 | 167.16143 | 7173 | 0 | 0.0 | 528666.0 |
Categorical feature audit:
| dtype | missing_percent | unique_values | top | freq | |
|---|---|---|---|---|---|
| SEX | object | 0.0 | 2 | Female | 18112 |
| EDUCATION | object | 0.0 | 4 | University | 14030 |
| MARRIAGE | object | 0.0 | 3 | Single | 15964 |
Reading the Stats Table — Key Observations for This Dataset¶
PAY_AMT1–PAY_AMT6: extreme right skew (> 10). Clients who pay large lump sums dominate the tail.log1pis the right choice — safe for zeros, compresses the right tail.BILL_AMT1–BILL_AMT6: moderate-to-high right skew, and some negative values (credit balance).yeo_johnsonhandles negatives thatlogandlog1pcannot.LIMIT_BAL: right-skewed credit limits.log1preduces the long tail.AGE: roughly normal —standard_scaleis appropriate.PAY_0–PAY_6: ordinal integer repayment status (-2 to 9). Treat as numerical;robust_scaleis appropriate for this bounded ordinal range.SEX,EDUCATION,MARRIAGE: three low-cardinality categoricals, 2–4 unique values each.label_encodeworks for tree models;onehot_encodefor linear models.
5. The Pipeline Mental Model¶
Before writing a single transform, we internalise one rule:
Transformers must be fit exclusively on training data. They must then be applied — without refitting — to validation and test data.
What TransformPipeline Remembers After Fitting¶
| Transform | Fitted Parameters Stored |
|---|---|
standard_scale |
mean, std per column |
robust_scale |
median, IQR per column |
yeo_johnson |
PowerTransformer object per column |
log1p |
Stateless — no fitting required |
winsorize |
Lower and upper clip thresholds per column |
label_encode |
LabelEncoder object per column |
onehot_encode |
OneHotEncoder object per column |
target_encode |
Leakage-aware target statistics: OOF values from fit_transform(..., y=target) for training, full-data smoothed map for transform(...) at inference |
6. Train/Test Split¶
X_train, X_test, y_train, y_test = train_test_split(
X_full, y_full, test_size=0.20, random_state=42, stratify=y_full
)
print(f"Training set : {X_train.shape[0]:,} samples (default rate: {y_train.mean():.3f})")
print(f"Test set : {X_test.shape[0]:,} samples (default rate: {y_test.mean():.3f})")
print()
print("Test set is sealed. No fitting step will touch it until final evaluation.")
Training set : 24,000 samples (default rate: 0.221) Test set : 6,000 samples (default rate: 0.221) Test set is sealed. No fitting step will touch it until final evaluation.
7. Numerical Transformations¶
7a. Logarithmic and Power Transforms: log1p, yeo_johnson, sqrt, boxcox¶
Decision rule for this dataset:
PAY_AMTcolumns: zeros present, right-skewed →log1pBILL_AMTcolumns: possible negatives, right-skewed →yeo_johnsonLIMIT_BAL: strictly positive, right-skewed →log1p
fig, axes = plt.subplots(2, 4, figsize=(18, 9))
fig.suptitle("Log / Power Transforms — Before vs After", fontsize=14, fontweight='bold')
transforms_demo = [
('PAY_AMT1', 'log1p', BLUE, GREEN),
('PAY_AMT2', 'log1p', BLUE, GREEN),
('BILL_AMT1','yeo_johnson', BLUE, ORANGE),
('LIMIT_BAL','log1p', BLUE, ORANGE),
]
results_power = {}
for col, method, c1, c2 in transforms_demo:
cfg = TransformConfig(name=f"{method}_{col}", transformer_type="numerical",
method=method, columns=[col], params={})
t = NumericalTransformer(cfg)
results_power[col] = t.fit_transform(X_train)
for i, (col, method, c1, c2) in enumerate(transforms_demo):
before = X_train[col]
after = results_power[col][col]
axes[0, i].hist(before.dropna(), bins=40, color=c1, alpha=0.8)
axes[0, i].set_title(f"`{col}`\n(before)", fontsize=10, fontweight='bold')
axes[0, i].text(0.97, 0.95, f'skew={before.skew():.2f}', transform=axes[0, i].transAxes,
ha='right', va='top', fontsize=9,
bbox=dict(boxstyle='round,pad=0.3', facecolor='white', edgecolor='grey', alpha=0.8))
axes[1, i].hist(after.dropna(), bins=40, color=c2, alpha=0.8)
axes[1, i].set_title(f"`{method}`\n(after)", fontsize=10, fontweight='bold')
axes[1, i].text(0.97, 0.95, f'skew={after.skew():.2f}', transform=axes[1, i].transAxes,
ha='right', va='top', fontsize=9,
bbox=dict(boxstyle='round,pad=0.3', facecolor='white', edgecolor='grey', alpha=0.8))
plt.tight_layout()
plt.show()
print("Skewness reduction summary:")
for col, method, _, _ in transforms_demo:
before_skew = X_train[col].skew()
after_skew = results_power[col][col].skew()
print(f" {col:15s} [{method:12s}] {before_skew:+.3f} \u2192 {after_skew:+.3f}")
Skewness reduction summary: PAY_AMT1 [log1p ] +15.312 → -1.299 PAY_AMT2 [log1p ] +21.124 → -1.241 BILL_AMT1 [yeo_johnson ] +2.691 → -2.029 LIMIT_BAL [log1p ] +1.000 → -0.517
7b. Scaling Transforms: standard_scale, robust_scale¶
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
fig.suptitle("Scaling Transforms on `AGE`", fontsize=13, fontweight='bold')
axes[0].hist(X_train['AGE'], bins=30, color=GREY, alpha=0.8)
axes[0].set_title(f"Original\nrange [{X_train['AGE'].min():.0f}, {X_train['AGE'].max():.0f}]", fontweight='bold')
for i, (method, params, color) in enumerate([
('standard_scale', {}, BLUE),
('robust_scale', {}, ORANGE),
], start=1):
cfg = TransformConfig(name=f"{method}_age", transformer_type="numerical",
method=method, columns=["AGE"], params=params)
t = NumericalTransformer(cfg)
result = t.fit_transform(X_train)['AGE']
axes[i].hist(result, bins=30, color=color, alpha=0.8)
axes[i].set_title(f"`{method}`\nrange [{result.min():.2f}, {result.max():.2f}]", fontweight='bold')
axes[i].set_xlabel('Scaled value')
plt.tight_layout()
plt.show()
# Show outlier effect on PAY_0 (can range from -2 to 9)
print("Repayment status (PAY_0) range in training data:")
print(f" min={X_train['PAY_0'].min()}, max={X_train['PAY_0'].max()}, unique values: {sorted(X_train['PAY_0'].unique())}")
print("\nUsing robust_scale for PAY columns — resistant to extreme delay values (8, 9 months).")
Repayment status (PAY_0) range in training data: min=-2, max=8, unique values: [np.int64(-2), np.int64(-1), np.int64(0), np.int64(1), np.int64(2), np.int64(3), np.int64(4), np.int64(5), np.int64(6), np.int64(7), np.int64(8)] Using robust_scale for PAY columns — resistant to extreme delay values (8, 9 months).
7c. Quantile Transforms: quantile_normal, quantile_uniform¶
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
fig.suptitle("Quantile Transforms on `PAY_AMT1`", fontsize=13, fontweight='bold')
axes[0].hist(X_train['PAY_AMT1'], bins=50, color=GREY, alpha=0.8)
axes[0].set_title(f"Original\nskew = {X_train['PAY_AMT1'].skew():.2f}", fontweight='bold')
for i, (method, color, label) in enumerate([
('quantile_normal', BLUE, 'quantile_normal\n\u2192 Gaussian output'),
('quantile_uniform', ORANGE, 'quantile_uniform\n\u2192 Uniform [0,1] output'),
], start=1):
cfg = TransformConfig(name=f"{method}_pamt1", transformer_type="numerical",
method=method, columns=["PAY_AMT1"], params={"n_quantiles": 1000})
t = NumericalTransformer(cfg)
result = t.fit_transform(X_train)['PAY_AMT1']
axes[i].hist(result, bins=50, color=color, alpha=0.8)
axes[i].set_title(f"{label}\nskew = {result.skew():.4f}", fontweight='bold')
axes[i].set_xlabel('Transformed value')
plt.tight_layout()
plt.show()
print("quantile_normal achieves near-zero skewness by construction — the most powerful normalisation available.")
quantile_normal achieves near-zero skewness by construction — the most powerful normalisation available.
7d. Winsorization: winsorize¶
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
fig.suptitle("Winsorization on `LIMIT_BAL` — Clipping Extreme Values", fontsize=13, fontweight='bold')
axes[0].boxplot(X_train['LIMIT_BAL'].dropna(), vert=True, patch_artist=True,
boxprops=dict(facecolor=GREY, alpha=0.7))
axes[0].set_title(f"Original\nmax={X_train['LIMIT_BAL'].max():,}", fontweight='bold')
axes[0].set_xticks([])
for i, (lower, upper, color) in enumerate([(0.01, 0.99, BLUE), (0.05, 0.95, ORANGE)], start=1):
cfg = TransformConfig(name=f"winsorize_{int(lower*100)}_{int(upper*100)}_bal",
transformer_type="numerical", method="winsorize",
columns=["LIMIT_BAL"], params={"lower": lower, "upper": upper})
t = NumericalTransformer(cfg)
t.fit(X_train)
result = t.transform(X_train)['LIMIT_BAL']
fitted = t.get_params()
clip_lo, clip_hi = fitted['LIMIT_BAL_lower'], fitted['LIMIT_BAL_upper']
axes[i].boxplot(result.dropna(), vert=True, patch_artist=True,
boxprops=dict(facecolor=color, alpha=0.7))
axes[i].set_title(f"Winsorize ({int(lower*100)}%–{int(upper*100)}%)\nclipped to [{clip_lo:,.0f}, {clip_hi:,.0f}]",
fontweight='bold')
axes[i].set_xticks([])
plt.tight_layout()
plt.show()
7e. Percentile Binning: percentile_binning¶
cfg_bin = TransformConfig(name="bin_age", transformer_type="numerical",
method="percentile_binning", columns=["AGE"], params={"n_bins": 4})
t_bin = NumericalTransformer(cfg_bin)
X_binned = t_bin.fit_transform(X_train)
bin_edges = t_bin.get_params()['AGE_bin_edges']
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle("Percentile Binning on `AGE`", fontsize=13, fontweight='bold')
axes[0].hist(X_train['AGE'], bins=30, color=GREY, alpha=0.8)
for edge in bin_edges[1:-1]:
axes[0].axvline(edge, color=RED, linestyle='--', lw=1.5, alpha=0.7)
axes[0].set_title('Original — dashed lines show bin edges', fontweight='bold')
axes[0].set_xlabel('AGE')
bin_counts = X_binned['AGE'].value_counts().sort_index()
axes[1].bar([str(b) for b in bin_counts.index], bin_counts.values, color=BLUE, alpha=0.8)
axes[1].set_title('After binning — approximately equal counts per bin', fontweight='bold')
axes[1].set_xlabel('Bin (0 = youngest quartile, 3 = oldest)')
axes[1].set_ylabel('Count')
plt.tight_layout()
plt.show()
print("Learned age quartile edges:")
names = ['Young (Q1)', 'Early-career (Q2)', 'Mid-career (Q3)', 'Senior (Q4)']
for name, (lo, hi) in zip(names, [(bin_edges[i], bin_edges[i+1]) for i in range(len(bin_edges)-1)]):
print(f" {name}: age [{lo:.0f}, {hi:.0f}) years")
Learned age quartile edges: Young (Q1): age [21, 28) years Early-career (Q2): age [28, 34) years Mid-career (Q3): age [34, 41) years Senior (Q4): age [41, 75) years
8. Categorical Transformations¶
8a. Label Encoding: label_encode¶
cfg_le = TransformConfig(name="label_sex", transformer_type="categorical",
method="label_encode", columns=["SEX"],
params={"unknown_strategy": "use_encoded_value", "unknown_value": -1})
t_le = CategoricalTransformer(cfg_le)
X_le = t_le.fit_transform(X_train)
learned_classes = t_le.get_params()['SEX_classes']
print("Learned label encoding for SEX:")
for i, cls in enumerate(learned_classes):
count = (X_train['SEX'] == cls).sum()
print(f" {cls!r:15s} \u2192 {i} (n={count:,})")
Learned label encoding for SEX: 'Female' → 0 (n=14,514) 'Male' → 1 (n=9,486)
8b. One-Hot Encoding: onehot_encode¶
cfg_ohe = TransformConfig(name="ohe_education", transformer_type="categorical",
method="onehot_encode", columns=["EDUCATION"],
params={"sparse": False, "drop": None})
t_ohe = CategoricalTransformer(cfg_ohe)
X_ohe = t_ohe.fit_transform(X_train)
print(f"Columns before OHE: {X_train.shape[1]}")
print(f"Columns after OHE: {X_ohe.shape[1]}")
new_cols = [c for c in X_ohe.columns if c.startswith('EDUCATION')]
print(f"\nNew columns: {new_cols}")
for col in new_cols:
print(f" {col:30s} mean={X_ohe[col].mean():.3f}")
Columns before OHE: 23 Columns after OHE: 26 New columns: ['EDUCATION_Graduate', 'EDUCATION_High_School', 'EDUCATION_Other', 'EDUCATION_University'] EDUCATION_Graduate mean=0.352 EDUCATION_High_School mean=0.163 EDUCATION_Other mean=0.016 EDUCATION_University mean=0.469
8c. Frequency Encoding: frequency_encode¶
cfg_fe = TransformConfig(name="freq_marriage", transformer_type="categorical",
method="frequency_encode", columns=["MARRIAGE"],
params={"normalize": True})
t_fe = CategoricalTransformer(cfg_fe)
X_fe = t_fe.fit_transform(X_train)
freq_map = t_fe.get_params()['MARRIAGE_freq_map']
print("Frequency map learned from training data:")
for cat, freq in sorted(freq_map.items(), key=lambda x: -x[1]):
print(f" {cat:10s} \u2192 {freq:.4f} ({freq*100:.1f}% of training rows)")
Frequency map learned from training data: Single → 0.5336 (53.4% of training rows) Married → 0.4538 (45.4% of training rows) Other → 0.0126 (1.3% of training rows)
8d. Target Encoding: target_encode¶
Target encoding is supervised: the encoder learns category-to-target statistics from the training target. In BitBullet, pass the target through fit(..., y=...) or pipeline.fit_transform(..., y=...) rather than storing y inside transformer params. This keeps the config declarative and the fitted state explicit.
The upgraded SDK target encoder is leakage-aware. During fit_transform it returns out-of-fold (OOF) encodings for the training rows, so a row never sees its own target. During later transform calls it uses the full training-data smoothed map, which is the state saved with the pipeline for inference. It supports regression targets, binary classification probabilities, and multiclass one-vs-rest probability columns.
target_pipeline = TransformPipeline(name="target_encoding_example")
target_pipeline.add(
"categorical",
"target_encode",
columns=["EDUCATION"],
params={
"target_type": "classification", # binary target -> P(positive_label | category)
"positive_label": 1,
"cv_folds": 5,
"cv_strategy": "stratified",
"smoothing": 10.0,
"random_state": 42,
},
)
# Training path: out-of-fold encodings, so each row is encoded by folds that did not contain it.
X_train_te = target_pipeline.fit_transform(X_train[["EDUCATION"]], y=y_train)
# Inference path: the saved full-training-data map is applied without refitting.
X_test_te = target_pipeline.transform(X_test[["EDUCATION"]])
target_step = target_pipeline.transformers[0]
description = target_step.describe()
audit = pd.DataFrame({
"education": X_train["EDUCATION"].to_numpy(),
"target": y_train.to_numpy(),
"oof_encoded": X_train_te["EDUCATION"].to_numpy(),
})
summary = (
audit.groupby("education")
.agg(
n=("target", "size"),
raw_default_rate=("target", "mean"),
mean_oof_encoding=("oof_encoded", "mean"),
)
.sort_values("mean_oof_encoding", ascending=False)
)
print("Target encoder audit:")
print(f" mode: {description['mode']}")
print(f" fit_scope: {description['fit_scope']}")
print(f" output: {description['output_columns_by_input']}")
print(f" test dtype: {X_test_te['EDUCATION'].dtype}")
display(summary)
print("\nMulticlass target encoding expands one source column into one probability channel per class:")
toy_X = pd.DataFrame({
"merchant_segment": ["grocery", "grocery", "fuel", "fuel", "travel", "travel", "grocery", "fuel", "travel", "other", "other", "other"],
})
toy_y = pd.Series(["approve", "review", "approve", "reject", "review", "reject", "approve", "review", "reject", "approve", "review", "reject"], name="decision")
multi_pipeline = TransformPipeline(name="multiclass_target_encoding_example")
multi_pipeline.add(
"categorical",
"target_encode",
columns=["merchant_segment"],
params={"target_type": "classification", "cv_folds": 3, "cv_strategy": "stratified", "smoothing": 2.0},
)
toy_encoded = multi_pipeline.fit_transform(toy_X, y=toy_y)
print([col for col in toy_encoded.columns if col.startswith("merchant_segment")])
8e. Rare Category Binning: bin_rare¶
print("EDUCATION distribution:")
edu_dist = X_train['EDUCATION'].value_counts(normalize=True)
for cat, freq in edu_dist.items():
print(f" {cat:15s}: {freq:.3f} ({freq*100:.1f}%)")
print()
for threshold in [0.01, 0.10, 0.20]:
cfg_br = TransformConfig(name=f"bin_rare_{threshold}", transformer_type="categorical",
method="bin_rare", columns=["EDUCATION"],
params={"threshold": threshold, "other_label": "other"})
t_br = CategoricalTransformer(cfg_br)
X_br = t_br.fit_transform(X_train)
rare_cats = t_br.get_params()['EDUCATION_rare_categories']
print(f" threshold={threshold:.2f}: {len(rare_cats)} merged \u2192 {X_br['EDUCATION'].nunique()} unique remain. Merged: {rare_cats}")
EDUCATION distribution: University : 0.469 (46.9%) Graduate : 0.352 (35.2%) High_School : 0.163 (16.3%) Other : 0.016 (1.6%) threshold=0.01: 0 merged → 4 unique remain. Merged: [] threshold=0.10: 1 merged → 4 unique remain. Merged: ['Other'] threshold=0.20: 2 merged → 3 unique remain. Merged: ['High_School', 'Other']
8f. Feature Hashing: hash_encode¶
print("MARRIAGE unique values:", X_train['MARRIAGE'].unique())
for n_features in [4, 8, 16]:
cfg_hash = TransformConfig(name=f"hash_{n_features}", transformer_type="categorical",
method="hash_encode", columns=["MARRIAGE"],
params={"n_features": n_features})
t_hash = CategoricalTransformer(cfg_hash)
t_hash.fit(X_train)
unique_cats = X_train['MARRIAGE'].unique()
hash_mapping = {cat: hash(str(cat)) % n_features for cat in unique_cats}
collisions = len(unique_cats) - len(set(hash_mapping.values()))
print(f" n_features={n_features:3d}: {collisions} collision(s) among {len(unique_cats)} categories: {hash_mapping}")
MARRIAGE unique values: ['Single' 'Married' 'Other']
n_features= 4: 0 collision(s) among 3 categories: {'Single': 2, 'Married': 3, 'Other': 0}
n_features= 8: 0 collision(s) among 3 categories: {'Single': 2, 'Married': 7, 'Other': 0}
n_features= 16: 0 collision(s) among 3 categories: {'Single': 2, 'Married': 7, 'Other': 0}
9. Composing a Production Pipeline¶
We now build a complete TransformPipeline for the credit card default dataset, guided by the EDA in Section 4:
| Feature Group | Transform | Reasoning |
|---|---|---|
PAY_AMT1–PAY_AMT6 |
log1p |
Extreme right skew, non-negative, many zeros |
BILL_AMT1–BILL_AMT6 |
yeo_johnson |
Right skew, possible negative values (credit balance) |
LIMIT_BAL |
log1p |
Strictly positive, right-skewed credit limit |
AGE |
standard_scale |
Roughly normal, no outlier concern |
PAY_0–PAY_6 |
robust_scale |
Ordinal integers, some extreme delay values (8, 9) |
SEX, MARRIAGE |
label_encode |
Low cardinality (2–3 values), tree models |
EDUCATION |
onehot_encode |
4 categories, slightly higher cardinality |
pay_amt_cols = [f'PAY_AMT{i}' for i in range(1, 7)]
bill_amt_cols = [f'BILL_AMT{i}' for i in range(1, 7)]
pay_cols = ['PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6']
binary_cats = ['SEX', 'MARRIAGE']
ohe_cats = ['EDUCATION']
default_pipeline = (
TransformPipeline(name="credit_default_pipeline")
# ── Numerical transforms ────────────────────────────────────────────────
.add("numerical", "log1p",
columns=pay_amt_cols + ['LIMIT_BAL'],
name="log1p_payment_amounts")
.add("numerical", "yeo_johnson",
columns=bill_amt_cols,
name="yj_bill_amounts")
.add("numerical", "standard_scale",
columns=["AGE"],
name="standard_scale_age")
.add("numerical", "robust_scale",
columns=pay_cols,
name="robust_scale_pay_status")
# ── Categorical transforms ──────────────────────────────────────────────
.add("categorical", "label_encode",
columns=binary_cats,
params={"unknown_strategy": "use_encoded_value", "unknown_value": -1},
name="label_encode_binary_cats")
.add("categorical", "onehot_encode",
columns=ohe_cats,
params={"sparse": False, "drop": None},
name="onehot_education")
)
print(default_pipeline)
print(f"\nTotal steps: {len(default_pipeline)}")
TransformPipeline(name='credit_default_pipeline', steps=6, status=not fitted) Total steps: 6
10. fit_transform on Training Data¶
X_train_t = default_pipeline.fit_transform(X_train, verbose=True)
print()
print(f"Input shape : {X_train.shape}")
print(f"Output shape : {X_train_t.shape}")
print(f"Pipeline fitted: {default_pipeline.is_fitted}")
print()
print("Columns AFTER transformation:")
print(list(X_train_t.columns))
Fitting [1/6]: log1p_payment_amounts Fitting [2/6]: yj_bill_amounts Fitting [3/6]: standard_scale_age Fitting [4/6]: robust_scale_pay_status Fitting [5/6]: label_encode_binary_cats Fitting [6/6]: onehot_education Transforming [1/6]: log1p_payment_amounts Transforming [2/6]: yj_bill_amounts Transforming [3/6]: standard_scale_age Transforming [4/6]: robust_scale_pay_status Transforming [5/6]: label_encode_binary_cats Transforming [6/6]: onehot_education Input shape : (24000, 23) Output shape : (24000, 26) Pipeline fitted: True Columns AFTER transformation: ['LIMIT_BAL', 'SEX', 'MARRIAGE', 'AGE', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6', 'BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6', 'PAY_AMT1', 'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6', 'EDUCATION_Graduate', 'EDUCATION_High_School', 'EDUCATION_Other', 'EDUCATION_University']
print("Spot check: PAY_AMT1 (log1p)")
print(f" Before: min={X_train['PAY_AMT1'].min():.0f}, max={X_train['PAY_AMT1'].max():.0f}, skew={X_train['PAY_AMT1'].skew():.3f}")
print(f" After : min={X_train_t['PAY_AMT1'].min():.3f}, max={X_train_t['PAY_AMT1'].max():.3f}, skew={X_train_t['PAY_AMT1'].skew():.3f}")
print()
print("Spot check: BILL_AMT1 (yeo_johnson)")
print(f" Before: skew={X_train['BILL_AMT1'].skew():.3f}")
print(f" After : skew={X_train_t['BILL_AMT1'].skew():.3f}")
print()
print("Spot check: EDUCATION one-hot columns")
edu_cols = [c for c in X_train_t.columns if c.startswith('EDUCATION')]
print(f" Columns: {edu_cols}")
print(f" Row sums: {X_train_t[edu_cols].sum(axis=1).unique()}")
Spot check: PAY_AMT1 (log1p) Before: min=0, max=873552, skew=15.312 After : min=0.000, max=13.680, skew=-1.299 Spot check: BILL_AMT1 (yeo_johnson) Before: skew=2.691 After : skew=-2.029 Spot check: EDUCATION one-hot columns Columns: ['EDUCATION_Graduate', 'EDUCATION_High_School', 'EDUCATION_Other', 'EDUCATION_University'] Row sums: [1.]
11. Save, Load, and Transform the Test Set¶
pipeline_path = "credit_default_pipeline.pkl"
default_pipeline.save(pipeline_path)
file_size_kb = os.path.getsize(pipeline_path) / 1024
print(f"Pipeline saved \u2192 {pipeline_path} ({file_size_kb:.1f} KB)")
loaded_pipeline = TransformPipeline.load(pipeline_path)
print(f"Pipeline loaded: {len(loaded_pipeline)} steps | fitted: {loaded_pipeline.is_fitted}")
X_test_t = loaded_pipeline.transform(X_test)
print(f"\nTest set: {X_test.shape} \u2192 {X_test_t.shape}")
assert list(X_train_t.columns) == list(X_test_t.columns), "Column mismatch!"
print("Column alignment: PASSED")
Pipeline saved → credit_default_pipeline.pkl (2.3 KB) Pipeline loaded: 6 steps | fitted: True Test set: (6000, 23) → (6000, 26) Column alignment: PASSED
X_train_t_verify = loaded_pipeline.transform(X_train)
is_identical = X_train_t.round(10).equals(X_train_t_verify.round(10))
print(f"Reproducibility check: loaded pipeline produces {'IDENTICAL' if is_identical else 'DIFFERENT'} results")
print()
print("Distribution comparison — train vs test (post-transform):")
print(f"{'Column':15s} {'train mean':>12s} {'test mean':>12s} {'train std':>12s} {'test std':>12s}")
print('-' * 60)
for col in ['PAY_AMT1', 'BILL_AMT1', 'LIMIT_BAL', 'AGE', 'PAY_0']:
print(f"{col:15s} {X_train_t[col].mean():12.4f} {X_test_t[col].mean():12.4f} "
f"{X_train_t[col].std():12.4f} {X_test_t[col].std():12.4f}")
Reproducibility check: loaded pipeline produces IDENTICAL results Distribution comparison — train vs test (post-transform): Column train mean test mean train std test std ------------------------------------------------------------ PAY_AMT1 6.6368 6.6027 3.2440 3.2755 BILL_AMT1 13533.5819 13716.1056 18330.9054 17488.7793 LIMIT_BAL 11.6633 11.6617 0.9400 0.9456 AGE -0.0000 0.0288 1.0000 1.0120 PAY_0 -0.0141 -0.0270 1.1232 1.1264
12. Pipeline Management: list_steps, disable_step, enable_step¶
steps = default_pipeline.list_steps()
print(f"{'#':>3} {'Name':40s} {'Type':12s} {'Method':20s} {'Enabled':8s}")
print('-' * 90)
for i, step in enumerate(steps, 1):
print(f"{i:>3} {step['name']:40s} {step['type']:12s} {step['method']:20s} "
f"{'Yes' if step['enabled'] else 'No':8s}")
if step['columns']:
print(f" columns: {step['columns']}")
# Name Type Method Enabled
------------------------------------------------------------------------------------------
1 log1p_payment_amounts numerical log1p Yes
columns: ['PAY_AMT1', 'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6', 'LIMIT_BAL']
2 yj_bill_amounts numerical yeo_johnson Yes
columns: ['BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6']
3 standard_scale_age numerical standard_scale Yes
columns: ['AGE']
4 robust_scale_pay_status numerical robust_scale Yes
columns: ['PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6']
5 label_encode_binary_cats categorical label_encode Yes
columns: ['SEX', 'MARRIAGE']
6 onehot_education categorical onehot_encode Yes
columns: ['EDUCATION']
X_with_log = default_pipeline.transform(X_train)
skew_with = X_with_log['PAY_AMT1'].skew()
print(f"With log1p enabled : PAY_AMT1 skew = {skew_with:.4f}")
default_pipeline.disable_step("log1p_payment_amounts")
X_without_log = default_pipeline.transform(X_train)
skew_without = X_without_log['PAY_AMT1'].skew()
print(f"With log1p disabled : PAY_AMT1 skew = {skew_without:.4f} (raw values pass through)")
default_pipeline.enable_step("log1p_payment_amounts")
X_re_enabled = default_pipeline.transform(X_train)
print(f"After re-enabling : PAY_AMT1 skew = {X_re_enabled['PAY_AMT1'].skew():.4f}")
With log1p enabled : PAY_AMT1 skew = -1.2993 With log1p disabled : PAY_AMT1 skew = 15.3121 (raw values pass through) After re-enabling : PAY_AMT1 skew = -1.2993
13. Visualising Transformations¶
# Interactive widget — use the dropdown to explore each transformed feature
plot_pipeline_transformations(default_pipeline, X_train, X_train_t)
interactive(children=(Dropdown(description='feature', options=('AGE', 'BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', '…
fig, axes = plt.subplots(4, 2, figsize=(14, 16))
fig.suptitle("Numerical Transforms — Before vs After (Training Data)", fontsize=14, fontweight='bold')
num_pairs = [
('PAY_AMT1', 'log1p', BLUE, GREEN),
('BILL_AMT1', 'yeo_johnson', BLUE, ORANGE),
('LIMIT_BAL', 'log1p', BLUE, GREEN),
('AGE', 'standard_scale', BLUE, ORANGE),
]
for i, (col, method, c_before, c_after) in enumerate(num_pairs):
before = X_train[col]
after = X_train_t[col]
axes[i, 0].hist(before.dropna(), bins=40, color=c_before, alpha=0.75)
axes[i, 0].set_title(f"`{col}` — Original [skew={before.skew():.2f}]", fontweight='bold', fontsize=10)
axes[i, 1].hist(after.dropna(), bins=40, color=c_after, alpha=0.75)
axes[i, 1].set_title(f"`{col}` — After `{method}` [skew={after.skew():.2f}]", fontweight='bold', fontsize=10)
plt.tight_layout()
plt.show()
14. Pipeline Metadata Inspection¶
meta = default_pipeline.metadata
print("Pipeline Metadata")
print("=" * 50)
print(f" Pipeline name : {default_pipeline.name}")
print(f" Created at : {meta.created_at}")
print(f" Fitted at : {meta.fitted_at}")
print(f" Number of steps : {meta.n_transformers}")
print(f" Input shape : {meta.input_shape}")
print(f" Output shape : {meta.output_shape}")
print(f" Is fitted : {default_pipeline.is_fitted}")
print(f" Column expansion : +{meta.output_shape[1] - meta.input_shape[1]} columns (from one-hot encoding)")
Pipeline Metadata ================================================== Pipeline name : credit_default_pipeline Created at : None Fitted at : 2026-05-06T14:27:18.945569 Number of steps : 6 Input shape : (24000, 23) Output shape : (24000, 26) Is fitted : True Column expansion : +3 columns (from one-hot encoding)
15. Conclusion¶
This notebook covered bitbullet.transform applied to a real-world credit default dataset with a genuine mix of transformation needs.
What We Built¶
| You Wrote | BitBullet Handled |
|---|---|
generate_feature_stats(df) |
Full audit — skewness, kurtosis, missing values, cardinality |
pipeline.add("numerical", "log1p", ...) |
Fit on training data, store parameters, apply consistently |
pipeline.add("numerical", "yeo_johnson", ...) |
Power transformer for negative-valued BILL_AMT columns |
pipeline.fit_transform(X_train, y=y_train) when supervised encoders are present |
Sequential chaining, target-aware fitting, fit-lock after completion |
pipeline.save(path) |
Compressed serialisation of all fitted parameters |
TransformPipeline.load(path) |
Exact reconstruction — identical parameters, identical output |
loaded_pipeline.transform(X_test) |
Apply exact training parameters — zero leakage |
Quick Decision Guide¶
PAY_AMT columns (zeros, extreme right skew) → log1p
BILL_AMT columns (right skew, possible negatives) → yeo_johnson
LIMIT_BAL (strictly positive, right-skewed) → log1p
AGE (roughly normal) → standard_scale
PAY_0–PAY_6 (ordinal integer, some extremes) → robust_scale
SEX, MARRIAGE (2–3 categories, tree model) → label_encode
EDUCATION (4 categories, nominal) → onehot_encode
Continue the Academy:
05_Model_Management.ipynb— versioning, comparison, and model governance