Files
pdf_signature_extraction/paper/v13_build/scripts/make_fig6_sensitivity.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

63 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Figure 6: two-measure sensitivity surface over the (cosine cut x dHash cut) plane.
Panel A: clean-group (B/C/D) flag rate -- how permissive the operating point is.
Panel B: Firm A minus B/C/D flag-rate contrast (pp) -- discrimination across the plane.
Shows the chosen HC point (0.95, dHash<=5) is not a cherry-picked threshold and exposes
the weaker MC band (dHash<=15). Reproduces from signature_analysis.db (DB columns only).
"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
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 CASE WHEN a.firm='勤業眾信聯合' THEN 1 ELSE 0 END isA,
s.max_similarity_to_same_accountant c, s.min_dhash_independent d
FROM signatures s JOIN accountants a ON s.assigned_accountant=a.name
WHERE a.firm IN ({','.join('?'*4)})
AND s.max_similarity_to_same_accountant IS NOT NULL AND s.min_dhash_independent IS NOT NULL""", BIG4)
rows = cur.fetchall(); con.close()
isA = np.array([r[0] for r in rows], bool)
c = np.array([r[1] for r in rows]); d = np.array([r[2] for r in rows])
cA, dA = c[isA], d[isA]; cB, dB = c[~isA], d[~isA]
cos_cuts = np.arange(0.85, 0.9901, 0.0025)
dh_cuts = np.arange(0, 21, 1)
A = np.zeros((len(dh_cuts), len(cos_cuts)))
B = np.zeros_like(A)
for j, cc in enumerate(cos_cuts):
for i, dd in enumerate(dh_cuts):
A[i, j] = 100 * np.mean((cA > cc) & (dA <= dd))
B[i, j] = 100 * np.mean((cB > cc) & (dB <= dd))
contrast = A - B
extent = [cos_cuts[0], cos_cuts[-1], dh_cuts[0], dh_cuts[-1]]
fig, axes = plt.subplots(1, 2, figsize=(10.5, 4.3))
for ax, Z, title, cmap, lab in [
(axes[0], B, '(a) Clean group (B/C/D) flag rate', 'viridis', 'flag rate (%)'),
(axes[1], contrast, '(b) Firm A B/C/D contrast', 'magma', 'contrast (pp)')]:
im = ax.imshow(Z, origin='lower', aspect='auto', extent=extent, cmap=cmap)
cb = fig.colorbar(im, ax=ax, pad=0.02); cb.set_label(lab, fontsize=8); cb.ax.tick_params(labelsize=7)
# operating points
ax.scatter([0.95], [5], marker='*', s=180, color='white', edgecolor='black', zorder=5,
label='HC operating point (0.95, dHash≤5)')
ax.axhline(15, color='white', ls=':', lw=1.0)
ax.text(0.853, 15.4, 'MC upper bound (dHash≤15)', color='white', fontsize=6.5, va='bottom')
ax.set_xlabel('cosine cut', fontsize=9)
ax.set_ylabel('dHash cut (≤)', fontsize=9)
ax.set_title(title, fontsize=9)
ax.tick_params(labelsize=7.5)
ax.legend(loc='lower left', fontsize=6.5, framealpha=0.85)
fig.suptitle('Figure 6. Sensitivity surface of the deployed rule over the two-measure threshold plane (Big-4, n=%d).' % len(c),
fontsize=9, y=1.02)
fig.tight_layout()
out = '/Volumes/NV2/pdf_recognize/paper/v13_build/figures/fig6.png'
fig.savefig(out, dpi=200, bbox_inches='tight')
plt.close(fig)
print(f"fig6 OK n={len(c)}; HC(0.95,5) contrast={contrast[5, np.argmin(abs(cos_cuts-0.95))]:.1f}pp; written {out}")