A few months ago I started building something small and it's turned into the most useful thing I've shipped in a while, so I wanted to write up how it actually works, since a lot of the interesting decisions were in what I didn't build rather than what I did.
The problem, briefly
When I was doing my economics degree, a good chunk of the class either couldn't afford Stata or EViews, or had it installed and running but genuinely didn't understand what the output meant. Durbin-Watson, Breusch-Pagan, VIF — these get taught for maybe one lecture and then you're expected to just know how to read them for the rest of your degree. And your supervisor has thirty other students, so they can't sit with you through every regression you run.
So I built StatMate. You upload your data, tell it what you're testing (does X affect Y, a yes/no outcome, or something over time), it runs the actual statistical test, and it explains what came out in plain English instead of just spitting numbers at you.
It's live at getstatmate.com and the code's on GitHub if you want to poke at it.
The stack
Frontend's just HTML, JS, and Tailwind, sitting on Netlify. Backend's FastAPI on Render, and the actual statistics run through statsmodels, not anything I wrote myself. That last part matters more than it sounds — I'll get to why.
The part I actually spent time on: not letting anything guess a number
Here's the decision that shaped basically everything else about the project. When I got to the part where StatMate explains the results in plain economic language, the obvious move is to throw the numbers at an LLM and ask it to write something nice.
I didn't do that. Every explanation is built from a template engine with conditional logic — check the R-squared band, check which p-value threshold a coefficient falls under, check which diagnostics failed — and stitch together sentences from that. No language model touches the actual interpretation.
Why. Because an LLM narrating freely can restate a coefficient slightly wrong and sound completely confident doing it. For a tool students are trusting with an actual grade, "confidently wrong" is worse than "boring but correct." The template can only ever say what its logic explicitly allows, built directly off numbers that already came out of a real regression. It literally cannot invent a result.
A snippet of what that looks like, since code's more honest than a description:
def _sig_label(p):
if p is None:
return "not testable"
if p < 0.01:
return "significant at the 1% level"
if p < 0.05:
return "significant at the 5% level"
if p < 0.10:
return "significant at the 10% level"
return "not statistically significant"
Nothing clever. That's the point.
Handling three different kinds of questions
Students don't all need the same test. So the question type a student picks routes to a genuinely different engine:
- "Does X affect Y" → OLS, with the full diagnostic battery (Breusch-Pagan for heteroskedasticity, VIF for multicollinearity, Durbin-Watson for autocorrelation, Jarque-Bera for normality)
- "Does X affect the probability of Y" → logistic regression, reporting odds ratios and a pseudo R-squared instead of the OLS diagnostics, which don't apply here
- "Does X affect Y over time" → runs an Augmented Dickey-Fuller stationarity test on every variable before the regression, because regressing one trending series on another gives you a spurious, meaningless R-squared that looks great and means nothing
That last one is probably the thing I'm proudest of getting right. It's an easy mistake for a student to make and a genuinely common one in real undergrad time-series projects — running a regression on two things that are both just trending upward and mistaking the fit for a real relationship.
What breaks, and how I handle it breaking
Early versions of this just let a raw exception surface if someone picked a text column as their Y variable, or picked the same column for both X and Y. That's a bad experience for a non-technical user, so the validation layer now catches these specifically and returns a plain sentence instead of a stack trace — "this column contains text, not numbers" rather than a ValueError traceback.
There's also a genuinely fun one: when X variables are perfectly correlated, statsmodels throws a LinAlgError because the underlying matrix becomes singular. Caught that one specifically too, since it's a real and common student mistake, not a bug in my code.
What's next
Getting real students and lecturers to actually use it and tell me what's broken matters more right now than adding features. I've got a WhatsApp reply I'm hoping for from my department head, and I posted this on LinkedIn hoping a few coursemates mid-project would try it on real data.
Longer term: panel data support, cointegration testing for the time series path, and probably a downloadable report format so the output can go straight into an actual project write-up instead of living only on screen.
If you're a student, a lecturer, or just enjoy seeing what other people build, I'd genuinely like feedback: getstatmate.com, repo's here.
Top comments (1)
I appreciate how you prioritized accuracy over using a potentially flashy language model for result interpretation, opting instead for a template engine with conditional logic to ensure the explanations are built directly from the regression output. The example code snippet for
_sig_label(p)illustrates this approach nicely, making it clear that the focus is on reliability rather than generating overly complex narratives. It's also commendable how StatMate handles different question types by routing them to distinct engines, such as OLS for "Does X affect Y" or logistic regression for probability assessments. Have you considered incorporating additional diagnostic tools or tests for more complex scenarios, such as panel data analysis or dealing with missing data, to further enhance StatMate's utility for students?