Linear Regression Feature Optimization

Introduction: Why Linear Models Need Tailored Feature Engineering

Welcome to "Evaluating and Finalizing Your Feature-Driven Model"! In this course, you'll discover one of the most important secrets in machine learning: different algorithms prefer different types of features. What makes a Random Forest model perform brilliantly might actually hurt a Linear Regression model's performance, and vice versa.

Linear regression operates under a fundamental assumption that relationships between features and your target variable should be linear and additive. This means the model expects that if you increase a feature by a certain amount, the target should change by a proportional amount consistently. However, real-world data rarely follows these perfect linear patterns naturally.

Consider our podcast dataset, where we're predicting listening time. A raw feature like Host_Popularity_percentage might have a complex relationship with listening time — perhaps there's a threshold effect where only very popular hosts (above 65%) significantly impact listening time, while moderate popularity doesn't matter much. Linear regression struggles with these threshold effects when given raw continuous values.

This is where model-specific feature engineering becomes crucial. Instead of feeding linear regression the raw popularity percentage, we can create a binary feature, Is_High_Host_Popularity, that captures this threshold relationship in a way linear regression can easily understand and use.

The exact threshold or bin cut point is not universal. Values like 65%, 70%, or 5-point bins should be treated as tunable hyperparameters: start with a domain-motivated guess, then compare a small set of candidates on validation data and keep the version that improves test-time generalization rather than just training fit.

To see the impact of feature engineering, let's look at our results. The baseline RMSE without any feature engineering is 13.87. After applying targeted feature engineering, we improve the model to an RMSE of 13.76. While this might seem modest, in competitive machine learning, such improvements often make the difference between winning and losing positions. It's also important to note that some features that seem helpful in one dataset may not help—or may even hurt—performance in another. Careful experimentation and validation are always required.

Creating Smart Features for Linear Relationships

The key to optimizing linear regression lies in creating features that expose linear relationships that were hidden in the original data. Let's start by examining how to build smart categorical and ratio features from continuous variables.

# Binary feature for threshold effect
result['Is_High_Host_Popularity'] = (result['Host_Popularity_percentage'] > 65).astype(int)

The Is_High_Host_Popularity feature transforms a continuous percentage into a binary indicator. This captures the threshold effect we discussed — instead of trying to learn a complex curve, linear regression can now simply learn that high-popularity hosts add a certain fixed amount to listening time. The .astype(int) converts the boolean result to 0s and 1s, which linear regression handles more efficiently.

A few example rows make the transformation clearer:

Host_Popularity_percentageIs_High_Host_Popularity
54.20
64.80
65.11
81.31

This helps show what the model is gaining. The raw values 64.8 and 65.1 are numerically close, but if listener behavior changes mainly after a threshold, the binary feature gives the model a much cleaner signal.

Now let's look at creating a meaningful ratio feature:

result['Ad_Per_Minute'] = result['Number_of_Ads'] / result['Episode_Length_minutes']
result['Ad_Per_Minute'] = result['Ad_Per_Minute'].replace([np.inf, -np.inf], np.nan).fillna(0)

The Ad_Per_Minute feature captures the density of advertisements, which is likely more predictive than raw ad count. A 60-minute episode with 6 ads has the same ad density as a 30-minute episode with 3 ads, and this density might be what actually affects listening behavior. Division operations can create infinite values when the denominator is zero, so we immediately replace any infinite values with NaN and fill them with 0 for proper handling.

Here is a small before-and-after example:

Number_of_AdsEpisode_Length_minutesRaw Ad_Per_MinuteCleaned Ad_Per_Minute
6600.100.10
3300.100.10
4200.200.20
40inf / undefined0.00

This shows both the value of the feature and the need for cleaning. In the last row, the raw calculation is invalid because the episode length is zero. If we leave that unaddressed, later stages such as scaling or model fitting can fail or become unreliable.

In a production pipeline, you would make that choice deliberately: for some problems, a zero episode length might indicate invalid data worth removing or separately investigating, while in others it is acceptable to keep the row and map the undefined ratio to a fallback value such as 0. The important part is to choose one rule, document it, and apply the same rule consistently to future data.

Binning and Rounding for Noise Reduction

Linear regression performs best when features have clean, predictable relationships with the target variable. One effective strategy is binning or rounding continuous features to reduce noise and create more stable linear relationships.

