Add scripts 34 + 35: Big-4-only calibration foundation
Scripts 34 and 35 produced the empirical foundation that triggers the
Paper A v4.0 Big-4 reframe.
Script 34 (Big-4-only pooled calibration):
Pool Firm A + KPMG + PwC + EY (437 CPAs); first time the
three-method framework yields dip-test multimodal results
(p<0.0001 on both cos and dh axes) anywhere in the analysis
family. 2D-GMM K=2 marginal crossings with bootstrap 95% CI
(n=500): cos = 0.9755 [0.974, 0.977], dh = 3.755 [3.48, 3.97].
Crossing offsets from Paper A v3.20.0 baseline (0.945, 8.10):
+0.030 (cos), -4.345 (dh) -- mid/small-firm tail had
substantially shifted the published threshold.
Script 35 (Big-4 K=3 cluster membership):
Hard-assigns each Big-4 CPA to one of the K=3 components.
Findings:
* Firm A (Deloitte): 0% in C1 (hand-sign-leaning),
17.5% in C2 (mixed), 82.5% in C3 (replicated).
* PwC has the strongest hand-sign tradition (24/102 = 23.5%
in C1), followed by EY (11.5%) and KPMG (8.9%).
* 40 CPAs total in C1 across KPMG/PwC/EY.
Implications confirmed by these scripts:
* Big-4-only scope is the methodologically defensible primary
analysis; the published 0.945/8.10 reflects between-firm
structure rather than within-pool mechanism boundary.
* Firm A's role pivots from "calibration anchor" to
"case study of templated end of Big-4."
* Paper A is being reframed as v4.0 on sub-branch
paper-a-v4-big4, per Partner Jimmy's earlier direction
suggestion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script 34: Big-4-Only Pooled Calibration
|
||||
==========================================
|
||||
Pool Firm A + KPMG + PwC + EY (drop all mid/small firms) and re-run
|
||||
the three-method framework + 2D GMM K=2/K=3 + bootstrap stability
|
||||
on the resulting accountant-level (cos_mean, dh_mean) plane.
|
||||
|
||||
Why this variant:
|
||||
Paper A's published "natural threshold" (cos=0.945, dh=8.10) was
|
||||
derived from a 3-comp 2D GMM on the FULL dataset (Big-4 + ~250
|
||||
mid/small-firm CPAs). The mid/small-firm tail adds extra noise
|
||||
and is itself heterogeneous (many firms, few CPAs each).
|
||||
Restricting to Big-4 only gives a cleaner four-firm contrast and
|
||||
may produce a tighter, more reproducible crossing.
|
||||
|
||||
Comparison table (the deliverable):
|
||||
| Source | cos crossing | dh crossing |
|
||||
| Paper A published (full 3-comp) | 0.945 | 8.10 |
|
||||
| Firm A alone (Script 32) | ~0.977 | ~4.6 |
|
||||
| Non-Firm-A alone (Script 32) | ~0.938 | ~7.5 |
|
||||
| Big-4 only pooled (this script) | ??? | ??? |
|
||||
| + bootstrap 95% CI | [..,..] | [..,..] |
|
||||
|
||||
Verdict (descriptive):
|
||||
TIGHTER bootstrap 95% CI half-width <= 0.005 (cos) AND <= 0.5 (dh)
|
||||
AND point estimate within 0.01 (cos) / 1.0 (dh) of 0.945/8.10
|
||||
COMPARABLE CI overlaps Paper A point estimate, half-width <= 0.01 / 1.0
|
||||
WIDER CI half-width > 0.01 (cos) OR > 1.0 (dh)
|
||||
|
||||
Output:
|
||||
reports/big4_only_pooled/
|
||||
big4_only_pooled_results.json
|
||||
big4_only_pooled_report.md
|
||||
panel_big4_only_<measure>.png
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from scipy import stats
|
||||
from scipy.signal import find_peaks
|
||||
from scipy.optimize import brentq
|
||||
from sklearn.mixture import GaussianMixture
|
||||
import diptest
|
||||
|
||||
DB = '/Volumes/NV2/PDF-Processing/signature-analysis/signature_analysis.db'
|
||||
OUT = Path('/Volumes/NV2/PDF-Processing/signature-analysis/reports/'
|
||||
'big4_only_pooled')
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
EPS = 1e-6
|
||||
Z_CRIT = 1.96
|
||||
MIN_SIGS = 10
|
||||
N_BOOTSTRAP = 500
|
||||
BOOT_SEED = 42
|
||||
|
||||
BIG4 = ('勤業眾信聯合', '安侯建業聯合', '資誠聯合', '安永聯合')
|
||||
|
||||
PAPER_A_COS = 0.945
|
||||
PAPER_A_DH = 8.10
|
||||
|
||||
|
||||
def load_big4_pooled():
|
||||
conn = sqlite3.connect(DB)
|
||||
cur = conn.cursor()
|
||||
cur.execute('''
|
||||
SELECT s.assigned_accountant,
|
||||
a.firm,
|
||||
AVG(s.max_similarity_to_same_accountant) AS cos_mean,
|
||||
AVG(CAST(s.min_dhash_independent AS REAL)) AS dh_mean,
|
||||
COUNT(*) AS n
|
||||
FROM signatures s
|
||||
JOIN accountants a ON s.assigned_accountant = a.name
|
||||
WHERE s.assigned_accountant IS NOT NULL
|
||||
AND s.max_similarity_to_same_accountant IS NOT NULL
|
||||
AND s.min_dhash_independent IS NOT NULL
|
||||
AND a.firm IN (?, ?, ?, ?)
|
||||
GROUP BY s.assigned_accountant
|
||||
HAVING n >= ?
|
||||
''', BIG4 + (MIN_SIGS,))
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
def gmm_2d_marginal_crossing(X, dim, K=2, seed=42):
|
||||
if len(X) < 8:
|
||||
return None, None
|
||||
gmm = GaussianMixture(n_components=K, covariance_type='full',
|
||||
random_state=seed, n_init=15, max_iter=500).fit(X)
|
||||
means = gmm.means_
|
||||
covs = gmm.covariances_
|
||||
weights = gmm.weights_
|
||||
if K != 2:
|
||||
return None, gmm
|
||||
m1, m2 = means[0][dim], means[1][dim]
|
||||
s1 = np.sqrt(covs[0][dim, dim])
|
||||
s2 = np.sqrt(covs[1][dim, dim])
|
||||
w1, w2 = weights[0], weights[1]
|
||||
|
||||
def diff(x):
|
||||
return (w2 * stats.norm.pdf(x, m2, s2)
|
||||
- w1 * stats.norm.pdf(x, m1, s1))
|
||||
xs = np.linspace(float(X[:, dim].min()), float(X[:, dim].max()), 2000)
|
||||
ys = diff(xs)
|
||||
ch = np.where(np.diff(np.sign(ys)) != 0)[0]
|
||||
if not len(ch):
|
||||
return None, gmm
|
||||
mid = 0.5 * (m1 + m2)
|
||||
crossings = []
|
||||
for i in ch:
|
||||
try:
|
||||
crossings.append(brentq(diff, xs[i], xs[i + 1]))
|
||||
except ValueError:
|
||||
continue
|
||||
if not crossings:
|
||||
return None, gmm
|
||||
return float(min(crossings, key=lambda c: abs(c - mid))), gmm
|
||||
|
||||
|
||||
def gmm_3comp_summary(X, seed=42):
|
||||
if len(X) < 12:
|
||||
return None
|
||||
gmm = GaussianMixture(n_components=3, covariance_type='full',
|
||||
random_state=seed, n_init=15, max_iter=500).fit(X)
|
||||
order = np.argsort(gmm.means_[:, 0])
|
||||
return {
|
||||
'means': [[float(m[0]), float(m[1])] for m in gmm.means_[order]],
|
||||
'weights': [float(w) for w in gmm.weights_[order]],
|
||||
'bic': float(gmm.bic(X)),
|
||||
'aic': float(gmm.aic(X)),
|
||||
}
|
||||
|
||||
|
||||
def fit_logit_gmm(x, K=2, seed=42):
|
||||
x = np.clip(np.asarray(x, dtype=float), EPS, 1 - EPS)
|
||||
z = np.log(x / (1 - x)).reshape(-1, 1)
|
||||
gmm = GaussianMixture(n_components=K, random_state=seed,
|
||||
max_iter=500).fit(z)
|
||||
order = np.argsort(gmm.means_.ravel())
|
||||
means = gmm.means_.ravel()[order]
|
||||
stds = np.sqrt(gmm.covariances_.ravel())[order]
|
||||
weights = gmm.weights_[order]
|
||||
crossing = None
|
||||
if K == 2:
|
||||
m1, s1, w1 = means[0], stds[0], weights[0]
|
||||
m2, s2, w2 = means[1], stds[1], weights[1]
|
||||
|
||||
def diff(z0):
|
||||
return (w2 * stats.norm.pdf(z0, m2, s2)
|
||||
- w1 * stats.norm.pdf(z0, m1, s1))
|
||||
zs = np.linspace(min(m1, m2) - 2, max(m1, m2) + 2, 2000)
|
||||
ys = diff(zs)
|
||||
ch = np.where(np.diff(np.sign(ys)) != 0)[0]
|
||||
if len(ch):
|
||||
try:
|
||||
z_cross = brentq(diff, zs[ch[0]], zs[ch[0] + 1])
|
||||
crossing = float(1 / (1 + np.exp(-z_cross)))
|
||||
except ValueError:
|
||||
pass
|
||||
return {
|
||||
'K': K,
|
||||
'aic': float(gmm.aic(z)),
|
||||
'bic': float(gmm.bic(z)),
|
||||
'crossing_original': crossing,
|
||||
}
|
||||
|
||||
|
||||
def kde_dip(values):
|
||||
arr = np.asarray(values, dtype=float)
|
||||
arr = arr[np.isfinite(arr)]
|
||||
dip, pval = diptest.diptest(arr, boot_pval=True, n_boot=2000)
|
||||
kde = stats.gaussian_kde(arr, bw_method='silverman')
|
||||
xs = np.linspace(arr.min(), arr.max(), 2000)
|
||||
density = kde(xs)
|
||||
peaks, _ = find_peaks(density, prominence=density.max() * 0.02)
|
||||
antimodes = []
|
||||
for i in range(len(peaks) - 1):
|
||||
seg = density[peaks[i]:peaks[i + 1]]
|
||||
if not len(seg):
|
||||
continue
|
||||
local = peaks[i] + int(np.argmin(seg))
|
||||
antimodes.append(float(xs[local]))
|
||||
return {
|
||||
'n': int(len(arr)),
|
||||
'dip': float(dip),
|
||||
'dip_pvalue': float(pval),
|
||||
'unimodal_alpha05': bool(pval > 0.05),
|
||||
'n_modes': int(len(peaks)),
|
||||
'antimode': antimodes[0] if antimodes else None,
|
||||
}
|
||||
|
||||
|
||||
def bd_mccrary(values, bin_width, direction):
|
||||
arr = np.asarray(values, dtype=float)
|
||||
arr = arr[np.isfinite(arr)]
|
||||
lo = float(np.floor(arr.min() / bin_width) * bin_width)
|
||||
hi = float(np.ceil(arr.max() / bin_width) * bin_width)
|
||||
edges = np.arange(lo, hi + bin_width, bin_width)
|
||||
counts, _ = np.histogram(arr, bins=edges)
|
||||
centers = (edges[:-1] + edges[1:]) / 2.0
|
||||
N = counts.sum()
|
||||
p = counts / N if N else counts.astype(float)
|
||||
n_bins = len(counts)
|
||||
z = np.full(n_bins, np.nan)
|
||||
for i in range(1, n_bins - 1):
|
||||
p_lo = p[i - 1]
|
||||
p_hi = p[i + 1]
|
||||
exp_i = 0.5 * (counts[i - 1] + counts[i + 1])
|
||||
var_i = (N * p[i] * (1 - p[i])
|
||||
+ 0.25 * N * (p_lo + p_hi) * (1 - p_lo - p_hi))
|
||||
if var_i <= 0:
|
||||
continue
|
||||
z[i] = (counts[i] - exp_i) / np.sqrt(var_i)
|
||||
transitions = []
|
||||
for i in range(1, len(z)):
|
||||
if np.isnan(z[i - 1]) or np.isnan(z[i]):
|
||||
continue
|
||||
ok = ((direction == 'neg_to_pos' and z[i - 1] < -Z_CRIT and z[i] > Z_CRIT)
|
||||
or (direction == 'pos_to_neg' and z[i - 1] > Z_CRIT and z[i] < -Z_CRIT))
|
||||
if ok:
|
||||
transitions.append({
|
||||
'threshold_between': float((centers[i - 1] + centers[i]) / 2.0),
|
||||
'z_before': float(z[i - 1]),
|
||||
'z_after': float(z[i]),
|
||||
})
|
||||
best = (max(transitions,
|
||||
key=lambda t: abs(t['z_before']) + abs(t['z_after']))
|
||||
if transitions else None)
|
||||
return {
|
||||
'n_transitions': len(transitions),
|
||||
'threshold': (best['threshold_between'] if best else None),
|
||||
}
|
||||
|
||||
|
||||
def bootstrap_2d_gmm_crossing(X, dim, n_boot=N_BOOTSTRAP, seed=BOOT_SEED):
|
||||
rng = np.random.default_rng(seed)
|
||||
crossings = []
|
||||
n = len(X)
|
||||
for b in range(n_boot):
|
||||
idx = rng.integers(0, n, size=n)
|
||||
Xb = X[idx]
|
||||
c, _ = gmm_2d_marginal_crossing(Xb, dim, K=2, seed=42)
|
||||
if c is not None:
|
||||
crossings.append(c)
|
||||
crossings = np.asarray(crossings)
|
||||
if len(crossings) < n_boot * 0.5:
|
||||
return None
|
||||
return {
|
||||
'n_successful_boot': int(len(crossings)),
|
||||
'mean': float(np.mean(crossings)),
|
||||
'median': float(np.median(crossings)),
|
||||
'std': float(np.std(crossings, ddof=1)),
|
||||
'ci95': [float(np.quantile(crossings, 0.025)),
|
||||
float(np.quantile(crossings, 0.975))],
|
||||
'ci_halfwidth': float(0.5 * (np.quantile(crossings, 0.975)
|
||||
- np.quantile(crossings, 0.025))),
|
||||
}
|
||||
|
||||
|
||||
def classify_stability(boot_cos, boot_dh, point_cos, point_dh):
|
||||
if boot_cos is None or boot_dh is None:
|
||||
return 'WIDER', ('Bootstrap failed to converge in >50% of resamples; '
|
||||
'crossing is unstable.')
|
||||
cos_hw = boot_cos['ci_halfwidth']
|
||||
dh_hw = boot_dh['ci_halfwidth']
|
||||
cos_offset = abs(point_cos - PAPER_A_COS) if point_cos is not None else None
|
||||
dh_offset = abs(point_dh - PAPER_A_DH) if point_dh is not None else None
|
||||
note = (f'CI half-width (cos) = {cos_hw:.4f}, (dh) = {dh_hw:.3f}; '
|
||||
f'offset from Paper A baseline (cos) = {cos_offset}, '
|
||||
f'(dh) = {dh_offset}.')
|
||||
if (cos_hw <= 0.005 and dh_hw <= 0.5
|
||||
and cos_offset is not None and cos_offset <= 0.01
|
||||
and dh_offset is not None and dh_offset <= 1.0):
|
||||
return 'TIGHTER', f'Big-4-only crossing is tighter and aligned. {note}'
|
||||
if cos_hw <= 0.01 and dh_hw <= 1.0:
|
||||
return 'COMPARABLE', (f'Big-4-only crossing is comparable to '
|
||||
f'published baseline in stability. {note}')
|
||||
return 'WIDER', (f'Big-4-only crossing is wider than the published '
|
||||
f'baseline -- restriction does not improve stability. {note}')
|
||||
|
||||
|
||||
def main():
|
||||
print('=' * 72)
|
||||
print('Script 34: Big-4-Only Pooled Calibration')
|
||||
print('=' * 72)
|
||||
rows = load_big4_pooled()
|
||||
by_firm = {}
|
||||
for r in rows:
|
||||
by_firm.setdefault(r[1], 0)
|
||||
by_firm[r[1]] += 1
|
||||
print(f'\nN Big-4 CPAs (n_signatures >= {MIN_SIGS}): {len(rows)}')
|
||||
for firm, n in sorted(by_firm.items(), key=lambda x: -x[1]):
|
||||
print(f' {firm}: {n}')
|
||||
|
||||
cos = np.array([r[2] for r in rows])
|
||||
dh = np.array([r[3] for r in rows])
|
||||
X = np.column_stack([cos, dh])
|
||||
|
||||
# Three-method on each margin
|
||||
out = {'sample_sizes': by_firm,
|
||||
'n_total_cpas': int(len(rows))}
|
||||
for desc, arr, bin_width, direction in [
|
||||
('cos_mean', cos, 0.002, 'neg_to_pos'),
|
||||
('dh_mean', dh, 0.2, 'pos_to_neg'),
|
||||
]:
|
||||
kde_r = kde_dip(arr)
|
||||
bd_r = bd_mccrary(arr, bin_width, direction)
|
||||
is_cos = (desc == 'cos_mean')
|
||||
x_norm = arr if is_cos else arr / 64.0
|
||||
loggmm2 = fit_logit_gmm(x_norm, K=2)
|
||||
if not is_cos and loggmm2.get('crossing_original') is not None:
|
||||
loggmm2['crossing_original'] = loggmm2['crossing_original'] * 64.0
|
||||
out[desc] = {
|
||||
'kde_dip': kde_r,
|
||||
'bd_mccrary': bd_r,
|
||||
'logit_gmm_2': loggmm2,
|
||||
}
|
||||
print(f'\n[{desc}]')
|
||||
print(f' KDE+dip: dip p={kde_r["dip_pvalue"]:.4f}, '
|
||||
f'n_modes={kde_r["n_modes"]}, antimode={kde_r["antimode"]}')
|
||||
print(f' BD/McCrary: {bd_r["n_transitions"]} transitions, '
|
||||
f'threshold={bd_r["threshold"]}')
|
||||
print(f' LogGMM-2 crossing: {loggmm2.get("crossing_original")}')
|
||||
|
||||
# 2D GMM K=2 marginal crossings + bootstrap
|
||||
print('\n[2D GMM K=2]')
|
||||
cross_cos, gmm2 = gmm_2d_marginal_crossing(X, dim=0, K=2)
|
||||
cross_dh, _ = gmm_2d_marginal_crossing(X, dim=1, K=2)
|
||||
print(f' cos crossing = {cross_cos}')
|
||||
print(f' dh crossing = {cross_dh}')
|
||||
print(f' K=2 BIC = {gmm2.bic(X):.2f}, AIC = {gmm2.aic(X):.2f}')
|
||||
print(f' Component means: {gmm2.means_.tolist()}')
|
||||
print(f' Component weights: {gmm2.weights_.tolist()}')
|
||||
|
||||
print('\n[2D GMM K=3 (for completeness)]')
|
||||
g3 = gmm_3comp_summary(X)
|
||||
print(f' Components (sorted by cos): {g3["means"]}')
|
||||
print(f' Weights: {g3["weights"]}')
|
||||
print(f' K=3 BIC = {g3["bic"]:.2f}, AIC = {g3["aic"]:.2f}')
|
||||
|
||||
print('\n[Bootstrap 95% CI on 2D GMM crossings]')
|
||||
boot_cos = bootstrap_2d_gmm_crossing(X, dim=0)
|
||||
boot_dh = bootstrap_2d_gmm_crossing(X, dim=1)
|
||||
if boot_cos:
|
||||
print(f' cos: median={boot_cos["median"]:.4f}, '
|
||||
f'95% CI=[{boot_cos["ci95"][0]:.4f}, {boot_cos["ci95"][1]:.4f}], '
|
||||
f'half-width={boot_cos["ci_halfwidth"]:.4f} '
|
||||
f'({boot_cos["n_successful_boot"]}/{N_BOOTSTRAP} resamples)')
|
||||
if boot_dh:
|
||||
print(f' dh: median={boot_dh["median"]:.4f}, '
|
||||
f'95% CI=[{boot_dh["ci95"][0]:.4f}, {boot_dh["ci95"][1]:.4f}], '
|
||||
f'half-width={boot_dh["ci_halfwidth"]:.4f} '
|
||||
f'({boot_dh["n_successful_boot"]}/{N_BOOTSTRAP} resamples)')
|
||||
|
||||
out['gmm_2d_2comp'] = {
|
||||
'cos_crossing': cross_cos,
|
||||
'dh_crossing': cross_dh,
|
||||
'bic': float(gmm2.bic(X)),
|
||||
'aic': float(gmm2.aic(X)),
|
||||
'means': gmm2.means_.tolist(),
|
||||
'weights': gmm2.weights_.tolist(),
|
||||
'bootstrap_cos': boot_cos,
|
||||
'bootstrap_dh': boot_dh,
|
||||
}
|
||||
out['gmm_2d_3comp'] = g3
|
||||
out['paper_a_baseline'] = {'cos': PAPER_A_COS, 'dh': PAPER_A_DH}
|
||||
|
||||
# Verdict
|
||||
verdict_class, verdict_msg = classify_stability(
|
||||
boot_cos, boot_dh, cross_cos, cross_dh)
|
||||
out['verdict'] = {'class': verdict_class, 'explanation': verdict_msg}
|
||||
print(f'\nVerdict: {verdict_class} -- {verdict_msg}')
|
||||
|
||||
# Plots: histogram + crossings overlay
|
||||
for desc, arr, bin_width, point in [
|
||||
('cos_mean', cos, 0.002, cross_cos),
|
||||
('dh_mean', dh, 0.2, cross_dh),
|
||||
]:
|
||||
boot = boot_cos if desc == 'cos_mean' else boot_dh
|
||||
baseline = PAPER_A_COS if desc == 'cos_mean' else PAPER_A_DH
|
||||
fig, ax = plt.subplots(figsize=(10, 5))
|
||||
lo = float(np.floor(arr.min() / bin_width) * bin_width)
|
||||
hi = float(np.ceil(arr.max() / bin_width) * bin_width)
|
||||
bins = np.arange(lo, hi + bin_width, bin_width)
|
||||
ax.hist(arr, bins=bins, density=True, alpha=0.55, color='steelblue',
|
||||
edgecolor='white')
|
||||
kde = stats.gaussian_kde(arr, bw_method='silverman')
|
||||
xs = np.linspace(arr.min(), arr.max(), 500)
|
||||
ax.plot(xs, kde(xs), 'g-', lw=1.5, label='KDE (Silverman)')
|
||||
if point is not None:
|
||||
ax.axvline(point, color='orange', lw=2, ls='--',
|
||||
label=f'2D-GMM K=2 crossing = {point:.4f}')
|
||||
ax.axvline(baseline, color='black', lw=2, ls=':',
|
||||
label=f'Paper A baseline = {baseline}')
|
||||
if boot is not None:
|
||||
ax.axvspan(boot['ci95'][0], boot['ci95'][1], color='orange',
|
||||
alpha=0.15,
|
||||
label=f"95% bootstrap CI = "
|
||||
f"[{boot['ci95'][0]:.4f}, {boot['ci95'][1]:.4f}]")
|
||||
ax.set_xlabel(desc)
|
||||
ax.set_ylabel('Density')
|
||||
ax.set_title(f'Big-4-only pooled accountant {desc} '
|
||||
f'(n={len(arr)} CPAs)')
|
||||
ax.legend(fontsize=9)
|
||||
fig.tight_layout()
|
||||
png = OUT / f'panel_big4_only_{desc}.png'
|
||||
fig.savefig(png, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f' plot: {png}')
|
||||
|
||||
out['generated_at'] = datetime.now().isoformat()
|
||||
(OUT / 'big4_only_pooled_results.json').write_text(
|
||||
json.dumps(out, indent=2, ensure_ascii=False), encoding='utf-8')
|
||||
print(f'\nJSON: {OUT / "big4_only_pooled_results.json"}')
|
||||
|
||||
# Markdown
|
||||
md = [
|
||||
'# Big-4-Only Pooled Calibration (Script 34)',
|
||||
f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
|
||||
'',
|
||||
'## Sample',
|
||||
'',
|
||||
f'- Population: Firm A + KPMG + PwC + EY (no mid/small firms)',
|
||||
f'- N CPAs (n_sigs >= {MIN_SIGS}): **{len(rows)}**',
|
||||
'',
|
||||
'| Firm | N CPAs |',
|
||||
'|---|---|',
|
||||
]
|
||||
for firm, n in sorted(by_firm.items(), key=lambda x: -x[1]):
|
||||
md.append(f'| {firm} | {n} |')
|
||||
md += ['', '## Comparison table', '',
|
||||
'| Source | cos crossing | dh crossing |',
|
||||
'|---|---|---|',
|
||||
f'| Paper A published (full 3-comp) | {PAPER_A_COS} | {PAPER_A_DH} |',
|
||||
f'| Firm A alone (Script 32) | ~0.977 | ~4.6 |',
|
||||
f'| Non-Firm-A alone (Script 32) | ~0.938 | ~7.5 |',
|
||||
f'| **Big-4 only pooled (this script, K=2)** | '
|
||||
f'**{cross_cos}** | **{cross_dh}** |']
|
||||
if boot_cos:
|
||||
md.append(f'| + bootstrap 95% CI (n={N_BOOTSTRAP}) | '
|
||||
f'[{boot_cos["ci95"][0]:.4f}, {boot_cos["ci95"][1]:.4f}] | '
|
||||
f'[{boot_dh["ci95"][0]:.4f}, {boot_dh["ci95"][1]:.4f}] |')
|
||||
md += ['', '## Three-method margin checks (Big-4-only)', '',
|
||||
'| Measure | dip p (KDE) | KDE antimode | BD/McCrary threshold | LogGMM-2 crossing |',
|
||||
'|---|---|---|---|---|',
|
||||
f'| cos_mean | {out["cos_mean"]["kde_dip"]["dip_pvalue"]:.4f} | '
|
||||
f'{out["cos_mean"]["kde_dip"]["antimode"]} | '
|
||||
f'{out["cos_mean"]["bd_mccrary"]["threshold"]} | '
|
||||
f'{out["cos_mean"]["logit_gmm_2"]["crossing_original"]} |',
|
||||
f'| dh_mean | {out["dh_mean"]["kde_dip"]["dip_pvalue"]:.4f} | '
|
||||
f'{out["dh_mean"]["kde_dip"]["antimode"]} | '
|
||||
f'{out["dh_mean"]["bd_mccrary"]["threshold"]} | '
|
||||
f'{out["dh_mean"]["logit_gmm_2"]["crossing_original"]} |',
|
||||
'',
|
||||
'## 2D GMM K=2 components',
|
||||
'',
|
||||
'| Component | mean cos | mean dh | weight |',
|
||||
'|---|---|---|---|']
|
||||
for i, (m, w) in enumerate(zip(gmm2.means_, gmm2.weights_)):
|
||||
md.append(f'| C{i+1} | {m[0]:.4f} | {m[1]:.4f} | {w:.3f} |')
|
||||
md.append(f'')
|
||||
md.append(f'BIC(K=2 2D)={gmm2.bic(X):.2f}, AIC={gmm2.aic(X):.2f}')
|
||||
md.append(f'BIC(K=3 2D)={g3["bic"]:.2f}, AIC={g3["aic"]:.2f}')
|
||||
md += ['', '## 2D GMM K=3 components', '',
|
||||
'| Component | mean cos | mean dh | weight |',
|
||||
'|---|---|---|---|']
|
||||
for i, (m, w) in enumerate(zip(g3['means'], g3['weights'])):
|
||||
md.append(f'| C{i+1} | {m[0]:.4f} | {m[1]:.4f} | {w:.3f} |')
|
||||
|
||||
md += ['', '## Verdict', '',
|
||||
f'**{verdict_class}** -- {verdict_msg}',
|
||||
'',
|
||||
'### Verdict legend',
|
||||
'- **TIGHTER**: bootstrap CI half-width <= 0.005 (cos) AND <= 0.5 '
|
||||
'(dh) AND point estimate within 0.01 (cos) / 1.0 (dh) of Paper A '
|
||||
'baseline (0.945, 8.10). Big-4-only restriction strictly improves '
|
||||
'stability without shifting the threshold materially.',
|
||||
'- **COMPARABLE**: CI half-width <= 0.01 (cos) / <= 1.0 (dh). '
|
||||
'Big-4-only is within published precision.',
|
||||
'- **WIDER**: bootstrap unstable -- mid/small-firm tail was '
|
||||
'apparently informative, not just noise.',
|
||||
'']
|
||||
(OUT / 'big4_only_pooled_report.md').write_text('\n'.join(md),
|
||||
encoding='utf-8')
|
||||
print(f'Report: {OUT / "big4_only_pooled_report.md"}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user