Skip to main content

enum2x2

Every 2×2 table a published summary statistic still allows.

PyPI Tests Python License: MIT

A study comparing two diagnostic criteria on one population computes its numbers from a 2×2 table, then publishes the numbers and not the table. The quantity a reader wants — how many patients the two criteria classify differently, and in which direction — is gone. It can often be recovered, because the same reports print the sample size, both marginal totals and a statistic such as κ, and together those leave a few integer tables, often just one.

The problem, concretely

A pooled series of 768 patients, two readings of the DSM-5 delirium criteria, published as κ = 0.29. That reads like two definitions that disagree a fair amount.

Here is the table underneath it:

relaxed + relaxed −
strict + 158 0
strict − 308 302

The strict class is nested inside the relaxed one. No patient is strict-positive and relaxed-negative; the 308 they differ on all fall the same way, relaxed-positive and strict-negative. One reading is simply three times wider than the other. Observed agreement is the highest these two marginals permit: κ = 0.29 is the highest κ these marginals allow.

None of that is in "κ = 0.29", and κ = 0.29 is all the paper printed.

Install

pip install enum2x2

For development:

git clone https://github.com/elliottower/enum2x2 && cd enum2x2 && pip install -e ".[test]"

Standard library only. Python 3.10+. make compare and make crosscheck need R and skip cleanly without it.

Use

import enum2x2

r = enum2x2.recover(768, n_a=158, n_b=466, kappa="0.29")
r                     # <Recovery unique on N=768: Table(158, 0, 308, 302)>
r.status              # 'unique'
r.table.discordant    # (0, 308)   -- 308 patients one way, none the other
r.table.asymmetry     # 1.0

r = enum2x2.recover(20306, n_a=866, n_b=1603, kappa="0.22")
r                     # <Recovery 11 tables on N=20306: 320-330/536-546/...>
r.cell_ranges["n10"]  # (536, 546)

r = enum2x2.recover(370, p_a="44.6", p_b="43.2", marginals_as_percent=True,
                    kappa="0.48", agreement="73", agreement_as_percent=True)
r.status              # 'infeasible'
r.reason              # 'the marginals and kappa admit 1 table(s); adding the
                      #  published agreement admits none'

What can close the table

A 2×2 with N fixed has three degrees of freedom and the two marginals use two, so one further quantity closes it. Each of these does, passed as the string the source printed:

enum2x2.recover(113, n_a=39, n_b=83, kappa="0.32")        # agreement coefficient
enum2x2.recover(113, n_a=39, n_b=83, agreement="0.61")    # raw observed agreement
enum2x2.recover(113, n_a=39, n_b=83, mcnemar="<0.001")    # a printed McNemar result
enum2x2.recover(113, n_a=39, n_b=83, discordant=44)       # the discordant total itself
enum2x2.recover(113, n_a=39, n_b=83, positive_agreement="0.64")   # specific agreement

Everything accepted, including Byrt's two indices, which are taken as checks but close nothing on their own:

input what it is notes
kappa Cohen's κ
phi the φ coefficient as printed, sign included decided exactly, through φ² with the sign kept
agreement observed agreement, (n11+n00)/N agreement_as_percent for "61"
mcnemar a printed p, point ("0.0002") or bound ("<0.001") mcnemar_test: exact (default), midp, chisq, chisq_cc; chisq_cc follows R's mcnemar.test
discordant n10 + n01, the count classified differently an integer, not a string
positive_agreement / negative_agreement positive / negative specific agreement (Cicchetti and Feinstein 1990) symmetric in the two criteria; not the FDA's PPA and NPA, which take one criterion as the comparator
jaccard overlap among cases either criterion identifies ignores n00
pabak prevalence-adjusted bias-adjusted κ
scott_pi / gwet_ac1 Scott's π, Gwet's AC1 proposed where κ's prevalence dependence bites
prevalence_index / bias_index Byrt's indices both are fixed by the marginals, so neither closes the table alone
odds_ratio / mcnemar_odds_ratio cross-product, and the paired n10/n01
phi_squared φ², for a source that printed the square a printed φ goes to phi: the square of φ's rounding interval is not the interval of a printed φ²

