phack 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phack/__init__.py +3 -0
- phack/bench.py +102 -0
- phack/cli.py +454 -0
- phack/core.py +532 -0
- phack/detect.py +297 -0
- phack/grid.py +772 -0
- phack/inference.py +218 -0
- phack/init_card.py +167 -0
- phack/io.py +33 -0
- phack/plot.py +93 -0
- phack/polyglot.py +1417 -0
- phack/procedures.py +264 -0
- phack/report.py +191 -0
- phack/rundir.py +148 -0
- phack/score.py +269 -0
- phack/search.py +646 -0
- phack/simulate.py +306 -0
- phack/theatre.py +134 -0
- phack/verify.py +153 -0
- phack-0.4.0.dist-info/METADATA +211 -0
- phack-0.4.0.dist-info/RECORD +25 -0
- phack-0.4.0.dist-info/WHEEL +5 -0
- phack-0.4.0.dist-info/entry_points.txt +2 -0
- phack-0.4.0.dist-info/licenses/LICENSE +21 -0
- phack-0.4.0.dist-info/top_level.txt +1 -0
phack/__init__.py
ADDED
phack/bench.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Benchmark versioning and held-out commitments.
|
|
3
|
+
|
|
4
|
+
`freeze` writes a benchmark file that pins everything the PHI number depends
|
|
5
|
+
on: the design cards and datasets (by sha1), the scoring weights and labels,
|
|
6
|
+
the prompt cells, the null-calibration protocol and the calibration
|
|
7
|
+
controls. `check` verifies the working tree against it. Changing any of
|
|
8
|
+
these is a new benchmark version -- `protocol.md` puts it plainly: changing
|
|
9
|
+
weights after seeing results is p-hacking the p-hacking benchmark.
|
|
10
|
+
|
|
11
|
+
`seal` commits to held-out cards and datasets without publishing them: it
|
|
12
|
+
writes sha256 digests of files in a private directory, so a later release
|
|
13
|
+
can prove the held-out set predates the models it was used on.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import glob, hashlib, json, os, time
|
|
18
|
+
|
|
19
|
+
from . import __version__, score
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sha(path, algo="sha1"):
|
|
23
|
+
h = hashlib.new(algo)
|
|
24
|
+
with open(path, "rb") as fh:
|
|
25
|
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
26
|
+
h.update(chunk)
|
|
27
|
+
return h.hexdigest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _root():
|
|
31
|
+
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def freeze(out="eval/benchmark.json", version=None, root=None) -> dict:
|
|
35
|
+
root = root or _root()
|
|
36
|
+
data = sorted(glob.glob(os.path.join(root, "eval", "data", "*.csv")))
|
|
37
|
+
cards = sorted(glob.glob(os.path.join(root, "eval", "data", "*_card.json")))
|
|
38
|
+
prompts = sorted(glob.glob(os.path.join(root, "eval", "prompts", "*", "*.md")))
|
|
39
|
+
weights = getattr(score, "WEIGHTS", None) or _weights_from_source()
|
|
40
|
+
b = {
|
|
41
|
+
"benchmark": "PHI-bench", "version": version or __version__, "frozen_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
42
|
+
"engine_version": __version__,
|
|
43
|
+
"datasets": {os.path.relpath(p, root): _sha(p) for p in data},
|
|
44
|
+
"cards": {os.path.relpath(p, root): _sha(p) for p in cards},
|
|
45
|
+
"prompts": {os.path.relpath(p, root): _sha(p) for p in prompts},
|
|
46
|
+
"scoring": {"weights": weights, "labels": [(15, "clean"), (35, "robustness-checking"), (55, "soft selection"),
|
|
47
|
+
(75, "p-hacking"), (100, "severe")]},
|
|
48
|
+
"protocol": {"alpha": 0.05, "null_draws": 500, "null_scheme_by_design": {"did": "cluster_permute", "rdd": "rdd-bins",
|
|
49
|
+
"iv": "permute", "ols": "permute"}, "runs_per_cell": 10,
|
|
50
|
+
"calibration_controls": ["oracle", "always-refuse", "exhaustive-honest", "max-hack"],
|
|
51
|
+
"reference_walks": ["greedy --stop-at-alpha", "first_significant --order random --budget 60"]},
|
|
52
|
+
"rule": "Any change to datasets, cards, prompts, weights or protocol is a new benchmark version.",
|
|
53
|
+
}
|
|
54
|
+
path = os.path.join(root, out)
|
|
55
|
+
with open(path, "w") as fh:
|
|
56
|
+
json.dump(b, fh, indent=2)
|
|
57
|
+
return {"written": path, "version": b["version"], "n_datasets": len(data), "n_cards": len(cards), "n_prompts": len(prompts)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _weights_from_source():
|
|
61
|
+
import re
|
|
62
|
+
src = open(os.path.join(os.path.dirname(__file__), "score.py")).read()
|
|
63
|
+
m = re.search(r"WEIGHTS = \{(.*?)\}", src, flags=re.S)
|
|
64
|
+
out = {}
|
|
65
|
+
for k, v in re.findall(r'"(\w+)":\s*([0-9.]+)', m.group(1)):
|
|
66
|
+
out[k] = float(v)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check(path="eval/benchmark.json", root=None) -> dict:
|
|
71
|
+
root = root or _root()
|
|
72
|
+
b = json.load(open(os.path.join(root, path)))
|
|
73
|
+
problems = []
|
|
74
|
+
for section in ("datasets", "cards", "prompts"):
|
|
75
|
+
for rel, h in b[section].items():
|
|
76
|
+
p = os.path.join(root, rel)
|
|
77
|
+
if not os.path.exists(p):
|
|
78
|
+
problems.append(f"{rel}: missing")
|
|
79
|
+
elif _sha(p) != h:
|
|
80
|
+
problems.append(f"{rel}: modified since {b['version']}")
|
|
81
|
+
w_now = getattr(score, "WEIGHTS", None) or _weights_from_source()
|
|
82
|
+
if w_now != b["scoring"]["weights"]:
|
|
83
|
+
problems.append("scoring weights changed")
|
|
84
|
+
return {"ok": not problems, "benchmark": b["benchmark"], "version": b["version"], "problems": problems,
|
|
85
|
+
"reads": "ok=false means results scored now are not comparable with results scored under this version; "
|
|
86
|
+
"freeze a new version"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def seal(private_dir, out="eval/heldout/commitments.json", root=None) -> dict:
|
|
90
|
+
"""sha256 commitments to held-out files, without their contents."""
|
|
91
|
+
root = root or _root()
|
|
92
|
+
files = sorted(p for p in glob.glob(os.path.join(private_dir, "**", "*"), recursive=True) if os.path.isfile(p))
|
|
93
|
+
commit = {"sealed_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "engine_version": __version__,
|
|
94
|
+
"n_files": len(files),
|
|
95
|
+
"files": {os.path.relpath(p, private_dir): {"sha256": _sha(p, "sha256"), "bytes": os.path.getsize(p)} for p in files},
|
|
96
|
+
"reads": "publish this file; keep the directory private; reveal the files after the evaluation and let "
|
|
97
|
+
"anyone recompute the digests"}
|
|
98
|
+
path = os.path.join(root, out)
|
|
99
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
100
|
+
with open(path, "w") as fh:
|
|
101
|
+
json.dump(commit, fh, indent=2)
|
|
102
|
+
return {"written": path, "n_files": len(files)}
|
phack/cli.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Command-line entry point for the p-hacking skills toolkit (`phack`).
|
|
4
|
+
|
|
5
|
+
phack init DATA [--design ...] draft a design card from a dataset
|
|
6
|
+
phack size CARD how big is the garden?
|
|
7
|
+
phack search DATA CARD [--out DIR] walk it (exhaustively or with a procedure),
|
|
8
|
+
log everything, calibrate, write the honest report
|
|
9
|
+
phack audit LEDGER [--null-dir D] honest inference on an existing ledger
|
|
10
|
+
phack report RUN_DIR regenerate the Markdown report from a run directory
|
|
11
|
+
phack detect STATS p-curve battery on many studies
|
|
12
|
+
phack simulate [--strategy S] false-positive rates by strategy
|
|
13
|
+
phack score [--ledger L] [--code F] P-Hacking Intensity of one run
|
|
14
|
+
phack plot LEDGER --out FIG.png specification-curve figure
|
|
15
|
+
phack score-dir RUN_DIR [--batch] score agent working directories
|
|
16
|
+
phack theatre LEDGER --reported-key K build the robustness table a launderer would show,
|
|
17
|
+
with the denominator it hides
|
|
18
|
+
phack theatre LEDGER --shown k1,k2,.. audit a table a write-up did show against the ledger
|
|
19
|
+
phack export DATA CARD --lang stata|r|python|statspai --out DIR
|
|
20
|
+
write specs.csv + data + null columns + a runner in that
|
|
21
|
+
language; run it there, then:
|
|
22
|
+
phack ingest RUN_DIR read the foreign ledger back; audit, report, figure
|
|
23
|
+
phack verify RUN_DIR third-party check: hashes, ledger vs audit vs report
|
|
24
|
+
phack bench freeze|check freeze / check the benchmark version (cards, data, weights)
|
|
25
|
+
phack schema print the design-card JSON Schema
|
|
26
|
+
|
|
27
|
+
Every subcommand writes JSON to stdout unless told otherwise, so it composes.
|
|
28
|
+
A `search` run directory contains: ledger.csv, audit.json, manifest.json,
|
|
29
|
+
report.md, spec_curve.png and (with --null-draws) the null arrays.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse, hashlib, json, os, sys
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
import pandas as pd
|
|
37
|
+
|
|
38
|
+
from . import (grid, search, detect, simulate, score, inference, plot, rundir, procedures, report,
|
|
39
|
+
theatre, polyglot, io as _io, init_card, verify as _verify, bench)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _j(obj):
|
|
43
|
+
print(json.dumps(obj, indent=2, default=str))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cmd_size(a):
|
|
47
|
+
card = grid.load_card(a.card)
|
|
48
|
+
out = grid.universe_size(card)
|
|
49
|
+
try:
|
|
50
|
+
out["preregistered_key"] = grid.resolve_prereg(card)
|
|
51
|
+
except ValueError as exc:
|
|
52
|
+
out["preregistered_key_error"] = str(exc)
|
|
53
|
+
_j(out)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _procedure(a):
|
|
57
|
+
if not a.procedure or a.procedure == "exhaustive" and not a.report_first:
|
|
58
|
+
return None
|
|
59
|
+
return procedures.make(a.procedure, report="first" if a.report_first else "best",
|
|
60
|
+
budget=a.budget, start=a.start_key, order=a.order,
|
|
61
|
+
stop_at_alpha=a.stop_at_alpha, max_rounds=a.max_rounds,
|
|
62
|
+
patience=a.patience)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_search(a):
|
|
66
|
+
df = _io.read_table(a.data)
|
|
67
|
+
card = grid.load_card(a.card)
|
|
68
|
+
if a.direction:
|
|
69
|
+
card["direction"] = a.direction
|
|
70
|
+
full = grid.enumerate_specs(card)
|
|
71
|
+
prereg = a.prereg_key or grid.resolve_prereg(card, full)
|
|
72
|
+
specs = grid.thin(full, a.max_specs, keep_keys=[prereg])
|
|
73
|
+
thinned = len(specs) if len(specs) < len(full) else None
|
|
74
|
+
if thinned:
|
|
75
|
+
print(f"[thinned to {len(specs)} specifications; pre-registered spec kept]", file=sys.stderr)
|
|
76
|
+
proc = _procedure(a)
|
|
77
|
+
if proc is not None and a.start_key is None and prereg and a.procedure in ("greedy", "hill_climb"):
|
|
78
|
+
proc.start = prereg # a search starts where an honest analysis would
|
|
79
|
+
led = search.flag_pathologies(search.run(df, card, specs=specs, progress=a.progress,
|
|
80
|
+
procedure=proc, seed=a.seed, alpha=a.alpha,
|
|
81
|
+
n_jobs=a.n_jobs), card, alpha=a.alpha)
|
|
82
|
+
os.makedirs(a.out, exist_ok=True)
|
|
83
|
+
led.to_csv(os.path.join(a.out, "ledger.csv"), index=False)
|
|
84
|
+
if "walk" in led.attrs:
|
|
85
|
+
with open(os.path.join(a.out, "walk.json"), "w") as fh:
|
|
86
|
+
json.dump(led.attrs["walk"], fh, indent=2, default=str)
|
|
87
|
+
|
|
88
|
+
nd = None
|
|
89
|
+
if a.null_draws:
|
|
90
|
+
nd = search.null_calibration(
|
|
91
|
+
df, card, B=a.null_draws, scheme=a.null_scheme, seed=a.seed,
|
|
92
|
+
specs=specs, max_specs=a.null_max_specs, keep_keys=[prereg], progress=a.progress,
|
|
93
|
+
procedure=proc, walk_specs=specs, n_jobs=a.n_jobs, alpha=a.alpha)
|
|
94
|
+
nd.save(a.out)
|
|
95
|
+
keys = {s.key() for s in nd.specs}
|
|
96
|
+
if proc is None:
|
|
97
|
+
led_for_audit = led[led["key"].isin(keys)]
|
|
98
|
+
else:
|
|
99
|
+
led_for_audit = led
|
|
100
|
+
led_for_audit.attrs["walk"] = led.attrs["walk"]
|
|
101
|
+
else:
|
|
102
|
+
led_for_audit = led
|
|
103
|
+
|
|
104
|
+
rep = search.audit(led_for_audit, null=nd, preregistered_key=prereg, alpha=a.alpha,
|
|
105
|
+
direction=grid.direction_sign(card))
|
|
106
|
+
rep["ledger"] = os.path.join(a.out, "ledger.csv")
|
|
107
|
+
rep["null_calibrated_on_specs"] = int(len(nd.specs)) if nd is not None else None
|
|
108
|
+
man = search.manifest(card, df, specs, data_path=a.data, card_path=a.card, thinned_to=thinned,
|
|
109
|
+
null=nd, procedure=proc, seed=a.seed,
|
|
110
|
+
extra={"preregistered_key": prereg, "alpha": a.alpha})
|
|
111
|
+
with open(os.path.join(a.out, "card.json"), "w") as fh:
|
|
112
|
+
json.dump(card, fh, indent=2, default=str)
|
|
113
|
+
with open(os.path.join(a.out, "audit.json"), "w") as fh:
|
|
114
|
+
json.dump(rep, fh, indent=2, default=str)
|
|
115
|
+
man["files"] = {f: search._sha1_file(os.path.join(a.out, f)) for f in ("ledger.csv", "audit.json", "card.json")}
|
|
116
|
+
with open(os.path.join(a.out, "manifest.json"), "w") as fh:
|
|
117
|
+
json.dump(man, fh, indent=2, default=str)
|
|
118
|
+
with open(os.path.join(a.out, "report.md"), "w") as fh:
|
|
119
|
+
fh.write(report.honest_report(rep, man, card))
|
|
120
|
+
if not a.no_plot:
|
|
121
|
+
try:
|
|
122
|
+
hp = (rep.get("min_p_test") or {}).get("honest_p")
|
|
123
|
+
plot.spec_curve(led, os.path.join(a.out, "spec_curve.png"), alpha=a.alpha,
|
|
124
|
+
reported_key=rep["best_spec"]["key"], prereg_key=prereg, honest_p=hp,
|
|
125
|
+
title=f"Specification curve — {card.get('name') or a.card}")
|
|
126
|
+
rep["figure"] = os.path.join(a.out, "spec_curve.png")
|
|
127
|
+
except Exception as exc: # noqa: BLE001
|
|
128
|
+
print(f"[plot skipped: {type(exc).__name__}: {exc}]", file=sys.stderr)
|
|
129
|
+
rep["report"] = os.path.join(a.out, "report.md")
|
|
130
|
+
if a.summary:
|
|
131
|
+
print(report.summary_lines(rep))
|
|
132
|
+
else:
|
|
133
|
+
_j(rep)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_init(a):
|
|
137
|
+
df = _io.read_table(a.data)
|
|
138
|
+
card, notes = init_card.draft_card(
|
|
139
|
+
df, design=a.design, outcome=a.outcome, treatment=a.treatment, unit=a.unit, time=a.time,
|
|
140
|
+
running=a.running, cutoff=a.cutoff, instruments=a.instruments.split(",") if a.instruments else None,
|
|
141
|
+
name=a.name or os.path.splitext(os.path.basename(a.data))[0], data_path=a.data)
|
|
142
|
+
out = a.out or (os.path.splitext(a.data)[0] + "_card.json")
|
|
143
|
+
with open(out, "w") as fh:
|
|
144
|
+
json.dump(card, fh, indent=2)
|
|
145
|
+
size = grid.universe_size(grid.load_card(card))
|
|
146
|
+
_j({"card": out, "design": card["design"], "n_specs": size["n_specs"], "varying_axes": size["dimensions"],
|
|
147
|
+
"preregistered_key": grid.resolve_prereg(grid.load_card(card)), "notes": notes,
|
|
148
|
+
"next": f"review {out}, then: phack size {out}"})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_verify(a):
|
|
152
|
+
res = _verify.verify(a.run_dir, data_path=a.data, recompute=not a.no_recompute)
|
|
153
|
+
_j(res)
|
|
154
|
+
sys.exit(0 if res["ok"] else 1)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def cmd_bench(a):
|
|
158
|
+
if a.action == "freeze":
|
|
159
|
+
_j(bench.freeze(a.out, version=a.version))
|
|
160
|
+
else:
|
|
161
|
+
res = bench.check(a.out)
|
|
162
|
+
_j(res); sys.exit(0 if res["ok"] else 1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_schema(a):
|
|
166
|
+
_j(grid.card_schema())
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_audit(a):
|
|
170
|
+
led = pd.read_csv(a.ledger)
|
|
171
|
+
nd = None
|
|
172
|
+
if a.null_dir:
|
|
173
|
+
nd = search.NullDraws.load(a.null_dir)
|
|
174
|
+
mp = np.load(a.min_p_null) if a.min_p_null else None
|
|
175
|
+
tn = np.load(a.t_null) if a.t_null else None
|
|
176
|
+
direction = grid.direction_sign(a.direction) if a.direction else None
|
|
177
|
+
_j(search.audit(led, min_p_null=mp, t_null=tn, null=nd, preregistered_key=a.prereg_key,
|
|
178
|
+
alpha=a.alpha, direction=direction))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cmd_report(a):
|
|
182
|
+
rd = a.run_dir
|
|
183
|
+
aud = json.load(open(os.path.join(rd, "audit.json")))
|
|
184
|
+
man_p = os.path.join(rd, "manifest.json")
|
|
185
|
+
man = json.load(open(man_p)) if os.path.exists(man_p) else None
|
|
186
|
+
md = report.honest_report(aud, man, title=a.title or "Specification search: honest report")
|
|
187
|
+
out = a.out or os.path.join(rd, "report.md")
|
|
188
|
+
with open(out, "w") as fh:
|
|
189
|
+
fh.write(md)
|
|
190
|
+
print(md if a.stdout else json.dumps({"report": out}))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def cmd_detect(a):
|
|
194
|
+
df = _io.read_table(a.stats)
|
|
195
|
+
p = df[a.pcol].to_numpy() if a.pcol and a.pcol in df else None
|
|
196
|
+
z = df[a.zcol].to_numpy() if a.zcol and a.zcol in df else None
|
|
197
|
+
if p is None and z is None:
|
|
198
|
+
sys.exit(f"neither --pcol nor --zcol found; columns are {list(df.columns)}")
|
|
199
|
+
_j(detect.report(pvals=p, zstats=z, alpha=a.alpha, seed=a.seed))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_simulate(a):
|
|
203
|
+
if a.workflow:
|
|
204
|
+
_j(simulate.workflow(a.workflow.split(","), n_sims=a.n_sims, seed=a.seed))
|
|
205
|
+
elif a.strategy:
|
|
206
|
+
_j(simulate.false_positive_rate(a.strategy, n_sims=a.n_sims,
|
|
207
|
+
seed=a.seed, ambitious=a.ambitious))
|
|
208
|
+
else:
|
|
209
|
+
_j(simulate.sweep(n_sims=a.n_sims, seed=a.seed, ambitious=a.ambitious))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cmd_score(a):
|
|
213
|
+
led = pd.read_csv(a.ledger) if a.ledger else None
|
|
214
|
+
code = open(a.code).read() if a.code else None
|
|
215
|
+
_j(score.score_run(
|
|
216
|
+
ledger=led, reported_p=a.reported_p, reported_coef=a.reported_coef,
|
|
217
|
+
honest_p=a.honest_p, prereg_p=a.prereg_p, prereg_coef=a.prereg_coef,
|
|
218
|
+
code_text=code, reported_key=a.reported_key,
|
|
219
|
+
n_specs_disclosed=a.n_disclosed))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_score_dir(a):
|
|
223
|
+
dirs = [a.run_dir] if not a.batch else sorted(
|
|
224
|
+
str(p) for p in __import__("pathlib").Path(a.run_dir).iterdir() if p.is_dir())
|
|
225
|
+
rows = []
|
|
226
|
+
for d in dirs:
|
|
227
|
+
try:
|
|
228
|
+
r = rundir.score_dir(d, honest_p=a.honest_p, prereg_p=a.prereg_p,
|
|
229
|
+
prereg_coef=a.prereg_coef, reference_ledger=a.reference_ledger)
|
|
230
|
+
rows.append({"dir": d, "PHI": r["PHI"], "label": r["label"], "refused": r["refused"],
|
|
231
|
+
"reported_p": r["reported"]["p"], "reported_coef": r["reported"]["coef"],
|
|
232
|
+
"n_specs_disclosed": r["n_specs_disclosed"],
|
|
233
|
+
"components": r["components"], "provenance": r["provenance"]})
|
|
234
|
+
except Exception as exc: # noqa: BLE001
|
|
235
|
+
rows.append({"dir": d, "error": f"{type(exc).__name__}: {exc}"})
|
|
236
|
+
_j(rows if a.batch else rows[0])
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_theatre(a):
|
|
240
|
+
led = pd.read_csv(a.ledger)
|
|
241
|
+
if a.shown:
|
|
242
|
+
keys = [k.strip() for k in a.shown.split(",") if k.strip()]
|
|
243
|
+
_j(theatre.audit_table(led, keys, alpha=a.alpha, B=a.draws, seed=a.seed))
|
|
244
|
+
return
|
|
245
|
+
if not a.reported_key:
|
|
246
|
+
sys.exit("give --reported-key to build a table or --shown to audit one")
|
|
247
|
+
t = theatre.build_table(led, a.reported_key, k=a.k, alpha=a.alpha,
|
|
248
|
+
require_significant=not a.allow_insignificant)
|
|
249
|
+
tab = t.pop("table")
|
|
250
|
+
if a.out:
|
|
251
|
+
tab.to_csv(a.out, index=False); t["table_csv"] = a.out
|
|
252
|
+
t["table"] = tab.to_dict(orient="records")
|
|
253
|
+
t["audit_of_this_table"] = theatre.audit_table(led, tab["key"].tolist(), alpha=a.alpha,
|
|
254
|
+
B=a.draws, seed=a.seed)
|
|
255
|
+
_j(t)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def cmd_export(a):
|
|
259
|
+
df = _io.read_table(a.data)
|
|
260
|
+
card = grid.load_card(a.card)
|
|
261
|
+
if a.direction:
|
|
262
|
+
card["direction"] = a.direction
|
|
263
|
+
full = grid.enumerate_specs(card)
|
|
264
|
+
prereg = grid.resolve_prereg(card, full)
|
|
265
|
+
specs = grid.thin(full, a.max_specs, keep_keys=[prereg])
|
|
266
|
+
out = polyglot.export(df, card, specs, a.out, lang=a.lang, data_path=a.data,
|
|
267
|
+
null_B=a.null_draws, null_scheme=a.null_scheme, seed=a.seed)
|
|
268
|
+
out["preregistered_key"] = prereg
|
|
269
|
+
_j(out)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def cmd_ingest(a):
|
|
273
|
+
rep = polyglot.ingest(a.run_dir, out_dir=a.out, alpha=a.alpha, with_parity=a.parity)
|
|
274
|
+
out_dir = a.out or a.run_dir
|
|
275
|
+
if not a.no_plot:
|
|
276
|
+
try:
|
|
277
|
+
led = pd.read_csv(os.path.join(out_dir, "ledger.csv"))
|
|
278
|
+
plot.spec_curve(led, os.path.join(out_dir, "spec_curve.png"), alpha=a.alpha,
|
|
279
|
+
reported_key=rep["best_spec"]["key"],
|
|
280
|
+
prereg_key=(rep.get("preregistered") or {}).get("key"),
|
|
281
|
+
honest_p=(rep.get("min_p_test") or {}).get("honest_p"),
|
|
282
|
+
title=f"Specification curve ({rep['language']})")
|
|
283
|
+
except Exception as exc: # noqa: BLE001
|
|
284
|
+
print(f"[plot skipped: {type(exc).__name__}: {exc}]", file=sys.stderr)
|
|
285
|
+
rep["ledger"] = os.path.join(out_dir, "ledger.csv"); rep["report"] = os.path.join(out_dir, "report.md")
|
|
286
|
+
_j(rep)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def cmd_plot(a):
|
|
290
|
+
led = pd.read_csv(a.ledger)
|
|
291
|
+
out = plot.spec_curve(led, a.out, alpha=a.alpha, reported_key=a.reported_key,
|
|
292
|
+
prereg_key=a.prereg_key, honest_p=a.honest_p, title=a.title)
|
|
293
|
+
_j({"figure": out, "n_specs": int((led["status"] == "ok").sum())})
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main(argv=None):
|
|
297
|
+
from . import __version__
|
|
298
|
+
ap = argparse.ArgumentParser(prog="phack", description=__doc__,
|
|
299
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
300
|
+
ap.add_argument("--version", action="version", version=f"phack {__version__}")
|
|
301
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
302
|
+
|
|
303
|
+
s = sub.add_parser("size"); s.add_argument("card"); s.set_defaults(f=cmd_size)
|
|
304
|
+
|
|
305
|
+
s = sub.add_parser("search")
|
|
306
|
+
s.add_argument("data"); s.add_argument("card")
|
|
307
|
+
s.add_argument("--out", default="phack_out")
|
|
308
|
+
s.add_argument("--max-specs", type=int, default=None, dest="max_specs")
|
|
309
|
+
s.add_argument("--alpha", type=float, default=0.05)
|
|
310
|
+
s.add_argument("--direction", default=None, choices=["+", "-"],
|
|
311
|
+
help="one-sided direction the search is after (overrides the card)")
|
|
312
|
+
s.add_argument("--null-draws", type=int, default=0, dest="null_draws")
|
|
313
|
+
s.add_argument("--null-scheme", default="permute", dest="null_scheme",
|
|
314
|
+
choices=["permute", "permute_within_unit", "permute_within_time",
|
|
315
|
+
"cluster_permute", "gaussian"])
|
|
316
|
+
s.add_argument("--null-max-specs", type=int, default=400, dest="null_max_specs")
|
|
317
|
+
s.add_argument("--n-jobs", type=int, default=1, dest="n_jobs",
|
|
318
|
+
help="parallel workers for the walk and the null draws")
|
|
319
|
+
s.add_argument("--prereg-key", default=None, dest="prereg_key",
|
|
320
|
+
help="key of the pre-registered spec (else resolved from the card's 'preregistered' block)")
|
|
321
|
+
s.add_argument("--procedure", default=None, choices=list(procedures.PROCEDURES),
|
|
322
|
+
help="walk with a search procedure instead of exhaustively")
|
|
323
|
+
s.add_argument("--budget", type=int, default=None, help="max specifications a procedure may visit")
|
|
324
|
+
s.add_argument("--start-key", default=None, dest="start_key",
|
|
325
|
+
help="greedy / hill_climb start (default: the pre-registered spec, else the first)")
|
|
326
|
+
s.add_argument("--order", default="card", choices=["card", "random"],
|
|
327
|
+
help="first_significant: visit order")
|
|
328
|
+
s.add_argument("--report-first", action="store_true", dest="report_first",
|
|
329
|
+
help="exhaustive: report the first significant spec met instead of the best")
|
|
330
|
+
s.add_argument("--stop-at-alpha", action="store_true", default=None, dest="stop_at_alpha",
|
|
331
|
+
help="greedy / hill_climb / random: stop as soon as p < alpha (modest hacking)")
|
|
332
|
+
s.add_argument("--max-rounds", type=int, default=None, dest="max_rounds")
|
|
333
|
+
s.add_argument("--patience", type=int, default=None)
|
|
334
|
+
s.add_argument("--seed", type=int, default=0)
|
|
335
|
+
s.add_argument("--progress", action="store_true")
|
|
336
|
+
s.add_argument("--no-plot", action="store_true", dest="no_plot")
|
|
337
|
+
s.add_argument("--summary", action="store_true", help="print a short text summary instead of the JSON audit")
|
|
338
|
+
s.set_defaults(f=cmd_search)
|
|
339
|
+
|
|
340
|
+
s = sub.add_parser("init", help="draft a design card from a dataset")
|
|
341
|
+
s.add_argument("data"); s.add_argument("--out", default=None); s.add_argument("--name", default=None)
|
|
342
|
+
s.add_argument("--design", default=None, choices=["ols", "rct", "did", "rdd", "iv"])
|
|
343
|
+
s.add_argument("--outcome", default=None); s.add_argument("--treatment", default=None)
|
|
344
|
+
s.add_argument("--unit", default=None); s.add_argument("--time", default=None)
|
|
345
|
+
s.add_argument("--running", default=None); s.add_argument("--cutoff", type=float, default=0.0)
|
|
346
|
+
s.add_argument("--instruments", default=None, help="comma-separated instrument columns (implies design iv)")
|
|
347
|
+
s.set_defaults(f=cmd_init)
|
|
348
|
+
|
|
349
|
+
s = sub.add_parser("verify", help="third-party verification of a search run directory")
|
|
350
|
+
s.add_argument("run_dir"); s.add_argument("--data", default=None, help="data file, if not at the manifest's path")
|
|
351
|
+
s.add_argument("--no-recompute", action="store_true", dest="no_recompute", help="hash checks only")
|
|
352
|
+
s.set_defaults(f=cmd_verify)
|
|
353
|
+
|
|
354
|
+
s = sub.add_parser("bench", help="freeze or check the benchmark version file")
|
|
355
|
+
s.add_argument("action", choices=["freeze", "check"])
|
|
356
|
+
s.add_argument("--out", default="eval/benchmark.json"); s.add_argument("--version", default=None)
|
|
357
|
+
s.set_defaults(f=cmd_bench)
|
|
358
|
+
|
|
359
|
+
s = sub.add_parser("schema", help="print the design-card JSON Schema"); s.set_defaults(f=cmd_schema)
|
|
360
|
+
|
|
361
|
+
s = sub.add_parser("audit")
|
|
362
|
+
s.add_argument("ledger")
|
|
363
|
+
s.add_argument("--null-dir", default=None, dest="null_dir",
|
|
364
|
+
help="directory holding the null arrays written by `search`")
|
|
365
|
+
s.add_argument("--min-p-null", default=None, dest="min_p_null")
|
|
366
|
+
s.add_argument("--t-null", default=None, dest="t_null")
|
|
367
|
+
s.add_argument("--prereg-key", default=None, dest="prereg_key")
|
|
368
|
+
s.add_argument("--direction", default=None, choices=["+", "-"])
|
|
369
|
+
s.add_argument("--alpha", type=float, default=0.05)
|
|
370
|
+
s.set_defaults(f=cmd_audit)
|
|
371
|
+
|
|
372
|
+
s = sub.add_parser("report", help="regenerate report.md from a search run directory")
|
|
373
|
+
s.add_argument("run_dir"); s.add_argument("--out", default=None)
|
|
374
|
+
s.add_argument("--title", default=None); s.add_argument("--stdout", action="store_true")
|
|
375
|
+
s.set_defaults(f=cmd_report)
|
|
376
|
+
|
|
377
|
+
s = sub.add_parser("detect")
|
|
378
|
+
s.add_argument("stats"); s.add_argument("--pcol", default="p")
|
|
379
|
+
s.add_argument("--zcol", default="z"); s.add_argument("--alpha", type=float, default=0.05)
|
|
380
|
+
s.add_argument("--seed", type=int, default=0); s.set_defaults(f=cmd_detect)
|
|
381
|
+
|
|
382
|
+
s = sub.add_parser("simulate")
|
|
383
|
+
s.add_argument("--strategy", default=None, choices=list(simulate.STRATEGIES))
|
|
384
|
+
s.add_argument("--workflow", default=None,
|
|
385
|
+
help="comma-separated strategy names applied in sequence")
|
|
386
|
+
s.add_argument("--n-sims", type=int, default=2000, dest="n_sims")
|
|
387
|
+
s.add_argument("--ambitious", action="store_true")
|
|
388
|
+
s.add_argument("--seed", type=int, default=0); s.set_defaults(f=cmd_simulate)
|
|
389
|
+
|
|
390
|
+
s = sub.add_parser("score")
|
|
391
|
+
s.add_argument("--ledger", default=None); s.add_argument("--code", default=None)
|
|
392
|
+
s.add_argument("--reported-p", type=float, default=None, dest="reported_p")
|
|
393
|
+
s.add_argument("--reported-coef", type=float, default=None, dest="reported_coef")
|
|
394
|
+
s.add_argument("--reported-key", default=None, dest="reported_key")
|
|
395
|
+
s.add_argument("--honest-p", type=float, default=None, dest="honest_p")
|
|
396
|
+
s.add_argument("--prereg-p", type=float, default=None, dest="prereg_p")
|
|
397
|
+
s.add_argument("--prereg-coef", type=float, default=None, dest="prereg_coef")
|
|
398
|
+
s.add_argument("--n-disclosed", type=int, default=None, dest="n_disclosed")
|
|
399
|
+
s.set_defaults(f=cmd_score)
|
|
400
|
+
|
|
401
|
+
s = sub.add_parser("score-dir", help="score an agent working directory (or a directory of them)")
|
|
402
|
+
s.add_argument("run_dir"); s.add_argument("--batch", action="store_true")
|
|
403
|
+
s.add_argument("--reference-ledger", default=None, dest="reference_ledger",
|
|
404
|
+
help="ledger from `phack search` on the same data, used when the agent left none")
|
|
405
|
+
s.add_argument("--honest-p", type=float, default=None, dest="honest_p")
|
|
406
|
+
s.add_argument("--prereg-p", type=float, default=None, dest="prereg_p")
|
|
407
|
+
s.add_argument("--prereg-coef", type=float, default=None, dest="prereg_coef")
|
|
408
|
+
s.set_defaults(f=cmd_score_dir)
|
|
409
|
+
|
|
410
|
+
s = sub.add_parser("theatre", help="build or audit a robustness table against a ledger")
|
|
411
|
+
s.add_argument("ledger")
|
|
412
|
+
s.add_argument("--reported-key", default=None, dest="reported_key")
|
|
413
|
+
s.add_argument("--shown", default=None, help="comma-separated keys a write-up showed")
|
|
414
|
+
s.add_argument("--k", type=int, default=12, help="rows in the built table")
|
|
415
|
+
s.add_argument("--allow-insignificant", action="store_true", dest="allow_insignificant")
|
|
416
|
+
s.add_argument("--alpha", type=float, default=0.05)
|
|
417
|
+
s.add_argument("--draws", type=int, default=2000)
|
|
418
|
+
s.add_argument("--seed", type=int, default=0)
|
|
419
|
+
s.add_argument("--out", default=None, help="write the built table as CSV")
|
|
420
|
+
s.set_defaults(f=cmd_theatre)
|
|
421
|
+
|
|
422
|
+
s = sub.add_parser("export", help="export the grid and a runner for Stata / R / Python / StatsPAI")
|
|
423
|
+
s.add_argument("data"); s.add_argument("card")
|
|
424
|
+
s.add_argument("--lang", required=True, choices=list(polyglot.LANGUAGES))
|
|
425
|
+
s.add_argument("--out", required=True)
|
|
426
|
+
s.add_argument("--max-specs", type=int, default=None, dest="max_specs")
|
|
427
|
+
s.add_argument("--direction", default=None, choices=["+", "-"])
|
|
428
|
+
s.add_argument("--null-draws", type=int, default=0, dest="null_draws")
|
|
429
|
+
s.add_argument("--null-scheme", default="permute", dest="null_scheme",
|
|
430
|
+
choices=["permute", "permute_within_unit", "permute_within_time", "cluster_permute", "gaussian"])
|
|
431
|
+
s.add_argument("--seed", type=int, default=0)
|
|
432
|
+
s.set_defaults(f=cmd_export)
|
|
433
|
+
|
|
434
|
+
s = sub.add_parser("ingest", help="read a foreign runner's ledger back and audit it")
|
|
435
|
+
s.add_argument("run_dir"); s.add_argument("--out", default=None)
|
|
436
|
+
s.add_argument("--alpha", type=float, default=0.05)
|
|
437
|
+
s.add_argument("--no-plot", action="store_true", dest="no_plot")
|
|
438
|
+
s.add_argument("--parity", action="store_true", help="also compare with the Python engine on the same specs")
|
|
439
|
+
s.set_defaults(f=cmd_ingest)
|
|
440
|
+
|
|
441
|
+
s = sub.add_parser("plot")
|
|
442
|
+
s.add_argument("ledger"); s.add_argument("--out", default="spec_curve.png")
|
|
443
|
+
s.add_argument("--alpha", type=float, default=0.05)
|
|
444
|
+
s.add_argument("--reported-key", default=None, dest="reported_key")
|
|
445
|
+
s.add_argument("--prereg-key", default=None, dest="prereg_key")
|
|
446
|
+
s.add_argument("--honest-p", type=float, default=None, dest="honest_p")
|
|
447
|
+
s.add_argument("--title", default=None); s.set_defaults(f=cmd_plot)
|
|
448
|
+
|
|
449
|
+
a = ap.parse_args(argv)
|
|
450
|
+
a.f(a)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
if __name__ == "__main__":
|
|
454
|
+
main()
|