Skip to contents

Prologue

In 2010 I was teaching an introductory statistics class to first-year psychology undergraduates, using base R. The experience of watching students try to learn statistics, programming, and the quirks of base R simultaneously — in a mandatory class that, if I’m honest, most of them probably didn’t want to take — made me want to do something to make the process less awful. So I wrote the first version of lsr to accompany my course notes. The course notes eventually became a textbook, Learning Statistics with R, and the package came along for the ride.

I wasn’t a very good programmer at the time. The code wasn’t well written, and I found the CRAN submission process genuinely hard to navigate — it was even more opaque then than it is now. But I did the best I could, students seemed to like it, and the package found an audience.

That was sixteen years ago. R has moved on since then, and so have I: I’m not an academic any more, and I no longer work in psychology. The package has been stable for a long time, and I’ve been reluctant to touch it precisely because it works well enough for what it was meant to do. But doing a maintenance pass on the code without also writing this article felt dishonest. Everything in lsr now has better alternatives elsewhere in the R ecosystem, and some of the choices I made are genuinely bad practice, not just outdated.

So, this is the article I wish had existed when I was first handing lsr to students: an honest account of what the package does well, where it falls short, and what to use instead once you’ve outgrown it.

What lsr is — and isn’t

lsr is a pedagogical scaffold. It is not a research tool, and it was never intended to be one. The functions wrap common statistical procedures to produce output that a beginner can read without already knowing what to look for. That’s the whole point, and for that purpose most of it works reasonably well.

It has another property that is worth naming honestly: it was assembled, not designed. I added functions whenever something occurred to me — if I was teaching a topic and thought a cleaner interface would help, I wrote one. There was no systematic plan, and the API reflects that. Some functions take a data frame and a formula, some take vectors, some take a table. If you have been using the package for a while and find yourself checking the help page every time you call a function, that’s why.

The rest of this article goes through the main areas of the package, explains what I think is good, what I think is problematic, and what I would recommend instead.

Hypothesis test wrappers

The six test functions — oneSampleTTest(), independentSamplesTTest(), pairedSamplesTTest(), associationTest(), goodnessOfFitTest(), and posthocPairwiseT() — are the core of the package, and they are the part I am most comfortable defending. The output is designed to be readable: hypotheses are stated in plain English, every component of the result is labelled, and an effect size is included automatically. For a student seeing a t-test for the first time, this is genuinely useful.

What the wrappers give up is flexibility. They cover a narrow set of use cases and give you very little control over what is computed or how. They also paper over some important distinctions. For example, independentSamplesTTest() always runs Welch’s t-test rather than Student’s, which is usually the right choice — but a student who uses this function without knowing that is learning a habit, not than a principle.

If you want hypothesis testing with clean, readable output but more flexibility and a better-developed theoretical framework, the infer package takes a simulation-based approach that connects naturally to how statisticians (well, some of them) think about inference. For output formatting in a research context, parameters from the easystats ecosystem produces well-organised tables suitable for reporting.

Effect sizes

cohensD(), etaSquared(), and cramersV() all work correctly and give sensible results for the standard cases they cover. The limitation is coverage: the functions compute a fixed set of effect size metrics with no support for uncertainty quantification, and the implementations have not kept pace with developments in the literature.

The effectsize package, also part of the easystats ecosystem, covers a much wider range of effect sizes, computes confidence intervals for them, and is actively maintained. If you are doing research rather than coursework, effectsize is the right tool.

Correlation matrices

correlate() produces a readable correlation matrix and, when test = TRUE, applies the Holm procedure to correct the p-values for multiple comparisons. This seems like a reasonable default, and it is what I would have recommended in 2010.

The problem is that the multiple testing correction approach is more contested than it looks. Running pairwise correlation tests and then correcting the p-values treats each correlation as independent of the others, but the correlations in a matrix are not independent — they are constrained by the structure of the data. Applying Holm gives a result that looks statistically careful without necessarily being so. This is the kind of subtlety a beginner cannot be expected to know, but that an intermediate user should be aware of.