McNemar is the one worth spelling out. It looks only at the two discordant cells, and the marginals already fix their difference, so the statistic fixes their sum and the table follows. A paper printing a McNemar result printed the table without saying so. The exact (binomial) variant is a ratio of integers and is decided exactly; the chi-square variants compute a tail probability that is not rational, and say so. The exact test recomputes its binomial tail for each candidate table, so with McNemar as the only closing figure it slows above a few thousand cases; alongside kappa it is fast.

κ is passed as the string the source printed, not as a float: "0.10" and "0.1" imply different intervals and a float cannot tell them apart. Passing a float is refused with that reason.

Four statuses, and they are kept apart: unique, set, infeasible (the published figures admit no common table), insufficient (a required figure was never published). An impossible call — a marginal above N, a κ outside [-1, 1], two marginals given for one criterion — raises InvalidInput. An impossible source is reported, never raised.

recover_many cannot raise, so a row that would have raised InvalidInput comes back with a fifth status, impossible, and its reason. Counting those as insufficient would inflate how many sources under-reported.

enum2x2 comparisons.csv       # a file of comparisons, one per row
make test                     # the test suite
make compare                  # against metafor::conv.2x2, under rounding
make crosscheck               # this kappa against every R implementation installed

Why rounding is the whole problem

With exact inputs the algebra is one line. For positive marginals p_A and p_B on N observations,

p_e  = p_A·p_B + (1 − p_A)(1 − p_B)
p_o  = κ(1 − p_e) + p_e
p_11 = (p_o + p_A + p_B − 1) / 2

But nobody prints exact inputs. A marginal printed 4.26% and a κ printed 0.22 are intervals, so the inputs identify a set of integer tables. This walks every non-negative integer table summing to N whose statistics round back to the printed strings, and reports the set. Where a source also prints its raw agreement, that enters as a further constraint and narrows the set.

Rounding conventions

Which interval a printed figure stands for depends on how the source got from the exact value to the digits. The default reading is round-half-up, so 14.7 stands for [14.65, 14.75]. A source that truncates prints 14.7 for everything in [14.7, 14.8), which shares only its lower end with the first.

convention a printed p means
half_up [p − u/2, p + u/2] the default; closed at both ends, because the tie rule is not stated either
truncate [p, p + u) the digits dropped and the sign kept
any either reading, figure by figure one source can round some figures and truncate others

u is one unit in the last printed place, read off the literal string, so "3.3" and "3.30" differ here as they do everywhere else.

enum2x2.recover(353, n_a=84, n_b=46, kappa="0.648", convention="any")

Goyal et al. (2025) print a kappa and a disagreement percentage for each of the 55 pairwise comparisons of eleven gestational-diabetes criteria on 353 women. Held against the table its own kappa identifies, each printed disagreement is consistent with either convention (33 of them), with truncation alone (15), or with rounding alone (7). Read as rounded throughout, 15 of the 55 comparisons return no compatible table; under any all 55 are uniquely identified.

Four outcomes, deliberately kept apart:

uniquely identified one table survives — you know the answer exactly
set-identified several do; cell ranges are reported across them
infeasible none does; the published figures admit no common table
insufficiently reported a required input was never published

The last two are different findings and collapsing them would report a gap in the record as a defect in the source.

Against metafor::conv.2x2

conv.2x2 converts a summary into a single table. This enumerates every table the summary admits. The names are meant to sit next to each other.

It takes an odds ratio, a phi coefficient or a chi-square — not Cohen's κ, which is what agreement studies report — and on rounding its documentation is candid:

The calculations underlying the function are exact only for unrounded values … The present function uses optimization methods to reconstruct the table counts so that the discrepancy between the reported measures and the reconstructed ones are minimized. This is not guaranteed to reconstruct the actual table exactly, but should usually yield a close match.

make compare measures what "close" costs, on 1,200 known tables at N = 2,000, 9,170 and 20,306 with one statistic printed to two decimals:

conv.2x2 point estimate enum2x2
exactly right 21.8% —
contains the true table — 100%
largest cell error median 2, 90th pct 8, max 20 —
uncertainty reported none the range itself

conv.2x2 is not broken; it does what it says. But it hands you one table and no indication of how far off it is. Where a finding rests on how many patients were reclassified in each direction, an unstated error of up to 20 patients can change it.

Where this sits among neighboring packages

