Linear Regression

Simple linear regression with lm(): coefficients, R2, prediction, and plotting.

Fit a simple linear regression with lm()

Use lm() to model a linear relationship between predictor values (x) and response values (y).

Model form:

\[ y = ax + b \]

  • \(a\): slope (how much y changes when x increases by 1)
  • \(b\): intercept (predicted y when x = 0)

In R, that same model is written as y ~ x inside lm().

  • Left side of ~: response variable (y)
  • Right side of ~: predictor variable (x)

So lm(absorbance ~ conc_ug_ml, data = calibration) means: “Model absorbance as a linear function of concentration.”

Extract slope and intercept with coef()

coef(model) returns the estimated coefficients as a small vector.

For a simple model y ~ x, indexing works like this:

  • coefs[[1]]: intercept (b in y = ax + b)
  • coefs[[2]]: slope (a in y = ax + b)

coef(model) returns a named vector (for example with names like (Intercept) and conc_ug_ml).

  • coefs[1] returns a vector of length 1 (still a vector, with its name attached)
  • coefs[[1]] returns the single numeric value only

Here we use [[ ]] because we want the plain number for calculations and cat() output.

Use summary() and extract R2

R2, used to interpret goodness of fit, can be assessed from summary(model) and extracted as $r.squared.

If you run summary(model) and scroll through the output, you also get:

  • Residuals (differences between observed and predicted values)
  • Coefficients table (estimate, standard error, t value, p-value)
  • Residual standard error
  • Adjusted R2
  • F-statistic

For full output, run:

Force the regression through (0,0)

If your scientific model requires zero intercept, fit without an intercept term.

Equivalent formulas:

  • y ~ 0 + x
  • y ~ x - 1
Note

Use a forced-zero model only when it is scientifically justified. Otherwise, it can bias slope estimates.

Fit separate regressions for each group with group_by()

Sometimes one regression line isn’t appropriate for the whole data set — for example, if you ran calibration curves for three different compounds and want to check each one’s line separately. group_by() combined with summarise() lets you fit a separate lm() model per compound and pull out coefficients and R² for each, in one step.

The tricky part is that lm() normally expects a named data frame to point to with data = .... Inside summarise(), there isn’t one — you’re working through the data group by group, not from an existing table. pick(everything()) solves this: it reassembles the current group’s columns into a small data frame on the spot, so lm() has something to fit. Since lm() returns a whole model object rather than a single number, it’s wrapped in list() so it can be stored in a single table cell; model[[1]] then unwraps that list to get the actual model back out, so coef() and summary() can work on it.

The result is one row per compound, with its own slope, intercept, and R², all pulled out using the same coef() and summary() from earlier on this page — just applied automatically to each group instead of repeating the code three times by hand.

Predict new values with the regression model

Standard approach: use predict() directly

In general, if you have new predictor values (x) and want predicted response values (y), use predict() directly.

If your model is absorbance ~ conc_ug_ml, that means:

  • input: new concentration values
  • output: predicted absorbance values

Rather than calculating the predictions separately and joining them back on afterwards, you can do it in one step with mutate(). Inside the pipe, . refers to new_conc itself as it flows through — so predict(model, newdata = .) tells predict() to use these new concentration values, and mutate() adds the result directly as a new column.

Going the other way: estimating x from new y values

The opposite direction can also be useful: if you have new response values (y) and want the corresponding predictor values (x).

In calibration work, this is common: you measure absorbance (response, y) and want concentration (predictor, x).

Because the fitted model is still absorbance ~ concentration, estimate concentration by rearranging the fitted line:

\[ concentration = \frac{\text{absorbance} - b}{a} \]

where a is slope and b is intercept.

Plot the regression with geom_smooth() and geom_abline()

geom_smooth(method = "lm") fits and draws the line directly.

Use geom_abline() if you want to draw a line from known coefficients.

Annotate equation and R2  on the plot

You can extract slope/intercept and R2, print them with cat(), and also place them on the plot.

Quick quality checks and common pitfalls

Simple linear regression works best when:

  • The relationship is approximately linear
  • Residual spread is reasonably constant
  • Outliers are checked and justified
  • Predictions are mainly within the measured range

How to use the diagnostic plot (plot(model, which = 1)):

  • X-axis: fitted values (predicted absorbance)
  • Y-axis: residuals (observed - predicted)

What you want to see:

  • A random cloud of points around 0
  • No clear curve pattern
  • Similar vertical spread across the full x-range

What can indicate a problem:

  • Curved pattern: the relationship may not be linear
  • Funnel shape (narrow on one side, wide on the other): non-constant variance
  • A few points far away from the rest: possible influential outliers to investigate

This plot is not a “pass/fail” test by itself. Use it as a quick visual check before interpreting slope, intercept, and predictions.

Starter diagnostic example: