Files
pdf_signature_extraction/paper/v13_build/scripts/make_fig3_density.py
T
gbanyanandClaude Opus 4.8 6781c00d5b Paper A v13 rev9.2: firm-key unification + canonical-number reconciliation (rev9.1 review R1-R6)
Resolve the Firm C/D "two-snapshot" inconsistency from the co-author rev9.1 review.
Root cause was NOT stale data but two live firm-assignment keys (assigned_accountant->
registry firm vs the excel_firm column) used inconsistently across tables; standardize
on the registry key, which the headline Table IV/II-b already use. Sole difference = 379
signatures with excel_firm=C but registry firm=D; A and B identical under both keys.

Numbers (all DB-verified, registry key, n=150,442):
- Table II-c Firm C/D recomputed (was mixing keys within firm C across periods, which
  manufactured the spurious "third value" 38,934); now C 22,449+16,164=38,613,
  D 9,945+7,188=17,133, all four firms reconcile.
- Table VI C/D counts + S III-B prose + Fig 3/6 captions -> 150,442.
- S V-B HC-rate text -> Firm C 21.6->26.7%, Firm D 22.0->28.0%.

Note on R4: the reviewer (PDF-only) asked to change S IV-C to 26.5/28.5 to match Table
II-c; DB verification showed the reverse - S IV-C's 26.7/28.0 are correct and Table II-c
was the stale outlier, so II-c was aligned to S IV-C (data-correct, opposite to the
literal instruction).

Accountant counts (R2 reviewer "179>171 impossible" = false positive; three distinct,
all-reproducible universes): Table I + S III-B -> 457 (>=2 sig, owns the 150,442
signatures); Table III documented as the 437 with >=10 signatures (K=3 GMM subset,
reproduces A=82.5/B=0.0/C=1.0/D=1.9% exactly); bootstrap 179/280 unchanged
(accountant_id key, correct and invariant to the A-vs-BCD contrast).

R3 (corpus scope): S III-B reworded - corpus = all retrievable reports, Big-4 as the
primary analysis sample (removes the "corpus = four firms" vs "non-Big-4 in robustness"
contradiction); per-firm counts now explicitly labelled A/B/C/D.
R5 (spelling): unify to American (artefact->artifact x11, centred->centered,
behaviour->behavior, analyse(d)->analyze(d), favours->favors).
R6: delete non-standard "(+)" marker in S IV-C.

Figures regenerated under the registry key: make_fig3_density.py and
make_fig6_sensitivity.py switched to the assigned_accountant join (fig3/fig6 n=150,442);
fig4/fig5 refreshed. FE/LOYO/bootstrap re-validated exactly (ORs 0.116/0.061/0.070,
LOYO 53.1-54.9pp, full 53.7pp).

Add CANONICAL_NUMBERS_rev9.1.md with full provenance, the analyzable/GMM definitions,
and the firm-key root cause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K35dXhb9XEM1mnYz6SSHpU
2026-06-30 15:19:18 +08:00

85 lines
3.9 KiB
Python

