Phase 3 close-out: Script 42 + §IV draft v2 (Table XV filled)
Script 42 tabulates the §III-L five-way per-signature classifier
output on the Big-4 sub-corpus (n=150,442 signatures classified)
and aggregates to document-level (n=75,233 unique PDFs) under
the worst-case rule.
Per-signature five-way overall (Table XV):
HC 74,593 49.58% high-confidence non-hand-signed
MC 39,817 26.47% moderate-confidence non-hand-signed
HSC 314 0.21% high style consistency
UN 35,480 23.58% uncertain
LH 238 0.16% likely hand-signed
Per-firm five-way (% within firm):
Firm A (Deloitte) HC 81.70%, MC 10.76%, UN 7.42%
Firm B (KPMG) HC 34.56%, MC 35.88%, UN 29.09%
Firm C (PwC) HC 23.75%, MC 41.44%, UN 34.21%
Firm D (EY) HC 24.51%, MC 29.33%, UN 45.65%
Document-level (Table XV-B, NEW):
HC 46,857 62.28%
MC 19,667 26.14%
HSC 167 0.22%
UN 8,524 11.33%
LH 18 0.02%
Total 75,233 unique Big-4 PDFs (single-firm 74,854; mixed-firm 379)
§IV v2 changes vs v1:
- Table XV populated with Script 42 counts
- Table XV-B (NEW): document-level worst-case counts
- Per-firm five-way breakdown (% within firm) added
- Per-firm document-level breakdown added
- Document-level paragraph in §IV-J updated to reference Table XV-B
- Phase 3 close-out checklist: item 1 (Table XV TBD) and item 4
(document-level counts) marked RESOLVED; remaining items reduced
from 5 to 3 (renumbering, content audit, codex open-questions)
The per-firm pattern is consistent with the §III-K Spearman-and-
cluster ordering: Firm A's signatures concentrate in HC (81.7%),
the three non-Firm-A firms have markedly lower HC and substantially
higher Uncertain rates (29-46%), with Firm D having the highest
Uncertain rate of the Big-4 -- consistent with the reverse-anchor
score (§III-K Score 2) ranking Firm D fractionally above Firm C in
the hand-leaning direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script 42: Five-Way Per-Signature Categorisation on Big-4 (Table XV fill)
|
||||
==========================================================================
|
||||
Phase 3 close-out. Tabulates the §III-L five-way per-signature
|
||||
classifier output on the Big-4 sub-corpus and aggregates to
|
||||
document-level (per-PDF) labels under the worst-case rule.
|
||||
|
||||
Five-way rule (inherited from v3.20.0 §III-K, retained as v4 §III-L):
|
||||
|
||||
cos > 0.95 AND dHash_indep <= 5 -> HC High-confidence non-hand-signed
|
||||
cos > 0.95 AND 5 < dHash <= 15 -> MC Moderate-confidence non-hand-signed
|
||||
cos > 0.95 AND dHash > 15 -> HSC High style consistency
|
||||
0.837 < cos <= 0.95 -> UN Uncertain
|
||||
cos <= 0.837 -> LH Likely hand-signed
|
||||
|
||||
Document-level worst-case rule (one PDF can carry up to 2 certifying-
|
||||
CPA signatures; the document inherits the most-replication-consistent
|
||||
signature label among the signatures present):
|
||||
|
||||
HC > MC > HSC > UN > LH
|
||||
|
||||
Output:
|
||||
reports/v4_big4/five_way_categorisation/
|
||||
per_signature_counts.csv
|
||||
per_firm_category_crosstab.csv
|
||||
per_document_counts.csv
|
||||
five_way_results.json
|
||||
five_way_report.md
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import csv
|
||||
import json
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
DB = '/Volumes/NV2/PDF-Processing/signature-analysis/signature_analysis.db'
|
||||
OUT = Path('/Volumes/NV2/PDF-Processing/signature-analysis/reports/'
|
||||
'v4_big4/five_way_categorisation')
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
BIG4 = ('勤業眾信聯合', '安侯建業聯合', '資誠聯合', '安永聯合')
|
||||
LABEL = {'勤業眾信聯合': 'Firm A (Deloitte)',
|
||||
'安侯建業聯合': 'Firm B (KPMG)',
|
||||
'資誠聯合': 'Firm C (PwC)',
|
||||
'安永聯合': 'Firm D (EY)'}
|
||||
|
||||
COS_HIGH = 0.95
|
||||
COS_LOW = 0.837
|
||||
DH_HIGH = 5
|
||||
DH_MOD = 15
|
||||
|
||||
# Worst-case priority (HC most-replication-consistent, LH most hand-signed)
|
||||
PRIORITY = {'HC': 0, 'MC': 1, 'HSC': 2, 'UN': 3, 'LH': 4}
|
||||
CATEGORIES = ['HC', 'MC', 'HSC', 'UN', 'LH']
|
||||
CAT_LONG = {
|
||||
'HC': 'High-confidence non-hand-signed',
|
||||
'MC': 'Moderate-confidence non-hand-signed',
|
||||
'HSC': 'High style consistency',
|
||||
'UN': 'Uncertain',
|
||||
'LH': 'Likely hand-signed',
|
||||
}
|
||||
|
||||
|
||||
def classify(cos, dh):
|
||||
if cos is None:
|
||||
return None # cannot classify
|
||||
if cos > COS_HIGH:
|
||||
if dh is None:
|
||||
return None # require dh for HC/MC/HSC distinction
|
||||
if dh <= DH_HIGH:
|
||||
return 'HC'
|
||||
if dh <= DH_MOD:
|
||||
return 'MC'
|
||||
return 'HSC'
|
||||
if cos > COS_LOW:
|
||||
return 'UN'
|
||||
return 'LH'
|
||||
|
||||
|
||||
def load_big4_signatures():
|
||||
conn = sqlite3.connect(DB)
|
||||
cur = conn.cursor()
|
||||
cur.execute('''
|
||||
SELECT s.signature_id, s.source_pdf, s.assigned_accountant, a.firm,
|
||||
s.max_similarity_to_same_accountant,
|
||||
s.min_dhash_independent
|
||||
FROM signatures s
|
||||
JOIN accountants a ON s.assigned_accountant = a.name
|
||||
WHERE s.assigned_accountant IS NOT NULL
|
||||
AND a.firm IN (?, ?, ?, ?)
|
||||
''', BIG4)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
print('=' * 72)
|
||||
print('Script 42: Five-Way Per-Signature Categorisation (Big-4)')
|
||||
print('=' * 72)
|
||||
rows = load_big4_signatures()
|
||||
print(f'\nN Big-4 signatures (loaded, including missing-descriptor): '
|
||||
f'{len(rows):,}')
|
||||
|
||||
# Per-signature classification
|
||||
per_sig = []
|
||||
n_unclassified = 0
|
||||
for r in rows:
|
||||
sig_id, pdf, cpa, firm, cos, dh = r
|
||||
cos_f = None if cos is None else float(cos)
|
||||
dh_f = None if dh is None else float(dh)
|
||||
cat = classify(cos_f, dh_f)
|
||||
if cat is None:
|
||||
n_unclassified += 1
|
||||
continue
|
||||
per_sig.append({
|
||||
'sig_id': sig_id, 'pdf': pdf, 'cpa': cpa, 'firm': firm,
|
||||
'cos': cos_f, 'dh': dh_f, 'cat': cat,
|
||||
})
|
||||
n_classified = len(per_sig)
|
||||
print(f' Classified: {n_classified:,}')
|
||||
print(f' Unclassified (missing cos/dh): {n_unclassified:,}')
|
||||
|
||||
# Overall per-signature counts
|
||||
overall = {c: 0 for c in CATEGORIES}
|
||||
for s in per_sig:
|
||||
overall[s['cat']] += 1
|
||||
print('\n=== Overall per-signature counts (Big-4 classified) ===')
|
||||
print(f' {"cat":<5} {"long":<40} {"n":>8} {"%":>7}')
|
||||
for c in CATEGORIES:
|
||||
n = overall[c]
|
||||
pct = 100 * n / n_classified if n_classified else 0.0
|
||||
print(f' {c:<5} {CAT_LONG[c]:<40} {n:>8,} {pct:>6.2f}%')
|
||||
|
||||
# Per-firm × category cross-tab
|
||||
by_firm = {f: {c: 0 for c in CATEGORIES} for f in BIG4}
|
||||
for s in per_sig:
|
||||
by_firm[s['firm']][s['cat']] += 1
|
||||
print('\n=== Per-firm × category cross-tab (counts) ===')
|
||||
print(f' {"Firm":<22} ' + ' '.join(f'{c:>8}' for c in CATEGORIES)
|
||||
+ f' {"total":>8}')
|
||||
for f in BIG4:
|
||||
cells = [by_firm[f][c] for c in CATEGORIES]
|
||||
total = sum(cells)
|
||||
print(f' {LABEL[f]:<22} '
|
||||
+ ' '.join(f'{n:>8,}' for n in cells)
|
||||
+ f' {total:>8,}')
|
||||
print('\n=== Per-firm × category cross-tab (% within firm) ===')
|
||||
for f in BIG4:
|
||||
cells = [by_firm[f][c] for c in CATEGORIES]
|
||||
total = sum(cells) or 1
|
||||
print(f' {LABEL[f]:<22} '
|
||||
+ ' '.join(f'{100*n/total:>7.2f}%' for n in cells)
|
||||
+ f' total {total:>6,}')
|
||||
|
||||
# Document-level (per-PDF) aggregation under worst-case rule
|
||||
by_pdf = {}
|
||||
for s in per_sig:
|
||||
pdf = s['pdf']
|
||||
if pdf not in by_pdf:
|
||||
by_pdf[pdf] = {'firm_set': set(), 'best_cat': None,
|
||||
'best_priority': 99, 'n_sigs': 0}
|
||||
bp = by_pdf[pdf]
|
||||
bp['n_sigs'] += 1
|
||||
bp['firm_set'].add(s['firm'])
|
||||
prio = PRIORITY[s['cat']]
|
||||
if prio < bp['best_priority']:
|
||||
bp['best_priority'] = prio
|
||||
bp['best_cat'] = s['cat']
|
||||
|
||||
n_docs = len(by_pdf)
|
||||
docs_overall = {c: 0 for c in CATEGORIES}
|
||||
for pdf, bp in by_pdf.items():
|
||||
docs_overall[bp['best_cat']] += 1
|
||||
print(f'\n=== Document-level (n={n_docs:,} unique Big-4 PDFs) ===')
|
||||
print(f' {"cat":<5} {"long":<40} {"n_docs":>8} {"%":>7}')
|
||||
for c in CATEGORIES:
|
||||
n = docs_overall[c]
|
||||
pct = 100 * n / n_docs if n_docs else 0.0
|
||||
print(f' {c:<5} {CAT_LONG[c]:<40} {n:>8,} {pct:>6.2f}%')
|
||||
|
||||
# Document-level by firm (use first firm in the set; PDFs with mixed
|
||||
# firm signatures are rare and reported separately)
|
||||
docs_by_firm = {f: {c: 0 for c in CATEGORIES} for f in BIG4}
|
||||
docs_mixed_firm = {c: 0 for c in CATEGORIES}
|
||||
n_mixed_firm = 0
|
||||
for pdf, bp in by_pdf.items():
|
||||
if len(bp['firm_set']) == 1:
|
||||
firm = next(iter(bp['firm_set']))
|
||||
if firm in BIG4:
|
||||
docs_by_firm[firm][bp['best_cat']] += 1
|
||||
else:
|
||||
n_mixed_firm += 1
|
||||
docs_mixed_firm[bp['best_cat']] += 1
|
||||
print(f'\n=== Document-level per-firm (single-firm PDFs only; '
|
||||
f'mixed-firm = {n_mixed_firm}) ===')
|
||||
print(f' {"Firm":<22} ' + ' '.join(f'{c:>8}' for c in CATEGORIES)
|
||||
+ f' {"total":>8}')
|
||||
for f in BIG4:
|
||||
cells = [docs_by_firm[f][c] for c in CATEGORIES]
|
||||
total = sum(cells)
|
||||
print(f' {LABEL[f]:<22} '
|
||||
+ ' '.join(f'{n:>8,}' for n in cells)
|
||||
+ f' {total:>8,}')
|
||||
|
||||
# Persist CSVs
|
||||
sig_csv = OUT / 'per_signature_counts.csv'
|
||||
with open(sig_csv, 'w', newline='', encoding='utf-8') as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(['cat', 'long_name', 'n', 'pct_of_classified'])
|
||||
for c in CATEGORIES:
|
||||
w.writerow([c, CAT_LONG[c], overall[c],
|
||||
f'{100*overall[c]/n_classified:.2f}'
|
||||
if n_classified else '0'])
|
||||
|
||||
firm_csv = OUT / 'per_firm_category_crosstab.csv'
|
||||
with open(firm_csv, 'w', newline='', encoding='utf-8') as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(['firm', 'firm_label'] + CATEGORIES + ['total']
|
||||
+ [f'{c}_pct' for c in CATEGORIES])
|
||||
for fk in BIG4:
|
||||
cells = [by_firm[fk][c] for c in CATEGORIES]
|
||||
total = sum(cells) or 1
|
||||
w.writerow([fk, LABEL[fk]] + cells + [sum(cells)]
|
||||
+ [f'{100*n/total:.2f}' for n in cells])
|
||||
|
||||
doc_csv = OUT / 'per_document_counts.csv'
|
||||
with open(doc_csv, 'w', newline='', encoding='utf-8') as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(['scope', 'cat', 'long_name', 'n', 'pct'])
|
||||
for c in CATEGORIES:
|
||||
w.writerow(['overall', c, CAT_LONG[c], docs_overall[c],
|
||||
f'{100*docs_overall[c]/n_docs:.2f}' if n_docs
|
||||
else '0'])
|
||||
for fk in BIG4:
|
||||
firm_total = sum(docs_by_firm[fk][c] for c in CATEGORIES) or 1
|
||||
for c in CATEGORIES:
|
||||
w.writerow([LABEL[fk], c, CAT_LONG[c],
|
||||
docs_by_firm[fk][c],
|
||||
f'{100*docs_by_firm[fk][c]/firm_total:.2f}'])
|
||||
for c in CATEGORIES:
|
||||
w.writerow(['mixed_firm', c, CAT_LONG[c], docs_mixed_firm[c],
|
||||
f'{100*docs_mixed_firm[c]/n_mixed_firm:.2f}'
|
||||
if n_mixed_firm else '0'])
|
||||
|
||||
payload = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'rule': {
|
||||
'cos_high': COS_HIGH, 'cos_low': COS_LOW,
|
||||
'dh_high': DH_HIGH, 'dh_mod': DH_MOD,
|
||||
},
|
||||
'priority': PRIORITY,
|
||||
'n_loaded': len(rows),
|
||||
'n_classified': n_classified,
|
||||
'n_unclassified': n_unclassified,
|
||||
'per_signature_overall': {c: overall[c] for c in CATEGORIES},
|
||||
'per_signature_by_firm': {fk: by_firm[fk] for fk in BIG4},
|
||||
'document_level': {
|
||||
'n_docs': n_docs,
|
||||
'overall': docs_overall,
|
||||
'by_firm_single_firm_docs_only': {
|
||||
fk: docs_by_firm[fk] for fk in BIG4
|
||||
},
|
||||
'n_mixed_firm_docs': n_mixed_firm,
|
||||
'mixed_firm_overall': docs_mixed_firm,
|
||||
},
|
||||
}
|
||||
json_path = OUT / 'five_way_results.json'
|
||||
json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False),
|
||||
encoding='utf-8')
|
||||
print(f'\nJSON: {json_path}')
|
||||
|
||||
# Markdown
|
||||
md = [
|
||||
'# §IV-J Five-Way Per-Signature Categorisation on Big-4 (Table XV fill)',
|
||||
f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
|
||||
'',
|
||||
'## Rule (inherited from v3.20.0 §III-K)',
|
||||
'',
|
||||
f'- HC : cos > {COS_HIGH} AND dHash_indep <= {DH_HIGH}',
|
||||
f'- MC : cos > {COS_HIGH} AND {DH_HIGH} < dHash <= {DH_MOD}',
|
||||
f'- HSC: cos > {COS_HIGH} AND dHash > {DH_MOD}',
|
||||
f'- UN : {COS_LOW} < cos <= {COS_HIGH}',
|
||||
f'- LH : cos <= {COS_LOW}',
|
||||
'',
|
||||
'## Sample',
|
||||
'',
|
||||
f'- Loaded Big-4 signatures: {len(rows):,}',
|
||||
f'- Classified (both descriptors available): '
|
||||
f'{n_classified:,}',
|
||||
f'- Unclassified (missing cos or dh): {n_unclassified:,}',
|
||||
'',
|
||||
'## Per-signature overall counts (Table XV — Big-4 subset)',
|
||||
'',
|
||||
'| Category | Long name | $n$ signatures | % of classified |',
|
||||
'|---|---|---|---|',
|
||||
]
|
||||
for c in CATEGORIES:
|
||||
n = overall[c]
|
||||
pct = 100 * n / n_classified if n_classified else 0.0
|
||||
md.append(f'| {c} | {CAT_LONG[c]} | {n:,} | {pct:.2f}% |')
|
||||
|
||||
md += ['', '## Per-firm × category cross-tab (counts)', '',
|
||||
'| Firm | HC | MC | HSC | UN | LH | total |',
|
||||
'|---|---|---|---|---|---|---|']
|
||||
for fk in BIG4:
|
||||
cells = [by_firm[fk][c] for c in CATEGORIES]
|
||||
total = sum(cells)
|
||||
md.append(f'| {LABEL[fk]} | '
|
||||
+ ' | '.join(f'{n:,}' for n in cells)
|
||||
+ f' | {total:,} |')
|
||||
|
||||
md += ['', '## Per-firm × category cross-tab (% within firm)', '',
|
||||
'| Firm | HC % | MC % | HSC % | UN % | LH % |',
|
||||
'|---|---|---|---|---|---|']
|
||||
for fk in BIG4:
|
||||
cells = [by_firm[fk][c] for c in CATEGORIES]
|
||||
total = sum(cells) or 1
|
||||
md.append(f'| {LABEL[fk]} | '
|
||||
+ ' | '.join(f'{100*n/total:.2f}%' for n in cells)
|
||||
+ ' |')
|
||||
|
||||
md += ['', '## Document-level (worst-case rule, per Big-4 PDF)', '',
|
||||
f'- N unique Big-4 PDFs: {n_docs:,}',
|
||||
f'- Mixed-firm PDFs (signatures from >1 Big-4 firm; reported '
|
||||
f'separately below): {n_mixed_firm:,}',
|
||||
'',
|
||||
'| Category | Long name | $n$ documents | % |',
|
||||
'|---|---|---|---|']
|
||||
for c in CATEGORIES:
|
||||
n = docs_overall[c]
|
||||
pct = 100 * n / n_docs if n_docs else 0.0
|
||||
md.append(f'| {c} | {CAT_LONG[c]} | {n:,} | {pct:.2f}% |')
|
||||
|
||||
md += ['', '## Document-level per-firm (single-firm PDFs only)', '',
|
||||
'| Firm | HC | MC | HSC | UN | LH | total |',
|
||||
'|---|---|---|---|---|---|---|']
|
||||
for fk in BIG4:
|
||||
cells = [docs_by_firm[fk][c] for c in CATEGORIES]
|
||||
total = sum(cells)
|
||||
md.append(f'| {LABEL[fk]} | '
|
||||
+ ' | '.join(f'{n:,}' for n in cells)
|
||||
+ f' | {total:,} |')
|
||||
|
||||
md += ['', '## Files',
|
||||
'- `per_signature_counts.csv` -- overall five-way per-signature counts',
|
||||
'- `per_firm_category_crosstab.csv` -- per-firm cross-tab',
|
||||
'- `per_document_counts.csv` -- document-level aggregation',
|
||||
'- `five_way_results.json` -- machine-readable full output',
|
||||
]
|
||||
md_path = OUT / 'five_way_report.md'
|
||||
md_path.write_text('\n'.join(md), encoding='utf-8')
|
||||
print(f'Report: {md_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user