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
63 lines
3.1 KiB
Python
63 lines
3.1 KiB
Python
"""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}")
|