Estimation

Lab setup

First, make sure you have completed the initial setup.

If you are part of a course

  1. Open Terminal. Run the update command to make sure you have the latest code.
    $ mwc update
  2. Move to this lab's directory.
    $ cd ~/Desktop/making_with_code/mwc2/unit3/lab_estimation
    

If you are working on your own

  1. Move to your MWC directory.
    $ cd ~/Desktop/making_with_code
    
  2. Get a copy of this lab's materials.
    git clone https://git.makingwithcode.org/mwc/lab_estimation.git

Fitting a model

The scatter plot below has a blue line which tries to estimate the relationship between x and y in the plot. This line allows you to quickly summarize the plot, and it allows you to estimate the y-value for new x-values.

Unfortunately, the line doesn't fit the data very well yet. The yellow squares represent error: the distance from each point to the line forms one side of a square. The total squared error, or loss, is shown at the bottom of the plot. Click on the plot, then use the arrow keys to improve the line's fit by reducing the loss as much as possible.

Searching systematically

So far you've been adjusting the slope and intercept by feel. There's a more systematic way: at any point, you can ask "if I nudge the intercept up a little, does the total squared error go up or down? What about the slope?" The answer to those two questions is the gradient, and repeatedly moving in the direction that decreases the loss is called gradient descent.

Turn on "Show gradient hints." Two arrows appear:

Each arrow points in the direction that decreases total squared error, and its length shows by how much—a long arrow means there's a lot of room for improvement; a short arrow means you're close to the bottom.

Two datasets, revisited

This lab uses Jupyter notebooks—an interactive environment for writing and running code. The notebook works through four short sections. Each one demos a technique on the Pokémon dataset from the Pokémon lab, then asks you to repeat it (and push further) on BRFSS, the health survey dataset from that same lab.

Your written answers go in questions.md, not the notebook—each checkpoint below tells you exactly what to answer and where.

💻 Start the notebook:

$ jupyter lab

Open lab_estimation.ipynb. A column reference for both datasets is near the top.

💻 Run the first few cells to load both datasets.

The BRFSS dataset has one row per survey respondent and includes:

ColumnDescription
ageAge band (18, 25, 35, 45, 55, or 65 meaning 65+)
sexmale or female
incomeAnnual income band, 1 (under $10k) to 8 (over $75k)
educationHighest education level, 1 (did not graduate high school) to 4 (college graduate)
sexual_orientationheterosexual, homosexual, bisexual, or other
healthSelf-reported general health, 1 (poor) to 5 (excellent)
no_doctorCouldn't afford to see a doctor in the last year (True/False)
exerciseDid any exercise in the last 30 days (True/False)
sleepAverage hours of sleep per night

Estimating income

In the toy, you adjusted two parameters by hand (slope and intercept), to fit a line to 24 points. BRFSS has over 160,000 respondents and many possible predictors, so we let scikit-learn do the fitting. The pattern is the same one you'll use for every model in this course:

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

df = pd.read_csv("brfss_2020.csv")
X = df[["education"]]
y = df["income"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

model = LinearRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test)

Lines 5–6 set up X and y. Notice the type of each: X is a DataFrame—a table, even though it only has one column here. X could just as easily hold five columns, one per predictor, and the rest of this code wouldn't change. y is a Series: a single value per example, which is why it's lowercase—unlike X, it's never a table with more than one column.

Line 8 divides the data into a training set and a test set, using train_test_split. This is the same idea you'll meet in every later lab: a model evaluated on the data it was trained on can look better than it really is, simply by memorizing quirks of that particular data. A test set the model never saw during training gives an honest measurement of how well it generalizes to new examples.

Lines 10–12 are the fit/predict pattern itself. fit is the gradient-descent-or-closed-form step: it searches for the parameters (slope and intercept, or one coefficient per predictor) that minimize the loss on the training data from line 8. predict is what you did by hovering over the toy—given new input, it returns the model's estimate; here it's applied to the test data, the half of line 8's split the model never trained on.

How well does the model fit?

The toy measured loss as total squared error. RMSE (root mean squared error) is the same idea, reported in a way you can actually interpret: it ends up in the same units as y, instead of squared units. Here's a function that computes it:

def root_mean_squared_error(y_true, y_pred):
    errors = y_true - y_pred
    squared_errors = errors ** 2
    mean_squared_error = squared_errors.mean()
    return mean_squared_error ** 0.5

Line 2 finds every error at once: y_true and y_pred are the same length (one actual value and one prediction per example), so errors ends up holding one actual-minus-predicted difference per example. Line 3 squares each of those, elementwise—same as the yellow squares in the toy. That squaring is also why total squared error isn't directly interpretable on its own: squaring a value in, say, dollars gives you a number in squared dollars, which isn't a unit anyone can picture. Line 4 averages all the squared errors into a single number (and, as a side effect, makes RMSE comparable across datasets of different sizes, unlike a running total). Line 5 takes the square root, which undoes the squaring from line 3 and lands the result back in the original, interpretable units of y.

A model predicting income (banded 1–8) with RMSE = 2 is, roughly, "typically off by about 2 income bands." Always ask whether that's good or bad relative to the range and spread of $y$ itself—an RMSE of 2 is unremarkable if $y$ ranges from 1 to 8, but would be a near-perfect fit if $y$ ranges from 1 to 1000.

💻 Work through sections 1–3 of the notebook: a single continuous-ish predictor, a binary predictor, and an ordinal predictor.

Multiple regression and overfitting

You can include more than one predictor:

from sklearn.linear_model import LinearRegression

X = df[["education", "income", "exercise", "age", "no_doctor"]]
y = df["health"]
model = LinearRegression()
model.fit(X, y)

💻 Work through section 4 of the notebook: multiple regression.

Closing discussion