Building Reusable Feature Pipelines: An Expert Guide
Feature engineering is perhaps the single most important factor in building successful machine learning models. While much attention is given to choosing the right model architecture or tuning hyperparameters, the reality is that the quality of the input features has a far greater impact on the final performance. One study by researchers at Google found that improving features resulted in 3x the accuracy gain compared to tuning the model.
As data scientists and ML engineers know all too well, feature engineering is time-consuming and brittle. It requires careful data exploration, iterative experimentation, and domain expertise to craft relevant features that capture the important signals. According to a 2016 Kaggle survey, data scientists spend over 50% of their time on feature engineering, more than any other part of the model development process:

Despite the effort involved, most feature engineering code is ad hoc and not easily reusable. Data scientists frequently write one-off scripts full of complex logic and hard-coded assumptions that are painful to maintain and do not generalize well to new datasets or problems. This leads to duplicated work, wasted time, and difficulty collaborating across teams.
The Rise of Feature Stores and Pipeline Frameworks
To address these challenges, many organizations are turning to feature stores and pipeline frameworks to make feature engineering more modular and reusable. A feature store provides a central place to register, store, and serve features for training and inference. Feature pipelines allow data scientists to define reusable sequences of transformations to compute feature values from raw data.
Together, these tools bring DevOps best practices to feature engineering. Rather than reinventing the wheel with each new project, data scientists can build up a library of standardized feature transformations that can be composed and reused across many different models and use cases. This has several powerful benefits:
-
Improved productivity: Data scientists spend less time on repetitive data wrangling and more time on high-value experimentation and model tuning. A survey by Uber found their feature store and pipeline framework reduced feature engineering time by 50-80%.
-
Higher quality features: Reusable pipelines enforce consistency and reduce bugs caused by one-off scripts. Data scientists can leverage battle-tested transformations that have been proven on other projects. Standardized features also make it easier to detect data drift and ensure models remain accurate over time.
-
Faster experimentation: With a library of reusable features, data scientists can rapidly test different feature combinations and engineering approaches. This is crucial for ML development, which often requires many iterations to converge on an optimal model. Netflix found that their feature store enabled 10x faster model experimentation.
-
Easier collaboration: Shared feature pipelines break down silos and enable data scientists to build on each other‘s work. Teams can divide ownership of features and review code more easily. This is especially valuable for organizations with multiple data science teams working on related problems.
Designing Reusable Feature Transforms
At the core of any good feature pipeline framework are reusable and composable feature transforms. Each transform should aim to do one thing well, have a clear interface, and make minimal assumptions about its inputs. Here are some best practices to keep in mind:
Separate Computation from Configuration
A well-designed transform separates the core computation from the specific parameters used in a given pipeline. For example, consider a transform that computes the time since a user‘s last login:
def time_since_last_login(events, user_col, timestamp_col, login_event_type):
last_login = events[events[event_type] == login_event_type] \
.groupby(user_col)[timestamp_col] \
.max()
return (events[timestamp_col] - last_login).dt.days
The core logic of finding the last login event and computing the time difference is independent of the specific column names or login event type. We can make this transform more reusable by extracting these parameters into a configuration object:
from dataclasses import dataclass
@dataclass
class TimeSinceConfig:
user_col: str
timestamp_col: str
event_col: str
event_val: str
def time_since_last_event(events, config):
last_event = events[events[config.event_col] == config.event_val] \
.groupby(config.user_col)[config.timestamp_col] \
.max()
return (events[config.timestamp_col] - last_event).dt.days
login_config = TimeSinceConfig(user_col=‘user_id‘,
timestamp_col=‘event_time‘,
event_col=‘event_type‘,
event_val=‘login‘)
time_since_login = time_since_last_event(events, login_config)
Now the transform can be reused to compute time since any type of event, not just logins, by passing in a different configuration. This pattern scales well as the number of parameters grows.
Make Transforms Type-Safe and Composable
Transforms should have clear input and output types and rely on standard data structures like DataFrames wherever possible. This ensures transforms can be safely chained together without manual type checking or conversion. It also allows for greater interoperability across programming languages and compute environments.
Many pipeline frameworks use a declarative API to define the input and output schema of each transform:
import pandera as pa
class TimeFeatures(pa.SchemaModel):
user_id: pa.typing.Series[int]
event_time: pa.typing.Series[datetime]
event_type: pa.typing.Series[str]
class Config:
coerce = True
@pa.check_types
def time_since_last_event(events: TimeFeatures, config: TimeSinceConfig) -> pa.typing.Series[float]:
...
The check_types decorator validates the inputs and outputs at runtime, catching any schema mismatches before they cause downstream failures. This is especially valuable in complex pipelines with many interconnected transforms.
Validate Assumptions in Unit Tests
Even with type checking, it‘s important to verify that transforms behave correctly on real data. Unit tests are essential for catching edge cases, monitoring data drift, and ensuring transforms can handle unexpected inputs gracefully.
A good unit test suite will check for things like:
- Missing values
- Uncommon or inconsistent data types
- Boundary conditions and outliers
- Large datasets that may cause performance issues
Here‘s an example test for the time_since_last_event transform using the pytest framework:
import pandas as pd
def test_time_since_last_event():
events = pd.DataFrame({‘user_id‘: [1, 1, 2, 2, 2],
‘event_time‘: pd.to_datetime([‘2022-01-01‘, ‘2022-01-02‘, ‘2022-01-03‘, ‘2022-01-04‘, ‘2022-01-05‘]),
‘event_type‘: [‘login‘, ‘purchase‘, ‘login‘, ‘login‘, ‘purchase‘]})
login_config = TimeSinceConfig(user_col=‘user_id‘,
timestamp_col=‘event_time‘,
event_col=‘event_type‘,
event_val=‘login‘)
output = time_since_last_event(events, login_config)
expected = pd.Series([pd.NaT, pd.Timedelta(days=1), pd.NaT, pd.Timedelta(days=1), pd.Timedelta(days=2)])
pd.testing.assert_series_equal(output, expected)
This test creates a small input DataFrame with a few login events and checks that the transform produces the expected time differences. It‘s not exhaustive, but it provides a quick smoke test to catch regressions and verify the basic logic.
Building a Real-World Pipeline
With these best practices in mind, let‘s walk through an example of building a reusable feature pipeline for a real-world prediction task. We‘ll use the Instacart Market Basket Analysis dataset, which contains over 3 million grocery orders from more than 200,000 Instacart users.
Our goal is to predict which products a user will purchase in their next order based on their purchase history and behavior. We‘ll build a pipeline to generate features like:
- User-level features (e.g. average order size, favorite product categories)
- Product-level features (e.g. purchase frequency, reorder ratio)
- User-product interaction features (e.g. last purchase date, total number of purchases)
Here‘s the full code for the pipeline:
import pandas as pd
from scipy.stats import entropy
@pa.check_types
def user_features(orders: OrderFeatures, products: ProductFeatures) -> UserFeatures:
# Compute user-level order statistics
user_stats = orders.groupby(‘user_id‘).agg(
total_orders=(‘order_number‘, ‘max‘),
avg_order_size=(‘order_size‘, ‘mean‘),
avg_days_between_orders=(‘days_since_prior_order‘, ‘mean‘)
)
user_stats = user_stats.add_prefix(‘user_‘).reset_index()
# Compute distribution of product categories purchased by each user
category_dist = orders.merge(products[[‘product_id‘, ‘category‘]], how=‘left‘) \
.groupby([‘user_id‘, ‘category‘]).size() \
.unstack(‘category‘, fill_value=0)
category_dist /= category_dist.sum(axis=1).values.reshape(-1, 1)
category_dist = category_dist.add_prefix(‘cat_prop_‘)
user_stats = user_stats.merge(category_dist, how=‘left‘, on=‘user_id‘)
# Compute entropy of category distribution for each user
user_stats[‘user_category_entropy‘] = user_stats.filter(regex=‘cat_prop_.*‘).apply(entropy, axis=1)
return user_stats
@pa.check_types
def product_features(orders: OrderFeatures, order_products: OrderProductFeatures) -> ProductFeatures:
# Compute total sales and reorders for each product
product_sales = order_products.groupby(‘product_id‘).agg(
total_purchases=(‘quantity‘, ‘sum‘),
total_reorders=(‘reordered‘, ‘sum‘)
)
# Compute reorder ratio
product_sales[‘reorder_ratio‘] = product_sales.total_reorders / product_sales.total_purchases
# Compute average position in cart
product_pos = order_products.groupby(‘product_id‘)[‘add_to_cart_order‘].mean()
product_sales = product_sales.merge(product_pos, on=‘product_id‘)
return product_sales.add_prefix(‘prod_‘)
@pa.check_types
def user_product_features(order_products: OrderProductFeatures) -> UserProductFeatures:
# Compute total quantity and unique orders for each user-product pair
user_prod_stats = order_products.groupby([‘user_id‘, ‘product_id‘]).agg(
total_purchases=(‘quantity‘, ‘sum‘),
unique_orders=(‘order_id‘, ‘nunique‘)
)
# Compute average cart position
order_products[‘relative_cart_pos‘] = order_products.add_to_cart_order / order_products.order_size
user_prod_stats[‘avg_cart_pos‘] = order_products.groupby([‘user_id‘, ‘product_id‘])[‘relative_cart_pos‘].mean()
return user_prod_stats.add_prefix(‘up_‘).reset_index()
Each transform takes in one or more input DataFrames and returns a new DataFrame with the engineered features. The input schemas are defined using the pandera library for runtime type checking.
To run the full pipeline, we simply pass the raw DataFrames to each transform in sequence:
@pa.check_types
def pipeline(orders: OrderFeatures,
products: ProductFeatures,
order_products: OrderProductFeatures) -> FeatureMatrix:
users = user_features(orders, products)
products = product_features(orders, order_products)
user_products = user_product_features(order_products)
# Join all features into a single matrix
features = user_products.merge(users, on=‘user_id‘).merge(products, on=‘product_id‘)
return features
The final output is a feature matrix with one row per user-product pair and columns for each engineered feature. This can be fed directly into a machine learning model for training and prediction.
Crucially, none of the transforms make any assumptions about the specific column names or schemas of the input DataFrames. The same exact code could be applied to a completely different dataset with minimal changes.
Putting It All Together
Feature pipelines are a powerful tool for streamlining and scaling machine learning workflows. By investing in reusable, composable transforms, data science teams can build up a rich library of features that can be mixed and matched to solve a wide variety of prediction problems. This leads to faster experimentation, higher quality models, and more productive data scientists.
Adopting a feature pipeline approach does require a shift in mindset and tooling. Data scientists need to think carefully about how to design transforms that are self-contained, configurable, and testable. Organizations need to invest in the right infrastructure and processes to support collaborative feature engineering, including feature stores, testing frameworks, and code review.
But the benefits are well worth the effort. Some of the world‘s most sophisticated machine learning organizations, including Uber, Netflix, and Google, have embraced feature pipelines as a core part of their modeling workflows. As the volume and complexity of data continues to grow, feature pipelines will only become more essential for scaling machine learning development.
The future of feature engineering is likely to involve even greater automation and reuse. Techniques like deep learning can be used to automatically generate features from raw data, while transfer learning can enable features learned on one task to be reused for related problems. Feature stores may evolve into marketplaces where organizations can share and discover features, similar to how pre-trained models are shared today.
Regardless of the specific technologies used, the key principles of modularity, composability, and automation will remain central to effective feature engineering. By adopting these principles today, data science teams can build a strong foundation for the machine learning systems of tomorrow.