# Binned features for more stable linear relationships
result['Binned_Episode_Length_minutes'] = round(result['Episode_Length_minutes']) // 2
result['Binned_Host_Popularity_percentage'] = round(result['Host_Popularity_percentage']) // 5
result['Binned_Guest_Popularity_percentage'] = round(result['Guest_Popularity_percentage']) // 5

Binning serves multiple purposes in linear regression optimization. First, it reduces the impact of measurement noise — the difference between 45.7% and 45.3% host popularity is likely not meaningful for predicting listening time, but it can confuse the linear model. Second, binning creates natural groupings that can reveal cleaner linear patterns. Third, it reduces overfitting by preventing the model from learning relationships based on insignificant decimal variations.

Here are a few example rows before binning:

Episode_Length_minutesHost_Popularity_percentageGuest_Popularity_percentage
42.764.851.2
43.165.150.9
58.972.468.3
59.272.668.0

And here are the same rows after binning:

Binned_Episode_Length_minutesBinned_Host_Popularity_percentageBinned_Guest_Popularity_percentage
211310
211310
291413
291413

This makes the purpose of binning more concrete. Values like 42.7 and 43.1 are slightly different, but they end up in the same broader bucket. Likewise, 64.8 and 65.1 become part of the same grouped popularity signal after binning. For a linear model, these grouped values are often easier to learn from than highly precise decimals.

When you add several related engineered features, it is also important to watch for multicollinearity. Linear regression can become less stable when the model sees many versions of nearly the same signal, such as a raw feature, its rounded copy, a binary threshold, and a related interaction term all at once. That is why we both test candidate transformations and drop redundant originals when a transformed version captures the same idea more cleanly.

Removing Redundant Features

An equally important strategy is knowing when to remove original features that might hurt linear performance:

# Drop original columns that may hurt linear performance
result = result.drop(['Episode_Length_minutes', 'Host_Popularity_percentage', 
                     'Guest_Popularity_percentage'], axis=1)

This strategic dropping prevents multicollinearity issues. Since we've created binned versions of the original continuous features, keeping the original features would give the model multiple ways to use the same information. Linear regression can struggle with this redundancy, often leading to unstable coefficients and reduced performance.

You can think of it like this:

Before droppingAfter dropping
Episode_Length_minutes, Binned_Episode_Length_minutesBinned_Episode_Length_minutes
Host_Popularity_percentage, Binned_Host_Popularity_percentage, Is_High_Host_PopularityBinned_Host_Popularity_percentage, Is_High_Host_Popularity
Guest_Popularity_percentage, Binned_Guest_Popularity_percentageBinned_Guest_Popularity_percentage

The goal is not to keep every possible version of a signal. The goal is to keep the versions that best match how linear regression learns.

The decision to replace rather than supplement original features is crucial for linear models. Unlike tree-based models that can naturally handle redundant features, linear regression benefits from having a clean, non-redundant feature set where each feature contributes unique information to the prediction.

Complete Implementation Walkthrough

Let's examine the complete engineer_features_for_linear() function and see how all these strategies work together:

def engineer_features_for_linear(df):
    result = df.copy()
    # Binary feature
    result['Is_High_Host_Popularity'] = (result['Host_Popularity_percentage'] > 65).astype(int)
    # Ratio feature
    result['Ad_Per_Minute'] = result['Number_of_Ads'] / result['Episode_Length_minutes']
    result['Ad_Per_Minute'] = result['Ad_Per_Minute'].replace([np.inf, -np.inf], np.nan).fillna(0)
    # Binned features
    result['Binned_Episode_Length_minutes'] = round(result['Episode_Length_minutes']) // 2
    result['Binned_Host_Popularity_percentage'] = round(result['Host_Popularity_percentage']) // 5
    result['Binned_Guest_Popularity_percentage'] = round(result['Guest_Popularity_percentage']) // 5
    # Drop original continuous features
    result = result.drop(['Episode_Length_minutes', 'Host_Popularity_percentage', 'Guest_Popularity_percentage'], axis=1)
    return result

The function follows a logical progression: first creating smart categorical and ratio features, then applying noise-reducing transformations, and finally cleaning up redundant features. Each step builds upon the previous one to create a feature set optimized specifically for linear regression.

Just as importantly, any thresholds, bin definitions, and scaling parameters you settle on should be treated as part of the model pipeline itself. Once you choose a cutoff such as 65% or fit a scaler on the training data, those same transformation settings should be reused for validation, test, and production data so the model sees features in the same form at every stage.

