Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Feature Engineering

At this point, we’ve grown quite familiar with the modeling process. We’ve introduced the concept of loss, used it to fit several types of models, and, most recently, extended our analysis to multiple regression. Along the way, we’ve forged our way through the mathematics of deriving the optimal model parameters in all its gory detail. It’s time to make our lives a little easier – let’s implement the modeling process in code!

In this lecture, we’ll explore two techniques for model fitting:

  1. Translating our derived formulas for regression to python

  2. Using python’s sklearn package

With our new programming frameworks in hand, we will also add sophistication to our models by introducing more complex features to enhance model performance.

Feature Engineering

Feature engineering is the process of transforming raw features into more informative features that can be used in modeling or EDA tasks and improve model performance.

Feature engineering allows you to:

  • Capture domain knowledge

  • Express non-linear relationships using linear models

  • Use non-numeric (qualitative) features in models

Feature Functions

A feature function describes the transformations we apply to raw features in a dataset to create a design matrix of transformed features (typically denoted as Φ\Phi). When we apply the feature function to our original dataset X\mathbb{X}, the result, Φ(X)\Phi(\mathbb{X}), is a transformed design matrix ready to be used in modeling.

For example, we might design a feature function that computes the square of an existing feature and adds it to the design matrix. In this case, our existing matrix [x][x] is transformed to [x,x2][x, x^2]. Its dimension increases from 1 to 2. Often, the dimension of the featurized dataset increases as seen here.

In this example, applying a feature function adds a column to the dataset.

The new features introduced by the feature function can then be used in modeling. Often, we use the symbol ϕi\phi_i to represent transformed features after feature engineering.

y^=θ0+θ1x+θ2x2y^=θ0+θ1ϕ1+θ2ϕ2\begin{align} \hat{y} &= \theta_0 + \theta_1 x + \theta_2 x^2 \\ \hat{y} &= \theta_0 + \theta_1 \phi_1 + \theta_2 \phi_2 \end{align}

In matrix notation, the symbol Φ\Phi is sometimes used to denote the design matrix after feature engineering has been performed. Note that in the usage below, Φ\Phi is now a feature-engineered matrix, rather than a function.

Y^=Φθ\hat{\mathbb{Y}} = \Phi \theta

More formally, we describe a feature function as transforming the original Rn×p\mathbb{R}^{n \times p} dataset X\mathbb{X} to a featurized Rn×p′\mathbb{R}^{n \times p'} dataset Φ\mathbb{\Phi}, where p′p' is typically greater than pp.

X∈Rn×p⟶Φ∈Rn×p′\mathbb{X} \in \mathbb{R}^{n \times p} \longrightarrow \Phi \in \mathbb{R}^{n \times p'}

One-Hot Encoding

A particularly powerful use of feature engineering is to allow us to perform regression on non-numeric features. One-hot encoding is a feature engineering technique that generates numeric features from categorical data, allowing us to use our usual methods to fit a regression model on the data.

To illustrate how this works, we’ll refer back to the tips dataset from previous lectures. Consider the "day" column of the dataset:

Click to see the code
import numpy as np
import seaborn as sns
import polars as pl
import sklearn.linear_model as lm
tips = pl.from_pandas(sns.load_dataset("tips"))
tips.head()
Loading...

At first glance, it doesn’t seem possible to fit a regression model to this non-numeric data – we can’t directly perform any mathematical operations on the entry “Sun”.

To resolve this, we instead create a new table with a feature for each unique value in the original "day" column. We then iterate through the "day" column. For each entry in "day" we fill the corresponding feature in the new table with 1. All other features are set to 0.

Illustration of one hot encoding.

In short, each category of a categorical variable gets its own feature

  • Value = 1 if a row belongs to the category

  • Value = 0 otherwise

The OneHotEncoder class of sklearn (documentation) offers a quick way to perform this one-hot encoding. You will explore its use in detail in the lab. For now, recognize that we follow a very similar workflow to when we were working with the LinearRegression class: we initialize a OneHotEncoder object, fit it to our data, and finally use .transform() to apply the fitted encoder.

Loading...

The one-hot encoded features can then be used in the design matrix to train a model:

Design matrix showing raw features highlighted separately from one-hot encoded features.
y^=θ1(total_bill)+θ2(size)+θ3(day_Fri)+θ4(day_Sat)+θ5(day_Sun)+θ6(day_Thur)\hat{y} = \theta_1 (\text{total}\_\text{bill}) + \theta_2 (\text{size}) + \theta_3 (\text{day}\_\text{Fri}) + \theta_4 (\text{day}\_\text{Sat}) + \theta_5 (\text{day}\_\text{Sun}) + \theta_6 (\text{day}\_\text{Thur})

Or in shorthand:

y^=θ1ϕ1+θ2ϕ2+θ3ϕ3+θ4ϕ4+θ5ϕ5+θ6ϕ6\hat{y} = \theta_{1}\phi_{1} + \theta_{2}\phi_{2} + \theta_{3}\phi_{3} + \theta_{4}\phi_{4} + \theta_{5}\phi_{5} + \theta_{6}\phi_{6}

Now, the day feature (or rather, the four new boolean features that represent day) can be used to fit a model.

Using sklearn to fit the new model, we can determine the model coefficients, allowing us to understand how each feature impacts the predicted tip.

Loading...

For example, when looking at the coefficient for day_Fri, we can now understand the impact of it being Friday on the predicted tip — if it is a Friday, the predicted tip increases by approximately $0.75.

When one-hot encoding, keep in mind that any set of one-hot encoded columns will always sum to a column of all ones, representing the bias column. More formally, the bias column is a linear combination of the OHE columns.

Illustration of how the original data is transformed with one hot encoding and a bias column.

We must be careful not to include this bias column in our design matrix. Otherwise, there will be linear dependence in the model, meaning X⊤X\mathbb{X}^{\top}\mathbb{X} would no longer be invertible, and our OLS estimate θ^=(X⊤X)−1X⊤Y\hat{\theta} = (\mathbb{X}^{\top}\mathbb{X})^{-1}\mathbb{X}^{\top}\mathbb{Y} fails.

To resolve this issue, we simply omit one of the one-hot encoded columns or we do not include an intercept term. The adjusted design matrices are shown below.

Illustration of removing one of the columns from one hot encoding or the bias column.

Either approach works — we still retain the same information as the omitted column being a linear combination of the remaining columns.

Polynomial Features

We have encountered a few cases now where models with linear features have performed poorly on datasets that show clear non-linear curvature.

As an example, consider the vehicles dataset, which contains information about cars. Suppose we want to use the hp (horsepower) of a car to predict its "mpg" (gas mileage in miles per gallon). If we visualize the relationship between these two variables, we see a non-linear curvature. Fitting a linear model to these variables results in a high (poor) value of RMSE.

y^=θ0+θ1(hp)\hat{y} = \theta_0 + \theta_1 (\text{hp})
Click to see the code
vehicles = pl.from_pandas(sns.load_dataset("mpg")).drop_nulls().rename({"horsepower": "hp"}).sort("hp")

X = vehicles[["hp"]]
Y = vehicles["mpg"]

hp_model = lm.LinearRegression()
hp_model.fit(X, Y)
hp_model_predictions = hp_model.predict(X)

import matplotlib.pyplot as plt

sns.scatterplot(data=vehicles, x="hp", y="mpg")
plt.plot(vehicles["hp"], hp_model_predictions, c="tab:red");

print(f"MSE of model with (hp) feature: {((Y - hp_model_predictions)**2).mean()}")
MSE of model with (hp) feature: 23.943662938603104
<Figure size 640x480 with 1 Axes>

As we can see from the plot, the data follows a curved line rather than a straight one. To capture this non-linearity, we can incorporate non-linear features. Let’s introduce a polynomial term, hp2\text{hp}^2, into our regression model. The model now takes the form:

y^=θ0+θ1(hp)+θ2(hp2)\hat{y} = \theta_0 + \theta_1 (\text{hp}) + \theta_2 (\text{hp}^2)
y^=θ0+θ1ϕ1+θ2ϕ2\hat{y} = \theta_0 + \theta_1 \phi_1 + \theta_2 \phi_2

How can we fit a model with non-linear features? We can use the exact same techniques as before: ordinary least squares, gradient descent, or sklearn. This is because our new model is still a linear model. Although it contains non-linear features, it is linear with respect to the model parameters. All of our previous work on fitting models was done under the assumption that we were working with linear models. Because our new model is still linear, we can apply our existing methods to determine the optimal parameters.

MSE of model with (hp^2) feature: 18.98476890761722
<Figure size 640x480 with 1 Axes>

Looking a lot better! By incorporating a squared feature, we are able to capture the curvature of the dataset. Our model is now a parabola centered on our data. Notice that our new model’s error has decreased relative to the original model with linear features.

Complexity and Overfitting

We’ve seen now that feature engineering allows us to build all sorts of features to improve the performance of the model. In particular, we saw that designing a more complex feature (squaring hp in the vehicles data previously) substantially improved the model’s ability to capture non-linear relationships. To take full advantage of this, we might be inclined to design increasingly complex features. Consider the following three models, each of different order (the maximum exponent power of each model):

  • Model with order 2: y^=θ0+θ1(hp)+θ2(hp2)\hat{y} = \theta_0 + \theta_1 (\text{hp}) + \theta_2 (\text{hp}^2)

  • Model with order 3: y^=θ0+θ1(hp)+θ2(hp2)+θ3(hp3)\hat{y} = \theta_0 + \theta_1 (\text{hp}) + \theta_2 (\text{hp}^2) + \theta_3 (\text{hp}^3)

  • Model with order 4: y^=θ0+θ1(hp)+θ2(hp2)+θ3(hp3)+θ4(hp4)\hat{y} = \theta_0 + \theta_1 (\text{hp}) + \theta_2 (\text{hp}^2) + \theta_3 (\text{hp}^3) + \theta_4 (\text{hp}^4)

Three different plots with different numbers of polynomial terms.

As we can see in the plots above, MSE continues to decrease with each additional polynomial term. To visualize it further, let’s plot models as the complexity increases from 0 to 7:

Comparison of plots with different numbers of polynomial terms.

When we use our model to make predictions on the same data that was used to fit the model, we find that the MSE decreases with each additional polynomial term (as our model gets more complex). The training error is the model’s error when generating predictions from the same data that was used for training purposes. We can conclude that the training error goes down as the complexity of the model increases.

Conceptual plotting of training error. As model complexity increases, training error decreases.

This seems like good news – when working on the training data, we can improve model performance by designing increasingly complex models.

However, high model complexity comes with its own set of issues. When building the vehicles models above, we trained the models on the entire dataset and then evaluated their performance on this same dataset. In reality, we are likely to instead train the model on a sample from the population, then use it to make predictions on data it didn’t encounter during training.

Let’s walk through a more realistic example. Say we are given a training dataset of just 6 datapoints and want to train a model to then make predictions on a different set of points. We may be tempted to make a highly complex model (e.g., degree 5), especially given it makes perfect predictions on the training data as clear on the left. However, as shown in the graph on the right, this model would perform horribly on the rest of the population!

On the left is the six datapoints with a perfectly fit curve. On the right, the rest of the population is shown and it is clear that the curve does not fit the population.

This phenomenon is called overfitting. The model effectively just memorized the training data it encountered when it was fitted, leaving it unable to generalize well to data it didn’t encounter during training. This is a problem: we want models that are generalizable to “unseen” data.

Additionally, since complex models are sensitive to the specific dataset used to train them, they have high variance. A model with high variance tends to vary more dramatically when trained on different datasets. Going back to our example above, we can see our degree-5 model varies erratically when we fit it to different samples of 6 points from vehicles.

Three different resamples from the above population.

We now face a dilemma: we know that we can decrease training error by increasing model complexity, but models that are too complex do not generalize well. This is because these overly complex models can’t be reapplied to new datasets due to high variance.

Illustration of underfitting and overfitting.

We can see that there is a clear trade-off that comes from the complexity of our model. As model complexity increases, the model’s error on the training data decreases. At the same time, the model’s variance tends to increase.

The takeaway here: we need to strike a balance in the complexity of our models; we want models that are generalizable to “unseen” data. A model that is too simple won’t be able to capture the key relationships between our variables of interest; a model that is too complex runs the risk of overfitting.

This begs the question: how do we control the complexity of a model? Stay tuned for Lecture 17 on Cross-Validation and Regularization!