The correlation package from the easystats ecosystem takes a more principled approach and supports a wider range of correlation types and correction methods. For anything beyond a simple exploratory look at pairwise associations, I would use that instead.

Data reshaping

wideToLong() and longToWide() convert between wide and long data formats by splitting column names on a separator character: if your columns are named score_pre and score_post, the function infers that score is the measurement variable and pre / post are the conditions. This is a tidy convention that works well in controlled examples.

It breaks down quickly with real data. Column names in practice are irregular, the separator you want is not always a single character, and even when the convention applies the results need careful checking. The functions also have no support for reshaping multiple measurement variables simultaneously.

tidyr::pivot_longer() and tidyr::pivot_wider() are the right tools for reshaping in modern R. They are more flexible, better documented, and handle edge cases that wideToLong() and longToWide() do not. The names_to and names_from arguments take a little getting used to, but the investment is worth it.

Plotting

bars() produces a bar chart with error bars. I included it because producing a decent bar chart in base R at the time required enough boilerplate to be distracting in an introductory class. It was always a minimal tool, and it remains one.

There is not much to say here: ggplot2 is the right way to visualise data in R. It has a steeper learning curve than bars(), but it also has an actual design and can produce essentially any plot you are likely to need. The time you spend learning ggplot2 will pay off in ways that learning bars() simply cannot.

Workspace utilities

This is the section where I feel the need to be most open about my poor choices.

who() works fine. It lists the objects in your workspace with their types and sizes. The problem is the name: I came to R from MATLAB, and I named the function after MATLAB’s who command without really thinking about whether the name made sense in R. It doesn’t especially, but it is also not actively harmful. In a modern IDE like Positron or RStudio the environment pane shows you the same information without any function call at all, so who() is solving a problem that has largely gone away.

rmAll() I am less comfortable defending. The function clears the workspace, which sounds useful if you want to start fresh. The problem is that wanting to “clear the workspace” is usually a sign of a deeper issue: a script that depends on whatever happens to be in the environment rather than being self-contained. The traditional idiom rm(list = ls()) has the same flaw, and I later wrote a separate package, sessioncheck, specifically to nudge beginners away from that practice. Providing rmAll() was, in retrospect, a step in the wrong direction. If you want a clean environment, restart your R session; if you want your script to be robust, write it in a way that is genuinely safe. Don’t write it in a way that leaves you vulnerable to whatever happens to be in the workspace, or the search path, or any of the other strange things that lurk underneath the surface of an R session.

unlibrary() is the function I feel most ambivalent about. The idea is reasonable enough — an easy way to detach a package, with an interface that mirrors library(). The problem starts one level up: library() is itself a confusing name, because you are loading a package, not a library in any sense that corresponds to how the word is usually used. Building unlibrary() on top of that doesn’t really clarify anything. If you need to detach a package, detach("package:name", unload = TRUE) is explicit, if not elegant; in practice the need arises rarely enough that it is not worth memorising either interface.

Epilogue

If you learned statistics using lsr and the accompanying textbook, I sincerely hope the experience was useful. The package was written to lower the barrier to entry for people who don’t always feel like they belong in a statistics class, and if it did that for you, it served its purpose.

The honest graduation path looks something like this. For tidy, readable statistical output in a research context, explore the easystats ecosystem: parameters for model summaries, effectsize for effect sizes, correlation for correlation matrices. For hypothesis testing with a modern inferential framework, look at infer. For data reshaping, learn tidyr::pivot_longer() and tidyr::pivot_wider(). For visualisation, invest in ggplot2.

None of these are difficult to pick up if you already have a working knowledge of R and basic statistics. The lsr versions of these things were designed to minimise what you needed to know up front; the alternatives are designed to maximise what you can do once you know a little more.

With the benefit of some time and distance, I’m glad I wrote lsr. That being said, I’m also very glad the R ecosystem has moved to a place where it is no longer necessary.