CoolFace
Datasetpublic

ZipLime/company-fundamentals

US Company Fundamentals — as reported, point in time Every number every US public company filed in XBRL since 2009, kept as it was filed, with the timestamp it became public. 16 206 companies · 433 717 filings · 97.9M facts · 2009-04-15 to 2026-06-30 The pipeline that produces this dataset lives in recipe/ inside this repository, at the same revision as the data. Nothing was assembled by hand — see PIPELINE.md for the method and for what it refuses to do. The one… See the full description on the dataset page: https://huggingface.co/datasets/ZipLime/company-fundamentals.

sourceHugging Faceapache-2.0updated 1d agoView on Hugging Face
0likes798downloads
Dataset Card

US Company Fundamentals — as reported, point in time

Every number every US public company filed in XBRL since 2009, kept as it was filed, with the timestamp it became public.

16 206 companies · 433 717 filings · 97.9M facts · 2009-04-15 to 2026-06-30

The pipeline that produces this dataset lives in `recipe/` inside this repository, at the same revision as the data. Nothing was assembled by hand — see PIPELINE.md for the method and for what it refuses to do.

The one thing that makes this different

Every other free fundamentals source tells you what a company's 2015 revenue is believed to be today. This one tells you what it was reported to be in 2016, and separately what it was restated to in 2017, and when each of those became public.

That is not a fine distinction. Measured across this corpus:

Company-concept-periods reported more than once10 351 310
changed value at all11.32%
changed by more than 1%8.20%
changed by more than 5%6.33%
changed by more than 25%4.20%

13 387 of 15 560 companies — 86% — have restated at least one figure by more than 1%. A backtest built on today's restated numbers trades on information that, for most companies, did not exist on the date it acts.

The original observation is never overwritten here. A restatement arrives as a second row with a later knowledge_date and a higher revision, and a read as of any past date returns what was actually knowable then. pit is partitioned by knowledge_year, so a point-in-time window prunes partitions instead of scanning years wider than it asked for.

python
import polars as pl
from datetime import datetime, UTC

pit = pl.read_parquet("data/pit/knowledge_year=*/*.parquet")
visible = pit.filter(pl.col("knowledge_date") <= datetime(2018, 6, 30, tzinfo=UTC))

# Coalesce column-wise, NOT by keeping the latest row. See the warning below.
carried = ["pit_event_id", "operation", "entity_id", "event_date",
           "knowledge_date", "knowledge_estimated", "revision", "ingested_at"]
values = [c for c in visible.columns if c not in {*carried, "logical_report_id"}]
as_of = (visible.sort(["logical_report_id", "revision", "knowledge_date"])
                .group_by("logical_report_id", maintain_order=True)
                .agg(*[pl.col(c).last() for c in carried],
                     *[pl.col(c).drop_nulls().last() for c in values]))

recipe/derive.py ships this as as_of(pit, at) if you would rather import it.

Do not take the latest row. unique(subset=["logical_report_id"], keep="last") is the obvious way to read this table and it is wrong. A filing that restates a period does not restate all of it: a 10-K carries three years of income statement and only two balance sheets, so the newest revision of a period three years back has revenue and no assets. Measured on 2022 periods read as of 2026, keeping the latest row loses 78% of `total_assets`, 21% of diluted EPS, 20% of revenue and 17% of operating cash flow. Coalescing per column keeps them, and the row it returns may be assembled from several filings — revisions_visible says how many.

The two dates

event_date       the day the fiscal period ended — what the number is about
knowledge_date   the second EDGAR accepted the filing — when you could read it

knowledge_date comes from the archive's accepted field, which states US Eastern local time rounded to the nearest minute. It is converted to UTC here; accepted_precision records that the true instant is within thirty seconds either side. Never key a backtest on event_date: a fiscal year ends in December and the 10-K arrives in February.

One filing states the same period at several lengths

This is the first thing an unfiltered read gets wrong. A 10-Q reports the quarter and the year to date, plus the balance sheet at that instant, and a 10-K adds the full year — so one submission produces rows with period_kind of quarter, half_year, nine_months, year, instant and sometimes multi_year, all for overlapping spans. Three kinds per filing is the median; 24 923 filings produce four and 3 667 produce five.

