Metadata-Version: 2.4
Name: edgar-geo-revenue
Version: 0.1.0
Summary: Extract geographic revenue breakdowns from SEC EDGAR 10-K filings
Author-email: MetricsHour <info@metricshour.com>
License: MIT
Project-URL: Homepage, https://metricshour.com
Project-URL: Repository, https://github.com/metricshour-netizen/edgar-geo-revenue
Project-URL: Documentation, https://metricshour-netizen.github.io/edgar-geo-revenue/
Keywords: sec,edgar,10-K,geographic revenue,xbrl,fundamentals,filings
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Requires-Dist: beautifulsoup4>=4.11
Dynamic: license-file

# 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.

```python
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

```bash
pip install requests beautifulsoup4
git clone https://github.com/metricshour-netizen/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.

## Where this comes from

This is the extraction layer from [MetricsHour](https://metricshour.com), which publishes
geographic revenue exposure for listed companies alongside macro and trade data. If you want
the parsed dataset with country pages and screening rather than the parser, that is what the
site does: [metricshour.com/screener](https://metricshour.com/screener).

## Licence

MIT — see [LICENSE](LICENSE).
