Panel regression and same store sales
Multi site businesses drown in a particular kind of confusion. The flagship had a record autumn, the small house had a soft year, marketing sent 40 percent more newsletters, and nobody can say which fact explains which. Panel regression is built for exactly this: data with a cross section of units (hotels, stores, markets, SaaS accounts) and a run of time periods at once. In this chapter I take five fictional hotels in the Salzkammergut through twelve quarters and show why a pooled regression flatters the newsletter and gets the sign on price wrong, how fixed effects compare each hotel only with its own history, what the Hausman test is really asking, why standard errors have to be clustered, and how the same machinery produces the like for like sales figure retailers publish. The residuals then do something a spreadsheet cannot: they show one hotel quietly falling behind itself for four quarters in a row, worth about €90,000, while its neighbours stay on track. This is part 9 of 21 of the Marketing Analytics series.
A good quarter or a good hotel?
Every January the five general managers of Seeblick Hotels sit around one table in Bad Ischl with the owner and a stack of printouts. The flagship on the Wolfgangsee reports its best autumn yet. Haus Bergblick, up the valley, reports a soft year and blames the weather, the road works and the booking platforms. Marketing reports 40 percent more newsletters than the year before and double the paid social. Everyone has a number. Nobody knows which number explains which.
The owner asked me a question I hear in some form from every multi site business: "Is Bergblick a bad hotel, or did Bergblick have a bad year?" Those are different problems with different fixes, and a spreadsheet that adds up all five houses cannot tell them apart. Panel regression can. It is the method for data with two dimensions at once, a cross section of units (hotels, stores, regions, accounts) and a run of time periods (weeks, months, quarters), and it is the statistical backbone of the "same store sales" figure every retailer publishes and few people can derive. This chapter shows how it works, what it says about marketing spend when the units differ wildly in size, and how it caught Bergblick underperforming its own history rather than its neighbours.
Where this sits in the series
We are in Part two of the series, dependent variable techniques, chapters 4 to 10. The previous chapter, on survival analysis, churn and customer lifetime value, followed individual customers through time until they left. This chapter also follows units through time, but the units are hotels and the outcome is bookings, so we are back in regression territory with a twist. The next chapter, on forecasting demand with autocorrelation and seasonality, takes the time dimension on its own and asks what next quarter looks like.
Why pooling everything hides the structure
Seeblick has five hotels and twelve quarters of data, 2023 to 2025. That is 60 rows, each with direct room nights, newsletter sends, paid social spend, the average room rate and the quarter. The obvious move is to throw the 60 rows into one regression and read off the coefficients. The marketing team had already done that in Excel and was proud of the result: every extra thousand newsletters brought 71 extra room nights, and a higher room rate went with more bookings, not fewer.
Both numbers are wrong in an instructive way. The hotel on the Wolfgangsee has 72 rooms, a lake terrace, a mailing list four times the size of the Gasthof Attersee's, and it charges €60 a night more. In the pooled data, high newsletter volume, high price and high bookings all travel together, not because newsletters or prices cause bookings but because they all belong to the big hotel. The regression sees a cloud of points and draws a line through it, and the line mostly describes hotel size.
The line chart below shows the raw material: the same summer bulge every year, and the levels stacked like shelves. The numbers throughout this chapter are illustrative, built to behave like the real thing.
Five houses, twelve quarters. The vertical gaps between the lines are the hotel effects; the wiggles within a line are what marketing and season have to explain.
The fix is to give every hotel its own baseline and ask a narrower question: when a given hotel sends more newsletters than usual, does it get more bookings than usual? That comparison is immune to hotel size, location and terrace views, because those do not change from quarter to quarter. It is called a fixed effects model, and the diagram shows the choice you are making.
Three ways to treat the hotel dimension. Pooled OLS pretends it is not there. Fixed effects give each unit its own intercept. Random effects treat the intercepts as draws from a distribution, which is efficient but only honest under a condition the Hausman test checks.
The panel model, with the maths
Write for direct room nights at hotel in quarter . The pooled regression the team ran is
where is newsletter sends in thousands, is paid social spend in thousands of euros, is the average room rate in tens of euros, is a single intercept shared by all hotels, the coefficients are the effects we want, and is everything the model does not explain. The trouble sits inside : it contains "this is the Wolfgangsee flagship", which is correlated with and , so the coefficients are biased.
The two way fixed effects model pulls the hotel and the quarter out of the error term and gives each its own parameter:
Here is the hotel fixed effect, one number per hotel that absorbs everything about that house that does not change over the three years: rooms, location, reputation, the breakfast. is the period effect, one number per quarter that absorbs everything that hit all five hotels at once: the season, a rainy July, a change in platform commission. The coefficients now measure only within hotel, within quarter variation, which is what we want.
You can estimate this with dummy variables, five for hotels and eleven for quarters. The more elegant route is the within transformation, which subtracts each hotel's own average from every observation:
where is hotel 's mean bookings over the twelve quarters and the bar means the same for each regressor. Notice that has vanished: it is constant within a hotel, so subtracting the hotel mean removes it exactly. Least squares on the demeaned data gives the same as the dummy version, and the transformation makes the meaning obvious. Each hotel is compared only with itself. The price coefficient now answers "when Bergblick raises its rate above its own typical level, what happens to Bergblick's bookings", not "do expensive hotels get more bookings".
Fixed or random effects: the Hausman intuition
There is a rival specification. Random effects keeps a hotel level term but treats it as a random draw rather than a parameter to estimate:
where is a common intercept, is the hotel's deviation from it with variance , and the last condition is the catch: the hotel's unobserved level must be uncorrelated with the regressors. Random effects uses both within and between variation, so its standard errors are smaller. It is the more efficient estimator when the condition holds and a biased one when it does not.
At Seeblick the condition is plainly violated: the big hotel sends more newsletters and charges more because it is the big hotel. The Hausman intuition is simply this: estimate both, and if the coefficients disagree by more than sampling noise could explain, the random effects assumption is broken and you keep fixed effects. Formally,
where and are the two coefficient vectors and the bracket is the difference of their covariance matrices. follows a chi squared distribution with as many degrees of freedom as there are coefficients tested. A large means the two estimates are further apart than chance allows.
The worked example: five hotels, twelve quarters
A few rows of the panel, so you can see what the data look like. Rooms available matters too, and I come back to it under same store sales.
| Hotel | Quarter | Direct room nights | Newsletter sends (k) | Paid social (€k) | Avg rate (€) |
|---|---|---|---|---|---|
| Seeblick am Wolfgangsee | 2025 Q3 | 3,520 | 48.0 | 9.5 | 218 |
| Haus Bergblick | 2025 Q3 | 1,990 | 19.5 | 5.5 | 149 |
| Gasthof Attersee | 2025 Q3 | 1,580 | 11.5 | 3.0 | 131 |
| Haus Bergblick | 2025 Q1 | 1,480 | 17.0 | 4.5 | 139 |
Now the three estimators side by side. Standard errors in brackets; the last column shows what happens to the fixed effects standard errors once they are clustered by hotel, which I explain below.
| Variable | Pooled OLS | Random effects | Fixed effects (hotel and quarter) | FE, clustered SE |
|---|---|---|---|---|
| Newsletter sends, per 1,000 | 71 (9) | 46 (10) | 38 (11) | 38 (16) |
| Paid social, per €1,000 | 9 (6) | 18 (7) | 21 (7) | 21 (10) |
| Average rate, per €10 | 12 (8) | minus 29 (13) | minus 44 (15) | minus 44 (22) |
| Q2 versus Q1 | 230 | 238 | 241 | 241 |
| Q3 versus Q1 | 590 | 612 | 624 | 624 |
| Q4 versus Q1 | 170 | 178 | 183 | 183 |
| R squared | 0.61 | 0.87 (within) | 0.87 (within) | |
| Hausman H (3 df) | 14.6 |
Reading it row by row. The newsletter coefficient nearly halves from pooled to fixed effects, 71 to 38. The pooled 71 was carrying the flagship's size on its back; the fixed effects 38 says that when any one hotel sends a thousand more newsletters than usual, it books about 38 more room nights than usual. At an average rate of roughly €160 that is about €6,000 of direct revenue per thousand sends, still a good trade, but half of what the team believed, and it changes what list growth is worth.
Paid social moves the other way, from 9 to 21. Pooled, it looked almost useless, because the small hotels spent proportionally more on it and had lower bookings for reasons that had nothing to do with social. Within hotel, an extra €1,000 brings about 21 room nights, roughly €3,400 of revenue. Positive return, and now visible.
The price coefficient matters most for the owner. Pooled says raising prices raises bookings, which is the flagship talking. Fixed effects says that when a hotel lifts its rate €10 above its own norm, it loses about 44 room nights a quarter. On a typical quarter of 1,800 nights that is an elasticity near minus 0.4 at the mean: rate rises still add revenue, but less than nights times price suggests. The seasonal effects barely move across the columns, which is reassuring: everyone has a summer.
The random effects column sits between the other two, as it must, because it blends within and between variation. The Hausman statistic of 14.6 on three degrees of freedom has a p value below 0.01, so the blend is not trustworthy here and we keep the fixed effects.
The clustered standard errors deserve a sentence. The 60 observations are not 60 independent draws; the twelve quarters of one hotel share whatever the model has missed about that hotel, so the residuals are correlated over time. Clustering by hotel lets the standard errors account for that. They grow by about 40 percent, and the newsletter effect goes from very confident to merely confident. With only five clusters the correction itself is shaky, which is one of the pitfalls below. I report both and say so.
What the fixed effects say about Haus Bergblick
The are not a nuisance; they are the second deliverable. Here they are, expressed as expected room nights per quarter at the group's average marketing and rate, with the residuals summed over 2025.
| Hotel | Rooms | Fixed effect (nights per quarter) | Nights per room | Sum of 2025 residuals |
|---|---|---|---|---|
| Seeblick am Wolfgangsee | 72 | 2,410 | 33.5 | plus 45 |
| Villa Traunsee | 48 | 1,660 | 34.6 | minus 20 |
| Haus Bergblick | 45 | 1,520 | 33.8 | minus 625 |
| Landhaus Mondsee | 38 | 1,310 | 34.5 | plus 60 |
| Gasthof Attersee | 34 | 1,090 | 32.1 | plus 35 |
The fixed effects, divided by rooms, show that the five houses are remarkably similar once marketing, rate and season are stripped out. Bergblick's 33.8 direct nights per room per quarter is right in the middle. It is not a bad hotel and it is not structurally behind its neighbours; had the owner looked only at totals she would have filed it as the smallest earner of the three mid sized houses and moved on.
The residuals say something else entirely. A residual is the gap between what a hotel actually booked and what its own fixed effect, the quarter effect and its own marketing predicted. For four hotels the 2025 residuals sum to something near zero, as they should. For Bergblick they sum to minus 625 room nights, and the pattern over time is the point.
Bergblick's quarterly residuals: eight quarters of noise around zero, then four quarters of a widening gap.
Twelve residuals, eight of them small and of mixed sign, then four in a row that are negative and growing: minus 95, minus 140, minus 210, minus 180. At Bergblick's average rate of about €145 that is roughly €90,000 of direct revenue that the hotel's own history, the group's marketing and the season all said should have arrived in 2025 and did not. That is what "underperforming its own history" means, and it is a very different diagnosis from "the smallest of the mid sized hotels". Road works, weather and platforms hit everyone, and the quarter effects already absorbed them. Something changed at Bergblick in the first quarter of 2025. In this fictional case it was a new revenue manager who had quietly shifted inventory to the platforms; in real cases I have seen a broken booking widget and a departed head receptionist. The panel model does not tell you the cause. It tells you where and when to look, with a number attached.
Same store sales and like for like comparisons
Retailers publish "same store sales growth" or "like for like sales" every quarter because total growth mixes two things: existing stores doing better or worse, and stores opening or closing. Growth of 8 percent with twelve new stores is a very different business from 8 percent with the same estate. The like for like figure keeps only stores that existed in both periods, at comparable capacity, and compares them with themselves:
where is the comparable set of units, is sales at unit in quarter , and is the same quarter a year earlier so the season cancels. That is the simple version. The panel fixed effects model is the same idea done properly: comparing a store with itself is exactly what the within transformation does, and the period effects are the "everyone had a bad quarter" adjustment the simple ratio cannot make. The residuals of the panel model are, in effect, a like for like figure per unit per period, corrected for the marketing and price that unit ran.
Two things about capacity. If a hotel closes a wing for renovation or a store shrinks its floor space, the unit is no longer comparable with its own history, so you either drop those periods or model bookings per available room; Landhaus Mondsee's winter refit in 2024 Q1 showed up as a mysterious dip until I did. And the comparable set must be fixed before you look at results. Deciding afterwards which stores count as comparable is how like for like numbers get flattered.
The same idea for SaaS accounts and multi market shops
The hotel example is the classic, but the structure appears everywhere I work. Werkbank, the fictional Rails SaaS for craft businesses I use across this series, has a few thousand accounts observed monthly: seats, logins, invoices sent, onboarding emails received, plan price. Accounts differ enormously in size, and a pooled regression of seat growth on onboarding emails mostly tells you that big accounts get more of everything. Account fixed effects ask instead whether an account that receives more onboarding touches than usual adds more seats than usual. The data are already in the right shape, one row per tenant per period, if the multi tenancy decisions in a Rails application that are expensive to reverse were made well: a tenant identifier on every table is what makes the panel possible later.
The Gift Bow, my Solidus demo hamper shop, sells into the UK and Austria: two markets and 104 weeks is a thin cross section, but even two market effects and week effects separate "the UK is a bigger market" from "the UK campaign in week 46 worked". Whenever your data have a "who" and a "when" at the same time, and the "who" differs in ways you cannot fully measure, you have a panel and should treat it as one.
How to run it
You need one row per unit per period, with the dependent variable, the marketing inputs aligned to the same period, the price, and a capacity measure. Twelve periods and five units is close to the minimum I would model; eight periods and three units is a chart, not a regression. Building the table is a morning in SQL; the within transformation itself is one window function, avg(x) over (partition by hotel), subtracted from each column. In Python the dummy variable version with clustered errors is a few lines, and I prefer it over the demeaned version because the fixed effects come out as named coefficients you can hand to the owner:
import pandas as pd
import statsmodels.formula.api as smf
df = pd.read_csv("seeblick_panel.csv") # one row per hotel and quarter
df["email_k"] = df["email_sends"] / 1000
df["social_k"] = df["paid_social_eur"] / 1000
df["rate10"] = df["avg_rate_eur"] / 10
fe = smf.ols(
"direct_nights ~ email_k + social_k + rate10 + C(hotel) + C(quarter)",
data=df,
).fit(cov_type="cluster", cov_kwds={"groups": df["hotel"]})
print(fe.summary())
df["resid"] = fe.resid # the Bergblick chart is this column
R users will reach for fixest, whose feols(y ~ x | hotel + quarter, cluster = ~hotel) is the most readable two way fixed effects specification I know.
Before trusting the result I do four checks. I plot residuals per unit over time, which is how the Bergblick pattern appeared in the first place. I compare the fixed and random effects tables and compute the Hausman statistic. I refit with the last two periods held out and check that the coefficients barely move. And I add next quarter's marketing spend as a regressor for this quarter's bookings: if future spend predicts current bookings, the causation is running backwards and the marketing team is reacting to bookings rather than driving them. From clean table to decision memo takes me about two weeks for a client of this size, and most of that is the data.
Pitfalls
Few clusters. Clustered standard errors are only asymptotically right, meaning they behave well with 40 or 50 clusters and poorly with five. With few clusters I report conventional, clustered and wild bootstrap standard errors, say which is which, and let the widest one drive any decision that costs money.
Time invariant variables disappear. Fixed effects absorb everything about a unit that does not change, so you cannot estimate the effect of a lake view, of being the flagship, or of a tenant being on the enterprise plan since day one. If that is the question, fixed effects is the wrong tool and you are back in cross sectional territory with all its confounding.
Marketing that reacts to sales. Hotels spend on paid social when bookings are soft and cut it when they are full. That produces a negative correlation between spend and bookings within hotel that has nothing to do with the effect of the spend. The lead test above catches the worst of it, lagging the spend by one period helps, and the proper fix is an experiment, which is Part five of this series. Fixed effects remove confounding between units; they do nothing about confounding within a unit over time.
Group level campaigns divided by five. When one brand campaign is run and finance splits the cost equally across the hotels, the "hotel level spend" is fiction and its coefficient is meaningless. Use spend that was genuinely targeted to the unit, or model the group campaign as a period effect, which is what it is.
Confusing a level with a trend. A hotel with a low fixed effect is smaller or worse located; a hotel with negative and growing residuals is changing. Bergblick had an ordinary level and a worrying trend. Reporting only the fixed effects would have missed the trend; reporting only totals would have misread the level. You need both tables, and you need enough periods: with four quarters the quarter effects eat everything and the marketing coefficients are noise. Three years of quarterly data is a workable minimum, and two years of weekly data beats five years of annual data.
How I do this for clients
The deliverable is a table of coefficients you can act on, a table of unit effects you can rank, and a residual chart per unit that tells you where to look this quarter. It comes with a one page decision memo: the number, its error margin stated honestly, and the risk if the number is wrong. For Seeblick the memo said, in effect, "list growth is worth about €6,000 per thousand contacts, rate rises still pay but at an elasticity near minus 0.4, and Bergblick has lost roughly €90,000 of direct revenue against its own history since January; find out why before you spend another euro on its marketing."
What I need from you is the panel: one row per unit per period, which usually means exports from the booking engine or shop, the newsletter tool, the ad accounts and the price list. If your systems cannot produce that table, that is the first finding and often the most valuable one; the data roles article explains who in a small business should own that pipe.
The first month works the way all my engagements do. A free workshop where we work out whether you have a panel at all and what the one question is. Then two weeks of real work on my time: the table, the models, the checks, the memo, before you commit to anything. After that it is either a fixed scope data science engagement or, where the answer feeds straight into spend decisions, a performance marketing retainer that can run on commission. You own the code, the data and the model; the pricing page sets out the terms in plain language. One accountable person, honest error bars, no vanity metrics.
Questions to ask your team or agency
- When you report that a channel works, is that within units over time, or is it mostly the big units having more of everything?
- Does every unit in the comparison have the same capacity in both periods, and who decided which units count as comparable, and when?
- Show me the residuals per unit over time. Which unit is drifting away from its own history, and since when?
- Are the standard errors clustered by unit, and how many clusters are there?
- Did you test whether next period's spend predicts this period's sales?
- Which of our questions are about differences between units, where fixed effects cannot help, and which are about changes within units, where they can?
This series is inspired by Mike Grigsby's Marketing Analytics (Kogan Page). The explanations, examples and numbers here are my own.