"""Figure 3 (real data version): 2D density of the two measures over the five-region scheme.
Replaces the earlier schematic with the actual distribution, with axis ticks and the rule cuts.
Reproduces from signature_analysis.db; Big-4, is_valid=1, both measures present."""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from matplotlib.patches import Rectangle
import numpy as np
import sqlite3
DB = "/Volumes/NV2/PDF-Processing/signature-analysis/signature_analysis.db"
BIG4 = ('勤業眾信聯合', '資誠聯合', '安侯建業聯合', '安永聯合')
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute(f"""
SELECT s.max_similarity_to_same_accountant, s.min_dhash_independent
FROM signatures s JOIN accountants a ON s.assigned_accountant=a.name
WHERE s.max_similarity_to_same_accountant IS NOT NULL
AND s.min_dhash_independent IS NOT NULL
AND a.firm IN ({','.join(['?']*4)})
""", BIG4)
rows = cur.fetchall()
con.close()
cos = np.array([r[0] for r in rows], dtype=float)
dh = np.array([r[1] for r in rows], dtype=float)
n = len(cos)
LO, HI = 0.8547, 0.95
DH1, DH2 = 5, 15
xmin, xmax = 0.70, 1.002
ymin, ymax = -0.5, 30
ycap = 30 # display cap; values above are piled into the top row for visibility
dh_disp = np.minimum(dh, ycap - 0.5)
fig, ax = plt.subplots(figsize=(5.6, 4.4))
# faint region tint behind the density
ax.add_patch(Rectangle((xmin, ymin), LO - xmin, ymax - ymin, facecolor='#bdc3c7', alpha=0.12, zorder=0))
ax.add_patch(Rectangle((LO, ymin), HI - LO, ymax - ymin, facecolor='#f7dc6f', alpha=0.12, zorder=0))
ax.add_patch(Rectangle((HI, ymin), xmax - HI, DH1 - ymin, facecolor='#cb4335', alpha=0.14, zorder=0))
ax.add_patch(Rectangle((HI, DH1), xmax - HI, DH2 - DH1, facecolor='#eb984e', alpha=0.14, zorder=0))
ax.add_patch(Rectangle((HI, DH2), xmax - HI, ymax - DH2, facecolor='#aed6f1', alpha=0.14, zorder=0))
# real 2D density (log counts)
xedges = np.linspace(xmin, xmax, 90)
yedges = np.arange(-0.5, ycap + 0.5, 1.0) # integer dHash bins
H, xe, ye = np.histogram2d(cos, dh_disp, bins=[xedges, yedges])
pcm = ax.pcolormesh(xe, ye, H.T, norm=LogNorm(vmin=1, vmax=H.max()),
cmap='viridis', zorder=1, shading='flat')
cb = fig.colorbar(pcm, ax=ax, pad=0.02)
cb.set_label('signatures per cell (log scale)', fontsize=8)
cb.ax.tick_params(labelsize=7)
# cut lines
ax.axvline(LO, color='gray', ls=':', lw=1.1, zorder=3)
ax.axvline(HI, color='black', ls='--', lw=1.1, zorder=3)
ax.plot([HI, xmax], [DH1, DH1], 'k--', lw=0.9, zorder=3)
ax.plot([HI, xmax], [DH2, DH2], 'k--', lw=0.9, zorder=3)
# region labels
ax.text((xmin + LO) / 2, 24, 'LH', ha='center', fontsize=10, weight='bold', color='#34495e', zorder=4)
ax.text((LO + HI) / 2, 24, 'UN', ha='center', fontsize=10, weight='bold', color='#7d6608', zorder=4)
ax.text((HI + xmax) / 2, 2.2, 'HC', ha='center', fontsize=10, weight='bold', color='#cb4335', zorder=4)
ax.text((HI + xmax) / 2, 9.7, 'MC', ha='center', fontsize=10, weight='bold', color='#a04000', zorder=4)
ax.text((HI + xmax) / 2, 24, 'HSC', ha='center', fontsize=9, weight='bold', color='#21618c', zorder=4)
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
ax.set_xticks([0.70, 0.75, 0.80, 0.8547, 0.90, 0.95, 1.00])
ax.set_xticklabels(['0.70', '0.75', '0.80', '0.855', '0.90', '0.95', '1.00'], fontsize=7.5)
ax.set_yticks([0, 5, 10, 15, 20, 25, 30])
ax.set_yticklabels(['0', '5', '10', '15', '20', '25', '≥30'], fontsize=7.5)
ax.set_xlabel('cosine similarity to same accountant (style)', fontsize=9)
ax.set_ylabel('min dHash distance (structure)', fontsize=9)
ax.set_title(f'Figure 3. Two-measure plane: real density over the five regions (Big-4, n={n:,})',
fontsize=8.5)
fig.tight_layout()
out = '/Volumes/NV2/pdf_recognize/paper/v13_build/figures/fig3.png'
fig.savefig(out, dpi=200, bbox_inches='tight')
plt.close(fig)
print(f'fig3 density OK: n={n:,}, dHash>=30 piled: {(dh>=ycap).sum()}, written {out}')