package direction framing takes returns
metafor::conv.2x2 reverse unpaired, group × outcome OR, φ, χ², sens/spec/PPV/NPV one table, by optimization
irr, psych forward paired agreement raw ratings κ, π, AC1, Bhapkar, Stuart–Maxwell
exact2x2, DTComPair forward paired raw counts tests and intervals
scrutiny (GRIM, GRIMMER, DEBIT) reverse means, SDs, binary means reported summaries whether the summary is attainable
mlscorecheck reverse classifier evaluation exact class counts on one side, rounded scores whether any table fits
enum2x2 reverse paired agreement κ, agreement, McNemar, and the rest above every table the figures admit

mlscorecheck is the closest. It reads printed scores as rounding intervals, as this package does, but takes the exact counts on one side and no second marginal, so where it runs it usually leaves many tables where both marginals leave one; on every table this package recovers, the two agree. We know of no other package that recovers a paired-agreement table from both marginals and reports every table the figures admit.

What it will not do

  • It does not tell you which criterion is correct. Neither does the table.
  • It does not separate disagreement between criteria from inconsistent application of either. That needs the same criterion applied twice to the same patients, which no aggregate report contains.
  • An empty candidate set says the published figures admit no common table under the definitions and rounding rules used here. It does not say an error was made — a denominator, an analysis set, or a version of a variable may simply have gone unstated.
  • Three or more categories are out of scope; the identity above is the binary case.

Validation

  1. Exhaustively, over the whole space. The enumerator must agree, as a set, with a brute-force sweep over every non-degenerate integer table at N = 18 and N = 22 — 1,258 and 2,212 of them, being every table for which κ is defined. Not a sample.
  2. Against a second implementation. Written from the definitions in exact rational arithmetic, sharing no code with the enumerator, over every table at N = 16, 20 and 24. Agreement between two implementations that share helpers proves only that the helpers are consistent. Those sweeps run at small N, where the granularity of κ and of the marginals is coarser than the interval arithmetic being checked, so the rounding boundaries are stated as concrete cases instead: a κ of exactly 0.625 sitting on the endpoint of "0.62", and a marginal printed "0.512" at N = 240 whose only valid count is lost to truncation.
  3. Against the established R implementations of κ itself. The enumeration is only as good as the statistic it inverts, so make crosscheck computes κ for 600 integer tables with this module and with every R implementation present on the machine, and reports the largest disagreement. Against irr::kappa2, psych::cohen.kappa, vcd::Kappa and DescTools::CohenKappa the worst disagreement is 6 × 10⁻¹⁵ — floating-point representation, not arithmetic. epiR::epi.kappa cannot be installed here (its dependency chain wants GDAL) and is checked against its source instead, which is exact: its (pO.p - pE.p)/(1 - pE.p) with pE.p <- sum(r.totals * c.totals)/n^2 is the same formula. Packages that are absent are named rather than silently skipped.

The study this was built for

The delirium table above, and eight other published comparisons, are analyzed in a manuscript on what a published agreement statistic conceals. That work lives in its own repository, with the corpus, the preregistration frozen before the enumeration was run, the results, and the sentence each published figure was read from. This package carries only the method.

Citation

See CITATION.cff.

License

MIT.

Release files for enum2x2 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for enum2x2 0.1.0
File Size Uploaded
enum2x2-0.1.0.tar.gz 254.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for enum2x2 0.1.0
File Interpreter ABI Platform
enum2x2-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 279.3 kB

Release files / enum2x2-0.1.0.tar.gz

Download URL enum2x2-0.1.0.tar.gz
Size 254.4 kB
Tags Source
SHA-256 checksum
How to use checksums
641929ef685ae45f7869ba1ddfc229af8e90d2aee7496fc9ac5f5931f50c1b19
BLAKE2b-256 checksum
How to use checksums
0199d4640d165414cb6a1d7b08287b2a7a8d408b2277c620ae25e95b718e411c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / enum2x2-0.1.0-py3-none-any.whl

Download URL enum2x2-0.1.0-py3-none-any.whl
Size 24.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ca598bef80e8f2d834a904218d681769e3234dbda7489e6c7bcbc74c281f8ed5
BLAKE2b-256 checksum
How to use checksums
0107055a98e50c59b29b993754a2892c72e8187b5581f468b5611b2ca3a6927b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page