Teaching statistics without teaching R: a Shiny app revisited
A while back, I wrote about a Shiny app I built for my Master students at Université Bordeaux Montaigne, one that automates the choice between a χ² test and Fisher’s exact test of independence depending on how the data are distributed. If you missed that post, the gist is this: ready-made corpus tools like AntConc or #LancsBox are excellent, but they do not hold your hand when it comes to selecting the right statistical test for your data. My app was a first attempt at filling that gap. It was functional, but far from perfect. This post is about what I did next.
The app now has a name: TestWise. The revised and (I dare say) definitive version is available on my GitHub repository. For those who want to understand what changed and why, read on.
A quick recap for latecomers
The original app was straightforward. Students upload a contingency table (in .xlsx, .csv, or .txt format), click “Submit”, and the app does two things automatically: it selects the appropriate test (χ² if all expected cell frequencies are above 5, Fisher’s exact test otherwise) and it generates a visualisation, namely an association plot for the χ² test, a mosaic plot for Fisher’s, along with frequency tables and a p-value with a plain-English interpretation. No R knowledge is required to run it, but the commented code on GitHub was always there for those who wanted to peek under the hood. Pedagogically speaking, I was going for a “black box with a transparent lid” kind of design.
The app worked. Students liked it. Then they started asking questions. Can I save the results? Can I export the plot? What does Cramér’s V mean and can the app compute it? Fair enough. That is when I rolled up my sleeves.
What changed and why
The revised app introduces a series of improvements that fall into three broad categories: architecture, pedagogy, and polish. Let me walk through them.

A reactive results architecture
This is the most invisible improvement. Students will not notice it, but it makes everything else possible. In the original code, all the computed results (observed frequencies, expected frequencies, residuals, p-value, test type, and so on) lived in scattered local variables inside a single observeEvent block. That worked fine as long as the results only needed to be displayed. The moment a student wanted to reuse those results (say, to generate a downloadable report), they had a problem. Local variables do not survive outside the block in which they are born.
The solution was to store all results in a single reactive value:
results <- reactiveVal(NULL)
You can think of this as a small warehouse that holds everything the app has computed after the user clicks “Run analysis”. Whenever a download button is pressed, it goes to the warehouse and retrieves what it needs.
A three-tab layout with guided navigation
The main panel is now organised into three tabs: Data preview, Results, and About this app. More importantly, the app switches tabs automatically: uploading a file jumps to the preview tab so students can verify their table was read correctly before running anything; clicking “Run analysis” jumps straight to the results. Students always know where to look next, without having to hunt around.

The sidebar is also restructured into three numbered steps: Upload, Options, Run. This makes the intended workflow explicit. Download buttons only appear after a successful analysis, so the interface is never cluttered with options that are not yet relevant.

A data preview tab
Students can now see their uploaded table before running the analysis. If the file was read incorrectly (e.g, wrong column separator, missing row labels, a stray empty column) they catch it immediately rather than after a confusing error message. This sounds minor but in practice it saves a lot of confusion, especially with students who are new to data tables, or not sure whether their .csv uses commas or semicolons.

Effect size: Cramér’s V
This was the most glaring statistical omission in the previous version. The app now computes Cramér’s V alongside the χ² test and displays it with a plain-English label (weak / moderate / strong) and a note explaining why it matters: a statistically significant result with a weak effect size may have limited practical relevance. Always interpret effect size alongside the p-value!

Cramér’s V is not computed for Fisher’s exact test, where the odds ratio is the more natural measure for 2×2 tables. The app says so explicitly rather than silently omitting it.
A configurable significance threshold
The significance level α is no longer hardcoded at 0.05. Students can set it to 0.01 or any other value; the interpretation text adapts accordingly. This is a small change with real pedagogical value: it makes α a manipulable parameter rather than a number that just appears from nowhere. In my experience, students who have adjusted α at least once have a much more intuitive grasp of what it means.

Monte Carlo simulation
The Monte Carlo option for Fisher’s exact test was present in the original code. Now, if the user forgets to tick the box and fisher.test() crashes with a workspace error on a large sparse table, the app now catches that error automatically and retries with Monte Carlo simulation, rather than showing the user an opaque crash message.

