Skip to content

How to Compose Feature Pipelines

This guide shows you how to combine transformers into feature engineering pipelines using FeaturePipeline, FeatureUnion, and ColumnTransformer. Use this when a single transformer is not enough and you need sequential processing, parallel feature branches, or both.

Prerequisites

Try it interactively

How to Compose Features with FeatureUnion

Combine lag features, rolling statistics, EMA, and scaling in parallel with FeatureUnion and automatic observation horizon resolution.

ViewOpen in marimo
How to Build a Feature Pipeline

Nest FeaturePipeline, FeatureUnion, and DecompositionPipeline for multi-level feature engineering with trend-season-residual decomposition.

ViewOpen in marimo
How to Build Panel Feature Pipelines

Combine ColumnForecaster, FeaturePipeline, FeatureUnion, and DecompositionPipeline on panel data with per-group scoring on KDD Cup air quality.

ViewOpen in marimo

Chain Transformers Sequentially

FeaturePipeline chains transformers so that the output of each step feeds into the next. Pass a list of (name, transformer) tuples:

from yohou.compose import FeaturePipeline
from yohou.preprocessing import LagTransformer
from yohou.stationarity import SeasonalDifferencing

pipeline = FeaturePipeline([
    ("diff", SeasonalDifferencing(seasonality=12)),
    ("lags", LagTransformer(lag=[1, 2, 3])),
])

pipeline.fit(y_train)
y_transformed = pipeline.transform(y_train)

Steps execute in order: SeasonalDifferencing removes the seasonal component, then LagTransformer creates autoregressive features from the differenced series.

The pipeline's observation_horizon is the cumulative sum across all steps, since each step's output feeds into the next:

print(pipeline.observation_horizon)  # 12 + 3 = 15

Run Transformers in Parallel

FeatureUnion runs multiple transformers on the same input and concatenates their outputs column-wise:

from yohou.compose import FeatureUnion
from yohou.preprocessing import LagTransformer, RollingStatisticsTransformer

features = FeatureUnion([
    ("lags", LagTransformer(lag=[1, 3, 6, 12])),
    ("rolling", RollingStatisticsTransformer(window_size=12, statistics=["mean", "std"])),
])

features.fit(y_train)
y_features = features.transform(y_train)

Since all branches receive the same input, the union's observation_horizon is the maximum across its transformers (not the sum):

print(features.observation_horizon)  # max(12, 12) = 12

Apply Different Transformers to Different Columns

ColumnTransformer routes each column subset to a dedicated transformer, then concatenates the results. Use this instead of FeatureUnion when different columns need different treatment:

from yohou.compose import ColumnTransformer
from yohou.preprocessing import LagTransformer, RollingStatisticsTransformer

ct = ColumnTransformer(
    transformers=[
        ("lags", LagTransformer(lag=[1, 2, 3]), ["temperature"]),
        ("rolling", RollingStatisticsTransformer(window_size=7), ["humidity"]),
    ],
    remainder="drop",
)

ct.fit(y_train)
y_features = ct.transform(y_train)

Set remainder="passthrough" to keep columns not assigned to any transformer. Set it to a transformer instance to apply a default transformation to unmatched columns:

ct = ColumnTransformer(
    transformers=[
        ("rolling", RollingStatisticsTransformer(window_size=7), ["humidity"]),
    ],
    remainder=LagTransformer(lag=[1, 2, 3]),  # default for all other columns
)

Like FeatureUnion, the observation_horizon is the maximum across all column transformers (including the remainder).

Nest Sequential and Parallel Stages

Place a FeatureUnion or ColumnTransformer inside a FeaturePipeline to first apply a shared preprocessing step, then branch into parallel feature extractors:

from yohou.compose import FeaturePipeline, FeatureUnion
from yohou.preprocessing import LagTransformer, RollingStatisticsTransformer
from yohou.stationarity import SeasonalDifferencing

feature_transformer = FeaturePipeline([
    ("diff", SeasonalDifferencing(seasonality=12)),
    ("features", FeatureUnion([
        ("lags", LagTransformer(lag=[1, 3, 6, 12])),
        ("rolling", RollingStatisticsTransformer(window_size=12, statistics=["mean", "std"])),
    ])),
])

The same pattern works with ColumnTransformer when features need column-specific treatment:

from yohou.compose import FeaturePipeline, ColumnTransformer
from yohou.preprocessing import LagTransformer, RollingStatisticsTransformer

feature_transformer = FeaturePipeline([
    ("features", ColumnTransformer(
        transformers=[
            ("lags", LagTransformer(lag=[1, 3, 6]), ["temperature"]),
            ("rolling", RollingStatisticsTransformer(window_size=12), ["humidity"]),
        ],
        remainder="passthrough",
    )),
])

Pass the composed transformer to a forecaster:

from yohou.point import PointReductionForecaster
from sklearn.linear_model import Ridge

forecaster = PointReductionForecaster(
    estimator=Ridge(),
    feature_transformer=feature_transformer,
)
forecaster.fit(y_train, forecasting_horizon=12)
predictions = forecaster.predict()

Access Named Steps

Use named_steps on a FeaturePipeline and named_transformers on a FeatureUnion to inspect or retrieve individual components after construction:

pipeline = FeaturePipeline([
    ("diff", SeasonalDifferencing(seasonality=12)),
    ("lags", LagTransformer(lag=[1, 3, 6, 12])),
])

pipeline.named_steps["diff"]         # SeasonalDifferencing(seasonality=12)
pipeline.named_steps["lags"].lag     # [1, 3, 6, 12]

Bracket indexing also works by position or name:

pipeline[0]       # first step
pipeline["diff"]  # same as named_steps["diff"]
pipeline[0:1]     # slice returns a new FeaturePipeline

Tune Nested Parameters

Both FeaturePipeline and FeatureUnion support get_params / set_params with double-underscore notation for nested access:

feature_transformer.set_params(features__lags__lag=[1, 2, 3])
feature_transformer.get_params()["features__lags__lag"]  # [1, 2, 3]

This integrates with hyperparameter search. See How to Tune Hyperparameters for details.

Use Pipelines with Panel Data

Pipelines work with panel data automatically. When column names use the __ naming convention, each transformer applies independently to each group's columns. No special configuration is needed:

forecaster = PointReductionForecaster(
    estimator=Ridge(),
    feature_transformer=feature_transformer,
)
# Transformers apply per group automatically
forecaster.fit(y_panel, forecasting_horizon=12)

For background on panel data conventions and setup, see How to Work with Panel Data.

See Also