SPB Git

spb/firc0001_uqo

Public

N4: two runs, two 10-K segmenters, cohort regression, fiscal_year audit, 10-K funnel, git/mtime evidence

Items: FIRC0001-AI_C-07 EDIT-04 EDIT-19 EDIT-20 ADVE-02 ADVE-11 AI_C-03 THEO-06 DATA-13 ECON-03 ECON-08.
No June git revision exists (first commit 0304133 on 2026-10-27); extractor files mtime 2026-06-17; df7905d..2324b58 = store.py +3/-1.
Legacy MD&A carries 141 vs 272 '$' tokens (92% of pairs fewer); 86 full_document 10-Ks (35 invisible, 51 via legacy);
10-K funnel 341,207 -> 116,540 + 27,164 = 143,704 exact; cohort dummy +8.8 (all runs) vs -23.0 (October only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 5, 2026) parent 37587f1 Browse files

4 changed files +20,001 −0

added code/audit/a04_git_evidence.sh +75 −0
@@ -0,0 +1,75 @@
1+#!/bin/bash
2+# N4 (a) - read-only git / file-system evidence on M3U96b:~/UQO_RESEARCH_DB for the two extraction runs.
3+# Writes results/runs/git_evidence.md and the two inter-revision patches. Never modifies git state.
4+set -u
5+OUT="$HOME/firc0001_review/results/runs"; mkdir -p "$OUT"
6+cd "$HOME/UQO_RESEARCH_DB" || exit 1
7+export GIT_PAGER=cat
8+FILES="project-db/spid/extract/llm.py project-db/spid/extract/rules.py project-db/spid/extract/base.py project-db/spid/taxonomy.py project-db/spid/resolve.py project-db/spid/ingest.py project-db/spid/config.py project-db/spid/pipeline.py project-db/spid/store.py project-db/spid/__main__.py project-db/run_full_extract.py src/parsing/html_parser.py src/parsing/section_extractor.py src/parsing/filing_parser.py src/parsing/chunker.py scripts/parse_filings.py"
9+{
10+echo "# Git and file-system evidence for the two SPID extraction runs (N4a)"
11+echo
12+echo "Generated $(date -Iseconds) on $(hostname) from a read-only inspection of \`~/UQO_RESEARCH_DB\` (clone of \`gitsrv:srv/git/uqo-research-db.git\`)."
13+echo
14+echo "## Repository state"
15+echo '```'
16+echo "HEAD: $(git rev-parse HEAD)"; echo "branch: $(git rev-parse --abbrev-ref HEAD)"; echo "tags: $(git tag | tr '\n' ' ')"
17+echo "remote: $(git remote -v | head -1)"
18+echo "working tree (git status --short | head -20):"; git status --short | head -20
19+echo '```'
20+echo
21+echo "## All commits (there is no commit before 2026-10-27; the June 2026 run has no git revision)"
22+echo '```'
23+git log --format='%h %ad %an %s' --date=iso
24+echo '```'
25+echo
26+echo "## First commit of each extractor / parser file (git log --diff-filter=A)"
27+echo '```'
28+for f in $FILES; do printf '%-45s %s\n' "$f" "$(git log --diff-filter=A --format='%h %ad' --date=iso -- "$f" | tail -1)"; done
29+echo '```'
30+echo
31+echo "## Commits touching each file"
32+echo '```'
33+for f in $FILES; do printf '%-45s %s\n' "$f" "$(git log --format='%h' -- "$f" | tr '\n' ' ')"; done
34+echo '```'
35+echo
36+echo "## File modification times (file-system evidence; extractor modules untouched since 2026-06-17)"
37+echo '```'
38+for f in $FILES; do [ -f "$f" ] && printf '%-45s %s %s bytes sha256=%s\n' "$f" "$(stat -f '%Sm' -t '%Y-%m-%d %H:%M:%S' "$f")" "$(stat -f '%z' "$f")" "$(shasum -a 256 "$f" | cut -c1-16)"; done
39+echo '```'
40+echo
41+echo "## git diff --stat 0304133..2324b58 (first commit -> revision cited by the paper) for the extractor, parser and driver"
42+echo '```'
43+git diff --stat 0304133 2324b58 -- project-db/spid src/parsing scripts/parse_filings.py project-db/run_full_extract.py
44+echo '```'
45+echo
46+echo "## git diff --stat df7905d..2324b58 (the pair named in the paper)"
47+echo '```'
48+git diff --stat df7905d 2324b58 -- project-db/spid src/parsing scripts/parse_filings.py project-db/run_full_extract.py
49+echo '```'
50+echo
51+echo "## git diff --stat 2324b58..HEAD (after the paper's revision)"
52+echo '```'
53+git diff --stat 2324b58 HEAD -- project-db/spid src/parsing scripts/parse_filings.py project-db/run_full_extract.py
54+echo '```'
55+echo
56+echo "## June 2026 run parameters from project-db/logs/extract.log (first and last lines; model id absent)"
57+echo '```'
58+head -2 project-db/logs/extract.log; echo '...'; tail -2 project-db/logs/extract.log
59+echo "grep -c 'gpt' project-db/logs/extract.log = $(grep -c 'gpt' project-db/logs/extract.log)"
60+echo "grep -c 'gpt' data/logs/spid_extract.log = $(grep -c 'gpt' data/logs/spid_extract.log)"
61+echo "project-db/logs mtimes:"; for f in project-db/logs/*; do printf ' %-40s %s\n' "$f" "$(stat -f '%Sm' -t '%Y-%m-%d %H:%M:%S' "$f")"; done
62+echo "project-db/.env present: $( [ -f project-db/.env ] && echo yes || echo no ) (content never read)"
63+echo "project-db/.env.example model line: $(grep -h 'SPID_LLM_MODEL' project-db/.env.example 2>/dev/null | head -2 | tr '\n' ' ')"
64+echo '```'
65+echo
66+echo "## Interpretation"
67+echo "- The repository's first commit (0304133, 2026-10-27) already contains \`project-db/\`; no revision of the June 2026 code exists in git. The sentence '2324b58 supersedes df7905d examined in the June study' names two October commits."
68+echo "- The extractor modules (llm.py, rules.py, base.py, taxonomy.py, resolve.py, ingest.py) carry 2026-06-17 modification times: the prompt, the gate, the taxonomy and the resolver used in October are, by file-system evidence, the files that existed before the June run. The driver/config/store (config.py, pipeline.py, __main__.py: 2026-10-27 12:34; store.py: 2026-10-27 21:34) were edited before the first commit, so their June versions are not recoverable."
69+echo "- The only change to these files between df7905d and 2324b58 is the one-line DuckDB 2.0 compatibility fix in store.py (see patch)."
70+} > "$OUT/git_evidence.md"
71+git diff 0304133 2324b58 -- project-db/spid src/parsing scripts/parse_filings.py project-db/run_full_extract.py > "$OUT/git_diff_0304133_2324b58.patch"
72+git diff df7905d 2324b58 -- project-db/spid src/parsing scripts/parse_filings.py project-db/run_full_extract.py > "$OUT/git_diff_df7905d_2324b58.patch"
73+git diff 2324b58 HEAD -- project-db/spid src/parsing scripts/parse_filings.py project-db/run_full_extract.py > "$OUT/git_diff_2324b58_HEAD.patch"
74+wc -l "$OUT"/*.patch
75+echo "git state after: $(git rev-parse HEAD) $(git status --short | wc -l) changed paths"
added code/audit/a04_runs_segmenters.py +202 −0
@@ -0,0 +1,202 @@
1+"""N4 - two runs, two 10-K segmenters, cohort confounding, fiscal_year audit, 10-K funnel (cluster D). Read-only."""
2+from __future__ import annotations
3+import json, sys
4+from pathlib import Path
5+import numpy as np
6+sys.path.insert(0, str(Path(__file__).resolve().parent))
7+from common import *
8+
9+r = Run("runs")
10+con = connect(SPID_DB)
11+tmin = con.execute("select min(extracted_at) from extracted_sections").fetchone()[0]
12+con.execute(f"""CREATE TEMP TABLE led AS
13+SELECT e.section_id, e.accession_number, e.n_mentions, CASE WHEN e.extracted_at = TIMESTAMP '{tmin}' THEN 'backfilled_june' ELSE 'october' END AS run,
14+ f.form_type, f.cik, f.filing_date, f.fiscal_year AS filing_fiscal_year, co.in_sp500, co.in_russell3000
15+FROM extracted_sections e JOIN src_filings f ON f.accession_number = e.accession_number LEFT JOIN src_companies co ON f.cik = co.cik""")
16+con.execute("""CREATE TEMP TABLE secmeta AS
17+SELECT section_id, section_name, word_count, 'parsed' AS segmenter FROM src_sections
18+UNION ALL SELECT section_id, section_name, word_count, 'legacy_ingest' FROM spid_sections""")
19+con.execute("CREATE TEMP TABLE ledm AS SELECT l.*, s.section_name, s.word_count, s.segmenter FROM led l LEFT JOIN secmeta s USING (section_id)")
20+
21+# ---- (b) legacy (ingest.py) vs parsed (src/parsing) text of the same 10-K items ----------------
22+MONEY = r"\$\s?\d[\d,]*(\.\d+)?\s?(billion|million|thousand|bn|mm)?"
23+YEAR = r"\b(19[89]\d|20[0-3]\d)\b"
24+feat = lambda src: f"""SELECT section_id, accession_number, section_name, word_count, length(text) AS nchar,
25+ length(text) - length(replace(text, '$', '')) AS dollar_tokens,
26+ len(regexp_extract_all(text, '{YEAR}')) AS year_tokens,
27+ len(regexp_extract_all(text, '{MONEY}')) AS money_tokens,
28+ len(regexp_extract_all(text, '\\b\\d[\\d,]*(\\.\\d+)?\\b')) AS numeric_tokens
29+FROM {src}"""
30+print("features: legacy", flush=True)
31+con.execute(f"CREATE TEMP TABLE leg AS {feat('spid_sections')}")
32+print("features: parsed 10-K of the same accessions", flush=True)
33+con.execute(f"CREATE TEMP TABLE par AS {feat('src_sections')} WHERE accession_number IN (SELECT DISTINCT accession_number FROM spid_sections)")
34+r.dump("segmenter_item_comparison.csv", con, """
35+WITH j AS (SELECT l.section_name, l.word_count lw, p.word_count pw, l.dollar_tokens ld, p.dollar_tokens pd, l.year_tokens ly, p.year_tokens py, l.money_tokens lm, p.money_tokens pm, l.numeric_tokens ln, p.numeric_tokens pn
36+ FROM leg l JOIN par p ON l.accession_number = p.accession_number AND l.section_name = p.section_name)
37+SELECT section_name, count(*) pairs,
38+ round(median(lw)) legacy_median_words, round(median(pw)) parsed_median_words,
39+ round(quantile_cont(lw*1.0/nullif(pw,0), 0.10),3) ratio_p10, round(quantile_cont(lw*1.0/nullif(pw,0), 0.50),3) ratio_p50, round(quantile_cont(lw*1.0/nullif(pw,0), 0.90),3) ratio_p90,
40+ round(avg(ld),1) legacy_mean_dollar, round(avg(pd),1) parsed_mean_dollar,
41+ round(avg(ly),1) legacy_mean_years, round(avg(py),1) parsed_mean_years,
42+ round(avg(lm),1) legacy_mean_money, round(avg(pm),1) parsed_mean_money,
43+ round(avg(ln),1) legacy_mean_numeric, round(avg(pn),1) parsed_mean_numeric,
44+ round(100.0*count(*) FILTER (WHERE lm < pm)/count(*),2) pct_legacy_fewer_money_tokens,
45+ round(100.0*count(*) FILTER (WHERE lm = 0 AND pm > 0)/count(*),2) pct_legacy_zero_money_parsed_positive,
46+ round(100.0*count(*) FILTER (WHERE ld < pd)/count(*),2) pct_legacy_fewer_dollar,
47+ round(100.0*count(*) FILTER (WHERE lw < 0.5*pw)/count(*),2) pct_legacy_lt_half_words,
48+ round(100.0*count(*) FILTER (WHERE lw > 2*pw)/count(*),2) pct_legacy_gt_double_words
49+FROM j GROUP BY 1 ORDER BY 2 DESC""")
50+S = {}
51+S["legacy_accessions"] = con.execute("select count(distinct accession_number) from spid_sections").fetchone()[0]
52+S["legacy_rows"] = con.execute("select count(*) from spid_sections").fetchone()[0]
53+S["legacy_accessions_with_parsed_counterpart"] = con.execute("select count(distinct accession_number) from par").fetchone()[0]
54+S["legacy_rows_matched_by_item"] = con.execute("select count(*) from leg l join par p on l.accession_number=p.accession_number and l.section_name=p.section_name").fetchone()[0]
55+S["legacy_section_names"] = con.execute("select section_name, count(*), round(median(word_count)) from spid_sections group by 1 order by 2 desc").fetchall()
56+S["parsed_section_names_same_accessions"] = con.execute("select section_name, count(*), round(median(word_count)) from par group by 1 order by 2 desc").fetchall()
57+S["legacy_items_per_accession"] = con.execute("select n, count(*) from (select accession_number, count(*) n from spid_sections group by 1) group by 1 order by 1").fetchall()
58+S["legacy_accessions_missing_item"] = con.execute("""select section_name, count(*) from (select a.accession_number, i.section_name from (select distinct accession_number from spid_sections) a cross join (select unnest(['business','risk_factors','properties','md_and_a','quantitative_disclosures']) section_name) i
59+ where not exists (select 1 from spid_sections s where s.accession_number=a.accession_number and s.section_name=i.section_name)) group by 1 order by 2 desc""").fetchall()
60+
61+# ---- (c) full_body rows ----------------------------------------------------------------------------
62+S["full_body_rows"] = con.execute("select count(*), count(distinct accession_number), round(median(word_count)), min(word_count), max(word_count) from spid_sections where section_name='full_body'").fetchone()
63+S["full_body_accessions_with_other_items"] = con.execute("select count(distinct accession_number) from spid_sections where section_name='full_body' and accession_number in (select accession_number from spid_sections where section_name<>'full_body')").fetchone()[0]
64+S["full_body_ledger_yield"] = con.execute("""select count(*), count(*) filter (where n_mentions>0), sum(n_mentions), round(sum(n_mentions)*1.0/count(*),4), round(100.0*count(*) filter (where n_mentions>0)/count(*),2)
65+ from ledm where segmenter='legacy_ingest' and section_name='full_body'""").fetchone()
66+S["legacy_item_ledger_yield"] = con.execute("""select section_name, count(*), count(*) filter (where n_mentions>0), sum(n_mentions), round(sum(n_mentions)*1.0/count(*),4), round(100.0*count(*) filter (where n_mentions>0)/count(*),2), round(median(word_count))
67+ from ledm where segmenter='legacy_ingest' group by 1 order by 2 desc""").fetchall()
68+S["parsed_10k_item_ledger_yield"] = con.execute("""select section_name, count(*), count(*) filter (where n_mentions>0), sum(n_mentions), round(sum(n_mentions)*1.0/count(*),4), round(100.0*count(*) filter (where n_mentions>0)/count(*),2), round(median(word_count))
69+ from ledm where segmenter='parsed' and form_type='10-K' group by 1 order by 2 desc""").fetchall()
70+S["full_body_accessions_parsed_items_available"] = con.execute("""select count(distinct p.accession_number), count(*) from par p where p.accession_number in (select accession_number from spid_sections where section_name='full_body') and p.section_name in ('business','risk_factors','properties','md_and_a','quantitative_disclosures')""").fetchone()
71+
72+# ---- (d) parser-fallback full_document 10-Ks never submitted ---------------------------------------------
73+S["full_document_10k"] = con.execute("""select count(*), count(distinct s.accession_number) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name='full_document'""").fetchone()
74+S["full_document_10k_in_legacy"] = con.execute("""select count(distinct s.accession_number) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name='full_document' and s.accession_number in (select accession_number from spid_sections)""").fetchone()[0]
75+S["full_document_10k_in_ledger"] = con.execute("""select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name='full_document' and s.section_id in (select section_id from extracted_sections)""").fetchone()[0]
76+S["full_document_10k_accessions_with_no_other_section"] = con.execute("""select count(*) from (select s.accession_number from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' group by 1 having bool_and(s.section_name='full_document'))""").fetchone()[0]
77+S["full_document_10k_invisible_accessions"] = con.execute("""select count(*) from (select s.accession_number from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.accession_number not in (select accession_number from spid_sections) group by 1 having bool_and(s.section_name='full_document'))""").fetchone()[0]
78+S["full_document_by_form"] = con.execute("""select f.form_type, count(*), count(distinct s.accession_number), count(*) filter (where s.section_id in (select section_id from extracted_sections)) in_ledger from src_sections s join src_filings f using(accession_number) where s.section_name='full_document' group by 1""").fetchall()
79+S["sections_10k_by_item_all_parsed"] = con.execute("""select s.section_name, count(*), round(median(s.word_count)), count(*) filter (where s.word_count<30) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' group by 1 order by 2 desc""").fetchall()
80+r.write_json("segmenters.json", S)
81+
82+# ---- (e) within S&P 500, within form: backfilled vs October ----------------------------------------------
83+r.dump("within_sp500_by_run_form.csv", con, """
84+SELECT run, form_type, count(*) rows, count(DISTINCT accession_number) filings, count(DISTINCT cik) issuers,
85+ count(*) FILTER (WHERE n_mentions>0) with_mentions, sum(n_mentions) mentions,
86+ round(100.0*count(*) FILTER (WHERE n_mentions>0)/count(*),2) productive_pct, round(sum(n_mentions)*1.0/count(*),4) mentions_per_row,
87+ round(median(word_count)) median_words, min(filing_date) first_filing, max(filing_date) last_filing
88+FROM ledm WHERE in_sp500 GROUP BY 1,2 ORDER BY 2,1""")
89+r.dump("within_sp500_10q_by_item_run.csv", con, """
90+SELECT section_name item, run, count(*) rows, count(*) FILTER (WHERE n_mentions>0) with_mentions, sum(n_mentions) mentions,
91+ round(100.0*count(*) FILTER (WHERE n_mentions>0)/count(*),2) productive_pct, round(sum(n_mentions)*1.0/count(*),4) mentions_per_row, round(median(word_count)) median_words
92+FROM ledm WHERE in_sp500 AND form_type='10-Q' GROUP BY 1,2 ORDER BY 1,2""")
93+con.execute("""CREATE TEMP TABLE pm AS SELECT m.*, l.run, l.in_sp500, l.in_russell3000 FROM project_mentions m JOIN led l USING (section_id)""")
94+r.dump("within_sp500_10q_mentions_by_run.csv", con, """
95+SELECT run, count(*) mentions, round(avg(confidence),4) mean_conf, round(median(confidence),3) median_conf,
96+ round(100.0*count(*) FILTER (WHERE confidence>=0.9)/count(*),2) pct_conf_ge_09, round(100.0*count(*) FILTER (WHERE amount_usd IS NOT NULL)/count(*),2) pct_with_amount,
97+ round(100.0*count(*) FILTER (WHERE len(locations)>0)/count(*),2) pct_with_location, round(100.0*count(*) FILTER (WHERE len(partners)>0)/count(*),2) pct_with_partner,
98+ round(100.0*count(*) FILTER (WHERE len(years)>0)/count(*),2) pct_with_years, round(avg(length(description)),1) mean_desc_chars
99+FROM pm WHERE in_sp500 AND form_type='10-Q' GROUP BY 1""")
100+r.dump("within_sp500_10q_type_mix_by_run.csv", con, """
101+SELECT project_type, count(*) FILTER (WHERE run='backfilled_june') june_n, count(*) FILTER (WHERE run='october') oct_n,
102+ round(100.0*count(*) FILTER (WHERE run='backfilled_june')/sum(count(*) FILTER (WHERE run='backfilled_june')) OVER (),2) june_pct,
103+ round(100.0*count(*) FILTER (WHERE run='october')/sum(count(*) FILTER (WHERE run='october')) OVER (),2) oct_pct
104+FROM pm WHERE in_sp500 AND form_type='10-Q' GROUP BY 1 ORDER BY 2+3 DESC""")
105+r.dump("mentions_by_run_form_all.csv", con, """
106+SELECT run, form_type, count(*) mentions, round(avg(confidence),4) mean_conf, round(100.0*count(*) FILTER (WHERE amount_usd IS NOT NULL)/count(*),2) pct_with_amount,
107+ round(100.0*count(*) FILTER (WHERE len(locations)>0)/count(*),2) pct_with_location, round(100.0*count(*) FILTER (WHERE fiscal_year IS NULL)/count(*),2) pct_fiscal_year_null
108+FROM pm GROUP BY 1,2 ORDER BY 2,1""")
109+# paired within-issuer comparison (10-Q, S&P 500 issuers present in both runs)
110+pairs = con.execute("""
111+WITH a AS (SELECT cik, run, count(*) rows, count(*) FILTER (WHERE n_mentions>0) prod, sum(n_mentions) men FROM ledm WHERE in_sp500 AND form_type='10-Q' GROUP BY 1,2)
112+SELECT j.cik, j.rows, j.prod, j.men, o.rows, o.prod, o.men FROM a j JOIN a o ON j.cik=o.cik AND j.run='backfilled_june' AND o.run='october'""").fetchall()
113+P = {"issuers_paired": len(pairs)}
114+if pairs:
115+ d_prod = np.array([o_p/o_r - j_p/j_r for _, j_r, j_p, j_m, o_r, o_p, o_m in pairs])
116+ d_men = np.array([o_m/o_r - j_m/j_r for _, j_r, j_p, j_m, o_r, o_p, o_m in pairs])
117+ P["mean_diff_productive_share_oct_minus_june"] = float(d_prod.mean()); P["se"] = float(d_prod.std(ddof=1)/np.sqrt(len(d_prod)))
118+ P["mean_diff_mentions_per_row_oct_minus_june"] = float(d_men.mean()); P["se_m"] = float(d_men.std(ddof=1)/np.sqrt(len(d_men)))
119+ P["june_mean_productive_share"] = float(np.mean([j_p/j_r for _, j_r, j_p, *_ in pairs])); P["oct_mean_productive_share"] = float(np.mean([o_p/o_r for _, j_r, j_p, j_m, o_r, o_p, o_m in pairs]))
120+r.write_json("within_issuer_paired_10q.json", P)
121+
122+# ---- (f) Table 6 within cohort + cohort dummy regression ------------------------------------------------
123+con.execute("""CREATE TEMP TABLE _m2 AS SELECT *, lower(coalesce(try(locations[1]), '')) AS loc1,
124+ regexp_extract(lower(regexp_replace(project_name, '[^A-Za-z ]', '', 'g')), '([a-z]{4,})', 1) AS name_tok FROM pm""")
125+con.execute("""CREATE TEMP TABLE k2 AS SELECT *, cik || '|' || project_type || '|' || CASE WHEN loc1 <> '' THEN loc1 ELSE coalesce(nullif(name_tok,''),'general') END AS gkey FROM _m2""")
126+grp = "CASE WHEN in_sp500 AND in_russell3000 THEN 'both' WHEN in_sp500 THEN 'sp500_only' WHEN in_russell3000 THEN 'russell_only' ELSE 'none' END"
127+r.dump("table6_cohort.csv", con, f"""
128+WITH allm AS (SELECT {grp} g, count(DISTINCT gkey) projects, count(DISTINCT cik) issuers, count(*) mentions FROM k2 GROUP BY 1),
129+ octm AS (SELECT {grp} g, count(DISTINCT gkey) projects, count(DISTINCT cik) issuers, count(*) mentions FROM k2 WHERE run='october' GROUP BY 1),
130+ oct10q8k AS (SELECT {grp} g, count(DISTINCT gkey) projects, count(DISTINCT cik) issuers, count(*) mentions FROM k2 WHERE run='october' AND form_type IN ('10-Q','8-K') GROUP BY 1),
131+ ledr AS (SELECT {grp} g, count(*) ledger_rows, count(*) FILTER (WHERE run='october') october_rows, count(DISTINCT cik) issuers_ledger FROM ledm GROUP BY 1)
132+SELECT a.g cohort, a.issuers, a.projects projects_all, round(a.projects*1.0/a.issuers,1) per_issuer_all,
133+ o.projects projects_october_only, round(o.projects*1.0/o.issuers,1) per_issuer_october_only,
134+ q.projects projects_oct_10q_8k, round(q.projects*1.0/q.issuers,1) per_issuer_oct_10q_8k,
135+ l.ledger_rows, l.october_rows, round(l.ledger_rows*1.0/l.issuers_ledger,1) ledger_rows_per_issuer, round(l.october_rows*1.0/l.issuers_ledger,1) october_rows_per_issuer
136+FROM allm a JOIN octm o USING (g) JOIN oct10q8k q USING (g) JOIN ledr l USING (g) ORDER BY 1""")
137+iss = con.execute(f"""
138+WITH p AS (SELECT cik, count(DISTINCT gkey) projects, count(DISTINCT gkey) FILTER (WHERE run='october') projects_oct FROM k2 GROUP BY 1),
139+ l AS (SELECT cik, any_value(in_sp500) sp, count(*) rows, count(*) FILTER (WHERE run='october') rows_oct, count(DISTINCT accession_number) filings FROM ledm GROUP BY 1)
140+SELECT l.cik, l.sp::int, coalesce(p.projects,0), coalesce(p.projects_oct,0), l.rows, l.rows_oct, l.filings FROM l LEFT JOIN p USING (cik)""").fetchall()
141+A = np.array([[x[1], x[2], x[3], x[4], x[5], x[6]] for x in iss], dtype=float)
142+def ols(y, X, names):
143+ X = np.column_stack([np.ones(len(y)), X]); b, *_ = np.linalg.lstsq(X, y, rcond=None); e = y - X @ b
144+ XtX_inv = np.linalg.inv(X.T @ X); meat = (X * (e**2)[:, None]).T @ X; V = XtX_inv @ meat @ XtX_inv * len(y)/(len(y)-X.shape[1])
145+ se = np.sqrt(np.diag(V)); return {n: {"coef": float(bb), "hc1_se": float(s)} for n, bb, s in zip(["const"]+names, b, se)} | {"n": int(len(y)), "r2": float(1 - (e**2).sum()/((y-y.mean())**2).sum())}
146+REG = {}
147+REG["projects_all_on_sp500"] = ols(A[:,1], A[:,[0]], ["in_sp500"])
148+REG["projects_october_only_on_sp500"] = ols(A[:,2], A[:,[0]], ["in_sp500"])
149+REG["projects_all_on_sp500_log_rows"] = ols(A[:,1], np.column_stack([A[:,0], np.log1p(A[:,3])]), ["in_sp500", "log1p_ledger_rows"])
150+REG["projects_october_only_on_sp500_log_rows_oct"] = ols(A[:,2], np.column_stack([A[:,0], np.log1p(A[:,4])]), ["in_sp500", "log1p_october_rows"])
151+REG["log_projects_all_on_sp500_log_rows"] = ols(np.log1p(A[:,1]), np.column_stack([A[:,0], np.log1p(A[:,3])]), ["in_sp500", "log1p_ledger_rows"])
152+REG["log_projects_oct_on_sp500_log_rows_oct"] = ols(np.log1p(A[:,2]), np.column_stack([A[:,0], np.log1p(A[:,4])]), ["in_sp500", "log1p_october_rows"])
153+REG["note"] = "issuer-level OLS, HC1 standard errors; 'october only' counts projects whose key is touched by at least one October-run mention; in_sp500 = cohort dummy (S&P 500 issuers were extracted in two runs and with two 10-K segmenters)"
154+r.write_json("table6_cohort_regression.json", REG)
155+
156+# ---- (g) fiscal_year audit and derived fill ------------------------------------------------------------------
157+G = {}
158+G["mentions_fiscal_year_null_by_run_form"] = con.execute("select run, form_type, count(*) filter (where fiscal_year is null), count(*) from pm group by 1,2 order by 2,1").fetchall()
159+G["src_filings_fiscal_year_null_by_form"] = con.execute("select form_type, count(*) filter (where fiscal_year is null), count(*) filter (where report_date is null), count(*) from src_filings group by 1").fetchall()
160+G["null_mentions_fillable_from_src_filings"] = con.execute("""select count(*) filter (where f.fiscal_year is not null), count(*) filter (where f.fiscal_year is null and f.report_date is not null), count(*)
161+ from pm m join src_filings f using(accession_number) where m.fiscal_year is null""").fetchone()
162+G["ledger_rows_filing_fiscal_year_null_by_run"] = con.execute("select run, count(*) filter (where filing_fiscal_year is null), count(*) from ledm group by 1").fetchall()
163+G["fill_rule"] = "fiscal_year_fill = coalesce(src_filings.fiscal_year, year(report_date), year(filing_date)); derived, not stored"
164+r.dump("fiscal_year_fill_by_accession.csv", con, """
165+SELECT DISTINCT m.accession_number, m.form_type, f.filing_date, f.report_date, f.fiscal_year AS fiscal_year_src,
166+ coalesce(f.fiscal_year, year(f.report_date), year(f.filing_date)) AS fiscal_year_fill,
167+ CASE WHEN f.fiscal_year IS NOT NULL THEN 'src_filings' WHEN f.report_date IS NOT NULL THEN 'year(report_date)' ELSE 'year(filing_date)' END AS fill_source
168+FROM pm m JOIN src_filings f USING (accession_number) WHERE m.fiscal_year IS NULL ORDER BY 1""")
169+G["mentions_null_by_fill_source"] = con.execute("""select case when f.fiscal_year is not null then 'src_filings' when f.report_date is not null then 'year(report_date)' else 'year(filing_date)' end s, count(*)
170+ from pm m join src_filings f using(accession_number) where m.fiscal_year is null group by 1""").fetchall()
171+G["mentions_fiscal_year_vs_filing_fiscal_year_mismatch_nonnull"] = con.execute("""select count(*) from pm m join src_filings f using(accession_number) where m.fiscal_year is not null and f.fiscal_year is not null and m.fiscal_year <> f.fiscal_year""").fetchone()[0]
172+r.write_json("fiscal_year_audit.json", G)
173+
174+# ---- (h) 10-K funnel ----------------------------------------------------------------------------------------
175+H = {}
176+H["parsed_10k_rows_total"] = con.execute("select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K'").fetchone()[0]
177+H["parsed_10k_rows_out_of_scope_item"] = con.execute("select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name not in ('business','risk_factors','properties','md_and_a','quantitative_disclosures')").fetchone()[0]
178+H["parsed_10k_rows_in_scope"] = H["parsed_10k_rows_total"] - H["parsed_10k_rows_out_of_scope_item"]
179+H["parsed_10k_in_scope_rows_of_legacy_accessions"] = con.execute("select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name in ('business','risk_factors','properties','md_and_a','quantitative_disclosures') and s.accession_number in (select accession_number from spid_sections)").fetchone()[0]
180+H["parsed_10k_in_scope_rows_lt30_words_non_legacy"] = con.execute("select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name in ('business','risk_factors','properties','md_and_a','quantitative_disclosures') and s.accession_number not in (select accession_number from spid_sections) and s.word_count < 30").fetchone()[0]
181+H["parsed_10k_in_scope_rows_ge30_non_legacy"] = con.execute("select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name in ('business','risk_factors','properties','md_and_a','quantitative_disclosures') and s.accession_number not in (select accession_number from spid_sections) and s.word_count >= 30").fetchone()[0]
182+H["parsed_10k_in_scope_rows_ge30_non_legacy_in_ledger"] = con.execute("select count(*) from src_sections s join src_filings f using(accession_number) where f.form_type='10-K' and s.section_name in ('business','risk_factors','properties','md_and_a','quantitative_disclosures') and s.accession_number not in (select accession_number from spid_sections) and s.word_count >= 30 and s.section_id in (select section_id from extracted_sections)").fetchone()[0]
183+H["legacy_rows"] = con.execute("select count(*) from spid_sections").fetchone()[0]
184+H["legacy_rows_in_ledger"] = con.execute("select count(*) from spid_sections where section_id in (select section_id from extracted_sections)").fetchone()[0]
185+H["legacy_rows_lt30"] = con.execute("select count(*) from spid_sections where word_count<30").fetchone()[0]
186+H["ledger_10k_rows"] = con.execute("select count(*) from ledm where form_type='10-K'").fetchone()[0]
187+H["ledger_10k_by_segmenter"] = con.execute("select segmenter, count(*) from ledm where form_type='10-K' group by 1").fetchall()
188+H["funnel"] = f"{H['parsed_10k_rows_total']} parsed 10-K rows - {H['parsed_10k_rows_out_of_scope_item']} out-of-scope items - {H['parsed_10k_in_scope_rows_of_legacy_accessions']} rows of legacy-ingested filings - {H['parsed_10k_in_scope_rows_lt30_words_non_legacy']} rows < 30 words = {H['parsed_10k_in_scope_rows_ge30_non_legacy']} (ledgered: {H['parsed_10k_in_scope_rows_ge30_non_legacy_in_ledger']}); + {H['legacy_rows_in_ledger']} legacy rows = {H['parsed_10k_in_scope_rows_ge30_non_legacy_in_ledger'] + H['legacy_rows_in_ledger']} vs ledger 10-K rows {H['ledger_10k_rows']}"
189+H["expected_ledger_10k"] = 143704
190+# 10-Q items in scope (all seven submitted?)
191+H["ledger_10q_items"] = con.execute("select section_name, count(*) from ledm where form_type='10-Q' group by 1 order by 2 desc").fetchall()
192+H["parsed_10q_items"] = con.execute("select s.section_name, count(*), count(*) filter (where s.word_count<30) from src_sections s join src_filings f using(accession_number) where f.form_type='10-Q' group by 1 order by 2 desc").fetchall()
193+H["ledger_8k_items"] = con.execute("select section_name, count(*) from ledm where form_type='8-K' group by 1 order by 2 desc").fetchall()
194+r.write_json("funnel_10k.json", H)
195+r.dump("legacy_vs_parsed_sp500_items.csv", con, """
196+SELECT 'legacy_ingest' segmenter, section_name, count(*) n, round(median(word_count)) median_words, count(*) FILTER (WHERE word_count<30) lt30 FROM spid_sections GROUP BY 1,2
197+UNION ALL
198+SELECT 'parsed_sp500_10k' segmenter, s.section_name, count(*), round(median(s.word_count)), count(*) FILTER (WHERE s.word_count<30) FROM src_sections s JOIN src_filings f USING (accession_number) JOIN src_companies co ON f.cik=co.cik WHERE f.form_type='10-K' AND co.in_sp500 GROUP BY 1,2
199+UNION ALL
200+SELECT 'parsed_nonsp500_10k' segmenter, s.section_name, count(*), round(median(s.word_count)), count(*) FILTER (WHERE s.word_count<30) FROM src_sections s JOIN src_filings f USING (accession_number) JOIN src_companies co ON f.cik=co.cik WHERE f.form_type='10-K' AND NOT co.in_sp500 GROUP BY 1,2
201+ORDER BY 1,3 DESC""")
202+r.finish()
added results/runs/fiscal_year_audit.json +85 −0
@@ -0,0 +1,85 @@
1+{
2+ "mentions_fiscal_year_null_by_run_form": [
3+ [
4+ "backfilled_june",
5+ "10-K",
6+ 15787,
7+ 15787
8+ ],
9+ [
10+ "october",
11+ "10-K",
12+ 823,
13+ 65708
14+ ],
15+ [
16+ "backfilled_june",
17+ "10-Q",
18+ 5817,
19+ 5817
20+ ],
21+ [
22+ "october",
23+ "10-Q",
24+ 28074,
25+ 146187
26+ ],
27+ [
28+ "backfilled_june",
29+ "8-K",
30+ 18326,
31+ 18326
32+ ],
33+ [
34+ "october",
35+ "8-K",
36+ 0,
37+ 56240
38+ ]
39+ ],
40+ "src_filings_fiscal_year_null_by_form": [
41+ [
42+ "10-Q",
43+ 22542,
44+ 22542,
45+ 95167
46+ ],
47+ [
48+ "8-K",
49+ 103927,
50+ 103928,
51+ 400606
52+ ],
53+ [
54+ "10-K",
55+ 7655,
56+ 7655,
57+ 31775
58+ ]
59+ ],
60+ "null_mentions_fillable_from_src_filings": [
61+ 0,
62+ 0,
63+ 68827
64+ ],
65+ "ledger_rows_filing_fiscal_year_null_by_run": [
66+ [
67+ "backfilled_june",
68+ 154964,
69+ 154964
70+ ],
71+ [
72+ "october",
73+ 114051,
74+ 968134
75+ ]
76+ ],
77+ "fill_rule": "fiscal_year_fill = coalesce(src_filings.fiscal_year, year(report_date), year(filing_date)); derived, not stored",
78+ "mentions_null_by_fill_source": [
79+ [
80+ "year(filing_date)",
81+ 68827
82+ ]
83+ ],
84+ "mentions_fiscal_year_vs_filing_fiscal_year_mismatch_nonnull": 0
85+}
\ No newline at end of file
added results/runs/fiscal_year_fill_by_accession.csv +19639 −0
Large diff (19,639 lines) is not loaded by default to keep the page fast.Load diff

Diff truncated — file too large.

Menu

Theme