Samplyr

A statistically significant case for declarative code, drawn from a friend's new sampling grammar for R

sampling

Disclaimer

  1. Render unto Caesar what is Caesar's: Claude helped me find up-to-date documentation links.

  2. No other disclaimers for this article.



The message that found me in Thiès

It's Saturday, around 1pm. Usually I'm lying down at that time on a Saturday, but not this one, I was outside, under the Senegalese sun.

With a few good friends, I was wandering around Keur Cherif Lo, a small commune situated in western Senegal. It forms part of the Pambal district in the department of Tivaouane (Thiès region).

I was looking for a plot of land to buy when I got a Signal notification, a message from my friend and brother Dicko. He asked me to proofread some slides he'd written for a talk he's giving at the useR! 2026 conference. We're really proud of him.

I wasn't in front of my computer, and it was unbearably hot, so I replied that I'd read everything calmly once I got home. Which I did, of course, I'm a man of my word, but mostly because Dicko rarely writes, and when he does, it's always extremely interesting.... Music

land

Describe the what, not the how

I work in an industry where it's often preferable to describe what you want to obtain, rather than how to obtain it.

This has a name: declarative programming, as opposed to imperative programming. A declarative language lets you express the logic of a computation without spelling out its control flow: SQL, regular expressions, and Makefiles are the textbook examples. Everything below, from Django models to Kubernetes manifests to samplyr itself, is really the same idea wearing different clothes.

I first ran into this idea when I wrote my first lines of Django in 2006.

# models.py — we describe the shape of the data we want,
# not the SQL needed to create or query it.
class House(models.Model):
    region = models.ForeignKey("Region", on_delete=models.CASCADE)
    beds = models.PositiveIntegerField()
    treated_nets = models.PositiveIntegerField()

# Django's ORM (the "engine") takes care of the "how":
# generating the CREATE TABLE statement, indexes, migrations, etc.

In reality, I'd already run into the concept without recognizing it, without really realizing it, back in 2005, when I wrote my first lines of SQL. When you write a SQL query, you don't describe the "how", you describe the "what":

-- We describe the "what": the shape of the result set we want.
SELECT region, SUM(beds) AS total_beds, SUM(treated_nets) AS total_nets
FROM houses
GROUP BY region;

-- The query planner (the "engine") decides "how":
-- which indexes to scan, what join order to use, how to sort and aggregate.

And the pattern shows up in so many places. I write quite a few Kubernetes manifests these days:

# We describe the desired state: three replicas of this container,
# always running, always listening on port 8000.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: survey-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: survey-api
  template:
    metadata:
      labels:
        app: survey-api
    spec:
      containers:
        - name: survey-api
          image: registry.example.com/survey-api:1.4.0
          ports:
            - containerPort: 8000

# Kubernetes (the "engine") takes care of the "how":
# scheduling pods, restarting crashed containers, rolling out updates.

Where this connects to sampling

Because the solution Dicko came up with follows roughly the same pattern: describe the "what", not the "how", and let the library produce the right result. We're talking about statistics and sampling here, fields that are, in some ways, quite far from mine, and yet the parallel exists, and I find that genuinely interesting.

The real problem: sampling a population fairly

Here's the original problem: if you have a list of every house in a country and you want to know the number of beds and the number of insecticide-treated mosquito nets, how do you go about it?

You could, of course, draw 300 houses at random from the total. And after the draw, end up with 250 houses clustered around a single town, 49 a bit further away, and one lone house completely and utterly isolated from the other 299.

Is that representative of the whole population? And from a cost and logistics standpoint, do you really want to travel that far just to survey a single house?

Enter Samplyr: a grammar for sampling designs

What Dicko actually wants is to carry out sampling that's as representative as possible of the total population under study. He wants to be certain that whenever he draws a sample, it follows a specific formula, a specific design: stratum > cluster > stage > draw.

Writing that design in R meant that, rereading the code afterwards, it became very, very hard to recognize the design and the steps that made it up. To his surprise, the same design written in SAS or SPSS actually read like the design itself. So he decided to fix the problem: write his R code in a way that lets him clearly see and read the strata, clusters, stages, and draws.

challenge accepted

To solve this, he built a kind of grammar that helps him write the design directly in R. Its engine takes care of executing the design and correctly applying it to the data under study.


Eyes opening

I didn't think you had to account for all these considerations before running surveys. I guess I learned something today.

I'm also realizing that when I do random.sample(items, x), the result of this function call may not be as representative as I thought it is. This is a very good lesson.

random.sample() performs simple random sampling (SRS): every item has an equal, independent chance of being drawn, with no notion of the population's structure (no regions, no stratums, no clusters). SRS is unbiased in expectation, but for any single draw, a stratum can be over or under represented purely by chance, especially with small sample sizes. Here's a population of 1000 houses spread unevenly across three regions:

import random
from collections import Counter

random.seed(10)

# 1000 houses spread unevenly across 3 regions
population = (["Dakar"] * 700) + (["Thies"] * 200) + (["Ziguinchor"] * 100)

sample = random.sample(population, 20)
print(Counter(sample))
# Counter({'Dakar': 17, 'Thies': 3})
# Ziguinchor is 10% of the population, and got drawn zero times.

Is there a way to use those concepts on a daily basis ?

In my day-to-day work, as a software engineer, I don't do anything related to statistics or probability. The people who (maybe) do are the epidemiologists and biostatisticians in the epidemiology department of the medical research center I currently work at.

It's a shame, really: we never talk to each other, we never collaborate. They're off in one corner, and we're in another. On rare occasions, they hand us R scripts that we somehow manage to run, just to get a few charts out of it and that's it.... It's a real pity... Anyway...

I suddenly feel the urge to update my CV


More on this topic

I strongly and vividly encourage you to read Dicko's slides to learn more. And if you like what you see, please share it far and wide, because he uses free and open source software to address humanitarian problems in Africa.

Feel free to visit Dicko's website or the useR! 2026 conference website.