Skip to main content

Python client for the e-Stat API (Japanese government statistics portal) with structured outputs for LLMs and data scientists.

Project description

pyestat

Structured, typed results from Japan's official statistics portal (e-Stat) — ready for LLMs and data scientists.

PyPI version Python versions License: MIT

WhyInstallUsageWriting rulesLicense

English日本語

Why another e-Stat library?

The e-Stat API returns JSON that is a thin re-encoding of the original XML: dimension codes hide under @-prefixed keys and the cell value under $, while the human-readable labels and units for those codes live in a separate CLASS_INF block you must join yourself. Logical errors arrive as HTTP 200 with a non-zero RESULT.STATUS. Existing Python wrappers stop at "give me a DataFrame" and pass these quirks through to the caller.

pyestat resolves them. With no per-table setup, each code comes back joined to its label and each value to its unit (the default, rule="auto"):

# e-Stat's raw VALUE cell: codes only — labels and units live in CLASS_INF
{"@cat01": "000", "@time": "2020000000", "$": "126146"}

# pyestat (rule="auto"): codes resolved to labels, value carries its unit
{"cat01": {"code": "000", "label": "男女計"},
 "time":  {"code": "2020000000", "label": "2020年",
           "normalized": "2020", "granularity": "yearly"},
 "value": {"value": "126146", "unit": "千人"}}

And where e-Stat splits one observation across rows — one row per measure — it folds them back into a single record, one column per measure:

# e-Stat: each measure on its own row, sharing the same time
{"@time": "2020000000", "@tab": "001", "$": "16"}                     # 数量
{"@time": "2020000000", "@tab": "002", "$": "35220", "@unit": "千円"}  # 金額

# pyestat (rule="auto"): one record, the measures pivoted into columns
{"time": {"code": "2020000000", "label": "2020年",
          "normalized": "2020", "granularity": "yearly"},
 "数量": {"value": "16",    "unit": None},
 "金額": {"value": "35220", "unit": "千円"}}

So an LLM agent or a researcher consumes a response without learning the e-Stat wire format, joining CLASS_INF by hand, reshaping rows, or special-casing the HTTP-200 error channel.

What you get

Find and fetch the table you need

  • Search the catalog by keyword or code — list_stats (searchWord, statsCode, …).
  • Look at a table's axes before you download it — get_meta_info.
  • Fetch only the slice you need — select narrows a huge table server-side by item / region / period, so the consumer price index (~13M rows) returns the few hundred you asked for, keyed by the same axis ids get_meta_info shows.
  • Pull a large table without loading it all into memory — iter_stats_data_pages yields one page at a time. Stop a runaway query before it downloads with max_rows (raises TooManyRowsError), and track a long pull with a progress callback.

Get back data you can use, not codes to decode

The default — rule="auto", no rule to write.

  • Read 男女計, not 000 — every code arrives with its label, the CLASS_INF lookup already done.
  • Sort and group on time directly — dates come back normalized with their granularity tagged (2020, yearly / monthly / …).
  • Treat one observation as one row — when e-Stat spreads a measure across several rows, you get them back together, each measure in its own column to read across (GDP, CPI, housing starts, trade). A table pyestat can't structure yet comes back as plain raw rows, never an error.
  • Keep only the figures you want to sum — aggregates="exclude" drops the subtotal and total rows, "only" keeps just the totals.

Hand off to your tools

  • Get one column per field for pandas and friends — to_flat().

Hit a table pyestat doesn't fold yet, or want column names of your own? That is a short rule away — see Writing your own rules.

Throughout, the numbers are never rewritten: they stay strings, and e-Stat's suppression markers (- / *** / X) are preserved as-is. e-Stat's habit of reporting errors as HTTP 200 surfaces as a typed EstatApiError, and dropped connections are retried for you.

Status

pyestat is pre-1.0. Two parts of the surface move at different speeds:

  • Settled — what you consume: the nested StatsDataResponse shape (with its to_flat() projection) and the EstatError hierarchy hold across 0.x.
  • Evolving — what you author: the RuleV2 rule schema may still change across 0.x as built-in coverage grows.

Install

uv add pyestat
# or
pip install pyestat

Usage

Register for an appId at https://www.e-stat.go.jp/api/, then pass it explicitly to EstatClient(app_id=...). A common convention is to keep it in an ESTAT_APP_ID environment variable and read it yourself:

import os

from pyestat import EstatClient, EstatApiError

client = EstatClient(app_id=os.environ["ESTAT_APP_ID"])

try:
    response = client.get_stats_data(stats_data_id="0003448237")
except EstatApiError as exc:
    # e-Stat reports logical errors with HTTP 200 + STATUS != 0.
    print(f"e-Stat refused the query: {exc.status} {exc.message}")
else:
    print(response.stats_data_id)   # "0003448237"
    for row in response.values:
        # The default rule="auto" returns self-describing *nested* cells:
        # each axis is {code, label}, time adds normalized/granularity,
        # the observation is {value, unit}.
        print(row)
        # -> {"cat01": {"code": "000", "label": "男女計"},
        #     "time":  {"code": "2020000000", "label": "2020年",
        #               "normalized": "2020", "granularity": "yearly"},
        #     "value": {"value": "126146", "unit": "千人"}}

Prefer one column per field (e.g. for pandas)? to_flat() projects the nested cells to the familiar suffix shape — losslessly, and as a no-op on a raw (rule=None) response:

flat = response.to_flat()
# -> [{"cat01": "000", "cat01_label": "男女計",
#      "time": "2020", "time_code": "2020000000",
#      "time_label": "2020年", "time_granularity": "yearly",
#      "value": "126146", "unit": "千人"}, ...]