Always filter on `period_kind`. year for annual figures, quarter for discrete quarters. Nothing sums across kinds: half_year already contains quarter.

`period_kind``quarters`RowsRevenue present
quarter11 045 02071%
instant0780 830— balance sheet only
year4345 43874%
half_year2216 86981%
nine_months3210 99982%
multi_year5–40020 36143%
implausible_duration> 40067 factsa typo: 3 603 quarters is nine hundred years

Balance-sheet columns are attached to the duration row ending on the same day, so a year row carries both the income statement and the balance sheet — you do not need to join instant rows yourself.

Share counts, prices and splits

Share counts here are as reported; price series are split-adjusted. Do not multiply one by the other without a split factor. This dataset does not carry one, and there is no warning built into the data — the numbers simply come out wrong by the split ratio. On DECK, earnings yield computed this way came out at 41.7% (a P/E of 1.8) against a true 6.9%.

This affects only ratios that mix a filing with a price: market capitalisation, P/E, earnings yield, price-to-book. Purely accounting ratios — margins, accruals, returns on equity, growth — are unaffected, because both sides come from the same filing.

The fix now exists: ZipLime/corporate-actions publishes a cumulative_split_factor per company and date span. Multiply an as-filed share count by it, or divide an as-filed EPS, and the figure lines up with a split-adjusted price series. On Deckers it is 6.0 before 2024-10-31 and 1.0 after — exactly the factor that turns the 41.7% back into 6.9%.

shares_outstanding_quality flags a second problem, one this dataset can see: 49 annual statements in six thousand state a count a thousand or a million times too small — 81 956 shares against $346M of profit. The check compares the reported count with the one implied by net income over diluted EPS:

ValueMeaning
okthe two agree within a factor of two
understated_1e3, understated_1e6reported count is off by that scale
inconsistentthey disagree by more than 2× — often a multi-class company reporting one class
invalidzero or negative
unverifiedno EPS or no net income to check against

The share is small and the consequence is not: a ranking strategy sorts on exactly these quantities, so a count understated by a thousand lands at the top of the sort.

Joins with the rest of the family

entity_id in the pit config is the issuer CIK — the same key used by ZipLime/insider-trading, ZipLime/insider-sale-notices and ZipLime/institutional-portfolio-13f. Fundamentals, insider trades, sale notices and institutional holdings join on one column, with no ticker dictionary in between — which is where this kind of join usually goes wrong.

Configs

ConfigRowsWhat one row is
fundamentals_wide2 619 517one financial statement: a filing, a period, 48 concepts as columns
fundamentals35 344 045one named concept resolved for one filing and one period, with the tag it came from
facts97 919 866one consolidated numeric fact exactly as filed
facts_dimensional87 038 061one fact qualified by a segment, geography or share class
filings433 717one XBRL submission, with its acceptance timestamp
pit2 619 517one point-in-time statement event, plus a Delta table for ziplime
quarters70one source archive consumed, with its SHA-256

Start with fundamentals_wide. Go to fundamentals when you need to know which tag produced a number, and to facts when you need something the 48 concepts do not cover.

The concept map, and its limits

US GAAP offers several tags for the same line of the same statement. Revenue arrives as Revenues, RevenueFromContractWithCustomerExcludingAssessedTax, SalesRevenueNet, or an interest-income tag if the filer is a bank. Picking one tag gets you a third of the market and a bias you cannot see.

48 concepts map 118 tags, in priority order set from measured usage rather than memory. Every resolved value keeps its source_tag, so you can always see which one won.

Coverage on annual reports (10-K, full-year periods):

ConceptCoverage
net income96.9%
total equity94.4%
operating cash flow94.3%
revenue83.8%
total assets76.7%a 10-K states 3 years of income and only 2 balance sheets
diluted EPS62.2%see below
long-term debt~50%US GAAP has no tag for total borrowings that everyone uses

Two of those need explaining rather than fixing.

Assets at 77% is the structure of the document: the median 10-K in this corpus reports three years of income statement and two balance sheets. The third year has no balance sheet to attach because the filing does not restate one.

EPS at 62% over the whole history is a story about XBRL adoption. In 2011, when only large filers tagged, diluted EPS was present in 84% of annual reports. In 2012 the smaller reporting companies arrived, filings tripled to 20 544, and coverage fell to 49%. It has climbed back to 87% by 2025. The dataset shows the history it has, not a smoothed version of it.

Debt at 50% is a real limit. LongTermDebtNoncurrent and LongTermDebt overlap, so components cannot be summed without counting the same borrowing twice; the map takes the first total-like tag present and misses companies that tag only individual instruments.

Verification

The concept map is a pile of judgement calls, and a wrong one produces a number that looks like a balance sheet and is not one. Two arithmetic identities the filings themselves had to satisfy are checked on every build and gate publication:

CheckHolds for
assets = liabilities + equity + redeemable instruments95.9% of 888 059 statements
income to common ÷ diluted shares = filed diluted EPS (5%)95.0% of 585 411 statements

Both numbers started lower and both gaps turned out to be accounting rather than mapping errors. The balance sheet failed on 11% of statements until redeemable preferred stock — which sits between liabilities and equity — got its own concept. EPS failed on 14% until income available to common shareholders, which is what EPS is actually computed on, got its own concept.

Schema — fundamentals_wide

Identifying columns: cik, accession_number, period_end, quarters, period_kind (instant · quarter · half_year · nine_months · year · multi_year · implausible_duration), form, fiscal_year, fiscal_period, filed_date, accepted_at, currency, shares_outstanding_quality.

Then one column per concept: revenue, cost_of_revenue, gross_profit, research_and_development, selling_general_administrative, operating_expenses, operating_income, interest_expense, pretax_income, income_tax_expense, net_income, net_income_to_common, net_income_to_noncontrolling, comprehensive_income, depreciation_amortization, share_based_compensation, eps_basic, eps_diluted, shares_basic_weighted, shares_diluted_weighted, dividends_per_share_declared, cash_and_equivalents, short_term_investments, accounts_receivable, inventory, current_assets, ppe_net, goodwill, intangible_assets, total_assets, accounts_payable, current_liabilities, short_term_debt, long_term_debt, total_liabilities, common_equity, total_equity, retained_earnings, noncontrolling_interest, temporary_equity, shares_outstanding, operating_cash_flow, investing_cash_flow, financing_cash_flow, capital_expenditure, dividends_paid, share_repurchase, debt_issued, debt_repaid, stock_issued.

Outflows are published as filed, positive: capital_expenditure is money spent, not a negative cash flow. Currency is whatever the filing used — about one fact in ten is not in dollars, and converting it would need an exchange rate the filing does not contain.

Known gaps

  • —No pre-2009 history. XBRL tagging begins with 2009 Q1. There is no structured financial statement before it, whatever a vendor claims.
  • —No analyst estimates, no price targets, no consensus. Those are proprietary, not public filings.
  • —No tickers. cik is the identifier; a CIK-to-ticker map is a separate problem with its own point-in-time trap, and pretending otherwise here would hide it.
  • —Custom tags are labelled, not interpreted. 15% of facts use tags a filer invented for itself. They are published with is_standard_taxonomy = false and no attempt is made to map them.
  • —Dimensional facts are a decomposition, not an addition. Never sum facts and facts_dimensional together.
  • —`period_kind` needs filtering, always. See the section above; this is the single most common way to read this dataset wrong.
  • —2 218 facts name an impossible period — years like 1011 and 2923, typed into a filing and republished faithfully by the SEC. They are kept in facts with period_end_quality = "out_of_range" and left out of the derived tables, where an event dated 2923 is not a date.

Provenance and updates

Source: SEC Financial Statement Data Sets, DERA. US Government work, public domain. The quarters config lists every archive consumed with its SHA-256, so a build can be checked against the bytes it was made from.

Rebuilt weekly, Monday 07:10 UTC, by a Hugging Face Job whose script (`jobs/run.py`) lints, tests, ingests, rebuilds, verifies and only then publishes. Weekly rather than daily because DERA publishes an archive once a quarter, some weeks after the quarter closes.