<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <atom:link href="https://max.pm/posts/rss.xml" rel="self" type="application/rss+xml" />
        <title>Max R. P. Grossmann</title>
        <link>https://max.pm/posts</link>
        <description>Mostly facts and logic. And, sometimes, unbridled emotion.</description>
        <language>en</language>


        <item>
<title>The world-historical mess of using proxies with software written in Go</title>
<link>https://max.pm/posts/go-proxy-mess/</link>
<guid>https://max.pm/posts/go-proxy-mess/</guid>
<description><![CDATA[
Two of my favorite pieces of software are written in Go: Syncthing, which does continuous file synchronization, and restic, which does encrypted, deduplicated backups. Both are truly, truly excellent &mdash; some of the best software ever written: reliable, well documented, and maintained with obvious care. This post is not about a flaw in either of them. It is about what happens when you try to run Go programs over a proxy &mdash; and about a mess that sits in Go's libraries, beneath both.

Why you would proxy Syncthing at all

It can be a good idea to run Syncthing over a proxy such as Tor. Your Syncthing device ID is not exactly a secret: you hand it to everyone you sync with. Anyone who ever knew your device ID can query Syncthing's global discovery servers for the addresses your device announces and can therefore permanently track your IP address and also know where you are. Routing Syncthing through Tor decouples your device ID from your physical location. I currently use Syncthing only with my own devices, but for you, that may matter (e.g., if you use Syncthing to securely collaborate with coauthors and share data, which I have done in the past).

Syncthing gets proxying right

Syncthing supports this out of the box via environment variables. In a systemd unit:

[Service]
Environment="ALL_PROXY=socks5://127.0.0.1:9050"
Environment="ALL_PROXY_NO_FALLBACK=1"
Environment="NO_PROXY=192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"

Three things are going on here:


ALL_PROXY points at the local Tor SOCKS5 port.
ALL_PROXY_NO_FALLBACK is essential. Without it, Syncthing races the proxied connection against a direct one and falls back to connecting directly if the proxy is down or slow &mdash; which would silently deanonymize you.
NO_PROXY exempts the RFC 1918 private ranges, so devices on your own LAN are still reached directly instead of being pointlessly (and unsuccessfully) routed through Tor. CIDR ranges, single IPs, hostnames and domain suffixes are all supported.


Under the hood, Syncthing builds its dialing on golang.org/x/net/proxy, and credit where credit is due: this is a clean, well-behaved little library. It reads ALL_PROXY and NO_PROXY in both spellings, and &mdash; best of all &mdash; when given a hostname, its SOCKS5 implementation sends that hostname to the proxy (SOCKS5 address type 0x03) instead of resolving it locally, so name resolution happens on the far side of the proxy. That is exactly the right default: DNS leaks are the classic way to ruin an otherwise proxied setup. In curl's terminology this is socks5h:// behavior; the library treats socks5:// and socks5h:// identically, and in the safe direction.

So far, so pleasant. Now suppose that, out of curiosity, you try the same trick with another Go program you trust. Say, restic.

The world-historical mess

Set ALL_PROXY=socks5://127.0.0.1:9050 for restic and run a backup to an S3 bucket. It completes flawlessly. There is just one small problem: none of that traffic went through your proxy. No error, no warning &mdash; your backup traffic, and your DNS lookups, went out over the plain network.

This is not restic's fault. Restic does the textbook-correct thing: it builds its HTTP transport with Proxy: http.ProxyFromEnvironment, the standard mechanism that virtually every Go HTTP client uses. The problem is what that mechanism actually reads.

Go has two entirely separate libraries for reading proxy configuration from the environment, and they disagree about which variables exist:


net/http.ProxyFromEnvironment, wired into http.DefaultTransport and copied into countless custom transports, calls httpproxy.FromEnvironment. Its source is unambiguous: it reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY (plus lowercase variants) and nothing else. The variable that curl and much of the Unix world have honored for decades is not rejected and not warned about &mdash; it is simply invisible.
golang.org/x/net/proxy &mdash; the nice library from above &mdash; does read ALL_PROXY. But net/http does not use it. Unless a program's author explicitly wired it into their dialing path, as Syncthing's did, it might as well not exist.


So whether ALL_PROXY does anything depends entirely on which Go program you happen to be running, and you cannot tell without reading its source. The Go team is aware: issue #31813, filed in 2019 by Brad Fitzpatrick himself, proposes that net/http.DefaultTransport honor ALL_PROXY. As of this writing it is still open. Related warts go back even longer: #16715 (for an unregistered non-SOCKS scheme, FromURL returns unknown scheme, while FromEnvironment swallows that error and silently returns the direct dialer) and #13456 (for a while, only the lowercase all_proxy spelling was read).

For restic specifically, the fix is to use the variables that net/http does read:

HTTPS_PROXY=socks5h://127.0.0.1:9050

(plus HTTP_PROXY if your backend is reached over plain HTTP).

Takeaways

First: for Go programs that use the standard HTTP transport, set HTTP_PROXY and HTTPS_PROXY, not ALL_PROXY.

Second: never just assume that setting a proxy variable did anything. The simplest test is to point the variable at a port where nothing is listening and check that the program now fails.

Third: if the proxy is security-critical, look up how the specific program dials out. Even a program that honors ALL_PROXY may, like Syncthing without ALL_PROXY_NO_FALLBACK, treat it as a suggestion rather than a rule.

None of this diminishes restic or Syncthing &mdash; they remain some of the finest software in existence, and Syncthing in particular went out of its way to get proxying right. The mess lives a layer below them, in a standard library and an extension library that have disagreed about the environment for a decade.
]]></description>
</item>
<item>
<title>Five years of unauthenticated remote data exfiltration and destruction in oTree, the world’s most popular experimental economics framework</title>
<link>https://max.pm/posts/otree-ws-vuln/</link>
<guid>https://max.pm/posts/otree-ws-vuln/</guid>
<description><![CDATA[

    tl;dr: If you are using oTree 5 or 6, you must immediately upgrade to oTree 5.11.5+ or 6.0.14+ and ensure that OTREE_AUTH_LEVEL=STUDY is set — the upgrade alone does nothing on a default deployment, such as on Heroku. See Mitigation for the exact steps.



    Disclosure: I am affiliated with uproot, a non-profit FLOSS competitor of oTree.


For over five years, any participant who received an oTree start link (a Prolific or MTurk worker, a lab undergraduate, a colleague invited to pilot) could download the built-in CSV data exports for every session the server had ever run, and permanently delete any of those sessions. No special access was required. A start link already contains the server's URL, and the two endpoints involved — /export and /delete_sessions — performed no authentication check.

oTree uses two communication channels: ordinary HTTP requests (page loads, form submissions, admin actions) and WebSockets (real-time features, including data export and session deletion). The authentication logic existed in the code but was only wired up for HTTP requests. WebSocket connections were registered through a separate code path that skipped authentication entirely. Since /export and /delete_sessions are WebSocket endpoints, setting an admin password, setting OTREE_AUTH_LEVEL=STUDY, or running in production mode made no difference — a server with every documented security setting at its strictest value was as exposed as a default install.

On 17 April 2026, oTree 5.11.5 and 6.0.14 shipped a partial fix for a vulnerability I discovered and privately reported to oTree's maintainer on 16 April. The vulnerability has been in the framework since the first beta of oTree 5 on 21 February 2021 (with earlier alphas affected since at least December 2020). oTree publishes no SECURITY.md, no PGP key, and no secure disclosure channel of any kind.

Between that initial disclosure and this post's publication on June 15, I worked tirelessly to discreetly contact dozens of experimenters, lab managers, and research-support staff around the world responsible for live or recently used oTree deployments. oTree has no official advisory channel or user registry, and the maintainer issued no public warning, so individual outreach was the only way to reach affected operators. That outreach was necessarily incomplete — this post is intended to reach the operators it could not.

Table of contents


    


Mitigation

Two things have to be true, at the same time, for your server to be safe from this bug: (1) you must be running a patched version of oTree, and (2) OTREE_AUTH_LEVEL must be set to STUDY (or DEMO) in the environment that actually runs oTree. Missing either one leaves /export and /delete_sessions reachable without credentials. The variable is read directly from the process environment at startup by otree/settings.py (AUTH_LEVEL = os.environ.get('OTREE_AUTH_LEVEL')), so changes only take effect on the next restart of every web and worker process. An empty string counts as "unset" for this purpose.

While you are in the environment, also set (or verify) two adjacent variables: OTREE_ADMIN_PASSWORD (a strong random password — the default project skeleton wires this through to ADMIN_PASSWORD = environ.get('OTREE_ADMIN_PASSWORD'), and without it the admin UI lets anyone in), and OTREE_PRODUCTION=1 (otherwise DEBUG is on, which exposes tracebacks and the unauthenticated demo pages).

On Heroku


    Click here to download an easy-to-understand guide for hardening a Heroku deployment.


Heroku is the deployment target oTree's own documentation walks operators through, and it is also where the default configuration is most obviously unsafe — the buildpack sets nothing auth-related for you. From a shell with the Heroku CLI authenticated and your app's git remote configured, run:

heroku config:set \
    OTREE_AUTH_LEVEL=STUDY \
    OTREE_ADMIN_PASSWORD='&lt;a long random password&gt;' \
    OTREE_PRODUCTION=1 \
    --app YOUR-APP-NAME

heroku config:set restarts all dynos automatically, so the new environment is picked up by both the web and worker processes (oTree's default Procfile runs otree prodserver1of2 and otree prodserver2of2 respectively, and both read OTREE_AUTH_LEVEL). Confirm with:

heroku config --app YOUR-APP-NAME | grep OTREE_

Then pin the patched oTree in requirements.txt — the skeleton ships a requirements.txt with a comment saying oTree may overwrite it, which you should remove before pinning so your pin survives:

# remove the "oTree-may-overwrite-this-file" header, then:
otree==5.11.5     # or otree==6.0.14 if you are on the 6.x line
psycopg2&gt;=2.8.4

Commit and deploy:

git add requirements.txt
git commit -m 'Pin patched oTree (WS auth fix)'
git push heroku main       # or: git push heroku master

If you run oTree from a container or a private registry rather than from a git push, rebuild the image against the pinned version and re-release it; heroku config:set alone does not upgrade the oTree package.

On anything else (systemd, Docker, a VM, a dedicated host)

Install a patched version of oTree, e.g., using pip install -U 'otree==5.11.5'.

The same two variables have to reach the oTree process. For systemd, add them to the unit's Environment= lines (or an EnvironmentFile=) and run systemctl daemon-reload &amp;&amp; systemctl restart otree. For Docker Compose, put them under environment: for every service that runs otree prodserver* (web and worker) and docker compose up -d. For a plain shell-started server, export OTREE_AUTH_LEVEL=STUDY before the otree prodserver invocation; putting it only in an interactive shell's .bashrc is not enough if the server is started by a process manager.

Verifying

After restarting, log in to the admin interface and open /server_check. You want a green alert reading "Password protection is on. Your app's AUTH_LEVEL is STUDY"; if instead you see the red "No password protection" alert, the variable did not reach the process and the WebSocket endpoints are still open. While there, also confirm the "DEBUG mode is off" alert is green.

Finally: setting these variables fixes the specific hole described in this post, but it does not retroactively tell you whether your data was exported or your sessions were wiped during the five years the endpoints were open. oTree writes no application-level audit log for either handler, so there is nothing in oTree itself to consult — but your reverse proxy may have kept records (see Checking your logs). The only durable mitigation is what the closing paragraph of this post already says — run oTree servers only for as long as a study strictly requires, and tear them down afterwards.

Checking your logs

Self-hosted setups

Although oTree keeps no record of requests to these endpoints, your reverse proxy almost certainly does. A WebSocket connection begins as an ordinary HTTP GET request; a successful upgrade is logged by nginx with status code 101. The two paths to look for are /export and /delete_sessions.

On a typical nginx setup:

# current (uncompressed) log:
grep -E '"GET /(export|delete_sessions) .+" 101 ' /var/log/nginx/access.log

# rotated logs (often gzip-compressed):
zgrep -E '"GET /(export|delete_sessions) .+" 101 ' /var/log/nginx/access.log.*

Any match is a successful WebSocket connection to one of the two vulnerable endpoints. Compare the source IP addresses against those you recognise (your own machine, your lab's VPN, etc.). A hit from an unfamiliar address is evidence that the endpoint was reached — and since neither endpoint required authentication, reaching it was sufficient to use it. Treat the server as compromised.

Heroku

On Heroku, the platform router logs every request but retains only a short rolling buffer (roughly 1,500 lines) unless you have configured a log drain. If you have a drain, search it the same way; otherwise the window has almost certainly closed for older requests. To search what Heroku still has:

heroku logs --num 9999999 --app YOUR-APP-NAME \
    | grep -E 'path="/(export|delete_sessions)"'

If either command produces output, treat the server as compromised: assume the exportable study data was read, and assess whether any sessions were deleted that you did not delete yourself.



Introduction

oTree is the dominant framework for online economic experiments. Non-free but source-available, it is used by hundreds of research groups. The accompanying paper has been cited thousands of times. I have extensively researched with and lectured on oTree. It runs paid studies on Prolific and MTurk. It stores participants' decisions, identifiers, and, depending on the experimenter, payoff information and free-text responses that are routinely personally identifiable.

Dissertations, tenure cases, replication packages, and grant-funded studies depend on data exports that for five years could be downloaded by anyone with curl, and on sessions that could be deleted by anyone willing to type a second command. The framework's selling point is that the researcher does not have to think about web infrastructure. That selling point requires that the framework actually handles web infrastructure correctly.

A note on sources before going further. oTree does have public GitHub repositories, issues, and a pull-request tab, but they do not provide a current public history of the 5.x/6.x code shipped on PyPI. For the versions discussed here, the source of truth is the PyPI source distribution that the author published under each version number.

The "version history" cited throughout this post comes from a Git repository that I had to build by hand: I downloaded each oTree release from PyPI, unpacked it, and committed the unpacked tree under a tag matching the release version, because I could not find an up-to-date upstream public history for the code actually shipped in these releases. When I refer below to particular versions of oTree, I am referring to the snapshot of the source tree that the author chose to publish to PyPI under that version number. That opacity is a significant factor in why this bug survived as long as it did.

I will at some point have to blog on how PyPI and similar software repositories simply should not allow packages that are not pulled and reproducibly built from publicly available sources. Relatedly, code that is not under institutionally approved FLOSS licenses ought not to be allowed on these repositories. Much more cleanliness is urgently needed. However, that is a matter for another day.

The bug

In plain terms: oTree's codebase contains a working authentication check for WebSocket connections — if an internal flag is set, the server verifies the user is logged in and rejects them otherwise. The problem is that the flag is never set. Here is the mechanism in detail.

oTree's HTTP views inherit a per-class attribute, _requires_login, which is set during URL registration in otree/urls.py:

def url_patterns_from_builtin_module(module_name: str):
    all_views = view_classes_from_module(module_name)
    view_urls = []
    for ViewCls in all_views:
        url_name = getattr(ViewCls, 'url_name', ViewCls.__name__)

        ViewCls._requires_login = {
            'STUDY': url_name not in ALWAYS_UNRESTRICTED,
            'DEMO':  url_name not in UNRESTRICTED_IN_DEMO_MODE,
            '':   False,
            None: False,
        }[settings.AUTH_LEVEL]
        ...

The base WebSocket consumer, _OTreeAsyncJsonWebsocketConsumer, also has a _requires_login attribute, hard-coded to False at the class level (otree/channels/consumers.py):

class _OTreeAsyncJsonWebsocketConsumer(WebSocketEndpoint):
    ...
    _requires_login = False
    ...
    async def on_connect(self, websocket):
        ...
        if (
            self._requires_login
            and not websocket.session.get(AUTH_COOKIE_NAME) == AUTH_COOKIE_VALUE
        ):
            ...
            await websocket.close(code=1008)
            return

The check is in place. The flag exists. The plumbing for switching it to True based on AUTH_LEVEL exists in the same file, for HTTP views. In releases before 5.11.5/6.0.14, it was simply never wired up for WebSockets. WebSocket routes are imported as a list and appended to the URL table directly:

routes += websocket_routes

That is the entire registration. In affected releases, nothing iterates over websocket_routes to set _requires_login. The class default — False — therefore wins for every single built-in WebSocket consumer, irrespective of AUTH_LEVEL=STUDY and irrespective of an admin password being set. The auth check in consumers.py is therefore unreachable by construction.

Two of the WebSocket consumers that this dead check fails to protect are particularly load-bearing:


WSDeleteSessions at /delete_sessions. Takes a list of session codes and deletes the corresponding rows. There is no other logic gating the deletion.

WSExportData at /export. Returns the framework's "wide" CSV: one row per participant, including participant/session fields, configured participant/session vars, and per-app/per-round player/group/subsession fields across all sessions. Session codes are among the columns.


The two endpoints compose: the output of one is shaped like the input of the other, and neither requires authentication. An attacker who has reached the export handler has, by construction, everything they need to reach the delete handler — and neither handler emits an application-level audit log entry.

The arbitrary-function-call escalation

In October 2025, in oTree 6.0.0b19, the export endpoint gained a new feature: a client can specify a function_name field, which is then passed into custom_export_app(). That function uses the name to look up and call the corresponding function in the experiment's code:

export_func = getattr(models_module, function_name, None)
...
rows = export_func(qs)

Before the fix, there was no validation that function_name referred to anything resembling a "custom export". Any function reachable as an attribute on the app's models module — the Python file where experimenters define their data structures and logic — would be looked up and called. Successful exploitation still depended on choosing a function with a compatible signature and return shape, and the names an attacker could target are easy to enumerate from a fresh oTree project. The practical impact of this part of the vulnerability is limited, as few experiments define interesting callable attributes. This bug pales in comparison to the ability of unauthenticated attackers to download and irrevocably delete session data.

The history

The chronology is worth setting out in full. All version numbers below are PyPI release tags; all dates are the upload dates of those releases.


19 December 2020 (5.0.0a9): The earliest 5.x (oTree Lite) release on PyPI already ships the broken-by-default pattern. The _requires_login machinery — the HTTP-view setter in otree/urls.py, and the WebSocket consumer base class with _requires_login = False — is already present. WSDeleteSessions and WSExportData already exist as unauthenticated consumers. An ALWAYS_UNRESTRICTED set listing class names that should remain open — WSChat, WSGroupWaitPage and so on — is already in place, which strongly implies the author believed that WebSocket auth was being assigned class-by-class. It was not. No WebSocket setter is added then, or in any subsequent release for over five years.

21 February 2021 (5.0.0b1): The first public beta of oTree Lite. WSDeleteSessions and WSExportData now appear in essentially their present form. They are not in ALWAYS_UNRESTRICTED. The author evidently intends them to require login. They do not. These two handlers remained largely unchanged through subsequent 5.x releases. The first stable oTree Lite release, 5.0.0, follows on 28 February 2021 with the same vulnerable code.

17 October 2025 (6.0.0b17): The author adds a print(url_name, ViewCls._requires_login) statement to url_patterns_from_builtin_module and ships it to PyPI. The function being debugged handles HTTP routes; the WebSocket routes, registered just below in the same file, have no equivalent authentication logic.

18 October 2025 (6.0.0b19): The function_name parameter is added to WSExportData and forwarded into custom_export_app() with no validation. The unauthenticated endpoint can now also be used to call functions in the experimenter's own code.

16 April 2026: I privately reported the vulnerability to the author. oTree offers no secure disclosure channel — no SECURITY.md, no PGP key, no dedicated security address. When I asked the maintainer for a secure channel through which to send the report, he rebuffed me; the report was therefore sent by ordinary unencrypted email.

17 April 2026 (5.11.5, 6.0.14): A small function, _assign_websocket_requires_login(), is finally added — over five years after the export and delete handlers first shipped — and wired into get_urlpatterns(). This websocket fix adds thirteen lines in each branch. The 6.0.14 patch additionally restricts the function_name parameter to names starting with 'custom_export'; 5.11.5 does not need this second fix because the function_name feature was never part of the 5.x line. I have not found a public advisory or release-note warning, although the framework's maintainer has acknowledged the vulnerability in private communications. Without a public advisory, existing users who upgrade will not know the release is security-critical, and those who do not upgrade will remain unaware of the exposure.


As of this writing, I have not found a public disclosure or warning to the operators whose subjects' data was exposed. Users deserve, at minimum, a clear security notice so they can assess their own exposure.

Why the fix is only partial

The 6.0.14 patch wires the WebSocket routes into the same dictionary lookup that HTTP views use:

cls._requires_login = {
    'STUDY': name not in ALWAYS_UNRESTRICTED,
    'DEMO':  name not in UNRESTRICTED_IN_DEMO_MODE,
    '':   False,
    None: False,
}[settings.AUTH_LEVEL]

Note the bottom two rows. AUTH_LEVEL is read from the environment variable OTREE_AUTH_LEVEL. If that variable is unset — as it is in every fresh install, on every developer machine, on every demo server, on every workshop laptop, on every Heroku deploy whose operator did not separately run heroku config:set OTREE_AUTH_LEVEL=STUDY (the framework sets nothing automatically on Heroku; OTREE_AUTH_LEVEL is an ordinary env var that the operator must configure themselves), and on every server whose operator did not read the small print of the deployment guide — then AUTH_LEVEL is None, and every WebSocket consumer is still wide open. The patch protects experimenters who have explicitly set OTREE_AUTH_LEVEL=STUDY in production. It protects no one else.

The /export and /delete_sessions endpoints on a default deployment are exactly as open as they were in the first public beta, 5.0.0b1, with the small consolation that the arbitrary-function escalation has been narrowed to attributes whose names happen to start with custom_export. The fix leaves the default deployment in the same state and adds a helper that only activates for operators who have already opted in to STUDY.

The documentation actively steers operators into the unprotected configuration. The Heroku setup page does not mention OTREE_AUTH_LEVEL at all; it covers dynos, Postgres and Redis, and stops there. The admin page introduces the variable as an optional feature toggle — "If you are launching an experiment and want visitors to only be able to play your app if you provided them with a start link, set the environment variable OTREE_AUTH_LEVEL to STUDY" — framed as gating participants to start links, not as the knob that decides whether strangers on the internet can download the database.

I have not found the default-unset behaviour documented anywhere, and I have not found documentation connecting AUTH_LEVEL to the /export or /delete_sessions WebSocket endpoints. An operator who sets OTREE_ADMIN_PASSWORD, sees the admin UI prompt for it, and concludes their server is protected is reacting in a way the documentation does little to correct.

Anyone who runs oTree should set OTREE_AUTH_LEVEL=STUDY and upgrade to versions 5.11.5 or 6.0.14 immediately. Anyone who does not leaves these built-in WebSocket endpoints exposed on a default deployment — the framework's threat model continues to hold that production-grade authentication is the experimenter's responsibility to enable.

Note that oTree 6 adds a "Powered by oTree" notice shown to participants, which may introduce extraneous stimuli that experimenters wish to avoid on methodological grounds. oTree 5 has also been patched, so 5.11.5 is a viable alternative.

I recommend using my modern oTree project skeleton which uses uv.

Broader context

oTree is maintained by one person, according to its website. The public GitHub repositories contain issue trackers, but I could not find a current version-control history for the 5.x/6.x code shipped on PyPI, a SECURITY.md, a security-advisory trail, or any secure method for reaching the maintainer to report a vulnerability. There is no published PGP key, no dedicated security contact, and no bug-bounty or coordinated-disclosure programme. When I reported this vulnerability on 16 April 2026, I first asked the maintainer for a secure channel; he rebuffed the request. The report was ultimately sent by ordinary unencrypted email — a medium that offers no confidentiality for vulnerability details in transit.

The bug itself is not exotic. The auth check exists in the consumer base class; the flag it tests is set for HTTP views in a function that lives in the same file as the WebSocket route registration. A review of that file would have surfaced the asymmetry. Single-maintainer projects carry inherent review limitations, and a public version-control history would lower the barrier for outside contributors to catch issues like this one.

The access-control allowlist is maintained as a hand-edited Python set of string class names — ALWAYS_UNRESTRICTED = {'WSChat', 'WSGroupWaitPage', ...} — with no enforcement that the strings refer to classes that exist, and no test exercising authentication across both transports. The fix preserves this design, so future access-control correctness still depends on these string lists staying in sync with the actual route classes.

If you are running an oTree server on the public internet today, set OTREE_AUTH_LEVEL=STUDY, set a strong admin password, upgrade to 5.11.5 or 6.0.14, and verify the deployment's access control independently rather than assuming the defaults are safe. Run oTree servers only for as long as a study strictly requires, and tear them down afterwards.
]]></description>
</item>
<item>
<title>My review of Sequencing.com</title>
<link>https://max.pm/posts/sequencing/</link>
<guid>https://max.pm/posts/sequencing/</guid>
<description><![CDATA[
This is an unbiased review of Sequencing.com, a company that offers Whole Genome Sequencing (WGS). Neither is this post sponsored nor was I provided with the product for free. Sequencing.com did not know in advance that I would write this review. I am just an individual nerd, biohacker, and scientist. See here for more information. I will be updating this post as necessary.

This is a review of their Comprehensive Health Screen offering. At the time of my purchase in 2026, that was their cheapest bundle (US$399).

Table of contents


    


Why I had my genome sequenced

WGS provides several types of information relevant for health management:


    Pharmacogenomics: The vast majority of people carry at least one pharmacogenomic variant that affects drug metabolism. Genes like CYP2D6, CYP2C19, and CYP2C9 influence how you process roughly 20% of common medications including antidepressants, opioids, statins, PPIs, and warfarin. Poor metabolizers may experience toxicity at normal doses, while ultra-rapid metabolizers may not respond to standard dosing.
    Disease risk variants: High-penetrance variants in genes like BRCA1/BRCA2 (cancer risk), HFE (hereditary hemochromatosis), and LDLR (familial hypercholesterolemia) can inform screening schedules and prevention strategies (see below).
    Carrier status: Identifies whether you carry recessive disease variants for conditions like cystic fibrosis, thalassemia, or spinal muscular atrophy. Relevant for family planning.
    Reanalysis over time: While one’s genome is static, variant databases and clinical guidelines are updated continuously. New variants are classified each year, and new gene-drug associations are discovered. With the raw data, you can reanalyze against updated databases every few years without needing to sequence again.


Also, I am a biohacker and nerd. In all sincerity probably the key reason to do this.

Common objections to WGS partially debunked

Information avoidance

A common concern: “What if I learn something I don’t want to know?” This is called information avoidance. People deliberately avoid information that is psychologically uncomfortable.

In reality, there are only a few genuine genetic death sentences where knowing (currently) provides no benefit. These are rare, high-penetrance, deterministic conditions with no known cure:


    Huntington's disease: CAG repeat expansion in the HTT gene. If you inherit it, you will develop the disease (typically onset in the 30s-40s). Life expectancy is 15-20 years after symptom onset. A gene therapy called AMT-130 showed a 75% slowing of disease progression in 2025 trials, but in March 2026 the FDA said the Phase I/II data were not sufficient to support a marketing application; no cure exists yet.
    Early-onset familial Alzheimer's disease: Variants in PSEN1, PSEN2, or APP genes. PSEN1 and APP variants have approximately 100% penetrance. Accounts for less than 1% of Alzheimer's cases. Onset typically before age 65. Drugs like lecanemab and donanemab exist but provide only marginal clinical benefit and do not prevent or cure the disease.
    Genetic prion diseases: All caused by mutations in the PRNP gene. All are invariably fatal with no known cure.
        
            Fatal familial insomnia: Progressive insomnia, dysautonomia, and neurodegeneration. Survival is 7 months to 6 years (average 18 months). Nearly 100% penetrance.
            Genetic Creutzfeldt-Jakob disease: Accounts for 5-15% of CJD cases. Rapid dementia and death, typically within one year.
            Gerstmann-Sträussler-Scheinker syndrome: Progressive ataxia and dementia. Survival is 2-10 years.
        
    
    Some forms of familial ALS: About 5-10% of ALS is genetic. SOD1 mutations (20% of familial cases) now have tofersen (Qalsody), which slows progression by about 50%. C9orf72 mutations (40% of familial cases) have no approved treatment yet. (However, most ALS is sporadic, not genetic.)
    Some extraordinarily rare mitochondrial diseases, such as those in POLG.


Crucially, if none of your family has ever had any of these conditions, your risk is very low. These are hereditary diseases that run in families. De novo mutations (spontaneous new mutations) do occur but are extraordinarily uncommon. For Huntington's, about 24% of new diagnoses lack family history, but these typically arise from expansion of "intermediate" CAG repeats (27-35) that were already present in a parent. True de novo mutations reinventing the disease from whole cloth are exceptionally rare. For early-onset Alzheimer's, de novo PSEN1 mutations account for roughly 8% of sporadic cases with onset before age 51 (which is itself rare). For prion diseases, de novo mutations are extremely rare (sporadic fatal insomnia has only about 30 recorded cases worldwide). The worry about discovering an untreatable genetic death sentence is largely unfounded for people without relevant family history.

It is also crucial to accept that every life on this planet will end. Life is only valuable because it is finite and that indeed, every life comes with a death sentence. The earlier you understand that, the better!

Most genetic risk is actionable

For nearly everything else, knowing allows you to take action. BRCA1/2 (cancer), APOE (Alzheimer’s risk factor, not determinant), HFE (hemochromatosis), LDLR (heart disease), pharmacogenomic variants enable you to:


    Adjust medication (avoiding drugs you can't metabolize, using alternatives)
    Intervene preventatively (prophylactic surgery, lipid management, iron monitoring)
    Get more screening (mammograms, colonoscopies, cardiac imaging)
    Modify your lifestyle (diet, exercise, avoiding specific environmental triggers)


My own results illustrate this well: over a dozen variants were flagged as "Pathogenic," and after manual verification, none were clinically actionable. The carrier variants were mild. The pharmacogenomic findings were genuinely useful. (More on that below.) Nothing in my genome dictated my health in a way I could not already influence through ordinary decisions about medication, screening, and lifestyle. For the vast majority of people, genetic determinism is simply wrong. Outside of a small number of high-penetrance Mendelian conditions (listed above), genes are probabilistic risk factors that interact with environment, behavior, and chance in ways we do not yet fully understand.

That last point deserves emphasis: we do not know enough about most of the genome to make strong predictions from it. The clinical utility of WGS today is concentrated in a few well-studied areas &mdash; pharmacogenomics, carrier screening for known Mendelian diseases, and a handful of high-penetrance cancer and cardiac genes. For the rest, variant databases are incomplete, penetrance estimates are uncertain, and gene-gene interactions are poorly characterized. WGS data contains a partial, evolving, and often ambiguous snapshot of risk. Knowing about a variant does not change the fact that it was already there. But it may let you do something about it &mdash; or, just as often, confirm that there is nothing to worry about.

Privacy and genetic discrimination

“What if someone gets my genetic data?” This has several dimensions, most of which are less serious than commonly assumed.

Law enforcement: Investigative genetic genealogy (the GEDmatch technique used to identify the Golden State Killer) and forensic DNA databases are tools for solving violent crime, where biological material is left at the scene. In the unlikely event that you or the likely readership of this blog are ever investigated for anything, it will almost certainly be white-collar and will not involve any DNA evidence whatsoever. If a relative is identified through your data as a perpetrator of violent crime, the responsibility lies with the person who committed the crime, not with the person who wanted to understand their own health. Keep your data off public genealogy databases if this concerns you, but do not let it stop you from getting sequenced.

Health insurance discrimination: In the United States, GINA (2008) prohibits health insurers and employers from using genetic information. Most developed countries have equivalent protections. Australia goes further: private health insurance uses community rating, meaning insurers cannot price based on individual health status at all. GINA does not cover life insurance, disability insurance, or long-term care insurance in the US; in Australia, the use of genetic test results in life insurance for policies below certain thresholds is currently limited, with a broader statutory ban scheduled to take effect on 2026-10-08.

But the deeper point is economic. Banning insurers from using genetic data while allowing individuals to test freely creates a textbook adverse selection problem. Individuals who discover high-risk variants have an incentive to buy more generous coverage; those who discover they are low-risk may reduce coverage or self-insure. The insured population shifts toward higher risk, and premiums rise for everyone still in the pool. This effect worsens as WGS adoption grows. If both parties had the same genetic information, this adverse selection would disappear: individual premiums would vary more (reflecting actual risk), but the average premium would decrease. Laws like GINA are distributive justice decisions &mdash; they shield high-risk individuals from bearing the full cost of genetic bad luck &mdash; but they achieve this by raising average premiums for everyone else. For most people, symmetric genetic information would make health insurance cheaper, not more expensive. This is really important to understand: insurance allows you to trade variance for a little bit on top of the expected value. If consumers accept more variance, premiums will drop on average.

Data custody: The genuine privacy risk is not sequencing itself but where the data lives afterward. 23andMe's bankruptcy in 2025, with roughly 15 million genotypes in its database, illustrated what happens when a genomics company fails: customer data becomes a business asset in insolvency proceedings. The mitigation is straightforward &mdash; download your raw data, verify it, store it on your own hardware, and do not depend on the company for long-term custody. This is what I did, and it is what I recommend. All reputable WGS providers also allow you to delete your data. They are legally required to do so; and considering the storage costs, they will also oblige out of self-interest.

“I’ll wait for cheaper or better technology”

Sequencing costs have dropped from roughly US$3 billion (Human Genome Project, 2003) to under US$400 for consumer 30x WGS. They will continue to fall, and long-read sequencing will eventually become the consumer standard. But your genome does not change. Sequencing now and reanalyzing later against improved variant databases gives you both immediate pharmacogenomic utility and long-term optionality. Clinical situations where you need pharmacogenomic data &mdash; surgery, a new prescription, an unexpected adverse drug reaction &mdash; arrive without warning. Having the data before you need it is the entire point.

“A genotyping array is good enough”

Consumer genotyping services (23andMe, AncestryDNA, etc.) use SNP arrays that test roughly 600,000 to 2 million pre-selected positions out of 3.1 billion base pairs in the human genome. They are inexpensive and useful for ancestry and common-variant associations, but they have structural limitations that WGS does not:


    Fixed panel: An array only tests positions chosen at chip design time. Novel variants, rare mutations, and anything not on the panel are invisible.
    Complex gene regions: Pharmacogenes like CYP2D6 involve deletions, duplications, and hybrid alleles that arrays characterize poorly. WGS captures the full structure.
    No reanalysis for new discoveries: When a new pathogenic variant is discovered next year, you can check your WGS data for it. Array data contains only what was on the chip &mdash; if the position was not on the panel, you have no data and never will.
    No structural variants: Arrays cannot detect inversions, large insertions or deletions, or copy number variants outside pre-designed probes.


Genotyping arrays are a snapshot of known variants at the time the chip was designed. WGS data is future-proof.

Preparing and learning about genomics

Before ordering, I spent time understanding genomics concepts and file formats:


    Reading: Archibald’s Genomics: A Very Short Introduction (Oxford University Press, 2018) is a very neat overview. I highly recommend it.
    File format basics: Familiarized myself with FASTQ (raw reads), BAM (aligned reads), and VCF/gVCF (variant calls). Understanding these formats helps verify data quality when it arrives.
    Analysis tools: Reviewed the GATK Best Practices pipeline, SAMtools, and Ensembl VEP. All are FLOSS and well-documented.
    Interpretation services: Explored tools like Promethease (US$12, generates SNPedia-based health reports from VCF files) and free alternatives like Impute.me (polygenic risk scores) and ClinVar (clinical variant database). I do not recommend Promethease, as it has been essentially decommissioned and is broken most of the time.
    Quality metrics: Learned what to check. Average depth should meet or exceed ordered coverage (30x), at least 95% of genome covered at 10x or higher, and FASTQ quality scores (Q30) above 85%.


You do not need to become a bioinformatician, but understanding the data formats and basic QC metrics helps ensure you get what you paid for.

My desiderata and expectations for WGS

Before ordering, I established some desirable factors for data quality and format:

Sequencing depth and technology


    30x coverage minimum: Each base pair should be read approximately 30 times. This is the industry standard, sufficient for detecting most variants with high confidence. (60x or 100x would be better for rare variants and mosaicism, but costs 2-3x more.)
    Illumina platform: NovaSeq 6000 or X Plus provides the highest base accuracy (Q40 on X Plus). Short-read sequencing is the established standard for SNP detection.
    PCR-free library preparation: Gold standard. Provides more uniform coverage across GC-rich regions, eliminates amplification bias, and improves indel detection. Sensitivity exceeds 99.77% for SNPs at 40x coverage.


Sequencing.com status: They provide 30x clinical-grade WGS. While not explicitly known, their sequencing platform is almost certainly Illumina.

Data formats and access

This is the most important point: I wanted raw data.


    FASTQ files: Raw sequencing reads with quality scores. Essential for complete reanalysis.
    BAM/CRAM files: Reads aligned to reference genome. Should be aligned to GRCh38 (hg38), not the older GRCh37.
    gVCF, not just VCF: Standard VCF files only contain positions where variants were detected. This creates ambiguity: was a missing position a reference call or insufficient coverage? gVCF (Genomic VCF) includes every position in the genome with confidence scores for reference calls and explicitly marked no-call regions.


Sequencing.com status: They provide FASTQ and VCF files (their "Genome VCF" is standard VCF, not gVCF). BAM files and mitochondrial heteroplasmy VCF are available upon request (email support after processing completes). gVCF is a paid add-on, or you can generate it yourself from BAM (see below). Alignment to GRCh38 plus rCRS MT. Lifetime data storage included (though I, needless to say, do not need that).

Open source compatibility

To my surprise, all major genome analysis tools are FLOSS (GATK, SAMtools, bcftools, Ensembl VEP, etc.) or at least source-available (Expansion Hunter)! Standard file formats (FASTQ, BAM, VCF) ensure compatibility with the entire bioinformatics ecosystem.

Sequencing.com status: Uses standard formats. No proprietary lock-in.

Why I chose Sequencing.com

At the time I ordered, the decisive factor was the combination of price, raw-data access, and clinical-grade logistics. I wanted 30x short-read WGS with FASTQ files, ordinary VCFs, an aligned BAM on request, and enough documentation that I could reproduce or challenge the vendor's interpretation myself. Sequencing.com was pretty much perfect on each front.

Note: Their consumer-facing health reports are more limited than the raw data. I would not buy WGS primarily for glossy app insights. I would buy it to get the sequence files, then analyze them myself with
transparent tools and manual follow-up where the result matters.

Sequencing.com automatically (and not too transparently) enrols you in a monthly subscription, but it is easy to cancel.

Timeline

Note: All times and dates are in the AEDT timezone (Australia/Sydney).


    2026-01-11: I ordered my kit.
    2026-01-12: The kit was shipped.
    2026-01-27: I received the kit in Australia. The kit is neatly packaged, and about 15x20x5 cm in size.
    2026-02-02: After much hassle, I was able to send back my sample to the United States. I used HS Tariff 0511994070 (ruling), described the item as a non-hazardous Exempt Human Specimen both on the customs form and the outer packaging itself, to fully comply with applicable regulations. The main issue was researching all of these rules (you’re welcome!), and finding a proper envelope. The envelope shipped by Sequencing is clearly far too small for any kind of international shipment. Note: other companies may well still use an ethanol-based stabilization buffer, which may not be shipped by ordinary mail. But that appears not to be the case for Sequencing, so an ordinary international shipment is fine.
    2026-02-14: The sample arrived at Sequencing.
    2026-02-23: I was informed that DNA extraction was now underway.
    2026-03-05: Sequencing was completed. Read on below.


Post-sequencing steps and experience

On 2026-03-05, I received a flurry of emails informing me that several steps had been completed, and finally, that “Congratulations! Your Genome Has Been Sequenced.” Wow, Sequencing.com was so much faster than expected! I immediately logged into my account and perused some of the insights. Interesting!

One of my key reasons to do WGS was so that I could obtain raw data. By default, Sequencing provides the following six files for download:


    *-30x-WGS-Sequencing_com-*.snp-indel.genome.vcf.gz (~184 MiB)
    *-30x-WGS-Sequencing_com-*.cnv.vcf.gz (~26 KiB)
    *-30x-WGS-Sequencing_com-*.sv.vcf.gz (~900 KiB)
    ULTIMATE-COMPATIBILITY-*-30x-WGS-Sequencing_com-*.txt (~15 MiB)
    *-30x-WGS-Sequencing_com-*.1.fq.gz (~23 GiB)
    *-30x-WGS-Sequencing_com-*.2.fq.gz (~24 GiB)


Each file needs to be separately “unarchived” in order to be downloaded. This is presumably because only a small fraction of customers ever download the raw data, so keeping hundreds of terabytes in an instant-access storage tier would be wasteful. Unarchiving took only about 20 minutes for all files I had to download.

For reasons explained below, I separately requested BAM files and mitochondrial heteroplasmy VCF. These are not provided by default. The *.bam file was provided on 2026-03-07 (~30 GiB).

The mitochondrial heteroplasmy VCF was provided some time later, after I inquired again. The *.mito.vcf.gz file is very small (~18 KiB).

All files are fine to use as-is. You do not need to uncompress them! All files are aligned to GRCh38, except for ULTIMATE-COMPATIBILITY* and *.mito.vcf.gz (aligned to GRCh37).

Generating gVCF from BAM

gVCF (Genomic VCF) is a file with detailed genetic information: it includes every position in the genome with confidence scores for reference calls and explicitly marked no-call regions. We will see how to use it below.

You can generate a gVCF file yourself using GATK (Genome Analysis Toolkit). This requires downloading a GRCh38 reference genome and takes about 10 hours on a typical desktop.

Get shell script


    If a “USER ERROR: Contig […] not present in the sequence dictionary” happens at the very end, after output.g.vcf.gz* have been written, you can run this validation command to ensure your output files are nonetheless complete. Should generate a lot of output and take about 5 minutes. If you see “OK” at the end, all is well. (The error happens because Sequencing.com’s BAMs are aligned against a reference that also included alt/random/unplaced scaffolds. These are not important.)
    Get shell script


Quality indicators


    
        
            Metric
            Value
            Assessment
        
    
    
        
            Total reads
            816M
            Solid for 30x WGS
        
        
            Mapped
            99.43%
            Excellent
        
        
            Properly paired
            98.25%
            Excellent
        
        
            Duplicates
            22.3M (2.7%)
            Low &mdash; good library complexity
        
        
            Singletons
            0.01%
            Negligible
        
        
            Median depth
            34x
            On target for &ldquo;30x&rdquo; product
        
        
            VCF PASS rate
            94.7%
            Normal
        
        
            Median variant QUAL
            222.4
            High
        
    


The first five rows are easy to reproduce with samtools flagstat on the BAM; the VCF rows come from bcftools. Overall, Sequencing.com did a solid job. The quality is legitimately good.

ClinVar annotation: what the “Pathogenic” variants actually are

After generating a gVCF, I annotated all called variants against ClinVar, NCBI's public database of clinically-relevant genetic variants, using the GRCh38 ClinVar VCF. The annotation pipeline (available here) matched each variant against ClinVar records and produced a report grouped by clinical significance. Of the millions of variants in a typical WGS dataset, ~47,000 overlapped with ClinVar entries.

The headline numbers looked alarming at first glance: over a dozen variants classified as Pathogenic or Pathogenic/Likely_pathogenic. The listed conditions sounded severe. Having never experienced any of them, I was skeptical.

I used my own knowledge and Claude Code to verify every single one of these hits against the raw sequencing data (BAM and the gVCF I generated myself). The results were sobering:


    
        
            Category
            What it means
        
    
    
        
            Sequencing artifacts (false calls)
            The variant does not actually exist in your genome
        
        
            Real but mislabeled in ClinVar
            Population-level risk associations, not Mendelian mutations
        
        
            Real, carrier only (recessive)
            One copy of a recessive variant causes no disease
        
        
            Benign (e.g. blood group antigen)
            Not a disease variant at all
        
    


In my case, every “Pathogenic” hit fell into one of these categories. None indicated active disease or required any medical intervention. This is not unusual but, in fact, expected for short-read WGS combined with automated ClinVar annotation. Here’s why.

How false positives happen

The sequencing artifacts fell into two categories:

Paralog cross-mapping. Many human genes have near-identical copies (paralogs) elsewhere in the genome. When a 150-base read comes from one copy, the aligner sometimes places it at the other copy instead. This generates phantom variant calls at positions where the two copies differ. The telltale sign: the variant-supporting reads have degraded mapping quality (MAPQ well below 60), while the reference-supporting reads map uniquely. The Very Short Introduction on Genomics recommended above explains the core issue: WGS uses a process called “shotgun sequencing” that repeatedly reads short sequences of DNA and then uses facts and logic (statistics!) to place these sequences at the right position. That process is simply not 100% accurate for paralogs.

A typical example: a gene with a ~90%-identical paralog elsewhere on the same chromosome generates “Pathogenic” variant calls at sites where the two copies differ. The tell is in the mapping quality: at the artifact site, most variant-supporting reads have low or ambiguous MAPQ scores, while reference-supporting reads map uniquely. At the worst sites, zero variant-supporting reads map uniquely &mdash; every single one is ambiguously placed. Compare to a clean region nearby, where all reads have perfect mapping quality. The contrast is stark and easy to verify in any BAM viewer.

Homopolymer slippage. Illumina sequencing-by-synthesis has a known weakness: runs of 6+ identical bases (e.g., AAAAAAAA or GGGGGG) cause the polymerase to occasionally slip, inserting or deleting a base. This generates spurious indel calls at ~10% allele fraction. The giveaway: GATK's own internal estimate (MLEAC) may conclude the true allele count is zero &mdash; meaning the variant caller itself does not believe its own call &mdash; and the quality score may be far below 1, where a real variant would be in excess of 100.

ClinVar mislabeling

Several variants were real (the sequencing was fine) but labeled “Pathogenic” despite being common population-level susceptibility associations. These were all in non-coding regions (intronic or UTR), all had 0 or 1 ClinVar review stars, and were associated with common-disease susceptibility rather than Mendelian disorders. They were originally identified in GWAS studies and submitted to ClinVar without clinical validation. “Pathogenic” here is an artifact of loose historical submission standards, not a clinical diagnosis.

Carrier status: real but not disease

Some variants were genuine, well-supported heterozygous calls in genes associated with autosomal recessive conditions. For recessive diseases, you need two broken copies (one from each parent) to be affected. Carrying one copy makes you a carrier, which may be relevant for family planning, but does not cause disease. In my case, the carrier variants were for mild and almost whimsical conditions, but that is not guaranteed &mdash; carrier status for severe recessive diseases like cystic fibrosis or sickle cell disease is common and worth knowing about.

But pharmacogenomics delivered

Separately from the “Pathogenic” hits, the annotation also identified pharmacogenomic variants that are genuinely clinically actionable. These were classified as drug_response in ClinVar (not “Pathogenic”), had 3-star expert-panel review, and were confirmed real by the same verification process, where they revealed perfect mapping quality, clean allele balance, no artifacts.

This is the kind of finding that justifies WGS. Pharmacogenomic variants affect how you metabolize specific drugs, and knowing about them before you need the drug can prevent serious adverse reactions. Unlike the “Pathogenic” hits that required manual debunking, these had immediate, unambiguous clinical utility.

False negatives: what WGS cannot see

False positives are easy to catch because you have a called variant to interrogate. False negatives (real variants that the pipeline missed entirely) are silent. Several mechanisms guarantee they exist in any short-read WGS dataset:


    Trinucleotide repeat expansions: Huntington's disease, Fragile X syndrome, myotonic dystrophy, and Friedreich's ataxia are all caused by repeat expansions that can span thousands of bases. A 150 bp read cannot span them. Specialized short-read tools can still infer some repeat sizes from spanning and flanking reads. I ran Expansion Hunter v5.0.0 against 31 disease-associated STR loci in my BAM; all calls fell within normal ranges. Expansion Hunter reports two allele sizes per autosomal locus, which you compare against published pathogenic thresholds (e.g., HTT becomes concerning above 36 CAG repeats, FMR1 above 55 CGG repeats). That is reassuring for those catalogued loci, but it is not equivalent to a long-read genome or a clinical repeat-expansion assay.
    Paralog masking: The same mechanism that creates false positives can hide real variants. If a true variant in a paralogous region causes reads to align ambiguously, the variant caller may not accumulate enough confident evidence to make a call.
    Large structural variants: Inversions, complex rearrangements, and insertions larger than the read length (~300 bp) are poorly detected by short reads.
    Coverage gaps: Even at 30x mean coverage, random fluctuations and GC bias mean some regions fall below 5x. On chromosome 1 alone, about 8% of positions had fewer than 5 reads in my data. A heterozygous variant in such a region has a significant chance of being missed.


A clean WGS report does not mean “no pathogenic variants exist.” It means “no pathogenic variants were detected in the regions and variant classes that short-read sequencing can reliably access.” Long-read sequencing (PacBio HiFi, Oxford Nanopore) closes most of these gaps, at higher cost and with platform-specific tradeoffs in throughput, error profile, and tooling.

Takeaway

WGS is powerful, but automated annotation without manual verification is unreliable for rare disease variants. If a pipeline tells you that you carry a pathogenic variant, the correct response is not panic but verification. Check the mapping quality. Check the allele balance. Check the sequence context. Check whether ClinVar's “Pathogenic” label actually reflects reviewed clinical evidence or a drive-by GWAS submission from earlier time.

The genuine value of WGS lies in pharmacogenomics (where it works well), carrier screening (where it requires understanding of inheritance patterns), and having the raw data available for reanalysis as databases improve. Anyone selling WGS as a simple health report card is misrepresenting what the technology can and cannot do. Genetic determinism is simply wrong.

A note on geographic ancestry analysis

WGS data can also be used for geographic ancestry inference, typically via principal component analysis (PCA) or model-based clustering (ADMIXTURE). I ran an informal PCA projecting my genome onto the 1000 Genomes reference panel (2,504 individuals across 5 super-populations). The result was entirely unsurprising: I landed squarely in the European cluster, confirming what I already knew.

For someone with known European ancestry, continental-level PCA is trivially confirmatory. The finer-grained breakdowns that consumer services advertise &mdash; "42% Northern European, 28% Mediterranean" and so on &mdash; require much stronger modeling assumptions. The choice of reference populations, the number of ancestral components (K in ADMIXTURE), and the algorithm used all materially affect the output. These percentages are not biological facts but model-dependent estimates that shift when you change the reference panel or the number of components. The underlying science is real, but the precision implied by consumer reports is not.

Ancestry analysis can be genuinely valuable for individuals with unknown parentage, recent admixture, or complex family histories. For a European who already knows they are European, it is the least interesting thing WGS can do.

Conclusion

I would say that WGS was mildly useful. As someone with reassuringly good genes and no particularly remarkable family history, pharmacogenomic variants are the most important kinds of insight I was able to get. Moreover, the entire process enabled me to learn a lot about human genetics. It was interesting.

I am happy with Sequencing.com and do currently (May 2026) recommend them. I appreciate their support for open standards and their provision of raw data. Moreover, their offerings are hearteningly non-gimmicky.

Changes to this document


    2026-05-27 (current version)
    Blog post was publicly released.

]]></description>
</item>
<item>
<title>Protection from oneself? Paternalistic temptations of German economic policy</title>
<link>https://max.pm/posts/impuls-paternalism/</link>
<guid>https://max.pm/posts/impuls-paternalism/</guid>
<description><![CDATA[

    This is the official English translation of a Kölner Impuls zur Wirtschaftspolitik about the same topic, released August 5, 2025.


Table of contents


    


Introduction

Economic policy may pursue many goals. Some have become so codified that standard economics textbooks treat them as cornerstones of normative judgment—questions of inequality and growth, to name just two. A vast body of economic scholarship has examined trade-offs among these and similar seemingly easily measurable “outcomes.” A less discussed aspect of modern economic policy is freedom, and the problem of paternalistic measures that curtail it.
Paternalism is a nuanced subject with competing definitions and classifications (Grossmann, 2025b), but we can draw on the conditions proposed by Dworkin (2020): paternalistic action requires that (i) the freedom of a decision-maker is interfered with, (ii) the decision-maker has not consented to this interference, and (iii) this interference is justified by reference to the decision-maker’s own goals. In place of goals, one may also substitute the decision-maker’s “values” or “welfare” (section 2 ibid.), regardless of whether the decision-maker actually holds or ought to hold them.
Paternalistic aspects of German policy rarely attract media attention. One such instance occurred when the Federal Constitutional Court declared the ban on the regular facilitation of suicide incompatible with the Basic Law and void (judgment of February 26, 2020, BVerfGE 153, 182). As correctly argued in the constitutional complaint—and confirmed by the Second Senate—this was primarily a paternalistic measure, irreconcilable with the fundamental liberty rights under Article 2(1) of the Basic Law.
As we shall see below, not all paternalistic interventions restrict the decision-maker’s freedom through outright prohibitions, but all of them modify a decision-making situation in some way by invoking the decision-maker’s well-being—usually to “protect” him. The restriction of the decision-maker’s freedom need only exist relative to a (non-intervention) baseline, i.e. it need only be relative. The restriction itself need not be the aim or desire of the intervener; only the effect matters. An intervention can also make an action impossible even where the “prohibition” penalizes only another market participant who stands in some bilateral relationship with the decision-maker. The decision-maker thereby loses the ability to consent to certain contractual arrangements. This reflects the incidence of market interventions: just as a tax may be statutorily paid by one market participant yet economically borne by another, state interventions can restrict the freedom of multiple market participants even though legally only one is subject to a supply- or demand-side prohibition.1
This essay aims to identify paternalistic narratives surrounding current economic policy, to render them visible and open to debate, and to present current economic research in this area. The penultimate section discusses approaches for a diverse society.
What is the value of freedom?
This essay confines itself to a “negative” understanding of freedom (Berlin, 1958), which conceives of freedom as the absence of interference or other constraints—not as a positive capacity to act in any particular way. The discussion is further limited to actions affecting only the individual in question and any consenting third parties—the possibility of strategic discourse-shifting is addressed below.
It is plain that paternalistic measures restrict freedom in the negative sense while potentially strengthening it—under the right definition—in the positive sense. Let me anticipate an example discussed more fully below: the state’s prior-approval requirement for gambling. A proponent might argue that it enables (i.e. positively empowers) people to live debt-free, with a happy family, and so on. Let us take this admittedly absurd, caricature-like compression of the evidence as given for the moment.2 The proponent interposes himself between the actor and his goals; he summarily declares certain other goals desirable for the actor and thereby already satisfies a core element of paternalistic action. Freedom in the positive sense is recursively constructed to serve ends that are state-prescribed rather than self-determined. Those who adopt a positive conception of freedom open the door to paternalism and even to oppression in the name of an alleged self-realization or “true freedom” (Berlin, 1958).
The gambling example is also particularly useful for grasping the value of freedom on non-ideological grounds. A core strategy of opponents of liberalization across all domains is to point to negative individual effects (internalities)—such as not being debt-free, as discussed above. It is therefore natural to ask why one would want liberalization. The answer lies in a subtle methodological argument that exposes practical limits of empirical economic research.
First, it is important to understand that the measures used in statistics and scientific studies are not naturally occurring data as such, but highly purpose-optimized, dimensionally reduced, and statistically processed capta that exist only because they serve a particular analytical purpose.3 Individual decisions, by contrast, are objective facts. This conceptual distinction between data and capta resolves the puzzle of the value of freedom: that a person chooses X rather than Y reveals that the decision-maker prefers X over Y ex ante. Even if this value cannot be captured in a simple metric, let alone articulated, it exists. The value of freedom is invisible and resides, encapsulated, within the acting subject. It defies everyday quantification,4 but is no less real than the capta we know all too well—gross domestic product or life expectancy. That many researchers and policymakers equate measurability with formalizability, and formalizability with comprehensibility, does not change this.5 That people routinely trade the measurable for the immeasurable—health for entertainment, say—does not demonstrate irrationality but rather the existence of subjective trade-offs that manifest in individual action.
Human decisions thus reveal the value of freedom, even when third parties find it difficult to comprehend.6 This observation is not meant to cast doubt on the value of empirical analysis; the author of this essay is a hardcore empiricist. Study results, however, are merely complements in a holistic weighing of conflicting values. The “outcomes” used in studies—mentioned at the outset of this essay—and the many conceivable relevant capta must not be conflated with economic policy goals, which they may or may not constitute. As Ludwig Erhard stated on many occasions, respect for the individual decisions of economic actors is a constitutive principle of the German economic order after 1945. The state must, as a matter of principle, accept individual decisions, even when they are accompanied by a deterioration in more easily measurable metrics.
This argument gains further strength when a decision is made by adults, is deliberate, even repeated, and is not contradicted by the decision-maker’s own statements. Yet even where these factors are absent, proof to the contrary is not easy to provide—particularly when the issue extends beyond a single case.
That restrictions on freedom require robust justification can trigger a strategic shift in discourse. The limitation to actions affecting only the individual can be circumvented by identifying external effects that are then used to front for the underlying paternalistic motivation. The “happy family life” invoked above is one such example, insofar as it appeals to the well-being of third parties. Extending the concept of externality to social interdependencies makes every individual decision potentially subject to regulation.7 If the dissatisfaction of third parties counts as an externality, the space for individual autonomy collapses. What is invoked as an externality is often spatially and socially circumscribed and can be negotiated and resolved within those structures (see Coase, 1960). The burden of proof lies with the claimant.
How paternalistic is German economic policy?
Germany is not generally regarded as a very paternalistic country. The Nanny State Index by Snowdon (2025) places Germany near the bottom of the list—Germany respects diverse lifestyles, even when it comes to cigarette consumption and sugary drinks. Nevertheless, numerous laws intervene in various areas of life in a paternalistic manner. Some have become so taken for granted that the media almost never questions them: the fact that we must contribute to a pension system is partly8 paternalistic; that all medications require regulatory approval, and many also require a prescription, is largely9 paternalistic; that one must demonstrate financial literacy to a broker before trading advanced financial products is overwhelmingly paternalistic; that one must wear a seatbelt in a car, equally so. The same holds for restrictions on organ and blood donation. Taxes on nicotine and alcohol products are officially justified on public-health grounds—which would be at least partly paternalistic—but as Fichte (2014) shows, fiscal motives in fact predominate.
A well-known paternalistic and overgeneralizing narrative is that of “protecting the vulnerable.” Under Section 3 of the Working Hours Act (ArbZG), the maximum permissible working time is 8 hours per working day (Monday through Saturday); it may be extended to 10 hours provided that an average of 8 hours per working day (= 48 hours per week) is not exceeded over six calendar months or 24 weeks—meaning up to 60 hours per week are temporarily permissible, so long as the average is balanced accordingly.
Recent research shows that many Germans would prefer to work fewer hours (Jarosch et al., 2025). This discussion paper reveals considerable heterogeneity in individually optimal working hours. Yet the Working Hours Act takes the form of a blanket prohibition. An employer who violates it commits a regulatory offense or even a criminal one (Sections 22, 23 ArbZG). First, this illustrates how important it is to think through policy incidence: the employee faces no restrictions per se, but cannot consent to additional hours because they cannot readily be offered. Second, one can observe that this regulation disregards all individual experience. Yet how much a person is able or willing to work obviously depends on many personal and professional circumstances. The state cannot know.
The old-school paternalism of the Working Hours Act pervades other areas as well. The 25th chapter of the Criminal Code offers some prime examples. One egregious—and largely unknown—case is the German gambling regime. Online gambling not expressly licensed by the authorities is prohibited, and German players themselves face criminal liability.10 The penalty is imprisonment for up to six months or a fine (Section 285 StGB). Naturally, some defendants will be able to invoke mistake of fact or mistake of law, or obtain a dismissal under Sections 153 ff. of the Code of Criminal Procedure. Far more serious—and independent of criminal culpability—is the possibility of confiscating all winnings without deducting stakes, up to 30 years after the “offense” (Sections 76a, 76b StGB); and this applies even if all winnings have long since been spent. Although the Interstate Treaty on Gambling of 2021 made many previously unlicensable games licensable, participation in foreign online gambling is strongly inadvisable unless the provider appears on the whitelist of the Joint Gambling Authority of the Länder, which is available online (Gemeinsame Glücksspielbehörde der Länder, 2025).
The primary objective of gambling regulation in Germany is the prevention of gambling addiction and the protection of players (Section 1 GlüStV 2021). These are quintessentially paternalistic aims. The state implicitly claims that all players require the same protection, regardless of personality or individual circumstances. The current regulatory framework is neither targeted nor efficient. At the same time, the state purveys its own gambling offerings, whose advertising is scarcely less forceful than that of the controversial private operators.
The state enforces “protection” regardless of the declared or revealed preferences of players. By overriding those preferences, demanding unconditional obedience to licensing processes for which the citizen bears no responsibility, and backing it all with criminal sanctions, the state exceeds its legitimate scope of action.11 Not least, the current legal framework renders prediction markets impossible—markets that help aggregate dispersed knowledge and could be of considerable social value (Arrow et al., 2008; Hayek, 1945).
The same applies to certain provisions of Section 291 StGB and Section 138 BGB (“usury”). The Federal Court of Justice has established when, for instance, loan interest rates constitute usury: a conspicuous disproportion is regularly found when the effective contractual rate exceeds the market rate by roughly 100% in relative terms or by 12 percentage points in absolute terms. Anyone with a basic grasp of economics knows that price controls—and an interest rate cap is nothing else—produce substantial market distortions. People with poor credit are therefore generally unable to obtain formal loans in Germany—even though they might willingly accept high interest rates in a given case. The consequences are manifold and in some cases likely include recourse to loan sharks or criminal activity. The state meets these consequences of paternalistic feel-good policy with stone-cold indifference.
It stands to reason that policy can also aim to systematically favor one side in negotiated solutions. To the extent that a regulation does not exceed the so-called “core” of a (Pareto) bargaining solution, it is not paternalistic, since no action is precluded and Dworkin’s first condition is not met. It is unlikely, however, that real-world regulation actually functions this way in every case.
Three well-known examples of policies where such a null effect on the extensive margin is often claimed are minimum wages, data protection laws, and consumer protection in general. The minimum wage law frequently invokes the protection of workers, and insofar as the minimum wage prevents even a single person from selling their labor at a freely negotiated price below the minimum, it is paternalistic. Despite widely circulated claims to the contrary, such effects can be demonstrated, though with considerable heterogeneity (Neumark et al., 2007; Neumark, 2017).
The same applies to data protection. The data-protection regime practiced in the European Union incorporates numerous recent policy innovations. One example is the principle of data minimization (“privacy by default”) enshrined in Article 25(2) GDPR: controllers may, by default, process only those personal data required for the respective purpose—even the data subject’s consent does not lift this restriction—while Articles 12 through 14 GDPR simultaneously oblige them to be transparent about the purpose and scope of data use. A growing number of American technology companies are excluding European users because of high compliance costs. Such situations look like paternalism through the back door, though one reaches the limits of Dworkin’s criteria: the General Data Protection Regulation, for example, binds only service providers, not consumers. When a product is not offered in Europe, that is formally the free decision of a company—but it may be causally traceable to a (paternalistically motivated) regulation. Once again, this underscores the importance of considering policy incidence.
Consumer protection can produce the same indirect paternalistic effect. If a consumer cannot purchase a product online because, say, it cannot be offered due to the 14-day right of withdrawal (Section 312g(1) BGB), paternalism is plausible. The legislature has, however, provided for exclusions from this right under certain conditions (Sections 312g(2), 356(5) BGB): for digital products delivered immediately, for instance, or custom-made goods. Intelligent regulation has thus nearly eliminated the paternalistic side effects of consumer protection. And consumers are presumably grateful for this shift in power—with the result that the paternalism objection falls away for consumer protection (Dworkin’s second condition).
What causes paternalism?
Why do people intervene in the decisions of others? One problem in studying this subject is that externalities can often be claimed. Regardless of how persuasive such objections may be, controlled economic experiments (Roth, 1986) put basic aspects of paternalistic intervention to the test. In these experiments, simple scenarios are typically constructed, then systematically varied and refined to elicit core determinants of human behavior.
One of the world’s first modern paternalism experiments was conducted at the Cologne Laboratory for Economic Research in 2018 (Ambuehl et al., 2021)—assisted by a student research assistant who is now the author of this essay. In any event, this study introduced an important experimental design: two people are paired, one of whom sets rules for the other. The so-called Choice Architect determines how much freedom a so-called Chooser receives. The Chooser could choose between payoffs at different points in time: either more later, or less sooner. But the Choice Architect first determined what the Chooser was even allowed to choose. The Choice Architect could, for instance, compel the Chooser to be more patient—and could even go so far as to leave the Chooser with only a single option. But the Choice Architect could also grant (negative) freedom. The Choice Architect could likewise advise the Chooser against certain decisions.
Choice Architects systematically tend to discourage the Chooser from impatient decisions or to prohibit them outright. A majority of Choice Architects believe that their restrictions help Choosers—evidence of a paternalistic motive. In 65 percent of cases where they removed options, they judged this to be beneficial to the Chooser. And the more restrictive the mandates, the more convinced Choice Architects are of their benefits.
Choice Architects project their own ideals onto others. Patient Choice Architects impose stricter patience mandates on Choosers—not because they wish to intervene more often, but because they believe patience is objectively better. This pattern also appears in surrogate decisions, where Choice Architects must select a single option for Choosers. Patient Choice Architects believe Choosers are more patient than they actually are, which leads many of them to underestimate the stringency of their interventions.
This account of the experiments in Ambuehl et al. (2021) already reveals important basic patterns of paternalistic behavior. Further experimental work deepens our understanding of the underlying mechanisms.
A central question is the extent to which asymmetric information justifies or causes paternalistic interventions. John Stuart Mill argued in his classic On Liberty (Mill, 1869) that interference with the freedom of others can only be justified when decision-makers are uninformed—as when someone is about to cross a decrepit bridge without knowing of the danger. This intuition has been formalized in a recent discussion paper (Grossmann, 2024) and systematically investigated through experiments.
Choosers face decisions between a safe sum of money and a binary lottery under varying degrees of ambiguity: Choice Architects know the objective probabilities in the lottery, while Choosers may not. The results show a strong relationship: the greater the ambiguity facing the Chooser, the more frequently Choice Architects intervene. This confirms Mill’s intuition on the extensive margin: ignorance on the part of the decision-maker legitimizes paternalistic intervention.
The findings on the intensive margin of intervention are equally revealing. When Choice Architects know the hypothetical well-informed preferences of Choosers, they do use this information in their interventions. Yet they weight their own preferences roughly as heavily as the Chooser’s. This is a clear contradiction of Mill’s normative demand that interventions follow exclusively the actual will of the affected party, where that will is known.
Closer examination reveals a remarkable asymmetry: Choice Architects give the safe option a slight boost, regardless of their own risk attitude or that of the Chooser. This points to the existence of “cosmic ideals”—options perceived as objectively “more correct” or “more reasonable.” This tendency toward risk aversion in paternalistic decisions may explain why many laws or decisions made on behalf of others exhibit an inherent precautionary bias (see also Batteux et al., 2020). Further experiments are needed, however, to investigate and understand this pattern systematically.
The experiment also examined whether Choice Architects strategically withhold information to justify later interventions. The overwhelming majority provide information to the Chooser when they can. Only a small minority exploit information asymmetries strategically. This shows that most people do not fundamentally wish to impose their will on others, but view intervention as a necessary evil when information gaps cannot be closed.
A further experiment (Grossmann, 2025a) examined the interplay between different paternalistic instruments (e.g. Tor, 2022). In a decision scenario, Choosers can open virtual boxes each containing $20—but one random box contains a bomb that eliminates all winnings. Choice Architects can both set an upper limit on the number of boxes opened (hard paternalism) and impose a one-day waiting period (soft paternalism).
The results from a large, representative U.S. sample are unambiguous: waiting periods and caps do not substitute for each other. Choice Architects who impose a waiting period do not relax the upper limit—on the contrary, they use both instruments cumulatively. Roughly 40% of Choice Architects impose the waiting period on top of the cap. This refutes the optimistic notion that soft paternalistic measures crowd out harder ones on their own.
Interestingly, Choice Architects do not adjust their caps when Choosers have more time to think. Comparing Grossmann (2024) and Grossmann (2025a) reveals that information functions as an (endogenous political-economy) substitute for intervention, leading to a markedly reduced probability of intervention. Waiting periods, by contrast, do not seem to bear fruit. Perhaps they are simply too weak to produce genuine improvement in decision-making: although Choice Architects believe that Choosers who deliberate longer make better decisions (closer to the Choice Architects’ preferred “bliss point”), they do not adjust their rules. Perhaps hard constraints primarily target extreme cases, while soft measures are meant to address moderate deviations.
These experimental findings show that paternalism is complexly motivated. Projection of one’s own preferences, overestimation of risks facing others, strategic exploitation of information asymmetries, and the cumulative use of multiple instruments—all contribute to the emergence of paternalistic regulation. The heterogeneity in the behavior of Choice Architects further underscores that blanket pronouncements about paternalism fall short. What emerges instead is a multifaceted picture of human motivations, ranging from genuine concern to projective overreach.
Reducing paternalism through smart regulation
It is hardly a novel insight that individuals can find themselves in situations where they need help or make mistakes. But this does not apply to everyone, even though classic prohibitions invariably do. Blanket rules can be replaced by graduated, targeted protection systems that serve both to prevent harm and to reduce paternalistic overreach.
The recognition of social diversity across multiple dimensions is of central importance here. Blanket bans on undesirable conduct ignore the fact that modern societies are profoundly heterogeneous. The Germans of 2025 come in many colors and forms. They have different preferences; they differ in ambition, forbearance, intelligence, and information. They learn from experience, but at different speeds. The Germans of 2025 are embedded in social milieus that they control and that control them in return. Policymakers must understand that they do not govern a collection of simplistic caricatures, and that laws in a country that considers itself free must reflect this diversity.
This diversity exists not only in the cross-section but also intertemporally. We all experience moments of weakness and moments of strength. One example of a diversity-respecting approach is “asymmetric paternalism” (Camerer et al., 2003). Asymmetric paternalism aims to help those who need help without curtailing the freedom of those who can act rationally. Regulation is designed to correct the mistakes of people with limited rationality while imposing minimal or no costs on fully rational actors.
An illustrative example is opt-out systems for retirement savings (Knabe &amp; Weimann, 2015; Madrian &amp; Shea, 2001): individuals are automatically enrolled in savings plans but can opt out at any time. Those who deliberately choose not to save bear only a minimal burden (the effort of opting out), while forgetful or procrastinating individuals are effectively protected. Another example is simplified information presentation for financial products or foods (e.g. the so-called Nutri-Score). Anyone wishing to consume these products is free to do so, yet the uninformed at least receive basic information that may prompt further inquiry.
These approaches recognize that people possess different capacities for self-regulation and that these capacities vary by situation. They avoid the collateral damage of blanket prohibitions while respecting individual autonomy and providing protective mechanisms for vulnerable groups. Elias et al. (2024) and Cseh et al. (2024) address solutions in the area of blood and organ donation. These approaches are also sensible in the broader medical context.
How can these ideas be applied within the framework of German economic policy? Regarding working hours, individual agreements with graduated safeguards could be introduced. Starting at 9 hours of daily work, documented consent could be required; from 10 hours, a medical fitness certificate. Vulnerable workers would remain protected, while those willing to work longer hours could do so flexibly. At the same time, it would be conceivable to grant employees the right to readily reduce their hours back to the contractually agreed level.
As for gambling, the new Interstate Treaty on Gambling already shows progress through self-exclusion systems that allow players to lock themselves out in lucid moments. These could be supplemented by time-limited “cooling-off periods” following major losses. Instead of blanket bans on foreign providers, automatic licensing upon satisfaction of minimum standards would be conceivable—and of course, the criminal liability of players must be abolished immediately, irrespective of these reforms.
The usury ceiling on consumer loans should be abolished entirely and existing transparency obligations expanded: lenders would be required to present interest rates in a standardized format alongside market benchmarks and to continue itemizing total costs under representative scenarios. A mandatory 24-hour reflection period for interest rates above 20% p.a. or high loan amounts would prevent impulsive decisions without depriving responsible citizens of their freedom of contract. Anyone who, upon mature reflection, wishes to accept high interest rates should be permitted to do so—and such contracts should be enforceable.
Targeted measures of this kind can preserve autonomy while achieving important state protection goals. The result: reduced risk, greater freedom, and fewer market distortions.
Conclusion
In a free economic order, individual decisions must be respected as a matter of principle, even when they are difficult to comprehend. Interventions can be justified only where effects on third parties are credibly demonstrated, or where a decision-maker earnestly communicates that he cannot extricate himself from his situation. This perspective is relevant not only from a classical liberal standpoint but aligns with the jurisprudence of the Federal Constitutional Court. The German constitutional order places high demands on paternalistic policy.
A house does not necessarily collapse when a wall is removed—and government action can serve many ends. But by sketching paternalistic narratives in German economic policy and rendering them visible as such, open to debate and challenge, this essay contributes to dismantling long-standing and long-outdated regulation. Smart regulation can support the vulnerable while respecting individual freedom.
References


Ambuehl, S., Bernheim, B. D., &amp; Ockenfels, A. (2021). What Motivates Paternalism? An Experimental Study. American Economic Review, 111(3), 787–830.


Arrow, K. J., Forsythe, R., Gorham, M., Hahn, R., Hanson, R., Ledyard, J. O., Levmore, S., Litan, R., Milgrom, P., Nelson, F. D., et al. (2008). The Promise of Prediction Markets. Science, 320(5878), 877–878.


Baker, S. R., Balthrop, J., Johnson, M. J., Kotter, J. D., &amp; Pisciotta, K. (2024). Gambling away stability: Sports betting’s impact on vulnerable households (Working Paper 33108). National Bureau of Economic Research. https://doi.org/10.3386/w33108


Batteux, E., Ferguson, E., &amp; Tunney, R. J. (2020). Do we make decisions for other people based on our predictions of their preferences? Evidence from financial and medical scenarios involving risk. Thinking &amp; Reasoning, 26(2), 188–217.


Berlin, I. (1958). Two Concepts of Liberty. Clarendon Press.


Buchanan, J. M. (1975). The samaritan’s dilemma. In E. S. Phelps (Ed.), Altruism, morality, and economic theory. Russell Sage Foundation.


Bundesrechtsanwaltskammer. (2024). Stellungnahme Nr. 19: Zu den vom Bundesjustizministerium im November 2023 veröffentlichten Eckpunkten zur Modernisierung des Strafrechts (Stellungnahme 19). Bundesrechtsanwaltskammer (BRAK). https://www.brak.de/fileadmin/05_zur_rechtspolitik/stellungnahmen-pdf/stellungnahmen-deutschland/2024/stellungnahme-der-brak-2024-19.pdf


Camerer, C., Issacharoff, S., Loewenstein, G., O’Donoghue, T., &amp; Rabin, M. (2003). Regulation for Conservatives: Behavioral Economics and the Case for "Asymmetric Paternalism". University of Pennsylvania Law Review, 151(3), 1211–1254.


Coase, R. (1960). The Problem of Social Cost. The Journal of Law &amp; Economics, 3, 1–44.


Coyne, C. J., Goodman, N., &amp; Quintas, A. (2025). Ways of seeing the world: Legibility in alternative institutional settings. European Economic Review, 178(105116).


Cseh, Á., Kurschat, C., &amp; Ockenfels, A. (2024). Organspenden: Neue wege beschreiten. Wirtschaftsdienst, 104(5), 293.


Drucker, J. (2011). Humanities approaches to graphical display. Digital Humanities Quarterly, 5(1), 1–21.


Dworkin, G. (2020). Paternalism. In E. N. Zalta (Ed.), The Stanford encyclopedia of philosophy (Fall 2020). https://plato.stanford.edu/archives/fall2020/entries/paternalism/; Metaphysics Research Lab, Stanford University.


Elias, J. J., Lacetera, N., Macis, M., Ockenfels, A., &amp; Roth, A. E. (2024). Quality and safety for substances of human origins: Scientific evidence and the new EU regulations. BMJ Global Health, 9(4).


Fichte, D. (2014). Problematische legitimation von tabak- und alkoholsteuern. Wirtschaftsdienst, 94(1), 62–68.


Gemeinsame Glücksspielbehörde der Länder. (2025, July 8). Whitelist: Übersicht der erlaubten Glücksspielanbieter [Amtliche Liste]. https://www.gluecksspiel-behoerde.de/de/fuer-spielende/uebersicht-erlaubter-anbieter-whitelist


Grossmann, M. R. P. (2024). Knowledge and Freedom: Evidence on the Relationship Between Information and Paternalism. https://arxiv.org/abs/2410.20970


Grossmann, M. R. P. (2025a). Paternalism and Deliberation: An Experiment on Making Formal Rules. https://arxiv.org/abs/2501.00863


Grossmann, M. R. P. (2025b). Paternalism and rule-making: Three experimental studies in political economy [Universität zu Köln]. https://kups.ub.uni-koeln.de/78150/


Hautamäki, S., Marionneau, V., Castrén, S., Palomäki, J., Raisamo, S., Lintonen, T., Pörtfors, P., &amp; Latvala, T. (2025). Methodologies and estimates of social costs of gambling: A scoping review. Social Science &amp; Medicine, 371, 117940.


Hayek, F. A. von. (1945). The Use of Knowledge in Society. The American Economic Review, 35(4), 519–530.


Hofmarcher, T., Romild, U., Spångberg, J., Persson, U., &amp; Håkansson, A. (2020). The societal costs of problem gambling in sweden. BMC Public Health, 20(1), 1921.


Jarosch, G., Pilossoph, L., &amp; Swaminathan, A. (2025). Should friday be the new saturday? Hours worked and hours wanted (Working Paper 33577). National Bureau of Economic Research. https://doi.org/10.3386/w33577


Knabe, A., &amp; Weimann, J. (2015). Ein sanft paternalistischer vorschlag zur lösung des rentenproblems. Wirtschaftsdienst, 95(10), 701–709.


Lemieux, P. (2021). The Threat of Externalities. Regulation, 44, 18.


Madrian, B. C., &amp; Shea, D. F. (2001). The power of suggestion: Inertia in 401(k) participation and savings behavior. The Quarterly Journal of Economics, 116(4), 1149–1187.


Mill, J. S. (1869). On Liberty (4th ed.). Longmans, Green, Reader; Dyer. https://en.wikisource.org/wiki/On_Liberty


Neumark, D. (2017). The employment effects of minimum wages: Some questions we need to answer (Working Paper 23584; Working Paper Series). National Bureau of Economic Research. https://doi.org/10.3386/w23584


Neumark, D., Wascher, W. L., et al. (2007). Minimum wages and employment. Foundations and Trends in Microeconomics, 3(1–2), 1–182.


Roth, A. E. (1986). Laboratory experimentation in economics. Economics &amp; Philosophy, 2(2), 245–273.


Scott, J. C. (1998). Seeing Like a State: How Certain Schemes to Improve the Human Condition Have Failed (First). Yale University Press.


Snowdon, C. (2025). Nanny state index 2025. EPICENTER. https://nannystateindex.org/wp-content/uploads/2025/05/NSI-2025-May-14.pdf


Tor, A. (2022). The law and economics of behavioral regulation. Review of Law &amp; Economics, 18(2), 223–281.





The ban on the regular facilitation of suicide is one of many examples where precisely this differentiation matters; see judgment of February 26, 2020, loc. cit., para. 212 et seq.↩︎
In this subject area, negative outcomes can in fact be demonstrated to a considerable extent (Baker et al., 2024; Hautamäki et al., 2025; Hofmarcher et al., 2020).↩︎
For more on the distinction between data and capta, see Drucker (2011).↩︎
The theoretical construct of consumer surplus is a captum.↩︎
This equation of measurability with comprehensibility corresponds to what Scott (1998) describes as the limits of techne (formalized, scientific-technical knowledge) relative to mētis (practical, context-specific experiential knowledge). As Coyne et al. (2025) argue, this reductionist view neglects precisely those forms of local knowledge that elude codification yet remain essential to the functioning of complex social systems.↩︎
The Federal Constitutional Court, for example, stated in the above judgment: “Article 1(1) of the Basic Law protects the dignity of man as he understands himself in his individuality and becomes aware of himself […]. What is decisive is the will of the holder of the fundamental right, which eludes evaluation on the basis of general values, religious precepts, social models for dealing with life and death, or considerations of objective reasonableness” (para. 210).↩︎
For a critical examination of the concept of externality, see Lemieux (2021).↩︎
Compulsory pension insurance can also be justified on redistributive grounds or by political-economic calculation. The latter argument holds that many people want to help poor retirees—and, knowing this, future retirees simply fail to save enough, relying on state handouts in later life, much like the Samaritan’s Dilemma (Buchanan, 1975). Compulsory insurance at least ensures fairer funding. How convincing these arguments are need not be discussed here.↩︎
The healthcare system may also seek to reduce the costs of mistreatment or overtreatment through self-medication (as distinct from the suffering itself—the keyword being “patient safety”). The prescription requirement also means—in Germany—that the chronically ill must continually visit physicians for new prescriptions, creating financial incentives for doctors to lobby for maintaining the requirement. (The option of repeat prescriptions under the e-prescription framework is scarcely used.) A prescription requirement can also help curb antibiotic resistance. None of these motives are paternalistic.↩︎
Whether a license from another EU member state, e.g. Malta, suffices is the subject of legal debates that cannot be fully recounted here. However, higher-court case law suggests that such a license does not suffice for German players.↩︎
In Germany, contrary to widespread claims, criminal law is not the “ultima ratio” but the “prima ratio”: every social problem and controversy—be it in gambling or in assisted suicide—is first met with criminal law. The paternalistic legitimation of Section 285 StGB crowns this deep Prussian perversion of German legal policy. To call Section 285 StGB asinine would be a gross understatement. It is no wonder that the literature regards it as unconstitutional. The German Federal Bar Association wrote in March 2024: “A legitimate legal interest is not discernible. In truth, Section 285 StGB is simply unconstitutional and represents an illegitimate penalization of the self-harming victim. For constitutional reasons, the self-harming consumption of ‘forbidden things’ must not be made punishable, even if such consumption stabilizes the forbidden actions of others and thereby destabilizes the prohibition itself. Section 285 StGB must be repealed without replacement.” (Bundesrechtsanwaltskammer, 2024, p. 7)↩︎


]]></description>
</item>
<item>
<title>“Interest first” is not a conspiracy (it’s a mathematical necessity)</title>
<link>https://max.pm/posts/loans/</link>
<guid>https://max.pm/posts/loans/</guid>
<description><![CDATA[



    
        Open web app
    


A common class of memes complains about banks’ “practice” to have homeowners and other borrowers pay interest first, and pay the principal only later.

These memes suggest that there must be some sort of conspiracy between bankers to subjugate borrowers. After all, why can’t I pay the principal first, thereby reducing the balance?

A common response to the meme is that homeowners entered this arrangement willingly. While that is generally true, it misses the point entirely. The payment schedule, with higher interest rate payments first, is mathematically determined and necessary. For a level-payment, fixed-rate, fixed-term, fully-amortizing loan, the payment schedule is dictated by facts and logic! So, not only is there no conspiracy, and not only did borrowers voluntarily agree to the terms of a loan, but banks simply have no choice about the payment schedule!

Simply put, for a standard fixed-rate, fixed-term, level-payment fully amortizing loan, the interest portion is larger at the start because interest is computed on the outstanding balance, which is largest at the start.

We can prove this mathematically. Let’s consider a simple loan of $B_0$ in your favorite currency. The loan has a fixed periodic interest rate $r$ and term of $N$ periods. (None of these assumptions are crucial, but they simplify the exposition tremendously.)

Let $B_n$ reflect the balance after $n$ payments, where $n \in \{0, 1, \ldots, N\}$. Initially, the balance is $B_0$; and after all $N$ payments we want the loan paid off, so $B_N = 0$. How does the balance change from one period to the next? Simple. Interest accrues on the current balance, and then the payment $P$ is subtracted. Mathematically,

\begin{equation}
B_{n+1} = (1 + r)B_n - P.
\label{loanrate}
\end{equation}

Equation \eqref{loanrate} is what mathematicians call a first-order linear difference equation. Its unique solution can be shown to be $B_n = c(1+r)^n + \frac{P}{r}$. Since we know that $B_0$ is the initial principal and $B_N = 0$, it follows after some simple calculations that $P$ must, mathematically have the following value:

\begin{equation}
P = \frac{rB_0(1+r)^N}{(1+r)^N - 1}.
\label{payment}
\end{equation}

Under this setup, this is the single possible payment! It is literally dictated by the laws of mathematics. Let’s inspect it further. In period $n$, interest owed is $I_n = rB_{n-1}$ and principal paid is $\Pi_n = P - I_n$. Substituting the solution for $B_{n-1}$:

\begin{align*}
I_n &= (rB_0 - P)(1+r)^{n-1} + P\\
\Pi_n &= (P - rB_0)(1+r)^{n-1}
\end{align*}

If we look at $I_{n+1} - I_n = r(rB_0 - P)(1+r)^{n-1}$, we find that this expression is actually negative. (This is because $P > rB_0$ to amortize the loan over time, so that the loan is actually paid off at $N$.) The implication is that the unique possible payment schedule indeed starts off with a "high" interest portion that subsequently trails off.

However, $\Pi_{n+1} - \Pi_n = r(P - rB_0)(1+r)^{n-1}$ is positive! Thus, it is correct that, over time, the portion paid to the principal increases.

So the next time someone complains about banks’ “practice” of having you “pay interest first:” There is no practice. There is no policy. There is no choice. The payment schedule is not a decision anyone made. It is a mathematical necessity, as unavoidable as $2 + 2 = 4$.

What if you tried to “pay principal first”?

Suppose you demanded that your bank let you pay principal first, meaning you wanted $\Pi_n$ to start high and decrease over time, rather than the other way around. What would happen?

For $\Pi_n = (P - rB_0)(1+r)^{n-1}$ to be decreasing, we would need $P - rB_0 \lt 0$, i.e., $P \lt rB_0$. But look at what this implies for the balance. After the first period, $B_1 - B_0 = rB_0 - P > 0.$

The balance is increasing. Your payment does not even cover the interest! This is called negative amortization, where the debt grows rather than shrinks. Far from paying the loan off faster, you’re falling further behind!

Worse still, with $P \le rB_0$, the loan cannot be paid off in finite time. The boundary condition $B_N = 0$ becomes impossible to satisfy for any finite $N$. You would have to either increase your payment (back above $rB_0$, restoring the "interest-first" structure), extend the term to infinity (possible only for $P = rB_0$), or default. No bueno. And once again, this is simply just mathematics. No politician can save you from plain mathematics.

In other words, “pay principal first” is not an alternative payment schedule. The assumptions that define a standard amortizing loan (a fixed rate, fixed term, fixed payment, balance paid off at maturity) mathematically require the interest-heavy-first structure. You cannot violate it without violating one of those assumptions.

It is true that borrowers are often allowed to pay extra principal early (prepayments/curtailments). That changes the balance path and reduces total interest, which may of course be sensible. But it is not mathematically possible to merely increase the percentage of $P$ being spent on $\Pi_n$ without violating important assumptions of the model. Similarly, borrowers can often opt for interest-only payments to temporarily reduce payments. Once again, there is no free lunch: such arrangements require either higher payments later, an extended term, or a balloon payment at maturity. Other loan structures can violate other assumptions of the model, which may be individually acceptable, but the fundamental equations dictated by mathematics do not and cannot change.
]]></description>
</item>
<item>
<title>LEDs on consumer devices shouldn’t double as aircraft beacons</title>
<link>https://max.pm/posts/bright-leds/</link>
<guid>https://max.pm/posts/bright-leds/</guid>
<description><![CDATA[
I recently purchased an Alogic ULCGE-SGR USB-C to Ethernet Adapter.

The device itself works OK-ish, but I believe the link light along with some nice headphones could be used to land planes.

Here’s an idea: for the vast majority of people, Ethernet adapters only need a light when there’s no link. Just go dark when everything works as intended.

It’s time for manufacturers of electric devices to stop contributing to light pollution.
]]></description>
</item>
<item>
<title>Steering clear of experimental economics hazards</title>
<link>https://max.pm/posts/experimental-hazards/</link>
<guid>https://max.pm/posts/experimental-hazards/</guid>
<description><![CDATA[


This page is a perpetual work-in-progress that presents my personal views on experiments in economics and what, to me, makes a good experiment, analysis, and paper.

Table of contents


    


Treatments matter only to the extent of their mutual differences

A core idea in experimental design is that only contrasts matter. This is one reason why the term “Control” is so misleading. “Control” is simply just one other treatment. It does not matter except for its differences to other treatments. (Whenever I speak of “two treatments”, I am referring to the classic design where you have one baseline/control and one extra treatment, often referred to as the treatment.)

More generally, I frequently see the following pattern: the baseline condition contains some established standard treatment, and the “treatment” is about a novel mechanism, approach, or joie de vivre. In many cases, the treatment differs in more than just the mechanism, approach, or joie de vivre. For example, if the treatment truly is novel, participants may simply be more experienced with or accustomed to the baseline. Or, if instructions differ (for example, the baseline has one fewer page of instructions), that is another difference. The (whole) contrast between treatments defines the interpretation of a treatment effect.

The ideal experiment changes exactly one thing. How can we get as close as possible to that ideal?

One approach is to explain more than is necessary. For example, when testing a classical mechanism against a novel mechanism, instructions may simply explain both mechanisms, and subsequently transparently randomize participants into either condition. That approach is not always sensible, but it does eliminate particular kinds of confounds.

Another problem that I see is that sometimes the comparisons implied by treatments simply do not matter or are difficult to interpret. For example, in 2×2 designs (more on them below), it can well be that the comparisons relating to the interaction are irrelevant. Or, in the case of binary outcomes, that we do not know how to reason about them. In that case, it can be proper to eliminate that fourth treatment, and keep only the three treatments in the upper left of the 2×2 matrix. If so, it can be sensible to increase the sample size for the north-western treatment if both remaining comparisons use it as baseline.

However, the fourth treatment can often be used productively, but typically only in terms of simple effects (i.e., against the off-diagonal treatments). More generally, treatments just do not matter—only treatment differences do, and you should focus on engineering those properly!

Use active controls in information provision experiments

Information provision experiments have a special hazard. Consider a simple design where you inform some participants about $X$ and leave others uninformed. What are you identifying? You identify the change from pre-existing beliefs to informed beliefs. But here is the problem: you have no control over the pre-existing belief. Some participants may believe $X$ is higher than the truth. Others may believe $X$ is lower than the truth. Your treatment effect is inherently heterogeneous and depends on what participants happened to believe before your intervention.

This is bad. You are identifying a mixture of positive and negative belief shocks. The direction of your treatment is not controlled by you. It is controlled by participants’ prior beliefs. You can adjust for those econometrically, but that control has no causal nature.

The solution is to use an active control treatment. Instead of comparing “informed” versus “uninformed,” in most cases you should compare “informed high” versus “informed low.” Tell one group that $X$ is high. Tell another group that $X$ is low. Now the pre-existing belief is marginalized out. Both groups receive information. The treatment difference isolates only the treatment-induced difference in beliefs. This is the right way to run information experiments.

Needless to say, there must be some genuine uncertainty about the true state. For example, it can help to provide participants with credible forecasts or estimates. Participants should believe the information (and ideally believe it equally in both conditions). Also, there are cases where active control treatments are not good design choices. I have an example. The above JEL paper discusses such issues.

An additional benefit: the active control also cancels out the mere effect of providing any information at all. Receiving information may have psychological effects independent of content. The active control differences out this confound as well.

Don’t sacrifice verisimilitude for shiny objects

A common issue I observe is how quickly verisimilitude, that is, the appearance of truth, is sacrificed for golden calves.

The fundamental conflict is easy to understand. As experimentalists, we study human behavior. As economists, we study markets. Nonetheless, many experimental economists are interested in “uneconomic” topics (as am I, though I call these topics studies in Non-Market Decision Making). Here’s an example. You might want to study how an outgroup member’s viewpoints on apolitical topics affects people’s “warmth” towards the outgroup.

Since “warmth” is not a rigorous economic concept, economist experimenters tend to practice an understandable bait and switch. They replace “warmth” by “giving in a dictator game” or “trust in the trust game” or “reciprocity in the trust game.” The golden calf of incentive compatibility must not be sacrificed!

But what about just asking people how warm they feel? Needless to say, and as fairly represented on that Wikipedia page, the feeling thermometer is not the most world-historically rigorous measure and it has certain important issues. However that may be, it also has a core advantage: without doubt it relates to the concept of “warmth,” even if that “warmth” is imprecisely measured.

So, while the feeling thermometer is not very valid as an economic construct, it is of high conceptual validity. Contrarily, economic games like the dictator or trust games are highly valid economically, but they are weak as conceptual representations of the concept of “warmth.” There are various other advantages and disadvantages of each approach. An important one in favor of measures like the feeling thermometer is simplicity: it truly is trivial to understand. The same holds true for other “less rigorous” measures, such as Big 5 questionnaires. Interpersonal comparability seems to be mostly a theoretical concern; many such measures are far more valid than the average economic experimentalist believes, though economists do not and cannot understand “why” they work. The feeling thermometer in particular is predictive of real-world behavior; it correlates with real-world outcomes (voting behavior, policy support).

As so often, econometrics comes to the rescue. Is there a way to combine the conceptual richness of the feeling thermometer with the golden calf that is incentive compatibility? Yes! Just elicit both and combine them into an index, assuming that the measures are noisy signals of a common latent variable. A solid approach is to use an average of z-scores, but there are other methods such as principal component analysis or factor analysis. The latter is valuable if many measures have been collected. Needless to say, any such approach must be preregistered, but it is highly valid.

The principle of verisimilitude can be used to turn many old-school lab experiments into modern survey experiments that have better external validity and are far, far cheaper and simpler to reason about.

Finally, experimental economists should understand that the study of human behavior is not fundamentally about “money.” Surely financial incentives are one way of making a design incentive compatible (and it does matter, especially when dealing with beliefs or perhaps with experimenter demand effects). However, human action can be revealed in a multitude of ways that are non-financial: If participants choose to read more about a topic? That’s revealed. If participants choose to wear an “I voted” button? That’s revealed. If participants choose to interact with the outgroup? That’s revealed. How we understand this revealed action is, of course, another question. This is where theory really comes in. But money should not really be the be-all and end-all of experimentalists’ attention.

The Golden Mean does not apply to experiments

An important insight that I learned far too late is that your treatments must always be as strong as humanly possible, under two crucial constraints: (i) no deception and (ii) no demand effects (unless you purposely want to test for them).

Do not implement “intermediate” treatments. It is wasteful. Statistically speaking, intermediate treatments have smaller effect sizes, decreasing power and/or increasing sample sizes.

In my experience, people sometimes feel bad about strong treatments. It is worth pondering why. Is it because a maximally strong treatment veers into demand territory? Then make a sharp left turn before that happens.
Is it because an extreme treatment might cause another, non-demand-related, psychological phenomenon that counteracts or artificially inflates a “true” effect? This kind of introspection is actually invaluable for theory-building! Perhaps you have found a new truth about human behavior simply by exaggerating a stimulus. In other words, if you are reluctant about stronger treatments, that may be because your theory of human behavior is too fragile, and needs a stronger foundation.


My paper Knowledge and Freedom started out with a very complicated design where draws were made from a lottery, and then the results were shown, and participants had to imagine someone else seeing any kind of lottery outcome for a given number of draws, etc., etc. The insight that extreme treatments matter did not only significantly improve the analysis, but it allowed me to get rid of all these complicated aspects of the design! Now the whole design is just predicated on whether the other person knows everything (basically, an infinite number of “draws”) or nothing (0 “draws”). That’s it. Much easier, much stronger, much better.

Summing up the previous two sections: measure richly but manipulate utterly.

Avoid nonparametric tests like the plague

Nonparametric tests are one of the most dangerous infohazards of our field. I want to show four things in this section: (i) nonparametric tests are often recommended based on severe econometric misconceptions; (ii) nonparametric tests test for things that don’t matter, generally speaking; (iii) nonparametric tests, even if used appropriately, have inferior statistical properties; (iv) parametric methods, and especially a particular kind of linear regression analysis with heteroskedasticity-consistent standard errors, are actually really good. In sum, nonparametric tests should not be used at all.

The following nonparametric tests are commonly used in experimental economics:


    
        
            Test
            Synonyms
            Use case
            Implementations
        
    
    
        
            Mann-Whitney U test
            Wilcoxon rank-sum test, Mann-Whitney-Wilcoxon test
            Two independent samples
            
                R: wilcox.test()
                Stata: ranksum
            
        
        
            Wilcoxon signed-rank test
            Paired Wilcoxon test
            Paired samples
            
                R: wilcox.test(paired=T)
                Stata: signrank
            
        
        
            Kruskal-Wallis test
            Kruskal-Wallis $h$ test
            Multiple independent groups
            
                R: kruskal.test()
                Stata: kwallis
            
        
        
            Kolmogorov-Smirnov test
            K-S test
            Comparing distributions
            
                R: ks.test()
                Stata: ksmirnov
            
        
        
            Fisher’s exact test
            Fisher-Irwin test
            Binary outcomes
            
                R: fisher.test()
                Stata: tabi ..., exact
            
        
        
            Spearman’s rank correlation
            Spearman’s rho
            Monotonic association
            
                R: cor.test(method="spearman")
                Stata: spearman
            
        
    


Below, I will focus on Wilcoxon-type tests (this includes Kruskal-Wallis), as these are the epitome of testing in old-school experimental economics. The other tests have their own issues, but also some strengths; the K-S test is especially useful, though probably not for what you would expect.

One nonparametric method that deserves a positive mention is the permutation test (also called a randomization test). Unlike Wilcoxon-type tests, permutation tests can directly test for differences in means, which is the estimand we typically care about. They inherit appealing distribution-free properties while remaining powerful and interpretable. That said, for the standard experimental settings discussed here, OLS-HC3 remains simpler and equally valid.

The use of nonparametric tests relies on common misconceptions

Two misconceptions lead, in my experience, people to use or recommend the use of nonparametric tests in our field. The first is how great and important it is not to make distributional assumptions. The second is that the thing being tested is somehow more relevant than with common parametric tests. This second claim will be examined below. Let’s for now focus on just the first one.

A common way the first claim is made is as follows: “Real-life data are not normally distributed, hence we should use nonparametric tests, BAZINGA!” In fact, these were the exact words used when I first learned about nonparametric tests. Claims like that are virtually always made to show nonparametric tests’ superiority over one particular alternative test: the t-test.

Whenever I refer to “the t-test,” I am here specifically referring to Welch’s t-test, the default in R’s t.test. Never, ever assume equal variances. If your software assumes so by default, your software is bad.

This argument is not even wrong. It is just irrelevant whether the underlying data are normally distributed. Rather, for the t-test to work, the mean of the data should have an approximate normal distribution. Now, it is of course accurate that if your data are normally distributed, then the mean itself is normally distributed. However, a remarkable result in statistics called the “Central Limit Theorem” shows that under very weak conditions, the distribution of the mean of any data with bounded mean and variance converges to a normal distribution.

The CLT gets even more remarkable. In all experiments known to me, data are inherently bounded. It simply is never possible to report arbitrary numbers. Just think of the public goods game. You simply cannot give less than zero or more than your endowment. The same principle applies to all experimental data. It is easy to prove that if a variable is bounded from above and below, all conditions of the CLT are satisfied and the mean of the variable will converge to a normal distribution. Simply put, the elicitation of bounded data kills pathological behavior of statistical tests.

Still, a commonly held misconception is that you need $n > 30$, or similar, for the CLT to apply. (This is an oversimplification. It depends heavily on the underlying distribution’s skewness.) However that may be, this does not mean that the t-test would fail. There are two cases that should be distinguished here: Type I errors and Type II errors.

For small $n$ and roughly equal group sizes, both the degrees of freedom, $\nu$, and the estimate of the standard deviation, $\sigma$, lead to such an unbelievably conservative distribution of t that I am confident to say that your test will not be oversized.

REWARD! If you are the first one to send me any R function at the indicated position subject to the conditions in the code, with the script running without error, the test at the end being rejected at $\alpha = 10^{-12}$, then I will admit defeat, update this post accordingly, and send a charity of your choice US$100:

    
        
            Show R code
            Hide R code
        

        
            
        
    

    This offer was posted on 2026-01-02 and is still valid.


Good luck.

Second, with respect to Type II errors: if you ever have $n \lt 30$, that’s a You problem. Increase your sample size or improve your design more generally. That’s part of why power analyses are useful.

Nonparametric tests’ null hypotheses are poorly understood

A common claim about Wilcoxon-type tests is that they are tests on the median. In general, that is false. See also this. And this. If you are happy with making the additional assumptions required to reframe Wilcoxon-type tests as tests on the median, then why not simply make the minimal assumptions for the t-test (see above) and actually test something real?

Generally speaking, Wilcoxon-type tests are tests on stochastic dominance. While in some very specific instances, stochastic dominance may be what matters (see below), in general it is not.

By the way: even if nonparametric tests tested for the median—or if you’re willing to make the necessary assumptions—that would only indicate the difference in medians between your conditions, not the movement of a “baseline median” person to a different outcome given treatment. The latter interpretation requires additional assumptions (such as rank invariance, constant additive treatment effects, or other restrictions on the joint distribution of potential outcomes). More broadly, your theory would have to justify why medians matter at all.

We know what t-tests do and that they have good properties

t-tests are tests on the mean. Means (or, rather, ATEs) matter a whole lot over a broad bandwidth of economic theory. And that’s that.

That t-tests have good properties was previously accomplished (warning: loud).

Linear regression with HC3 standard errors (“OLS-HC3”) is even better than t-tests

OLS is essentially an extension of the t-test. The t-test is just OLS with a binary treatment indicator. OLS generalizes this to multiple treatments, continuous controls, and factorial designs. It should be the natural framework for experimental analysis.

But what about heteroskedasticity? That problem is basically solved. Just use HC3 standard errors. They have excellent finite-sample properties. And yes, they are better than HC0, HC1, or HC2. Just use HC3. In R: lmtest::coeftest(model, vcov = sandwich::vcovHC(model, type = "HC3")). In Stata: reg y t, vce(hc3). Note that Stata by default uses HC1 if you just specify robust, so don’t do that.

OLS has many incredible benefits beyond robustness. You can easily include additional control variables to improve precision (see the ANCOVA section below). You can cluster standard errors by session, group, or any other unit. You can estimate multiple treatment effects simultaneously by using saturated regressions. Coefficients are directly interpretable. There are no convergence issues. Results are transparent and reproducible. OLS-HC3 should be the default workhorse for experimental economics. It just works.

Note: there is a critical chapter on “robust” standard errors, including HC3, in Mostly Harmless Econometrics, Section 8.1. Their simulation results rely on very small sample sizes, unequal between treatments (literally $N_1 = 0.1 \cdot 30 = 3$ in one of the treatments) and should be viewed as extreme and purely pedagogical. Also, interestingly, the newer HC4 standard errors (but not HC5!), mitigate the issue. I attach R code below to replicate the results in their final column for the cases of (i) no and (ii) lots of heterogeneity. If you change FACTOR or TREATED, you can see how well HC3 in fact performs under even slightly more realistic scenarios despite low sample sizes.


    
        Show R code
        Hide R code
    

    
        
    


Even the analysis of binary outcomes using OLS-HC3 is probably fine (with simple designs)

A common concern is that when your outcome variable is binary (0 or 1), you “should” use logit or probit instead of OLS. This concern is largely misplaced for experimental work with simple designs.

The linear probability model (LPM, just OLS with a binary dependent variable) has many advantages: (i) coefficients are directly interpretable as percentage point changes in probability; (ii) there are no convergence issues; (iii) it does not impose functional form assumptions about how treatment effects vary across the probability distribution; (iv) with HC3 standard errors, it is robust to heteroskedasticity (which is inherent with binary outcomes).

The main remaining criticism of the LPM is that fitted values can fall outside $[0, 1]$. I have never seen fitted values matter in experimental economics. If you extrapolate, you must choose an appropriate data-generating process.

For simple treatment comparisons, the LPM with HC3 standard errors is an excellent default choice. It is transparent, robust, and interpretable. It just works. More precisely, the LPM is valid when all right-hand-side variables are categorical and fully interacted. Once continuous covariates enter the model, functional form issues can arise. But for testing differences between treatments, where the key regressor is just a dummy variable, this is not a concern. Logit and probit models should be reserved for cases where (i) you have strong theoretical reasons to impose a specific functional form (such as from utility theory), or (ii) you are working with observational data where extrapolation matters.

One final note: if you do use logit or probit, report marginal effects, not raw coefficients. Raw coefficients from nonlinear models are nearly impossible to interpret and compare across studies. Marginal effects at the mean (or average marginal effects) restore interpretability.

Interactions (and why you probably shouldn’t care about them)

As I mention below in the section on factorial designs, interactions are fundamentally model-dependent. An interaction that is significant in OLS may be insignificant in logit, or vice versa.

Moreover, interactions are almost always underpowered ex ante.

In factorial designs, as discussed above, you should include the interaction term in your regression to avoid functional form misspecification, but you should typically focus on main effects or simple effects for interpretation. The interaction coefficient itself is rarely of interest. Feel free to just eliminate conditions where multiple treatments are active if you don’t need the resulting comparisons.

Small design changes can vastly improve statistical inference

One of the highest-return design modifications you can make is to collect a baseline measure of your outcome variable before randomizing participants into treatment. Then, include this baseline measure as a control variable in your analysis. This approach is called ANCOVA (analysis of covariance) and it can dramatically improve statistical power.

Consider the standard regression for a randomized experiment:

\begin{equation}
    y_i = \beta_0 + \tau T_i + \gamma \pmb{X} + \varepsilon_i
\end{equation}

Here, $y_i$ is your outcome, $T_i$ is the treatment indicator, and $\pmb{X}$ represents any additional control variables (demographics, etc.). Now suppose you collect a baseline measure $y_i^0$ of the outcome before treatment assignment. You can then estimate:

\begin{equation}
    y_i = \beta_0 + \beta_1 y_i^0 + \tau T_i + \gamma \pmb{X} + \varepsilon_i
\end{equation}

This helps because $y_i^0$ absorbs individual-level variation in the outcome. If people differ substantially in their baseline levels, the inclusion of $y_i^0$ reduces the residual variance $\text{Var}(\varepsilon_i)$. This directly shrinks the standard error of $\hat{\tau}$, increasing your statistical power without requiring a larger sample!

The gains can be enormous. But: timing matters. The baseline measure must be collected before randomization. However, any pre-treatment variable that is correlated with the outcome will help. For example, if your outcome is post-treatment donations, a baseline measure of past donations or general prosociality will still improve precision.

Measurement error in $y_i^0$ attenuates $\beta_1$ but does not bias $\hat{\tau}$, because $T_i$ is randomized and thus by construction uncorrelated with $\varepsilon_i$.

Summing up, if your experiment allows for it, always collect a baseline measure. There is essentially no downside. Preregister the inclusion of $y_i^0$ and always include it in your main specification.

Carefully design comparisons in factorial designs (e.g., 2×2)


    
        Show R code
        Hide R code
    

    
        
    


Suppose you have two factors, $g$ and $h$. Both are dummy variables (1 if “turned on” and 0 otherwise). Participants get iid randomly assigned to $g$ and/or $h$. As is clear, this is a 2×2 between-subjects design. Moreover, you have your outcomes, y. What can you do?

There are three standard effects in 2×2 designs: main effects, simple effects, and interaction effects.

Economists are inherently (and rightly) suspicious of interaction effects, and thus I will not be covering them. A core challenge with interpreting interactions is that they work only for certain outcome variables and models. With binary outcome variables, for example, an interaction that is not significant in the linear probability model/with OLS may be significant in a logit model, a probit model, both, or neither. It is a huge unresolved and likely unresolvable mess. Never rely on the interaction in a factorial design unless you fully understand the model to be used on the outcome.

Simple effects are simple indeed; they refer to the effect of one factor at a fixed level of the other. In R, just do lm(y ~ h, data = df[df$g == 1, ]) (with optional control variables, see above) and get lunch. Your day is completed.

Main effects are not so simple. The main effect of, say, $g$, requires you to average over $h$ with some weights. If these weights can be derived from a policy question or your theoretical framework, great! Otherwise, just use equal weights.

Denote by $\mu_{g,h}$ the population mean in some factorial treatment $g, h$. Then, the main effects of $g$ and $h$ are defined as follows:

\begin{align}
\tau_g^{\text{M}} = \left[ w_1 \mu_{1,0} + (1-w_1) \mu_{1,1} \right] - \left[ w_1 \mu_{0,0} + (1-w_1) \mu_{0,1} \right]\\
\tau_h^{\text{M}} = \left[ w_2 \mu_{0,1} + (1-w_2) \mu_{1,1} \right] - \left[ w_2 \mu_{0,0} + (1-w_2) \mu_{1,0} \right]
\end{align}

In the following, I assume $w_1 = w_2 = \frac{1}{2}$. As I argued above, linear regressions are an excellent method to analyze your experiment. How, then, can we get $\tau_g^{\text{M}}, \tau_h^{\text{M}}$ from a neat linear regression?

Unfortunately, this linear model is simply wrong:

\begin{equation}
y_i = \beta_0 + \beta_1 g_i + \beta_2 h_i + \beta_3 g_i h_i + \varepsilon_i
\end{equation}

Crucially, with main effects, it is not correct to just throw your (binary) dummy treatment indicators into the linear model as-is. That would only be correct if you did not use an interaction term in your linear model. Given my skepticism about interaction terms, you may ask why I would ever propose using one of these! The reason is simple: including the interaction term makes your model saturated. It can exactly recover all four cell means without imposing any functional form restrictions. If you omit the interaction term, you assume additivity; if this assumption is wrong, your main effect estimates will be biased. Including the interaction term protects against this misspecification. Therefore, you should always include the interaction term but (probably) ignore the coefficient on it.

In general, $\beta_1 \neq \tau_g^{\text{M}}$ and $\beta_2 \neq \tau_h^{\text{M}}$. Why is that? Simple:

\begin{equation}
\frac{\partial y_i}{\partial g_i} = \beta_1 + \beta_3 h_i
\label{marginal}
\end{equation}

This marginal effect depends on the value of $h_i$! Only at $h_i = \frac{1}{2}$ does the marginal effect equal the main effect. So, we must transform $g_i, h_i$ as follows:

\begin{align}
g'_i = g_i - \frac{1}{2}\\
h'_i = h_i - \frac{1}{2}
\end{align}

This effect coding or deviation coding ensures that the marginal effect is equal to the main effect, since we can now ignore the final term in the equivalent of Equation \eqref{marginal}. The following linear model recovers the main effects:

\begin{equation}
y_i = \beta_0 + \tau_g^{\text{M}} g'_i + \tau_h^{\text{M}} h'_i + \Xi g'_i h'_i + \varepsilon_i
\end{equation}

Needless to say, you can include further control variables as usual (and should ignore $\Xi$). However, transforming dummies is crucial—as long as you do that, your coefficients are meaningful! See here and here for excellent references.


    
        Show R code for 2×2×2 designs
        Hide R code for 2×2×2 designs
    

    
        
    


Null effects can in fact be quantified

A common reaction to a non-significant result is utter despair. That reaction is easily wrong or at least premature. A p-value above 0.05 tells you that you failed to reject the null hypothesis. It does not tell you that the effect is zero, small, or negligible. It says nothing about effect size.

A critical distinction must be made between underpowered null results (which are in general not so valuable) and well-powered, tightly bounded null results (which are valuable). If your study had 20% power to detect a small effect and you find $p = 0.5$, you have learned essentially nothing. Your confidence interval will be wide, spanning large positive and negative effects. Contrarily, if your study has 90% power and you find $p = 0.5$ with a confidence interval of $[-0.05, 0.15]$ in standardized units, you have learned something important: the effect, if it exists at all, is small. Let’s not mix both of these cases!

Null results can be valuable, but only if they are informative. An informative null result rules out effects larger than some threshold. To claim an informative null in a frequentist framework, you must demonstrate that your confidence interval is sufficiently narrow.

There are two main approaches to quantifying null effects: equivalence testing (TOST) and Bayesian methods. Both allow you to make positive claims about the absence or negligibility of an effect, rather than merely failing to reject the null.

Using TOST


    
        Show R code
        Hide R code
    

    
        
    


The two one-sided tests (TOST) procedure tests whether your effect lies within a pre-specified equivalence region. Instead of testing $H_0: \tau = 0$, you test whether $\tau$ is practically equivalent to zero by checking if the confidence interval for $\tau$ lies entirely within some interval $[-\Delta, \Delta]$, where $\Delta$ is the smallest effect size you consider meaningful.

The conventional choice is $\Delta = 0.2$ in standardized units (Cohen’s $d$), though you should justify this threshold based on your research context. A narrower equivalence region (say, $\Delta = 0.1$) makes a stronger claim but requires more statistical power. The TOST procedure uses a 90% confidence interval, which corresponds to two one-sided tests at $\alpha = 0.05$.

Here is an example (from the code above):


    To assess whether the effect is practically negligible, we report the 90% confidence interval, which corresponds to a two one-sided tests (TOST) equivalence procedure at
    $\alpha = 0.05$
    (Lakens et al., 2018). The estimated coefficient is
    $-0.032$
    ($\text{SE} = 0.072$),
    yielding a 90% CI of
    $[-0.150, 0.086]$.
    Given the pooled outcome standard deviation of
    $1.329$,
    this corresponds to
    $[-0.113, 0.065]$
    in standardized units (Cohen’s $d$).
    Using the conventional threshold of $|d| = 0.2$ as the smallest effect size of interest, the 90% CI lies entirely within the equivalence region $[−0.2, 0.2]$, supporting the conclusion of practical equivalence.


Using Bayesian t-tests


    
        Show R code
        Hide R code
    

    
        
    


NEW: Open web app

My personal intuitionist view of evidence is as follows: any study should yield one of three conclusions: evidence for X, evidence for Y, or indeterminate. Bayesian tests deliver on all three possibilities. A Bayes Factor (BF) can provide evidence for the null, evidence for the alternative, or remain inconclusive ($\text{BF} \approx 1$). Frequentist methods, by contrast, do not distinguish between evidence for $H_0$ and poor data. This is why I prefer Bayesian approaches for quantifying null effects, though TOST serves a similar purpose when properly powered. However, Bayesian methods come with batteries included. As I like to say: “Bayesian methods solve all problems, for free.” It’s true. Really!

Simply put, Bayesian methods provide an alternative approach by computing a Bayes Factor, which quantifies the relative evidence for the null hypothesis versus the alternative. A $\text{BF}_{01} > 3$ suggests moderate evidence for the null, while $\text{BF}_{01} > 10$ suggests strong evidence. Conversely, $\text{BF}_{01} \lt 1/3 \equiv 1 / \text{BF}_{10}$ suggests at least moderate evidence for an effect. Read more here.

Bayesian t-tests (e.g., as implemented in the BayesFactor package in R) specify a prior distribution on effect sizes and compute how much the data update your beliefs. The advantage of this approach is that you can actually claim evidence for the null, not just failure to reject it. The disadvantage is that results depend on your choice of prior. Therefore, always use the now-standard default prior of $\text{Cauchy}\left(0, \frac{\sqrt{2}}{2}\right)$ and conduct robustness analyses.

Here is an example (from the code above, same data as with the TOST example):


    We conducted a Bayesian independent samples t-test (Rouder et al., 2009) using a default prior with scale parameter $r = \frac{\sqrt{2}}{2} \approx 0.707$ (Cauchy distribution centered at zero). The Bayes Factor strongly favored the null hypothesis, with $\text{BF}_{01} = 15.0$ (equivalently, $\text{BF}_{10} = 0.067$), indicating that the observed data are 15 times more likely under the null hypothesis of no effect than under the alternative hypothesis. This constitutes strong evidence for the absence of an effect. To assess robustness to prior specification, we conducted sensitivity analyses with alternative scale parameters: $r = 0.5$ yielded $\text{BF}_{10} = 0.094$ ($\text{BF}_{01} = 10.7$); $r = 1.0$ yielded $\text{BF}_{10} = 0.047$ ($\text{BF}_{01} = 21.1$); and $r = 2.0$ yielded $\text{BF}_{10} = 0.024$ ($\text{BF}_{01} = 42.2$). Across all specifications, we obtained consistent strong evidence favoring the null hypothesis ($\text{BF}_{01} > 10$ in all cases), demonstrating that our conclusion is robust to reasonable prior choices.


In practice, TOST and Bayesian methods often agree qualitatively. If you have a well-powered study with tight confidence intervals, TOST will show equivalence and the Bayes Factor will favor the null (as in the example above). If your study is underpowered, neither method will save you: TOST will fail to show equivalence and the Bayes Factor will be inconclusive (close to 1). The lesson is simple: power matters for null results just as much as for positive results. Design your study properly.

Know what you are doing, measuring and estimating

Any experiment starts with a theory of human behavior. This theory may not be mathematical, but it is always a statement about counterfactual behavior. The section title promises three requirements: know what you are doing (treatment design), know what you are measuring (outcome elicitation), and know what you are estimating (estimands). All three must align with your theory’s objects.

Know what you are doing

For example, suppose that you want to study workers’ effort when provided with information about coworkers’ effort. Now this is clearly an experiment about workers’ beliefs. These beliefs are changed through information, and then those beliefs are (probably) thought to induce a response in effort. I have talked more above about some recommendations for such experiments, but for now let us just focus on treatment design.

Putting aside questions of deception, a simple experiment could tell some participants (i) that their group of coworkers had an average effort level of $e_1$ and some other participants (ii) that their group of coworkers had an average effort level of $e_2$, where $e_2 \lt e_1$. Do you see a problem with this design?


    
        Show problem
        Hide problem
    

    
        This design is built on the assumption that workers’ effort level is shaped by beliefs about the average effort of others. But it could well be that their effort responds to the minimum effort, the maximum effort, or any other statistic of the distribution.

        If your theory proves that only beliefs about averages should matter, then such a design is wholly appropriate. And even if people’s effort is in fact a function of the minimum, the minimum could still be correlated with the average, so the design may “work” accordingly. Still, beliefs are extraordinarily rich objects, and any particular experimental configuration of beliefs must grapple with that complexity. As Einstein put it: “theory decides what we can observe.” So true!
    


Formal mathematical theory has the great feature that it forces you to make explicit assumptions. Formal theory constrains an experimenter’s degrees of freedom. This is why theory is invaluable even when not strictly necessary: it disciplines experimental design.

Know what you are measuring

Your outcome measures must be as close as possible to theoretically relevant objects. There are important exceptions: sometimes the theoretically relevant object (such as a preference parameter) cannot be directly elicited as an outcome measure. Simple tasks like binary choices, coupled with a structural model, can nonetheless be highly informative about it. Theory still dictates what is relevant. It just also dictates the mapping from observable outcomes to theoretical objects.

In the absence of formal theory, or if theory makes no predictions for a particular object, your planned analyses can serve as a reduced-form guide to what you need to measure. A key principle: comparisons are only valid when the objects being compared are of the same kind. The null hypothesis must be a sensible benchmark.

Suppose you want to compare first-order beliefs (what I believe about others) with second-order beliefs (what I believe others believe about others). You elicit the first-order belief by asking: “What is the average effort of others?”

Now, how should you elicit the second-order belief? An invalid approach would be to ask: “What is the most common belief about the average effort of others?” This compares a mean (first-order) with a mode (second-order). These are two different statistics that need not coincide even under rationality.

A valid approach could ask: “What is the average belief about the average effort of others?” Now both elicitations concern means. Under common priors and rational expectations, the first-order and second-order beliefs should coincide. Any divergence could reveal false consensus, projection bias, or asymmetric updating. Crucially, the comparison is valid in principle because the null hypothesis (equality under rationality) is a meaningful benchmark.

Know what you are estimating

We have discussed estimands in greater detail above. For now, keep the following principle in mind, which I call the Fundamental Law of Experimental Economics:

If theory predicts a monotonic individual response, a zero average treatment effect (ATE) implies zero individual effects.

This law holds because the mean cannot hide offsetting responses when all individual effects have the same sign. Under random assignment, the ATE therefore directly tests the theory. This is a precise statement: the ATE is an expectation (an integral over the distribution of individual types) that identifies the causal effect of a stimulus. Random assignment “marginalizes out” unobservable theoretical parameters, leaving only the predicted effect.

The Fundamental Law immediately suggests how to design good experiments: isolate behavioral factors that, under theory, predict a monotonic change. Such factors deliver clean comparative statics. Contrarily, if theory permits heterogeneous signs (some individuals respond positively, others negatively) a zero ATE is uninformative. It could reflect no effects, or it could reflect large offsetting effects. Without further restrictions, you cannot distinguish these cases.

The upshot is that experimental design and statistical analysis must be in concordance. The estimands of interest must directly test the predicted monotonic relationships that your theory delivers. This is what it means to know what you are estimating.

Radical openness is likely costless or a free lunch (if you start early)

Transparency is free if you do things properly from the beginning. Do good theory. Write clean analysis code. Organize your files sensibly. When the time comes to publish a replication package, you will have virtually no extra work. The cost is zero. Actually, the cost is negative, because good practices save you time during the project itself!

Better yet: just upload sanitized data immediately after your experimental sessions to GitHub (if permitted). Make your analysis scripts public from day one. There is little reason to wait. If your IRB or data agreements prohibit immediate sharing, fine. Otherwise, default to radical openness. And no, you will not be “scooped.” Your project is, in all likelihood, not that interesting.

Preanalysis plans and preregistrations are also valuable. They protect you from accusations of p-hacking. They force you to think clearly about your design and analysis before seeing the data. They improve the credibility of your findings and they make our whole science better. Also, by committing to analyses, sample sizes, and the like beforehand you can remove the need for later decisions. Just do it.
]]></description>
</item>
<item>
<title>A brief overview of Bayes Factors</title>
<link>https://max.pm/posts/bayes-factors/</link>
<guid>https://max.pm/posts/bayes-factors/</guid>
<description><![CDATA[



    Bayes' theorem expresses the following relationships between various probabilities:


\begin{equation}
P(H \,\vert\, D) = \frac{P(H) P(D \,\vert\, H)}{P(D)}
\label{bayes}
\end{equation}


    $P(H \,\vert\, D)$ is the posterior (as in, post having data, given the data).
    $P(H)$ is the prior.
    $P(D \,\vert\, H)$ is the likelihood of the data (given the hypothesis).
    $P(D)$ is the marginal likelihood of the data.


Proving Bayes' theorem

\begin{align*}
P(H \,\cap\, D) &= P(H \,\vert\, D) P(D)\\
P(D \,\cap\, H) &= P(D \,\vert\, H) P(H)
\end{align*}

Since $P(H \,\cap\, D) = P(D \,\cap\, H)$, Equation \eqref{bayes} follows.

Working in odds space (deriving a Bayes Factor)

Suppose now we are working with two hypotheses, $H_1 , H_2$. We can use Equation \eqref{bayes} twice to obtain the following representation of posterior odds:

\begin{align*}
\frac{P(H_1 \,\vert\, D)}{P(H_2 \,\vert\, D)} &= \frac{\frac{P(H_1) P(D \,\vert\, H_1)}{P(D)}}{\frac{P(H_2) P(D \,\vert\, H_2)}{P(D)}}\\
&= \frac{P(H_1) P(D \,\vert\, H_1)}{P(H_2) P(D \,\vert\, H_2)}\\
&= \underbrace{\frac{P(H_1)}{P(H_2)}}_{\text{Prior Odds}} \cdot \underbrace{\frac{P(D \,\vert\, H_1)}{P(D \,\vert\, H_2)}}_{\text{Bayes Factor}}
\end{align*}

In other words, Posterior&nbsp;Odds = Prior&nbsp;Odds · Bayes&nbsp;Factor.

More precisely, we use a subscript and define

\begin{equation}
BF_{12} = \frac{P(D \,\vert\, H_1)}{P(D \,\vert\, H_2)}
\label{bf}.
\end{equation}

Note how $BF_{12}$ is about likelihood odds, and the subscript translates “top to bottom” to numerator and denominator in the Bayes Factor.

Crucially,

\begin{equation}
BF_{21} = \frac{1}{BF_{12}}.
\label{reversed}
\end{equation}

Calculating the posterior probability from a Bayes Factor

Suppose now that either $H_1$ or $H_2$ is true, and that there are no other possibilities. That is, $H_1 \cup H_2 = \Omega$, so $H_1, H_2$ divide all states of the world, $P(\Omega) = 1$. Then, $P(H_2 \,\vert\, D) = 1 - P(H_1 \,\vert\, D)$.

This means that the posterior probability can be calculated as follows:

\begin{align*}
\frac{P(H_1 \,\vert\, D)}{1-P(H_1 \,\vert\, D)} &= \frac{P(H_1)}{P(H_2)} \cdot \frac{P(D \,\vert\, H_1)}{P(D \,\vert\, H_2)}\\
\\
\Longleftrightarrow\\
P(H_1 \,\vert\, D) &= \frac{\frac{P(H_1)}{P(H_2)} \cdot BF_{12}}{1 + \frac{P(H_1)}{P(H_2)} \cdot BF_{12}}
\end{align*}

In the common case where prior odds are equal to 1, the posterior probability has a convenient expression:

\begin{equation}
P(H_1 \,\vert\, D) = \frac{BF_{12}}{1 + BF_{12}}
\label{postprob}
\end{equation}

Bayes Factors and strength of evidence

If $BF_{12} \geq 1$, use the below table; otherwise, use Equation \eqref{reversed} to calculate $BF_{21}$. The second column assumes prior odds equal to 1.


    
        
            Bayes Factor
            Posterior probability
            InterpretationA
        
    
    
        
            $1$
            $0.5$
            No evidenceB
        

        
            $1 \dots 3$
            $0.5 \dots 0.75$
            Anecdotal evidenceB
        

        
            $3 \dots 10$
            $0.75 \dots 0.91$
            Moderate evidenceB
        

        
            $10 \dots 30$
            $0.91 \dots 0.97$
            Strong evidenceB
        

        
            $30 \dots 100$
            $0.97 \dots 0.99$
            Very strong evidenceB
        

        
            $> 100$
            $> 0.99$
            Extremely strong evidenceB
        
    
    
        A As per Jeffreys (tradition has it).
        B In favor of the hypothesis under consideration.
    


Relationship to likelihood ratio tests

The classical likelihood ratio test statistic is

\begin{equation}
\Lambda = \frac{\sup_{\theta \in \Theta_1} P(D \,\vert\, \theta)}{\sup_{\theta \in \Theta_0} P(D \,\vert\, \theta)}
\end{equation}

where $\Theta_0, \Theta_1$ are the parameter spaces under the null and alternative. This ratio compares the best-fitting parameter values.

The Bayes Factor instead compares average likelihoods:

\begin{equation}
BF_{10} = \frac{\int P(D \,\vert\, \theta) \, p(\theta \,\vert\, H_1) \, d\theta}{\int P(D \,\vert\, \theta) \, p(\theta \,\vert\, H_0) \, d\theta}
\end{equation}

The averaging penalizes models with diffuse priors over large parameter spaces: probability mass wasted on poor-fitting parameter values drags down the marginal likelihood. This provides an automatic Occam's razor absent from $\Lambda$.

Decomposing Bayes Factors

In some cases, there is no single data set $D$ that can be used to evaluate the hypotheses, but rather multiple “evidence” $E_1 , E_2, \dots, E_m$ such that $E \equiv \bigcap_{i=1}^{m} E_{i}$ is the totality of evidence.

Rewriting Equation \eqref{bayes} in terms of evidence, we get

\begin{equation}
P(H \,\vert\, E) = \frac{P(H) P(E \,\vert\, H)}{P(E)}.
\end{equation}

Using the chain rule, for $m=2$ it holds that

\begin{equation}
P(E \,\vert\, z) = P(E_1 \,\vert\, z) P(E_2 \,\vert\, E_1, z)
\end{equation}

for any conditioning variable, $z$. More generally,

\begin{equation}
P(E \,\vert\, z) = P(E_1 \,\vert\, z) \prod_{i=2}^{m} P(E_i \,\vert\, E_1, \dots, E_{i-1}, z).
\end{equation}

The Bayes Factor can thus be decomposed as

\begin{align*}
\frac{P(E \,\vert\, H_1)}{P(E \,\vert\, H_2)} &= \frac{P(E_1 \,\vert\, H_1) \prod_{i=2}^{m} P(E_i \,\vert\, E_1, \dots, E_{i-1}, H_1)}{P(E_1 \,\vert\, H_2) \prod_{i=2}^{m} P(E_i \,\vert\, E_1, \dots, E_{i-1}, H_2)}\\
&= \frac{P(E_1 \,\vert\, H_1)}{P(E_1 \,\vert\, H_2)} \prod_{i=2}^{m} \frac{P(E_i \,\vert\, E_1, \dots, E_{i-1}, H_1)}{P(E_i \,\vert\, E_1, \dots, E_{i-1}, H_2)}\\
&= BF_{12}^{E_1} \cdot BF_{12}^{E_2 \, \vert \, E_1} \cdot \dots \cdot BF_{12}^{E_m \, \vert \, E_1, \dots, E_{m-1}}
\end{align*}

This decomposition is useful when evidence arrives sequentially or when different pieces of evidence have qualitatively different sources. Instead of computing a single likelihood ratio over all evidence at once, you can update beliefs incrementally. Each factor $BF_{12}^{E_i \, \vert \, E_1, \dots, E_{i-1}}$ measures how much the $i$-th piece of evidence favors $H_1$ over $H_2$, given what was already known.

In practice, this matters when some evidence is easier to evaluate than others, or when you want to diagnose which pieces of evidence drive the overall conclusion. A large overall Bayes factor might be dominated by a single $E_i$, or it might accumulate from many modest contributions. The decomposition makes this transparent. It also helps when combining evidence from heterogeneous sources (e.g., experimental data and observational data) where assuming independence would be wrong, but the conditional structure is tractable.

When the pieces of evidence are conditionally independent given the hypothesis (when $P(E_i \mid E_1, \dots, E_{i-1}, H) = P(E_i \mid H)$ for all $i$) the decomposition simplifies to a product of unconditional Bayes factors:

\begin{equation}
\frac{P(E \,\vert\, H_1)}{P(E \,\vert\, H_2)} = \prod_{i=1}^{m} \frac{P(E_i \,\vert\, H_1)}{P(E_i \,\vert\, H_2)} = \prod_{i=1}^{m} BF_{12}^{E_i}
\end{equation}

This is convenient but rarely justified in practice. Evidence from the same domain or measurement process is typically correlated, and treating dependent evidence as independent can inflate confidence.
]]></description>
</item>
<item>
<title>Your R code may not be yours</title>
<link>https://max.pm/posts/r_not_yours/</link>
<guid>https://max.pm/posts/r_not_yours/</guid>
<description><![CDATA[

    This is not legal advice.


tl;dr: Your R analysis code likely has to be licensed under the GNU GPL. My r-snippets README tells you how to proceed for maximum legal compliance.



I have a confession: Until recently, I believed my research code was mine to license however I pleased. Then I discovered a significant legal gray area that should concern anyone sharing R code publicly.

While preparing a replication package, I stumbled onto a contested question in open source licensing: If your R code loads certain popular GPL-licensed packages, can you legally license it however you want? The answer is uncertain, but the safer and more logical interpretation suggests you cannot, strictly speaking.



Many of the most popular R packages used in econometrics and empirical research are licensed under the GPL (GNU General Public License). The sandwich package is perhaps the most important case. It’s utterly ubiquitous for robust standard errors. But it’s far from alone: lmtest, plm, fixest, AER, ivreg, and MASS are all GPL-licensed. These aren’t obscure packages. These are veritable workhorses of empirical economics.

Now, if these packages were licensed under the LGPL (Lesser GPL), there would be no controversy. The LGPL explicitly permits linking to libraries without viral copyleft effects. But they aren’t. They’re licensed under the GPL, and this is where things get complicated.

According to the Free Software Foundation’s interpretation of the GPL, code that loads and uses GPL libraries is a “combined” or derived work and must itself be licensed under the GPL. Under this reading, that library(sandwich) at the top of your analysis script makes your entire file a GPL work. Your carefully crafted LICENSE file declaring your replication code as CC0 or MIT is potentially invalid. You may have inadvertently contravened copyright law.



There is no settled case law on whether loading a GPL library in an interpreted language creates a derivative work under copyright law. This matters enormously, and there are two competing theories:

The FSF’s position: The GPL FAQ states that when an interpreter provides “bindings” to GPL facilities, the interpreted program is “effectively linked” to those facilities. Under this view, library(sandwich) creates a combined work that must be distributed under GPL terms. This interpretation has some logical force: if there were no difference between GPL and LGPL for interpreted languages, why would LGPL exist? Under the idea of “software freedom” espoused by the FSF, it simply cannot make a difference whether the language is interpreted or not, as the GPL is supposed to protect users’ rights. These rights are paradoxically best-protected by a maximally infectious copyleft license.

The R community’s position: The well-known “R Packages” book by Hadley Wickham states it’s their “personal opinion that the license of your package doesn’t need to be compatible with the licenses of R packages that you merely use by calling their exported R functions.” The R Foundation clarified in 2009 that R code doesn’t need to be GPL-licensed just because it uses R. Thousands of CRAN packages use MIT licenses despite likely depending on GPL packages, suggesting widespread acceptance of this interpretation. Certainly is is correct that the mere use of R the programming language itself does not impose a particular license on R code, but interfacing too closely with R APIs may (see below).

Which interpretation is correct? Legally, we don’t know. There’s been no court case. But here’s the critical point: the FSF’s interpretation is safer from a compliance perspective. If you want to be conservative about license compliance, treating your code as GPL when it loads GPL packages is the less risky choice. (Indeed, the risk is zero, but it is sad that we cannot be even more liberal.) The FSF’s position is also logically more convincing:

Perhaps I can use an economic analogy: if the use of a GPL-licensed work is highly substitutable, the GPL does not apply to your use. But if the use is not highly substitutable, the GPL applies. In other words, if your code is so narrow that it relies on a particular (GPL-licensed) implementation, it is a derivative. However, copyright determinations involve many factors (creativity, expression vs idea, transformative use), so this is more of a heuristic. Nonetheless, as analysis code often works around particular, highly specialized implementations, it is likely to constitute a derivative.

There is an important exception: if you’re only using your code privately or within your organization, the GPL doesn’t restrict you. The copyleft provisions only apply when you distribute the code to others. But when you submit a replication package to a journal, post your code on GitHub, or share your analysis with another scientist, you’re distributin’.



I suspect there are thousands of replication packages sitting in journal data repositories right now with licensing terms that might be problematic under the stricter interpretation. Researchers who carefully chose licenses for their code, possibly unaware that this legal question even exists. Code released as “public domain” or CC0 that might need to be GPL.

Are these researchers violating copyright law? Under the FSF’s interpretation, possibly. Under the R community’s interpretation, no. The legal ambiguity around interpreted languages means there’s room for disagreement, even among sophisticated users of open source software. That uncertainty itself is a problem when we want clear terms for code reuse.



When I discovered this issue, I had to make a choice for my r-snippets repository. These snippets use (excellent) GPL packages. I could have relied on the permissive interpretation, but I opted for the conservative approach: I updated the README to license them under GPL.

For my own replication package, we’re taking the same conservative approach, even though I tend to prefer more permissible licenses. (It depends on context, though.)



Let me be very clear about one thing: If you’re writing an R package meant to be used as a library by others, strongly consider LGPL instead of GPL.

The LGPL (Lesser GNU General Public License) was designed precisely to avoid this ambiguity. It allows your library to remain free and open source while explicitly permitting users to link to it without their code becoming a derived work. No legal uncertainty, no competing interpretations. This works as long as your library doesn’t bind too closely to R-internal APIs. If your library uses standard features of R the language, LGPL is fine.

I’ve done exactly this with some of my own software (unrelated to R). uproot is licensed under the LGPL specifically to avoid creating this problem for users. If you’re writing a library that’s meant to be called by other people’s code (which is basically the definition of an R package), LGPL removes all ambiguity. It is fundamentally a question about where to draw the line. The LGPL draws the line a bit closer than the GPL.



The one piece of good news: the GPL applies to code, not to data or to output generated by that code. You can still license your datasets under CC0 or CC-BY or whatever terms are appropriate. The GPL doesn’t “infect” your data, only your code. My r-snippets README contains further information.



Licensing really matters. And open science depends on clarity about reuse rights. The current situation creates uncertainty: If I find your replication package with a LICENSE file saying MIT but your code loads GPL packages, what am I supposed to conclude? That you’re following the permissive interpretation? That you’re unaware of the question? That you researched it and made a conscious choice?

The GPL is not a bad license. It has served the free software community well for decades, and many people prefer its strong copyleft provisions. I do too, in some cases. But in the context of interpreted languages and package dependencies, its requirements are unclear. If you’re going to use GPL code (and if you’re doing econometrics in R, you almost certainly are), you should at least understand that this legal uncertainty exists and decide which interpretation you’re comfortable with.



So check your code. Check your licenses. If you’re loading GPL packages, you face a choice: follow the conservative FSF interpretation and license your code as GPL, or rely on the R community’s permissive interpretation. Given the uncertainty, the GPL approach is arguably safer. But whichever you choose, you should make that choice consciously.

Your R code might not be yours to license freely. The law is unclear, but now you know the question exists.
]]></description>
</item>
<item>
<title>Using the Framework Laptop 13 with Debian 13 (trixie)</title>
<link>https://max.pm/posts/framework_13_debian/</link>
<guid>https://max.pm/posts/framework_13_debian/</guid>
<description><![CDATA[
I recently ordered and obtained a brand-new Framework Laptop 13 with AMD Ryzen™ AI 300 Series - Ryzen™ AI 7 350.

Changes to this document


    2025-10-01
    Post created.

    2025-10-04
    Added microphone configuration.

    2025-10-06
    Added touchpad notes.

    2026-01-24
    Added NVMe crash investigation and fix.

    2026-02-04
    Added SSD disappearance issue. Switched to 60W charger.

    2026-03-16 (current version)
    Updated SSD disappearance issue: the problem has recurred repeatedly.


Table of contents


    


Assembly

Needless to say, I ordered the DIY edition. The official guide seems basically correct, though I was unable to identify a “white line” (Step 8).

Linux kernel

Linux 6.12.48 works very well. However, I took the liberty to add Backports and install Linux 6.16.3 (apt install linux-image-amd64/trixie-backports). The issues described below applied to both versions.

So, despite what others write on the Internet, it literally does not matter one bit. Linux 6.12.48, which comes with Debian trixie, is recent enough. Wi-Fi works perfectly well, and so does Bluetooth and everything else I can think of. I am writing these lines on kernel 6.12.48.

Always just use Debian. You'll never need to worry about anything. I recommend completely ignoring other Linux distros, especially Ubuntu and similar slop.

I also recommend using btrfs, but that too is not specific to a Framework laptop.

Graphics issue

I am a user of dwm and I encountered the following issue: after I had played a video, the screen essentially froze in place and refused to redraw those parts on the screen that had changed. Whenever I switched to another dwm “tag” (a workspace, if you will), the screen was redrawn, however.

In any case, that was fixed by putting amdgpu.dcdebugmask=0x12 into GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub and running sudo update-grub. This disables PSR (Panel Self-Refresh).

By the way, screen brightness can be controlled by means of teeing to /sys/devices/pci0000*/*/*0/drm/card0/card0-eDP-1/amdgpu_bl0/brightness.

Bluetooth/audio issue

Regrettably Debian has decided to go full pipewire. Never go full pipewire. I like pulseaudio especially because it has always worked flawlessly (as has systemd, by the way—another irrelevant controversy to the detriment of users). One of the issues with pipewire is that I simply could not get it to work with Bluetooth devices. Blueman just showed No audio endpoints registered and refused to connect with my headphones. Audio did play, but pavucontrol (another excellent piece of software) was no help in getting it all to work.

That one was resolved with systemctl --user disable --now pipewire pipewire-pulse wireplumber pipewire.socket pipewire-pulse.socket &amp;&amp; sudo systemctl --global mask pipewire pipewire-pulse wireplumber pipewire.socket pipewire-pulse.socket and then systemctl --user enable --now pulseaudio.service pulseaudio.socket Remember to install pulseaudio-module-bluetooth, which automatically pulls in pulseaudio.

To get the builtin microphone (the “Internal Stereo Microphone”) to work, you need to use the “Play HiFi quality Music (Mic1, Mic2, Speaker)” profile of your Family 17h/19h/1ah HD Audio Controller. See Configuration in pavucontrol.

NVMe stability

On January 20 and January 24, I encountered random crashes in the middle of the night. The screen started flickering and the device was completely inoperable. Further investigations revealed that these crashes occurred under heavy I/O, namely during my nightly btrfs scrub start / cronjob. The issue could be reproduced by running the same plus glmark2 --run-forever plus having some spiky CPU benchmarks. These crashes were presaged by audit messages in dmesg and concluded with nvme nvme0: controller is down; will reset. The fix recommended in these messages and on the internet (adding kernel parameters) was not quite sufficient, but read on.

After a tremendous amount of research, I came to a theory that is related to power supply triggering buggy NVMe power state management. At home, I use a 30W USB-C charger, not the 60W charger I purchased with the laptop. Under heavy load, 30W are not sufficient (even 60W can be borderline), and the power controller will draw some current from the battery to make up for the difference. That in turn causes the battery to enter the discharging state (as may be verified with upower). This kicks in a feature called PCIe Dynamic Link Power Management. As anyone knows, such features are inherently problematic on Linux. BAZINGA!

I was able to resolve this issue by disabling that feature in the BIOS. I am for now also keeping the suggested kernel parameters (nvme_core.default_ps_max_latency_us=0 pcie_aspm=off pcie_port_pm=off plus amdgpu.dcdebugmask=0x12, see above). I have not yet tried removing these kernel parameters. Disabling PCIe Dynamic Link Power Management appears to be the key causal factor (but there may be interactions, and for now I am happy to throw these parameters at the kernel). Despite substantial effort, I have been unable to reproduce the crashes under this configuration. I will update this post should that change. Note that disabling these power management features increases power consumption.

The issue has not recurred since.

SSD disappearance

Separately, I once awoke to a screen saying that no operating system could be found. Once again, this happened during my nightly btrfs scrub start / run. After a reboot, everything once again looked normal.

While WD BLACK SN850 is known to suffer from “sudden death,” the SN850X that I have is not immune either. Reports of BSODs, drive disconnections, and ASPM-related crashes are not hard to find. However, all SMART data looked entirely normal. Despite tremendous effort, I was unable to figure out the core issue, or to reproduce the crash. I thus followed the ubiquitous recommendation to “reseat” the SSD (though I had already put it in very tightly and there was no discernible slack), and switched to the official 60W charger.

Unfortunately, the SSD disappearance has recurred again and again—roughly once every two weeks, always during the nightly btrfs scrub, and even under the appropriate 60W charger. This is frustrating. The issue remains unresolved.

Overall assessment

I am impressed by the Framework Laptop 13. It's a high-performing laptop, aesthetically pleasing, and pretty compatible with Debian GNU/Linux. The keyboard is good, too (especially because it is very quiet).

The build quality in general is very good. Right-clicking works through the bottom right corner of the touchpad. Compared to the keyboard, clicking in general is kind of noisy, which is a downside.

One issue that could limit my enjoyment is that it has only four slots for expansion cards, with one of the four taken up by the power supply. So far this has remained only a theoretical concern. Also, I would like to get a Mini-DisplayPort expansion card.

After 4 months of usage, I am still happy with my Framework 13.

Benchmarks

Here is some further information about my previous laptop (a ThinkPad T440s) and my new Framework, including hardinfo2 benchmarks.


]]></description>
</item>
    </channel>
</rss>
