Skip to main content

edgar-geo-revenue

Extract geographic revenue breakdowns from SEC EDGAR 10-K filings. Give it a ticker, get back the revenue-by-country split the company actually disclosed.

import edgar_geo_revenue as egr

egr.set_user_agent("Your Name your@email.com")   # SEC requires a real contact

egr.fetch_geo_revenue("AAPL")
# {'fiscal_year': 2025,
#  'total_revenue_usd': 416161000000,
#  'segments': [['US', 36.47], ['CN', 15.47]],
#  'region_segments': [['INTL', 48.06]],
#  'pct_coverage': 100.0}

No database, no API key, no account. Two dependencies: requests and beautifulsoup4.

Why this is harder than it looks

Fetching the filing is the easy part. The problem is that "revenue by geography" has no standard location, table shape, or label vocabulary anywhere in EDGAR:

  • The XBRL geographic tags are incomplete. Plenty of filers disclose a geographic split in the rendered financial-statement pages while tagging little or nothing usable, so an XBRL-only approach silently reports "no data" for companies that clearly disclosed one.
  • The table is in an unpredictable R-file. The rendered filing is split into R1.htm, R2.htm … R200.htm, and the geographic note can be anywhere in that range.
  • Label vocabulary is unbounded. "United States", "U.S.", "US and Canada", "Europe, the Middle East and Africa (EMEA)", "Non-US", "Greater China", "Rest of world" — with or without footnote markers, Oxford commas, ampersands, and parenthetical abbreviations.
  • Neighbouring tables look almost identical. Long-lived assets by geography, deposits by geography, and pension assets by geography all sit near the revenue note and share its row labels.

This library encodes the selection and resolution rules that survived a production audit of 463 SEC filers. Of those, 262 had a usable geographic disclosure; a large share of the remainder genuinely do not disclose one, which is a legitimate result rather than a parser failure.

What it returns

Field Meaning
fiscal_year Fiscal year the figures belong to, from the filing's own period metadata
total_revenue_usd Total revenue for that year, in USD
segments [[ISO2, percent], …] — resolved country-level rows
region_segments [[REGION, percent], …] — rows that are genuinely region-level
pct_coverage How much of total revenue the returned rows account for

None means no geographic disclosure was found. That is a normal outcome.

Design notes, including what deliberately does not happen

These are the expensive lessons. Each one is a heuristic that looks reasonable, was tried in production, produced wrong numbers, and was removed.

A region-level disclosure is never split into per-country estimates. If a filer reports "Europe: 30%", it is tempting to apportion that across European countries by GDP or by some fixed weighting. REGION_SPLITS exists in this codebase as a registry of which labels are regions, and its weights are never used to manufacture country rows. Doing so fabricates data that the filer never disclosed. Region rows are returned as region rows.

Combined place labels are keyed on the resolved set, not the label text. "Europe, the Middle East and Africa" resolves to {EUROPE, MEA} and is EMEA. "U.S. and Canada" resolves to {US, CA} and is North America. Keying on the resolved set covers every punctuation variant at once, which an exact-string match cannot — and before this existed, those disclosures were dropped rather than stored.

There is no safe heuristic for picking the "Total" row. Attempts to identify it positionally or by label produced wrong denominators. The total comes from the filing's own revenue figure instead.

The R-file scan must not stop at the first miss. An early implementation broke out of the loop on the first failed fetch, which truncated the search well before reaching the geographic note in longer filings.

A partition that does not add up is rejected, not rescaled. If resolved country percentages sum to 109%, the rows are overlapping subtotals rather than a partition, and returning them scaled to 100% would invent a split. The library refuses and logs why:

_build_result: rejecting — segment pct sum 109.2% (max single 29.3%)

Sanity-check the magnitude against something independent. Every wrong-table bug found in production (deposits mistaken for revenue, pension assets, an equity-method JV, PP&E) was off by 3x or more, never subtly wrong. Wide bounds catch all of them without false-positiving on ordinary growth.

Install

pip install requests beautifulsoup4
git clone https://github.com/Metricshour/edgar-geo-revenue.git
cd edgar-geo-revenue && pip install -e .

SEC rate limits

The SEC requires a User-Agent with a real contact address and rate-limits to roughly 10 requests per second. Call set_user_agent() before use; requests without a genuine contact get blocked. The library sleeps between filing fetches.

Contribute

Wrong geographic numbers are worse than missing ones. High-value reports:

  • False positives — wrong table (assets / deposits / pensions as revenue)
  • Missed notes — filer discloses geo revenue, tool returns None
  • Label gaps — place strings that should resolve and do not
  • Bad rejects / accepts — partition logic too strict or too loose

Open an issue with ticker + expected vs actual, or a PR with a regression case. Docs site: metricshour.github.io/edgar-geo-revenue.

Where this comes from

Extraction layer from MetricsHour. For the parsed dataset with screening rather than running the parser: metricshour.com/screener.

Licence

MIT — see LICENSE.

Metadata

Release files for edgar-geo-revenue 0.1.2

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

Source distribution (sdist)

Source distribution for edgar-geo-revenue 0.1.2
File Size Uploaded
edgar_geo_revenue-0.1.2.tar.gz 32.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for edgar-geo-revenue 0.1.2
File Interpreter ABI Platform
edgar_geo_revenue-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 62.8 kB

Release files / edgar_geo_revenue-0.1.2.tar.gz

Download URL edgar_geo_revenue-0.1.2.tar.gz
Size 32.3 kB
Tags Source
SHA-256 checksum
How to use checksums
fb22d1e83279faf79293aa3762a3cbc770670beb491b0689d174a251ca0291d7
BLAKE2b-256 checksum
How to use checksums
f79d09a98700bccd18678c110b08d049e320b24579797a217e0ca59d6d7a4333
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / edgar_geo_revenue-0.1.2-py3-none-any.whl

Download URL edgar_geo_revenue-0.1.2-py3-none-any.whl
Size 30.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
df1baf26a9f518b4ebc7f1af236b96d9f61dc640e014f50a2dd4ce24974b1c78
BLAKE2b-256 checksum
How to use checksums
6202c46f7c3302641ff576ec597ebb580f87bfdc9dc92f1cc4336c834195c04c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

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