Skip to content
Heterodata An Arcanum Research project Shaikh Data
Shaikh Data
The comparative layer GitHub ↗

How the pipeline works

This dataset is built by the Anu framework — a staged, documented data-construction pipeline. Each script carries a phase prefix (L / P / V / M / A / O) that says what it does, so the code reads as an ordered method, not a pile of scripts.

View all the code on GitHub ↗

Pipeline phases

  1. L##

    Loading

    Fetch and read the raw source data — API pulls, archived tables, digitised book figures — and validate that the fetched units match what the named source promises.

  2. P##

    Processing

    Construct each series: rebase, splice vintages, deflate, and combine inputs. Composite and formula series are assembled here, with a dimensional-analysis check whenever units differ. Processing — and processing only.

  3. V##

    Validation

    Check every constructed series against the original published figures and tables — mean absolute error, endpoint sanity, unit and scale audits. A series only passes when it reproduces the source values. The exact figure, table, or page being reproduced is cited in that series' provenance record (Explore → the construction drawer).

  4. M##

    Manual adjustment

    Apply documented hand corrections where the source itself requires them — each adjustment recorded with its rationale, never a silent edit.

  5. A##

    Analysis

    Derive the downstream analytical quantities the book reports (ratios, trends, decompositions) from the validated series.

  6. O##

    Output

    Write the publishable artifacts: the tidy per-series CSVs, the Parquet files, the data dictionary, and the per-series provenance records that ship with the data.

What the series IDs mean

Series IDs are a separate scheme from the pipeline phases above — a letter in a series ID says nothing about how it was built, only what kind of series it is.

S###

Primary series 136

Primary series — the figures and tables from the book or study being replicated. The digits encode the original work's chapter and sequence (e.g. S201 = Chapter 2, series 01); the exact source figure/table/page for each series is cited in its provenance record.

XS

Extra series 0

Extra series — book-appendix series and series drawn from other studies. These are listed after all primary series, split into a book-appendix section and an other-studies section.

Reproduce a figure

Every chart on this site is built by one small, documented transform that turns a published series file into the table the chart plots — then an optional client-side reindex. Below is that exact transform, in both R and Python; pick a language, copy or download it, and you reproduce the figure from the same CSV the Download page serves.

Pivot a The comparative layer series (long year,value,subseries_id) → chart table
# Pivot a long series CSV (columns: year, value, optional subseries_id) to the
# wide year-by-subseries table the site charts. Faithful port of the app transform.
pivot_series <- function(path) {
  df <- read.csv(path, colClasses = "character", check.names = FALSE)
  if (!all(c("year", "value") %in% names(df))) stop("need year + value columns")
  df$subseries_id <- if ("subseries_id" %in% names(df)) trimws(df$subseries_id) else ""
  df$subseries_id[df$subseries_id == ""] <- "value"
  df$year  <- trimws(df$year)
  df$value <- trimws(df$value)
  df <- df[nzchar(df$year), , drop = FALSE]
  df <- df[!(df$value == "" | tolower(df$value) == "nan"), , drop = FALSE]

  subs  <- unique(df$subseries_id)                 # subseries in first-seen order
  years <- unique(df$year)                          # years in first-seen order
  wide  <- data.frame(year = years, stringsAsFactors = FALSE)
  for (s in subs) {
    m <- df[df$subseries_id == s, c("year", "value")]
    wide[[s]] <- as.numeric(m$value[match(wide$year, m$year)])
  }
  keep <- rowSums(!is.na(wide[, subs, drop = FALSE])) > 0  # drop all-blank years
  wide[keep, , drop = FALSE]
}

tbl <- pivot_series("S201.csv")   # one trace per subseries column
# Pivot a long series CSV (columns: year, value, optional subseries_id) to the
# wide year-by-subseries table the site charts. Faithful port of the app transform.
import csv

def pivot_series(path):
    with open(path, encoding="utf-8", newline="") as f:
        reader = csv.DictReader(f)
        fields = reader.fieldnames or []
        if "year" not in fields or "value" not in fields:
            raise ValueError("need year + value columns")
        has_sub = "subseries_id" in fields
        table, sub_order, year_order = {}, [], []
        for rec in reader:
            year = (rec.get("year") or "").strip()
            val  = (rec.get("value") or "").strip()
            if not year:
                continue
            sub = (rec.get("subseries_id") or "").strip() if has_sub else ""
            sub = sub or "value"
            if sub not in sub_order:
                sub_order.append(sub)
            if year not in table:
                table[year] = {}; year_order.append(year)
            if val and val.lower() != "nan":
                table[year][sub] = val
    header = ["year"] + sub_order
    rows = []
    for year in year_order:                            # first-seen year order
        row = [year] + [table[year].get(s, "") for s in sub_order]
        if any(c != "" for c in row[1:]):              # drop all-blank years
            rows.append(row)
    return header, rows

header, rows = pivot_series("S201.csv")  # one trace per subseries column
Reindex a column to 100 at a base year (the chart's "index" view)
# Reindex a numeric column so the base year = 100 (the chart's index toggle).
reindex_to_100 <- function(years, values, base_year) {
  base <- values[match(base_year, years)]
  if (is.na(base) || base == 0) stop("base year has no value")
  100 * values / base
}
# Reindex a numeric column so the base year = 100 (the chart's index toggle).
def reindex_to_100(years, values, base_year):
    base = values[years.index(base_year)]
    if base in (None, 0):
        raise ValueError("base year has no value")
    return [100 * v / base if v is not None else None for v in values]

The app plumbing around these transforms (the FastAPI routes, the cache build, the Plotly rendering) is Python-only and not ported to R — porting the web server would invent code that doesn't exist. The data transforms above are the part you reproduce a figure with; the full pipeline is on GitHub.