Training and Evaluating the Linear Regression Model

Here is the complete pipeline for training and evaluating the optimized linear regression model:

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from scripts.data_preprocess import preprocess

# Load and prepare the dataset
df = pd.read_csv('data/data.csv')
train_data, test_data = train_test_split(df, test_size=0.2, random_state=42)

# Identify numerical and categorical features
numerical_features = train_data.select_dtypes(include=['int64', 'float64']).columns.tolist()
categorical_features = train_data.select_dtypes(include=['object', 'category']).columns.tolist()
target_col = 'Listening_Time_minutes'
if target_col in numerical_features:
    numerical_features.remove(target_col)
if target_col in categorical_features:
    categorical_features.remove(target_col)

# Preprocess the data
train_processed, test_processed = preprocess(train_data, test_data, numerical_features, categorical_features)

# Apply feature engineering function
train_final = engineer_features_for_linear(train_processed)
test_final = engineer_features_for_linear(test_processed)

# Scale continuous engineered features for Linear Regression
scale_features = [
    'Ad_Per_Minute',
    'Binned_Episode_Length_minutes',
    'Binned_Host_Popularity_percentage',
    'Binned_Guest_Popularity_percentage'
]

scaler = MinMaxScaler()
train_final[scale_features] = scaler.fit_transform(train_final[scale_features])
test_final[scale_features] = scaler.transform(test_final[scale_features])

# Prepare target variable
y_train = train_final['Listening_Time_minutes']
y_test = test_final['Listening_Time_minutes']

X_train = train_final.drop(['id', 'Listening_Time_minutes'], axis=1)
X_test = test_final.drop(['id', 'Listening_Time_minutes'], axis=1)

# Train and evaluate Linear Regression
lr = LinearRegression()
lr.fit(X_train, y_train)
lr_preds = lr.predict(X_test)
lr_rmse = np.sqrt(mean_squared_error(y_test, lr_preds))
print(f"Final Linear Regression RMSE: {lr_rmse:.4f}")

# Print model coefficients
print("\nLinear Regression Coefficients:")
for name, coef in zip(X_train.columns, lr.coef_):
    print(f"{name}: {coef:.4f}")

When you run this code, you should see output similar to:

Final Linear Regression RMSE: 13.7635

Linear Regression Coefficients:
Is_High_Host_Popularity: 1.2345
Ad_Per_Minute: -2.3456
Binned_Episode_Length_minutes: 0.5678
Binned_Host_Popularity_percentage: 0.1234
Binned_Guest_Popularity_percentage: 0.2345
...

This result demonstrates the impact of model-specific feature engineering. The RMSE of 13.76 is a measurable improvement over the baseline RMSE of 13.87 (using only the original features, without any feature engineering).

Why Some Features Help and Others Hurt

It's important to recognize that not all features that seem helpful will actually improve linear regression performance. For example, including both the original continuous features and their binned versions can introduce multicollinearity, which destabilizes the model and can worsen predictions. Similarly, features that work well for tree-based models (like raw continuous variables or high-cardinality categorical features) may not help linear regression, and vice versa.

In some datasets, a feature like Is_High_Host_Popularity might be highly predictive, while in others, it could be irrelevant or even misleading. The effectiveness of each feature depends on the underlying data distribution and the relationships present. This is why it's crucial to experiment, validate, and always check your model's performance after each feature engineering step.

Summary and Practice Preparation

You've now learned the fundamental principles of optimizing features specifically for linear regression models, and seen how these principles are applied in a real pipeline to achieve an RMSE of 13.76, improving upon the baseline of 13.87. The key insights from this lesson are that linear models perform best when features expose linear relationships, avoid multicollinearity, and reduce noise through binning or rounding.

The small before-and-after row examples in this lesson also make the transformations more concrete: threshold features simplify difficult patterns, ratio features can create invalid values if not cleaned carefully, and binning reduces tiny decimal differences that may behave more like noise than useful signal.

The approach we've covered differs significantly from generic feature engineering. Instead of creating as many features as possible and letting the model sort them out, we've been strategic about creating features that align with linear regression's assumptions. We've transformed threshold effects into binary features, captured non-linear patterns through binning, created meaningful ratios, and cleaned up redundant information.

The exercises will help you internalize when and why each technique works, preparing you for the next units, where we'll explore how Random Forest and LightGBM models prefer different feature engineering approaches.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal