This post is the second in a series on the multiple linear regression model. In a previous post, I introduced the model and much of it’s associated theory. In this post, I’ll continue exploring the multiple linear regression model with an example in Python, including a comparison of the scikit-learn and statsmodels libraries.

### Introducing the Example

As a working example, I’ll explore the effect weather had on airline flights leaving three New York City airports in 2013. In particular, I’ll join the airlines, flights, and weather datasets from the nycflights13 Python package and investigate the relationship wind speed, precipitation and visibility has on departure delay.

### Preparing the Data

Fortunately, the datasets I’ll be working with do not require much cleaning and organization before applying the model. After renaming the fields for clarity and converting the timestamp_hour fields to a datetime64 data type, I’ll join the dataframes via pandas’ merge and name it nyc. I’ll also drop missing values using pd.dropna(). In practice, a more thorough investigation into these missing values would be needed, but I’ll ignore this for demonstration purposes.

import pandas as pd
from nycflights13 import airlines, flights, weather

# Organizing airlines ---------------------------------------------------------

# Renaming fields for clarity
airlines = airlines.rename(columns={'carrier':'carrier_abbreviation',
'name': 'carrier_name'})

# Organizing flights ----------------------------------------------------------

# Renaming fields for clarity
flights = flights.rename(columns={'dep_delay': 'departure_delay',
'carrier': 'carrier_abbreviation',
'origin': 'orig_airport',
'time_hour': 'timestamp_hour'})

# Assigning timestamp_hour field to datetime type
flights = flights.assign(timestamp_hour=pd.to_datetime(flights.timestamp_hour, utc=True))

# Organizing weather ----------------------------------------------------------

# Renaming fields for clarity
weather = weather.rename(columns={'origin': 'orig_airport',
'wind_speed': 'wind_speed_mph',
'precip': 'precipitation_inches',
'visib': 'visibility_miles',
'time_hour': 'timestamp_hour'})

# Assigning timestamp_hour field to datetime type
weather = weather.assign(timestamp_hour=pd.to_datetime(weather.timestamp_hour, utc=True))

# Joining Data ----------------------------------------------------------------

# Joining
nyc = \
flights \
.merge(right=airlines, how='left', on='carrier_abbreviation') \
.merge(right=weather, how='left', on=['orig_airport', 'timestamp_hour'])

# Selecting relevant fields and reordering
nyc = nyc[['timestamp_hour',
'orig_airport',
'carrier_name',
'departure_delay',
'wind_speed_mph',
'precipitation_inches',
'visibility_miles']]

# Dropping missing values
nyc = nyc.dropna()


At this point, the nyc dataframe has been created. A glimpse of the data structure can be seen below.

>>> nyc.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 326915 entries, 0 to 336769
Data columns (total 7 columns):
#   Column                Non-Null Count   Dtype
---  ------                --------------   -----
0   timestamp_hour        326915 non-null  datetime64[ns, UTC]
1   orig_airport          326915 non-null  object
2   carrier_name          326915 non-null  object
3   departure_delay       326915 non-null  float64
4   wind_speed_mph        326915 non-null  float64
5   precipitation_inches  326915 non-null  float64
6   visibility_miles      326915 non-null  float64
dtypes: datetime64[ns, UTC](1), float64(4), object(2)
memory usage: 20.0+ MB

timestamp_hour orig_airport            carrier_name  departure_delay  wind_speed_mph  precipitation_inches  visibility_miles
0 2013-01-01 10:00:00+00:00          EWR   United Air Lines Inc.              2.0        12.65858                   0.0              10.0
1 2013-01-01 10:00:00+00:00          LGA   United Air Lines Inc.              4.0        14.96014                   0.0              10.0
2 2013-01-01 10:00:00+00:00          JFK  American Airlines Inc.              2.0        14.96014                   0.0              10.0
3 2013-01-01 10:00:00+00:00          JFK         JetBlue Airways             -1.0        14.96014                   0.0              10.0
4 2013-01-01 11:00:00+00:00          LGA    Delta Air Lines Inc.             -6.0        16.11092                   0.0              10.0


### Exploratory Data Analysis

Before diving into modeling, it’s good practice to perform some initial exploratory data analysis. I’ll use the seaborn data visualization library to create some plots.

Because the data is so large, I’ll take a small sample of 10,000 observations and create a pair plot.

import seaborn as sns

# Taking random sample, defining random_state for reproducibility
nyc_small = nyc.sample(n=10000, random_state=1234)

# Creating pair plot
sns.pairplot(nyc_small)\
.fig.suptitle('10,000 Flights Leaving NYC, 2013: Pair Plot', y=1)


From the pair plot, it’s clear the data is quite noisy. However, we can still learn a thing or two from the plot. For example, most days in New York City tend to have good to mild weather with high visibility, low precipitation and moderate wind speeds. Also, from this small sample, no striking patterns or correlation stand out.

Perhaps instead of attempting to understand how these weather patterns affect individual departure delays, we should aggregate our data per hour and explore how average weather patterns affect average departure delays.

# Aggregating data per hour
nyc_per_hour = \
nyc\
.groupby("timestamp_hour")\
.agg(mean_departure_delay=('departure_delay', 'mean'),
mean_wind_speed_mph=('wind_speed_mph', 'mean'),
mean_precipitation_inches=('precipitation_inches', 'mean'),
mean_visibility_miles=('visibility_miles', 'mean'))\
.reset_index()

sns.pairplot(nyc_per_hour)\
.fig.suptitle('Flights Leaving NYC, 2013: Pair Plot of Average Per Hour Values', y=1)


The pair plot for the new aggregated nyc_per_hour dataset is still quite busy. As a last plot, let’s create a correlation heatmap to confirm the lack of correlation among the variables.

import matplotlib.pyplot as plt
import numpy as np

labels =['Departure Delay', 'Wind Speed (MPH)', 'Precipitation (in)', 'Visibility (miles)']

sns\
.heatmap(nyc_per_hour.corr(),
annot=True,
xticklabels=labels,
yticklabels=labels)\
.set_title('Correlation Heatmap of Average per Hour Values')

plt.xticks(rotation=45)


From the heatmap, we can see that the three predictor variables are mostly uncorrelated. For our multiple linear regression model, this is preferred. Multicollinearity among predictors can lead to spurious predictions and high variance among the regression coefficients. A variety of techniques can be helpful when dealing with mutlicollinearity among predictors, such as lasso regression and principal component analysis. These topics will be covered in more depth in later posts.

It is worth noting that there is a somewhat strong correlation between average precipitation and average visibility, with a Pearson correlation of -0.38. This isn’t surprising to see, as we might expect rainy days to have low visibility. For the purposes of this demonstration, I’ll ignore this multicollinearity.

From the heatmap as well, we can also see there is a small degree of correlation between average precipitation and average departure delay, with a Pearson correlation of 0.22, and a comparable negative correlation between average visibility and average departure delay with a Pearson correlation of -0.21.

### Multiple Linear Regression via scikit-learn

After some exploratory analysis, we’re ready for modeling. I’ll first train the model using scikit-learn, and then train the same model using statsmodels. While scikit-learn is an excellent machine learning package, it isn’t intended for statistical inference. For this reason, I’ll explore the model summary of statsmodels in-depth to learn more about the regression in the next section.

Fitting the model via the scikit-learn API is quite simple. An estimator object is first created, and then is fit using the predictor variables X on the response variable y.

from sklearn.linear_model import LinearRegression

# Assign estimator
linear_reg = LinearRegression()

# Defining predictors and response
X = nyc_per_hour[['mean_wind_speed_mph', 'mean_precipitation_inches', 'mean_visibility_miles']]
y = nyc_per_hour['mean_departure_delay']

# Fitting model
linear_reg.fit(X=X, y=y)


After fitting the model, we can view the regression coefficients and intercept using the coef_ and intercept_ attributes. We could also perform prediction with the fitted model using the predict() method.

Of particular interest to this post is the score() method. For the linear regression model, this method returns $R^2$, or the coefficient of determination. This value indicates the proportion of variance explained by the model.

>>> linear_reg.score(X, y)
0.08295782066352853


For the particular fitted model above, the $R^2$ value is approximately 0.08, indicating about 8% of the total variance in the average flight departure delay is explained by the linear combination of average wind speed, average precipitation, and average visibility. Needless to say, that leaves a lot of variance unexplained, and our current linear model could almost certainly be improved upon.

In future posts, I’ll explore techniques that may better fit the data.

### Multiple Linear Regression via statsmodels

Using the main statsmodels API provides a similar experience to scikit-learn’s API. To train a multiple linear regression model via statsmodels, we can use the OLS() function.

