Chapter 13: Multiple Regression
Learning Objectives
- Explain why a single predictor can mislead, and how adding variables lets you compare observations while holding other variables constant.
- Build dummy variables in Excel, choose a baseline category, and avoid the dummy variable trap.
- Interpret the coefficients, R squared, adjusted R squared, overall F test and individual t tests of a multiple regression in business terms.
- Recognize omitted variable bias and multicollinearity, and check a model's residuals.
- Fit a multiple regression with
LINEST, the Analysis ToolPak and R for Excel, explain why the baseline category changes the coefficients, and use the model to predict a business scenario.
13.1 What Drives a Busy Day?
Dana Whitfield is planning next year's staffing budget. Every store manager wants more hours on busy days, and Dana wants to know what actually makes a day busy. "Is it the promotions? Weekends? The weather? Is a drive-thru worth what it costs to build? I need to know how much each of these matters on its own, not all tangled together."
That last phrase is the problem. In Ridgeline's data, these things are tangled together. Bigger stores are mostly in the city, where it's also busier for reasons that have nothing to do with seats. Mountain stores are colder and quieter. Some stores have drive-thrus and some don't. If you compare transactions on hot and cold days, you're also comparing summer with winter and Mountain stores with Metro stores. A simple regression with one predictor, like the ones in Chapter 12. Correlation and Simple Regression, can't untangle that. This chapter's tool can.
Marcus has a related question, left over from Chapter 10. Comparing Many Groups: ANOVA. Metro customers are less satisfied than Mountain customers, but Metro customers also wait much longer (5.8 minutes on average, against 3.1 in the Mountain stores). "Is something wrong with our Metro stores, or is it just the lines?" The same tool answers that too.
13.2 Why One Predictor Isn't Enough
Multiple regression extends the regression line to several explanatory variables at once.
Multiple regression — A regression with two or more explanatory variables: ŷ = b0 + b1x1 + b2x2 + … + bkxk. Each coefficient b is chosen by least squares, just as in simple regression.
The big change is in what each coefficient means. In simple regression, the slope of seats tells you how transactions differ between stores with more and fewer seats, and nothing else. Any other difference between those stores rides along. In multiple regression, the coefficient of seats tells you how transactions differ between store-days with more and fewer seats that are alike on every other variable in the model.
Holding other variables constant — The meaning of a coefficient in multiple regression: the predicted change in y for a one-unit increase in that x when every other explanatory variable in the model stays the same. It's also described as "controlling for" or "adjusting for" the other variables.
Think of it as a fairer comparison. In Chapter 9. Comparing Two Groups, you compared promo days with other days at the same store, a paired design, so differences between stores couldn't muddy the comparison. Multiple regression does something similar with arithmetic instead of design. It can compare weekend days with weekdays at the same store, the same temperature and the same promo status, even if no two days in the data match exactly.
The flip side is the danger of leaving something out.
Omitted variable — A variable that affects the response and is related to one or more explanatory variables but isn't included in the model. Leaving it out biases the coefficients of the variables it's related to, because they absorb its effect. This is called omitted variable bias.
An omitted variable is the regression version of the confounding variable from Chapter 9. Comparing Two Groups. You'll see a clear case of it in Ridgeline's data shortly.
13.3 Prepare the Data in Excel
Dana's question uses the DailySales Table, where each row is one store on one day of 2025: 4,380 rows. The variables you need are:
| Variable | Where it lives | Type |
|---|---|---|
transactions |
DailySales | numeric (the response) |
promo_day |
DailySales | Yes/No |
| weekend | not there yet | Yes/No, from day_of_week |
high_temp_f |
DailySales | numeric, °F |
drive_thru |
Stores | Yes/No |
seats |
Stores | numeric |
region |
Stores | Metro, Suburban or Mountain |
Two jobs come first: bring the store information into each daily row, and turn every categorical variable into numbers that a regression can use.
13.3.1 Add a weekend column
Click the first empty cell to the right of revenue in the DailySales Table, type the header weekend, and enter:
=IF(OR([@day_of_week]="Sat",[@day_of_week]="Sun"),"Yes","No")
The Table fills it down all 4,380 rows. Check it with =COUNTIF(DailySales[weekend],"Yes"), which should give 1,248 (104 weekend days at 12 stores).
13.3.2 Dummy variables
A regression needs numbers. Text like "Yes" and "Metro" has to be recoded.
Dummy variable — A column coded 1 when an observation is in a particular category and 0 when it isn't. A dummy lets a categorical variable enter a regression. Its coefficient is the predicted difference between that category and the baseline category, holding the other variables constant.
For a Yes/No variable you need one dummy: 1 for Yes, 0 for No. Add these columns to the DailySales Table, in this order, side by side:
| Header | Formula | What it holds |
|---|---|---|
promo01 |
=IF([@promo_day]="Yes",1,0) |
1 on a promo day |
weekend01 |
=IF([@weekend]="Yes",1,0) |
1 on Saturday or Sunday |
drive01 |
=IF(XLOOKUP([@store_id],Stores[store_id],Stores[drive_thru])="Yes",1,0) |
1 if the store has a drive-thru |
store_seats |
=XLOOKUP([@store_id],Stores[store_id],Stores[seats]) |
the store's seats |
temp |
=[@high_temp_f] |
the day's high temperature |
metro01 |
=IF(XLOOKUP([@store_id],Stores[store_id],Stores[region])="Metro",1,0) |
1 for a Metro store |
mountain01 |
=IF(XLOOKUP([@store_id],Stores[store_id],Stores[region])="Mountain",1,0) |
1 for a Mountain store |
XLOOKUP finds the row's store_id in the Stores Table and returns that store's drive-thru, seats or region. The temp column just copies high_temp_f. You'll see why it's there in a moment.
The DailySales Table scrolled right to show the new columns weekend, promo01, weekend01, drive01, store_seats, temp, metro01 and mountain01, all filled with 0s and 1s (or seats and temperatures). The formula bar shows =IF(XLOOKUP([@store_id],Stores[store_id],Stores[region])="Metro",1,0) for a selected metro01 cell.
Why the odd names? Later, R for Excel will merge
DailySaleswithStores, which already has columns namedseats,drive_thruandregion. Giving your helper columns different names (store_seats,drive01) keeps the two from colliding.
13.3.3 The baseline category and the dummy variable trap
Region has three categories, but you built only two dummies. That's deliberate.
Baseline category — The category of a categorical variable that gets no dummy. Its effect is built into the intercept, and every dummy's coefficient is a comparison with it. It's also called the reference category.
Here the baseline is Suburban. A Suburban store has metro01 = 0 and mountain01 = 0, so the model already knows where Suburban stores are. The metro01 coefficient will say how a Metro store differs from a comparable Suburban store, and mountain01 how a Mountain store does.
Why not add a third dummy, suburban01? Because every row would then have metro01 + mountain01 + suburban01 = 1. That's exactly the column of 1s that the intercept already uses. The model can't tell the intercept from the three dummies, and infinitely many sets of coefficients fit equally well. This is the dummy variable trap. Depending on the software, you get an error, a coefficient of NA, or a coefficient quietly set to 0. The rule: a categorical variable with k categories gets k − 1 dummies. Yes/No variables have two categories, so they get one dummy each.
Which category should be the baseline? Mathematically, any. The predictions are identical. Practically, choose the one that makes the comparisons you care about easiest to read. Suburban sits in the middle on most measures, so "Metro versus Suburban" and "Mountain versus Suburban" are natural.
13.4 How Multiple Regression Works
13.4.1 The idea
Multiple regression chooses the coefficients by least squares, exactly as in Chapter 12. Correlation and Simple Regression: it picks the set of b's that makes the sum of squared residuals as small as possible. With one x the fitted result is a line. With two it's a flat plane through a 3-D cloud of points. With seven you can't draw it, but the arithmetic is the same.
What makes "holding constant" work is that the coefficients are fitted together. If two predictors overlap, such as seats and region, least squares has to divide the credit between them. It gives each one only the part of the explanation that the other variables can't provide. That's why a coefficient can change, sometimes a lot, when you add or remove another variable.
13.4.2 Fit it with LINEST
LINEST handles several x columns at once, as long as they're next to each other. That's why you built the dummies side by side and copied temperature into temp. A structured reference like DailySales[[promo01]:[temp]] means "every column from promo01 through temp."
Start with the five predictors without region. Call it Model A. On a new sheet, enter:
=LINEST(DailySales[transactions],DailySales[[promo01]:[temp]],TRUE,TRUE)
Then Model B, which adds the two region dummies:
=LINEST(DailySales[transactions],DailySales[[promo01]:[mountain01]],TRUE,TRUE)
LINEST returns coefficients in reverse order. For Model B, the first row of the spill is mountain01, metro01, temp, store_seats, drive01, weekend01, promo01, and finally the intercept in the last column. Type a row of labels above the result, right to left, before you read anything. The second row holds the matching standard errors. Rows 3 to 5 hold R², the standard error of the regression, F, its degrees of freedom and the sums of squares in the first two columns only; the rest of those rows show
#N/A, which is normal.
A worksheet with =LINEST(DailySales[transactions],DailySales[[promo01]:[mountain01]],TRUE,TRUE) spilling into a 5-row by 8-column grid. A typed label row above it reads, left to right: mountain01, metro01, temp, store_seats, drive01, weekend01, promo01, intercept. The first row shows −83.46, 81.17, −1.58, 1.98, 48.82, 60.85, 32.82, 327.96. Rows 3–5 show values in the first two columns and #N/A in the others.
Rearranged into the usual order, here are both models side by side:
| Predictor | Model A coefficient | Model B coefficient | Model B 95% CI |
|---|---|---|---|
| Intercept | 137.6 | 328.0 | |
| Promo day (Yes = 1) | 36.9 | 32.8 | [30.1, 35.5] |
| Weekend (Yes = 1) | 60.9 | 60.8 | [58.7, 63.0] |
| Drive-thru (Yes = 1) | 44.1 | 48.8 | [46.7, 50.9] |
| Seats (per seat) | 6.24 | 1.98 | [1.85, 2.11] |
| High temperature (per °F) | −0.91 | −1.58 | [−1.64, −1.53] |
| Metro (vs. Suburban) | 81.2 | [78.3, 84.0] | |
| Mountain (vs. Suburban) | −83.5 | [−86.1, −80.8] | |
| R² | 0.572 | 0.868 | |
| Adjusted R² | 0.571 | 0.868 | |
| Standard error of the regression | 58.1 | 32.3 |
Every p-value in both models is below 0.001.
13.4.3 Reading Model B
Each coefficient is a difference holding the other variables in the model constant:
- Promo day: +32.8. A promo day brings about 33 more transactions than a non-promo day at the same kind of store, on the same kind of day, at the same temperature. That's close to the paired estimate of about 34 from Chapter 9. Comparing Two Groups, which is reassuring: two different methods, one answer.
- Weekend: +60.8. A Saturday or Sunday brings about 61 more transactions than a weekday, all else equal.
- Drive-thru: +48.8. A store with a drive-thru does about 49 more transactions a day than a comparable store without one.
- Seats: +1.98. Each additional seat goes with about 2 more transactions a day.
- Temperature: −1.58. Each degree hotter goes with about 1.6 fewer transactions. A 90°F day has about 32 fewer transactions than a 70°F day, all else equal.
- Metro: +81.2, Mountain: −83.5. Compared with a Suburban store that has the same seats and drive-thru status, a Metro store does about 81 more transactions a day, and a Mountain store about 83 fewer.
- Intercept: 328.0. The predicted transactions for a non-promo weekday at a Suburban store with no drive-thru, zero seats, and a high of 0°F. No such store-day exists. As in Chapter 12. Correlation and Simple Regression, the intercept is just where the plane is anchored. Don't interpret it.
13.4.4 A worked prediction
Dana asks for a concrete number: "What should Lakeside staff for on a Saturday promo day with a high of 70°F?" Lakeside is a Suburban store with a drive-thru and 44 seats. Put each value into Model B. Suburban is the baseline, so both region dummies are 0:
| Term | Coefficient | Value for the scenario | Product |
|---|---|---|---|
| Intercept | 327.96 | 1 | 327.96 |
| Promo day | 32.82 | 1 | 32.82 |
| Weekend | 60.85 | 1 | 60.85 |
| Drive-thru | 48.82 | 1 | 48.82 |
| Seats | 1.9775 | 44 | 87.01 |
| High temperature | −1.5838 | 70 | −110.87 |
| Metro | 81.17 | 0 | 0 |
| Mountain | −83.46 | 0 | 0 |
| Predicted transactions | 446.6 |
In Excel, type the coefficients in B2:B9 and the scenario values in C2:C9, and compute =SUMPRODUCT(B2:B9,C2:C9). You'll get the same prediction from R later, with an interval around it.
13.4.5 R squared and adjusted R squared
Model A explains 57% of the variation in daily transactions. Adding region raises that to 87%, and the typical prediction error (the standard error of the regression) falls from 58 to 32 transactions. Region matters a lot.
R² has a weakness in multiple regression: it never goes down when you add a variable, even a useless one. Add a column of random numbers and R² creeps up a little, purely by chance. Adjusted R² corrects for that.
Adjusted R squared — A version of R² that accounts for the number of explanatory variables. It rises when a new variable improves the model by more than chance would, and falls when it doesn't. Use it to compare models with different numbers of predictors.
- In words: adjusted R² = 1 − (1 − R²) × (n − 1) / (n − k − 1), where k is the number of explanatory variables.
- For Model B: 1 − (1 − 0.8678) × 4,379 / 4,372 = 0.8676.
With 4,380 rows and only seven predictors, the penalty is tiny, so R² and adjusted R² are almost equal. With small data sets, such as 104 staff, the gap is wider and matters more.
13.4.6 Omitted variable bias: why seats changed
Look at the seats coefficient. In Model A, each seat was worth 6.2 transactions a day. In Model B, it's worth 2.0. Nothing about the seats changed. What changed is that region joined the model.
Ridgeline's Metro stores are its biggest (45 seats on average, against 34.5 in Suburban and 28.5 in Mountain stores) and its busiest, for reasons beyond seats: downtown foot traffic, commuters, a university. In Model A, region was an omitted variable. Seats was the only variable that could stand in for "this is a big-city store," so it took credit for Metro's extra business. Once region is in the model, seats keeps only the part of the effect that belongs to seats.
Temperature moved too, from −0.91 to −1.58, in the other direction. The Mountain stores are both colder (an average high of about 48°F, against about 60–62°F elsewhere) and quieter. In Model A, a cold day partly meant "probably a Mountain store," which made cold weather look worse for business than it really is, and hid part of the true heat effect. Holding region constant uncovered it.
This is omitted variable bias at work, and it's the main reason multiple regression exists. For Dana, the practical lesson is sharp: Model A would have told her that adding 10 seats to a store brings about 62 more transactions a day. Model B says about 20. Using Model A to justify a renovation would badly overstate the payoff.
Adding variables can't remove every confounder. Model B holds constant the variables in it. Anything you didn't measure, such as parking, competition nearby or a manager's skill, can still bias the coefficients. Multiple regression on observational data shows associations adjusted for what you included. It doesn't prove cause.
13.4.7 The overall F test and the individual t tests
A multiple regression output has two kinds of test, and they ask different questions.
- The overall F test asks: do any of the explanatory variables help predict y? Its null hypothesis is that every slope is zero. For Model B, F = 4,099 with 7 and 4,372 degrees of freedom, and the p-value (Significance F) is far below 0.001. At least one variable matters, which is hardly a surprise.
- Each t test asks: does this variable add anything, given all the others? Its null hypothesis is that this one slope is zero, holding the rest constant. Each coefficient's t is its estimate divided by its standard error, just as in simple regression.
The two can disagree. A model can have a highly significant F while several individual variables are not significant: those variables add nothing once the others are known. You'll see exactly that in the satisfaction model below.
13.4.8 Multicollinearity
When explanatory variables are strongly related to each other, the model struggles to separate their effects.
Multicollinearity — Strong correlation among the explanatory variables of a regression. It makes the coefficients unstable and inflates their standard errors, so individual variables can look insignificant even when, together, they predict well.
The usual check is the variance inflation factor (VIF). For each explanatory variable, regress it on all the other explanatory variables and find that regression's R². Then VIF = 1 / (1 − R²). A VIF of 1 means the variable is unrelated to the others. A common rule of thumb treats values above 5, and certainly above 10, as a warning.
In Model B, the VIFs are all below 3: seats 1.91, temperature 1.15, drive-thru 1.22, and the region dummies under 3. Seats and region overlap, which is why adding region changed the seats coefficient, but not so much that the model can't tell them apart. Multicollinearity isn't a problem here.
13.4.9 Checking the residuals
The assumptions are the same as for simple regression: linearity, independence, constant spread and roughly normal residuals. Check them the same way, with residual plots. In multiple regression, plot the residuals against the predicted values, and against each numeric predictor.
For Model B, the plot against predicted values is an even band. The plot against temperature isn't quite. The residuals average about +8 on days with highs in the 50s and low 60s and about −12 on days in the 80s. The line for temperature is a little too simple. Transactions hold up through mild weather and then fall faster once it's hot, a curve that a single straight-line slope can't follow.
A scatterplot of Model B residuals (vertical axis, about −100 to +100) against high_temp_f (horizontal axis, 10 to 103°F), with a horizontal line at zero. The cloud bows slightly: residuals sit a little above zero in the 50s and 60s and below zero above about 75°F.
A residual plot like this tells you how to improve the model. One common fix is a helper column such as =MAX(0,[@high_temp_f]-65), "degrees above 65°F," which lets the model have one slope in mild weather and a steeper one in the heat. Try it as an extension.
Independence needs thought, too. The 4,380 rows are 12 stores observed every day, and a store's busy days tend to cluster. Store-level variables like seats, drive-thru and region vary across only 12 stores, not 4,380 independent observations. So their standard errors and confidence intervals are probably narrower than they should be. The direction and rough size of these effects are believable. The precision is overstated.
13.5 Describe the Data to AI
Open R for Excel. In 1. Data, choose Add a table… twice, once for DailySales and once for Stores. Type this data description into the 2. Ask your AI box:
Each row of DailySales is one store on one day of 2025. transactions is the number of customer transactions that day. promo_day is Yes or No. high_temp_f is the day's high temperature in °F. day_of_week is Mon to Sun. Each row of Stores is one store; join it to DailySales by store_id to get region (Metro, Suburban or Mountain), seats, and drive_thru (Yes or No). Ignore my 0/1 helper columns in DailySales.
Question: How much does each of these change daily transactions, holding the others constant: promo day, weekend (Saturday or Sunday), drive-thru, seats, high temperature, and region? Then predict transactions at the Lakeside store (Suburban, drive-thru, 44 seats) on a Saturday promo day with a high of 70°F.
I'm in an intro business statistics course and have learned simple and multiple regression, dummy variables and baseline categories, R squared and adjusted R squared, and prediction intervals.
Please fit a multiple regression with Suburban as the baseline region, and give a 95% prediction interval for the Lakeside scenario. Use base R only. Send the key results to Excel with excel_out(), and explain how to read them.
Click Copy prompt and paste the prompt into your AI assistant.
Needs: a reply (sent with the full R for Excel prompt) whose base-R script merges DailySales with Stores by store_id, creates a weekend variable from day_of_week, sets Suburban as the baseline with relevel(factor(region), ref = 'Suburban'), fits lm(transactions ~ promo_day + weekend + drive_thru + seats + high_temp_f + region), sends the model to Excel with excel_out(), and uses predict(..., interval = 'prediction') for the Lakeside scenario. The explanation should interpret coefficients as differences holding other variables constant. If the reply forgets the baseline or uses a package such as car, the text after this block uses that as the teaching example.
13.6 Read the Script
A script for this usually looks like the one below.
# Join store details (region, seats, drive_thru) onto each daily row
sales <- merge(DailySales, Stores, by = "store_id")
# Weekend = Saturday or Sunday
sales$weekend <- ifelse(sales$day_of_week %in% c("Sat", "Sun"), "Yes", "No")
# Make Suburban the baseline region (R would pick Metro, alphabetically first)
sales$region <- relevel(factor(sales$region), ref = "Suburban")
# Model A (no region) and Model B (with region)
model_a <- lm(transactions ~ promo_day + weekend + drive_thru + seats + high_temp_f, data = sales)
model_b <- lm(transactions ~ promo_day + weekend + drive_thru + seats + high_temp_f + region, data = sales)
excel_out(model_a, "Model A: transactions without region")
excel_out(model_b, "Model B: transactions with region")
# Predict the Lakeside scenario with a 95% prediction interval
lakeside <- data.frame(promo_day = "Yes", weekend = "Yes", drive_thru = "Yes",
seats = 44, high_temp_f = 70, region = "Suburban")
excel_out(predict(model_b, newdata = lakeside, interval = "prediction"), "Lakeside, Saturday promo day, 70F")
# Residuals against temperature
plot(sales$high_temp_f, resid(model_b), xlab = "High temperature (F)", ylab = "Residual")
abline(h = 0)
What each part does:
- The join.
mergematches each daily row to its store bystore_id, so every row now has region, seats and drive-thru. It's the R version of yourXLOOKUPcolumns. - Weekend. The
ifelseline builds the same Yes/No column you built withIF(OR(...)). - The baseline. This line matters most. R makes dummies from text columns automatically, which is convenient: you don't build
promo01ormetro01in R. But it picks the baseline alphabetically. For region, that's Metro.relevel(..., ref = "Suburban")makes Suburban the baseline instead. For the Yes/No columns, "No" comes first alphabetically, so No is the baseline and each coefficient is the effect of Yes, which is what you want. - The models. The formula
transactions ~ promo_day + weekend + …reads "transactions explained by promo day, weekend, and so on." The+signs just list the predictors. - The prediction. A one-row table describes the Lakeside scenario in the same words as the data.
predictwithinterval = "prediction"returns the predicted value and the 95% prediction interval. - The plot. Residuals against temperature, to look for the bend you saw earlier.
13.6.1 Why the AI's coefficients may look different
Suppose the AI's script doesn't include the relevel line. Then Metro is the baseline, and the output shows regionMountain and regionSuburban instead of regionMetro and regionMountain. The coefficients look completely different: Mountain −164.6 and Suburban −81.2, with an intercept of 409.1.
Nothing is wrong. Those numbers compare each region with Metro instead of Suburban. Mountain versus Metro is −83.5 − 81.2 = −164.6, and Suburban versus Metro is the mirror of Metro versus Suburban, −81.2. The other coefficients, R², the predictions and the prediction intervals are identical. Changing the baseline only changes which comparisons the output prints. Always check which category is missing from the coefficient list. That's the baseline, and every dummy is compared with it.
Watch for packages. If you ask an AI for VIFs, it will often write
library(car)andvif(model). Thecarpackage isn't part of base R, and R for Excel would have to download it. Ask for "base R only," or skip it: the VIF formula, 1 / (1 − R²), takes onelmper variable.
13.7 Run It in R for Excel
Paste the reply into 3. Run R code and click Run. If you get an error, click Copy a prompt to fix it and send that to the AI. A common one is a name collision: if DailySales already has a column named seats or region, merge renames the pair seats.x and seats.y, and the model can't find seats. That's why your helper columns have different names.
Send each output card to a New sheet, and click Insert into Excel under the plot.
R for Excel's Results area after the Model B script runs: output cards titled "Model A: transactions without region," "Model B: transactions with region" and "Lakeside, Saturday promo day, 70F," each with New sheet and At selected cell buttons, plus a residual plot against temperature with an Insert into Excel button.
Each model sheet is laid out like the Analysis ToolPak: Regression Statistics, ANOVA and a coefficients table. On the Model B sheet, R Square is 0.8678, Adjusted R Square 0.8676, Standard Error 32.3, Observations 4,380, F 4,099, and the coefficients match the table above, labeled with R's names (promo_dayYes, weekendYes, drive_thruYes, seats, high_temp_f, regionMetro, regionMountain).
The Lakeside card shows a fit of 446.6, with a 95% prediction interval from 383 to 510 transactions.
13.8 Interpret and Verify
13.8.1 The ToolPak with several X columns
- Choose Data > Data Analysis > Regression.
- Input Y Range: the
transactionscolumn with its header,$F$1:$F$4381on theDailySalessheet. - Input X Range: all seven helper columns at once, from the
promo01header to the bottom ofmountain01(for example$J$1:$P$4381). The ToolPak, likeLINEST, needs the X columns side by side, and it accepts up to 16 of them. - Check Labels and Residuals, choose New Worksheet Ply, and click OK.
Compare the ToolPak sheet with R's Model B sheet. They use different labels (promo01 in one, promo_dayYes in the other) but every number should match: R Square, the ANOVA table, each coefficient, standard error, t Stat, P-value and 95% limit. If one model has Metro as its baseline and the other Suburban, the region rows and the intercept won't match, for the reason explained above. Fix the baseline before you go looking for errors.
Then check the Lakeside fit against your SUMPRODUCT (446.6), and give it a reality check. Lakeside had seven Saturday promo days in 2025, with highs between 49°F and 81°F. Its transactions on those days ranged from 432 to 507, comfortably inside the prediction interval.
13.8.2 Write the conclusion
What drives a busy day, each holding the others constant. A weekend day brings about 61 more transactions than a weekday, a promo day about 33 more, and each degree of heat about 1.6 fewer. A drive-thru is worth about 49 transactions a day, and each seat about 2. Region is the biggest single factor: a Metro store does about 81 more transactions a day than a comparable Suburban store, and a Mountain store about 83 fewer. Together these explain 87% of the day-to-day variation in transactions. For staffing, plan Lakeside's Saturday promo days at about 450 transactions, and expect almost any such day to fall between roughly 380 and 510. Two cautions: these are associations in 2025 data, not controlled experiments, and the effects of store features like seats and drive-thrus rest on only 12 stores, so they're less precise than the output suggests.
13.9 The Same Tool, Three More Questions
Multiple regression can revisit questions from earlier chapters and sharpen their answers. Each of these uses one lm line in the same kind of script. The models are labeled C, D and E.
13.9.1 Is it Metro, or is it the lines?
In Chapter 10. Comparing Many Groups: ANOVA, average satisfaction differed by region: Metro 7.01, Suburban 7.83, Mountain 8.36. A regression of satisfaction on region alone (Model C0, Suburban baseline) restates that: Metro −0.83 and Mountain +0.52, both significant. Now add wait time, plus loyalty membership and order type (Model C):
# Region is in Stores, so join it onto each survey response
svy <- merge(Survey, Stores, by = "store_id")
svy$region <- relevel(factor(svy$region), ref = "Suburban")
model_c <- lm(satisfaction ~ wait_minutes + region + loyalty_member + order_type, data = svy)
excel_out(model_c, "Model C: satisfaction")
| Predictor | Model C0 (region only) | Model C (with wait) | p-value in Model C |
|---|---|---|---|
| Wait (per minute) | −0.33 | < 0.001 | |
| Metro (vs. Suburban) | −0.83 | −0.30 | 0.008 |
| Mountain (vs. Suburban) | +0.52 | +0.14 | 0.206 |
| Loyalty member (Yes) | −0.06 | 0.529 | |
| Drive-thru order (vs. Counter) | +0.06 | 0.618 | |
| Mobile order (vs. Counter) | −0.05 | 0.685 | |
| R² | 0.168 | 0.409 |
Read it carefully, because it's one of the most useful results in the book.
- Once wait is in the model, most of the regional gap disappears. The Metro gap shrinks from −0.83 to −0.30, and the Mountain advantage from +0.52 to +0.14, which is no longer significant.
- The arithmetic shows where the gap went. Metro customers wait about 1.55 minutes longer than Suburban customers (5.81 versus 4.26). At −0.33 points per minute, that's about 0.51 points of the 0.83-point gap. The other 0.30 is what's left: something about Metro stores beyond waiting. For Mountain, waits about 1.17 minutes shorter are worth about +0.38 points, and only +0.14 remains.
- Loyalty and order type drop out. Holding wait and region constant, neither one is related to satisfaction (p > 0.5). In Chapter 11. Relationships in Categorical Data, mobile customers were more likely to recommend Ridgeline. This model suggests why: mobile orders wait less. It's the wait, not the ordering channel.
- The F test and the t tests disagree, as promised. The overall F test is highly significant (F = 68.3, p < 0.001), while three of the six predictors aren't significant individually.
- Each extra minute of waiting costs about 0.33 points, holding region constant. That's a little less than the 0.37 from the simple regression in Chapter 12. Correlation and Simple Regression, because part of that simple slope was a Metro effect. Most of it survived.
The business message for Marcus and Rosa is clear: fix the waits, not the regions. Most of what looked like a Metro satisfaction problem is a Metro line problem, which is something Ridgeline can act on with staffing, equipment or mobile ordering. A smaller Metro gap of about 0.3 points remains, worth investigating separately.
13.9.2 Does training still matter, holding tenure and role constant?
In Chapter 9. Comparing Two Groups, trained staff had customer ratings 0.37 points higher than untrained staff. But trained staff also had more tenure (26.5 months versus 14.1) and were more often managers and shift leads. Is it training, or experience? Model D answers that, using the Staff Table:
# Baseline: completed_training = No, role = Barista (both first alphabetically)
model_d <- lm(customer_rating ~ completed_training + tenure_months + role, data = Staff)
excel_out(model_d, "Model D: staff rating")
| Predictor | Model D0 (training only) | Model D | p-value in Model D |
|---|---|---|---|
| Completed training (Yes) | +0.37 | +0.34 | < 0.001 |
| Tenure (per month) | +0.0053 | 0.019 | |
| Manager (vs. Barista) | −0.25 | 0.069 | |
| Shift Lead (vs. Barista) | +0.03 | 0.747 | |
| R² | 0.325 | 0.390 | |
| Adjusted R² | 0.319 | 0.365 |
Holding tenure and role constant, trained staff are rated about 0.34 points higher (95% CI 0.23 to 0.45). That's barely smaller than the 0.37 from the simple comparison. Tenure matters a little too: about 0.06 points per year. But tenure and role don't explain away the training gap. Here the gap between R² and adjusted R² is bigger than in the sales model, because there are only 104 staff.
This resolves the confounding question from Chapter 9. Comparing Two Groups as far as observational data can. Tenure and role weren't the explanation. Something else could still be, such as more motivated employees choosing to take the training. Only a randomized rollout, like the coupon test, could settle that completely.
13.9.3 A t-test is a regression
Run Model E on the coupon experiment:
# One Yes/No-style predictor: group is Control (baseline) or Coupon
model_e <- lm(spend_next_30 ~ group, data = CouponTest)
excel_out(model_e, "Model E: coupon spend")
The intercept is 21.94, the Control group's mean spend. The groupCoupon coefficient is 3.81, the difference between the two group means, with t = 4.14 and p < 0.001. Those are exactly the numbers from the equal-variance two-sample t-test (t = 4.14), and the Welch test in Chapter 9. Comparing Two Groups gave almost the same (t = 4.16). A regression with one dummy is a two-sample t-test. A regression with dummies for several groups is an ANOVA. Regression is the general tool that the earlier tests are special cases of, and its advantage is that you can keep adding variables to hold constant.
13.10 Summary
Multiple regression fits one equation with several explanatory variables, chosen by least squares, and each coefficient is a difference holding the other variables constant. Categorical variables enter as dummy variables: k − 1 dummies for k categories, with the missing category as the baseline that every dummy is compared with. In Excel, build dummies with IF and XLOOKUP, place the X columns side by side, and use LINEST (which returns coefficients in reverse order) or the ToolPak's Regression tool. In R, lm makes the dummies itself but chooses the baseline alphabetically, so use relevel to pick it, and expect different-looking coefficients when the baseline differs.
For Ridgeline's daily transactions, a weekend adds about 61, a promo day about 33, a drive-thru about 49, and each degree of heat costs about 1.6, with Metro stores about 81 above comparable Suburban stores and Mountain stores about 83 below. Adding region raised R² from 0.57 to 0.87 and cut the seats effect from 6.2 to 2.0 per seat, a clear case of omitted variable bias. VIFs below 3 showed multicollinearity wasn't a problem, and a residual plot against temperature showed where the model could improve. The same tool showed that most of Metro's satisfaction gap runs through its longer waits, that training is still worth about 0.34 rating points holding tenure and role constant, and that a regression with one dummy reproduces a two-sample t-test. Multiple regression adjusts for what you measure. It can't adjust for what you didn't.