From af08391a685b21cef1db374c1edff271f484e789 Mon Sep 17 00:00:00 2001 From: gbanyan Date: Mon, 27 Apr 2026 21:40:42 +0800 Subject: [PATCH] Paper A v3.19.0: address Gemini 3.1 Pro round-19 Major Revision findings Gemini 3.1 Pro round-19 (paper/gemini_review_v3_18_4.md) caught FOUR serious issues that all 18 prior AI review rounds missed, including fabricated rationalizations and a real statistical flaw. All four verified by direct DB / script inspection. Verdict: Major Revision; this commit closes every flagged item. Fabricated rationalization corrections (text only, numbers unchanged): - Section IV-H "656 documents excluded" rewritten. Previous text claimed the exclusion was because "single-signature documents have no same-CPA pairwise comparison" -- a fabricated explanation that contradicts the paper's cross-document matching methodology. The truth, verified against signature_analysis/09_pdf_signature_verdict.py L44 (WHERE s.is_valid = 1 AND s.assigned_accountant IS NOT NULL): the 656 documents are excluded because none of their detected signatures could be matched to a registered CPA name (assigned_accountant IS NULL). - Section IV-F.2 "two CPAs excluded for disambiguation ties" rewritten. No disambiguation logic exists in script 24; the 178 vs 180 difference comes from two registered Firm A partners being singletons in the corpus (one signature each, so per-signature best-match cosine is undefined and they do not appear in the matched-signature table that feeds the 70/30 split). - Appendix B Table XIII provenance corrected. The previous attribution to 13_deloitte_distribution_analysis.py / accountant_similarity_analysis.json was wrong: neither artifact has year_month grouping. New script 29_firm_a_yearly_distribution.py reproduces Table XIII exactly from the database via accountants.firm + signatures.year_month grouping. Statistical flaw corrections (numbers updated): - Inter-CPA negative anchor rewritten in 21_expanded_validation.py. The prior implementation drew 50,000 random cross-CPA pairs from a LIMIT-3000 random subsample, reusing each signature ~33 times and artificially tightening Wilson FAR confidence intervals on Table X. The corrected implementation samples 50,000 i.i.d. pairs uniformly across the full 168,755-signature matched corpus. - Re-run script 21. Table X numbers are close to v3.18.4 but no longer rest on the inflated-precision artifact: cos > 0.837: FAR 0.2101 (was 0.2062), CI [0.2066, 0.2137] cos > 0.900: FAR 0.0250 (was 0.0233), CI [0.0237, 0.0264] cos > 0.945: FAR 0.0008 (unchanged at this resolution) cos > 0.950: FAR 0.0005 (was 0.0007), CI [0.0003, 0.0007] cos > 0.973: FAR 0.0002 (was 0.0003), CI [0.0001, 0.0004] cos > 0.979: FAR 0.0001 (was 0.0002), CI [0.0001, 0.0003] - Inter-CPA cosine summary stats also updated: mean 0.763 (was 0.762) P95 0.886 (was 0.884) P99 0.915 (was 0.913) max 0.992 (was 0.988) - Manuscript IV-F.1 prose updated to reflect the i.i.d. full-corpus sampling. Rebuild Paper_A_IEEE_Access_Draft_v3.docx. Note: this is v3.19.0 because v3.19 closes both fabrication and a genuine statistical flaw, not just provenance polish. Co-Authored-By: Claude Opus 4.7 (1M context) --- paper/Paper_A_IEEE_Access_Draft_v3.docx | Bin 285971 -> 286166 bytes paper/paper_a_appendix_v3.md | 2 +- paper/paper_a_results_v3.md | 16 +-- signature_analysis/21_expanded_validation.py | 83 ++++++++---- .../29_firm_a_yearly_distribution.py | 123 ++++++++++++++++++ 5 files changed, 192 insertions(+), 32 deletions(-) create mode 100644 signature_analysis/29_firm_a_yearly_distribution.py diff --git a/paper/Paper_A_IEEE_Access_Draft_v3.docx b/paper/Paper_A_IEEE_Access_Draft_v3.docx index 91f52a03be88da7ec32d4d5d871aa73fae7eee85..bb8067f06ef2908c5b7b8adddbf9c6c30caf5f43 100644 GIT binary patch delta 18460 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 delta 18087 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 UQ0p0R5A>7_?iA&n0|i(83uz(B*#H0l diff --git a/paper/paper_a_appendix_v3.md b/paper/paper_a_appendix_v3.md index f1384bc..ed7ae2e 100644 --- a/paper/paper_a_appendix_v3.md +++ b/paper/paper_a_appendix_v3.md @@ -49,7 +49,7 @@ For reproducibility, the following table maps each numerical table in Section IV | Table X (cosine threshold sweep, FAR vs inter-CPA negatives) | `21_expanded_validation.py` | `reports/expanded_validation/expanded_validation_results.json` | | Table XI (held-out vs calibration Firm A capture rates) | `24_validation_recalibration.py` | `reports/validation_recalibration/validation_recalibration.json` | | Table XII (operational-cut sensitivity 0.95 vs 0.945) | `24_validation_recalibration.py` | `reports/validation_recalibration/validation_recalibration.json` | -| Table XIII (Firm A per-year cosine distribution) | `13_deloitte_distribution_analysis.py` | derived from `reports/accountant_similarity_analysis.json` filtered to Firm A; figures in `reports/figures/` | +| Table XIII (Firm A per-year cosine distribution) | `29_firm_a_yearly_distribution.py` | `reports/firm_a_yearly/firm_a_yearly_distribution.json` | | Tables XIV / XV (partner-level similarity ranking) | `22_partner_ranking.py` | `reports/partner_ranking/partner_ranking_results.json` | | Table XVI (intra-report classification agreement) | `23_intra_report_consistency.py` | `reports/intra_report/intra_report_results.json` | | Table XVII (document-level five-way classification) | `09_pdf_signature_verdict.py`; `12_generate_pdf_level_report.py` | `reports/pdf_signature_verdicts.json`; `reports/pdf_signature_verdict_report.md` (CSV / XLSX bulk reports also at `reports/`) | diff --git a/paper/paper_a_results_v3.md b/paper/paper_a_results_v3.md index 17096ad..0728e84 100644 --- a/paper/paper_a_results_v3.md +++ b/paper/paper_a_results_v3.md @@ -150,7 +150,7 @@ We report three validation analyses corresponding to the anchors of Section III- Of the 182,328 extracted signatures, 310 have a same-CPA nearest match that is byte-identical after crop and normalization (pixel-identical-to-closest = 1); these form the byte-identity positive anchor---a pair-level proof of image reuse that serves as conservative ground truth for non-hand-signed signatures, subject to the source-template edge case discussed in Section V-G. Within Firm A specifically, 145 of these byte-identical signatures are distributed across 50 distinct partners (of 180 registered Firm A partners), with 35 of the byte-identical pairs spanning different fiscal years; this Firm A decomposition is reproduced by `signature_analysis/28_byte_identity_decomposition.py` and reported in `reports/byte_identity_decomp/byte_identity_decomposition.json` (Appendix B). -As the gold-negative anchor we sample 50,000 random cross-CPA signature pairs (inter-CPA cosine: mean $= 0.762$, $P_{95} = 0.884$, $P_{99} = 0.913$, max $= 0.988$). +As the gold-negative anchor we sample 50,000 i.i.d. random cross-CPA signature pairs from the full 168,755-signature matched corpus (inter-CPA cosine: mean $= 0.763$, $P_{95} = 0.886$, $P_{99} = 0.915$, max $= 0.992$). Because the positive and negative anchor populations are constructed from different sampling units (byte-identical same-CPA pairs vs random inter-CPA pairs), their relative prevalence in the combined anchor set is arbitrary, and precision / $F_1$ / recall therefore have no meaningful population interpretation. We accordingly report FAR with Wilson 95% confidence intervals against the large inter-CPA negative anchor in Table X. The primary quantity reported by Table X is FAR: the probability that a random pair of signatures from *different* CPAs exceeds the candidate threshold. @@ -159,12 +159,12 @@ We do not report an Equal Error Rate: EER is meaningful only when the positive a @@ -178,7 +178,7 @@ The very low FAR at the operational cut is therefore informative about specifici ### 2) Held-Out Firm A Validation (within-Firm-A sampling variance disclosure) We split Firm A CPAs randomly 70 / 30 at the CPA level into a calibration fold (124 CPAs, 45,116 signatures) and a held-out fold (54 CPAs, 15,332 signatures). -The total of 178 Firm A CPAs differs from the 180 in the Firm A registry by two CPAs whose signatures could not be matched to a single assigned-accountant record because of disambiguation ties in the CPA registry and which we therefore exclude from both folds; this handling is made explicit here. +The total of 178 Firm A CPAs differs from the 180 in the Firm A registry by two registered Firm A partners whose signatures in the corpus are singletons (only one signature each, so the per-signature best-match cosine is undefined and they do not appear in the same-CPA matched-signature table that script `24_validation_recalibration.py` reads); they are therefore not represented in either fold by construction rather than by an explicit exclusion rule. Thresholds are re-derived from calibration-fold percentiles only. Table XI reports both calibration-fold and held-out-fold capture rates with Wilson 95% CIs and a two-proportion $z$-test. @@ -340,7 +340,7 @@ We note that this test uses the calibrated classifier of Section III-K rather th ## H. Classification Results Table XVII presents the final classification results under the dual-descriptor framework with Firm A-calibrated thresholds for 84,386 documents. -The document count (84,386) differs from the 85,042 documents with any YOLO detection (Table III) because 656 documents carry only a single detected signature, for which no same-CPA pairwise comparison and therefore no best-match cosine / min dHash statistic is available; those documents are excluded from the classification reported here. +The document count (84,386) differs from the 85,042 documents with any YOLO detection (Table III) because 656 documents have no signature whose extracted handwriting could be matched to a registered CPA name (every such signature has `assigned_accountant IS NULL` in the database, typically because the auditor's report page deviates from the standard two-signature layout or the OCRed printed CPA name was not present in the registry); the per-document classifier requires at least one CPA-matched signature so that a same-CPA best-match similarity exists, and these documents are therefore excluded from the classification reported here. We emphasize that the document-level proportions below reflect the *worst-case aggregation rule* of Section III-K: a report carrying one stamped signature and one hand-signed signature is labeled with the most-replication-consistent of the two signature-level verdicts. Document-level rates therefore represent the share of reports in which *at least one* signature is non-hand-signed rather than the share in which *both* are; the intra-report agreement analysis of Section IV-G.3 (Table XVI) reports how frequently the two co-signers share the same signature-level label within each firm, so that readers can judge what fraction of the non-hand-signed document-level share corresponds to fully non-hand-signed reports versus mixed reports. diff --git a/signature_analysis/21_expanded_validation.py b/signature_analysis/21_expanded_validation.py index eeb6e80..5aa37da 100644 --- a/signature_analysis/21_expanded_validation.py +++ b/signature_analysis/21_expanded_validation.py @@ -85,44 +85,78 @@ def load_signatures(): return rows -def load_feature_vectors_sample(n=2000): - """Load feature vectors for inter-CPA negative-anchor sampling.""" +def load_signature_ids_for_negative_pool(seed=SEED): + """Load lightweight (sig_id, accountant) pool from the entire matched + corpus. Per Gemini round-19 review, the prior implementation drew + 50,000 inter-CPA pairs from a tiny LIMIT-3000 random subset, reusing + each signature ~33 times and artificially tightening Wilson FAR CIs. + The corrected implementation samples pairs i.i.d. across the FULL + matched corpus (~168k signatures); only the unique signatures that + actually appear in the sampled pairs need feature vectors loaded. + """ conn = sqlite3.connect(DB) cur = conn.cursor() cur.execute(''' - SELECT signature_id, assigned_accountant, feature_vector + SELECT signature_id, assigned_accountant FROM signatures WHERE feature_vector IS NOT NULL AND assigned_accountant IS NOT NULL - ORDER BY RANDOM() - LIMIT ? - ''', (n,)) + ''') rows = cur.fetchall() conn.close() - out = [] - for r in rows: - vec = np.frombuffer(r[2], dtype=np.float32) - out.append({'sig_id': r[0], 'accountant': r[1], 'feature': vec}) - return out + sig_ids = np.array([r[0] for r in rows], dtype=np.int64) + accts = np.array([r[1] for r in rows]) + return sig_ids, accts -def build_inter_cpa_negative(sample, n_pairs=N_INTER_PAIRS, seed=SEED): - """Sample random cross-CPA pairs; return their cosine similarities.""" +def load_features_for_ids(sig_ids): + conn = sqlite3.connect(DB) + cur = conn.cursor() + placeholders = ','.join('?' * len(sig_ids)) + cur.execute( + f'SELECT signature_id, feature_vector FROM signatures ' + f'WHERE signature_id IN ({placeholders})', + [int(s) for s in sig_ids], + ) + rows = cur.fetchall() + conn.close() + feat_by_id = {} + for sid, blob in rows: + feat_by_id[int(sid)] = np.frombuffer(blob, dtype=np.float32) + return feat_by_id + + +def build_inter_cpa_negative(sig_ids, accts, n_pairs=N_INTER_PAIRS, seed=SEED): + """Sample i.i.d. random cross-CPA pairs from the full matched corpus + and return their cosine similarities. + """ rng = np.random.default_rng(seed) - n = len(sample) - feats = np.stack([s['feature'] for s in sample]) - accts = np.array([s['accountant'] for s in sample]) - sims = [] + n = len(sig_ids) + pairs = [] tries = 0 - while len(sims) < n_pairs and tries < n_pairs * 10: + seen_pairs = set() + while len(pairs) < n_pairs and tries < n_pairs * 10: i = rng.integers(n) j = rng.integers(n) if i == j or accts[i] == accts[j]: tries += 1 continue - sim = float(feats[i] @ feats[j]) - sims.append(sim) + a, b = (i, j) if i < j else (j, i) + if (a, b) in seen_pairs: + tries += 1 + continue + seen_pairs.add((a, b)) + pairs.append((a, b)) tries += 1 + + needed_ids = sorted({int(sig_ids[i]) for pair in pairs for i in pair}) + feat_by_id = load_features_for_ids(needed_ids) + + sims = [] + for i, j in pairs: + fi = feat_by_id[int(sig_ids[i])] + fj = feat_by_id[int(sig_ids[j])] + sims.append(float(fi @ fj)) return np.array(sims) @@ -212,9 +246,12 @@ def main(): print(f'Firm A signatures: {int(firm_a_mask.sum()):,}') # --- (1) INTER-CPA NEGATIVE ANCHOR --- - print(f'\n[1] Building inter-CPA negative anchor ({N_INTER_PAIRS} pairs)...') - sample = load_feature_vectors_sample(n=3000) - inter_cos = build_inter_cpa_negative(sample, n_pairs=N_INTER_PAIRS) + print(f'\n[1] Building inter-CPA negative anchor ({N_INTER_PAIRS} ' + f'i.i.d. pairs from full matched corpus)...') + pool_sig_ids, pool_accts = load_signature_ids_for_negative_pool() + print(f' pool size: {len(pool_sig_ids):,} matched signatures') + inter_cos = build_inter_cpa_negative(pool_sig_ids, pool_accts, + n_pairs=N_INTER_PAIRS) print(f' inter-CPA cos: mean={inter_cos.mean():.4f}, ' f'p95={np.percentile(inter_cos, 95):.4f}, ' f'p99={np.percentile(inter_cos, 99):.4f}, ' diff --git a/signature_analysis/29_firm_a_yearly_distribution.py b/signature_analysis/29_firm_a_yearly_distribution.py new file mode 100644 index 0000000..d43e935 --- /dev/null +++ b/signature_analysis/29_firm_a_yearly_distribution.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Script 29: Firm A Per-Year Cosine Distribution (Table XIII) +============================================================ +Generates the year-by-year Firm A per-signature best-match cosine +distribution reported as Table XIII in the manuscript. Codex / Gemini +round-19 review identified that this table previously had no dedicated +generating script (Appendix B incorrectly attributed it to Script 08, +which has no year_month extraction). + +Definition: + Firm A membership is via CPA registry (accountants.firm joined on + signatures.assigned_accountant), matching the convention used by + scripts 24 and 28. + + For each fiscal year (substr(year_month, 1, 4)): + - N signatures with non-null max_similarity_to_same_accountant + - mean of max_similarity_to_same_accountant (the per-signature + best-match cosine) + - share with max_similarity_to_same_accountant < 0.95 (the + left-tail rate cited in Section IV-G.1) + +Output: + reports/firm_a_yearly/firm_a_yearly_distribution.json + reports/firm_a_yearly/firm_a_yearly_distribution.md +""" + +import json +import sqlite3 +from datetime import datetime +from pathlib import Path + +DB = '/Volumes/NV2/PDF-Processing/signature-analysis/signature_analysis.db' +OUT = Path('/Volumes/NV2/PDF-Processing/signature-analysis/reports/' + 'firm_a_yearly') +OUT.mkdir(parents=True, exist_ok=True) + +FIRM_A = '勤業眾信聯合' + + +def yearly_distribution(conn): + cur = conn.cursor() + cur.execute(""" + SELECT substr(s.year_month, 1, 4) AS year, + COUNT(*) AS n_sigs, + AVG(s.max_similarity_to_same_accountant) AS mean_cos, + SUM(CASE + WHEN s.max_similarity_to_same_accountant < 0.95 + THEN 1 ELSE 0 + END) AS n_below_095 + FROM signatures s + JOIN accountants a ON s.assigned_accountant = a.name + WHERE a.firm = ? + AND s.max_similarity_to_same_accountant IS NOT NULL + AND s.year_month IS NOT NULL + GROUP BY year + ORDER BY year + """, (FIRM_A,)) + + rows = [] + for year, n_sigs, mean_cos, n_below in cur.fetchall(): + rows.append({ + 'year': int(year), + 'n_signatures': n_sigs, + 'mean_best_match_cosine': round(mean_cos, 4), + 'n_below_cosine_095': n_below, + 'pct_below_cosine_095': round(100.0 * n_below / n_sigs, 2), + }) + return rows + + +def write_markdown(payload, path): + rows = payload['yearly_rows'] + lines = [] + lines.append('# Firm A Per-Year Cosine Distribution (Table XIII)') + lines.append('') + lines.append(f"Generated at: {payload['generated_at']}") + lines.append('') + lines.append('Firm A membership: CPA registry ' + '(accountants.firm = "勤業眾信聯合"). Per-signature ' + 'best-match cosine = ' + 'signatures.max_similarity_to_same_accountant.') + lines.append('') + lines.append('| Year | N sigs | mean best-match cosine | % below 0.95 |') + lines.append('|------|--------|------------------------|--------------|') + for r in rows: + lines.append( + f"| {r['year']} | {r['n_signatures']:,} | " + f"{r['mean_best_match_cosine']:.4f} | " + f"{r['pct_below_cosine_095']:.2f}% |" + ) + path.write_text('\n'.join(lines) + '\n', encoding='utf-8') + + +def main(): + conn = sqlite3.connect(DB) + try: + payload = { + 'generated_at': datetime.now().isoformat(timespec='seconds'), + 'database_path': DB, + 'firm_a_label': FIRM_A, + 'firm_a_membership_definition': ( + 'CPA registry: accountants.firm joined on ' + 'signatures.assigned_accountant' + ), + 'cosine_metric': 'signatures.max_similarity_to_same_accountant', + 'yearly_rows': yearly_distribution(conn), + } + finally: + conn.close() + + json_path = OUT / 'firm_a_yearly_distribution.json' + json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), + encoding='utf-8') + print(f'Wrote {json_path}') + + md_path = OUT / 'firm_a_yearly_distribution.md' + write_markdown(payload, md_path) + print(f'Wrote {md_path}') + + +if __name__ == '__main__': + main()