It is worth noting, statmodels does have a couple of nuances. First, by default, the OLS() function does not include an intercept. To include an intercept value in the model, we can use the add_constant() method.

Second, instead of the more common x and y parameter names, statsmodels uses exog and endog, referring to exogenous and endogenous.

Below, I’ll fit a multiple linear regression model using the main statsmodels API and the same X and y values defined above.

import statsmodels.api as sm

# Including constant term

# Fitting model
linear_reg_sm = sm.OLS(endog=y, exog=X)
result = linear_reg_sm.fit()


In addition to the API interface above, statsmodels also provides a formula interface. This interface makes use of the patsy package under the hood, which, by the way, just might be the best package name I’ve ever heard of. As a former R user, this formula interface is quite familiar.

import statsmodels.formula.api as smf

# Fitting model
linear_reg_smf = smf.ols('mean_departure_delay ~ mean_wind_speed_mph + mean_precipitation_inches + mean_visibility_miles', data=nyc_per_hour)
result = linear_reg_smf.fit()

result.summary()


The summary() method can be used to view a useful summary table of the regression.

<class 'statsmodels.iolib.summary.Summary'>
"""
OLS Regression Results
================================================================================
Dep. Variable:     mean_departure_delay   R-squared:                       0.083
Method:                   Least Squares   F-statistic:                     207.5
Date:                  Tue, 12 Jan 2021   Prob (F-statistic):          7.29e-129
Time:                          20:30:14   Log-Likelihood:                -30586.
No. Observations:                  6886   AIC:                         6.118e+04
Df Residuals:                      6882   BIC:                         6.121e+04
Df Model:                             3
Covariance Type:              nonrobust
============================================================================================
coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------
const                       24.4845      1.413     17.332      0.000      21.715      27.254
mean_wind_speed_mph          0.5334      0.049     10.949      0.000       0.438       0.629
mean_precipitation_inches  127.3312     11.298     11.270      0.000     105.183     149.479
mean_visibility_miles       -1.9262      0.143    -13.493      0.000      -2.206      -1.646
==============================================================================
Omnibus:                     4515.819   Durbin-Watson:                   0.385
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            63194.997
Skew:                           2.986   Prob(JB):                         0.00
Kurtosis:                      16.586   Cond. No.                         683.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
"""


This summary table provides quite a bit of information. An interpretation of the $R^2$ value has already been discussed above. A few more statistics worth discussing here are the p-value of the F-statistic, indicated by Prob (F-statistic), and the p-values for the intercept and predictor terms, labeled as the P>|t| column.

The F-statistic can be used to determine if there exists any relationship between a predictor and the response. To simply look at the individual predictor p-values to determine if this relationship exists is flawed, as we would expect some individual variables to appear significant by chance alone, especially as the number of predictors in a model increases.

In this instance, the associated p-value of the F-statistic is approximately 7.29e-129, which is incredibly low. This p-value provides strong evidence that at least one predictor has an association with the response.

After determining that it is highly probable that a relationship does exist between at least one predictor and the response, we can investigate the individual predictor and intercept p-values. In the summary chart above, all four of these p-values are 0.000, indicating they are very close to zero. The pvalues method can be used to view the actual p-values.

>>> result.pvalues
const                       6.654574e-66
mean_wind_speed_mph         1.132716e-27
mean_precipitaton_inches    3.322614e-29
mean_visibility_miles       5.681725e-41


It is clear all of these p-values are quite small. Therefore, we can declare we have strong evidence that each of average wind speed, average precipitation, average visibility, and the intercept are significant and associated with average departure delay.

Lastly, I should discuss the implication of the very low $R^2$ and the very significant F-statistic and t-statistics.

With an ideal model fit, we would have simultaneously a high $R^2$ statistic, and significant F-statistics and t-statistics. However, in this instance, this is clearly not the case. Is this a problem? Should the results be thrown out? Not quite.

From the significant F-statistic and t-statistics, we have strong evidence that the predictor variables are associated with a change in average departure delay. We know this. From the low $R^2$ statistic, we also know that our multiple linear regression model does not capture the bulk of the average departure delay variance. So, our individual predictors are performing quite well, but our overall model is performing quite poorly. In general, this can be attributed to the wide variation in departure delay times in the data.

Other regression techniques - such as using interaction terms - might improve our fit on the data, and thus explain more of the variance in average departure delay. Similarly, including other predictor variables might also help in explaining more of the unexplained variance. In following posts, both of these topics will be explored.