import pandas as pd
df = pd.DataFrame(flat)

Pass rule=None instead to get e-Stat's raw rows unchanged (@-prefixed dimensions become plain keys, "$" becomes "value") — flat scalars, no labels or normalization.

Fetching one slice of a large table

Some tables are too big to pull whole — the consumer price index is millions of rows across every item, region, and period. select filters server-side, keyed by the axis ids get_meta_info reports, so you fetch only the slice you want:

# 総合 (all items) × 全国 (nationwide) × 指数 (the index), annual rows only
resp = client.get_stats_data(
    "0003427113",  # 2020-base CPI — ~13M rows unfiltered
    select={"cat01": "0001", "area": "00000", "tab": "1", "time": {"level": "1"}},
)
# a few hundred rows, structured exactly as above — not the whole catalog

A select value is a code, a list of codes, or a mapping setting any of code / level (a single level or a range) / from / to (an inclusive code range) — each key optional, as the example's time: {"level": "1"} shows. The codes are e-Stat's own — pyestat passes them through as-is, with no catalog lookup, so a wrong code isn't flagged client-side; e-Stat just returns no rows for it. Read the codes off get_meta_info:

for axis in client.get_meta_info("0003427113").class_objs:
    print(axis.id, axis.name, len(axis.classes))  # axis id, name, member count

The result is an ordinary response — to_flat() it into pandas and take your analysis from there.

Writing your own rules

pyestat ships built-in rules for a small set of tables and falls back to rule="auto" for the rest. When you want different structuring — or domain-specific column names — supply your own RuleV2:

from pyestat import EstatClient, RuleV2

custom = RuleV2.model_validate({
    "schema_version": "2",
    "match": {"role_pattern": ["value", "area", "time"]},
    "output": [
        {"column": "year",   "source": {"role": "time"}, "transform": "yearly"},
        {"column": "region", "source": {"role": "area"}},
        {"column": "value",  "source": {"role": "value"}},
    ],
})

client = EstatClient(user_rules=[custom])

A rule declares the output columns you want, each drawn from an axis role the classifier infers, so one rule covers every table sharing that role pattern. Pivoting rows split across a meta-axis, naming columns for to_flat(), and dropping rule files into a directory are covered in Writing rules →.

The RuleV2 schema is evolving across 0.x — see Status.

Error behavior

On the default rule="auto" path, whether a rule failure reaches you turns on who authored the failing rule — fall back when it is pyestat's, surface when it is yours:

  • A built-in rule that cannot apply degrades to lossless raw output instead of raising: its failure is internal and you cannot edit it, so preserved data beats a crash.
  • A rule you supplied — an explicit rule=RuleV2(...), a user_rules= entry, or a file in ./pyestat_rules — that cannot apply raises a typed error so you can fix it and re-run.

So get_stats_data(id) on a table pyestat does not yet handle returns usable raw rows rather than failing, while a mistake in your own rule is reported.

Every pyestat error inherits from EstatError, so a coarse except EstatError catches them all; catch a leaf (EstatApiError, TooManyRowsError, …) when you want to act on one case.

Configuring the appId

Usage shows the basic convention — pass app_id explicitly, kept in an ESTAT_APP_ID variable. How that variable reaches the environment is your project's call; a few common patterns:

Shell export (interactive use):

export ESTAT_APP_ID="<your-app-id>"
python your_script.py

.env file + python-dotenv (local development, Jupyter):

echo 'ESTAT_APP_ID=<your-app-id>' > .env
# in your code (or notebook cell):
import os

from dotenv import load_dotenv
from pyestat import EstatClient

load_dotenv()
client = EstatClient(app_id=os.environ["ESTAT_APP_ID"])

Docker / Compose: pass -e ESTAT_APP_ID=... or set it under environment: in your compose file.

CI (GitHub Actions, etc.): store the appId as an encrypted secret and inject it as an env var in the workflow step.

Production: pull it from your secret manager (AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault / ...) at startup and pass it to EstatClient(app_id=...).

pyestat deliberately avoids reading the environment or bundling a dotenv loader, so it does not constrain how you manage secrets.

Development

uv sync                              # install runtime + dev deps
cp .env.example .env                 # then fill in your ESTAT_APP_ID
uv run pytest                        # runs unit + live API tests
uv run pytest -m "not integration"   # unit only (no network)

The live integration test under tests/test_get_stats_data_integration.py auto-skips if ESTAT_APP_ID is not set, so the unit suite stays hermetic without extra flags.

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyestat-0.2.0.tar.gz (60.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyestat-0.2.0-py3-none-any.whl (73.1 kB view details)

Uploaded Python 3

File details

Details for the file pyestat-0.2.0.tar.gz.

File metadata

  • Download URL: pyestat-0.2.0.tar.gz
  • Upload date:
  • Size: 60.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pyestat-0.2.0.tar.gz
Algorithm Hash digest
SHA256 04e0f903390f1094d40728697f8737fac987055b0892cd9a3f267a708f1d54c5
MD5 714f4d80819b4f02378ab550cb12b58c
BLAKE2b-256 1ebc043bc6fee39cc29e3a228e1b54d7dc9843d075a5e88178e1aaa9621c898e

See more details on using hashes here.

File details

Details for the file pyestat-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pyestat-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 73.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pyestat-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9a5c36b321ee9805bf3474f1901c7410c8dfe18b46d8fa3ad935290ec78cb014
MD5 1ec3e4a40fd119f82dc6e44dc6a5da9e
BLAKE2b-256 73565769540d4a2e55146bfa908220df97c77865f45be5725ebcd7c281ff2660

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page