# meta-analysis.cz data API — v1

Static JSON and tabular files at stable URLs. There is no server: every endpoint
is a file, served over HTTPS with CORS enabled, so you can fetch it from a
browser, a script, or a notebook without a key, a quota, or a login.

Base: `https://meta-analysis.cz`

## Endpoints

| URL | What it is |
|---|---|
| `/api/v1/datasets.json` | Index of every dataset: paper, DOI, file URLs, row counts, which columns hold the effect and its standard error |
| `/api/v1/codebooks/{id}.json` | Every column of one dataset: type, missingness, distinct values, summary statistics, inferred role |
| `/api/v1/datapackage.json` | The same collection as a [Frictionless](https://frictionlessdata.io) tabular data package |
| `/api/v1/croissant.json` | [MLCommons Croissant](http://mlcommons.org/croissant/) record, for ML dataset tooling |
| `/data/v1/{id}/{id}.parquet` | One dataset, all original columns, original column names |
| `/data/v1/{id}/{id}.csv` | The same, as CSV (published for datasets under 4 MB) |
| `/data/v1/estimates_harmonised.parquet` | All literatures pooled into one estimate-level table |
| `/data/v1/estimates_harmonised.csv` | The same, as CSV |
| `/estimates.csv` | One headline result per paper — the table behind `/results/`, with its caveat and citation |

## Quick start

```python
import pandas as pd

# every estimate from every literature, in one table
df = pd.read_parquet("https://meta-analysis.cz/data/v1/estimates_harmonised.parquet")
df.groupby("dataset")["t_stat"].median()

# or one literature at a time, with all of its hand-coded moderators
cls = pd.read_parquet("https://meta-analysis.cz/data/v1/class/class.parquet")
```

```r
# R
inst <- arrow::read_parquet("https://meta-analysis.cz/data/v1/estimates_harmonised.parquet")
# or, without arrow:
inst <- read.csv("https://meta-analysis.cz/data/v1/estimates_harmonised.csv")
```

**Read the Parquet where you can, and pass `float_precision="round_trip"` where you
cannot.** The CSV is not the problem: it carries every value at full precision and
round-trips exactly. Pandas' default CSV parser is the problem, and it is not exact. It
moves 16,794 of the 52,800 `se` values, 9,008 `effect` values and 9,960 `t_stat` values,
each by up to about 2.9e-11. So this applies to any column you read, not only the derived
ones, and recomputing `effect / se` yourself does not avoid it.

That is invisible almost everywhere and decisive at a threshold. 96 estimates sit within
1e-9 of |t| = 1.96 and 26 sit exactly on it, so any count either side of that line has to
state its convention: bins here are closed on the left, and an estimate reported as
exactly 1.96 is counted above. On that convention the caliper comes out as 508 below and
633 above
from the Parquet, 503 and 639 from the same CSV at pandas' defaults, and 506 and 634 if
you recompute the ratio from a default-parsed CSV. The figures published on this site
quote the Parquet. Writing the CSV with more digits does not help; it makes the default
parse worse. The Parquet is canonical. Read the CSV as:

```python
pd.read_csv("https://meta-analysis.cz/data/v1/estimates_harmonised.csv",
            float_precision="round_trip")
```

```bash
curl -s https://meta-analysis.cz/api/v1/datasets.json | jq '.datasets[] | {id, n_estimates}'
```

## The harmonised table

One row per harmonised **observation**, pooled across literatures. The primary
analysis set is **49,664 estimates satisfying the source papers' own sample
definitions**; a further 3,136 estimates those papers excluded are carried
alongside for robustness work, giving **52,800 rows from 42 literatures** in all.
Cite the paper-sample figure. The change from 1.3.0's 52,800 rows is not growth in
the evidence base, and 52,800 is not "all estimates": it is a selection from the
67,606 source rows. Filter `in_paper_sample` to true to reproduce a paper's
own sample, and read `paper_sample_exclusion` to see which clause removed a row.

Rows are not always independent estimates — `price_puzzle`
carries one row per impulse response per horizon, and `house_prices` ships about
seven horizons per impulse response. Check `horizon` before treating rows as
independent. Version **2.0.0**.

2.0.0 is a breaking release. `class` published the partial correlation of an
appendix robustness block as its effect and now publishes the test-score scale its
main models use, with the partial correlation carried in `effect_alt`; anything
computed against 1.3.0 or earlier used the robustness metric. `activism` published
twenty estimates with the wrong sign and reached 1,254 of the 1,851 rows available.
`armington` featured a pair that cannot reproduce its own paper. The `pcc` and
`se_pcc` columns are retired, having duplicated `effect` in seven of the eight
datasets that filled them.

1.1.0 added `finance_growth`, taking the table to 41 literatures, and 1.1.1
removed 20 `price_puzzle` rows that corresponded to no source estimate, leaving
49,669. 1.2.0 does two things. It corrects `n_obs` on 4,614 rows, where
`armington` and `migrant` were publishing the number of ESTIMATES a study
reports as its sample size: `armington` now carries the paper's own data-size
variable and `migrant`, which ships no sample size at all, carries none. And it
adds `cbequity` — 176 partial correlations from 9 studies on whether central
bank financial strength matters for inflation — taking the table to 42
literatures and 49,845 rows. 1.2.1 corrects three datasets that were being read or
labelled wrongly: `frisch` now publishes the extensive-margin file its paper reports
(762 rows from 38 studies) instead of a 723-row draft that matches neither published
sample, `activism` no longer ships a second header row as an estimate (1,974 to 1,973),
and `lags` names `mon_bot` as its outcome rather than the VAR lag order. It also
publishes the publication year `finance_growth` and `inflation` always held, relabels
`spillovers` and `bma` as semi-elasticities, and corrects `skill`’s direction note.
The table goes to 49,866 rows. 1.3.0 adds the intensive margin to `frisch`: the paper
reports over 700 estimates on each of the two margins of labour supply, and only the
extensive one was published as a dataset. Both are now included, distinguished by a
`margin` column, taking that dataset from 762 rows to 1,471 over 57 studies (21 papers
report on both margins) and its contribution to this table from 553 rows to 1,128. The two
margins are different outcomes and must not be averaged: in this table they are told
apart by `source_file`, which names the member each row came from, because `margin`
lives in the per-dataset file and does not survive harmonisation. The table goes to 50,441
rows.

Core columns are present for every row: `dataset`, `study_id`, `estimate_id`,
`effect`, `se`, `t_stat`, `precision`. The rest are harmonised moderators, and
they are populated only where the source dataset recorded them — coverage per
column runs from about 90% (`n_obs`) down to under 20% for the more specialised
ones. Check for nulls rather than assuming.

`precision` is exactly `1 / se`. It is provided because most meta-analysis
software expects it, not as independent information — it carries nothing `se`
does not, and weighting by both would double-count.

Provenance travels with every row. `source_file`, `effect_col`, and `se_col` name
the file and the exact columns each value came from, so any number can be traced
back to the published dataset and checked. `se_is_derived` marks rows whose
standard error was reconstructed rather than read directly (from a reported
t-statistic, or as the mean of an asymmetric confidence interval).

**Raw effect levels are not comparable across literatures.** An elasticity, a
partial correlation and a dollar value per tonne of carbon all live in the
`effect` column; `effect_units` tells you which is which. Within a literature the
units are *usually* consistent, which is what estimator comparisons need. Do not
assume it. In several literatures the source papers themselves mix scales: in
`size`, `trust`, `habits` and `eis` the per-study median absolute effect spans
four to seven orders of magnitude, which no real heterogeneity produces and which
a percent-versus-decimal split does. `activism` carries a direction note recording
exactly that, reconstructed; the others do not, and are not yet resolved.

The practical consequence is sharp, because it survives the remedy this page
recommends. On `habits` a precision-weighted mean after 1st–99th winsorising
returns about 0.001 for a literature whose median estimate is 0.47. **Check the
per-study spread of `effect` before pooling a literature**, and treat a span of
more than about two orders of magnitude as a scale problem rather than as
heterogeneity. Comparing across literatures needs an explicitly standardised
measure — and ratios are unsuitable where the denominator may sit near zero or
change sign.

The harmonisation may still be revised. For a reference that does not move, cite
the archived deposit:

> **https://doi.org/10.5281/zenodo.21773678** — cite this. It always resolves to the newest version.
>
> `https://doi.org/10.5281/zenodo.22647394` is version 2.0.0, the table served here and the newest
> deposit. Cite this one in a replication package, where the exact files matter.
>
> `https://doi.org/10.5281/zenodo.22529684` is version 1.3.0, superseded. Anything computed against
> it for `class` used that literature's robustness scale rather than the one its main models use,
> and twenty `activism` estimates carried the wrong sign.
>
> `https://doi.org/10.5281/zenodo.22212666` is version 1.2.0, superseded. It differs from what
> is served here: 1.3.0 adds the intensive margin to `frisch`.
>
> `https://doi.org/10.5281/zenodo.21789702` is version 1.0.0, superseded by 1.1.1, which
> removes 20 `price_puzzle` rows that corresponded to no source estimate.
>
> `https://doi.org/10.5281/zenodo.21773679` is version 0.9.0-beta, superseded; do not start
> new work from it.

The deposit is immutable: it holds that version's harmonised table, index,
codebooks, and documentation, with checksums. The live files here may change; the
DOI will not.

## Before you pool

**Cluster on `(dataset, study_id)`, never on `study_id` alone.** `study_id` is unique within
a literature, not across the table. There are 524 distinct values but 2,963 real
literature-study pairs, so clustering on the bare column silently merges unrelated studies
and collapses 82% of your clusters, from 2,963 to 524. And if you deduplicate, do it
on `(dataset, estimate_id)`, which is unique by construction: rows elsewhere can legitimately
agree in every other column, so a blanket `drop_duplicates()` deletes real estimates. On a pooled regression the standard
errors happen to come out within 1% of the correct ones, so nothing looks wrong; the damage is
to the cluster count that cluster-robust inference depends on, and to any literature-level
statistic where the merged studies are genuinely unrelated.

**Three literatures have fewer `study_id` values than they have studies, so those 2,963 pairs
are 20 short of the truth.** Where a source file carries no column whose NAME matches
`study_id`/`idstudy`, the harmoniser factorises the study label instead, and a label is the
author string: two papers by the same authors become one study. It affects `euro` (52 values
for 61 studies — the paper says "there are 61 studies", one estimate each), `resource_curse`
(36 for the 43 its page reports, so Sachs and Warner 1995, 1997 and 2001 are one study) and
`activism` (38 where the source numbers 42 articles in the analysis sample). Every other
literature takes its `study_id` from a real identifier column and is unaffected. If your
inference depends on the cluster count in one of those three, take the study identifier from
the per-dataset file — `id` and `study` in euro, `ID` in resource_curse, `ArticleNo` in
activism — rather than from the pooled column. The harmoniser will prefer those columns at the
next data revision; the pooled values are not changed underneath a published version.

These are real published estimates, and several literatures are heavy-tailed.
The `eis` file, for instance, runs from −10,000 to 100,000 with standard errors
to match; 68 rows across the collection have a standard error below 1e-4. Those
values are in the source files, not an artefact of the harmonisation, and they
are kept so the table stays faithful to what was published.

The practical consequence: a raw mean is meaningless on some literatures, and an
inverse-variance weight of 1/se² lets a handful of near-zero standard errors
dominate everything else. Winsorise, or work with medians, before pooling —
which is what the underlying papers do. As a worked check, FAT-PET run on the
1st–99th percentile winsorised data reproduces the published conclusions:
`education` corrects to about 0.02 and `excess_sensitivity` to about 0.01, both
of which their papers describe as near zero, and `forward` corrects to 0.92
against a null of 1. On the same winsorised data, with standard errors clustered
by study, the FAT publication-bias intercept lies beyond ±1.96 in 26 of the 42
literatures.

**All 42 pooled literatures are verified** — 22 `domain_reviewed`, 20 `code_traced`. Where a
paper ships no replication code, the mapping was checked against its published results by
hand: `gasoline_price`'s abstract reports corrected elasticities of -0.31 long-run and -0.09
short-run with published averages "exaggerated twofold", and the shipped data gives -0.691 and
-0.227, reproducing that; `finance_growth`'s shipped column reproduces its paper's Table 1
exactly — 67 studies, 1,334 estimates, median 0.14, mean 0.15, random-effects 0.14 with a
matching confidence interval.

Per-column minimum, maximum, median, and quartiles for every dataset are in its
codebook, so you can see the tails before you load anything.

One more thing worth knowing if you use `n_obs` as an instrument, as MAIVE does:
two source files store the sample size as its logarithm rather than as a count.
`n_obs` here is always a count — a log column is either replaced by the raw one
from the same file or exponentiated, and `datasets.json` records where that
happened. Check `effect_units` and the direction notes too: a few literatures
store proportions where the paper reports percentages, and `migrant` stores a
**negative inverse** elasticity — take -1/effect to recover the elasticity, and
convert its standard error by the delta method rather than using the stored one.
`skill` is suspected of a similar inversion but it is **not confirmed**, so its
units are left as `elasticity`; that open question is recorded in `units.json`.

## Known defects in this release

**75 `class` estimates are invalid as partial correlations**, and since 2.0.0 they live in
`effect_alt`, not `effect`. 2 lie outside `[-1, 1]`, to |1.372|, and 73 sit at exactly ±1 beside
a positive standard error. Do NOT filter this literature on `abs(effect) < 1`: `effect` is now the
change in test scores in hundredths of a standard deviation, which legitimately exceeds 1 on about
a thousand estimates, and that filter would silently delete valid data. The bound applies to
`effect_alt`. They are in the source
file, 72 of them in one study, and publication-bias tests on this literature are sensitive to
them: an unweighted regression of `effect` on `se` moves sharply when they are excluded, while
the precision-weighted form does not. A further 82 rows of the per-dataset `class` file store a
partial correlation of exactly zero beside a non-zero t-statistic; those do not enter the
pooled table. Everything is kept as published; correction is scheduled for the next data
revision. Until then, filter on `abs(effect) < 1` where you need strictly valid correlations.


## What is not in the harmonised table, and why

Every dataset on the site is published. Some cannot join a pooled
effect/standard-error table, and the reason is recorded for each in
`datasets.json`.

**No per-estimate precision exists:**

- **`fdi`** — no standard error, t-statistic, or weight anywhere in the file.
  Model averaging over its moderators still works; that is what the paper does.
- **`lags`** — its outcome is a transmission lag in months, which has no
  sampling standard error.
- **`ews`**, **`pcc`** — a country-level crisis database and a literature search
  listing. Neither is a set of extracted estimates.

**Two papers written on one dataset.** Keeping both would count the same
estimates twice and present one literature as two independent ones:

- **`hedge`** — the same 1,019 estimates as `alphas`, identical row by row.
  `alphas` is pooled; cite the published *Journal of Economic Surveys* 2024
  version for these estimates.
- **`substitution`** — the same 2,735 estimates as `eis`. Two papers, one
  dataset. It carries additional country-level moderators and is published in
  full.

### What a "literature" means here, and what is only name-harmonised

**Dataset IDs are not literature families.** The catalogue counts *contributing dataset
IDs*. Two of them describe the same literature: `trust` is a later, separate collection of
the size-premium literature that `size` also covers, which is why only the 212 estimates
`size` does not already carry are pooled. So "42 literatures" means 42 contributing dataset
IDs, not 42 independent bodies of evidence: `trust` and `size` cover the same size-premium
literature, so the 42 IDs represent at most 41 literature families. Treat those two as one
family in any analysis that assumes independence.

**Several pooled columns are literature-local.** `study_id` and `estimate_id` are unique
within a dataset, not across the table; a study appearing in two literatures carries two
different ids, and nothing here links them. Several moderators share a column name across
literatures while their source definitions differ — the name was harmonised, the definition
was not. Where the definition matters, read the per-dataset codebook rather than assuming
the column means the same thing everywhere. `effect`, `se`, `t_stat` and `precision` are
the columns harmonised by definition and arithmetic, not merely by name.

### A caveat on `citations`

`citations` is **not on one scale across literatures.** It is passed through from each
paper's own coding, and the papers did not agree:

- most literatures carry a raw count (`size` reaches 15,628);
- eight carry a log, with maxima between 4 and 10 (`students` falls in that band too,
  but it is the standardised regressor below, not a log);
- `students` carries a standardised regressor, which is why 26 of its values are negative.

Nothing in the harmonisation reconciles these, and the column name does not distinguish
them, so **do not pool, rank, or regress on `citations` across literatures** without first
checking the scale within each. The next data revision nulls the
non-count literatures rather than silently rescaling them. `impact_factor` shows a
weaker version of the same signature and is under review.

- **`trust`** — from 1.0.0 this is **pooled**, but only for the 212 estimates
  `size` does not already carry. It is the later collection (2026 against 2019)
  of the same literature, the size premium, and the *smaller* one at 1,613 rows
  against `size`'s 1,746; of those 1,613 rows, 1,401 (86.9%) already appear in
  `size` and are dropped here so nothing is counted twice, leaving the 212 above. That makes this literature a
  deliberate splice of two separately-assembled collections. **If the size
  premium is your subject, use either per-dataset file whole** rather than the
  pooled rows.

`price_puzzle` and `lags` share a source *file* but use different columns of it,
so both are kept. So are `bma` and `spillovers`, which take the horizontal and
vertical halves of one FDI database.

These overlaps were found by comparing the (effect, standard error) value sets
of every pair of literatures. That check now runs on every build: a complete
overlap fails outright, and a partial one fails too unless it has been ruled on
explicitly.

## Column resolution

Which column holds the effect, and which its standard error, was resolved
arithmetically rather than by column name: the correct pair is the one where
`effect / se` reproduces the t-statistic the dataset already reports. That test
settles most datasets outright. Where it could not — no t-statistic column, an
asymmetric confidence interval, a wide layout, an outcome that is itself a
t-statistic — the mapping was taken from the paper's own published replication
code or other documented evidence, and `verified_by` in the harmonisation report
records which file and which line of reasoning settled it.

## Two products, not one

**The archive is a faithful mirror. The harmonised table is an interpretation.**
They are separate things and should be trusted differently. All 42 literatures'
mappings are verified, but the table still involves judgement the archive does
not.

*Archive* — the original files, faithful CSV and Parquet mirrors, codebooks, and
paper/DOI metadata. Faithful conversions of what was published.

*Harmonised table* — 52,800 selected estimates, automatically mapped and in some
cases transformed. Every column mapping is verified against the paper's own
replication code or published results.

Every dataset carries an `audit_status` so you can filter on review quality
rather than read prose:

| status | meaning | count |
|---|---|---|
| `domain_reviewed` | checked by hand against the paper's own replication code, or against its published results where no code exists | 22 of 42 pooled |
| `code_traced` | mapping confirmed by reading the paper's code and comparing the variables it regresses | 20 of 42 pooled |
| `duplicate_excluded` | same estimates as another literature | 2 |
| `excluded_no_precision` | no per-estimate standard error exists | 2 |

The arithmetic test proves that two columns form a statistical pair. It cannot
distinguish a headline estimand from a robustness one, a short-run from a
long-run effect, or a baseline sample from a filtered one; `audit_status` records
which further check settled each literature.

## `/estimates.csv`, and why the headline is prose

One row per meta-analysis, fifteen columns. `project` joins to `datasets.json.id`, and
`source_quote` is meant to carry the verbatim sentence from the paper that the figure
came from, so every number can be traced to the words that state it. On ten of the
fifty-five rows it does not: the quote is verbatim from that paper's catalogue
abstract in `tools/papers.json` rather than from the paper. Each is named, with the
reason, in `tools/evidence_exceptions.json`, and `tools/check_evidence.py` fails on any
new one, so the exceptions are a recorded debt rather than a silent tolerance.

**`headline` is prose on purpose, and only one of the fifty-five rows is a bare
number.** The others are ranges, qualitative statements, or clauses that carry
their own conditions -- a percentage change per one-percentage-point rise,
peaking after two years. Reducing those to a single numeric column would drop the
conditions and read as authoritative, and the ranges are not even the same kind
of object: one is a 95% credible interval, another a range across
specifications, another a range of corrected means. A file named
`estimates.csv`, on a site about publication bias, is exactly where an invented
midpoint would be quoted back as the result.

Three columns are derived mechanically and are safe to parse:

| column | what it is |
|---|---|
| `question` | the question the paper answers, the same string `/results/` marks up as a schema.org `Question` |
| `basis_n_estimates` | the estimate count stated in `basis`, or empty |
| `basis_n_studies` | the study count stated in `basis`, or empty |

They are named for `basis` because that is what they read. **They are not the
dataset's row counts** and will not always match `datasets.json`: `basis` states
what the paper's headline rests on, `n_estimates` counts rows in the published
file, and the two differ wherever the paper's headline uses a subset. Both are
empty wherever `basis` does not state the number unambiguously -- an approximate
count, or a unit that is not a study, such as a meta-analysis or an experiment.

## Licence

**Everything here is CC BY 4.0.** The datasets, their CSV and Parquet conversions,
the harmonised table, the index, the codebooks, this documentation, and the papers
themselves.

You may use, adapt, and redistribute any of it, for any purpose, including
commercially and **including as training data for machine-learning models**. You
do not need to ask. Every dataset in `datasets.json` carries
`rights_status: cc-by-4.0` and a `license_url`, so nothing here requires a
judgement call.

The only condition is credit. **If you use an individual dataset, cite its paper**
— `datasets.json` carries the title, authors and DOI of each one, so you do not
have to look it up. If you use the collection, cite that too. See `/LICENSE`.

Cite the collection as:

> Havranek, T. and Z. Irsova (2026). meta-analysis.cz: harmonised
> estimate-level data from meta-analyses in economics. Zenodo.
> https://doi.org/10.5281/zenodo.21773678