Tooltip help on every option
Each option in the sidebar now has a hover tooltip (powered by shinyBS) explaining what it does in plain language. The Monte Carlo tooltip explains what the simulation actually does. The α tooltip explains the 5% risk threshold in concrete terms. Students do not need to know R to use the app, but they do need to understand what they are asking it to do.


An “About” tab with a full in-app glossary
Rather than sending students to an external resource every time they encounter an unfamiliar term, TestWise now includes a dedicated tab covering: the null hypothesis, p-values, Cramér’s V, Pearson residuals, and how to read both the association plot and the mosaic plot. It also includes an honest caveat about the mosaic plot, which I will come to below.



Exporting results
Students can now download a formatted HTML report (via rmarkdown and kableExtra, with properly styled tables) and a publication-quality PNG of the plot at 300 DPI with user-controlled dimensions. Default dimensions are computed dynamically from the size of the uploaded table. The HTML report includes Cramér’s V when available, and a footer crediting the app and the licence.

Dynamic plot sizing and the overlap fix
The original app had a fixed plot height of 500 pixels regardless of table size, which made large tables either unreadable or cut off. The revised app computes plot height from the number of rows and columns:
base_h <- 350
plot_height_px <- max(base_h, base_h + 55 * (num_rows - 1) + 35 * (num_cols - 1))
A bug that surfaced during testing made the plot overflow into the interpretation text directly below it: the height was being passed to renderPlot() on the server side but not to plotOutput() on the UI side, so the HTML container stayed at Shiny’s default 400px while the rendered image was taller. The correct fix is to set the height on plotOutput(), where the container lives:
plotOutput("plot", height = paste0(r$plot_height_px, "px"))
Removing the height argument from renderPlot() entirely avoids any further mismatch between the two sides.
The mosaic plot caveat
I want to say something explicitly here, because the previous version of this app was quietly misleading on this point. The mosaic plot is not the canonical or default companion to Fisher’s exact test. The association plot has a tight statistical justification: it was designed specifically to visualise Pearson residuals from the χ² test. The mosaic plot is a general-purpose tool for contingency tables. It works equally well with either test, and a heatmap or a bar chart of proportions would be just as valid.

The mosaic plot is used in TestWise for pragmatic reasons: it gives a clear visual impression of cell frequencies and is immediately readable to most students. But I did not want students to walk away thinking that “Fisher’s test → mosaic plot” is some kind of natural law, any more than “χ² test → association plot” is. Both the results panel and the About tab now say this explicitly. The key results are always the p-value and the frequency tables; the plot is a reading aid.
What did not change and why
The core statistical logic is unchanged. The app still runs a χ² test first to obtain expected frequencies, then uses those to decide which test to apply. Fisher’s exact test is triggered whenever any expected cell frequency falls below 5. The decision is automatic and explained to the student in plain language every time.
Limitations I am still sitting with
Any tool that automates statistical decision-making risks giving students a false sense of mastery. My best defence against this is the commented source code on GitHub and the habit, which I try to cultivate in class, of asking students to explain why the app chose one test over another before they interpret the results. The app is a scaffold, not a crutch.
Scalability is a perennial caveat. TestWise works well for the small to medium tables that are typical in a corpus linguistics student project. Very large tables may require further optimisation, though the automatic Monte Carlo fallback handles most cases gracefully.
Let me know what you think!
TestWise is a more complete tool than its predecessor: the same pedagogical philosophy, but with the affordances that students actually need: export, effect size, in-app explanations, and a workflow that does not require them to already know what they are doing. If you used the first version in your classes, I think you will find this one considerably easier to work with. If you are coming to it fresh, the logic is the same as I described in the original post. You just now get to take the results home with you, and the app takes a little more care to explain itself along the way.
Feedback, bug reports, and pull requests are always welcome on GitHub. And if you use this in your teaching, I would genuinely love to hear how it goes.
The text only may be used under licence Creative Commons Attribution Non Commercial 4.0 International. All other elements (illustrations, imported files) are “All rights reserved”, unless otherwise stated.
OpenEdition suggests that you cite this post as follows:
Guillaume Desagulier (March 30, 2026). Teaching statistics without teaching R: a Shiny app revisited. Around the word. Retrieved May 20, 2026 from https://doi.org/10.58079/15yw4


orcid.org/0000-0003-4895-0788