diff --git a/.github/workflows/benchmark-frontend.yml b/.github/workflows/benchmark-frontend.yml index 1d7a23532..d6d51e2e6 100644 --- a/.github/workflows/benchmark-frontend.yml +++ b/.github/workflows/benchmark-frontend.yml @@ -12,7 +12,10 @@ on: - 'dev/tests/test_benchmark_cv_source.py' - 'dev/tests/test_cv_runner_instrumentation.py' - 'dev/tests/test_frontend_contracts.py' + - 'dev/tests/test_frontend_cv_determinism.py' - 'dev/tests/test_frontend_domain_coverage.py' + - 'dev/tests/test_panel_stage_b_frontend_source.py' + - 'dev/tests/test_panel_stage_b_applicable_hausman_parser.py' - 'dev/tests/fixtures/benchmark_frontend/**' - 'frontend/**' - 'docs/assets/benchmarks/**' @@ -28,7 +31,10 @@ on: - 'dev/tests/test_benchmark_cv_source.py' - 'dev/tests/test_cv_runner_instrumentation.py' - 'dev/tests/test_frontend_contracts.py' + - 'dev/tests/test_frontend_cv_determinism.py' - 'dev/tests/test_frontend_domain_coverage.py' + - 'dev/tests/test_panel_stage_b_frontend_source.py' + - 'dev/tests/test_panel_stage_b_applicable_hausman_parser.py' - 'dev/tests/fixtures/benchmark_frontend/**' - 'frontend/**' - 'docs/assets/benchmarks/**' @@ -61,7 +67,10 @@ jobs: dev/tests/test_benchmark_cv_source.py \ dev/tests/test_cv_runner_instrumentation.py \ dev/tests/test_frontend_contracts.py \ - dev/tests/test_frontend_domain_coverage.py -v + dev/tests/test_frontend_cv_determinism.py \ + dev/tests/test_frontend_domain_coverage.py \ + dev/tests/test_panel_stage_b_frontend_source.py \ + dev/tests/test_panel_stage_b_applicable_hausman_parser.py -v - name: Validate generator output run: python dev/benchmarks/generate_benchmark_data.py --check --strict-sources diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e496c907c..322fc0111 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -183,8 +183,31 @@ jobs: python -m pytest \ dev/tests/test_logistic_cv_torch_dtype.py \ dev/tests/test_panel_stage_a_torch_cpu.py \ + dev/tests/test_panel_stage_b_torch_cpu.py \ -q --tb=short + panel-stage-b-linearmodels: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install current external panel reference + run: | + python -m pip install --upgrade pip + python -m pip install -e . "linearmodels==7.0" pytest + - name: Confirm linearmodels reference version + run: | + python - <<'PY' + import linearmodels + assert linearmodels.__version__ == "7.0" + print("linearmodels", linearmodels.__version__) + PY + - name: Run Panel Stage B external alignment + run: python -m pytest dev/tests/test_panel_stage_b_linearmodels.py -q --tb=short + static-contracts: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 732d6bfc5..f65151c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to statgpu are documented here, organized by release and dat ## 2026-08-08 +### PR #122 — Panel Tier-1 diagnostics Stage B + +- Added structured Panel `fit_statistics_` with parameter-based within/between/overall R², adjusted R², and classical model F statistics while preserving Stage-A inference and legacy df/R² attributes. +- Added classical pooling F, one-way entity Breusch-Pagan LM (including Baltagi-Li unbalanced panels), and one-way classical FE-vs-RE Hausman with explicit applicability diagnostics. +- Added NumPy/CuPy/Torch coverage, formula-row alignment, maintained Torch 2.0 CPU tests, executable linearmodels 7.0 definition alignment, and an exact-head physical GPU acceptance runner that rechecks coefficient inference as well as Stage-B diagnostics. + ### PR #121 — CuPy inverse-quantile LUT correctness - Fixed CuPy `betaincinv` and `gammaincinv` LUT cache tuple ordering so inverse quantiles no longer collapse to boundary values and downstream confidence intervals retain their correct width. diff --git a/RELEASING.md b/RELEASING.md index dd9397aa1..e4f08882b 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -80,6 +80,15 @@ contracts, documentation contracts, and the Python 3.9–3.12 regression matrix. For changes affecting CuPy, Torch, inference, device routing, or performance, record physical-GPU acceptance on the exact release source commit. +Physical evidence is tied to both the numerical implementation and the validator +that defines the acceptance matrix. If a physical-validation runner changes after +an artifact has been accepted—for example, because review adds a previously +uncovered inference branch—the old artifact remains useful historical evidence +but no longer proves the new acceptance contract. Return the affected pull +request to a pending/draft state, rerun the changed validator on an exact clean +candidate head, and only then promote new canonical evidence or restore a +Ready/merge-ready conclusion. + ### Package validation The `Release package validation` workflow automatically: diff --git a/dev/benchmarks/benchmark_coverage_matrix.json b/dev/benchmarks/benchmark_coverage_matrix.json index 01238a5a3..296b0514b 100644 --- a/dev/benchmarks/benchmark_coverage_matrix.json +++ b/dev/benchmarks/benchmark_coverage_matrix.json @@ -82,18 +82,22 @@ }, { "capability_id": "panel-estimation", - "label": "Panel estimation coverage", + "label": "Panel estimation and diagnostics coverage", "status": "partial_canonical", "source_ids": [ - "new-modules-20260624-bcbdb676223b" + "new-modules-20260624-bcbdb676223b", + "panel-stage-b-pr122-20260809-2056f836bfe2" ], "representative_dimensions": [ "estimator", "backend", - "aligned_scale" + "aligned_scale", + "physical_validation", + "diagnostics", + "inference_regression" ], "issue": "#108", - "disposition": "Aligned PanelOLS and RandomEffects evidence exists; additional estimators and covariance variants remain open." + "disposition": "June timing rows cover aligned PanelOLS and RandomEffects. PR #122 adds canonical validation-only CuPy/Torch evidence for the 17-case Stage-B estimator matrix, five Hausman diagnostics per backend (including a physically applicable nonzero-effect statistic/p-value/df path), backend provenance, Stage-A inference regression, and the disconnected two-way FE df=1 physical boundary; broader performance/covariance timing remains open." }, { "capability_id": "gam-nonparametric", diff --git a/dev/benchmarks/benchmark_panel_stage_b_identity_overhead.py b/dev/benchmarks/benchmark_panel_stage_b_identity_overhead.py new file mode 100644 index 000000000..9b9ee7ef1 --- /dev/null +++ b/dev/benchmarks/benchmark_panel_stage_b_identity_overhead.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Measure Stage-B full-content identity overhead on physical GPU backends. + +This benchmark isolates the cost of the collision-resistant X/y SHA-256 used by +Hausman sample identity. It compares ordinary PanelOLS/RandomEffects fit time +against the same fit with only ``_full_content_digest`` replaced by a constant +stub. Numerical work, low-order audit reductions, estimator setup, and all other +Stage-B code remain unchanged in the baseline. + +The script is intentionally separate from ``validate_panel_stage_b_gpu.py``: +that runner remains correctness/provenance-only and its frontend source must not +acquire inferred timing or speedup fields. +""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import platform +import subprocess +import time +from pathlib import Path + +import numpy as np + +import statgpu.panel._diagnostics as diagnostics +from statgpu.panel import PanelOLS, RandomEffects + + +def _git_sha() -> str: + return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + + +def _git_status_porcelain() -> str: + return subprocess.check_output(["git", "status", "--porcelain"], text=True) + + +def _version(name: str): + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return None + + +def _parse_scales(value: str): + out = [] + for item in value.split(","): + n_text, k_text = item.strip().lower().split("x", 1) + n, k = int(n_text), int(k_text) + if n <= 0 or k <= 0: + raise ValueError("benchmark scales must be positive NxK pairs") + out.append((n, k)) + return out + + +def _sync(backend: str): + if backend == "cupy": + import cupy as cp + + cp.cuda.Stream.null.synchronize() + elif backend == "torch": + import torch + + torch.cuda.synchronize() + + +def _to_backend(X, y, entity, backend: str): + if backend == "cupy": + import cupy as cp + + return ( + cp.asarray(X), + cp.asarray(y), + cp.asarray(entity, dtype=cp.int64), + ) + if backend == "torch": + import torch + + return ( + torch.as_tensor(X, dtype=torch.float64, device="cuda"), + torch.as_tensor(y, dtype=torch.float64, device="cuda"), + torch.as_tensor(entity, dtype=torch.int64, device="cuda"), + ) + raise ValueError(backend) + + +def _device_arg(backend: str): + return {"cupy": "cuda", "torch": "torch"}[backend] + + +def _dataset(n: int, k: int, seed: int): + rng = np.random.default_rng(seed) + X = rng.normal(size=(n, k)).astype(np.float64) + beta = np.linspace(0.2, 0.8, k, dtype=np.float64) + entity = np.arange(n, dtype=np.int64) // 20 + n_entities = int(entity.max()) + 1 + alpha = np.linspace(-0.5, 0.5, n_entities, dtype=np.float64)[entity] + y = X @ beta + alpha + rng.normal(scale=0.2, size=n) + return X, y.astype(np.float64), entity + + +def _fit(model_name: str, X, y, entity, backend: str): + device = _device_arg(backend) + if model_name == "PanelOLS": + model = PanelOLS(entity_effects=True, cov_type="nonrobust", device=device) + elif model_name == "RandomEffects": + model = RandomEffects(device=device) + else: + raise ValueError(model_name) + model.fit(X, y, entity_ids=entity) + return model + + +def _timed_fit(model_name, X, y, entity, backend, *, disable_digest: bool): + original = diagnostics._full_content_digest + if disable_digest: + diagnostics._full_content_digest = lambda _X, _y: "0" * 64 + try: + _sync(backend) + start = time.perf_counter() + _fit(model_name, X, y, entity, backend) + _sync(backend) + return time.perf_counter() - start + finally: + diagnostics._full_content_digest = original + + +def _median(values): + return float(np.median(np.asarray(values, dtype=np.float64))) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--expected-sha", required=True) + parser.add_argument("--backends", default="cupy,torch") + parser.add_argument( + "--scales", + default="10000x2,100000x2,100000x10,500000x2", + help="comma-separated NxK pairs", + ) + parser.add_argument("--repeats", type=int, default=3) + args = parser.parse_args() + + sha = _git_sha() + if sha != args.expected_sha: + raise RuntimeError(f"wrong source head: {sha} != {args.expected_sha}") + dirty = _git_status_porcelain() + if dirty.strip(): + raise RuntimeError( + "identity benchmark requires a clean working tree; uncommitted changes:\n" + + dirty + ) + if args.repeats < 1: + raise ValueError("--repeats must be positive") + + backends = [x.strip() for x in args.backends.split(",") if x.strip()] + if not backends or any(x not in {"cupy", "torch"} for x in backends): + raise ValueError("--backends must contain cupy and/or torch") + scales = _parse_scales(args.scales) + + rows = [] + for scale_index, (n, k) in enumerate(scales): + X_np, y_np, entity_np = _dataset(n, k, seed=20260808 + scale_index) + for backend in backends: + X, y, entity = _to_backend(X_np, y_np, entity_np, backend) + for model_name in ("PanelOLS", "RandomEffects"): + # Warm both paths before measurement to avoid one-time import/ + # allocator effects being attributed to the digest. + _timed_fit( + model_name, X, y, entity, backend, disable_digest=False + ) + _timed_fit( + model_name, X, y, entity, backend, disable_digest=True + ) + + with_digest = [] + without_digest = [] + for _ in range(args.repeats): + with_digest.append( + _timed_fit( + model_name, + X, + y, + entity, + backend, + disable_digest=False, + ) + ) + without_digest.append( + _timed_fit( + model_name, + X, + y, + entity, + backend, + disable_digest=True, + ) + ) + + normal = _median(with_digest) + baseline = _median(without_digest) + overhead = normal - baseline + ratio = normal / baseline if baseline > 0.0 else None + rows.append( + { + "backend": backend, + "model": model_name, + "n_samples": n, + "n_features": k, + "repeats": args.repeats, + "with_digest_seconds": normal, + "without_digest_seconds": baseline, + "digest_overhead_seconds": overhead, + "with_over_without_ratio": ratio, + "with_digest_samples": with_digest, + "without_digest_samples": without_digest, + } + ) + + payload = { + "schema_version": 1, + "git_sha": sha, + "working_tree_clean": True, + "benchmark": "panel_stage_b_full_content_identity_overhead", + "timing_scope": "end-to-end estimator fit with vs without only the SHA-256 full-content digest", + "target_scale_source": "PR122 fresh-review performance finding", + "environment": { + "python": platform.python_version(), + "platform": platform.platform(), + "packages": { + name: _version(name) + for name in ("statgpu", "numpy", "cupy", "torch") + }, + }, + "rows": rows, + } + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2)) + print(f"PASS — identity-overhead benchmark recorded: {args.out}") + + +if __name__ == "__main__": + main() diff --git a/dev/benchmarks/benchmark_source_catalog.json b/dev/benchmarks/benchmark_source_catalog.json index 51c162448..4f9c6eca7 100644 --- a/dev/benchmarks/benchmark_source_catalog.json +++ b/dev/benchmarks/benchmark_source_catalog.json @@ -131,6 +131,31 @@ "issue": "#112", "reason": "Exact-head P100 focused correctness evidence for PR #116 is retained for audit but is intentionally not a dashboard timing source." }, + { + "rule_id": "pr122-panel-stage-b-physical-validation", + "priority": 20, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "issue": "#93", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "path_regex": "^results/pr122_p100/panel_stage_b_gpu_validation_.*[.]json$" + }, + { + "rule_id": "superseded-pr122-panel-stage-b-canonical-a57efcea", + "priority": 21, + "path": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809.json", + "classification": "superseded_or_duplicate", + "canonical_eligible": false, + "provenance_status": "complete", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "superseded_by": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "issue": "#93", + "reason": "The 42-row a57efcea Stage-B canonical validation source is retained immutably for audit but is superseded by the exact-clean-head 2701aa9f P100 source that also physically validates an applicable Hausman branch." + }, { "rule_id": "distribution-current-needs-audit", "priority": 30, diff --git a/dev/benchmarks/frontend_data/parsers/__init__.py b/dev/benchmarks/frontend_data/parsers/__init__.py index 1edb4d459..0ac0a7d61 100644 --- a/dev/benchmarks/frontend_data/parsers/__init__.py +++ b/dev/benchmarks/frontend_data/parsers/__init__.py @@ -18,6 +18,7 @@ from .unsupervised import parse_unsupervised_benchmark from .new_modules_complete import parse_new_modules_with_anova_benchmark from .pr74_complete import parse_pr74_inference_benchmark +from .panel_stage_b import parse_panel_stage_b_physical_validation __all__ = [ "parse_penalized_glm_bench_perf", @@ -36,4 +37,5 @@ "parse_new_modules_benchmark", "parse_new_modules_with_anova_benchmark", "parse_p2_benchmark", + "parse_panel_stage_b_physical_validation", ] diff --git a/dev/benchmarks/frontend_data/parsers/cv_package.py b/dev/benchmarks/frontend_data/parsers/cv_package.py index 9c8766439..2b3c76df4 100644 --- a/dev/benchmarks/frontend_data/parsers/cv_package.py +++ b/dev/benchmarks/frontend_data/parsers/cv_package.py @@ -30,6 +30,20 @@ def _digest_id(prefix: str, value: Any) -> str: return f"{prefix}-{hashlib.sha256(payload).hexdigest()[:16]}" +def _stable_pstdev_ms(values: list[float]) -> float: + """Return a cross-Python-stable population std for generated JSON. + + ``statistics.pstdev`` may differ by one floating-point ULP across supported + Python runtimes for the same benchmark samples. The dashboard bundle is a + committed deterministic artifact whose generation id hashes the serialized + floats, so retain substantially more precision than the benchmark warrants + while normalizing those runtime-only representation differences. + """ + if len(values) <= 1: + return 0.0 + return round(statistics.pstdev(values), 12) + + def _category_ids(model_id: str) -> list[str]: primary = _PRIMARY_CATEGORY[model_id] if model_id in {"RidgeCV", "LassoCV", "ElasticNetCV", "LogisticRegressionCV"}: @@ -187,7 +201,7 @@ def parse_cv_benchmark( assert convergence is not None assert scores is not None - std_ms = statistics.pstdev(total_samples) if len(total_samples) > 1 else 0.0 + std_ms = _stable_pstdev_ms(total_samples) converged_rate = 1.0 if convergence["final_refit_converged"] else 0.0 validation_status = ( "pass" diff --git a/dev/benchmarks/frontend_data/parsers/panel_stage_b.py b/dev/benchmarks/frontend_data/parsers/panel_stage_b.py new file mode 100644 index 000000000..8f9d2d564 --- /dev/null +++ b/dev/benchmarks/frontend_data/parsers/panel_stage_b.py @@ -0,0 +1,315 @@ +from __future__ import annotations +"""Parse PR #122 Panel Stage-B physical GPU validation evidence.""" + +import hashlib +import json +import math +from pathlib import Path +from typing import Any + +from ..canonical import make_scale_key, make_scale_label + +_PARSER_NAME = "parse_panel_stage_b_physical_validation_v1" +_PARSER_VERSION = "1.0" + + +def _stable_id(kind: str, *parts: object) -> str: + payload = json.dumps(parts, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + return f"{kind}-" + hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16] + + +def _scale(case: dict[str, Any]) -> dict[str, Any]: + n_samples = int(case["n_samples"]) + n_features = int(case["n_features"]) + return { + "scale_key": make_scale_key(n_samples, n_features), + "n_samples": n_samples, + "n_features": n_features, + "label": make_scale_label(n_samples, n_features), + } + + +def _validation(checks: list[str], status: str, filepath: Path) -> dict[str, Any]: + normalized = "pass" if status == "success" else "fail" + return { + "status": normalized, + "checks": [{"metric": metric, "status": normalized} for metric in checks], + "quality": "reported", + "source_file": filepath.name, + } + + +def _apply_aggregate_validation_contract( + validation: dict[str, Any], + *, + schema_ok: bool, + source_ok: bool, + backend_ok: bool, + executed_backend_ok: bool, +) -> dict[str, Any]: + """Prevent a failed aggregate physical run from emitting passing rows.""" + failed_metrics: list[str] = [] + if not schema_ok: + failed_metrics.append("source_schema_status_ok") + if not source_ok: + failed_metrics.append("source_validation_status_success") + if not backend_ok: + failed_metrics.append("backend_validation_status_success") + if not executed_backend_ok: + failed_metrics.append("executed_backend_matches_requested") + + if failed_metrics: + validation["status"] = "fail" + validation["checks"].extend( + {"metric": metric, "status": "fail"} for metric in failed_metrics + ) + return validation + + +def _apply_applicable_hausman_contract( + validation: dict[str, Any], diagnostic: dict[str, Any] +) -> dict[str, Any]: + """Require numeric evidence for the dedicated applicable Hausman fixture.""" + ok = diagnostic.get("applicable") is True + try: + statistic = float(diagnostic.get("statistic")) + pvalue = float(diagnostic.get("pvalue")) + df = float(diagnostic.get("df")) + ok = ( + ok + and math.isfinite(statistic) + and statistic >= 0.0 + and math.isfinite(pvalue) + and 0.0 <= pvalue <= 1.0 + and math.isfinite(df) + and df > 0.0 + ) + except (TypeError, ValueError): + ok = False + + validation["checks"].append( + { + "metric": "hausman_applicable_statistic_pvalue_df", + "status": "pass" if ok else "fail", + } + ) + if not ok: + validation["status"] = "fail" + return validation + + +def parse_panel_stage_b_physical_validation( + filepath: Path, env_id: str +) -> tuple[list[dict], list[dict], list[str]]: + """Emit validation-only Panel runs; this source contains no timings.""" + data = json.loads(filepath.read_text(encoding="utf-8")) + warnings: list[str] = [] + + schema_ok = data.get("schema_status") == "ok" + source_ok = data.get("status") == "success" + if not schema_ok: + warnings.append(f"{filepath.name}: source schema_status is not ok") + if not source_ok: + warnings.append(f"{filepath.name}: physical validation status is not success") + if data.get("protocol", {}).get("timing_collected") is not False: + warnings.append( + f"{filepath.name}: PR122 physical source must remain validation-only" + ) + + source = { + "file": filepath.name, + "date": data.get("source_date", ""), + "parser": _PARSER_NAME, + "parser_version": _PARSER_VERSION, + } + case_catalog = { + case["case_id"]: case for case in data.get("case_catalog", []) + } + runs: list[dict] = [] + model_ids: set[str] = set() + + for backend in ("cupy", "torch"): + backend_result = data.get("backend_results", {}).get(backend, {}) + backend_ok = backend_result.get("status") == "success" + executed_backend = backend_result.get("executed_backend") + executed_backend_ok = executed_backend == backend + if not backend_ok: + warnings.append( + f"{filepath.name}: {backend} backend validation status is not success" + ) + if not executed_backend_ok: + warnings.append( + f"{filepath.name}: requested {backend} but executed {executed_backend!r}" + ) + + for case_id, status in backend_result.get("model_cases", {}).items(): + case = case_catalog.get(case_id) + if case is None: + warnings.append(f"{filepath.name}: unknown case {case_id!r}") + continue + model_id = str(case["model_id"]) + model_ids.add(model_id) + scale = _scale(case) + validation = _apply_aggregate_validation_contract( + _validation(case.get("checks", []), str(status), filepath), + schema_ok=schema_ok, + source_ok=source_ok, + backend_ok=backend_ok, + executed_backend_ok=executed_backend_ok, + ) + + runs.append( + { + "run_id": "", + "benchmark_session_id": f"{env_id}-panel-stage-b-pr122", + "env_id": env_id, + "category_ids": ["panel"], + "model_id": model_id, + "case_id": _stable_id( + "case", case_id, case.get("variant"), scale["scale_key"] + ), + "method_config_id": _stable_id( + "method", + "panel-stage-b-physical-validation", + model_id, + case.get("variant"), + ), + "variant": str(case.get("variant") or case_id), + "penalty": None, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": backend, + "scale": scale, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": data.get("git_sha"), + "working_tree_clean": bool(data.get("working_tree_clean")), + }, + "source": dict(source), + "metrics": { + "validation": validation, + "inference": { + "ok": validation["status"] == "pass", + "quality": "reported", + "source_file": filepath.name, + }, + }, + } + ) + + for diagnostic_id, diagnostic in backend_result.get("diagnostics", {}).items(): + applicable_fixture = diagnostic_id == "hausman_applicable_nonzero_effect" + explicit_re_constant = diagnostic_id.startswith( + "hausman_explicit_re_constant_" + ) + parameterization = ( + "re-explicit-constant" if explicit_re_constant else "standard" + ) + + if applicable_fixture: + balance = "dedicated" + variant = "hausman-applicable-nonzero-effect" + n_samples, n_features = 48, 1 + validation_checks = [ + "hausman_backend_consistency", + "backend_provenance", + ] + method_parts: list[object] = [ + "panel-stage-b-physical-validation", + "hausman", + "applicable-nonzero-effect", + ] + else: + balance = ( + "unbalanced" if diagnostic_id.endswith("unbalanced") else "balanced" + ) + variant = ( + f"hausman-re-explicit-constant-{balance}" + if explicit_re_constant + else f"hausman-{balance}" + ) + n_samples = 49 if balance == "unbalanced" else 54 + n_features = 2 + validation_checks = [ + "hausman_backend_consistency", + "backend_provenance", + ] + method_parts = [ + "panel-stage-b-physical-validation", + "hausman", + balance, + ] + if explicit_re_constant: + method_parts.append(parameterization) + + scale = { + "scale_key": make_scale_key(n_samples, n_features), + "n_samples": n_samples, + "n_features": n_features, + "label": make_scale_label(n_samples, n_features), + } + status = str(diagnostic.get("status", "failed")) + validation = _apply_aggregate_validation_contract( + _validation(validation_checks, status, filepath), + schema_ok=schema_ok, + source_ok=source_ok, + backend_ok=backend_ok, + executed_backend_ok=executed_backend_ok, + ) + if applicable_fixture: + validation = _apply_applicable_hausman_contract(validation, diagnostic) + + model_ids.add("PanelOLS") + runs.append( + { + "run_id": "", + "benchmark_session_id": f"{env_id}-panel-stage-b-pr122", + "env_id": env_id, + "category_ids": ["panel"], + "model_id": "PanelOLS", + "case_id": _stable_id("case", diagnostic_id, scale["scale_key"]), + "method_config_id": _stable_id("method", *method_parts), + "variant": variant, + "penalty": None, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": backend, + "scale": scale, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": parameterization, + "applicable": bool(diagnostic.get("applicable")), + **( + { + "diagnostic_fixture": "nonzero-effect-applicable", + "statistic": diagnostic.get("statistic"), + "pvalue": diagnostic.get("pvalue"), + "df": diagnostic.get("df"), + } + if applicable_fixture + else {} + ), + "measurement_git_sha": data.get("git_sha"), + "working_tree_clean": bool(data.get("working_tree_clean")), + }, + "source": dict(source), + "metrics": {"validation": validation}, + } + ) + + models = [ + { + "model_id": model_id, + "primary_category_id": "panel", + "category_ids": ["panel"], + "supports_penalty": False, + "supports_inference": True, + } + for model_id in sorted(model_ids) + ] + return runs, models, warnings diff --git a/dev/benchmarks/frontend_data/registry.py b/dev/benchmarks/frontend_data/registry.py index e2c80b2b9..512a73041 100644 --- a/dev/benchmarks/frontend_data/registry.py +++ b/dev/benchmarks/frontend_data/registry.py @@ -22,6 +22,7 @@ parse_new_modules_benchmark, parse_new_modules_with_anova_benchmark, parse_p2_benchmark, + parse_panel_stage_b_physical_validation, ) MINIMUM_DASHBOARD_SOURCE_DATE = date(2026, 6, 1) @@ -56,6 +57,7 @@ "new_modules_benchmark": parse_new_modules_benchmark, "new_modules_with_anova_benchmark": parse_new_modules_with_anova_benchmark, "p2_benchmark": parse_p2_benchmark, + "panel_stage_b_physical_validation": parse_panel_stage_b_physical_validation, } diff --git a/dev/benchmarks/frontend_sources.json b/dev/benchmarks/frontend_sources.json index 2f39177f5..ebaa06243 100644 --- a/dev/benchmarks/frontend_sources.json +++ b/dev/benchmarks/frontend_sources.json @@ -9,16 +9,21 @@ "host": "hz-4.matpool.com" }, "remote-p100-cv-20260807": { - "label": "Tesla P100 CV rerun — 2026-08-07", + "label": "Tesla P100 CV rerun \u2014 2026-08-07", "gpu": "Tesla P100-SXM2-16GB", "cpu": "x86_64", "host": "wE6lDe" }, "remote-p100-pr116-20260807": { - "label": "Tesla P100 PR #116 CV validation — 2026-08-07", + "label": "Tesla P100 PR #116 CV validation \u2014 2026-08-07", "gpu": "Tesla P100-SXM2-16GB", "cpu": "x86_64", "host": "wE6lDe" + }, + "remote-p100-pr122-20260809": { + "label": "Tesla P100 PR #122 Panel Stage B validation \u2014 2026-08-09", + "gpu": "Tesla P100-SXM2-16GB", + "cpu": "x86_64" } }, "frameworks": { @@ -55,44 +60,48 @@ }, "comparisons": { "penalized-glm-perf-20260622": { - "label": "Penalized GLM performance — 2026-06-22", + "label": "Penalized GLM performance \u2014 2026-06-22", "env_id": "remote-p100" }, "glm-solver-20260623": { - "label": "GLM solver benchmark — 2026-06-23", + "label": "GLM solver benchmark \u2014 2026-06-23", "env_id": "remote-p100" }, "coxph-efron-20260622": { - "label": "CoxPH Efron benchmark — 2026-06-22", + "label": "CoxPH Efron benchmark \u2014 2026-06-22", "env_id": "remote-p100" }, "loss-functions-20260623": { - "label": "Robust, quantile, and CoxPH loss benchmark — 2026-06-23", + "label": "Robust, quantile, and CoxPH loss benchmark \u2014 2026-06-23", "env_id": "remote-p100" }, "ordered-inference-pr74": { - "label": "Ordered, penalized, bootstrap, and quantile inference — 2026-07-12", + "label": "Ordered, penalized, bootstrap, and quantile inference \u2014 2026-07-12", "env_id": "remote-p100" }, "unsupervised-20260627": { - "label": "Unsupervised benchmark — 2026-06-27", + "label": "Unsupervised benchmark \u2014 2026-06-27", "env_id": "remote-p100" }, "new-modules-20260624": { - "label": "Panel, GAM, and ANOVA benchmark — 2026-06-24", + "label": "Panel, GAM, and ANOVA benchmark \u2014 2026-06-24", "env_id": "remote-p100" }, "p2-benchmark-20260617": { - "label": "Covariance and nonparametric benchmark — 2026-06-17", + "label": "Covariance and nonparametric benchmark \u2014 2026-06-17", "env_id": "remote-p100" }, "cv-benchmark-20260807": { - "label": "Cross-validation benchmark — 2026-08-07", + "label": "Cross-validation benchmark \u2014 2026-08-07", "env_id": "remote-p100-cv-20260807" }, "cv-benchmark-pr116-20260807": { - "label": "Cross-validation benchmark after PR #116 repair — 2026-08-07", + "label": "Cross-validation benchmark after PR #116 repair \u2014 2026-08-07", "env_id": "remote-p100-pr116-20260807" + }, + "panel-stage-b-pr122-20260809": { + "label": "Panel Stage B physical validation \u2014 PR #122 \u2014 2026-08-09", + "env_id": "remote-p100-pr122-20260809" } }, "sources": [ @@ -230,6 +239,22 @@ "measurement_git_sha": "e6e4846b06604ed53e65fc9afd9054bd5777098f", "raw_git_sha": "e6e4846b06604ed53e65fc9afd9054bd5777098f", "provenance_note": "Physical PR #116 validation was executed on Tesla P100 from exact numerical implementation head e6e4846b06604ed53e65fc9afd9054bd5777098f. The artifact records that SHA directly; the historical pre-fix P100 source remains registered separately." + }, + { + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "comparison_id": "panel-stage-b-pr122-20260809", + "path": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf", + "parser": "panel_stage_b_physical_validation", + "parser_version": "1.0", + "env_id": "remote-p100-pr122-20260809", + "required": true, + "allowed_issue_codes": [], + "source_date": "2026-08-09", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "raw_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "provenance_note": "Final PR #122 Stage-B correctness/backend-provenance source. Exact clean P100 measurement 2701aa9feb3796c33c94e6480fcb78c80c6a809c passed all 17 estimator cases and five Hausman diagnostics on each of CuPy and Torch, with requested/executed backend identity and no CPU fallback. The dedicated hausman_applicable_nonzero_effect fixture is applicable on both GPU backends with df=1 and statistic/p-value agreement versus NumPy at floating-point noise. Raw artifact results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json has Git blob fa3a253e6d882a4e69be29e7e3b1dce7b223b9a9 and was committed by 0d0d654d825cea872672f27d02107a58048b345f. The older focused disconnected-FE artifact results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json (Git blob 3bda0b2040479ba8201e2722eb990ba086c3f3b9, measurement a57efcea29b0e87ecb89865c5a6902d5773812c6) is retained only as supplementary evidence for the unchanged df=1 path. No timing was collected." } ] } diff --git a/dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py b/dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py new file mode 100644 index 000000000..b7c5219d5 --- /dev/null +++ b/dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +"""Focused physical GPU gate for disconnected two-way PanelOLS rank handling. + +This complements ``validate_panel_stage_b_gpu.py`` after the PR #122 +Ready-for-review fix that moved the component-aware ``N + T - C`` rank ahead +of the fixed-effects residual-df feasibility gate. The fixture is deliberately +chosen so the historical count gives residual df 0 while the correct +component-aware rank gives residual df 1. +""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import platform +import subprocess +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np + +from statgpu.backends import _to_numpy +from statgpu.panel import PanelOLS + + +def _git_sha() -> str: + return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + + +def _git_status_porcelain() -> str: + return subprocess.check_output(["git", "status", "--porcelain"], text=True) + + +def _version(name: str): + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return None + + +def _fixture(): + # Two disconnected 2x2 blocks plus one singleton cell. + # N = T = 5, C = 3, n = 9, k = 1. + # Historical nuisance count: (N - 1) + (T - 1) = 8 -> df = 0. + # Correct effect rank: N + T - C = 7 -> df = 1. + entity = np.asarray([0, 0, 1, 1, 2, 2, 3, 3, 4], dtype=np.int64) + time = np.asarray([0, 1, 0, 1, 2, 3, 2, 3, 4], dtype=np.int64) + X = np.asarray( + [1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 0.0], + dtype=np.float64, + ).reshape(-1, 1) + y = np.asarray( + [1.0, -1.0, -1.0, 1.0, 2.0, -2.0, -2.0, 2.0, 0.0], + dtype=np.float64, + ) + return X, y, entity, time + + +def _to_backend_arrays(X, y, entity, time, backend): + if backend == "numpy": + return X, y, entity, time + if backend == "cupy": + import cupy as cp + + return ( + cp.asarray(X), + cp.asarray(y), + cp.asarray(entity, dtype=cp.int64), + cp.asarray(time, dtype=cp.int64), + ) + if backend == "torch": + import torch + + return ( + torch.as_tensor(X, dtype=torch.float64, device="cuda"), + torch.as_tensor(y, dtype=torch.float64, device="cuda"), + torch.as_tensor(entity, dtype=torch.int64, device="cuda"), + torch.as_tensor(time, dtype=torch.int64, device="cuda"), + ) + raise ValueError(backend) + + +def _device_arg(backend): + return {"numpy": "cpu", "cupy": "cuda", "torch": "torch"}[backend] + + +def _backend_name(model): + return model._get_backend(backend="auto").name + + +def _array(value): + return np.asarray(_to_numpy(value), dtype=np.float64) + + +def _test_result(result): + return { + "applicable": bool(result.applicable), + "reason": result.reason, + "statistic": result.statistic, + "pvalue": result.pvalue, + "df": result.df, + "distribution": result.distribution, + } + + +def _snapshot(model): + fit = model.fit_statistics_ + metadata = fit.metadata + diagnostic_df = metadata["diagnostic_df"] + return { + "coef": _array(model.coef_).ravel().tolist(), + "bse": _array(model.bse_).ravel().tolist(), + "tvalues": _array(model.tvalues_).ravel().tolist(), + "pvalues": _array(model.pvalues_).ravel().tolist(), + "conf_int": _array(model.conf_int_).tolist(), + "nobs": int(model.nobs), + "df_resid": int(model.df_resid), + "rsquared_within": float(model.rsquared_within), + "fit_statistics": { + "rsquared_within": fit.rsquared_within, + "rsquared_between": fit.rsquared_between, + "rsquared_overall": fit.rsquared_overall, + "rsquared_adj": fit.rsquared_adj, + "f_statistic": fit.f_statistic, + "f_pvalue": fit.f_pvalue, + "f_df": None if fit.f_df is None else list(fit.f_df), + }, + "diagnostic_df": { + "effect_rank": int(diagnostic_df["effect_rank"]), + "incidence_components": diagnostic_df["incidence_components"], + "rank_x": int(diagnostic_df["rank_x"]), + "df_resid": int(diagnostic_df["df_resid"]), + "df_total": int(diagnostic_df["df_total"]), + }, + "legacy_df_resid": int(metadata["legacy_df_resid"]), + "public_df_resid_basis": metadata["public_df_resid_basis"], + "pooling_f": _test_result(model.pooling_f_test()), + } + + +def _fit(backend): + X, y, entity, time = _fixture() + Xb, yb, eb, tb = _to_backend_arrays(X, y, entity, time, backend) + model = PanelOLS( + entity_effects=True, + time_effects=True, + cov_type="nonrobust", + device=_device_arg(backend), + ).fit(Xb, yb, entity_ids=eb, time_ids=tb) + return model, _snapshot(model) + + +def _assert_structural_contract(snapshot, *, label): + if snapshot["legacy_df_resid"] != 0: + raise AssertionError(f"{label}: expected legacy df 0") + if snapshot["public_df_resid_basis"] != "component-aware": + raise AssertionError(f"{label}: component-aware public df not used") + if snapshot["df_resid"] != 1: + raise AssertionError(f"{label}: expected public df_resid=1") + diagnostic = snapshot["diagnostic_df"] + expected = { + "effect_rank": 7, + "incidence_components": 3, + "rank_x": 1, + "df_resid": 1, + } + for name, value in expected.items(): + if diagnostic[name] != value: + raise AssertionError( + f"{label}: diagnostic_df[{name!r}]={diagnostic[name]!r} != {value!r}" + ) + + +def _compare(reference, candidate, *, rtol, atol, label): + differences = {} + for field in ("coef", "bse", "tvalues", "pvalues", "conf_int"): + actual = np.asarray(candidate[field], dtype=np.float64) + expected = np.asarray(reference[field], dtype=np.float64) + np.testing.assert_allclose( + actual, + expected, + rtol=rtol, + atol=atol, + err_msg=f"{label}.{field}", + ) + differences[field] = float(np.max(np.abs(actual - expected))) + + for field in ("nobs", "df_resid", "legacy_df_resid"): + if candidate[field] != reference[field]: + raise AssertionError( + f"{label}.{field}: {candidate[field]!r} != {reference[field]!r}" + ) + + if candidate["public_df_resid_basis"] != reference["public_df_resid_basis"]: + raise AssertionError(f"{label}.public_df_resid_basis mismatch") + if candidate["diagnostic_df"] != reference["diagnostic_df"]: + raise AssertionError(f"{label}.diagnostic_df mismatch") + + for field, expected in reference["fit_statistics"].items(): + actual = candidate["fit_statistics"][field] + if expected is None: + if actual is not None: + raise AssertionError(f"{label}.fit_statistics.{field} expected None") + elif field == "f_df": + np.testing.assert_allclose(actual, expected, rtol=0, atol=0) + else: + np.testing.assert_allclose( + actual, + expected, + rtol=rtol, + atol=atol, + err_msg=f"{label}.fit_statistics.{field}", + ) + differences[f"fit_statistics.{field}"] = float(abs(actual - expected)) + + ref_pool = reference["pooling_f"] + cand_pool = candidate["pooling_f"] + for field in ("applicable", "reason", "df", "distribution"): + if cand_pool[field] != ref_pool[field]: + raise AssertionError(f"{label}.pooling_f.{field} mismatch") + for field in ("statistic", "pvalue"): + expected = ref_pool[field] + actual = cand_pool[field] + if expected is None: + if actual is not None: + raise AssertionError(f"{label}.pooling_f.{field} expected None") + else: + np.testing.assert_allclose(actual, expected, rtol=rtol, atol=atol) + differences[f"pooling_f.{field}"] = float(abs(actual - expected)) + return differences + + +def _environment(backends): + gpu = None + if "torch" in backends: + import torch + + if not torch.cuda.is_available(): + raise RuntimeError("Torch backend requested but CUDA is unavailable") + gpu = torch.cuda.get_device_name(0) + elif "cupy" in backends: + import cupy as cp + + if cp.cuda.runtime.getDeviceCount() < 1: + raise RuntimeError("CuPy backend requested but CUDA is unavailable") + props = cp.cuda.runtime.getDeviceProperties(0) + gpu = props["name"].decode() if isinstance(props["name"], bytes) else props["name"] + return { + "python": platform.python_version(), + "platform": platform.platform(), + "gpu": gpu, + "packages": { + name: _version(name) + for name in ("statgpu", "numpy", "scipy", "cupy", "torch") + }, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--backends", default="cupy,torch") + parser.add_argument("--expected-sha", required=True) + parser.add_argument("--rtol", type=float, default=5e-6) + parser.add_argument("--atol", type=float, default=5e-7) + args = parser.parse_args() + + backends = [value.strip() for value in args.backends.split(",") if value.strip()] + if not backends or any(value not in {"cupy", "torch"} for value in backends): + raise ValueError("--backends must contain cupy and/or torch") + + sha = _git_sha() + clean = _git_status_porcelain() == "" + if sha != args.expected_sha: + raise RuntimeError(f"HEAD {sha} != --expected-sha {args.expected_sha}") + if not clean: + raise RuntimeError("working tree must be clean for physical validation") + + reference_model, reference = _fit("numpy") + if _backend_name(reference_model) != "numpy": + raise RuntimeError("NumPy reference did not execute on NumPy") + _assert_structural_contract(reference, label="numpy") + + result = { + "schema_version": 1, + "validation": "panel_stage_b_disconnected_two_way_fe", + "generated_at": datetime.now(timezone.utc).isoformat(), + "git_sha": sha, + "working_tree_clean": clean, + "status": "success", + "protocol": { + "reference_backend": "numpy", + "requested_backends": backends, + "rtol": args.rtol, + "atol": args.atol, + "timing_collected": False, + "fixture": "two disconnected 2x2 blocks plus one singleton", + "expected_rank_contract": { + "nobs": 9, + "n_entities": 5, + "n_times": 5, + "incidence_components": 3, + "effect_rank": 7, + "rank_x": 1, + "legacy_df_resid": 0, + "component_aware_df_resid": 1, + }, + }, + "environment": _environment(backends), + "reference": reference, + "backend_results": {}, + } + + for backend in backends: + try: + model, snapshot = _fit(backend) + executed_backend = _backend_name(model) + if executed_backend != backend: + raise AssertionError( + f"requested {backend}, executed backend {executed_backend!r}" + ) + _assert_structural_contract(snapshot, label=backend) + differences = _compare( + reference, + snapshot, + rtol=args.rtol, + atol=args.atol, + label=backend, + ) + result["backend_results"][backend] = { + "status": "success", + "executed_backend": executed_backend, + "snapshot": snapshot, + "differences_vs_numpy": differences, + } + except Exception as exc: + result["status"] = "failed" + result["backend_results"][backend] = { + "status": "failed", + "error": f"{type(exc).__name__}: {exc}", + } + + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps(result, indent=2, sort_keys=True)) + if result["status"] != "success": + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/dev/benchmarks/validate_panel_stage_b_gpu.py b/dev/benchmarks/validate_panel_stage_b_gpu.py new file mode 100644 index 000000000..954776bc7 --- /dev/null +++ b/dev/benchmarks/validate_panel_stage_b_gpu.py @@ -0,0 +1,701 @@ +#!/usr/bin/env python3 +"""Physical CuPy/Torch acceptance for Panel Tier-1 Stage B (Issue #93). + +This is a correctness/backend-provenance gate, not a performance benchmark. +It validates the new parameter-based fit statistics and specification tests on +balanced and unbalanced panels against the NumPy implementation while proving +that requested CuPy/Torch CUDA backends actually execute. It also rechecks the +maintained coefficient-inference outputs so Stage-B integration cannot regress +the Stage-A bse/t/p/CI/df contracts. +""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import platform +import subprocess +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np + +from statgpu.backends import _to_numpy +from statgpu.panel import ( + BetweenOLS, + FamaMacBeth, + FirstDifferenceOLS, + PanelOLS, + PooledOLS, + RandomEffects, +) + + +def _git_sha() -> str: + return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + + +def _git_status_porcelain() -> str: + return subprocess.check_output(["git", "status", "--porcelain"], text=True) + + +def _version(name: str): + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return None + + +def _dataset(seed: int, *, unbalanced: bool): + rng = np.random.default_rng(seed) + n_entities, n_times = 9, 6 + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + X = rng.normal(size=(entity.size, 2)) + entity_effect = np.repeat(np.linspace(-0.7, 0.8, n_entities), n_times) + time_effect = np.tile(np.linspace(-0.22, 0.27, n_times), n_entities) + y = ( + 0.9 * X[:, 0] + - 0.4 * X[:, 1] + + entity_effect + + 0.25 * time_effect + + rng.normal(scale=0.18, size=entity.size) + ) + if unbalanced: + keep = np.ones(entity.size, dtype=bool) + keep[[1, 8, 17, 31, 44]] = False + X, y, entity, time = X[keep], y[keep], entity[keep], time[keep] + return X.astype(np.float64), y.astype(np.float64), entity, time + + +def _to_backend_arrays(X, y, entity, time, backend): + if backend == "numpy": + return X, y, entity, time + if backend == "cupy": + import cupy as cp + + return ( + cp.asarray(X), + cp.asarray(y), + cp.asarray(entity, dtype=cp.int64), + cp.asarray(time, dtype=cp.int64), + ) + if backend == "torch": + import torch + + return ( + torch.as_tensor(X, dtype=torch.float64, device="cuda"), + torch.as_tensor(y, dtype=torch.float64, device="cuda"), + torch.as_tensor(entity, dtype=torch.int64, device="cuda"), + torch.as_tensor(time, dtype=torch.int64, device="cuda"), + ) + raise ValueError(backend) + + +def _device_arg(backend): + return {"numpy": "cpu", "cupy": "cuda", "torch": "torch"}[backend] + + +def _backend_name(model): + if isinstance(model, FamaMacBeth): + return model._backend_name + return model._get_backend(backend="auto").name + + +def _array(value): + return np.asarray(_to_numpy(value), dtype=np.float64) + + +def _fit_stats(model): + result = model.fit_statistics_ + payload = { + "rsquared_within": result.rsquared_within, + "rsquared_between": result.rsquared_between, + "rsquared_overall": result.rsquared_overall, + "rsquared_adj": result.rsquared_adj, + "f_statistic": result.f_statistic, + "f_pvalue": result.f_pvalue, + "f_df": None if result.f_df is None else tuple(float(x) for x in result.f_df), + } + return payload + + +def _test_result(result): + return { + "applicable": bool(result.applicable), + "reason": result.reason, + "statistic": result.statistic, + "pvalue": result.pvalue, + "df": result.df, + "distribution": result.distribution, + } + + +def _model_snapshot(model): + payload = { + "coef": _array(model.coef_).ravel(), + "bse": _array(model.bse_).ravel(), + "tvalues": _array(model.tvalues_).ravel(), + "pvalues": _array(model.pvalues_).ravel(), + "conf_int": _array(model.conf_int_), + "nobs": int(model.nobs), + "df_resid": int(model.df_resid), + "fit_statistics": _fit_stats(model), + } + covariance = getattr(model, "_panel_cov_params", None) + if covariance is not None: + payload["diagnostic_covariance"] = _array(covariance) + if hasattr(model, "pooling_f_test") and isinstance(model, PanelOLS): + payload["pooling_f"] = _test_result(model.pooling_f_test()) + if hasattr(model, "breusch_pagan_lm_test") and isinstance(model, PooledOLS): + payload["bp_lm"] = _test_result(model.breusch_pagan_lm_test()) + if isinstance(model, RandomEffects): + meta = model.fit_statistics_.metadata + model_f_meta = meta.get("model_f", {}) + payload["random_effects_diagnostic_contract"] = { + "has_explicit_constant": bool(meta.get("has_explicit_constant")), + "constant_column_index": meta.get("constant_column_index"), + "restricted_rank": int(meta.get("restricted_rank", 0)), + "model_f_rank_restricted": int(model_f_meta.get("rank_restricted", 0)), + "model_f_restricted_design_supplied": bool( + model_f_meta.get("restricted_design_supplied", False) + ), + } + return payload + + +def _fit_cases(X, y, entity, time, backend, *, unbalanced): + Xb, yb, eb, tb = _to_backend_arrays(X, y, entity, time, backend) + device = _device_arg(backend) + suffix = "unbalanced" if unbalanced else "balanced" + cases = {} + + pooled = PooledOLS(device=device).fit(Xb, yb, entity_ids=eb) + cases[f"pooled_{suffix}"] = pooled + + scrambled_time_np = (3 * time + 2 * entity + 1) % 11 + if backend == "numpy": + scrambled_time = scrambled_time_np + elif backend == "cupy": + import cupy as cp + + scrambled_time = cp.asarray(scrambled_time_np, dtype=cp.int64) + else: + import torch + + scrambled_time = torch.as_tensor( + scrambled_time_np, dtype=torch.int64, device="cuda" + ) + pooled_hac = PooledOLS(cov_type="hac", bandwidth=2, device=device).fit( + Xb, + yb, + time_index=scrambled_time, + entity_ids=eb, + ) + cases[f"pooled_hac_unsorted_{suffix}"] = pooled_hac + + between = BetweenOLS(cov_type="robust", device=device).fit( + Xb, yb, entity_ids=eb + ) + cases[f"between_{suffix}"] = between + + first_diff = FirstDifferenceOLS(cov_type="robust", device=device).fit( + Xb, yb, entity_ids=eb, time_ids=tb + ) + cases[f"first_difference_{suffix}"] = first_diff + + fe = PanelOLS(entity_effects=True, cov_type="nonrobust", device=device).fit( + Xb, yb, entity_ids=eb + ) + cases[f"panel_entity_{suffix}"] = fe + + re = RandomEffects(device=device).fit(Xb, yb, entity_ids=eb) + cases[f"random_effects_{suffix}"] = re + + # Exercise the explicit-constant RandomEffects branch on the physical GPU + # using the same two slopes as FE. This validates both transformed-intercept + # diagnostics and the standard Hausman parameterization where FE absorbs the + # common intercept while RE estimates it explicitly. + X_constant = np.column_stack([np.ones(X.shape[0]), X]) + Xcb, ycb, ecb, _ = _to_backend_arrays( + X_constant, y, entity, time, backend + ) + re_constant = RandomEffects(device=device).fit(Xcb, ycb, entity_ids=ecb) + cases[f"random_effects_explicit_constant_{suffix}"] = re_constant + + fmb = FamaMacBeth(cov_type="newey-west", bandwidth=2, device=device).fit( + Xb, + yb, + time_ids=tb, + entity_ids=eb, + ) + cases[f"fama_macbeth_{suffix}"] = fmb + + diagnostics = { + f"hausman_{suffix}": _test_result(fe.hausman_test(re)), + f"hausman_explicit_re_constant_{suffix}": _test_result( + fe.hausman_test(re_constant) + ), + } + + if not unbalanced: + two_way = PanelOLS( + entity_effects=True, + time_effects=True, + cov_type="nonrobust", + device=device, + ).fit(Xb, yb, entity_ids=eb, time_ids=tb) + cases["panel_two_way_balanced"] = two_way + + return cases, diagnostics + + +def _hausman_applicable_dataset(): + '''Deterministic nonzero-effect panel with an applicable fitted Hausman test.''' + seed = 20260810 + n_entities, n_times = 12, 4 + effect_scale, noise_scale = 0.005, 0.1 + rng = np.random.default_rng(seed) + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + X = rng.normal(size=(entity.size, 1)) + entity_effect = np.repeat( + rng.normal(scale=effect_scale, size=n_entities), + n_times, + ) + y = ( + 0.8 * X[:, 0] + + entity_effect + + rng.normal(scale=noise_scale, size=entity.size) + ) + metadata = { + "seed": seed, + "n_entities": n_entities, + "n_times": n_times, + "entity_effect_scale": effect_scale, + "noise_scale": noise_scale, + } + return X.astype(np.float64), y.astype(np.float64), entity, time, metadata + + +def _require_applicable_hausman_coverage(diagnostics, *, backend): + '''Require a successful applicable Hausman statistic/p-value per backend.''' + applicable = [] + for name, payload in diagnostics.items(): + if payload.get("status") != "success" or payload.get("applicable") is not True: + continue + statistic = payload.get("statistic") + pvalue = payload.get("pvalue") + df = payload.get("df") + if statistic is None or pvalue is None or df is None: + continue + if not ( + np.isfinite(float(statistic)) + and np.isfinite(float(pvalue)) + and np.isfinite(float(df)) + and float(df) > 0.0 + ): + continue + applicable.append(name) + if not applicable: + raise AssertionError( + f"{backend}: physical Hausman gate requires at least one successful " + "applicable statistic/pvalue/df case" + ) + return sorted(applicable) + + +def _scalar_diff(actual, expected, *, rtol, atol, label): + if expected is None: + if actual is not None: + raise AssertionError(f"{label}: expected None, got {actual}") + return 0.0 + np.testing.assert_allclose(actual, expected, rtol=rtol, atol=atol, err_msg=label) + return float(abs(float(actual) - float(expected))) + + +def _compare_test_result(reference, candidate, *, rtol, atol, label): + if candidate["applicable"] != reference["applicable"]: + raise AssertionError( + f"{label}: applicability {candidate['applicable']} != {reference['applicable']}" + ) + if candidate["distribution"] != reference["distribution"]: + raise AssertionError(f"{label}: distribution mismatch") + if candidate["reason"] != reference["reason"]: + raise AssertionError( + f"{label}: reason {candidate['reason']!r} != {reference['reason']!r}" + ) + differences = {} + for field in ("statistic", "pvalue"): + differences[field] = _scalar_diff( + candidate[field], + reference[field], + rtol=rtol, + atol=atol, + label=f"{label}.{field}", + ) + ref_df = reference["df"] + cand_df = candidate["df"] + if ref_df is None: + if cand_df is not None: + raise AssertionError(f"{label}.df expected None") + elif isinstance(ref_df, (tuple, list)): + np.testing.assert_allclose(cand_df, ref_df, rtol=0, atol=0) + else: + np.testing.assert_allclose(cand_df, ref_df, rtol=0, atol=0) + return differences + + +def _max_abs_difference(actual, expected): + if actual.size == 0: + return 0.0 + return float(np.max(np.abs(actual - expected))) + + +def _compare_model(reference, candidate, *, rtol, atol, label): + differences = {} + for field in ("coef", "bse", "tvalues", "pvalues", "conf_int"): + np.testing.assert_allclose( + candidate[field], + reference[field], + rtol=rtol, + atol=atol, + err_msg=f"{label}.{field}", + ) + differences[field] = _max_abs_difference( + candidate[field], reference[field] + ) + + for field in ("nobs", "df_resid"): + if int(candidate[field]) != int(reference[field]): + raise AssertionError( + f"{label}.{field}: {candidate[field]} != {reference[field]}" + ) + differences[field] = 0.0 + + for field, expected in reference["fit_statistics"].items(): + actual = candidate["fit_statistics"][field] + if field == "f_df": + if expected is None: + if actual is not None: + raise AssertionError(f"{label}.fit_statistics.f_df expected None") + else: + np.testing.assert_allclose(actual, expected, rtol=0, atol=0) + continue + differences[f"fit_statistics.{field}"] = _scalar_diff( + actual, + expected, + rtol=rtol, + atol=atol, + label=f"{label}.fit_statistics.{field}", + ) + + if "diagnostic_covariance" in reference: + np.testing.assert_allclose( + candidate["diagnostic_covariance"], + reference["diagnostic_covariance"], + rtol=rtol, + atol=atol, + err_msg=f"{label}.diagnostic_covariance", + ) + differences["diagnostic_covariance"] = _max_abs_difference( + candidate["diagnostic_covariance"], + reference["diagnostic_covariance"], + ) + + if "random_effects_diagnostic_contract" in reference: + if candidate.get("random_effects_diagnostic_contract") != reference[ + "random_effects_diagnostic_contract" + ]: + raise AssertionError( + f"{label}.random_effects_diagnostic_contract mismatch: " + f"{candidate.get('random_effects_diagnostic_contract')} != " + f"{reference['random_effects_diagnostic_contract']}" + ) + differences["random_effects_diagnostic_contract"] = 0.0 + + for test_name in ("pooling_f", "bp_lm"): + if test_name in reference: + nested = _compare_test_result( + reference[test_name], + candidate[test_name], + rtol=rtol, + atol=atol, + label=f"{label}.{test_name}", + ) + differences.update( + {f"{test_name}.{name}": value for name, value in nested.items()} + ) + return differences + + +def _environment(backends): + gpu = None + if "torch" in backends: + import torch + + if not torch.cuda.is_available(): + raise RuntimeError("Torch backend requested but CUDA is unavailable") + gpu = torch.cuda.get_device_name(0) + elif "cupy" in backends: + import cupy as cp + + if cp.cuda.runtime.getDeviceCount() < 1: + raise RuntimeError("CuPy backend requested but CUDA is unavailable") + props = cp.cuda.runtime.getDeviceProperties(0) + gpu = props["name"].decode() if isinstance(props["name"], bytes) else props["name"] + return { + "python": platform.python_version(), + "platform": platform.platform(), + "gpu": gpu, + "packages": { + name: _version(name) + for name in ("statgpu", "numpy", "scipy", "cupy", "torch") + }, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--backends", default="cupy,torch") + parser.add_argument("--expected-sha", required=True) + parser.add_argument("--rtol", type=float, default=5e-6) + parser.add_argument("--atol", type=float, default=5e-7) + args = parser.parse_args() + + backends = [value.strip() for value in args.backends.split(",") if value.strip()] + if not backends or any(value not in {"cupy", "torch"} for value in backends): + raise ValueError("--backends must contain cupy and/or torch") + + sha = _git_sha() + if sha != args.expected_sha: + raise RuntimeError(f"wrong source head: {sha} != {args.expected_sha}") + dirty = _git_status_porcelain() + if dirty.strip(): + raise RuntimeError( + "physical acceptance requires a clean working tree; uncommitted changes:\n" + + dirty + ) + + datasets = { + "balanced": _dataset(20260808, unbalanced=False), + "unbalanced": _dataset(20260809, unbalanced=True), + } + reference_models = {} + reference_diagnostics = {} + for name, (X, y, entity, time) in datasets.items(): + models, diagnostics = _fit_cases( + X, y, entity, time, "numpy", unbalanced=(name == "unbalanced") + ) + reference_models.update( + {case: _model_snapshot(model) for case, model in models.items()} + ) + reference_diagnostics.update(diagnostics) + + ( + hausman_X, + hausman_y, + hausman_entity, + hausman_time, + hausman_fixture_metadata, + ) = _hausman_applicable_dataset() + hausman_fe_reference = PanelOLS( + entity_effects=True, + cov_type="nonrobust", + device="cpu", + ).fit(hausman_X, hausman_y, entity_ids=hausman_entity) + hausman_re_reference = RandomEffects(device="cpu").fit( + hausman_X, hausman_y, entity_ids=hausman_entity + ) + hausman_reference = _test_result( + hausman_fe_reference.hausman_test(hausman_re_reference) + ) + hausman_variance_difference = float( + np.asarray(hausman_fe_reference._panel_cov_params, dtype=np.float64)[0, 0] + - np.asarray(hausman_re_reference._panel_cov_params, dtype=np.float64)[0, 0] + ) + if not hausman_reference["applicable"]: + raise AssertionError( + "hosted reference Hausman fixture must remain applicable: " + f"{hausman_reference['reason']}" + ) + if hausman_variance_difference <= 1e-6: + raise AssertionError( + "hosted reference Hausman covariance-difference margin is too small: " + f"{hausman_variance_difference}" + ) + reference_diagnostics["hausman_applicable_nonzero_effect"] = hausman_reference + + results = {} + for backend in backends: + backend_payload = {"models": {}, "diagnostics": {}} + for name, (X, y, entity, time) in datasets.items(): + models, diagnostics = _fit_cases( + X, y, entity, time, backend, unbalanced=(name == "unbalanced") + ) + for case, model in models.items(): + actual_backend = _backend_name(model) + if actual_backend != backend: + raise AssertionError( + f"{case}: requested {backend}, executed {actual_backend}" + ) + snapshot = _model_snapshot(model) + differences = _compare_model( + reference_models[case], + snapshot, + rtol=args.rtol, + atol=args.atol, + label=case, + ) + backend_payload["models"][case] = { + "status": "success", + "executed_backend": actual_backend, + "max_abs_differences": differences, + } + for case, result in diagnostics.items(): + differences = _compare_test_result( + reference_diagnostics[case], + result, + rtol=args.rtol, + atol=args.atol, + label=case, + ) + backend_payload["diagnostics"][case] = { + "status": "success", + "max_abs_differences": differences, + "applicable": result["applicable"], + "reason": result["reason"], + } + Xhb, yhb, ehb, _ = _to_backend_arrays( + hausman_X, + hausman_y, + hausman_entity, + hausman_time, + backend, + ) + device = _device_arg(backend) + hausman_fe = PanelOLS( + entity_effects=True, + cov_type="nonrobust", + device=device, + ).fit(Xhb, yhb, entity_ids=ehb) + hausman_re = RandomEffects(device=device).fit(Xhb, yhb, entity_ids=ehb) + for model_name, model in ( + ("hausman_applicable_fe", hausman_fe), + ("hausman_applicable_re", hausman_re), + ): + actual_backend = _backend_name(model) + if actual_backend != backend: + raise AssertionError( + f"{model_name}: requested {backend}, executed {actual_backend}" + ) + + hausman_result = _test_result(hausman_fe.hausman_test(hausman_re)) + if not hausman_result["applicable"]: + raise AssertionError( + f"hausman_applicable_nonzero_effect unexpectedly inapplicable on " + f"{backend}: {hausman_result['reason']}" + ) + hausman_differences = _compare_test_result( + reference_diagnostics["hausman_applicable_nonzero_effect"], + hausman_result, + rtol=args.rtol, + atol=args.atol, + label="hausman_applicable_nonzero_effect", + ) + fe_coef = _array(hausman_fe.coef_).ravel() + re_coef = _array(hausman_re.coef_).ravel() + fe_cov = _array(hausman_fe._panel_cov_params) + re_cov = _array(hausman_re._panel_cov_params) + np.testing.assert_allclose( + fe_coef, + _array(hausman_fe_reference.coef_).ravel(), + rtol=args.rtol, + atol=args.atol, + err_msg="hausman_applicable_nonzero_effect.fe_coef", + ) + np.testing.assert_allclose( + re_coef, + _array(hausman_re_reference.coef_).ravel(), + rtol=args.rtol, + atol=args.atol, + err_msg="hausman_applicable_nonzero_effect.re_coef", + ) + np.testing.assert_allclose( + fe_cov, + _array(hausman_fe_reference._panel_cov_params), + rtol=args.rtol, + atol=args.atol, + err_msg="hausman_applicable_nonzero_effect.fe_covariance", + ) + np.testing.assert_allclose( + re_cov, + _array(hausman_re_reference._panel_cov_params), + rtol=args.rtol, + atol=args.atol, + err_msg="hausman_applicable_nonzero_effect.re_covariance", + ) + backend_payload["diagnostics"]["hausman_applicable_nonzero_effect"] = { + "status": "success", + "max_abs_differences": hausman_differences, + "fit_max_abs_differences": { + "fe_coef": _max_abs_difference( + fe_coef, _array(hausman_fe_reference.coef_).ravel() + ), + "re_coef": _max_abs_difference( + re_coef, _array(hausman_re_reference.coef_).ravel() + ), + "fe_covariance": _max_abs_difference( + fe_cov, _array(hausman_fe_reference._panel_cov_params) + ), + "re_covariance": _max_abs_difference( + re_cov, _array(hausman_re_reference._panel_cov_params) + ), + }, + "applicable": True, + "reason": hausman_result["reason"], + "statistic": hausman_result["statistic"], + "pvalue": hausman_result["pvalue"], + "df": hausman_result["df"], + "reference_statistic": hausman_reference["statistic"], + "reference_pvalue": hausman_reference["pvalue"], + "reference_df": hausman_reference["df"], + "reference_variance_difference": hausman_variance_difference, + "fixture": dict(hausman_fixture_metadata), + } + _require_applicable_hausman_coverage( + backend_payload["diagnostics"], backend=backend + ) + results[backend] = backend_payload + + payload = { + "schema_version": 2, + "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "git_sha": sha, + "working_tree_clean": True, + "status": "success", + "environment": _environment(backends), + "tolerances": {"rtol": args.rtol, "atol": args.atol}, + "datasets": { + **{ + name: {"nobs": int(len(values[1]))} + for name, values in datasets.items() + }, + "hausman_applicable_nonzero_effect": { + "nobs": int(len(hausman_y)), + **dict(hausman_fixture_metadata), + "reference_variance_difference": hausman_variance_difference, + }, + }, + "backends": results, + } + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2)) + print(f"PASS — Panel Stage B physical GPU validation: {args.out}") + + +if __name__ == "__main__": + main() diff --git a/dev/plans/panel_p1_stage_b_diagnostics_df_addendum.md b/dev/plans/panel_p1_stage_b_diagnostics_df_addendum.md new file mode 100644 index 000000000..63e2cf1c6 --- /dev/null +++ b/dev/plans/panel_p1_stage_b_diagnostics_df_addendum.md @@ -0,0 +1,172 @@ +# Panel Stage B diagnostic-df addendum + +This addendum is normative for `panel_p1_stage_b_diagnostics_plan.md` and closes second-round definition issues discovered immediately before estimator integration. Where this addendum explicitly overrides a conflicting clause in the main plan, the addendum is the final Stage-B contract. + +## Why a separate diagnostic df is required + +Stage A intentionally preserved the historical `PanelOLS.df_resid` convention: + +```text +df_resid_legacy = n - k - [(N - 1) entity effects] - [(T - 1) time effects] +``` + +for the corresponding included effects. This quantity continues to drive the existing nonrobust/robust covariance, t statistics, p-values, and confidence intervals and **must not change in Stage B**. + +The standard poolability/model-F definitions in `linearmodels`/`plm`, however, count the rank of the full fixed-effect nuisance space. With no explicit exogenous constant, a one-way entity FE model has nuisance-effect rank `N`. For two-way entity+time effects, let `C` denote the number of connected components in the observed bipartite entity-time incidence graph; the full dummy-space rank is `N + T - C`. The familiar `N + T - 1` expression is therefore the connected-panel special case `C = 1`, not a valid formula for every incomplete panel. In the ordinary connected full-rank case this makes the standard diagnostic residual df one lower than statgpu's legacy Stage-A inference df. + +Therefore Stage B introduces an internal **standard diagnostic residual df** rather than changing `model.df_resid`. + +## Standard diagnostic rank/df + +Let `r_x` be the numerical rank of the transformed slope design used for FE estimation. + +For current statgpu `PanelOLS`, formula intercepts are stripped before fitting and the estimator does not add a separate constant. Consequently the standard effect-space rank is: + +- no effects: `0`; +- entity only: `N`; +- time only: `T`; +- entity + time: `N + T - C`, where `C` is the connected-component count of the observed entity-time incidence graph. + +Then + +```text +df_model_diag = r_x + effect_rank_standard +df_resid_diag = n - df_model_diag +``` + +The implementation stores these values only in Stage-B diagnostic metadata/internal context. It does not overwrite the Stage-A `df_resid` attribute. For two-way FE it also records the incidence-component count so the rank decision is auditable. + +The primary transformed FE response lives in the orthogonal complement of the nuisance-effect space, so the corresponding total-variation degrees of freedom for Stage-B adjusted R² are + +```text +df_total_diag = n - effect_rank_standard +``` + +and hence + +```text +R2_adj = 1 - (RSS / df_resid_diag) / (TSS_transformed / df_total_diag). +``` + +This replaces the earlier provisional `n-1` wording in the main plan for FE adjusted R². It is the rank-consistent definition: the restricted zero-slope model has exactly the nuisance effects removed before the transformed total sum of squares is formed. + +If a future PanelOLS path contains a retained identified exogenous constant, the effect-rank accounting must switch to the equivalent constant-present parameterization rather than double counting the common mean. + +## Which Stage-B quantities use which df + +Use **standard diagnostic df** for new standardized diagnostics whose external definitions depend on model rank: + +- pooling F denominator df; +- pooling F numerator df through nested-model rank difference; +- `PanelFitStatistics.f_statistic` denominator df for `PanelOLS`; +- `PanelFitStatistics.rsquared_adj` residual and total df for `PanelOLS`. + +Use the existing **legacy Stage-A df** unchanged for: + +- existing covariance calculations; +- existing `bse_`, `tvalues_`, `pvalues_`, `conf_int_`; +- existing public `PanelOLS.df_resid`; +- any legacy summary field that already reports it. + +For PooledOLS, BetweenOLS, FirstDifferenceOLS, and RandomEffects, Stage B should use effective numerical rank for the new model-F/adjusted-R² metadata. When the existing fit is full rank this agrees with the current residual df. Stage B does not modify legacy inference df if a rank-deficient corner case exposes a difference. + +## Pooling F after this correction + +Construct the pooled null on the exact aligned level sample. Since current statgpu FE does not retain an exogenous constant, apply the same common-constant projection used by `linearmodels` before the pooled slope regression. Let + +```text +df_pool_diag = n - rank(X_centered) - 1 +``` + +where the `-1` is the projected common constant. Then + +```text +df_num = df_pool_diag - df_resid_diag_FE +F = ((RSS_pool - RSS_FE) / df_num) / (RSS_FE / df_resid_diag_FE) +``` + +This is equivalent to the external effect-rank formulation in the ordinary full-rank case and remains auditable under rank deficiency and disconnected two-way incidence graphs. + +The exact-fit boundary matches the classical model-F rule: + +- if `RSS_FE` is numerically zero while `RSS_pool - RSS_FE` is materially positive, report the limiting result `F = inf`, `p = 0` with the ordinary numerator/denominator df; +- if both pooled and FE RSS are numerically zero, the ratio is indeterminate and the structured result remains inapplicable; +- a materially negative nested-model RSS difference remains inapplicable rather than being clipped. + +## R² constant convention clarification + +For standardized parameter-based **overall** and **between** R², centering depends on an actual identified constant in the level exogenous design. Fixed effects alone do not set `has_constant=True` for these two parameter-based measures. + +The common-constant projection used by the **pooling F test** is a separate nested-test construction and must not be reused as a general R² centering rule. + +For `RandomEffects`, an explicit nonzero constant column in the supplied level design is detected directly. The quasi-demeaned transformed version of that same column is retained as the restricted intercept design for adjusted R² and classical model F, including on unbalanced panels where the transformed intercept is not a vector of ones. + +Constant detection is **scale equivariant**: multiplying an identified nonzero constant column by any nonzero unit-conversion factor must not change whether it is classified as a constant. The tolerance is therefore relative to that column's own magnitude; there is no `max(1, scale)` absolute floor. An exactly zero column is not treated as an intercept. + +## Normative post-review overrides + +Final PR review exposed additional correctness boundaries that supersede conflicting early-plan wording in Sections 5.1--5.3, 5.5, 6, 7, and 12 of `panel_p1_stage_b_diagnostics_plan.md`. + +### Hausman sample/design identity + +The earlier O(k) low-order-moment fingerprint is retained only as optional audit metadata; it is **not** sufficient proof that FE and RE used the same aligned numerical sample. Distinct row sequences can share sums, sums of squares, and index-weighted first moments. + +A standard FE-vs-RE Hausman comparison may absorb the common intercept in the FE nuisance-effect space while estimating an explicit intercept in RE. Consequently identity is defined on the **canonical common-slope design**, not on raw full design matrices that differ only by an RE intercept. + +The authoritative Stage-B identity contract is: + +- remove the identified explicit constant column, if any, before constructing the Hausman design identity; +- compute a versioned SHA-256 digest over every aligned float64 **slope-X/y** value in row order, including shape framing; +- compare exact canonical slope feature names, the canonical slope count, the digest, and the aligned entity-code signature; +- do **not** require FE and RE `has_constant` metadata to agree when their canonical slope design is identical; +- retain a `coefficient_indices` map from canonical slope positions back to each fitted model's original coefficient/covariance positions, so an RE intercept at position 0 produces slope indices `(1, 2, ...)` rather than silently shifting the comparison; +- require exact digest equality rather than an `allclose` tolerance for identity; +- on CuPy/Torch fits, transfer the canonical slope X/y to host in bounded chunks solely for hashing; no single extra full-design host allocation is required; +- persist only the digest, coefficient-index map, and compact metadata after hashing, never a second retained CPU copy of the design. + +This bounded hashing transfer is an explicit exception to the early-plan statement that Hausman may transfer only O(k) fingerprint scalars. The statistical estimation, covariance construction, and fit-statistic reductions remain backend-native; the exception exists only to make the identity check collision-resistant. + +The full-content digest is constructed only when the fitted FE model is actually in the Stage-B Hausman domain: one-way entity FE with nonrobust covariance. Robust/clustered, time-only, and two-way FE are rejected before identity comparison and must not pay the full X/y host-hash cost. `RandomEffects` remains a potential Hausman input and therefore retains the identity contract. + +For array input without explicit feature names, canonical slopes are renumbered after removal of an RE-only constant (`x1`, `x2`, ...), while the stored coefficient-index map preserves the original RE positions. For named/formula designs, the canonical slope names are retained and the intercept is excluded. + +### Scale-equivariant numerical tolerances + +RSS, covariance matrices, and coefficient differences carry units. Numerical applicability/rank decisions must therefore be invariant to a change of units. In particular: + +- model-F and pooling-F RSS tolerances scale with the compared RSS values and do not use an absolute `max(1, RSS)` floor; +- Hausman eigenvalue/range tolerances scale with the covariance/difference norms and do not impose a unit-sized absolute floor; +- multiplying `y` and fitted coefficients by `c`, or multiplying a Hausman coefficient difference by `c` and its covariance difference by `c^2`, must leave the dimensionless test statistic/applicability unchanged up to floating-point roundoff. + +The ordinary dimensionless post-computation guard on a near-zero negative test statistic may retain a unit floor because the statistic itself is dimensionless. + +### Classical model-F exact-fit boundary + +The early generic “unavailable when unrestricted RSS is zero” wording is too coarse. The final Stage-B contract is: + +- if unrestricted RSS is numerically zero and restricted RSS is materially positive, report the limiting classical result `F = inf`, `p = 0`, with the ordinary numerator/denominator df; +- if both restricted and unrestricted RSS are numerically zero, the joint-slope statistic is indeterminate and remains unavailable with an explicit reason; +- material nesting violations remain inapplicable rather than being silently clipped. + +### RandomEffects explicit constant + +`RandomEffects` must detect an actual nonzero constant column in the supplied level design. Because Swamy-Arora quasi-demeaning transforms that column and an unbalanced panel generally does not leave a vector of ones, the transformed constant column itself is the restricted design for adjusted R²/model-F accounting. No implicit intercept is invented when the level design has none. + +Physical CUDA validation must include balanced and unbalanced explicit-constant RandomEffects cases, in addition to the ordinary no-explicit-constant cases, and must compare the constant/restricted-design metadata as well as numerical fit statistics. The same runner must also evaluate FE-vs-RE Hausman with the RE-only explicit constant on both balanced and unbalanced panels, so the canonical slope digest and coefficient-index mapping are exercised on CuPy and Torch CUDA. + +The final physical runner therefore contains 17 distinct estimator model cases per backend and four Hausman diagnostic cases per backend: ordinary and explicit-RE-constant parameterizations on balanced and unbalanced panels. + +## Review status + +- **[HIGH][INFER] fixed in specification** — Stage-B poolability/model-F inference no longer reuses a legacy FE residual df that differs from the standard nuisance-effect rank convention. +- **[HIGH][INFER] fixed after review** — two-way FE nuisance rank uses the observed incidence-graph component count (`N + T - C`) instead of assuming every incomplete panel is connected. +- **[HIGH][API/INFER] fixed after review** — Hausman identity uses a collision-resistant full-content digest; low-order moments alone are no longer accepted as proof of sample identity. +- **[CRITICAL][INFER] fixed after fresh review** — F/Hausman applicability tolerances and explicit-constant detection are scale equivariant instead of imposing a unit-sized absolute floor. +- **[HIGH][INFER] fixed after fresh review** — an exact FE fit with positive pooled RSS reports the limiting pooling `F=inf, p=0`; the both-zero case remains explicitly inapplicable. +- **[HIGH][INFER/API] fixed after thread-aware re-review** — Hausman identity is canonicalized to common slopes, so an FE-absorbed intercept and an RE-only explicit constant no longer cause a false identity rejection or coefficient-index shift. +- **[HIGH][TEST/BACKEND] fixed locally after fresh review** — the physical runner includes balanced/unbalanced explicit-constant RandomEffects cases plus explicit-constant Hausman diagnostics and checks the restricted-design contract; exact-head P100 execution is still required after the final code head is fixed. +- **[MEDIUM][PERF] fixed/measurement pending** — FE fits outside the Hausman domain no longer build the full-content digest; a dedicated physical benchmark measures the remaining digest overhead for Hausman-compatible one-way FE/RE fits. +- **[MEDIUM][INFER] fixed in specification** — FE adjusted R² uses nuisance-rank-consistent total df rather than provisional `n-1`. +- **[MEDIUM][INFER] fixed in specification** — overall/between R² centering is explicitly separated from the pooling-F common-constant correction. +- **[MEDIUM][INFER] fixed after review** — RandomEffects explicit-constant diagnostics retain the transformed intercept in the restricted fit-space definition. +- **[MEDIUM][INFER] fixed after review** — exact unrestricted fits with a nonzero restricted RSS report `F=inf, p=0` instead of being discarded as unavailable. diff --git a/dev/plans/panel_p1_stage_b_diagnostics_plan.md b/dev/plans/panel_p1_stage_b_diagnostics_plan.md new file mode 100644 index 000000000..5e03a83f0 --- /dev/null +++ b/dev/plans/panel_p1_stage_b_diagnostics_plan.md @@ -0,0 +1,524 @@ +# Panel Tier-1 Stage B — diagnostics and fit-statistics plan + +Issue: #93 +Stage-A base: PR #119 / merge commit `e9e0ec43b8b2cfcd7600013a60cb02100c72f4f5` +Branch: `agent/panel-p1-stage-b-diagnostics` + +## 1. Scope and impact classification + +Stage B adds the user-visible specification tests and fit statistics promised by Issue #93 while preserving the Stage-A estimator transformations, coefficient estimates, covariance definitions, prediction contracts, and strict-device rules. + +Active impact axes: + +- **Public API** — new structured diagnostic functions/results and new fitted-statistics attributes. +- **Inference** — Hausman, pooling F, Breusch–Pagan LM, model F statistics, p-values, and degrees of freedom. +- **Backend** — all sufficient-statistic accumulation must work on NumPy, CuPy, and Torch without explicit-device fallback. +- **Formula** — diagnostic metadata and sample identity must remain aligned after Patsy missing-row filtering. +- **Docs/artifacts** — EN/CN model docs, changelogs, external-definition matrix, and physical-GPU evidence. + +Inactive gates: + +- **Loss / penalty / solver / CV** — Stage B does not modify an optimization objective, regularization, solver, or tuning path. +- **Performance benchmark** — no speedup claim is planned. A physical runner is a correctness/provenance gate, not a timing benchmark. Any material memory/performance regression discovered during review reactivates this gate. + +Validation target: `remote-full` before Stage B is called COMPLETE. + +## 2. Capability decisions + +| Model / capability | backend | inference | formula | Stage-B diagnostics / fit stats | +| --- | --- | --- | --- | --- | +| `PanelOLS` | three-backend | supported | supported | pooling F; standard within/between/overall R²; adjusted R²; classical model F; Hausman input | +| `RandomEffects` | three-backend | supported | supported | within/between/overall R²; adjusted R²; classical model F; Hausman input | +| `PooledOLS` | three-backend | supported | supported | overall R² always; within/between R² and BP-LM when `entity_ids` supplied; adjusted R²; classical model F | +| `BetweenOLS` | three-backend | supported | supported | within/between/overall R²; adjusted R²; classical model F | +| `FirstDifferenceOLS` | three-backend | supported | supported | within/between/overall R²; adjusted R² on differenced fit basis; classical model F | +| `FamaMacBeth` | three-backend | supported | supported | overall R² always; within/between R² only if optional `entity_ids` supplied; no residual-OLS model F is synthesized from the beta-series covariance | + +`FamaMacBeth` remains a special covariance/inference family. Stage B must not route it through residual OLS sandwich or claim that a pooled-OLS F statistic is its beta-series joint significance test. + +## 3. External definition matrix + +Stage B will record and test against the following definitions. External packages are references, not authorities that override statgpu's existing estimator contracts. + +### 3.1 Parameter-based R² + +Primary alignment: `linearmodels` 7.0. + +References: + +- https://bashtage.github.io/linearmodels/panel/faq.html +- https://bashtage.github.io/linearmodels/panel/mathematical-formula.html +- https://bashtage.github.io/linearmodels/_modules/linearmodels/panel/model.html + +`linearmodels` explicitly distinguishes parameter-based R² from correlation-based measures used by some other software. Stage B adopts the parameter-based family because it evaluates the actual estimated coefficient vector. + +For unweighted data and a fitted coefficient vector `beta`: + +- **Overall**: residual `e_o = y - X beta`; center `y` only when the corresponding level equation has an identified explicit/implicit constant; `R²_o = 1 - SSE_o / TSS_o`. +- **Between**: form entity means `y_bar_i`, `X_bar_i`; residual `e_b = y_bar - X_bar beta`; center the between response when the corresponding level equation has an identified explicit/implicit constant; `R²_b = 1 - SSE_b / TSS_b`. +- **Within**: entity-demean `y` and `X`; `e_w = y_within - X_within beta`; `R²_w = 1 - SSE_w / TSS_w`. + +For the new standardized `fit_statistics_` fields, a zero total sum of squares follows the `linearmodels` convention and reports `0.0`, with `metadata['degenerate_total_ss'][] = True`. Stage B does **not** rewrite legacy estimator attributes that already have another degenerate-TSS behavior; those remain frozen for compatibility. + +Important compatibility rule: Stage A froze the existing public `PanelOLS.rsquared_within`. For two-way FE that legacy attribute is computed on the full entity+time transformed fit, whereas the standard `linearmodels` `rsquared_within` is entity-within. **Stage B must not silently change the legacy attribute.** The new `fit_statistics_.rsquared_within` is the explicitly documented standard entity-within measure; metadata records `legacy_rsquared_within` when the existing attribute differs (notably two-way FE). For one-way entity FE the two coincide up to numerical tolerance. + +### 3.2 Classical model F statistic + +Primary alignment: `linearmodels.PanelResults.f_statistic`. + +Reference: + +- https://bashtage.github.io/linearmodels/panel/panel/linearmodels.panel.results.PanelResults.f_statistic.html + +For a model fit in its estimator-specific estimation space: + +`F = ((RSS_R - RSS_U) / q) / (RSS_U / df_resid)` + +where the restriction sets all estimable non-constant slope coefficients to zero, `q` is the **effective restriction rank** (`rank_unrestricted - rank_restricted`, not blindly the raw column count), and `df_resid` is the model's established residual degrees of freedom. + +Stage B's `PanelFitStatistics.f_statistic` is the **classical homoskedastic model F**. It does not silently turn into a robust Wald statistic when `cov_type='robust'` or `'clustered'`. Robust Wald/model tests are a separate contract and are not added in Stage B. + +Estimator fit spaces: + +- `PooledOLS`: pooled level design including its intercept. +- `PanelOLS`: the existing effect-transformed design and current absorbed-effect residual df. +- `RandomEffects`: the existing quasi-demeaned GLS design. +- `BetweenOLS`: entity-mean regression. +- `FirstDifferenceOLS`: first-difference regression. +- `FamaMacBeth`: do not manufacture a residual-OLS F statistic; beta-series joint Wald inference may be a later explicitly named capability. + +If there are no estimable non-constant restrictions, the F field is unavailable with an explicit metadata reason rather than dividing by zero. + +### 3.3 Pooling F / fixed-effect significance test + +Primary alignment: `linearmodels.PanelEffectsResults.f_pooled` and `plm::pFtest`. + +References: + +- https://bashtage.github.io/linearmodels/panel/panel/linearmodels.panel.results.PanelEffectsResults.f_pooled.html +- https://bashtage.github.io/linearmodels/_modules/linearmodels/panel/model.html +- https://rdrr.io/cran/plm/man/pFtest.html +- https://rdrr.io/cran/plm/src/R/test_general.R + +For the same aligned estimation sample and regressors: + +`F_pool = ((RSS_pool - RSS_FE) / df_num) / (RSS_FE / df_resid_FE)`. + +The restricted pooled model must be constructed with the **same constant convention as the FE model's nested null**, including the no-explicit-constant correction used by `linearmodels`: + +- when the level design contains an identified explicit constant, fit the pooled regression with that level design; +- when the FE specification has effects but no explicit constant column, project both pooled `y` and pooled `X` off the common constant before the pooled slope fit, and reduce the effect-test numerator df by one. This prevents the common mean from being incorrectly counted as a tested fixed effect. + +The primary numerator-df calculation is the nested-model rank/df difference, equivalent to `df_resid_pool - df_resid_FE` after the constant correction. The implementation records both effective ranks and the final df in metadata; it never hard-codes `N-1`, `T-1`, or `N+T-2`. + +Contract: + +- only `PanelOLS` fits with at least one included effect are applicable; +- pooling comparison is recomputed internally on the same post-formula/post-missing-data sample during `PanelOLS.fit()`; users do not have to fit a second `PooledOLS` object; +- the test is classical/homoskedastic, matching the external definitions; covariance type does not change the RSS-based statistic; +- null: all included fixed effects are jointly zero; +- alternative: at least one included effect is nonzero; +- distribution: `F(df_num, df_resid_FE)`. + +Numerical nesting checks: + +- if `RSS_pool - RSS_FE >= 0`, use it directly; +- if the difference is negative only within a scale-aware floating-point tolerance, normalize it to zero and record `metadata['roundoff_normalized']=True`; +- a materially negative difference indicates a violated nesting/rank contract and returns `applicable=False` with an explicit reason; it is never silently clipped into a valid-looking positive statistic. + +### 3.4 Breusch–Pagan LM for entity random effects + +Primary alignment: `plm::plmtest(type='bp', effect='individual')`, including the Baltagi–Li unbalanced-panel version. + +References: + +- https://rdrr.io/cran/plm/man/plmtest.html +- https://rdrr.io/cran/plm/src/R/test_general.R + +The test uses **pooled-OLS residuals**. Let `e_it` be pooled residuals, `n` the number of observations, and `T_i` the observation count of entity `i`: + +`CP = sum_it e_it^2` + +`A1 = sum_i (sum_t e_it)^2 / CP - 1` + +`M11 = sum_i T_i^2` + +`LM1 = n * sqrt(1 / (2 * (M11 - n))) * A1` + +`LM_BP = LM1^2 ~ chi2(1)`. + +This formula applies to balanced and unbalanced panels and matches the current `plm` implementation attributed to Baltagi and Li (1990) for incomplete panels. + +Stage-B scope is **one-way entity BP-LM** because statgpu's current `RandomEffects` is one-way entity RE. A two-way BP statistic would test a broader error-components model that statgpu does not currently estimate and is not exposed under the RE-vs-pooled diagnostic name in this PR. + +Contract: + +- add optional `entity_ids=None` to `PooledOLS.fit()`; existing calls remain unchanged; +- when entity IDs are supplied, formula row filtering aligns them through the existing side-array machinery; +- pooled residual group sums and counts are accumulated on the selected backend during fit; full numerical residual arrays are not copied to CPU for the test; +- if `cov_type='hac'` and `time_index` causes a stable numerical row reorder, the aligned `entity_ids` diagnostic codes are reordered by the **same** permutation before any residual grouping, R² accumulation, or sample fingerprinting; diagnostic metadata may never remain in pre-sort order while X/y are post-sort; +- null: entity random-effect variance is zero (pooled OLS sufficient); +- alternative: a nonzero entity random-effect component is present; +- distribution: `chi2(1)`; +- require at least two entities, positive pooled residual sum of squares, and `M11 > n`; otherwise return an inapplicable structured result with the exact reason. + +The name/documentation must distinguish this panel error-components BP-LM from the cross-sectional heteroskedasticity Breusch–Pagan test. + +### 3.5 Classical Hausman FE-vs-RE + +Primary alignment: Hausman (1978), `plm::phtest` original quadratic-form method, and Stata `hausman`. + +References: + +- https://rdrr.io/cran/plm/man/phtest.html +- https://rdrr.io/cran/plm/src/R/test_general.R +- https://www.stata.com/manuals/rhausman.pdf + +For common non-intercept coefficients: + +`d = beta_FE - beta_RE` + +`D = V_FE - V_RE` + +`H = d' D^{-1} d ~ chi2(q)` for full-rank `D`. + +Stage-B applicability is deliberately stricter than a blind matrix solve: + +- `fe_model` must be a fitted `PanelOLS` with `entity_effects=True` and `time_effects=False`; +- `re_model` must be a fitted `RandomEffects`; +- both models must represent the same aligned estimation sample and common slope design; +- the original quadratic-form test is available only for the current **classical/nonrobust** covariance pair. A robust/clustered FE covariance is not relabeled as a robust Hausman test. `plm` documents robustification through an auxiliary-regression Hausman variant, which is outside Stage B; +- intercepts are excluded, following `plm` and Stata; common slope names/order are matched explicitly; +- no common estimable slope => inapplicable. + +Covariance-difference handling: + +1. symmetrize `D` numerically as `(D + D.T)/2` on the small final matrix; +2. compute an eigenvalue/rank tolerance scaled by matrix norm and machine epsilon; +3. if an eigenvalue is materially negative, return `applicable=False` with reason `covariance difference is not positive semidefinite`; do **not** force a statistic by absolute values or eigenvalue clipping; +4. if `D` is positive semidefinite but rank-deficient, statgpu may use a Moore–Penrose inverse on the identified range and set chi-square df to `rank(D)`, but only if `d` lies in the column space within tolerance; record `metadata['used_pinv']=True`, numerical rank, tolerance, and `metadata['definition_extension']='singular PSD generalized-inverse Hausman'`; +5. this singular-PSD generalized-inverse case is a documented statgpu extension to the ordinary full-rank `plm`/Stata path, not claimed as byte-for-byte external behavior; +6. if `d` has a material component in the null space, return inapplicable rather than pretending the unidentified direction contributes zero; +7. a computed statistic slightly below zero only from roundoff may be normalized to zero with metadata; a materially negative statistic is inapplicable. + +This makes singular/indefinite behavior explicit as required by Issue #93 and avoids generic linear-algebra exceptions. + +## 4. Public API proposal + +### 4.1 Result objects + +Keep the Stage-A frozen dataclasses and make them public through `statgpu.panel` and top-level `statgpu`: + +- `PanelTestResult` +- `PanelFitStatistics` + +`PanelTestResult` fields remain: + +- `statistic` +- `pvalue` +- `distribution` +- `df` +- `null` +- `alternative` +- `applicable` +- `reason` +- `metadata` + +No exception is used for an econometrically inapplicable but otherwise well-formed diagnostic. Programming errors (wrong object type, unfitted object when a fitted model is required, malformed metadata length) remain exceptions. + +### 4.2 Diagnostic functions + +Add public functions in `statgpu.panel._diagnostics` and export them from `statgpu.panel` and top-level `statgpu`: + +```python +hausman_test(fe_model: PanelOLS, re_model: RandomEffects) -> PanelTestResult +pooling_f_test(fe_model: PanelOLS) -> PanelTestResult +breusch_pagan_lm_test(pooled_model: PooledOLS) -> PanelTestResult +``` + +These functions consume fitted model state/sufficient statistics. They do not accept arbitrary covariance matrices as a pseudo-public escape hatch in Stage B. + +Convenience estimator methods may delegate exactly to these functions: + +```python +fe.pooling_f_test() +fe.hausman_test(re) +pooled.breusch_pagan_lm_test() +``` + +If methods are added, there must be one implementation source of truth in `_diagnostics.py`; methods are thin delegates only. + +### 4.3 Fit statistics + +After every supported fit, expose: + +```python +model.fit_statistics_: PanelFitStatistics +``` + +with standard fields: + +- `rsquared_within` +- `rsquared_between` +- `rsquared_overall` +- `rsquared_adj` +- `f_statistic` +- `f_pvalue` +- `f_df` +- `metadata` + +`metadata` must identify the R² convention and estimator-specific adjusted-R² basis, and give explicit reasons for unavailable fields. + +Do not rename/remove the existing `PanelOLS.rsquared_within` or estimator-specific `rsquared` attributes in Stage B. + +## 5. Internal architecture + +### 5.1 Shared diagnostics helpers + +Create `statgpu/panel/_diagnostics.py` for: + +- construction of applicable/inapplicable `PanelTestResult` objects; +- parameter-based within/between/overall R² sufficient-statistic helpers; +- adjusted-R² helper; +- classical model-F helper; +- pooling-F helper; +- BP-LM helper; +- Hausman small-matrix comparison and applicability logic; +- compact backend-native sample/design fingerprint construction. + +Core observation-scale operations take `xp` and backend arrays. Only final scalars, O(k) numerical fingerprint components, small `k x k` covariance matrices, feature names, and index metadata may be converted to NumPy. + +### 5.2 Covariance persistence + +`BasePanelModel._panel_store_ols_inference()` already returns `cov_params`. Stage B stores the final small covariance matrix needed by diagnostics as an **internal** CPU ndarray (for example `_panel_cov_params`) on FE/RE and other relevant OLS-style models while preserving all existing `bse_/tvalues_/pvalues_/conf_int_` values. + +Do not create a new universal public `cov_params_` contract merely to implement Hausman. `FamaMacBeth.cov_params_` is an existing estimator-specific attribute and remains unchanged. + +The conversion is limited to `k x k`; no full design or residual matrix is copied to host merely for Hausman. + +### 5.3 Sample/design identity for Hausman + +FE/RE fits store compact immutable diagnostic metadata sufficient to reject mismatched samples/designs without retaining a full second host copy of X/y. + +Identity components: + +- `nobs`; +- aligned entity label/code sequence signature and entity counts; +- aligned retained-row signature for formula fits when available; +- feature-name sequence after formula/model-matrix construction; +- numeric design width and intercept-presence metadata; +- effect specification; +- a **backend-native numerical fingerprint** of aligned `X`, `y`, and row order, reduced on the selected backend to O(k) scalars before host conversion. + +The numerical fingerprint must include multiple independent deterministic moments, e.g. per-column/y sum, sum of squares, and an index-weighted first moment (using a deterministic row weight sequence), all accumulated in float64. It is an integrity check rather than a cryptographic hash. Comparison uses a scale-aware floating-point tolerance so the same float64 data on NumPy/CuPy/Torch are accepted while materially different samples/designs are rejected. + +For raw array fits without formula names, deterministic positional slope names (`x1`, `x2`, ...) are used for coefficient matching **in addition to** the numerical fingerprint. Same `nobs`, entity counts, or shape alone is never treated as proof that the samples match. + +If identity metadata are missing or disagree materially, `hausman_test` returns `applicable=False` with the precise mismatch reason rather than guessing. + +### 5.4 R² accumulation + +Compute R² variants during `fit()` while backend numerical arrays are available, then store only scalar results. Do not retain a second full copy of `X`/`y` for later diagnostics. + +For estimators where the model coefficient includes an explicit intercept, use the existing design convention to determine centering. For FE, standard R² variants use the level slope vector and the level/entity-demeaned data as defined above; fixed effects themselves are not inserted into overall/between predictions. + +For `PooledOLS` and `FamaMacBeth`, add optional `entity_ids=None` to `fit()` only to unlock panel decomposition metrics; coefficient estimates are unchanged. Absence of IDs leaves within/between fields as `None` with reasons in metadata. + +Any estimator-specific row reorder (currently notably PooledOLS HAC time sorting and FirstDifference sorting/differencing) must carry diagnostic metadata through the exact same permutation/transform before sufficient statistics are accumulated. + +### 5.5 Adjusted R² + +Use an explicit residual-variance / total-variance definition on each estimator's **primary fit space**: + +`R²_adj = 1 - (RSS / df_resid) / (TSS / df_total)`. + +`df_total` is the effective total-sum-of-squares df in that fit space: + +- centered fit with an identified constant / implicit absorbed mean: `n_fit - 1`; +- uncentered fit with no constant: `n_fit`. + +For FE, `n_fit` is the number of retained observations while `df_resid` remains the established Stage-A residual df including absorbed effects. For BetweenOLS, `n_fit` is the number of entity means; for FirstDifferenceOLS, the number of retained first differences; for RE, the quasi-demeaned sample size; for PooledOLS, pooled sample size. + +`metadata['rsquared_adj_basis']` records `fit_space`, `df_total`, and `df_resid` so the convention is auditable. + +FamaMacBeth is not assigned this residual-OLS adjusted R²; its average period adjusted R² is a distinct statistic in some software and is not silently put into `PanelFitStatistics.rsquared_adj` without a separate explicit contract. + +## 6. Edge-case and failure contracts + +Tests must cover at least: + +- unfitted models passed to diagnostic functions; +- wrong estimator types; +- FE model with no effects passed to pooling F; +- time-only/two-way FE passed to one-way FE-vs-RE Hausman; +- FE robust/clustered covariance passed to classical Hausman; +- same shapes/entity counts but materially mismatched X/y samples in Hausman; +- same data across NumPy/CuPy/Torch accepted by fingerprint tolerance; +- no common slope coefficients; +- singular PSD Hausman covariance difference with identified `d`; +- singular PSD difference with `d` outside the identified range; +- materially indefinite covariance difference; +- roundoff-level negative Hausman/pooling quantities versus materially negative violations; +- BP-LM with one entity, singleton-only entities, zero pooled RSS, and unbalanced panels; +- PooledOLS HAC sorting with unsorted `time_index` and entity IDs, proving X/y/entity diagnostic alignment after sorting; +- constant outcome / zero TSS standardized R² behavior (`0.0` plus degenerate metadata) while legacy attributes remain unchanged; +- rank-deficient pooled design using effective restriction rank; +- formula missing-row alignment for IDs; +- explicit CUDA/Torch request with unavailable backend must fail rather than fall back. + +## 7. Test plan + +### 7.1 Analytic unit tests + +Add `dev/tests/test_panel_stage_b_diagnostics.py` with deterministic small panels and hand-computed sufficient statistics for: + +- pooling F formula, no-explicit-constant correction, and df; +- balanced and unbalanced entity BP-LM; +- full-rank Hausman quadratic form; +- singular/indefinite Hausman applicability behavior; +- parameter-based within/between/overall R²; +- adjusted R² basis; +- classical model F. + +### 7.2 Existing-estimator regression + +Extend the Stage-A golden suite or add a Stage-B compatibility file to assert that Stage B does not change existing: + +- coefficients; +- bse/t/p/CI; +- predictions; +- existing `rsquared` / legacy `PanelOLS.rsquared_within` values; +- RE variance components/theta; +- FMB beta-series covariance. + +### 7.3 Three-backend parity + +For every new observation-scale statistic, compare NumPy/CuPy/Torch results on the same deterministic balanced and unbalanced panels. + +Hosted optional-backend tests may use existing CPU-compatible Torch coverage, but physical CuPy/Torch CUDA remains the final backend acceptance gate. + +Target numeric parity unless an external implementation uses a different estimator definition: + +- deterministic analytic/backend parity: `rtol <= 5e-8`, `atol <= 5e-9` by default; +- external model-statistics comparisons: tighter where definitions are identical, with any relaxed tolerance justified per field. + +### 7.4 Formula tests + +Cover: + +- explicit/implicit intercept behavior; +- categorical terms and interactions already supported by panel formula parsing; +- missing-row alignment of `entity_ids`/`time_ids`; +- effect tokens for PanelOLS; +- diagnostics computed from exactly the retained estimation sample. + +### 7.5 External alignment + +Add a reproducible external-comparison script/artifact that records definitions rather than just numbers. + +Python `linearmodels` comparisons: + +- PanelOLS one-way and two-way: coefficients, standard R² variants, classical model F, pooled F; +- PooledOLS, BetweenOLS, FirstDifferenceOLS, RandomEffects: R² variants and model F where definitions/parameterization match; +- note any Swamy-Arora parameterization difference before comparing RE quantities. + +R `plm` comparisons: + +- `pFtest(within, pooling)`; +- `plmtest(pooling, type='bp', effect='individual')` on balanced and unbalanced panels; +- `phtest(within, random)` for a well-conditioned classical Hausman example. + +For the singular-PSD generalized-inverse Hausman extension, use an analytic matrix fixture rather than claiming `plm`/Stata parity. + +Stata is documentation/reference-only unless a licensed callable environment is available. Record its Hausman formula/interpretation, not unverifiable claimed numeric parity. + +## 8. Physical GPU acceptance + +Add `dev/benchmarks/validate_panel_stage_b_gpu.py` as a correctness/provenance runner. + +Requirements: + +- exact expected SHA; +- clean working tree; +- requested backend must actually execute (`cupy` / CUDA Torch); +- balanced and unbalanced data; +- compare new fit statistics and diagnostics against NumPy references; +- include at least PanelOLS pooling F, PooledOLS BP-LM, FE/RE Hausman prerequisites, R² variants, adjusted R², and model F; +- include an unsorted-HAC PooledOLS case with entity IDs to guard metadata permutation; +- record environment/package/GPU provenance and max absolute differences in JSON. + +No performance claim is made from this runner. + +## 9. Documentation plan + +Update, in EN-first / CN-follow order: + +- panel model documentation covering `PanelOLS`, `RandomEffects`, `PooledOLS`, and shared diagnostics; +- model/index capability table if present; +- root `CHANGELOG.md`; +- `docs/en/changelog.md`; +- `docs/cn/changelog.md`. + +Documentation must explicitly state: + +- parameter-based versus correlation-based R²; +- standardized zero-TSS behavior and preservation of legacy attributes; +- the preserved legacy `PanelOLS.rsquared_within` compatibility distinction for two-way FE; +- classical/homoskedastic nature of model F and pooling F; +- pooling-F implicit-constant correction; +- BP-LM is the panel error-components test, not the heteroskedasticity BP test; +- classical Hausman restrictions, data-identity checks, and explicit singular/indefinite behavior; +- the generalized-inverse singular-PSD Hausman case is a statgpu extension with rank df; +- which statistics are unavailable without entity IDs; +- three-backend/no-silent-fallback behavior. + +## 10. Implementation sequence and gates + +1. **Plan review gate** — audit this document against Issue #93, Stage-A contracts, external definitions, and repo workflow. Fix all HIGH and relevant MEDIUM findings before source edits. +2. **Result/API substrate** — export Stage-A result dataclasses; add `_diagnostics.py`; define applicability helpers and public exports. +3. **Sufficient-statistic helpers** — R²/model-F/pooling-F/BP helpers with NumPy tests first, written backend-generically from the start. +4. **Estimator integration** — persist small internal covariance matrices; populate `fit_statistics_`; compute pooling/BP contexts during fit; add optional entity metadata where required without changing coefficients; propagate every numerical row transform to diagnostics metadata. +5. **Hausman integration** — numerical sample/design fingerprints, common-coefficient matching, PSD/rank logic. +6. **Three-backend targeted tests** — NumPy/CuPy/Torch parity and strict-device failure behavior. +7. **External Python/R alignment** — run strongest available local baselines; if R unavailable locally, retain exact script/command and mark only that external gate remote-pending. +8. **Full hosted CI** — complete test suite, Python matrix, static/docs, maintenance, release-package/front-end gates as applicable. +9. **Physical GPU** — exact clean-head CuPy/Torch runner. +10. **Auto-fix review loop** — fresh code review after evidence; no unresolved CRITICAL/HIGH/in-scope MEDIUM findings before promotion. +11. **Docs/changelog sync** — no user-visible capability advertised before tests and definitions are final. + +## 11. Explicit non-goals for Stage B + +- robust/auxiliary-regression Hausman test; +- two-way random effects or two-way BP-LM as an RE-vs-pooled selector; +- RandomEffects robust covariance; +- HC0/HC2/HC3 expansion; +- Driscoll–Kraay; +- cluster small-sample expansion; +- FamaMacBeth residual-OLS covariance or residual-OLS model F; +- changing Stage-A estimator coefficients, covariance normalizations, prediction behavior, or legacy R² attributes. + +These remain Stage C or later work unless a blocking correctness dependency is discovered. + +## 12. Plan-review findings closed before implementation + +- **[HIGH][INFER] fixed** — pooling F now specifies the implicit-common-constant correction and corresponding numerator-df decrement when the FE design lacks an explicit constant, matching the nested linearmodels definition. +- **[HIGH][API/INFER] fixed** — Hausman sample compatibility no longer relies on `nobs`/entity order alone; the plan requires a backend-native O(k) numerical X/y/order fingerprint. +- **[HIGH][BACKEND] fixed** — PooledOLS HAC sorting must apply the identical permutation to entity diagnostic metadata before BP/R²/fingerprint accumulation. +- **[MEDIUM][INFER] fixed** — standardized fit-stat R² adopts linearmodels' zero-TSS `0.0` convention with explicit degenerate metadata while preserving existing legacy attributes. +- **[MEDIUM][API] fixed** — Hausman only needs an internal small covariance matrix; Stage B will not create a new universal public `cov_params_` contract. +- **[MEDIUM][INFER] fixed** — singular-PSD generalized-inverse Hausman behavior is labeled explicitly as a statgpu extension and validated analytically rather than presented as direct plm/Stata parity. + +## 13. Completion criteria + +Stage B can be called COMPLETE only when: + +- the three public diagnostic tests return structured `PanelTestResult` objects with documented applicability behavior; +- `fit_statistics_` is populated consistently where defined; +- existing numerical estimator behavior remains frozen by regression tests; +- all new observation-scale operations have NumPy/CuPy/Torch parity with no silent fallback; +- linearmodels/plm definition alignment is recorded and tested where callable; +- formula/missing-row alignment is covered; +- hosted CI passes on the final exact head; +- physical CuPy/Torch evidence passes on the final exact clean head; +- fresh review has no unresolved CRITICAL, HIGH, or in-scope MEDIUM findings; +- EN/CN docs and all three changelogs are synchronized. diff --git a/dev/reviews/pr122_physical_gpu_validation.md b/dev/reviews/pr122_physical_gpu_validation.md new file mode 100644 index 000000000..b3e688142 --- /dev/null +++ b/dev/reviews/pr122_physical_gpu_validation.md @@ -0,0 +1,71 @@ +# PR #122 Panel Stage B physical GPU validation + +## Physical acceptance status + +**PHYSICAL_GPU_ACCEPTED** for the Stage-B runner measured at exact clean implementation head `2701aa9feb3796c33c94e6480fcb78c80c6a809c`. + +This status covers the active physical correctness/backend-provenance gate only. Exact-final-head hosted CI and a fresh `.claude/skills/code-review.md` review remain lifecycle gates after evidence promotion; the physical runner itself must not change without another P100 rerun. + +## Accepted full P100 artifact + +- path: `results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json` +- measurement SHA: `2701aa9feb3796c33c94e6480fcb78c80c6a809c` +- artifact repository commit: `0d0d654d825cea872672f27d02107a58048b345f` +- Git blob: `fa3a253e6d882a4e69be29e7e3b1dce7b223b9a9` +- schema version: 2 +- working tree clean: true +- top-level status: success +- GPU: Tesla P100-SXM2-16GB +- Python: 3.9.16 +- NumPy: 1.24.2 +- SciPy: 1.10.1 +- Torch: 2.0.0 +- timing collected: false + +For both CuPy and Torch, all 17 estimator cases passed with the requested backend actually executed and no CPU fallback. + +## Hausman branch coverage + +Each backend passed five Hausman diagnostics: + +1. `hausman_balanced` — structured `applicable=false` / non-PSD covariance difference; +2. `hausman_explicit_re_constant_balanced` — structured `applicable=false`; +3. `hausman_unbalanced` — structured `applicable=false`; +4. `hausman_explicit_re_constant_unbalanced` — structured `applicable=false`; +5. `hausman_applicable_nonzero_effect` — **`applicable=true`**, df=1. + +For the dedicated applicable fixture, CuPy recorded statistic `1.1965942530851057` and p-value `0.27400344142676447`; Torch recorded statistic `1.1965942530849238` and p-value `0.2740034414268009`; the NumPy reference is statistic `1.196594253085033`, p-value `0.274003441426779`, df=1. Maximum statistic differences are `7.26e-14` (CuPy) and `1.09e-13` (Torch), and all FE/RE coefficient/covariance differences are below `1e-12`. + +The fixture uses seed 20260810, 12 entities, 4 observations per entity, one slope, entity-effect scale 0.005, and noise scale 0.1. Its NumPy FE-minus-RE diagnostic covariance margin is `4.6413153162319366e-05`, safely above the hosted stability guard. + +## Supplementary disconnected-FE evidence + +The focused disconnected two-way FE artifact remains valid supplementary evidence for an unchanged numerical path: + +- path: `results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json` +- measurement SHA: `a57efcea29b0e87ecb89865c5a6902d5773812c6` +- artifact repository commit: `72b3279d2028e8ec2af30e138e123aceb611ae8c` +- Git blob: `3bda0b2040479ba8201e2722eb990ba086c3f3b9` +- legacy residual df: 0 +- component-aware/public residual df: 1 +- effect rank: 7 +- incidence components: 3 +- CuPy/Torch confidence intervals agree with NumPy at floating-point noise. + +It is deliberately identified as historical supplementary evidence rather than relabeled as a `2701aa9feb3796c33c94e6480fcb78c80c6a809c` measurement. + +## Promoted canonical evidence + +- canonical path: `results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json` +- canonical SHA-256: `2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf` +- source id: `panel-stage-b-pr122-20260809-2056f836bfe2` +- validation rows: 44 = 17 estimator cases x 2 backends + 5 Hausman diagnostics x 2 backends +- timing/speedup: absent by contract + +The parser requires the dedicated applicable row to preserve finite nonnegative statistic, p-value in [0,1], and positive df; missing or invalid numeric evidence fails closed. The older 42-row canonical source remains in the repository as explicitly superseded historical audit evidence and is no longer the registered source. + +## Physical conclusion + +The P2 finding “Require an applicable Hausman case in the GPU gate” is physically closed. Both physical backends now exercise the applicable statistic/p-value/df path and the structured-inapplicable path without fallback. + +**Physical gate: ACCEPTED.** diff --git a/dev/tests/test_benchmark_catalog.py b/dev/tests/test_benchmark_catalog.py index 62cb676e8..56729b848 100644 --- a/dev/tests/test_benchmark_catalog.py +++ b/dev/tests/test_benchmark_catalog.py @@ -84,6 +84,17 @@ def test_catalog_retains_distinct_noncanonical_dispositions(entries): assert focused["provenance_status"] == "validation_evidence" assert focused["issue"] == "#112" + panel_raw = next( + entry for entry in entries + if entry["path"] + == "results/pr122_p100/panel_stage_b_gpu_validation_faa95ce7.json" + ) + assert panel_raw["classification"] == "not_canonical_ready" + assert panel_raw["provenance_status"] == "validation_evidence" + assert panel_raw["timing_protocol_status"] == "not_applicable" + assert panel_raw["statistical_alignment_status"] == "accepted" + assert panel_raw["issue"] == "#93" + def test_coverage_matrix_is_referentially_complete(coverage_matrix, manifest): from dev.benchmarks.frontend_data.catalog import validate_coverage_matrix @@ -97,6 +108,10 @@ def test_coverage_matrix_is_referentially_complete(coverage_matrix, manifest): "cv-benchmark-20260807-1347184c988d", "cv-benchmark-pr116-20260807-bd8d512adced", ] + assert rows["panel-estimation"]["source_ids"] == [ + "new-modules-20260624-bcbdb676223b", + "panel-stage-b-pr122-20260809-2056f836bfe2", + ] assert rows["distribution-api"]["issue"] == "#101" assert rows["feature-selection-knockoff"]["issue"] == "#103" assert rows["penalized-coxph"]["issue"] == "#107" @@ -119,9 +134,9 @@ def test_inventory_v2_reconciles_literal_counts( assert inventory["inventory_version"] == "2.0" assert inventory["discovered_json_artifacts"] == len(entries) assert inventory["classified_candidate_sources"] == len(entries) - assert inventory["registered_sources"] == len(manifest["sources"]) == 10 - assert inventory["available_registered_sources"] == 10 - assert inventory["parsed_registered_sources"] == 10 + assert inventory["registered_sources"] == len(manifest["sources"]) == 11 + assert inventory["available_registered_sources"] == 11 + assert inventory["parsed_registered_sources"] == 11 assert inventory["eligible_sources"] == ( inventory["registered_sources"] + inventory["eligible_unregistered_sources"] diff --git a/dev/tests/test_benchmark_frontend_data.py b/dev/tests/test_benchmark_frontend_data.py index d19b94647..968270b91 100644 --- a/dev/tests/test_benchmark_frontend_data.py +++ b/dev/tests/test_benchmark_frontend_data.py @@ -159,7 +159,7 @@ class TestManifestMode: def test_manifest_loads_with_exact_current_sources(self, manifest): assert manifest is not None assert manifest["minimum_source_date"] == "2026-06-01" - assert len(manifest["sources"]) == 10 + assert len(manifest["sources"]) == 11 assert all(source.get("source_date") for source in manifest["sources"]) def test_canonical_generate(self, generator, manifest, results_dir): @@ -174,11 +174,11 @@ def test_canonical_generate(self, generator, manifest, results_dir): assert output["frameworks"] assert output["comparisons"] assert output["meta"]["generation_id"] - assert report["files_seen"] == 10 - assert report["files_parsed"] == 10 - assert inventory["registered_sources"] == 10 - assert inventory["available_sources"] == 10 - assert inventory["parsed_sources"] == 10 + assert report["files_seen"] == 11 + assert report["files_parsed"] == 11 + assert inventory["registered_sources"] == 11 + assert inventory["available_sources"] == 11 + assert inventory["parsed_sources"] == 11 assert not any( run["source"]["source_id"].startswith("transitional:") for run in output["runs"] diff --git a/dev/tests/test_benchmark_inventory_v2.py b/dev/tests/test_benchmark_inventory_v2.py index dc87b98c2..4ee720b8c 100644 --- a/dev/tests/test_benchmark_inventory_v2.py +++ b/dev/tests/test_benchmark_inventory_v2.py @@ -58,7 +58,7 @@ def test_canonical_inventory_v2_publishes_audited_catalog_snapshot() -> None: entry for entry in inventory["catalog_entries"] if entry["classification"] == "registered_canonical" ] - assert len(registered) == inventory["registered_sources"] == 10 + assert len(registered) == inventory["registered_sources"] == 11 assert all(entry["registered"] for entry in registered) @@ -80,13 +80,13 @@ def test_serialized_inventory_omits_legacy_aliases() -> None: assert "eligible_total" not in serialized assert "available_sources" not in serialized assert "parsed_sources" not in serialized - assert serialized["available_registered_sources"] == 10 - assert serialized["parsed_registered_sources"] == 10 + assert serialized["available_registered_sources"] == 11 + assert serialized["parsed_registered_sources"] == 11 # Old Python callers can still read the two non-semantic aliases without # reintroducing those keys into the published JSON contract. - assert inventory["available_sources"] == 10 - assert inventory["parsed_sources"] == 10 + assert inventory["available_sources"] == 11 + assert inventory["parsed_sources"] == 11 def test_modified_repository_manifest_retains_legacy_inventory_contract() -> None: diff --git a/dev/tests/test_frontend_cv_determinism.py b/dev/tests/test_frontend_cv_determinism.py new file mode 100644 index 000000000..028331194 --- /dev/null +++ b/dev/tests/test_frontend_cv_determinism.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_cv_std_serialization_normalizes_one_ulp_runtime_drift(monkeypatch) -> None: + """The deterministic frontend bundle must be stable across Python runtimes.""" + from dev.benchmarks.frontend_data.parsers import cv_package + + values = [1.0, 2.0] + + monkeypatch.setattr( + cv_package.statistics, + "pstdev", + lambda _values: 0.03478300871847092, + ) + upper = cv_package._stable_pstdev_ms(values) + + monkeypatch.setattr( + cv_package.statistics, + "pstdev", + lambda _values: 0.03478300871847091, + ) + lower = cv_package._stable_pstdev_ms(values) + + assert upper == lower == 0.034783008718 + + +def test_cv_std_serialization_preserves_single_repeat_zero() -> None: + from dev.benchmarks.frontend_data.parsers.cv_package import _stable_pstdev_ms + + assert _stable_pstdev_ms([12.5]) == 0.0 + + +def test_pr116_sensitive_cv_row_has_stable_canonical_std() -> None: + """Exercise the real row that previously drifted by one ULP on 3.9/3.11.""" + from dev.benchmarks.frontend_data.parsers.cv_package import parse_cv_benchmark + + source = REPO_ROOT / "results" / "pr116_p100" / "cv_benchmark_pr116_p100.json" + runs, _models, warnings = parse_cv_benchmark( + source, "remote-p100-pr116-20260807" + ) + + assert warnings == [] + ridge_numpy = next( + run + for run in runs + if run["model_id"] == "RidgeCV" + and run["framework"] == "statgpu" + and run["backend"] == "numpy" + ) + assert ridge_numpy["metrics"]["timing"]["std_ms"] == 0.083881039848 diff --git a/dev/tests/test_frontend_domain_coverage.py b/dev/tests/test_frontend_domain_coverage.py index eab413c7c..d77c40a9f 100644 --- a/dev/tests/test_frontend_domain_coverage.py +++ b/dev/tests/test_frontend_domain_coverage.py @@ -48,7 +48,7 @@ def test_published_categories_have_runs(canonical_output): def test_dashboard_uses_only_june_2026_or_later_sources(canonical_output): output, _, _, manifest = canonical_output assert manifest["minimum_source_date"] == "2026-06-01" - assert len(manifest["sources"]) == 10 + assert len(manifest["sources"]) == 11 manifest_dates = { source["source_id"]: date.fromisoformat(source["source_date"]) @@ -297,6 +297,63 @@ def test_panel_exposes_complete_aligned_scale_matrix(canonical_output): assert {run["source"]["parser_version"] for run in panel_runs} == {"1.4"} +def test_panel_stage_b_physical_validation_is_published_without_timing(canonical_output): + output, _, _, _ = canonical_output + rows = [ + run + for run in output["runs"] + if run["source"]["source_id"] + == "panel-stage-b-pr122-20260809-2056f836bfe2" + ] + assert len(rows) == 44 + assert {run["backend"] for run in rows} == {"cupy", "torch"} + assert {run["model_id"] for run in rows} == { + "PooledOLS", + "BetweenOLS", + "FirstDifferenceOLS", + "PanelOLS", + "RandomEffects", + "FamaMacBeth", + } + assert all(run["metrics"]["validation"]["status"] == "pass" for run in rows) + assert all("timing" not in run["metrics"] for run in rows) + assert all("speedup" not in run["metrics"] for run in rows) + assert all( + run["parameters"]["measurement_git_sha"] + == "2701aa9feb3796c33c94e6480fcb78c80c6a809c" + for run in rows + ) + + hausman = [run for run in rows if run["parameters"].get("diagnostic") == "hausman"] + assert len(hausman) == 10 + applicable = [ + run + for run in hausman + if run["parameters"].get("diagnostic_fixture") == "nonzero-effect-applicable" + ] + assert len(applicable) == 2 + assert {run["backend"] for run in applicable} == {"cupy", "torch"} + assert all(run["parameters"]["applicable"] is True for run in applicable) + assert {run["parameters"]["df"] for run in applicable} == {1.0} + assert all(run["parameters"]["statistic"] >= 0.0 for run in applicable) + assert all(0.0 <= run["parameters"]["pvalue"] <= 1.0 for run in applicable) + + inapplicable = [run for run in hausman if run not in applicable] + assert len(inapplicable) == 8 + assert all(run["parameters"]["applicable"] is False for run in inapplicable) + assert {run["parameters"]["parameterization"] for run in hausman} == { + "standard", + "re-explicit-constant", + } + assert {run["variant"] for run in hausman} == { + "hausman-balanced", + "hausman-unbalanced", + "hausman-re-explicit-constant-balanced", + "hausman-re-explicit-constant-unbalanced", + "hausman-applicable-nonzero-effect", + } + + def test_unsupervised_exposes_complete_source_matrix(canonical_output): output, _, _, _ = canonical_output rows = [ @@ -353,8 +410,8 @@ def test_unsupervised_exposes_complete_source_matrix(canonical_output): def test_generated_bundle_has_expected_complete_run_count(canonical_output): output, report, _, _ = canonical_output - assert len(output["runs"]) == 1818 - assert report["runs_generated"] == 1818 + assert len(output["runs"]) == 1862 + assert report["runs_generated"] == 1862 def test_missing_domain_sources_are_manifest_registered(canonical_output): @@ -366,6 +423,7 @@ def test_missing_domain_sources_are_manifest_registered(canonical_output): "unsupervised_benchmark", "new_modules_with_anova_benchmark", "p2_benchmark", + "panel_stage_b_physical_validation", } <= parsers @@ -389,6 +447,10 @@ def test_domain_models_are_present(canonical_output): "KMeans", "PanelOLS", "RandomEffects", + "PooledOLS", + "BetweenOLS", + "FirstDifferenceOLS", + "FamaMacBeth", "GAM", "EmpiricalCovariance", "OneWayANOVA", diff --git a/dev/tests/test_panel_stage_b_applicable_hausman_parser.py b/dev/tests/test_panel_stage_b_applicable_hausman_parser.py new file mode 100644 index 000000000..0926c7b7e --- /dev/null +++ b/dev/tests/test_panel_stage_b_applicable_hausman_parser.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT)) +SOURCE_PATH = ( + REPO_ROOT + / "results" + / "benchmark_frontend_sources" + / "panel_stage_b_pr122_p100_20260809_2701aa9f.json" +) + + +def _synthetic_source(tmp_path: Path, *, include_numeric: bool) -> Path: + data = json.loads(SOURCE_PATH.read_text(encoding="utf-8")) + for backend in ("cupy", "torch"): + diagnostics = data["backend_results"][backend]["diagnostics"] + template = dict(next(iter(diagnostics.values()))) + template["status"] = "success" + template["applicable"] = True + template["reason"] = None + if include_numeric: + template["statistic"] = 1.1965942530850693 + template["pvalue"] = 0.2740034414267718 + template["df"] = 1.0 + diagnostics["hausman_applicable_nonzero_effect"] = template + + synthetic = tmp_path / ( + "panel_stage_b_with_applicable_hausman.json" + if include_numeric + else "panel_stage_b_missing_applicable_numeric.json" + ) + synthetic.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return synthetic + + +def test_parser_distinguishes_dedicated_applicable_hausman_fixture(tmp_path) -> None: + from dev.benchmarks.frontend_data.parsers import ( + parse_panel_stage_b_physical_validation, + ) + + runs, _models, warnings = parse_panel_stage_b_physical_validation( + _synthetic_source(tmp_path, include_numeric=True), + "synthetic-pr122-applicable", + ) + + assert warnings == [] + dedicated = [ + run for run in runs if run["variant"] == "hausman-applicable-nonzero-effect" + ] + assert len(dedicated) == 2 + assert {run["backend"] for run in dedicated} == {"cupy", "torch"} + assert {run["scale"]["n_samples"] for run in dedicated} == {48} + assert {run["scale"]["n_features"] for run in dedicated} == {1} + assert {run["parameters"]["parameterization"] for run in dedicated} == {"standard"} + assert { + run["parameters"]["diagnostic_fixture"] for run in dedicated + } == {"nonzero-effect-applicable"} + assert all(run["parameters"]["applicable"] is True for run in dedicated) + assert {run["parameters"]["statistic"] for run in dedicated} == { + 1.1965942530850693 + } + assert {run["parameters"]["pvalue"] for run in dedicated} == { + 0.2740034414267718 + } + assert {run["parameters"]["df"] for run in dedicated} == {1.0} + assert all(run["metrics"]["validation"]["status"] == "pass" for run in dedicated) + assert all( + { + check["metric"]: check["status"] + for check in run["metrics"]["validation"]["checks"] + }["hausman_applicable_statistic_pvalue_df"] + == "pass" + for run in dedicated + ) + + standard_balanced = [ + run for run in runs if run["variant"] == "hausman-balanced" + ] + assert len(standard_balanced) == 2 + dedicated_methods = {run["method_config_id"] for run in dedicated} + standard_methods = {run["method_config_id"] for run in standard_balanced} + assert dedicated_methods.isdisjoint(standard_methods) + dedicated_cases = {run["case_id"] for run in dedicated} + standard_cases = {run["case_id"] for run in standard_balanced} + assert dedicated_cases.isdisjoint(standard_cases) + + +def test_parser_fails_closed_when_applicable_numeric_evidence_is_missing(tmp_path) -> None: + from dev.benchmarks.frontend_data.parsers import ( + parse_panel_stage_b_physical_validation, + ) + + runs, _models, warnings = parse_panel_stage_b_physical_validation( + _synthetic_source(tmp_path, include_numeric=False), + "synthetic-pr122-missing-numeric", + ) + + assert warnings == [] + dedicated = [ + run for run in runs if run["variant"] == "hausman-applicable-nonzero-effect" + ] + assert len(dedicated) == 2 + assert all(run["metrics"]["validation"]["status"] == "fail" for run in dedicated) + assert all( + { + check["metric"]: check["status"] + for check in run["metrics"]["validation"]["checks"] + }["hausman_applicable_statistic_pvalue_df"] + == "fail" + for run in dedicated + ) diff --git a/dev/tests/test_panel_stage_b_diagnostics.py b/dev/tests/test_panel_stage_b_diagnostics.py new file mode 100644 index 000000000..17c173647 --- /dev/null +++ b/dev/tests/test_panel_stage_b_diagnostics.py @@ -0,0 +1,389 @@ +"""Analytic and fitted-model contracts for Panel Tier-1 Stage B diagnostics.""" + +from __future__ import annotations + +import numpy as np +from numpy.testing import assert_allclose +from scipy import stats + +from statgpu.panel import PanelOLS, PooledOLS, RandomEffects +from statgpu.panel._diagnostics import ( + _bp_lm_from_components, + _build_fit_statistics, + _diagnostic_identity, + _fingerprints_match, + _hausman_quadratic, + _pooling_f_from_sums, +) +from statgpu.panel._results import PanelFitStatistics, PanelTestResult + + +def test_pooling_f_matches_hand_formula(): + result = _pooling_f_from_sums( + rss_pooled=18.0, + rss_effects=12.0, + df_num=3, + df_denom=40, + metadata={"constant_correction": False}, + ) + expected = ((18.0 - 12.0) / 3.0) / (12.0 / 40.0) + assert isinstance(result, PanelTestResult) + assert result.applicable + assert_allclose(result.statistic, expected, rtol=0, atol=1e-14) + assert_allclose(result.pvalue, stats.f.sf(expected, 3, 40), rtol=1e-12) + assert result.df == (3.0, 40.0) + assert result.metadata["classical_homoskedastic"] is True + + +def test_pooling_f_roundoff_negative_is_normalized_but_material_violation_is_not(): + tiny = _pooling_f_from_sums( + rss_pooled=10.0 - 1e-14, + rss_effects=10.0, + df_num=2, + df_denom=20, + ) + assert tiny.applicable + assert tiny.statistic == 0.0 + assert tiny.metadata["roundoff_normalized"] is True + + bad = _pooling_f_from_sums( + rss_pooled=9.0, + rss_effects=10.0, + df_num=2, + df_denom=20, + ) + assert not bad.applicable + assert "nested-model contract failed" in bad.reason + + +def test_bp_lm_matches_baltagi_li_unbalanced_formula(): + counts = np.asarray([2.0, 3.0, 4.0]) + group_sums = np.asarray([0.6, -0.2, 0.9]) + cp = 5.5 + nobs = int(counts.sum()) + a1 = np.sum(group_sums ** 2) / cp - 1.0 + m11 = np.sum(counts ** 2) + lm1 = nobs * np.sqrt(1.0 / (2.0 * (m11 - nobs))) * a1 + expected = lm1 ** 2 + + result = _bp_lm_from_components( + nobs=nobs, + residual_ss=cp, + group_residual_sums=group_sums, + group_counts=counts, + ) + assert result.applicable + assert result.df == 1.0 + assert_allclose(result.statistic, expected, rtol=0, atol=1e-14) + assert_allclose(result.pvalue, stats.chi2.sf(expected, 1), rtol=1e-12) + assert result.metadata["definition"].startswith("Baltagi-Li") + + +def test_bp_lm_rejects_singletons_only_and_zero_rss(): + singletons = _bp_lm_from_components( + nobs=3, + residual_ss=2.0, + group_residual_sums=[0.1, -0.1, 0.0], + group_counts=[1.0, 1.0, 1.0], + ) + assert not singletons.applicable + assert "repeated observations" in singletons.reason + + zero = _bp_lm_from_components( + nobs=4, + residual_ss=0.0, + group_residual_sums=[0.0, 0.0], + group_counts=[2.0, 2.0], + ) + assert not zero.applicable + assert "must be positive" in zero.reason + + +def test_hausman_full_rank_matches_quadratic_form(): + d = np.asarray([0.2, -0.1]) + D = np.asarray([[0.08, 0.01], [0.01, 0.05]]) + expected = float(d @ np.linalg.solve(D, d)) + result = _hausman_quadratic(d, D) + assert result.applicable + assert result.df == 2.0 + assert result.metadata["used_pinv"] is False + assert_allclose(result.statistic, expected, rtol=1e-12) + assert_allclose(result.pvalue, stats.chi2.sf(expected, 2), rtol=1e-12) + + +def test_hausman_singular_psd_uses_identified_range_and_rank_df(): + D = np.asarray([[2.0, 0.0], [0.0, 0.0]]) + result = _hausman_quadratic([1.0, 0.0], D) + assert result.applicable + assert result.df == 1.0 + assert result.metadata["used_pinv"] is True + assert result.metadata["definition_extension"].startswith("singular PSD") + assert_allclose(result.statistic, 0.5, rtol=0, atol=1e-14) + + outside = _hausman_quadratic([1.0, 0.1], D) + assert not outside.applicable + assert "outside the identified" in outside.reason + + +def test_hausman_indefinite_covariance_difference_is_explicitly_inapplicable(): + result = _hausman_quadratic([0.1, 0.2], [[1.0, 0.0], [0.0, -0.1]]) + assert not result.applicable + assert "not positive semidefinite" in result.reason + + +def test_parameter_based_r2_and_adjusted_r2_are_hand_checkable(): + entity = np.repeat(np.arange(3), 3) + X = np.column_stack( + [ + np.ones(9), + np.linspace(-1.0, 1.0, 9), + ] + ) + beta = np.asarray([1.2, 0.7]) + entity_shift = np.repeat(np.asarray([-0.4, 0.0, 0.5]), 3) + noise = np.asarray([0.1, -0.1, 0.0] * 3) + y = X @ beta + entity_shift + noise + resid = y - X @ beta + rss = float(resid @ resid) + tss = float(np.sum((y - y.mean()) ** 2)) + + result = _build_fit_statistics( + y, + X, + beta, + xp=np, + entity_codes=entity, + has_constant=True, + rss_fit=rss, + tss_fit=tss, + df_resid=7, + df_total=8, + metadata={"fit_space": "pooled"}, + ) + assert isinstance(result, PanelFitStatistics) + expected_overall = 1.0 - rss / tss + expected_adj = 1.0 - (rss / 7.0) / (tss / 8.0) + assert_allclose(result.rsquared_overall, expected_overall, rtol=1e-12) + assert_allclose(result.rsquared_adj, expected_adj, rtol=1e-12) + assert result.rsquared_within is not None + assert result.rsquared_between is not None + assert result.f_statistic is not None + assert result.f_df == (1.0, 7.0) + + +def test_standardized_r2_zero_tss_is_zero_with_metadata(): + X = np.ones((6, 1)) + y = np.ones(6) + result = _build_fit_statistics( + y, + X, + np.asarray([1.0]), + xp=np, + entity_codes=np.repeat(np.arange(3), 2), + has_constant=True, + rss_fit=0.0, + tss_fit=0.0, + df_resid=5, + df_total=5, + ) + assert result.rsquared_overall == 0.0 + assert result.rsquared_between == 0.0 + assert result.rsquared_within == 0.0 + assert result.rsquared_adj == 0.0 + assert result.metadata["degenerate_total_ss"] == { + "within": True, + "between": True, + "overall": True, + } + + +def test_numerical_fingerprint_detects_row_order_and_data_changes(): + X = np.arange(12.0).reshape(6, 2) / 10.0 + y = np.linspace(-0.5, 0.5, 6) + entity = np.asarray([0, 0, 1, 1, 2, 2]) + left = _diagnostic_identity( + X, + y, + xp=np, + entity_codes=entity, + feature_names=["x1", "x2"], + ) + same = _diagnostic_identity( + X.copy(), + y.copy(), + xp=np, + entity_codes=entity.copy(), + feature_names=["x1", "x2"], + ) + ok, reason = _fingerprints_match(left, same) + assert ok + assert reason == "" + + changed_y = y.copy() + changed_y[-1] += 0.2 + different = _diagnostic_identity( + X, + changed_y, + xp=np, + entity_codes=entity, + feature_names=["x1", "x2"], + ) + ok, reason = _fingerprints_match(left, different) + assert not ok + assert "fingerprint mismatch" in reason + + order = np.asarray([1, 0, 2, 3, 4, 5]) + reordered = _diagnostic_identity( + X[order], + y[order], + xp=np, + entity_codes=entity[order], + feature_names=["x1", "x2"], + ) + ok, reason = _fingerprints_match(left, reordered) + assert not ok + assert "identity mismatch" in reason or "fingerprint mismatch" in reason + + +def _balanced_panel(seed=930): + rng = np.random.default_rng(seed) + n_entities, n_times = 6, 4 + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + X = rng.normal(size=(entity.size, 2)) + alpha = np.repeat(np.linspace(-0.7, 0.8, n_entities), n_times) + y = 0.9 * X[:, 0] - 0.45 * X[:, 1] + alpha + rng.normal( + scale=0.18, size=entity.size + ) + return X, y, entity, time + + +def _demean_by_entity(values, entity): + values = np.asarray(values) + out = values.copy() + for group in np.unique(entity): + mask = entity == group + out[mask] -= values[mask].mean(axis=0) + return out + + +def test_panelols_pooling_f_uses_standard_diagnostic_df_not_legacy_inference_df(): + X, y, entity, _ = _balanced_panel() + model = PanelOLS(entity_effects=True).fit(X, y, entity_ids=entity) + + # Stage-A legacy inference df remains untouched. + assert model.df_resid == len(y) - X.shape[1] - (len(np.unique(entity)) - 1) + diagnostic_df = model.fit_statistics_.metadata["diagnostic_df"] + assert diagnostic_df["df_resid"] == model.df_resid - 1 + assert diagnostic_df["effect_rank"] == len(np.unique(entity)) + + yw = _demean_by_entity(y, entity) + Xw = _demean_by_entity(X, entity) + beta_fe = np.linalg.pinv(Xw) @ yw + rss_fe = float(np.sum((yw - Xw @ beta_fe) ** 2)) + yc = y - y.mean() + Xc = X - X.mean(axis=0) + beta_pool = np.linalg.pinv(Xc) @ yc + rss_pool = float(np.sum((yc - Xc @ beta_pool) ** 2)) + df_pool = len(y) - np.linalg.matrix_rank(Xc) - 1 + df_fe = len(y) - np.linalg.matrix_rank(Xw) - len(np.unique(entity)) + df_num = df_pool - df_fe + expected = ((rss_pool - rss_fe) / df_num) / (rss_fe / df_fe) + + result = model.pooling_f_test() + assert result.applicable + assert result.df == (float(df_num), float(df_fe)) + assert_allclose(result.statistic, expected, rtol=1e-11, atol=1e-12) + # The maintained distribution backend agrees with SciPy to ~1e-16 absolute + # here. Since the tail probability is ~2.5e-8, require strict absolute + # accuracy without amplifying floating-point noise through an excessive + # relative tolerance requirement. + assert_allclose( + result.pvalue, + stats.f.sf(expected, df_num, df_fe), + rtol=1e-8, + atol=1e-15, + ) + assert model.fit_statistics_.f_df == ( + float(np.linalg.matrix_rank(Xw)), + float(df_fe), + ) + + +def test_pooled_bp_lm_matches_direct_residual_formula(): + X, y, entity, _ = _balanced_panel(seed=931) + model = PooledOLS().fit(X, y, entity_ids=entity) + design = np.column_stack([np.ones(len(y)), X]) + resid = y - design @ model.coef_ + sums = np.asarray([resid[entity == g].sum() for g in np.unique(entity)]) + counts = np.asarray([(entity == g).sum() for g in np.unique(entity)], dtype=float) + cp = float(resid @ resid) + a1 = float(np.sum(sums ** 2) / cp - 1.0) + m11 = float(np.sum(counts ** 2)) + lm1 = len(y) * np.sqrt(1.0 / (2.0 * (m11 - len(y)))) * a1 + expected = lm1 ** 2 + + result = model.breusch_pagan_lm_test() + assert result.applicable + assert_allclose(result.statistic, expected, rtol=1e-11, atol=1e-12) + assert_allclose(result.pvalue, stats.chi2.sf(expected, 1), rtol=1e-11) + assert model.fit_statistics_.rsquared_within is not None + assert model.fit_statistics_.rsquared_between is not None + + +def test_pooled_hac_sort_reorders_entity_diagnostic_metadata_with_xy(): + X, y, entity, time = _balanced_panel(seed=932) + scrambled_time = (3 - time + 2 * entity) % 4 + baseline = PooledOLS(cov_type="nonrobust").fit(X, y, entity_ids=entity) + hac = PooledOLS(cov_type="hac", bandwidth=1).fit( + X, + y, + time_index=scrambled_time, + entity_ids=entity, + ) + assert_allclose(hac.coef_, baseline.coef_, rtol=1e-12, atol=1e-12) + assert_allclose( + hac.breusch_pagan_lm_test().statistic, + baseline.breusch_pagan_lm_test().statistic, + rtol=1e-12, + atol=1e-12, + ) + assert_allclose( + hac.fit_statistics_.rsquared_between, + baseline.fit_statistics_.rsquared_between, + rtol=1e-12, + atol=1e-12, + ) + + +def test_hausman_checks_sample_identity_and_classical_fe_covariance(): + X, y, entity, _ = _balanced_panel(seed=933) + fe = PanelOLS(entity_effects=True).fit(X, y, entity_ids=entity) + re = RandomEffects().fit(X, y, entity_ids=entity) + same = fe.hausman_test(re) + assert "identity mismatch" not in (same.reason or "") + assert "fingerprint" not in (same.reason or "") + + changed_y = y.copy() + changed_y[-1] += 0.25 + re_changed = RandomEffects().fit(X, changed_y, entity_ids=entity) + mismatch = fe.hausman_test(re_changed) + assert not mismatch.applicable + assert "fingerprint mismatch" in mismatch.reason + + fe_robust = PanelOLS(entity_effects=True, cov_type="robust").fit( + X, y, entity_ids=entity + ) + robust = fe_robust.hausman_test(re) + assert not robust.applicable + assert "nonrobust FE covariance" in robust.reason + + +def test_pooled_bp_without_entity_ids_is_structured_inapplicable(): + X, y, _, _ = _balanced_panel(seed=934) + model = PooledOLS().fit(X, y) + result = model.breusch_pagan_lm_test() + assert not result.applicable + assert "entity_ids" in result.reason + assert model.fit_statistics_.rsquared_within is None + assert model.fit_statistics_.rsquared_between is None diff --git a/dev/tests/test_panel_stage_b_fit_statistics.py b/dev/tests/test_panel_stage_b_fit_statistics.py new file mode 100644 index 000000000..a04575db8 --- /dev/null +++ b/dev/tests/test_panel_stage_b_fit_statistics.py @@ -0,0 +1,137 @@ +"""Cross-estimator fit-statistics contracts for Panel Tier-1 Stage B.""" + +from __future__ import annotations + +import numpy as np +from numpy.testing import assert_allclose + +from statgpu.panel import ( + BetweenOLS, + FamaMacBeth, + FirstDifferenceOLS, + PanelFitStatistics, + PanelOLS, + PooledOLS, + RandomEffects, +) + + +def _panel(seed=1240, unbalanced=False): + rng = np.random.default_rng(seed) + n_entities, n_times = 7, 5 + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + X = rng.normal(size=(entity.size, 2)) + alpha = np.repeat(np.linspace(-0.5, 0.65, n_entities), n_times) + tau = np.tile(np.linspace(-0.15, 0.2, n_times), n_entities) + y = 0.75 * X[:, 0] - 0.42 * X[:, 1] + alpha + tau + rng.normal( + scale=0.18, size=entity.size + ) + if unbalanced: + keep = np.ones(entity.size, dtype=bool) + keep[[1, 7, 18, 29]] = False + X, y, entity, time = X[keep], y[keep], entity[keep], time[keep] + return X, y, entity, time + + +def _assert_standard(result): + assert isinstance(result, PanelFitStatistics) + assert result.metadata["r2_definition"] == "parameter-based" + for value in ( + result.rsquared_within, + result.rsquared_between, + result.rsquared_overall, + ): + assert value is None or np.isfinite(value) + + +def test_fit_statistics_populated_for_ols_style_panel_estimators(): + X, y, entity, time = _panel(unbalanced=True) + models = [ + PooledOLS().fit(X, y, entity_ids=entity), + BetweenOLS().fit(X, y, entity_ids=entity), + FirstDifferenceOLS().fit(X, y, entity_ids=entity, time_ids=time), + PanelOLS(entity_effects=True).fit(X, y, entity_ids=entity), + RandomEffects().fit(X, y, entity_ids=entity), + ] + for model in models: + _assert_standard(model.fit_statistics_) + assert model.fit_statistics_.rsquared_adj is not None + assert np.isfinite(model.fit_statistics_.rsquared_adj) + assert model.fit_statistics_.f_statistic is not None + assert np.isfinite(model.fit_statistics_.f_statistic) + assert model.fit_statistics_.f_pvalue is not None + assert 0.0 <= model.fit_statistics_.f_pvalue <= 1.0 + assert model.fit_statistics_.f_df is not None + + +def test_two_way_fe_uses_standard_effect_rank_without_changing_legacy_df(): + X, y, entity, time = _panel(seed=1241) + model = PanelOLS(entity_effects=True, time_effects=True).fit( + X, + y, + entity_ids=entity, + time_ids=time, + ) + n = len(y) + k = X.shape[1] + N = len(np.unique(entity)) + T = len(np.unique(time)) + assert model.df_resid == n - k - (N - 1) - (T - 1) + diag = model.fit_statistics_.metadata["diagnostic_df"] + assert diag["effect_rank"] == N + T - 1 + assert diag["df_resid"] == n - np.linalg.matrix_rank( + X + - np.vstack([X[entity == g].mean(axis=0) for g in entity]) + - np.vstack([X[time == t].mean(axis=0) for t in time]) + + X.mean(axis=0) + ) - (N + T - 1) + assert diag["df_resid"] == model.df_resid - 1 + assert model.fit_statistics_.metadata["legacy_rsquared_within"] == model.rsquared_within + + +def test_fama_macbeth_exposes_r2_but_not_residual_ols_f_or_adjusted_r2(): + X, y, entity, time = _panel(seed=1242) + model = FamaMacBeth(cov_type="newey-west", bandwidth=1).fit( + X, + y, + time_ids=time, + entity_ids=entity, + ) + result = model.fit_statistics_ + _assert_standard(result) + assert result.rsquared_within is not None + assert result.rsquared_between is not None + assert result.rsquared_overall is not None + assert result.rsquared_adj is None + assert result.f_statistic is None + assert result.f_pvalue is None + assert result.f_df is None + assert "beta-series" in result.metadata["unavailable"]["model_f"] + + +def test_fama_macbeth_without_entity_ids_leaves_decomposition_unavailable(): + X, y, _, time = _panel(seed=1243) + result = FamaMacBeth().fit(X, y, time_ids=time).fit_statistics_ + assert result.rsquared_within is None + assert result.rsquared_between is None + assert result.rsquared_overall is not None + assert "within_between_r2" in result.metadata["unavailable"] + + +def test_between_and_first_difference_leave_existing_legacy_r2_unchanged(): + X, y, entity, time = _panel(seed=1244) + between = BetweenOLS().fit(X, y, entity_ids=entity) + fd = FirstDifferenceOLS().fit(X, y, entity_ids=entity, time_ids=time) + assert_allclose( + between.fit_statistics_.metadata["legacy_rsquared"], + between.rsquared, + rtol=0, + atol=0, + ) + assert_allclose( + fd.fit_statistics_.metadata["legacy_rsquared"], + fd.rsquared, + rtol=0, + atol=0, + ) diff --git a/dev/tests/test_panel_stage_b_formula.py b/dev/tests/test_panel_stage_b_formula.py new file mode 100644 index 000000000..e3d3e67a8 --- /dev/null +++ b/dev/tests/test_panel_stage_b_formula.py @@ -0,0 +1,158 @@ +"""Formula/missing-row contracts for Panel Tier-1 Stage B diagnostics.""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +from numpy.testing import assert_allclose + +import statgpu +from statgpu.panel import FamaMacBeth, PanelOLS, PooledOLS + + +def _frame(seed=1230): + rng = np.random.default_rng(seed) + n_entities, n_times = 8, 4 + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + x1 = rng.normal(size=entity.size) + x2 = rng.normal(size=entity.size) + alpha = np.repeat(np.linspace(-0.4, 0.5, n_entities), n_times) + y = 0.7 + 0.85 * x1 - 0.3 * x2 + alpha + rng.normal( + scale=0.15, size=entity.size + ) + frame = pd.DataFrame( + {"y": y, "x1": x1, "x2": x2, "entity": entity, "time": time} + ) + # Patsy will drop these rows. Keep at least three observations in every + # affected period so Fama-MacBeth remains estimable after filtering. + frame.loc[[1, 10], "x1"] = np.nan + return frame + + +def _assert_fit_statistics(actual, expected): + for field in ( + "rsquared_within", + "rsquared_between", + "rsquared_overall", + "rsquared_adj", + "f_statistic", + "f_pvalue", + ): + a = getattr(actual, field) + e = getattr(expected, field) + if e is None: + assert a is None + else: + assert_allclose(a, e, rtol=1e-9, atol=1e-11) + assert actual.f_df == expected.f_df + + +def test_pooled_formula_aligns_entity_ids_before_bp_and_r2(): + frame = _frame() + entity_full = frame["entity"].to_numpy() + fitted = PooledOLS().fit( + formula="y ~ x1 + x2", + data=frame, + entity_ids=entity_full, + ) + + retained = frame.dropna(subset=["x1", "x2", "y"]) + reference = PooledOLS().fit( + retained[["x1", "x2"]].to_numpy(), + retained["y"].to_numpy(), + entity_ids=retained["entity"].to_numpy(), + ) + assert_allclose(fitted.coef_, reference.coef_, rtol=1e-10, atol=1e-11) + _assert_fit_statistics(fitted.fit_statistics_, reference.fit_statistics_) + assert_allclose( + fitted.breusch_pagan_lm_test().statistic, + reference.breusch_pagan_lm_test().statistic, + rtol=1e-9, + atol=1e-11, + ) + + +def test_panelols_formula_effect_tokens_use_exact_retained_sample_for_pooling_f(): + frame = _frame(seed=1231) + fitted = PanelOLS().fit( + formula="y ~ x1 + x2 | entity", + data=frame, + ) + retained = frame.dropna(subset=["x1", "x2", "y"]) + reference = PanelOLS(entity_effects=True).fit( + retained[["x1", "x2"]].to_numpy(), + retained["y"].to_numpy(), + entity_ids=retained["entity"].to_numpy(), + ) + assert fitted.entity_effects is True + assert_allclose(fitted.coef_, reference.coef_, rtol=1e-10, atol=1e-11) + _assert_fit_statistics(fitted.fit_statistics_, reference.fit_statistics_) + assert_allclose( + fitted.pooling_f_test().statistic, + reference.pooling_f_test().statistic, + rtol=1e-9, + atol=1e-11, + ) + assert fitted.pooling_f_test().df == reference.pooling_f_test().df + + +def test_fama_macbeth_formula_aligns_optional_entity_ids_for_r2_only(): + frame = _frame(seed=1232) + time_full = frame["time"].to_numpy() + entity_full = frame["entity"].to_numpy() + fitted = FamaMacBeth(cov_type="newey-west", bandwidth=1).fit( + formula="y ~ x1 + x2", + data=frame, + time_ids=time_full, + entity_ids=entity_full, + ) + retained = frame.dropna(subset=["x1", "x2", "y"]) + reference = FamaMacBeth(cov_type="newey-west", bandwidth=1).fit( + retained[["x1", "x2"]].to_numpy(), + retained["y"].to_numpy(), + time_ids=retained["time"].to_numpy(), + entity_ids=retained["entity"].to_numpy(), + ) + assert_allclose( + np.asarray(fitted.coef_), + np.asarray(reference.coef_), + rtol=1e-10, + atol=1e-11, + ) + assert_allclose( + fitted.fit_statistics_.rsquared_within, + reference.fit_statistics_.rsquared_within, + rtol=1e-9, + atol=1e-11, + ) + assert_allclose( + fitted.fit_statistics_.rsquared_between, + reference.fit_statistics_.rsquared_between, + rtol=1e-9, + atol=1e-11, + ) + assert_allclose( + fitted.fit_statistics_.rsquared_overall, + reference.fit_statistics_.rsquared_overall, + rtol=1e-9, + atol=1e-11, + ) + assert fitted.fit_statistics_.rsquared_adj is None + assert fitted.fit_statistics_.f_statistic is None + + +def test_stage_b_diagnostic_api_is_available_from_panel_and_top_level(): + from statgpu.panel import ( + PanelFitStatistics, + PanelTestResult, + breusch_pagan_lm_test, + hausman_test, + pooling_f_test, + ) + + assert statgpu.PanelFitStatistics is PanelFitStatistics + assert statgpu.PanelTestResult is PanelTestResult + assert statgpu.hausman_test is hausman_test + assert statgpu.pooling_f_test is pooling_f_test + assert statgpu.breusch_pagan_lm_test is breusch_pagan_lm_test diff --git a/dev/tests/test_panel_stage_b_frontend_source.py b/dev/tests/test_panel_stage_b_frontend_source.py new file mode 100644 index 000000000..8e859c86d --- /dev/null +++ b/dev/tests/test_panel_stage_b_frontend_source.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import hashlib +import json +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT)) + +SOURCE_PATH = ( + REPO_ROOT + / "results" + / "benchmark_frontend_sources" + / "panel_stage_b_pr122_p100_20260809_2701aa9f.json" +) +RAW_SOURCE_PATH = ( + REPO_ROOT + / "results" + / "pr122_p100" + / "panel_stage_b_gpu_validation_2701aa9f.json" +) +FOCUSED_SOURCE_PATH = ( + REPO_ROOT + / "results" + / "pr122_p100" + / "panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json" +) +EXPECTED_SHA256 = "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" +RAW_EXPECTED_BLOB_SHA = "fa3a253e6d882a4e69be29e7e3b1dce7b223b9a9" +FOCUSED_EXPECTED_BLOB_SHA = "3bda0b2040479ba8201e2722eb990ba086c3f3b9" +SOURCE_ID = "panel-stage-b-pr122-20260809-2056f836bfe2" +MEASUREMENT_SHA = "2701aa9feb3796c33c94e6480fcb78c80c6a809c" +ARTIFACT_COMMIT = "0d0d654d825cea872672f27d02107a58048b345f" +FOCUSED_MEASUREMENT_SHA = "a57efcea29b0e87ecb89865c5a6902d5773812c6" +FOCUSED_ARTIFACT_COMMIT = "72b3279d2028e8ec2af30e138e123aceb611ae8c" +ENV_ID = "remote-p100-pr122-20260809" + + +def _git_blob_sha(path: Path) -> str: + raw = path.read_bytes() + header = f"blob {len(raw)}\0".encode("ascii") + return hashlib.sha1(header + raw).hexdigest() + + +def test_pr122_physical_source_contract_and_hash() -> None: + from dev.benchmarks.frontend_data.canonical import source_sha256 + + data = json.loads(SOURCE_PATH.read_text(encoding="utf-8")) + focused = json.loads(FOCUSED_SOURCE_PATH.read_text(encoding="utf-8")) + + assert source_sha256(SOURCE_PATH) == EXPECTED_SHA256 + assert _git_blob_sha(RAW_SOURCE_PATH) == RAW_EXPECTED_BLOB_SHA + assert _git_blob_sha(FOCUSED_SOURCE_PATH) == FOCUSED_EXPECTED_BLOB_SHA + assert data["source_schema_version"] == "1.0" + assert data["source_date"] == "2026-08-09" + assert data["git_sha"] == MEASUREMENT_SHA + assert data["working_tree_clean"] is True + assert data["status"] == "success" + assert data["schema_status"] == "ok" + assert data["validation_tier"] == "remote-full" + assert data["protocol"]["timing_collected"] is False + assert data["backend_times"] == {"numpy": None, "cupy": None, "torch": None} + assert data["compatibility_matrix"]["cupy"]["model_cases"] == "17/17" + assert data["compatibility_matrix"]["torch"]["model_cases"] == "17/17" + assert data["compatibility_matrix"]["cupy"]["diagnostics"] == "5/5" + assert data["compatibility_matrix"]["torch"]["diagnostics"] == "5/5" + assert data["compatibility_matrix"]["cupy"]["cpu_fallback"] is False + assert data["compatibility_matrix"]["torch"]["cpu_fallback"] is False + assert data["compatibility_matrix"]["cupy"]["disconnected_fe"] == "pass" + assert data["compatibility_matrix"]["torch"]["disconnected_fe"] == "pass" + + raw = data["raw_artifact"] + assert raw["path"] == str(RAW_SOURCE_PATH.relative_to(REPO_ROOT)) + assert raw["repository_commit"] == ARTIFACT_COMMIT + assert raw["git_blob_sha"] == RAW_EXPECTED_BLOB_SHA + + focused_meta = data["focused_artifact"] + assert focused_meta["path"] == str(FOCUSED_SOURCE_PATH.relative_to(REPO_ROOT)) + assert focused_meta["repository_commit"] == FOCUSED_ARTIFACT_COMMIT + assert focused_meta["git_blob_sha"] == FOCUSED_EXPECTED_BLOB_SHA + assert focused_meta["status"] == "success" + + assert focused["git_sha"] == FOCUSED_MEASUREMENT_SHA + assert focused["working_tree_clean"] is True + assert focused["status"] == "success" + assert focused["reference"]["legacy_df_resid"] == 0 + assert focused["reference"]["df_resid"] == 1 + assert focused["reference"]["diagnostic_df"]["effect_rank"] == 7 + assert focused["reference"]["diagnostic_df"]["incidence_components"] == 3 + assert focused["backend_results"]["cupy"]["executed_backend"] == "cupy" + assert focused["backend_results"]["torch"]["executed_backend"] == "torch" + assert focused["backend_results"]["cupy"]["status"] == "success" + assert focused["backend_results"]["torch"]["status"] == "success" + assert focused["backend_results"]["torch"]["differences_vs_numpy"]["conf_int"] < 1e-12 + for backend in ("cupy", "torch"): + diagnostic = data["backend_results"][backend]["diagnostics"][ + "hausman_applicable_nonzero_effect" + ] + assert diagnostic["status"] == "success" + assert diagnostic["applicable"] is True + assert diagnostic["reason"] is None + assert diagnostic["df"] == 1.0 + assert 0.0 <= diagnostic["pvalue"] <= 1.0 + assert diagnostic["statistic"] >= 0.0 + assert diagnostic["max_abs_differences"]["statistic"] < 2e-13 + assert diagnostic["max_abs_differences"]["pvalue"] < 5e-14 + + assert data["environment"]["gpu"] == "Tesla P100-SXM2-16GB" + assert data["environment"]["packages"]["cupy"] is None + + +def test_pr122_parser_emits_validation_only_frontend_runs() -> None: + from dev.benchmarks.frontend_data.parsers import ( + parse_panel_stage_b_physical_validation, + ) + + runs, models, warnings = parse_panel_stage_b_physical_validation( + SOURCE_PATH, ENV_ID + ) + + assert warnings == [] + assert len(runs) == 44 + assert len(models) == 6 + assert {run["backend"] for run in runs} == {"cupy", "torch"} + assert {model["model_id"] for model in models} == { + "PooledOLS", + "BetweenOLS", + "FirstDifferenceOLS", + "PanelOLS", + "RandomEffects", + "FamaMacBeth", + } + assert all("timing" not in run["metrics"] for run in runs) + assert all("speedup" not in run["metrics"] for run in runs) + assert all(run["metrics"]["validation"]["status"] == "pass" for run in runs) + assert all( + run["parameters"]["measurement_git_sha"] == MEASUREMENT_SHA for run in runs + ) + assert all(run["parameters"]["working_tree_clean"] is True for run in runs) + + hausman = [run for run in runs if run["parameters"].get("diagnostic") == "hausman"] + assert len(hausman) == 10 + assert all("inference" not in run["metrics"] for run in hausman) + applicable = [ + run for run in hausman + if run["parameters"].get("diagnostic_fixture") == "nonzero-effect-applicable" + ] + assert len(applicable) == 2 + assert {run["backend"] for run in applicable} == {"cupy", "torch"} + assert all(run["parameters"]["applicable"] is True for run in applicable) + assert {run["parameters"]["df"] for run in applicable} == {1.0} + assert all(run["parameters"]["statistic"] >= 0.0 for run in applicable) + assert all(0.0 <= run["parameters"]["pvalue"] <= 1.0 for run in applicable) + structured_inapplicable = [run for run in hausman if run not in applicable] + assert len(structured_inapplicable) == 8 + assert all(run["parameters"]["applicable"] is False for run in structured_inapplicable) + assert {run["parameters"]["parameterization"] for run in hausman} == { + "standard", + "re-explicit-constant", + } + assert {run["variant"] for run in hausman} == { + "hausman-balanced", + "hausman-unbalanced", + "hausman-re-explicit-constant-balanced", + "hausman-re-explicit-constant-unbalanced", + "hausman-applicable-nonzero-effect", + } + + estimator_runs = [run for run in runs if run not in hausman] + assert len(estimator_runs) == 34 + assert all(run["metrics"]["inference"]["ok"] is True for run in estimator_runs) + + explicit_re = [ + run + for run in estimator_runs + if run["model_id"] == "RandomEffects" + and run["variant"].startswith("explicit-constant-") + ] + assert len(explicit_re) == 4 + assert {run["scale"]["n_features"] for run in explicit_re} == {3} + + +def test_pr122_parser_is_registered_in_manifest() -> None: + from dev.benchmarks.frontend_data.parsers import ( + parse_panel_stage_b_physical_validation, + ) + from dev.benchmarks.frontend_data.registry import PARSER_FUNCTIONS, load_manifest + + assert ( + PARSER_FUNCTIONS["panel_stage_b_physical_validation"] + is parse_panel_stage_b_physical_validation + ) + manifest = load_manifest(REPO_ROOT) + assert manifest is not None + entry = next(source for source in manifest["sources"] if source["source_id"] == SOURCE_ID) + assert entry["path"] == str(SOURCE_PATH.relative_to(REPO_ROOT)) + assert entry["original_path"] == str(RAW_SOURCE_PATH.relative_to(REPO_ROOT)) + assert entry["sha256"] == EXPECTED_SHA256 + assert entry["parser"] == "panel_stage_b_physical_validation" + assert entry["parser_version"] == "1.0" + assert entry["env_id"] == ENV_ID + assert entry["source_date"] == "2026-08-09" + assert entry["measurement_git_sha"] == MEASUREMENT_SHA + assert entry["raw_git_sha"] == MEASUREMENT_SHA + assert RAW_EXPECTED_BLOB_SHA in entry["provenance_note"] + assert FOCUSED_EXPECTED_BLOB_SHA in entry["provenance_note"] diff --git a/dev/tests/test_panel_stage_b_hausman_covariance.py b/dev/tests/test_panel_stage_b_hausman_covariance.py new file mode 100644 index 000000000..7d14eae83 --- /dev/null +++ b/dev/tests/test_panel_stage_b_hausman_covariance.py @@ -0,0 +1,73 @@ +"""Regression contract for Stage-B Hausman covariance degrees of freedom.""" + +from __future__ import annotations + +import numpy as np +from numpy.testing import assert_allclose + +from statgpu.panel import PanelOLS, RandomEffects + + +def _panel(seed=1250): + rng = np.random.default_rng(seed) + n_entities, n_times = 9, 5 + entity = np.repeat(np.arange(n_entities), n_times) + X = rng.normal(size=(entity.size, 2)) + alpha = np.repeat(np.linspace(-0.6, 0.7, n_entities), n_times) + y = 0.9 * X[:, 0] - 0.35 * X[:, 1] + alpha + rng.normal( + scale=0.2, size=entity.size + ) + return X, y, entity + + +def test_hausman_uses_standard_fe_covariance_without_changing_legacy_inference(): + X, y, entity = _panel() + fe = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity + ) + re = RandomEffects().fit(X, y, entity_ids=entity) + + raw_fe = np.asarray(fe._panel_cov_params_raw) + raw_re = np.asarray(re._panel_cov_params_raw) + diag_meta = fe.fit_statistics_.metadata["diagnostic_df"] + legacy_df = fe.fit_statistics_.metadata["legacy_df_resid"] + standard_df = diag_meta["df_resid"] + + # Stage A's historical FE inference denominator is intentionally retained. + assert legacy_df == fe.df_resid + assert standard_df == legacy_df - 1 + assert_allclose(fe.bse_ ** 2, np.diag(raw_fe), rtol=1e-12, atol=1e-14) + + # Classical Hausman, however, needs the full nuisance-effect model rank. + # Only the small diagnostic covariance copy is rescaled; public bse/CI above + # still come from the raw Stage-A covariance. + expected_fe_diagnostic = raw_fe * (legacy_df / standard_df) + assert_allclose( + fe._panel_cov_params, + expected_fe_diagnostic, + rtol=1e-12, + atol=1e-14, + ) + + # RandomEffects has no legacy FE nuisance-df mismatch, so its diagnostic + # covariance is exactly the inference covariance. + assert_allclose(re._panel_cov_params, raw_re, rtol=0, atol=0) + + # The Hausman entry point must consume the diagnostic matrices. Whether the + # finite-sample covariance difference is PSD is data-dependent; this check + # locks the exact matrix passed to the quadratic-form contract. + result = fe.hausman_test(re) + expected_difference = expected_fe_diagnostic - raw_re + eigvals = np.linalg.eigvalsh(0.5 * (expected_difference + expected_difference.T)) + assert_allclose( + result.metadata["minimum_eigenvalue"], + eigvals.min(), + rtol=1e-10, + atol=1e-12, + ) + assert_allclose( + result.metadata["maximum_eigenvalue"], + eigvals.max(), + rtol=1e-10, + atol=1e-12, + ) diff --git a/dev/tests/test_panel_stage_b_linearmodels.py b/dev/tests/test_panel_stage_b_linearmodels.py new file mode 100644 index 000000000..9c11e582d --- /dev/null +++ b/dev/tests/test_panel_stage_b_linearmodels.py @@ -0,0 +1,256 @@ +"""Executable external-definition alignment against linearmodels 7.0. + +This file is run in a dedicated CI job that installs linearmodels==7.0. It +compares only quantities whose estimator parameterizations and transformed +samples are intentionally aligned in Stage B. RandomEffects coefficients are +excluded because statgpu's existing Swamy-Arora path is a preserved +model-specific contract; for RandomEffects we compare only structural +diagnostic contracts that do not require coefficient equality. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest +from numpy.testing import assert_allclose + +pytest.importorskip( + "linearmodels", + reason="linearmodels is an external Stage-B definition gate, not a base dependency", +) + +from linearmodels.panel import ( + BetweenOLS as LMBetweenOLS, + FirstDifferenceOLS as LMFirstDifferenceOLS, + PanelOLS as LMPanelOLS, + PooledOLS as LMPooledOLS, + RandomEffects as LMRandomEffects, +) + +from statgpu.panel import ( + BetweenOLS, + FirstDifferenceOLS, + PanelOLS, + PooledOLS, + RandomEffects, +) + + +def _panel(seed=1260, *, unbalanced=False): + rng = np.random.default_rng(seed) + n_entities, n_times = 9, 6 + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + X = rng.normal(size=(entity.size, 2)) + alpha = np.repeat(np.linspace(-0.65, 0.75, n_entities), n_times) + tau = np.tile(np.linspace(-0.2, 0.25, n_times), n_entities) + y = 0.85 * X[:, 0] - 0.4 * X[:, 1] + alpha + tau + rng.normal( + scale=0.2, size=entity.size + ) + if unbalanced: + keep = np.ones(entity.size, dtype=bool) + keep[[1, 8, 17, 31, 44]] = False + X, y, entity, time = X[keep], y[keep], entity[keep], time[keep] + return X, y, entity, time + + +def _gap_free_unbalanced_panel(seed=1263): + """Return an unbalanced panel with contiguous time support per entity.""" + X, y, entity, time = _panel(seed=seed, unbalanced=False) + keep = np.ones(len(y), dtype=bool) + keep[(entity == 1) & (time >= 5)] = False + keep[(entity == 3) & (time >= 4)] = False + keep[(entity == 7) & (time >= 3)] = False + return X[keep], y[keep], entity[keep], time[keep] + + +def _lm_data(X, y, entity, time, *, constant=False): + index = pd.MultiIndex.from_arrays([entity, time], names=["entity", "time"]) + y_series = pd.Series(y, index=index, name="y") + X_frame = pd.DataFrame(X, index=index, columns=["x1", "x2"]) + if constant: + X_frame.insert(0, "const", 1.0) + return y_series, X_frame + + +def _assert_r2(actual, expected): + assert_allclose( + actual.rsquared_within, + expected.rsquared_within, + rtol=2e-10, + atol=2e-11, + ) + assert_allclose( + actual.rsquared_between, + expected.rsquared_between, + rtol=2e-10, + atol=2e-11, + ) + assert_allclose( + actual.rsquared_overall, + expected.rsquared_overall, + rtol=2e-10, + atol=2e-11, + ) + + +def _assert_model_f(actual, expected): + assert actual.f_statistic is not None + assert_allclose( + actual.f_statistic, + expected.f_statistic.stat, + rtol=2e-9, + atol=2e-11, + ) + assert_allclose( + actual.f_pvalue, + expected.f_statistic.pval, + rtol=2e-8, + atol=1e-14, + ) + assert actual.f_df == ( + float(expected.f_statistic.df), + float(expected.f_statistic.df_denom), + ) + + +def test_one_way_panelols_matches_linearmodels_parameter_r2_f_pooling_and_diagnostic_covariance(): + X, y, entity, time = _panel(unbalanced=True) + y_lm, X_lm = _lm_data(X, y, entity, time, constant=False) + lm = LMPanelOLS(y_lm, X_lm, entity_effects=True).fit( + cov_type="unadjusted", debiased=True + ) + sg = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity + ) + + assert_allclose(sg.coef_, lm.params.to_numpy(), rtol=2e-10, atol=2e-11) + _assert_r2(sg.fit_statistics_, lm) + _assert_model_f(sg.fit_statistics_, lm) + + pooled = sg.pooling_f_test() + assert pooled.applicable + assert_allclose(pooled.statistic, lm.f_pooled.stat, rtol=2e-9, atol=2e-11) + assert_allclose(pooled.pvalue, lm.f_pooled.pval, rtol=2e-8, atol=1e-14) + assert pooled.df == (float(lm.f_pooled.df), float(lm.f_pooled.df_denom)) + assert_allclose( + sg._panel_cov_params, + lm.cov.to_numpy(), + rtol=5e-9, + atol=5e-11, + ) + + +def test_two_way_panelols_matches_linearmodels_standard_diagnostics(): + X, y, entity, time = _panel(seed=1261) + y_lm, X_lm = _lm_data(X, y, entity, time, constant=False) + lm = LMPanelOLS( + y_lm, + X_lm, + entity_effects=True, + time_effects=True, + ).fit(cov_type="unadjusted", debiased=True) + sg = PanelOLS( + entity_effects=True, + time_effects=True, + cov_type="nonrobust", + ).fit(X, y, entity_ids=entity, time_ids=time) + + assert_allclose(sg.coef_, lm.params.to_numpy(), rtol=2e-10, atol=2e-11) + _assert_r2(sg.fit_statistics_, lm) + _assert_model_f(sg.fit_statistics_, lm) + pooled = sg.pooling_f_test() + assert pooled.applicable + assert_allclose(pooled.statistic, lm.f_pooled.stat, rtol=2e-9, atol=2e-11) + assert_allclose(pooled.pvalue, lm.f_pooled.pval, rtol=2e-8, atol=1e-14) + assert pooled.df == (float(lm.f_pooled.df), float(lm.f_pooled.df_denom)) + assert_allclose(sg._panel_cov_params, lm.cov.to_numpy(), rtol=5e-9, atol=5e-11) + + +def test_pooled_and_between_match_linearmodels_on_general_unbalanced_panel(): + X, y, entity, time = _panel(seed=1262, unbalanced=True) + y_lm, X_lm_const = _lm_data(X, y, entity, time, constant=True) + + lm_pool = LMPooledOLS(y_lm, X_lm_const).fit( + cov_type="unadjusted", debiased=True + ) + sg_pool = PooledOLS().fit(X, y, entity_ids=entity) + assert_allclose(sg_pool.coef_, lm_pool.params.to_numpy(), rtol=2e-10, atol=2e-11) + _assert_r2(sg_pool.fit_statistics_, lm_pool) + _assert_model_f(sg_pool.fit_statistics_, lm_pool) + + lm_between = LMBetweenOLS(y_lm, X_lm_const).fit( + cov_type="unadjusted", debiased=True + ) + sg_between = BetweenOLS().fit(X, y, entity_ids=entity) + assert_allclose( + sg_between.coef_, lm_between.params.to_numpy(), rtol=2e-10, atol=2e-11 + ) + _assert_r2(sg_between.fit_statistics_, lm_between) + _assert_model_f(sg_between.fit_statistics_, lm_between) + + +def test_random_effects_explicit_constant_matches_linearmodels_f_df_structure(): + X, y, entity, time = _panel(seed=1264, unbalanced=True) + y_lm, X_lm_const = _lm_data(X, y, entity, time, constant=True) + lm = LMRandomEffects(y_lm, X_lm_const).fit( + cov_type="unadjusted", debiased=True + ) + + X_const = np.column_stack([np.ones(X.shape[0]), X]) + sg = RandomEffects().fit(X_const, y, entity_ids=entity) + + assert sg.fit_statistics_.metadata["has_explicit_constant"] is True + assert sg.fit_statistics_.metadata["model_f"]["rank_restricted"] == 1 + assert sg.fit_statistics_.metadata["model_f"]["restricted_design_supplied"] is True + assert sg.fit_statistics_.f_df == ( + float(lm.f_statistic.df), + float(lm.f_statistic.df_denom), + ) + + +def test_hausman_absorbed_intercept_matches_linearmodels_parameter_structure(): + X, y, entity, time = _panel(seed=1265, unbalanced=True) + y_lm, X_lm = _lm_data(X, y, entity, time, constant=False) + _, X_lm_const = _lm_data(X, y, entity, time, constant=True) + + lm_fe = LMPanelOLS(y_lm, X_lm, entity_effects=True).fit( + cov_type="unadjusted", debiased=True + ) + lm_re = LMRandomEffects(y_lm, X_lm_const).fit( + cov_type="unadjusted", debiased=True + ) + assert tuple(lm_fe.params.index) == ("x1", "x2") + assert tuple(lm_re.params.index) == ("const", "x1", "x2") + + sg_fe = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity + ) + sg_re = RandomEffects().fit( + np.column_stack([np.ones(X.shape[0]), X]), + y, + entity_ids=entity, + ) + result = sg_fe.hausman_test(sg_re) + + assert "identity mismatch" not in (result.reason or "") + assert "no common estimable slope" not in (result.reason or "") + assert result.metadata["common_features"] == ("x1", "x2") + assert result.metadata["fe_coefficient_indices"] == (0, 1) + assert result.metadata["re_coefficient_indices"] == (1, 2) + assert result.metadata["re_explicit_constant_excluded"] is True + + +def test_first_difference_matches_linearmodels_when_transformed_sample_is_common(): + X, y, entity, time = _gap_free_unbalanced_panel() + y_lm, X_lm = _lm_data(X, y, entity, time, constant=False) + lm_fd = LMFirstDifferenceOLS(y_lm, X_lm).fit( + cov_type="unadjusted", debiased=True + ) + sg_fd = FirstDifferenceOLS().fit( + X, y, entity_ids=entity, time_ids=time + ) + assert_allclose(sg_fd.coef_, lm_fd.params.to_numpy(), rtol=2e-10, atol=2e-11) + _assert_r2(sg_fd.fit_statistics_, lm_fd) + _assert_model_f(sg_fd.fit_statistics_, lm_fd) diff --git a/dev/tests/test_panel_stage_b_physical_runner_contract.py b/dev/tests/test_panel_stage_b_physical_runner_contract.py new file mode 100644 index 000000000..178b0dde5 --- /dev/null +++ b/dev/tests/test_panel_stage_b_physical_runner_contract.py @@ -0,0 +1,148 @@ +"""Hosted smoke checks for the PR122 physical GPU acceptance runner.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from statgpu.panel import PanelOLS, RandomEffects + +from dev.benchmarks.validate_panel_stage_b_gpu import ( + _dataset, + _fit_cases, + _hausman_applicable_dataset, + _model_snapshot, + _require_applicable_hausman_coverage, +) + + +def test_physical_runner_covers_explicit_constant_re_balanced_and_unbalanced(): + expected_counts = {"balanced": 9, "unbalanced": 8} + + for name, unbalanced in (("balanced", False), ("unbalanced", True)): + X, y, entity, time = _dataset(20260808 + int(unbalanced), unbalanced=unbalanced) + models, diagnostics = _fit_cases( + X, + y, + entity, + time, + "numpy", + unbalanced=unbalanced, + ) + + assert len(models) == expected_counts[name] + assert set(diagnostics) == { + f"hausman_{name}", + f"hausman_explicit_re_constant_{name}", + } + + case = f"random_effects_explicit_constant_{name}" + assert case in models + contract = _model_snapshot(models[case])["random_effects_diagnostic_contract"] + assert contract == { + "has_explicit_constant": True, + "constant_column_index": 0, + "restricted_rank": 1, + "model_f_rank_restricted": 1, + "model_f_restricted_design_supplied": True, + } + + absorbed = diagnostics[f"hausman_explicit_re_constant_{name}"] + assert "identity mismatch" not in (absorbed["reason"] or "") + assert "no common estimable slope" not in (absorbed["reason"] or "") + + +def test_physical_runner_total_model_case_contract_is_seventeen(): + balanced = _dataset(20260808, unbalanced=False) + unbalanced = _dataset(20260809, unbalanced=True) + balanced_models, balanced_diagnostics = _fit_cases( + *balanced, "numpy", unbalanced=False + ) + unbalanced_models, unbalanced_diagnostics = _fit_cases( + *unbalanced, "numpy", unbalanced=True + ) + + case_ids = set(balanced_models) | set(unbalanced_models) + diagnostic_ids = set(balanced_diagnostics) | set(unbalanced_diagnostics) + assert len(case_ids) == 17 + assert len(diagnostic_ids) == 4 + assert "random_effects_explicit_constant_balanced" in case_ids + assert "random_effects_explicit_constant_unbalanced" in case_ids + assert "hausman_explicit_re_constant_balanced" in diagnostic_ids + assert "hausman_explicit_re_constant_unbalanced" in diagnostic_ids + + + +def test_physical_runner_has_stable_nonzero_effect_applicable_hausman_fixture(): + X, y, entity, _time, metadata = _hausman_applicable_dataset() + fe = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity + ) + re = RandomEffects().fit(X, y, entity_ids=entity) + result = fe.hausman_test(re) + variance_difference = float( + np.asarray(fe._panel_cov_params)[0, 0] + - np.asarray(re._panel_cov_params)[0, 0] + ) + + assert metadata == { + "seed": 20260810, + "n_entities": 12, + "n_times": 4, + "entity_effect_scale": 0.005, + "noise_scale": 0.1, + } + assert metadata["entity_effect_scale"] > 0.0 + assert X.shape == (48, 1) + assert result.applicable is True + assert result.reason is None + assert result.df == 1.0 + assert np.isfinite(float(result.statistic)) + assert np.isfinite(float(result.pvalue)) + assert variance_difference > 1e-6 + + +def test_physical_runner_requires_recorded_applicable_hausman_per_backend(): + with pytest.raises(AssertionError, match="successful applicable"): + _require_applicable_hausman_coverage( + { + "hausman_balanced": { + "status": "success", + "applicable": False, + }, + "hausman_unbalanced": { + "status": "success", + "applicable": False, + }, + }, + backend="torch", + ) + + with pytest.raises(AssertionError, match="successful applicable"): + _require_applicable_hausman_coverage( + { + "hausman_missing_values": { + "status": "success", + "applicable": True, + } + }, + backend="cupy", + ) + + applicable = _require_applicable_hausman_coverage( + { + "hausman_balanced": { + "status": "success", + "applicable": False, + }, + "hausman_applicable_nonzero_effect": { + "status": "success", + "applicable": True, + "statistic": 1.2, + "pvalue": 0.27, + "df": 1.0, + }, + }, + backend="cupy", + ) + assert applicable == ["hausman_applicable_nonzero_effect"] diff --git a/dev/tests/test_panel_stage_b_random_effects_backend_regression.py b/dev/tests/test_panel_stage_b_random_effects_backend_regression.py new file mode 100644 index 000000000..cddfae34b --- /dev/null +++ b/dev/tests/test_panel_stage_b_random_effects_backend_regression.py @@ -0,0 +1,72 @@ +"""Regression coverage for backend-stable explicit-constant RandomEffects.""" + +from __future__ import annotations + +import numpy as np +import pytest +from numpy.testing import assert_allclose + +from dev.benchmarks.validate_panel_stage_b_gpu import _dataset +from statgpu.panel import RandomEffects + + +@pytest.mark.parametrize("unbalanced", [False, True]) +def test_explicit_constant_re_does_not_depend_on_singular_solve_exception( + monkeypatch, + unbalanced, +): + """The within auxiliary fit must never solve a structural singular system. + + An explicit level constant is annihilated by entity demeaning. NumPy raises + on the resulting singular normal equations, but some GPU linalg stacks may + return a value or warning instead. Simulate that non-raising behavior and + require RandomEffects to avoid the singular solve entirely. + """ + X, y, entity, _ = _dataset( + 20260808 + int(unbalanced), + unbalanced=unbalanced, + ) + X_constant = np.column_stack([np.ones(X.shape[0]), X]) + + expected = RandomEffects(device="cpu").fit( + X_constant, + y, + entity_ids=entity, + ) + + original_solve = np.linalg.solve + singular_calls = [] + + def nonraising_solve(A, b): + A = np.asarray(A) + if A.ndim == 2 and A.shape[0] == A.shape[1]: + rank = np.linalg.matrix_rank(A) + if rank < A.shape[0]: + singular_calls.append((A.shape, int(rank))) + # Emulate a backend that does not raise on a singular solve. + return np.zeros_like(np.asarray(b), dtype=np.float64) + return original_solve(A, b) + + monkeypatch.setattr(np.linalg, "solve", nonraising_solve) + actual = RandomEffects(device="cpu").fit( + X_constant, + y, + entity_ids=entity, + ) + + assert singular_calls == [] + assert_allclose(actual.coef_, expected.coef_, rtol=1e-11, atol=1e-12) + assert_allclose(actual.bse_, expected.bse_, rtol=1e-11, atol=1e-12) + assert_allclose( + actual.variance_components_["sigma2_e"], + expected.variance_components_["sigma2_e"], + rtol=1e-12, + atol=1e-14, + ) + assert_allclose( + actual.variance_components_["sigma2_a"], + expected.variance_components_["sigma2_a"], + rtol=1e-12, + atol=1e-14, + ) + assert_allclose(actual.theta_, expected.theta_, rtol=1e-12, atol=1e-14) diff --git a/dev/tests/test_panel_stage_b_ready_review_regressions.py b/dev/tests/test_panel_stage_b_ready_review_regressions.py new file mode 100644 index 000000000..a3814f5da --- /dev/null +++ b/dev/tests/test_panel_stage_b_ready_review_regressions.py @@ -0,0 +1,119 @@ +"""Regression coverage for findings raised after PR #122 became ready for review.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import numpy as np + +from statgpu.panel import BetweenOLS, PanelOLS + + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +SOURCE_PATH = ( + REPO_ROOT + / "results" + / "benchmark_frontend_sources" + / "panel_stage_b_pr122_p100_20260809.json" +) + + +def test_disconnected_two_way_fe_uses_component_rank_before_df_gate() -> None: + # Three disconnected entity-time incidence components: two 2x2 blocks and + # one singleton. The true nuisance rank is N + T - C = 7, whereas the + # historical count uses (N - 1) + (T - 1) = 8. + entity = np.asarray([0, 0, 1, 1, 2, 2, 3, 3, 4], dtype=np.int64) + time = np.asarray([0, 1, 0, 1, 2, 3, 2, 3, 4], dtype=np.int64) + X = np.asarray([1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 0.0]).reshape(-1, 1) + y = np.asarray([1.0, -1.0, -1.0, 1.0, 2.0, -2.0, -2.0, 2.0, 0.0]) + + model = PanelOLS(entity_effects=True, time_effects=True).fit( + X, + y, + entity_ids=entity, + time_ids=time, + ) + + metadata = model.fit_statistics_.metadata + diagnostic_df = metadata["diagnostic_df"] + assert metadata["legacy_df_resid"] == 0 + assert metadata["public_df_resid_basis"] == "component-aware" + assert model.df_resid == 1 + assert diagnostic_df["incidence_components"] == 3 + assert diagnostic_df["effect_rank"] == 7 + assert diagnostic_df["rank_x"] == 1 + assert diagnostic_df["df_resid"] == 1 + assert np.all(np.isfinite(model.bse_)) + + +def _parse_mutated_source(tmp_path: Path, mutate) -> tuple[list[dict], list[str]]: + from dev.benchmarks.frontend_data.parsers import ( + parse_panel_stage_b_physical_validation, + ) + + data = json.loads(SOURCE_PATH.read_text(encoding="utf-8")) + mutate(data) + path = tmp_path / "mutated_panel_stage_b.json" + path.write_text(json.dumps(data), encoding="utf-8") + runs, _models, warnings = parse_panel_stage_b_physical_validation( + path, "test-pr122-aggregate-failure" + ) + return runs, warnings + + +def test_panel_stage_b_parser_rejects_failed_source_summary(tmp_path: Path) -> None: + runs, warnings = _parse_mutated_source( + tmp_path, + lambda data: data.__setitem__("status", "failed"), + ) + + assert len(runs) == 42 + assert any("physical validation status is not success" in item for item in warnings) + assert all(run["metrics"]["validation"]["status"] == "fail" for run in runs) + assert all( + any( + check["metric"] == "source_validation_status_success" + and check["status"] == "fail" + for check in run["metrics"]["validation"]["checks"] + ) + for run in runs + ) + estimator_runs = [run for run in runs if "inference" in run["metrics"]] + assert estimator_runs + assert all(run["metrics"]["inference"]["ok"] is False for run in estimator_runs) + + +def test_panel_stage_b_parser_rejects_failed_backend_summary(tmp_path: Path) -> None: + def mutate(data: dict) -> None: + data["backend_results"]["cupy"]["status"] = "failed" + + runs, warnings = _parse_mutated_source(tmp_path, mutate) + cupy_runs = [run for run in runs if run["backend"] == "cupy"] + torch_runs = [run for run in runs if run["backend"] == "torch"] + + assert len(cupy_runs) == 21 + assert len(torch_runs) == 21 + assert any("cupy backend validation status is not success" in item for item in warnings) + assert all(run["metrics"]["validation"]["status"] == "fail" for run in cupy_runs) + assert all(run["metrics"]["validation"]["status"] == "pass" for run in torch_runs) + assert all( + any( + check["metric"] == "backend_validation_status_success" + and check["status"] == "fail" + for check in run["metrics"]["validation"]["checks"] + ) + for run in cupy_runs + ) + + +def test_between_ols_keeps_detailed_class_contract_docstring() -> None: + doc = BetweenOLS.__doc__ or "" + assert "Collapses the data to entity means" in doc + assert "Parameters" in doc + assert "Attributes" in doc + assert "fit_statistics_" in doc diff --git a/dev/tests/test_panel_stage_b_review_regressions.py b/dev/tests/test_panel_stage_b_review_regressions.py new file mode 100644 index 000000000..46bdf3eaf --- /dev/null +++ b/dev/tests/test_panel_stage_b_review_regressions.py @@ -0,0 +1,334 @@ +"""Regression tests for PR #122 review findings closed after physical promotion.""" + +from __future__ import annotations + +import numpy as np +from numpy.testing import assert_allclose + +from statgpu.panel import PanelOLS, RandomEffects +from statgpu.panel._diagnostic_context import ( + explicit_constant_column, + fixed_effect_diagnostic_df, +) +from statgpu.panel._diagnostics import ( + _classical_model_f, + _diagnostic_identity, + _fingerprints_match, + _hausman_quadratic, + _pooling_f_from_sums, +) + + +def test_two_way_effect_rank_counts_disconnected_incidence_components(): + entity = np.asarray([0, 0, 1, 1, 2, 2, 3, 3], dtype=np.int64) + time = np.asarray([0, 1, 0, 1, 2, 3, 2, 3], dtype=np.int64) + X_transformed = np.arange(1.0, 9.0).reshape(-1, 1) + + result = fixed_effect_diagnostic_df( + X_transformed, + xp=np, + nobs=8, + n_entities=4, + n_times=4, + entity_effects=True, + time_effects=True, + entity_codes=entity, + time_codes=time, + ) + + assert result["incidence_components"] == 2 + assert result["effect_rank"] == 4 + 4 - 2 + assert result["rank_x"] == 1 + assert result["df_resid"] == 1 + assert result["df_total"] == 2 + + +def test_hausman_identity_rejects_low_order_moment_collision(): + y_left = np.asarray([0.0, 1.0, 3.0, 2.0]) + y_right = np.asarray([0.0, 2.0, 1.0, 3.0]) + X = np.asarray( + [ + [0.2, -0.4], + [0.7, 0.1], + [-0.3, 0.5], + [1.1, -0.2], + ] + ) + entity = np.asarray([0, 0, 1, 1], dtype=np.int64) + + assert y_left.sum() == y_right.sum() + assert np.dot(y_left, y_left) == np.dot(y_right, y_right) + weights = np.arange(1.0, 5.0) + assert np.dot(y_left, weights) == np.dot(y_right, weights) + + left = _diagnostic_identity(X, y_left, xp=np, entity_codes=entity) + right = _diagnostic_identity(X, y_right, xp=np, entity_codes=entity) + matched, reason = _fingerprints_match(left, right) + + assert not matched + assert "content_digest" in reason + assert ( + left["fingerprint"]["content_digest"] + != right["fingerprint"]["content_digest"] + ) + + +def test_hausman_identity_allows_re_only_explicit_constant(): + rng = np.random.default_rng(20260811) + entity = np.repeat(np.arange(4), 5) + X_slopes = rng.normal(size=(entity.size, 2)) + y = X_slopes @ np.asarray([0.7, -0.25]) + rng.normal(scale=0.1, size=entity.size) + X_re = np.column_stack([np.ones(entity.size), X_slopes]) + + fe_identity = _diagnostic_identity( + X_slopes, + y, + xp=np, + entity_codes=entity, + has_constant=False, + ) + re_identity = _diagnostic_identity( + X_re, + y, + xp=np, + entity_codes=entity, + has_constant=True, + ) + matched, reason = _fingerprints_match(fe_identity, re_identity) + + assert matched, reason + assert fe_identity["feature_names"] == ("x1", "x2") + assert re_identity["feature_names"] == ("x1", "x2") + assert fe_identity["coefficient_indices"] == (0, 1) + assert re_identity["coefficient_indices"] == (1, 2) + assert re_identity["constant_column_index"] == 0 + assert ( + fe_identity["fingerprint"]["content_digest"] + == re_identity["fingerprint"]["content_digest"] + ) + + X_re_changed = X_re.copy() + X_re_changed[0, 2] += 0.5 + changed_identity = _diagnostic_identity( + X_re_changed, + y, + xp=np, + entity_codes=entity, + has_constant=True, + ) + changed_match, changed_reason = _fingerprints_match( + fe_identity, changed_identity + ) + assert not changed_match + assert "content_digest" in changed_reason + + +def test_hausman_end_to_end_excludes_re_only_explicit_constant(): + rng = np.random.default_rng(20260812) + counts = np.asarray([5, 4, 5, 4, 5, 4]) + entity = np.repeat(np.arange(counts.size), counts) + X_slopes = rng.normal(size=(entity.size, 2)) + alpha = np.repeat(np.linspace(-0.35, 0.45, counts.size), counts) + y = 1.25 + X_slopes @ np.asarray([0.65, -0.3]) + alpha + rng.normal( + scale=0.16, size=entity.size + ) + + fe = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X_slopes, y, entity_ids=entity + ) + re = RandomEffects().fit( + np.column_stack([np.ones(entity.size), X_slopes]), + y, + entity_ids=entity, + ) + result = fe.hausman_test(re) + + assert "identity mismatch" not in (result.reason or "") + assert "no common estimable slope" not in (result.reason or "") + assert result.metadata["common_features"] == ("x1", "x2") + assert result.metadata["fe_coefficient_indices"] == (0, 1) + assert result.metadata["re_coefficient_indices"] == (1, 2) + assert result.metadata["re_explicit_constant_excluded"] is True + + +def test_classical_model_f_reports_infinite_statistic_for_exact_fit(): + x = np.linspace(-1.0, 1.0, 8) + X = np.column_stack([np.ones(x.size), x]) + params = np.asarray([1.25, -0.8]) + y = X @ params + + statistic, pvalue, df, metadata = _classical_model_f( + y, + X, + params, + xp=np, + df_resid=x.size - np.linalg.matrix_rank(X), + has_constant=True, + ) + + assert np.isinf(statistic) + assert pvalue == 0.0 + assert df == (1.0, 6.0) + assert metadata["exact_fit"] is True + assert metadata["rss_restricted"] > 0.0 + assert metadata["rss_unrestricted"] <= metadata["rss_restricted"] + + +def test_classical_model_f_is_invariant_to_response_units(): + x = np.linspace(-1.0, 1.0, 12) + X = np.column_stack([np.ones(x.size), x]) + y = 0.8 + 0.45 * x + np.asarray( + [0.08, -0.04, 0.03, -0.06, 0.05, -0.02, 0.01, 0.04, -0.03, 0.02, -0.01, 0.05] + ) + params = np.linalg.lstsq(X, y, rcond=None)[0] + df_resid = x.size - np.linalg.matrix_rank(X) + + reference = _classical_model_f( + y, + X, + params, + xp=np, + df_resid=df_resid, + has_constant=True, + ) + assert reference[0] is not None and np.isfinite(reference[0]) + + for scale in (1e-8, 1e-12): + candidate = _classical_model_f( + scale * y, + X, + scale * params, + xp=np, + df_resid=df_resid, + has_constant=True, + ) + assert candidate[0] is not None and np.isfinite(candidate[0]) + assert_allclose(candidate[0], reference[0], rtol=1e-10, atol=1e-12) + assert_allclose(candidate[1], reference[1], rtol=1e-10, atol=1e-14) + assert candidate[2] == reference[2] + + +def test_hausman_quadratic_is_invariant_to_parameter_units(): + difference = np.asarray([0.2, -0.1]) + covariance_difference = np.asarray([[0.04, 0.01], [0.01, 0.09]]) + reference = _hausman_quadratic(difference, covariance_difference) + assert reference.applicable + + for scale in (1e-8, 1e-12): + candidate = _hausman_quadratic( + scale * difference, + (scale * scale) * covariance_difference, + ) + assert candidate.applicable + assert candidate.df == reference.df + assert_allclose(candidate.statistic, reference.statistic, rtol=1e-10, atol=1e-12) + assert_allclose(candidate.pvalue, reference.pvalue, rtol=1e-10, atol=1e-14) + + +def test_explicit_constant_detection_is_invariant_to_column_units(): + slope = np.linspace(-1.0, 1.0, 9) + for scale in (1.0, 1e-8, 1e-12): + X = np.column_stack([np.full(slope.size, scale), slope]) + assert explicit_constant_column(X, xp=np) == 0 + + zero_column = np.column_stack([np.zeros(slope.size), slope]) + assert explicit_constant_column(zero_column, xp=np) is None + + +def test_pooling_f_reports_infinite_statistic_for_exact_effect_fit(): + result = _pooling_f_from_sums( + rss_pooled=4.0, + rss_effects=0.0, + df_num=3, + df_denom=10, + ) + + assert result.applicable + assert np.isinf(result.statistic) + assert result.pvalue == 0.0 + assert result.df == (3.0, 10.0) + assert result.metadata["exact_fit"] is True + + +def test_pooling_f_both_exact_models_remains_inapplicable(): + result = _pooling_f_from_sums( + rss_pooled=0.0, + rss_effects=0.0, + df_num=3, + df_denom=10, + ) + + assert not result.applicable + assert result.statistic is None + assert result.pvalue is None + assert "both zero" in result.reason + + +def test_random_effects_explicit_constant_uses_constant_aware_diagnostics(): + rng = np.random.default_rng(20260808) + counts = np.asarray([5, 4, 3, 5, 4, 3]) + entity = np.repeat(np.arange(counts.size), counts) + slope = rng.normal(size=entity.size) + X = np.column_stack([np.ones(entity.size), slope]) + alpha = np.repeat(np.linspace(-0.4, 0.5, counts.size), counts) + y = 1.7 + 0.65 * slope + alpha + rng.normal(scale=0.12, size=entity.size) + + model = RandomEffects().fit(X, y, entity_ids=entity) + result = model.fit_statistics_ + + assert result.metadata["has_explicit_constant"] is True + assert result.metadata["constant_column_index"] == 0 + assert result.metadata["restricted_rank"] == 1 + assert result.metadata["model_f"]["rank_restricted"] == 1 + assert result.metadata["model_f"]["restricted_design_supplied"] is True + assert result.f_df == (1.0, float(entity.size - np.linalg.matrix_rank(X))) + + level_resid = y - X @ model.coef_ + expected_overall = 1.0 - np.dot(level_resid, level_resid) / np.sum( + (y - y.mean()) ** 2 + ) + assert_allclose(result.rsquared_overall, expected_overall, rtol=1e-12, atol=1e-12) + + +def test_random_effects_without_constant_preserves_uncentered_diagnostic_basis(): + rng = np.random.default_rng(20260809) + entity = np.repeat(np.arange(6), 4) + X = rng.normal(size=(entity.size, 2)) + y = X @ np.asarray([0.7, -0.3]) + np.repeat( + np.linspace(-0.3, 0.4, 6), 4 + ) + rng.normal(scale=0.15, size=entity.size) + + model = RandomEffects().fit(X, y, entity_ids=entity) + result = model.fit_statistics_ + + assert result.metadata["has_explicit_constant"] is False + assert result.metadata["constant_column_index"] is None + assert result.metadata["restricted_rank"] == 0 + assert result.metadata["model_f"].get("restricted_design_supplied") is None + assert result.metadata["model_f"]["rank_restricted"] == 0 + + +def test_fe_full_content_identity_only_for_hausman_compatible_fits(): + rng = np.random.default_rng(20260810) + entity = np.repeat(np.arange(6), 4) + time = np.tile(np.arange(4), 6) + X = rng.normal(size=(entity.size, 2)) + y = X @ np.asarray([0.6, -0.25]) + np.repeat( + np.linspace(-0.4, 0.5, 6), 4 + ) + rng.normal(scale=0.1, size=entity.size) + + classical = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity + ) + robust = PanelOLS(entity_effects=True, cov_type="robust").fit( + X, y, entity_ids=entity + ) + two_way = PanelOLS( + entity_effects=True, + time_effects=True, + cov_type="nonrobust", + ).fit(X, y, entity_ids=entity, time_ids=time) + + assert isinstance(classical._panel_diagnostic_identity, dict) + assert robust._panel_diagnostic_identity is None + assert two_way._panel_diagnostic_identity is None diff --git a/dev/tests/test_panel_stage_b_torch_cpu.py b/dev/tests/test_panel_stage_b_torch_cpu.py new file mode 100644 index 000000000..79efc9486 --- /dev/null +++ b/dev/tests/test_panel_stage_b_torch_cpu.py @@ -0,0 +1,261 @@ +"""Hosted Torch-CPU parity coverage for Panel Tier-1 Stage B diagnostics.""" + +from __future__ import annotations + +import numpy as np +import pytest +from numpy.testing import assert_allclose + +from statgpu.panel import FamaMacBeth, PanelOLS, PooledOLS, RandomEffects +from statgpu.panel._diagnostic_context import explicit_constant_column +from statgpu.panel._diagnostics import _diagnostic_identity, _fingerprints_match + + +torch = pytest.importorskip("torch") + + +def _panel(seed=1220): + rng = np.random.default_rng(seed) + n_entities, n_times = 7, 5 + entity = np.repeat(np.arange(n_entities), n_times) + time = np.tile(np.arange(n_times), n_entities) + X = rng.normal(size=(entity.size, 2)) + alpha = np.repeat(np.linspace(-0.5, 0.7, n_entities), n_times) + y = 0.8 * X[:, 0] - 0.35 * X[:, 1] + alpha + rng.normal( + scale=0.22, size=entity.size + ) + return X, y, entity, time + + +def _torch_arrays(X, y, entity, time): + return ( + torch.as_tensor(X, dtype=torch.float64), + torch.as_tensor(y, dtype=torch.float64), + torch.as_tensor(entity, dtype=torch.int64), + torch.as_tensor(time, dtype=torch.int64), + ) + + +def _assert_fit_statistics_close(actual, expected, *, include_adjusted=True): + for name in ("rsquared_within", "rsquared_between", "rsquared_overall"): + a = getattr(actual, name) + e = getattr(expected, name) + if e is None: + assert a is None + else: + assert_allclose(a, e, rtol=5e-9, atol=5e-10) + if include_adjusted: + assert_allclose( + actual.rsquared_adj, + expected.rsquared_adj, + rtol=5e-9, + atol=5e-10, + ) + if expected.f_statistic is None: + assert actual.f_statistic is None + assert actual.f_pvalue is None + assert actual.f_df is None + else: + assert_allclose( + actual.f_statistic, + expected.f_statistic, + rtol=5e-9, + atol=5e-10, + ) + assert_allclose( + actual.f_pvalue, + expected.f_pvalue, + rtol=1e-8, + atol=1e-14, + ) + assert_allclose(actual.f_df, expected.f_df, rtol=0, atol=0) + + +def test_stage_b_pooled_torch_cpu_matches_numpy(): + X, y, entity, time = _panel() + X_t, y_t, entity_t, time_t = _torch_arrays(X, y, entity, time) + + expected = PooledOLS().fit(X, y, entity_ids=entity) + actual = PooledOLS().fit(X_t, y_t, entity_ids=entity_t) + assert_allclose(actual.coef_, expected.coef_, rtol=1e-10, atol=1e-11) + _assert_fit_statistics_close(actual.fit_statistics_, expected.fit_statistics_) + bp_expected = expected.breusch_pagan_lm_test() + bp_actual = actual.breusch_pagan_lm_test() + assert bp_actual.applicable == bp_expected.applicable + assert_allclose(bp_actual.statistic, bp_expected.statistic, rtol=5e-9, atol=5e-10) + assert_allclose(bp_actual.pvalue, bp_expected.pvalue, rtol=1e-8, atol=1e-14) + + scrambled = torch.as_tensor((3 - time + 2 * entity) % 5, dtype=torch.int64) + hac = PooledOLS(cov_type="hac", bandwidth=1).fit( + X_t, + y_t, + time_index=scrambled, + entity_ids=entity_t, + ) + assert_allclose( + hac.breusch_pagan_lm_test().statistic, + bp_actual.statistic, + rtol=5e-9, + atol=5e-10, + ) + + +def test_stage_b_panel_fe_torch_cpu_pooling_and_fit_stats_match_numpy(): + X, y, entity, time = _panel(seed=1221) + X_t, y_t, entity_t, _ = _torch_arrays(X, y, entity, time) + + expected = PanelOLS(entity_effects=True).fit(X, y, entity_ids=entity) + actual = PanelOLS(entity_effects=True).fit(X_t, y_t, entity_ids=entity_t) + assert_allclose(actual.coef_, expected.coef_, rtol=1e-10, atol=1e-11) + _assert_fit_statistics_close(actual.fit_statistics_, expected.fit_statistics_) + pool_expected = expected.pooling_f_test() + pool_actual = actual.pooling_f_test() + assert pool_actual.applicable == pool_expected.applicable + assert_allclose(pool_actual.statistic, pool_expected.statistic, rtol=5e-9, atol=5e-10) + assert_allclose(pool_actual.pvalue, pool_expected.pvalue, rtol=1e-8, atol=1e-14) + assert pool_actual.df == pool_expected.df + assert actual.df_resid == expected.df_resid + assert actual.fit_statistics_.metadata["diagnostic_df"] == expected.fit_statistics_.metadata["diagnostic_df"] + + +def test_stage_b_disconnected_two_way_fe_torch_cpu_uses_component_df(): + entity = np.asarray([0, 0, 1, 1, 2, 2, 3, 3, 4], dtype=np.int64) + time = np.asarray([0, 1, 0, 1, 2, 3, 2, 3, 4], dtype=np.int64) + X = np.asarray([1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 0.0]).reshape(-1, 1) + y = np.asarray([1.0, -1.0, -1.0, 1.0, 2.0, -2.0, -2.0, 2.0, 0.0]) + X_t, y_t, entity_t, time_t = _torch_arrays(X, y, entity, time) + + expected = PanelOLS(entity_effects=True, time_effects=True).fit( + X, y, entity_ids=entity, time_ids=time + ) + actual = PanelOLS(entity_effects=True, time_effects=True).fit( + X_t, y_t, entity_ids=entity_t, time_ids=time_t + ) + + assert_allclose(actual.coef_, expected.coef_, rtol=1e-10, atol=1e-11) + assert_allclose(actual.bse_, expected.bse_, rtol=1e-10, atol=1e-11) + assert_allclose(actual.tvalues_, expected.tvalues_, rtol=1e-10, atol=1e-11) + assert_allclose(actual.pvalues_, expected.pvalues_, rtol=1e-10, atol=1e-12) + assert_allclose(actual.conf_int_, expected.conf_int_, rtol=1e-10, atol=1e-10) + assert actual.df_resid == expected.df_resid == 1 + assert actual.fit_statistics_.metadata["legacy_df_resid"] == 0 + assert actual.fit_statistics_.metadata["public_df_resid_basis"] == "component-aware" + assert actual.fit_statistics_.metadata["diagnostic_df"] == expected.fit_statistics_.metadata["diagnostic_df"] + assert actual.fit_statistics_.metadata["diagnostic_df"]["incidence_components"] == 3 + + +def test_stage_b_random_effects_torch_cpu_fit_stats_and_identity_match_numpy(): + X, y, entity, time = _panel(seed=1222) + X_t, y_t, entity_t, _ = _torch_arrays(X, y, entity, time) + + expected = RandomEffects().fit(X, y, entity_ids=entity) + actual = RandomEffects().fit(X_t, y_t, entity_ids=entity_t) + assert_allclose(actual.coef_, expected.coef_, rtol=1e-9, atol=1e-10) + _assert_fit_statistics_close(actual.fit_statistics_, expected.fit_statistics_) + + fe_expected = PanelOLS(entity_effects=True).fit(X, y, entity_ids=entity) + fe_actual = PanelOLS(entity_effects=True).fit(X_t, y_t, entity_ids=entity_t) + h_expected = fe_expected.hausman_test(expected) + h_actual = fe_actual.hausman_test(actual) + assert h_actual.applicable == h_expected.applicable + if h_expected.applicable: + assert_allclose(h_actual.statistic, h_expected.statistic, rtol=1e-7, atol=1e-9) + assert_allclose(h_actual.pvalue, h_expected.pvalue, rtol=1e-7, atol=1e-12) + assert h_actual.df == h_expected.df + else: + assert h_actual.reason == h_expected.reason + + +def test_stage_b_random_effects_explicit_constant_torch_cpu_matches_numpy(): + rng = np.random.default_rng(1224) + counts = np.asarray([5, 4, 3, 5, 4, 3]) + entity = np.repeat(np.arange(counts.size), counts) + time = np.concatenate([np.arange(count) for count in counts]) + slope = rng.normal(size=entity.size) + X = np.column_stack([np.ones(entity.size), slope]) + alpha = np.repeat(np.linspace(-0.45, 0.55, counts.size), counts) + y = 1.4 + 0.72 * slope + alpha + rng.normal(scale=0.14, size=entity.size) + X_t, y_t, entity_t, _ = _torch_arrays(X, y, entity, time) + + expected = RandomEffects().fit(X, y, entity_ids=entity) + actual = RandomEffects().fit(X_t, y_t, entity_ids=entity_t) + + assert_allclose(actual.coef_, expected.coef_, rtol=1e-9, atol=1e-10) + _assert_fit_statistics_close(actual.fit_statistics_, expected.fit_statistics_) + assert actual.fit_statistics_.metadata["has_explicit_constant"] is True + assert actual.fit_statistics_.metadata["constant_column_index"] == 0 + assert actual.fit_statistics_.metadata["model_f"]["rank_restricted"] == 1 + assert actual.fit_statistics_.metadata["model_f"]["restricted_design_supplied"] is True + assert ( + actual._panel_diagnostic_identity["fingerprint"]["content_digest"] + == expected._panel_diagnostic_identity["fingerprint"]["content_digest"] + ) + + +def test_stage_b_absorbed_intercept_identity_matches_on_torch_cpu(): + rng = np.random.default_rng(1225) + entity = np.repeat(np.arange(5), 4) + slopes = rng.normal(size=(entity.size, 2)) + y = slopes @ np.asarray([0.55, -0.2]) + rng.normal(scale=0.1, size=entity.size) + slopes_t = torch.as_tensor(slopes, dtype=torch.float64) + y_t = torch.as_tensor(y, dtype=torch.float64) + entity_t = torch.as_tensor(entity, dtype=torch.int64) + re_t = torch.cat( + [torch.ones((entity.size, 1), dtype=torch.float64), slopes_t], dim=1 + ) + + fe_identity = _diagnostic_identity( + slopes_t, + y_t, + xp=torch, + entity_codes=entity_t, + has_constant=False, + ) + re_identity = _diagnostic_identity( + re_t, + y_t, + xp=torch, + entity_codes=entity_t, + has_constant=True, + ) + matched, reason = _fingerprints_match(fe_identity, re_identity) + + assert matched, reason + assert fe_identity["coefficient_indices"] == (0, 1) + assert re_identity["coefficient_indices"] == (1, 2) + assert fe_identity["feature_names"] == re_identity["feature_names"] == ("x1", "x2") + + +def test_stage_b_torch_constant_detection_is_scale_invariant(): + slope = torch.linspace(-1.0, 1.0, 9, dtype=torch.float64) + for scale in (1.0, 1e-8, 1e-12): + X = torch.stack( + [torch.full_like(slope, scale), slope], + dim=1, + ) + assert explicit_constant_column(X, xp=torch) == 0 + + +def test_stage_b_fama_macbeth_torch_cpu_r2_matches_numpy_without_ols_f(): + X, y, entity, time = _panel(seed=1223) + X_t, y_t, entity_t, time_t = _torch_arrays(X, y, entity, time) + + expected = FamaMacBeth(cov_type="newey-west", bandwidth=1).fit( + X, y, time_ids=time, entity_ids=entity + ) + actual = FamaMacBeth(cov_type="newey-west", bandwidth=1).fit( + X_t, y_t, time_ids=time_t, entity_ids=entity_t + ) + assert_allclose( + actual.coef_.detach().cpu().numpy(), + np.asarray(expected.coef_), + rtol=1e-9, + atol=1e-10, + ) + _assert_fit_statistics_close( + actual.fit_statistics_, expected.fit_statistics_, include_adjusted=False + ) + assert actual.fit_statistics_.rsquared_adj is None + assert actual.fit_statistics_.f_statistic is None + assert actual.fit_statistics_.f_pvalue is None + assert actual.fit_statistics_.f_df is None \ No newline at end of file diff --git a/docs/assets/benchmarks/data/benchmark_data.json b/docs/assets/benchmarks/data/benchmark_data.json index bb083b1af..b7b025ba4 100644 --- a/docs/assets/benchmarks/data/benchmark_data.json +++ b/docs/assets/benchmarks/data/benchmark_data.json @@ -4,7 +4,7 @@ "meta": { "generator": "dev/benchmarks/generate_benchmark_data.py", "git_sha": "deterministic", - "generation_id": "ceac2591d688920101474d09e145c7595e6d9460ce347828b4225139fcd60644" + "generation_id": "b425b95947fcb5ee7dcd4e6e10f1b108cfe82b804f5d46428e63c1fb5ca35b1d" }, "environments": [ { @@ -27,6 +27,12 @@ "gpu": "Tesla P100-SXM2-16GB", "cpu": "x86_64", "host": "wE6lDe" + }, + { + "env_id": "remote-p100-pr122-20260809", + "label": "Tesla P100 PR #122 Panel Stage B validation — 2026-08-09", + "gpu": "Tesla P100-SXM2-16GB", + "cpu": "x86_64" } ], "categories": [ @@ -110,6 +116,15 @@ "supports_penalty": false, "supports_inference": false }, + { + "model_id": "BetweenOLS", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, { "model_id": "BonferroniCorrection", "primary_category_id": "anova", @@ -165,6 +180,24 @@ "supports_penalty": false, "supports_inference": true }, + { + "model_id": "FamaMacBeth", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, + { + "model_id": "FirstDifferenceOLS", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, { "model_id": "GAM", "primary_category_id": "nonparametric", @@ -382,6 +415,15 @@ "supports_penalty": true, "supports_inference": true }, + { + "model_id": "PooledOLS", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, { "model_id": "QuantileRegression", "primary_category_id": "robust_quantile", @@ -562,6 +604,11 @@ "label": "Covariance and nonparametric benchmark — 2026-06-17", "env_id": "remote-p100" }, + { + "comparison_id": "panel-stage-b-pr122-20260809", + "label": "Panel Stage B physical validation — PR #122 — 2026-08-09", + "env_id": "remote-p100-pr122-20260809" + }, { "comparison_id": "penalized-glm-perf-20260622", "label": "Penalized GLM performance — 2026-06-22", @@ -84727,7 +84774,7 @@ "metrics": { "timing": { "fit_time_ms": 9.062319993972778, - "std_ms": 0.011520549774405635, + "std_ms": 0.011520549774, "min_ms": 9.062081575393677, "max_ms": 9.086638689041138, "sample_count": 3, @@ -84882,7 +84929,7 @@ "metrics": { "timing": { "fit_time_ms": 17.41701364517212, - "std_ms": 0.10901293932213849, + "std_ms": 0.109012939322, "min_ms": 17.355382442474365, "max_ms": 17.611205577850342, "sample_count": 3, @@ -85037,7 +85084,7 @@ "metrics": { "timing": { "fit_time_ms": 12.491881847381592, - "std_ms": 0.10357847916711392, + "std_ms": 0.103578479167, "min_ms": 12.459933757781982, "max_ms": 12.69388198852539, "sample_count": 3, @@ -85192,7 +85239,7 @@ "metrics": { "timing": { "fit_time_ms": 7.204592227935791, - "std_ms": 0.03478300871847091, + "std_ms": 0.034783008718, "min_ms": 7.1443915367126465, "max_ms": 7.226705551147461, "sample_count": 3, @@ -85347,7 +85394,7 @@ "metrics": { "timing": { "fit_time_ms": 29.17364239692688, - "std_ms": 0.11058449868330528, + "std_ms": 0.110584498683, "min_ms": 29.15489673614502, "max_ms": 29.398292303085327, "sample_count": 3, @@ -85502,7 +85549,7 @@ "metrics": { "timing": { "fit_time_ms": 76.099693775177, - "std_ms": 0.15514420258897654, + "std_ms": 0.155144202589, "min_ms": 75.85901021957397, "max_ms": 76.23404264450073, "sample_count": 3, @@ -85657,7 +85704,7 @@ "metrics": { "timing": { "fit_time_ms": 34.79743003845215, - "std_ms": 0.08212314243487934, + "std_ms": 0.082123142435, "min_ms": 34.72268581390381, "max_ms": 34.921795129776, "sample_count": 3, @@ -85812,7 +85859,7 @@ "metrics": { "timing": { "fit_time_ms": 7.392674684524536, - "std_ms": 0.06373652588749994, + "std_ms": 0.063736525887, "min_ms": 7.343053817749023, "max_ms": 7.496058940887451, "sample_count": 3, @@ -85968,7 +86015,7 @@ "metrics": { "timing": { "fit_time_ms": 76.40200853347778, - "std_ms": 0.12667643292286518, + "std_ms": 0.126676432923, "min_ms": 76.32750272750854, "max_ms": 76.62561535835266, "sample_count": 3, @@ -86125,7 +86172,7 @@ "metrics": { "timing": { "fit_time_ms": 153.78409624099731, - "std_ms": 1.2051632389990876, + "std_ms": 1.205163238999, "min_ms": 151.39129757881165, "max_ms": 154.0849506855011, "sample_count": 3, @@ -86282,7 +86329,7 @@ "metrics": { "timing": { "fit_time_ms": 122.1560537815094, - "std_ms": 1.2313895375387158, + "std_ms": 1.231389537539, "min_ms": 122.06673622131348, "max_ms": 124.722421169281, "sample_count": 3, @@ -86439,7 +86486,7 @@ "metrics": { "timing": { "fit_time_ms": 7.413893938064575, - "std_ms": 0.03394906482522582, + "std_ms": 0.033949064825, "min_ms": 7.350414991378784, "max_ms": 7.428675889968872, "sample_count": 3, @@ -86595,7 +86642,7 @@ "metrics": { "timing": { "fit_time_ms": 50.83748698234558, - "std_ms": 0.5573683115942412, + "std_ms": 0.557368311594, "min_ms": 50.72379112243652, "max_ms": 51.95888876914978, "sample_count": 3, @@ -86750,7 +86797,7 @@ "metrics": { "timing": { "fit_time_ms": 103.54551672935486, - "std_ms": 0.43010181393284636, + "std_ms": 0.430101813933, "min_ms": 102.88560390472412, "max_ms": 103.92677783966064, "sample_count": 3, @@ -87008,7 +87055,7 @@ "metrics": { "timing": { "fit_time_ms": 32.809287309646606, - "std_ms": 0.22101482164439643, + "std_ms": 0.221014821644, "min_ms": 32.615602016448975, "max_ms": 33.15025568008423, "sample_count": 3, @@ -87163,7 +87210,7 @@ "metrics": { "timing": { "fit_time_ms": 5.704343318939209, - "std_ms": 0.05382879156628554, + "std_ms": 0.053828791566, "min_ms": 5.664348602294922, "max_ms": 5.793154239654541, "sample_count": 3, @@ -87318,7 +87365,7 @@ "metrics": { "timing": { "fit_time_ms": 79.3997049331665, - "std_ms": 0.2710391095246946, + "std_ms": 0.271039109525, "min_ms": 79.18986678123474, "max_ms": 79.8402726650238, "sample_count": 3, @@ -87473,7 +87520,7 @@ "metrics": { "timing": { "fit_time_ms": 53.322941064834595, - "std_ms": 0.0964612495050475, + "std_ms": 0.096461249505, "min_ms": 53.19675803184509, "max_ms": 53.432852029800415, "sample_count": 3, @@ -87628,7 +87675,7 @@ "metrics": { "timing": { "fit_time_ms": 367.21765995025635, - "std_ms": 1.2007614908480113, + "std_ms": 1.200761490848, "min_ms": 367.20117926597595, "max_ms": 369.7565793991089, "sample_count": 3, @@ -87783,7 +87830,7 @@ "metrics": { "timing": { "fit_time_ms": 387.08221912384033, - "std_ms": 1.7942564379041452, + "std_ms": 1.794256437904, "min_ms": 385.0722312927246, "max_ms": 389.46205377578735, "sample_count": 3, @@ -87938,7 +87985,7 @@ "metrics": { "timing": { "fit_time_ms": 226.59814357757568, - "std_ms": 1.4370176592588964, + "std_ms": 1.437017659259, "min_ms": 225.08418560028076, "max_ms": 228.59317064285278, "sample_count": 3, @@ -88093,7 +88140,7 @@ "metrics": { "timing": { "fit_time_ms": 9.013503789901733, - "std_ms": 0.08388103984794355, + "std_ms": 0.083881039848, "min_ms": 8.965373039245605, "max_ms": 9.162425994873047, "sample_count": 3, @@ -88248,7 +88295,7 @@ "metrics": { "timing": { "fit_time_ms": 17.37356185913086, - "std_ms": 0.1751727379638956, + "std_ms": 0.175172737964, "min_ms": 17.317146062850952, "max_ms": 17.713725566864014, "sample_count": 3, @@ -88403,7 +88450,7 @@ "metrics": { "timing": { "fit_time_ms": 12.376457452774048, - "std_ms": 0.1469539035687877, + "std_ms": 0.146953903569, "min_ms": 12.127071619033813, "max_ms": 12.476563453674316, "sample_count": 3, @@ -88558,7 +88605,7 @@ "metrics": { "timing": { "fit_time_ms": 7.113069295883179, - "std_ms": 0.022110917652291093, + "std_ms": 0.022110917652, "min_ms": 7.089287042617798, "max_ms": 7.1433186531066895, "sample_count": 3, @@ -88713,7 +88760,7 @@ "metrics": { "timing": { "fit_time_ms": 29.22457456588745, - "std_ms": 0.07344943972222218, + "std_ms": 0.073449439722, "min_ms": 29.094338417053223, "max_ms": 29.266953468322754, "sample_count": 3, @@ -88868,7 +88915,7 @@ "metrics": { "timing": { "fit_time_ms": 76.48885250091553, - "std_ms": 0.7515277112219284, + "std_ms": 0.751527711222, "min_ms": 76.3988196849823, "max_ms": 78.03615927696228, "sample_count": 3, @@ -89023,7 +89070,7 @@ "metrics": { "timing": { "fit_time_ms": 34.95398163795471, - "std_ms": 0.08122915143822747, + "std_ms": 0.081229151438, "min_ms": 34.94828939437866, "max_ms": 35.12337803840637, "sample_count": 3, @@ -89178,7 +89225,7 @@ "metrics": { "timing": { "fit_time_ms": 7.375836372375488, - "std_ms": 0.2661149489547812, + "std_ms": 0.266114948955, "min_ms": 7.340103387832642, "max_ms": 7.92163610458374, "sample_count": 3, @@ -89334,7 +89381,7 @@ "metrics": { "timing": { "fit_time_ms": 75.77601075172424, - "std_ms": 0.45205802075521384, + "std_ms": 0.452058020755, "min_ms": 75.26275515556335, "max_ms": 76.36910676956177, "sample_count": 3, @@ -89491,7 +89538,7 @@ "metrics": { "timing": { "fit_time_ms": 153.40235829353333, - "std_ms": 0.5537958099223218, + "std_ms": 0.553795809922, "min_ms": 153.37586402893066, "max_ms": 154.56366539001465, "sample_count": 3, @@ -89648,7 +89695,7 @@ "metrics": { "timing": { "fit_time_ms": 121.93247675895691, - "std_ms": 0.2863016881758537, + "std_ms": 0.286301688176, "min_ms": 121.55237793922424, "max_ms": 122.25282192230225, "sample_count": 3, @@ -89805,7 +89852,7 @@ "metrics": { "timing": { "fit_time_ms": 7.36507773399353, - "std_ms": 0.0928811561208376, + "std_ms": 0.092881156121, "min_ms": 7.2509050369262695, "max_ms": 7.4784159660339355, "sample_count": 3, @@ -89961,7 +90008,7 @@ "metrics": { "timing": { "fit_time_ms": 51.06106400489807, - "std_ms": 0.12425235011605137, + "std_ms": 0.124252350116, "min_ms": 50.835102796554565, "max_ms": 51.12466216087341, "sample_count": 3, @@ -90116,7 +90163,7 @@ "metrics": { "timing": { "fit_time_ms": 95.80263495445251, - "std_ms": 3.1773095837367222, + "std_ms": 3.177309583737, "min_ms": 89.1546905040741, "max_ms": 95.98329663276672, "sample_count": 3, @@ -90271,7 +90318,7 @@ "metrics": { "timing": { "fit_time_ms": 49.2972731590271, - "std_ms": 2.079922258152906, + "std_ms": 2.079922258153, "min_ms": 48.696696758270264, "max_ms": 53.378403186798096, "sample_count": 3, @@ -90426,7 +90473,7 @@ "metrics": { "timing": { "fit_time_ms": 32.66507387161255, - "std_ms": 0.16058180541689593, + "std_ms": 0.160581805417, "min_ms": 32.47341513633728, "max_ms": 32.866716384887695, "sample_count": 3, @@ -90581,7 +90628,7 @@ "metrics": { "timing": { "fit_time_ms": 5.652785301208496, - "std_ms": 0.0647337375076041, + "std_ms": 0.064733737508, "min_ms": 5.633354187011719, "max_ms": 5.779355764389038, "sample_count": 3, @@ -90736,7 +90783,7 @@ "metrics": { "timing": { "fit_time_ms": 79.96299862861633, - "std_ms": 0.3447674872577324, + "std_ms": 0.344767487258, "min_ms": 79.8010528087616, "max_ms": 80.5998146533966, "sample_count": 3, @@ -90891,7 +90938,7 @@ "metrics": { "timing": { "fit_time_ms": 53.66373062133789, - "std_ms": 0.07744715297714648, + "std_ms": 0.077447152977, "min_ms": 53.63589525222778, "max_ms": 53.81232500076294, "sample_count": 3, @@ -91046,7 +91093,7 @@ "metrics": { "timing": { "fit_time_ms": 368.74711513519287, - "std_ms": 1.0174916822778004, + "std_ms": 1.017491682278, "min_ms": 367.16771125793457, "max_ms": 369.62711811065674, "sample_count": 3, @@ -91201,7 +91248,7 @@ "metrics": { "timing": { "fit_time_ms": 393.74133944511414, - "std_ms": 10.053154619538892, + "std_ms": 10.053154619539, "min_ms": 372.9921281337738, "max_ms": 394.8514759540558, "sample_count": 3, @@ -91356,7 +91403,7 @@ "metrics": { "timing": { "fit_time_ms": 229.87377643585205, - "std_ms": 0.8445346932663552, + "std_ms": 0.844534693266, "min_ms": 229.26145792007446, "max_ms": 231.27886652946472, "sample_count": 3, @@ -91413,6 +91460,3062 @@ } }, "comparison_id": "cv-benchmark-pr116-20260807" + }, + { + "run_id": "7279d751fec4ae72", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-0df334186e5af228", + "method_config_id": "method-c0fdde15c26f43a5", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "56290607cc2a0cf2", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dfd23de8fa23785c", + "method_config_id": "method-e4f7696d4033b5bd", + "variant": "balanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "695e9058534086b0", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-ef4ada9396e927aa", + "method_config_id": "method-c6458b366c6c5b4b", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "477b4803cfaac953", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-77e21b1b2625849e", + "method_config_id": "method-a891526d2820b20c", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6ca556cca3c1dc6c", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7505e931c271bead", + "method_config_id": "method-936c0cf8f8c6843c", + "variant": "entity-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "e21809c952ed9df4", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-f697cc38be66bb5b", + "method_config_id": "method-75395c8069822f63", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "ac6d5fc5e070c379", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-376492aadf7db27e", + "method_config_id": "method-af8a0d67f1b0b4e8", + "variant": "explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p3", + "n_samples": 54, + "n_features": 3, + "label": "54×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d4465ae990d85c88", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-23b6169b7248b5ff", + "method_config_id": "method-b815058aea2adbef", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "e3be78f30afda49c", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-6e32a6271b846410", + "method_config_id": "method-64417e823f34055a", + "variant": "two-way-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "4452fae4e283ef7b", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dbbafa2f82f52516", + "method_config_id": "method-0ef4a02559810006", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "056e0de8e3170092", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-3fac3845d997d0be", + "method_config_id": "method-0ce477d0912b1b99", + "variant": "unbalanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "a7069bd41b6ce3c2", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-9319e500c0d05810", + "method_config_id": "method-fd5b0f3282f7b323", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "c4014db4618f0475", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-7e6762b4b150ea72", + "method_config_id": "method-dc8c010b9bd69c0b", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "547f90cd8206fe0e", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7a6f0593ba47178d", + "method_config_id": "method-73caf204ad0ce7d3", + "variant": "entity-fe-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "3fc25da005609311", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-8c5eda16f11a344d", + "method_config_id": "method-2062c9b03141cff2", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "445759ea0b44a878", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-9a06cd1ab60b638f", + "method_config_id": "method-b230d1629d626033", + "variant": "explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p3", + "n_samples": 49, + "n_features": 3, + "label": "49×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6b0cbd5594989983", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-9ae95425684d313a", + "method_config_id": "method-2b49a6a725b5ea3c", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "83cf372c6fccda77", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-4a81e6630ee4a244", + "method_config_id": "method-4ed205ed0ad3db26", + "variant": "hausman-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "095e3a18069cd015", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-c1327c75008f9dfa", + "method_config_id": "method-6352b566ecc4def7", + "variant": "hausman-re-explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "a5ce2c8e73fdfcd3", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-528c7a3f7e02b3e3", + "method_config_id": "method-6580ac1c68723cc9", + "variant": "hausman-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "08bb5121ff32dd95", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7053bf05f9fc593f", + "method_config_id": "method-b49c53698837f046", + "variant": "hausman-re-explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "a72370d3b5ccdeef", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-0571875762c64d2c", + "method_config_id": "method-d58d685666eb74d4", + "variant": "hausman-applicable-nonzero-effect", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n48_p1", + "n_samples": 48, + "n_features": 1, + "label": "48×1" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": true, + "diagnostic_fixture": "nonzero-effect-applicable", + "statistic": 1.1965942530851057, + "pvalue": 0.27400344142676447, + "df": 1.0, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + }, + { + "metric": "hausman_applicable_statistic_pvalue_df", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6cd624414dece5bf", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-0df334186e5af228", + "method_config_id": "method-c0fdde15c26f43a5", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d515d6684ce4f746", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dfd23de8fa23785c", + "method_config_id": "method-e4f7696d4033b5bd", + "variant": "balanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6ccea7600064931d", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-ef4ada9396e927aa", + "method_config_id": "method-c6458b366c6c5b4b", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d6e9e3dbe35b8b12", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-77e21b1b2625849e", + "method_config_id": "method-a891526d2820b20c", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "1704f2b8ba0d05c5", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7505e931c271bead", + "method_config_id": "method-936c0cf8f8c6843c", + "variant": "entity-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "547b8f511ea88b95", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-f697cc38be66bb5b", + "method_config_id": "method-75395c8069822f63", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "c3b0d6f5d6fdfd93", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-376492aadf7db27e", + "method_config_id": "method-af8a0d67f1b0b4e8", + "variant": "explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p3", + "n_samples": 54, + "n_features": 3, + "label": "54×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "89b036b7f61372b0", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-23b6169b7248b5ff", + "method_config_id": "method-b815058aea2adbef", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "75dadabb2c7d4a31", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-6e32a6271b846410", + "method_config_id": "method-64417e823f34055a", + "variant": "two-way-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "99bb431cee9ad0b9", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dbbafa2f82f52516", + "method_config_id": "method-0ef4a02559810006", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "4b1040cb15d08732", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-3fac3845d997d0be", + "method_config_id": "method-0ce477d0912b1b99", + "variant": "unbalanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "3a94049592ef4a57", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-9319e500c0d05810", + "method_config_id": "method-fd5b0f3282f7b323", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "40b888d3411cdbec", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-7e6762b4b150ea72", + "method_config_id": "method-dc8c010b9bd69c0b", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d102490eea35e9d7", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7a6f0593ba47178d", + "method_config_id": "method-73caf204ad0ce7d3", + "variant": "entity-fe-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "9ca6ae85a86d0dce", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-8c5eda16f11a344d", + "method_config_id": "method-2062c9b03141cff2", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "fc273a5c73d0ecea", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-9a06cd1ab60b638f", + "method_config_id": "method-b230d1629d626033", + "variant": "explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p3", + "n_samples": 49, + "n_features": 3, + "label": "49×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "bd74d2455189bbd5", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-9ae95425684d313a", + "method_config_id": "method-2b49a6a725b5ea3c", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "cee85afecd82d8ee", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-4a81e6630ee4a244", + "method_config_id": "method-4ed205ed0ad3db26", + "variant": "hausman-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "4712fb5193e2fb7f", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-c1327c75008f9dfa", + "method_config_id": "method-6352b566ecc4def7", + "variant": "hausman-re-explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "b07fbe6ab06e9c8a", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-528c7a3f7e02b3e3", + "method_config_id": "method-6580ac1c68723cc9", + "variant": "hausman-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "81049fac88841b43", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7053bf05f9fc593f", + "method_config_id": "method-b49c53698837f046", + "variant": "hausman-re-explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "3fd46d0bebaa4152", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-0571875762c64d2c", + "method_config_id": "method-d58d685666eb74d4", + "variant": "hausman-applicable-nonzero-effect", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n48_p1", + "n_samples": 48, + "n_features": 1, + "label": "48×1" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": true, + "diagnostic_fixture": "nonzero-effect-applicable", + "statistic": 1.1965942530849238, + "pvalue": 0.2740034414268009, + "df": 1.0, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + }, + { + "metric": "hausman_applicable_statistic_pvalue_df", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" } ] } \ No newline at end of file diff --git a/docs/assets/benchmarks/data/parse_report.json b/docs/assets/benchmarks/data/parse_report.json index 5abc6f0d0..68a6a5657 100644 --- a/docs/assets/benchmarks/data/parse_report.json +++ b/docs/assets/benchmarks/data/parse_report.json @@ -1,10 +1,10 @@ { "report_version": "2.0", - "files_seen": 10, - "files_parsed": 10, + "files_seen": 11, + "files_parsed": 11, "files_skipped": 0, - "runs_generated": 1818, - "generation_id": "ceac2591d688920101474d09e145c7595e6d9460ce347828b4225139fcd60644", + "runs_generated": 1862, + "generation_id": "b425b95947fcb5ee7dcd4e6e10f1b108cfe82b804f5d46428e63c1fb5ca35b1d", "issues": [ { "source_id": "glm-solver-20260623-1b6197d94d88", diff --git a/docs/assets/benchmarks/data/source_inventory.json b/docs/assets/benchmarks/data/source_inventory.json index 3a6f459fd..efd33ae22 100644 --- a/docs/assets/benchmarks/data/source_inventory.json +++ b/docs/assets/benchmarks/data/source_inventory.json @@ -1,22 +1,22 @@ { "inventory_version": "2.0", "catalog_version": "2.0", - "catalog_digest": "1de71937d6d994c3ada387018a087e0d1c7b146099853c6ab700f0993f25a5fe", + "catalog_digest": "c9c6655a95d2347de0344a9431f75ec3b218c0b70a8c842719b2f5b8174bed54", "coverage_matrix_version": "1.0", - "coverage_matrix_digest": "b9264e98be2d774a9251ff122a36a0acb16ab64217a1ae9bcc1ac2e11599edf0", - "discovered_json_artifacts": 89, - "classified_candidate_sources": 89, - "eligible_sources": 10, - "registered_sources": 10, - "available_registered_sources": 10, - "parsed_registered_sources": 10, + "coverage_matrix_digest": "8ed8713f8ef977ad4b33eab83a33d65227e6e12a8a5179a0d7e6dea848a85a91", + "discovered_json_artifacts": 95, + "classified_candidate_sources": 95, + "eligible_sources": 11, + "registered_sources": 11, + "available_registered_sources": 11, + "parsed_registered_sources": 11, "eligible_unregistered_sources": 0, - "not_canonical_ready_sources": 42, - "historical_or_excluded_sources": 37, - "superseded_or_duplicate_sources": 9, + "not_canonical_ready_sources": 45, + "historical_or_excluded_sources": 39, + "superseded_or_duplicate_sources": 10, "unrelated_json_artifacts": 0, "unclassified_artifacts": 0, - "catalog_policy_digest": "23cd0e6e1ccc7a563a42fd6d999a7f6f0a3590726f93030c41c95672ebe19e0a", + "catalog_policy_digest": "c32b3e90a3b6f9694bfa6052c5e19629220f9266b59eef0a8d5dd62a3705e637", "catalog_entries": [ { "path": "results/bench_large_scale.json", @@ -828,6 +828,42 @@ "issue": null, "rule_id": "manifest-registration" }, + { + "path": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809.json", + "artifact_type": "json", + "source_date": "2026-08-09", + "classification": "superseded_or_duplicate", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "complete", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "The 42-row a57efcea Stage-B canonical validation source is retained immutably for audit but is superseded by the exact-clean-head 2701aa9f P100 source that also physically validates an applicable Hausman branch.", + "superseded_by": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "issue": "#93", + "rule_id": "superseded-pr122-panel-stage-b-canonical-a57efcea" + }, + { + "path": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "artifact_type": "json", + "source_date": "2026-08-09", + "classification": "registered_canonical", + "canonical_eligible": true, + "registered": true, + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "parser": "panel_stage_b_physical_validation", + "parser_version": "1.0", + "provenance_status": "complete", + "timing_protocol_status": "accepted", + "statistical_alignment_status": "accepted", + "reason": "Registered in frontend_sources.json and protected by manifest SHA256.", + "superseded_by": null, + "issue": null, + "rule_id": "manifest-registration" + }, { "path": "results/benchmark_frontend_sources/penalized_cox_trusted_gradient_pr80_20260727.json", "artifact_type": "json", @@ -1224,6 +1260,78 @@ "issue": "#112", "rule_id": "pr116-logistic-cv-focused-validation" }, + { + "path": "results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json", + "artifact_type": "json", + "source_date": null, + "classification": "historical_or_excluded", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "incomplete_date", + "timing_protocol_status": "unknown", + "statistical_alignment_status": "unknown", + "reason": "Artifact has no deterministically recoverable result date and is excluded pending explicit provenance review.", + "superseded_by": null, + "issue": "#100", + "rule_id": "undated-json" + }, + { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "artifact_type": "json", + "source_date": null, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "superseded_by": null, + "issue": "#93", + "rule_id": "pr122-panel-stage-b-physical-validation" + }, + { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json", + "artifact_type": "json", + "source_date": null, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "superseded_by": null, + "issue": "#93", + "rule_id": "pr122-panel-stage-b-physical-validation" + }, + { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_faa95ce7.json", + "artifact_type": "json", + "source_date": null, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "superseded_by": null, + "issue": "#93", + "rule_id": "pr122-panel-stage-b-physical-validation" + }, { "path": "results/pr74_inference_validation.json", "artifact_type": "json", @@ -1627,5 +1735,5 @@ "current_evidence_not_canonical_ready": 2, "partial_canonical": 7 }, - "generation_id": "ceac2591d688920101474d09e145c7595e6d9460ce347828b4225139fcd60644" + "generation_id": "b425b95947fcb5ee7dcd4e6e10f1b108cfe82b804f5d46428e63c1fb5ca35b1d" } \ No newline at end of file diff --git a/docs/cn/changelog.md b/docs/cn/changelog.md index 842af67e9..098c4abb0 100644 --- a/docs/cn/changelog.md +++ b/docs/cn/changelog.md @@ -1,12 +1,24 @@ # Changelog > 语言:中文
-> 最后更新:2026-08-08
+> 最后更新:2026-08-09
> 页面定位:变更记录
> 切换:[English](../en/changelog.md) ## 2026-08-08 +### PR #122 — Panel Tier-1 diagnostics Stage B + +- 新增公开的结构化 `PanelTestResult`、`PanelFitStatistics` 以及维护中 panel estimator 的标准化 `fit_statistics_`。新的 fit statistics 包含 parameter-based within/between/overall R²、显式定义的 adjusted R²,以及在存在 residual-OLS 拟合空间时的 classical homoskedastic model F。 +- Stage-A 的 coefficient inference 与 legacy R²/df 行为保持不变。特别是 `PanelOLS` 继续公开历史 residual df 和 BSE/t/p/CI;Stage-B diagnostics 使用单独的标准 fixed-effect nuisance-rank df,经典 Hausman 只读取按该标准 denominator 重标度的小型 diagnostic covariance,不修改公共 inference。 +- 新增 fixed-effects classical pooling F、one-way entity error-components Breusch-Pagan LM(包含 Baltagi-Li unbalanced-panel 公式)以及 classical one-way entity FE-vs-RE Hausman。计量上不适用的情况返回结构化 reason;Hausman covariance difference 若为奇异 PSD,则使用明确记录的 generalized-inverse/rank extension;若实质 indefinite,则直接报告不可用。 +- `PooledOLS.fit()` 与 `FamaMacBeth.fit()` 的可选 `entity_ids` 只用于 Stage-B within/between fit statistics 和 panel BP-LM。Pooled HAC 稳定排序现在让 entity diagnostic metadata 与 X/y 使用完全相同的 permutation;formula missing-row filtering 也会在形成 diagnostics 前对齐 observation-level side arrays。 +- 增加 analytic/fitted regression、维护中的 Python 3.9 + Torch 2.0 CPU parity,以及可执行的 `linearmodels==7.0` definition-alignment job。FirstDifference 的外部比较只在两边 transformed sample 定义一致的 panel 上执行;Stage B 不会为了 external gate 静默改变 Stage-A 对内部缺期采用 adjacent-observed-row differencing 的既有契约。 +- 新增 `dev/benchmarks/validate_panel_stage_b_gpu.py` 作为 exact-head physical correctness/provenance gate。此前在数值实现 `a57efcea29b0e87ecb89865c5a6902d5773812c6` 上接受的 P100 artifact 继续作为不可变的历史证据保留:CuPy 与 Torch 各自通过全部 17 个 estimator case,requested/executed backend 一致且无 fallback;focused disconnected two-way FE artifact 也把 df=1 inference boundary 验证到机器精度。该运行中每个 backend 的 4 个 Hausman parameterization 都是正确的结构化 `applicable=false` case,因此它们验证了 applicability/reason parity,但没有在物理 GPU 上执行 applicable Hausman 的 statistic/p-value/df 路径。 +- 重新打开的 physical gate 已在精确 clean measurement head `2701aa9feb3796c33c94e6480fcb78c80c6a809c` 上闭合:Tesla P100 的 CuPy 与 Torch 各自通过全部 17 个 estimator case 和 5 个 Hausman diagnostic,requested/executed backend 一致且没有 CPU fallback。新增的 48-observation nonzero-effect fixture 在两个 backend 上均为 `applicable=true`、df=1;Hausman statistic 相对 NumPy 的最大差异不超过 `1.10e-13`,p-value 不超过 `2.19e-14`。新的 44-row canonical validation source 保留该分支的 statistic/pvalue/df;旧的 42-row a57efcea source 继续作为历史审计证据保留。本次证据不包含 timing 或 speedup 声明。 + +关联:Issue #93 与 pull request #122。 + ### PR #121 — CuPy inverse-quantile LUT 正确性修复 - 修正 CuPy `betaincinv()` 与 `gammaincinv()` 的 LUT cache tuple 顺序。LUT builder 原本已经按 `(x_grid, y_grid)` 存储,但缓存读取时反向解包,导致 inverse lookup 在错误坐标轴上搜索,并可能把 quantile 推到 clipped boundary。 diff --git a/docs/cn/models/panel.md b/docs/cn/models/panel.md index de91a7f84..774ed2a45 100644 --- a/docs/cn/models/panel.md +++ b/docs/cn/models/panel.md @@ -1,7 +1,7 @@ # Panel 模型 > 语言:中文 -> 最后更新:2026-07-24 +> 最后更新:2026-08-08 > 页面定位:模型文档 > 切换:[English](../../en/models/panel.md) @@ -10,13 +10,15 @@ `statgpu.panel` 提供六类面板数据估计器: - `PanelOLS`:个体和/或时间固定效应; -- `RandomEffects`:可行 GLS 随机效应; +- `RandomEffects`:Swamy-Arora 可行 GLS 随机效应; - `PooledOLS`:不去均值的堆叠 OLS; - `BetweenOLS`:在个体均值上回归; - `FirstDifferenceOLS`:个体内一阶差分; - `FamaMacBeth`:逐期横截面回归后对系数取平均。 -数组输入的数值路径支持 NumPy、CuPy CUDA 与 Torch CUDA。formula 构造以及字符串/分类 entity、time、cluster 标签属于明确的 CPU 元数据边界,只会将对齐后的紧凑编码传入数值后端。显式 GPU device 不会静默回退 CPU。 +数组输入的数值路径支持 NumPy、CuPy CUDA 与 Torch CUDA。formula 构造以及字符串/分类 entity、time、cluster 标签属于明确的 CPU 元数据边界,只会把对齐后的紧凑编码传入数值后端。显式 GPU device 不会静默回退 CPU。 + +Tier-1 Panel 路线的 Stage B 在不改变 Stage-A 系数、预测、协方差归一化和 legacy inference 契约的前提下,新增参数型 fit statistics 和三类结构化 specification test。 ## 路径 @@ -28,22 +30,29 @@ from statgpu.panel import ( BetweenOLS, FirstDifferenceOLS, FamaMacBeth, + PanelTestResult, + PanelFitStatistics, + hausman_test, + pooling_f_test, + breusch_pagan_lm_test, clustered_covariance, two_way_clustered_covariance, hac_covariance, ) ``` +诊断结果类和三类检验函数也从顶层 `statgpu` 导出。 + ## 模型汇总 -| 模型 | 变换 | 主要推断选项 | -|---|---|---| -| `PanelOLS` | 个体/时间组内变换 | nonrobust、HC1 robust、clustered | -| `RandomEffects` | Swamy-Arora 可行 GLS | nonrobust | -| `PooledOLS` | 堆叠 OLS | nonrobust、robust、clustered、HAC | -| `BetweenOLS` | 个体均值 | nonrobust、robust、clustered | -| `FirstDifferenceOLS` | 个体内一阶差分 | nonrobust、robust | -| `FamaMacBeth` | 逐期横截面回归 | nonrobust、Newey-West | +| 模型 | 变换 | 主要推断选项 | Stage-B fit statistics | +|---|---|---|---| +| `PanelOLS` | 个体/时间组内变换 | nonrobust、HC1 robust、clustered | within/between/overall R²、adjusted R²、classical model F、pooling F | +| `RandomEffects` | Swamy-Arora 可行 GLS | nonrobust | within/between/overall R²、adjusted R²、classical model F、Hausman 输入 | +| `PooledOLS` | 堆叠 OLS | nonrobust、robust、clustered、HAC | overall R² 始终可用;提供 `entity_ids` 后增加 within/between R² 和 BP-LM;另有 adjusted R² 与 classical model F | +| `BetweenOLS` | 个体均值 | nonrobust、robust | within/between/overall R²、adjusted R²、classical model F | +| `FirstDifferenceOLS` | 个体内一阶差分 | nonrobust、robust | within/between/overall R²、差分拟合空间的 adjusted R²、classical model F | +| `FamaMacBeth` | 逐期横截面回归 | nonrobust、Newey-West | 参数型 within/between/overall R²;不定义 residual-OLS adjusted R² 或 model F | ## 核心估计方程 @@ -55,7 +64,7 @@ y_{it}^{\mathrm{within}} = y_{it} - \bar y_{i\cdot}, X_{it}^{\mathrm{within}} = X_{it} - \bar X_{i\cdot}. $$ -个体与时间双向固定效应会进一步减去时间均值并加回总体均值。 +个体和时间双向固定效应会进一步移除时间均值并加回总体均值。 `PooledOLS` 拟合 @@ -63,32 +72,183 @@ $$ \hat\beta = X^+ y, $$ -其中 \(X^+\) 在需要时表示 Moore-Penrose 伪逆。`BetweenOLS` 对个体均值执行 OLS,`FirstDifferenceOLS` 对 Δ\(X\) 和 Δ\(y\) 执行 OLS,`FamaMacBeth` 对逐期系数向量求平均。 +其中 \(X^+\) 在需要时表示 Moore-Penrose 伪逆。`BetweenOLS` 对个体均值执行 OLS,`FirstDifferenceOLS` 对 \(\Delta X\) 和 \(\Delta y\) 执行 OLS,`FamaMacBeth` 对逐期系数向量求平均。 -## 协方差与推断 +## 协方差与现有推断 | `cov_type` | 行为 | |---|---| | `"nonrobust"` | 经典 OLS 协方差和 t 推断 | | `"robust"` | HC1 sandwich 协方差和渐近正态推断 | -| `"clustered"` | 单向或双向聚类稳健协方差 | +| `"clustered"` | 在相应 estimator 支持范围内使用聚类稳健协方差 | | `"hac"` | `PooledOLS` 使用 Bartlett/Newey-West HAC | -| `"newey-west"` | 对 `FamaMacBeth` 系数路径应用 HAC | +| `"newey-west"` | 对 `FamaMacBeth` 系数时间序列应用 HAC | + +Stage B 不修改原有 `bse_`、`tvalues_`、`pvalues_`、`conf_int_` 或 estimator-specific covariance 定义。RandomEffects robust covariance、HC0/HC2/HC3、Driscoll-Kraay 和扩展 cluster correction 仍属于后续 Stage C。 ### PooledOLS HAC 时间排序 -对 `PooledOLS(cov_type="hac")`,应在 `fit` 中传入 `time_index=`。实现会验证该侧数组,并使用稳定时间排序,同时保持所有数值数组对齐。因此,只要时间标签不变,对原始行进行排列不会改变 HAC 协方差(除数值误差外)。 +对 `PooledOLS(cov_type="hac")`,应向 `fit` 传入 `time_index=`。实现采用稳定时间排序,并让 X、y 和 Stage-B entity diagnostic metadata 使用同一个 permutation。因此 BP-LM 与参数型 R² 不会因为 HAC 排序后仍使用旧 entity metadata 而发生错位。 ### 秩亏 PooledOLS 秩亏设计需要区分拟合空间与系数空间: -- 拟合、预测、残差、RSS、有效秩和拟合空间比较仍然有效; -- `df_resid` 按 `nobs - rank(X)` 计算,而不是 `nobs - n_columns`; -- 精确共线时,单个系数不唯一; -- 因此系数级协方差、BSE、检验统计量、p 值和置信区间不可唯一识别,应标记为 `NOT_COMPARABLE`,而不是运行错误,也不能作为唯一推断结果报告。 +- 拟合、预测、残差、RSS、有效 rank 与拟合空间比较仍然有效; +- `df_resid` 使用 `nobs - rank(X)`,而不是 `nobs - n_columns`; +- 精确共线时单个系数不唯一; +- 因而系数级 covariance、BSE、test statistic、p-value 和 confidence interval 不应被解释为唯一识别的系数推断。 + +Stage-B model-F 的 restriction rank 使用有效数值 rank,而不是直接使用原始列数。 + +## 标准化 `fit_statistics_` + +支持的拟合对象提供结构化 `PanelFitStatistics`: + +```python +stats = model.fit_statistics_ + +stats.rsquared_within +stats.rsquared_between +stats.rsquared_overall +stats.rsquared_adj +stats.f_statistic +stats.f_pvalue +stats.f_df +stats.metadata +``` + +### 参数型 R² + +within/between/overall R² 采用与 `linearmodels` 对齐的 parameter-based 定义:各统计量直接评价同一个拟合系数向量,而不是使用 fitted-value correlation 的平方。 + +给定 \(\hat\beta\): + +- **overall R²** 在 level panel 上评价 \(y-X\hat\beta\); +- **between R²** 在个体均值上评价 \(\bar y_i-\bar X_i\hat\beta\); +- **within R²** 在 entity-demeaned 的 y 和 X 上评价同一系数向量。 + +只有 level regressor design 中存在实际可识别的常数项时,overall 和 between total sum of squares 才中心化。固定效应本身不会自动改变这一规则。`RandomEffects` 会检测传入 level design 中显式的非零常数列,并在 adjusted R² 与 restricted model-F 中保留其 quasi-demeaned transformed column。常数检测的容差相对于该列自身量级定义,因此仅改变单位不会把一个非零常数误判成 slope,反之亦然。若 total sum of squares 为 0,Stage-B 标准字段返回 `0.0`,并在 `metadata["degenerate_total_ss"]` 中标记。 + +### Legacy `PanelOLS.rsquared_within` + +`PanelOLS.rsquared_within` 为兼容 Stage A 保持原样。双向 FE 下,它描述历史上的 entity+time 完整 transformed fit,因此可能与标准化的 entity-within `fit_statistics_.rsquared_within` 不同。Stage B 不覆盖旧属性,并在 fit-statistics metadata 中保留兼容值。 + +### Adjusted R² 与 diagnostic df + +新的标准化 diagnostics 使用完整 fixed-effect nuisance space 的 rank。当前 `PanelOLS` 不保留 exogenous intercept,因此标准 nuisance-effect rank 为: + +- 仅 entity effects:\(N\); +- 仅 time effects:\(T\); +- entity + time effects:\(N+T-C\),其中 \(C\) 是观测到的 entity-time incidence graph 的 connected-component 数。 + +因此常见的 \(N+T-1\) 只是连通面板 \(C=1\) 的特例;若 incomplete panel 的 incidence graph 不连通,diagnostic df 使用实际 dummy-space rank,而不会硬编码连通情形。 + +若 transformed slope design 的数值 rank 为 \(r_X\),Stage B 使用 + +$$ +\mathrm{df}_{\mathrm{resid,diag}} += n-r_X-r_{\mathrm{effects}}, +$$ + +以及 + +$$ +\mathrm{df}_{\mathrm{total,diag}} += n-r_{\mathrm{effects}} +$$ + +来计算标准 FE model F 与 adjusted R²。 + +这套 diagnostic df 与历史公开 `PanelOLS.df_resid` 明确分离;旧 df 继续服务已有 covariance、t statistic、p-value、confidence interval 与 summary。Hausman 使用一份仅供 diagnostic 的小型 FE covariance,并按标准 nuisance-rank denominator 重标度;Stage-A 公共 BSE/CI 不受影响。 + +### Classical model F + +对 OLS-style panel estimator,Stage B 报告主拟合空间上的 classical homoskedastic joint-slope F: + +$$ +F= +\frac{(RSS_R-RSS_U)/q} + {RSS_U/\mathrm{df}_{\mathrm{resid}}}, +$$ + +其中 \(q\) 是有效 restriction rank。即使 estimator 的 coefficient covariance 使用 robust 或 clustered 选项,这一字段也不会静默变成 robust Wald test。当 unrestricted regression 精确拟合而 restricted regression 的 RSS 为正时,标准化结果采用经典极限值 `F=inf`、`p=0`,而不是把统计量标成 unavailable。RSS 的 zero/nesting tolerance 按 RSS 自身量级缩放,因此将 response 与 fitted coefficient 同时乘一个单位变换常数不会改变无量纲 F statistic 或 applicability。 + +`FamaMacBeth` 不定义 residual-OLS model F 或 adjusted R²,因为其 covariance 来自逐期横截面系数时间序列;Stage B 不会把 beta-series inference 重命名成 residual OLS inference。 -PR79 验证管线在秩亏场景中继续检查 prediction、RSS、rank 与拟合空间合同,同时排除不可识别的系数空间比较。 +## Specification Tests + +所有 specification test 都返回 `PanelTestResult`。在计量意义上不适用的情况返回 `applicable=False` 并给出 `reason`;真正的 API 编程错误仍正常抛出异常。 + +### Pooling F test + +对至少包含一个固定效应的已拟合 `PanelOLS`: + +```python +result = fe.pooling_f_test() +# 或 +result = pooling_f_test(fe) +``` + +经典 poolability 原假设是所有纳入的固定效应联合为 0。统计量使用**同一个对齐后的估计样本**比较 fixed-effect model 与其 nested pooled regression。 + +当 FE level design 没有显式常数时,pooled null 会先从 y 和 X 中投影掉共同常数,再拟合 slope,避免把共同均值误计入被检验的 fixed effects。分子和分母自由度由有效 nested-model rank 推导,而不是硬编码 effect count。 + +如果 fixed-effects fit 精确拟合(`RSS_FE` 在数值上为 0),而 restricted pooled RSS 实质为正,则经典极限结果为 `F=inf`、`p=0`;若 pooled 与 FE RSS 都为 0,则比值不定,返回结构化 inapplicable。以上 zero/nesting 判定使用相对于 RSS 量级的 tolerance,不使用固定的 unit-size floor。 + +即使 FE 对象的系数推断采用 robust 或 clustered covariance,这个 pooling F 仍是 classical/homoskedastic test。 + +### 个体随机效应 Breusch-Pagan LM + +向 `PooledOLS.fit()` 提供 `entity_ids`: + +```python +pooled = PooledOLS().fit(X, y, entity_ids=entity_ids) +result = pooled.breusch_pagan_lm_test() +# 或 +result = breusch_pagan_lm_test(pooled) +``` + +这里是 **panel error-components Breusch-Pagan LM test**,不是横截面异方差的 Breusch-Pagan test。Stage B 实现 one-way entity 版本,并包含 `plm::plmtest(type="bp", effect="individual")` 使用的 Baltagi-Li incomplete/unbalanced-panel 公式。 + +原假设是 entity random-effect variance 为 0。至少需要两个 entity、正的 pooled RSS,并且至少一个 entity 有重复观测。没有 `entity_ids` 时返回结构化 inapplicable 结果,不会根据行顺序猜测 panel structure。 + +### Classical Hausman:FE 与 RE + +```python +fe = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity_ids +) +re = RandomEffects().fit(X, y, entity_ids=entity_ids) + +result = fe.hausman_test(re) +# 或 +result = hausman_test(fe, re) +``` + +Stage B 实现 one-way entity FE-versus-RE 的经典二次型 Hausman: + +$$ +H=(\hat\beta_{FE}-\hat\beta_{RE})^\top + (V_{FE}-V_{RE})^{-1} + (\hat\beta_{FE}-\hat\beta_{RE}). +$$ + +适用性规则是显式的: + +- FE 必须只有 one-way entity effects; +- FE coefficient covariance 必须是 classical/nonrobust; +- FE 与 RE 必须来自同一个对齐后的 y/entity 样本和同一个 canonical slope design; +- RE 可以比 FE 多一个显式常数,因为 entity FE 已吸收共同 intercept;该常数不会进入 Hausman coefficient vector; +- 行/样本一致性使用所有对齐 float64 **slope-X/y** 值的 collision-resistant SHA-256 digest,并结合 entity-code signature 与 canonical feature metadata,而不是只比较 shape 或低阶 moments; +- canonical slope position 会保留到各模型原始 coefficient/covariance position 的映射,因此 RE 的第 0 列 intercept 不会把 `x1`、`x2` 错配到错误系数; +- covariance difference 若实质上 indefinite,则返回 inapplicable,不通过 eigenvalue clipping 强行制造统计量。 + +数组输入下,在移除 RE-only constant 后 slope 会重新按 `x1`、`x2`、... 做 canonical 编号;named/formula design 则保留 slope 名称。Hausman covariance rank 与 identified-range tolerance 按 covariance/coefficient 自身量级缩放,所以改变 outcome 单位不会改变同一数学问题的 applicability。 + +GPU 拟合下,canonical slope full-content digest 仅为了 hashing 而通过有界 chunk 分批复制到 host。只有可进入 Stage-B Hausman 的 one-way entity nonrobust FE 与 `RandomEffects` 保留该 identity;robust/clustered、time-only、two-way FE 在 identity compare 前就会被判不适用,因此不承担完整 X/y hashing 开销。拟合对象只保存 digest/index metadata,不保留第二份完整 CPU design copy。统计估计、covariance 构造与 fit-statistic reduction 仍在所选数值 backend 上完成。 + +若 covariance difference 为 positive semidefinite 但 rank deficient,statgpu 提供显式记录的 generalized-inverse extension:只在 coefficient difference 位于 identified range 内时计算,并使用 numerical rank 作为 chi-square df。metadata 会记录 `used_pinv=True` 和 `singular PSD generalized-inverse Hausman`。Stage B 不实现 robust auxiliary-regression Hausman。 ## 参数与 fit 签名 @@ -104,9 +264,11 @@ PanelOLS( ``` ```python -model.fit(y, X, entity_ids=entity_ids, time_ids=time_ids, cluster=cluster) +model.fit(X, y, entity_ids=entity_ids, time_ids=time_ids, cluster=cluster) ``` +formula 输入也可使用已有 pipe syntax,例如 `"y ~ x1 + x2 | entity"`。formula 的 missing-row filtering 会同步对齐 observation-level side arrays。 + ### `PooledOLS` ```python @@ -120,10 +282,16 @@ PooledOLS( ``` ```python -model.fit(X, y, cluster=None, time_index=None) +model.fit( + X, + y, + cluster=None, + time_index=None, + entity_ids=None, +) ``` -clustered 推断需要 `cluster`。HAC 推断强烈建议传入 `time_index`,该参数用于定义稳定时间顺序。 +clustered inference 需要 `cluster`。`time_index` 定义 HAC 的稳定时间排序。`entity_ids` 是可选项,不改变系数;提供后会启用标准化 within/between R² 与 panel BP-LM。 ### 其他模型 @@ -140,11 +308,13 @@ FamaMacBeth( ) ``` +`FamaMacBeth.fit(..., entity_ids=None)` 中的可选 entity IDs 只用于 Stage-B within/between R²;beta-series estimation 与 covariance path 保持不变。 + ## CPU 与 GPU 示例 ```python import numpy as np -from statgpu.panel import PanelOLS, PooledOLS, FamaMacBeth +from statgpu.panel import PanelOLS, PooledOLS, RandomEffects n_entities, n_times = 50, 10 n = n_entities * n_times @@ -153,20 +323,20 @@ time_ids = np.tile(np.arange(n_times), n_entities) X = np.random.default_rng(0).normal(size=(n, 3)) y = X @ np.array([1.0, -0.5, 0.3]) + np.random.default_rng(1).normal(size=n) * 0.1 -# CPU 固定效应。 -fe = PanelOLS(entity_effects=True, cov_type="robust", device="cpu") -fe.fit(y, X, entity_ids=entity_ids) +fe = PanelOLS(entity_effects=True, device="cpu").fit( + X, y, entity_ids=entity_ids +) +print(fe.fit_statistics_.rsquared_within) +print(fe.pooling_f_test()) -# 使用显式时间顺序的 HAC PooledOLS。 -pooled_hac = PooledOLS(cov_type="hac", device="cpu") -pooled_hac.fit(X, y, time_index=time_ids) +pooled = PooledOLS(device="cpu").fit(X, y, entity_ids=entity_ids) +print(pooled.breusch_pagan_lm_test()) -# CuPy CUDA Fama-MacBeth;元数据标签可保留在 CPU。 -fm = FamaMacBeth(cov_type="newey-west", device="cuda") -fm.fit(X, y, time_ids=time_ids) +re = RandomEffects(device="cpu").fit(X, y, entity_ids=entity_ids) +print(fe.hausman_test(re)) ``` -Torch CUDA 路径应传入 CUDA tensor,并设置 `device="torch"`。数组输入的公开预测方法会保留 estimator 后端。 +CuPy CUDA 使用 `device="cuda"`;Torch CUDA 使用 CUDA tensor 并设置 `device="torch"`。Stage-B 的统计变换与 sufficient-statistic accumulation 跟随所选数值 backend;formula/label metadata、最终 scalar 与小型 covariance matrix 使用 CPU metadata boundary;Hausman-compatible one-way FE/RE 拟合还会仅为 collision-resistant identity hashing 对 canonical slope X/y 做有界分块 host copy。 ## 输出 @@ -174,28 +344,28 @@ Torch CUDA 路径应传入 CUDA tensor,并设置 `device="torch"`。数组输 - `coef_`; - 在系数空间可识别时的 `bse_`、`tvalues_`、`pvalues_`、`conf_int_`; -- 适用模型的 `rsquared` 或 `rsquared_within`; -- `nobs`、`df_resid` 与有效秩; +- 历史兼容的 `rsquared` 或 `rsquared_within`; +- 标准化的 `fit_statistics_`; +- `nobs`、`df_resid` 与有效 rank; - `FamaMacBeth` 的 `betas_`、`cov_params_` 与 `n_periods`。 -对精确秩亏的 `PooledOLS`,下游使用者不得将系数级推断解释为唯一识别结果。 +`PanelTestResult` 包含 `statistic`、`pvalue`、`distribution`、`df`、`null`、`alternative`、`applicable`、`reason` 和 `metadata`。 ## Formula 与元数据边界 -formula 计算可能因缺失值删除行。entity、time、cluster 等侧数组会按保留行同步对齐。字符串或分类标签在 CPU 上 factorize;数值变换和回归仍在所选后端执行。 +formula evaluation 可能因为 missing value 删除行。entity、time、cluster 等 side array 会与保留行同步对齐。字符串和分类标签在 CPU 上 factorize;数值变换与 sufficient-statistic calculation 继续留在所选 backend。对 Hausman-compatible 拟合,会先排除 RE-only 显式常数,再把 canonical slope X/y 以有界 chunk 复制到 host 计算 SHA-256 identity;随后只保存 digest、原始 coefficient-index 映射、feature/entity metadata 与小型 covariance matrix,不保存第二份完整 CPU design copy。 ## 验证 -PR #79 已验证 NumPy、CuPy CUDA 与 Torch CUDA 的维护中面板路径。最终维护中的真实 GPU 测试在 Tesla P100 上 **33/33** 通过,覆盖后端保持的 `PooledOLS.predict()` 与秩亏 `NOT_COMPARABLE` 合同。exact-head GitHub Actions 同时通过 Python 3.9–3.12 regression matrix 与完整 CPU suite。 - -相关内容: +Stage A / PR #119 建立共享 Panel framework,并在 Tesla P100 上通过 10 个 CuPy + 10 个 Torch exact-head physical cases。 -- `dev/reviews/pr79_physical_gpu_validation.md`; -- `dev/tests/test_pr79_physical_gpu.py`; -- Issue #83:清理未纳入维护测试树的旧 GPU 诊断脚本。 +Stage B 增加 maintained analytic/fitted-model regression tests、formula/missing-row alignment、Python 3.9 + Torch 2.0 CPU parity,以及可执行的 `linearmodels==7.0` external-definition gate。physical runner 每个 backend 包含 17 个 estimator cases 与 4 个 Hausman diagnostic cases,其中包括 balanced/unbalanced 显式常数 RandomEffects,以及 FE 吸收 intercept、RE 显式估计 intercept 的 Hausman 参数化。最终 promotion 要求 `dev/benchmarks/validate_panel_stage_b_gpu.py` 在 clean exact commit 上同时通过 CuPy 与 Torch CUDA;该 runner 是 correctness/provenance gate,而不是性能 benchmark。另有独立 physical benchmark 用于测量 Hausman-compatible FE/RE 上剩余 full-content identity 开销。 ## 参考文献 +- Hausman, J. A. (1978). Specification Tests in Econometrics. +- Breusch, T. S., & Pagan, A. R. (1980). The Lagrange Multiplier Test and its Applications to Model Specification in Econometrics. +- Baltagi, B. H., & Li, Q. (1990). A Lagrange Multiplier Test for the Error Components Model with Incomplete Panels. - White, H. (1980). A heteroskedasticity-consistent covariance matrix estimator. - Newey, W. K., & West, K. D. (1987). A simple, positive semi-definite, heteroskedasticity and autocorrelation consistent covariance matrix. - Fama, E. F., & MacBeth, J. D. (1973). Risk, return, and equilibrium. diff --git a/docs/en/changelog.md b/docs/en/changelog.md index d9126e537..bba565eee 100644 --- a/docs/en/changelog.md +++ b/docs/en/changelog.md @@ -1,12 +1,24 @@ # Changelog > Language: English
-> Last updated: 2026-08-08
+> Last updated: 2026-08-09
> This page: Changelog
> Switch: [Chinese](../cn/changelog.md) ## 2026-08-08 +### PR #122 — Panel Tier-1 diagnostics Stage B + +- Added public structured `PanelTestResult` and `PanelFitStatistics` outputs plus standardized `fit_statistics_` on the maintained panel estimators. The new fit statistics use parameter-based within/between/overall R², an explicitly defined adjusted R², and a classical homoskedastic model F statistic where the estimator has a residual-OLS fit space. +- Kept Stage-A coefficient inference and legacy R²/df behavior unchanged. In particular, `PanelOLS` continues to expose its historical public residual df and BSE/t/p/CI, while Stage-B diagnostics use a separate standard fixed-effect nuisance-rank df; the classical Hausman calculation consumes only a diagnostic small covariance rescaled to that standard denominator. +- Added the classical pooling F test for fixed effects, the one-way entity error-components Breusch-Pagan LM test including the Baltagi-Li unbalanced-panel formula, and the classical one-way entity FE-vs-RE Hausman test. Inapplicable econometric cases return structured reasons; singular positive-semidefinite Hausman covariance differences use a documented generalized-inverse/rank extension, while materially indefinite differences are rejected. +- Added optional `entity_ids` to `PooledOLS.fit()` and `FamaMacBeth.fit()` solely for Stage-B within/between fit statistics and the panel BP-LM path. Pooled HAC sorting now carries entity diagnostic metadata through the same stable permutation as X/y. Formula missing-row filtering aligns observation-level side arrays before diagnostics are formed. +- Added analytic/fitted regressions, maintained Python 3.9 + Torch 2.0 CPU parity, and an executable `linearmodels==7.0` definition-alignment job. FirstDifference external comparison is restricted to panels where both implementations use the same transformed sample; Stage B does not silently redefine the Stage-A adjacent-observed-row differencing contract for internal time gaps. +- Added `dev/benchmarks/validate_panel_stage_b_gpu.py` as the exact-head physical correctness/provenance gate. The previously accepted P100 artifacts at numerical implementation `a57efcea29b0e87ecb89865c5a6902d5773812c6` remain immutable historical evidence: CuPy and Torch each passed all 17 estimator cases with requested-backend provenance and no fallback, while the focused disconnected two-way FE artifact validated the df=1 inference boundary to machine precision. The four Hausman parameterizations per backend in that run were all correctly structured `applicable=false` cases, so they validate applicability/reason parity but do not physically exercise an applicable Hausman statistic/p-value/df path. +- The reopened physical gate is now closed on exact clean measurement head `2701aa9feb3796c33c94e6480fcb78c80c6a809c`: Tesla P100 CuPy and Torch each passed all 17 estimator cases and all five Hausman diagnostics with requested/executed backend identity and no CPU fallback. The dedicated 48-observation nonzero-effect fixture is `applicable=true` on both backends with df=1; its Hausman statistic differs from NumPy by at most `1.10e-13` and p-value by at most `2.19e-14`. The promoted 44-row canonical validation source preserves statistic/pvalue/df for that branch, while the older 42-row a57efcea source remains historical audit evidence. No timing or speedup claim is made. + +Related: Issue #93 and pull request #122. + ### PR #121 — CuPy inverse-quantile LUT correctness - Corrected the CuPy LUT cache tuple order used by `betaincinv()` and `gammaincinv()`. The LUT builders already stored `(x_grid, y_grid)`, but the cached values were unpacked in reverse, so inverse lookup searched the wrong axis and could collapse quantiles to clipped boundary values. @@ -136,4 +148,4 @@ Related: Issue #45, Issue #81, Issue #82, Issue #83, and pull request #87. ## Earlier history Detailed entries through 2026-08-03 are retained in -[the archived changelog](changelog-history-through-2026-08-03.markdown). \ No newline at end of file +[the archived changelog](changelog-history-through-2026-08-03.markdown). diff --git a/docs/en/guides/statgpu_benchmark_dashboard.md b/docs/en/guides/statgpu_benchmark_dashboard.md index bff682fa7..1ca48ff9e 100644 --- a/docs/en/guides/statgpu_benchmark_dashboard.md +++ b/docs/en/guides/statgpu_benchmark_dashboard.md @@ -6,7 +6,7 @@ The browser is a presentation layer over a generated benchmark bundle. Raw resul ## Current coverage -The canonical manifest is the source of truth for current benchmark inputs, and the deployed inventory is the source of truth for live counts. **Snapshot (2026-08-07):** 9 registered/available/parsed sources produce 1,796 normalized runs, including the first current six-family CV package: +The canonical manifest is the source of truth for current benchmark inputs, and the deployed inventory is the source of truth for live counts. **Snapshot (2026-08-08):** 11 registered/available/parsed sources produce 1,852 normalized runs across 46 models. The current source set includes the post-repair PR #116 CV source and the PR #122 Panel Stage-B physical validation source: | Source | Frontend coverage | |---|---| @@ -19,12 +19,14 @@ The canonical manifest is the source of truth for current benchmark inputs, and | `unsupervised_20260627.json` | Complete source matrix for PCA, clustering, decomposition, mini-batch methods, UMAP, and t-SNE | | `ordered_inference_pr74.json` | Ordered, Quantile, sandwich, oracle, and bootstrap inference configurations | | `cv_benchmark_20260807.json` | RidgeCV, LassoCV, ElasticNetCV, LogisticRegressionCV, PenalizedGLM_CV, and CoxPHCV with explicit backend dispositions | +| `results/pr116_p100/cv_benchmark_pr116_p100.json` | Exact-head P100 CV source after the PR #116 Torch LogisticRegressionCV repair | +| `panel_stage_b_pr122_p100_20260808.json` | Validation-only P100 evidence for Panel Stage-B diagnostics, backend provenance, and Stage-A inference regression | -These sources populate penalized GLM and GLM, recent linear models, robust/quantile regression, survival analysis, unsupervised learning, ordered models, nonparametric methods, panel models, covariance estimation, and ANOVA. +These sources populate penalized GLM and GLM, recent linear models, robust/quantile regression, survival analysis, unsupervised learning, ordered models, nonparametric methods, panel models, covariance estimation, ANOVA, and current CV families. GAM coverage includes `1K×3`, `10K×5`, and `100K×10` for two distinct variants: the ordinary pyGAM comparison and the uniform-knot precision-aligned comparison. Each variant contains statgpu NumPy/CuPy/Torch rows and a pyGAM reference, together with runner-reported speedup and prediction-difference validation. The solver is represented as fixed `lambda=1.0`, matching the source runner rather than incorrectly labelling the work as GCV. -Aligned Panel coverage includes `10K×10` and `100K×20` for both PanelOLS and RandomEffects. Each model/scale contains statgpu NumPy/CuPy/Torch rows and a linearmodels reference, together with runner-reported speedup and coefficient-relative-error metrics. +Panel evidence is intentionally split by measurement purpose. The June 24 timing source includes aligned `10K×10` and `100K×20` PanelOLS and RandomEffects comparisons; each model/scale contains statgpu NumPy/CuPy/Torch rows and a linearmodels reference, together with runner-reported speedup and coefficient-relative-error metrics. PR #122 adds a separate canonical source at `results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260808.json` (SHA256 `882892c6e3077fe3b9f6084212647311da795fd05d1ed9f12ec53da1e05d0d4d`). It records the exact clean implementation head `636988751bcbfad3442d24d3073cdfcd2b3ac637` on Tesla P100 and contributes 34 validation-only CuPy/Torch rows covering PooledOLS, BetweenOLS, FirstDifferenceOLS, PanelOLS, RandomEffects, FamaMacBeth, Stage-B fit/specification diagnostics, backend provenance, and Stage-A coefficient-inference regression. The physical validator did **not** collect timing, so these runs intentionally omit `metrics.timing` and `metrics.speedup`; the frontend must not infer a performance claim from them. Unsupervised coverage retains all 131 source rows. PCA, KMeans, GaussianMixture, NMF, TruncatedSVD, IncrementalPCA, MiniBatchKMeans, and MiniBatchNMF expose every small/medium/large configuration. DBSCAN exposes both 10-dimensional and 50-dimensional variants at all three scales; AgglomerativeClustering, UMAP, and t-SNE expose every scale actually run. Large labels follow the arrays passed to fit, so estimators capped at 50 input features are correctly shown as `100K×50` instead of the uncapped `100K×100` runner template. @@ -32,7 +34,7 @@ The PR #74 source now exposes all of its methods. In addition to Ordered Logit/P ANOVA coverage includes one-way ANOVA, two-way ANOVA, Welch ANOVA, Tukey HSD, and Bonferroni correction at three scales on NumPy, CuPy, and Torch. One-way ANOVA also contains aligned SciPy timing and F-statistic validation rows. -The current bundle should not be interpreted as complete coverage of every implementation in the repository. In particular, the robust source contains CPU Huber and Quantile fit comparisons but no Bisquare, Fair, or robust-loss GPU fit matrix. Ordered scales remain too small to locate a GPU crossover; covariance currently contains only EmpiricalCovariance; Feature Selection has no eligible structured source; and ANOVA has too few synchronization-safe scale points for a precise crossover interval. +The current bundle should not be interpreted as complete coverage of every implementation in the repository. In particular, the robust source contains CPU Huber and Quantile fit comparisons but no Bisquare, Fair, or robust-loss GPU fit matrix. Ordered scales remain too small to locate a GPU crossover; covariance currently contains only EmpiricalCovariance; Feature Selection has no eligible structured source; and ANOVA has too few synchronization-safe scale points for a precise crossover interval. Panel Stage C covariance completion and broader Panel timing remain tracked separately from the PR #122 correctness source. A June distribution benchmark also exists and reports 139/139 SciPy precision checks plus NumPy/CuPy/Torch timings for 15 distributions. It is not yet registered because only a rounded Markdown report is committed, without raw repeats, per-check errors, or a structured category/source contract. It is recorded as a P1 structured-conversion or rerun task rather than being ignored or represented with invented metadata. @@ -84,6 +86,8 @@ The timing chart uses `metrics.timing.fit_time_ms`. Group identity includes comp Focused labels omit repeated scale and Auto/best text. Full-matrix labels use two lines. Both modes use bounded label width and full tooltip text, avoiding a dense diagonal label wall. +Validation-only runs with no `metrics.timing`, including the PR #122 physical Panel source, do not create timing bars. + ### Speedup A value above one means faster than the reference; a value below one is a slowdown. A dashed gray line marks 1× parity, with a compact `1×` badge above the bar area. Horizontal tick labels include the `×` unit. @@ -93,7 +97,7 @@ A value above one means faster than the reference; a value below one is a slowdo Semantic validation checks computed references, positive timings, compatible identities, and numerical agreement with the timing ratio. -The global summary card shows only the fastest runner-reported GPU speedup. Computed timing ratios remain available in the chart and raw data for auditing; the two reference semantics are not mixed into one headline. +The global summary card shows only the fastest runner-reported GPU speedup. Computed timing ratios remain available in the chart and raw data for auditing; the two reference semantics are not mixed into one headline. Validation-only sources without timing are excluded from speedup aggregation. ## Visual theme @@ -115,7 +119,7 @@ Panels appear only when filtered rows contain the corresponding metric group: - **Convergence**: iteration summaries and convergence rates. - **Selection**: precision, recall, FDP, F1, Jaccard, FDR, and selected-set size when a current source exists. -The Inference panel covers Ordered Logit/Probit, Quantile kernel/bootstrap inference, and the restored sandwich/oracle/bootstrap configurations. ANOVA one-way rows expose SciPy-relative F-statistic validation in the Validation panel. +The Inference panel covers Ordered Logit/Probit, Quantile kernel/bootstrap inference, the restored sandwich/oracle/bootstrap configurations, and the PR #122 estimator rows' reported physical inference-pass status. ANOVA one-way rows expose SciPy-relative F-statistic validation in the Validation panel. PR #122 Hausman applicability rows remain validation-only because the generated validation datasets yielded `applicable=false`; the parser does not invent a test statistic. ## Metric provenance @@ -142,8 +146,11 @@ All three files share one `generation_id`. In canonical mode, inventory fields r python -m pip install -U pytest jsonschema pytest \ dev/tests/test_benchmark_frontend_data.py \ + dev/tests/test_benchmark_catalog.py \ + dev/tests/test_benchmark_inventory_v2.py \ dev/tests/test_frontend_contracts.py \ - dev/tests/test_frontend_domain_coverage.py -v + dev/tests/test_frontend_domain_coverage.py \ + dev/tests/test_panel_stage_b_frontend_source.py -v python dev/benchmarks/generate_benchmark_data.py \ --out frontend/public/data/benchmark_data.json \ @@ -166,7 +173,7 @@ npm run test:e2e:production 2. Register SHA256, environment, comparison, parser, allowed issue codes, and `source_date` in `frontend_sources.json`. 3. Ensure `source_date` is on or after the manifest's `minimum_source_date`. 4. Implement or reuse a parser and register it in `registry.py`. -5. Return schema-compliant runs with canonical case/method identities. +5. Return schema-compliant runs with canonical case/method identities. Validation-only sources are allowed, but missing timing/speedup must remain absent rather than inferred. 6. Add parser, date-policy, domain-coverage, and interaction tests. 7. Regenerate the bundle and rebuild deployed assets. diff --git a/docs/en/models/panel.md b/docs/en/models/panel.md index 9b6b3ca80..267e5ae52 100644 --- a/docs/en/models/panel.md +++ b/docs/en/models/panel.md @@ -1,7 +1,7 @@ # Panel Models > Language: English -> Last updated: 2026-07-24 +> Last updated: 2026-08-08 > This page: Model documentation > Switch: [Chinese](../../cn/models/panel.md) @@ -10,7 +10,7 @@ The `statgpu.panel` module provides six panel-data estimators: - `PanelOLS`: entity and/or time fixed effects; -- `RandomEffects`: feasible GLS random effects; +- `RandomEffects`: Swamy-Arora feasible GLS random effects; - `PooledOLS`: stacked OLS without demeaning; - `BetweenOLS`: regression on entity means; - `FirstDifferenceOLS`: within-entity first differences; @@ -18,6 +18,8 @@ The `statgpu.panel` module provides six panel-data estimators: Array-input numerical paths support NumPy, CuPy CUDA, and Torch CUDA. Formula construction and categorical entity/time/cluster labels are intentional CPU metadata boundaries; compact aligned codes are transferred to the selected numerical backend. Explicit GPU devices do not silently fall back to CPU. +Stage B of the Tier-1 panel roadmap adds parameter-based fit statistics and three structured specification tests without changing the Stage-A coefficient, prediction, covariance-normalization, or legacy inference contracts. + ## Paths ```python @@ -28,22 +30,29 @@ from statgpu.panel import ( BetweenOLS, FirstDifferenceOLS, FamaMacBeth, + PanelTestResult, + PanelFitStatistics, + hausman_test, + pooling_f_test, + breusch_pagan_lm_test, clustered_covariance, two_way_clustered_covariance, hac_covariance, ) ``` +The diagnostic result classes and functions are also exported from top-level `statgpu`. + ## Model Summary -| Model | Transformation | Main inference choices | -|---|---|---| -| `PanelOLS` | Entity/time within transformation | nonrobust, HC1 robust, clustered | -| `RandomEffects` | Swamy-Arora feasible GLS | nonrobust | -| `PooledOLS` | Stacked OLS | nonrobust, robust, clustered, HAC | -| `BetweenOLS` | Entity means | nonrobust, robust, clustered | -| `FirstDifferenceOLS` | Within-entity first differences | nonrobust, robust | -| `FamaMacBeth` | Cross-sectional regressions by period | nonrobust, Newey-West | +| Model | Transformation | Main inference choices | Stage-B fit statistics | +|---|---|---|---| +| `PanelOLS` | Entity/time within transformation | nonrobust, HC1 robust, clustered | within/between/overall R², adjusted R², classical model F, pooling F | +| `RandomEffects` | Swamy-Arora feasible GLS | nonrobust | within/between/overall R², adjusted R², classical model F, Hausman input | +| `PooledOLS` | Stacked OLS | nonrobust, robust, clustered, HAC | overall R² always; within/between R² and BP-LM when `entity_ids` is supplied; adjusted R² and classical model F | +| `BetweenOLS` | Entity means | nonrobust, robust | within/between/overall R², adjusted R², classical model F | +| `FirstDifferenceOLS` | Within-entity first differences | nonrobust, robust | within/between/overall R², adjusted R² on the differenced fit space, classical model F | +| `FamaMacBeth` | Cross-sectional regressions by period | nonrobust, Newey-West | parameter-based within/between/overall R²; no residual-OLS adjusted R² or model F | ## Core Estimating Equations @@ -55,7 +64,7 @@ y_{it}^{\mathrm{within}} = y_{it} - \bar y_{i\cdot}, X_{it}^{\mathrm{within}} = X_{it} - \bar X_{i\cdot}. $$ -With entity and time effects, the two-way transformation adds back the grand mean. +With entity and time effects, the two-way transformation also removes time means and adds back the grand mean. `PooledOLS` fits @@ -65,19 +74,21 @@ $$ where \(X^+\) denotes the inverse or Moore-Penrose pseudoinverse as required. `BetweenOLS` applies OLS to entity means, `FirstDifferenceOLS` applies OLS to Δ\(X\) and Δ\(y\), and `FamaMacBeth` averages period-specific coefficient vectors. -## Covariance and Inference +## Covariance and Existing Inference | `cov_type` | Behavior | |---|---| | `"nonrobust"` | Classical OLS covariance and t-based inference | | `"robust"` | HC1 sandwich covariance and asymptotic normal inference | -| `"clustered"` | One-way or two-way cluster-robust covariance | +| `"clustered"` | Cluster-robust covariance where supported by the estimator | | `"hac"` | Bartlett/Newey-West HAC for `PooledOLS` | | `"newey-west"` | HAC applied to the `FamaMacBeth` coefficient path | +Stage B does not change existing `bse_`, `tvalues_`, `pvalues_`, `conf_int_`, or estimator-specific covariance definitions. RandomEffects robust covariance, HC0/HC2/HC3, Driscoll-Kraay, and expanded cluster corrections remain later Stage-C work. + ### PooledOLS HAC ordering -For `PooledOLS(cov_type="hac")`, pass `time_index=` to `fit`. The implementation validates the side array and uses a stable time ordering while keeping all numerical arrays aligned. Consequently, a row permutation with unchanged time labels produces the same HAC covariance, up to numerical tolerance. +For `PooledOLS(cov_type="hac")`, pass `time_index=` to `fit`. The implementation validates the side array and uses a stable time ordering while keeping X, y, and Stage-B entity diagnostic metadata under the identical permutation. Consequently, BP-LM and parameter-based R² cannot accidentally group residuals using pre-sort entity metadata. ### Rank-deficient PooledOLS @@ -86,9 +97,158 @@ A rank-deficient design separates fitted-space validity from coefficient-space i - fitting, prediction, residuals, RSS, rank, and fitted-space comparisons remain valid; - `df_resid` is computed as `nobs - rank(X)`, not `nobs - n_columns`; - individual coefficients are not unique under exact collinearity; -- coefficient-level covariance, BSE, test statistics, p-values, and confidence intervals are therefore non-identifiable and should be reported as `NOT_COMPARABLE`, not as a runtime error or a successful unique inference result. +- coefficient-level covariance, BSE, test statistics, p-values, and confidence intervals are therefore non-identifiable and should not be interpreted as unique coefficient inference. + +Stage-B model-F restrictions use effective numerical rank rather than blindly using the raw column count. + +## Standardized `fit_statistics_` + +Supported fits expose a structured `PanelFitStatistics` object: + +```python +stats = model.fit_statistics_ + +stats.rsquared_within +stats.rsquared_between +stats.rsquared_overall +stats.rsquared_adj +stats.f_statistic +stats.f_pvalue +stats.f_df +stats.metadata +``` + +### Parameter-based R² + +The within/between/overall R² family follows the parameter-based convention used by `linearmodels`: each statistic evaluates the fitted coefficient vector rather than squaring a fitted-value correlation. + +For coefficient vector \(\hat\beta\): + +- **overall R²** evaluates \(y-X\hat\beta\) on the level panel; +- **between R²** evaluates entity means \(\bar y_i-\bar X_i\hat\beta\); +- **within R²** evaluates entity-demeaned \(y\) and \(X\). + +Overall and between total sums of squares are centered only when the actual level regressor design contains an identified constant. Fixed effects by themselves do not change this centering rule. `RandomEffects` detects an explicit nonzero constant column in the supplied level design and retains its quasi-demeaned transformed column when defining adjusted R² and the restricted model-F regression. Constant detection uses a tolerance relative to the column's own magnitude, so changing units does not turn a nonzero constant into a slope or vice versa. A zero total sum of squares is reported as `0.0` in the standardized Stage-B field and marked in `metadata["degenerate_total_ss"]`. + +### Legacy `PanelOLS.rsquared_within` + +`PanelOLS.rsquared_within` is retained exactly for Stage-A compatibility. In a two-way FE model it describes the historical full entity+time transformed fit and can differ from the standardized entity-within `fit_statistics_.rsquared_within`. Stage B does not silently overwrite the legacy attribute; the compatibility value is recorded in fit-statistics metadata. + +### Adjusted R² and diagnostic degrees of freedom + +For new standardized diagnostics, `PanelOLS` uses the rank of the complete fixed-effect nuisance space. Current `PanelOLS` does not retain an exogenous intercept, so the standard nuisance-effect rank is: + +- entity effects only: \(N\); +- time effects only: \(T\); +- entity and time effects: \(N+T-C\), where \(C\) is the number of connected components in the observed entity-time incidence graph. + +Thus the familiar \(N+T-1\) formula is the connected-panel special case \(C=1\); incomplete panels with disconnected incidence components receive their actual dummy-space rank rather than a hard-coded connected-panel rank. + +If \(r_X\) is the numerical rank of the transformed slope design, Stage B uses + +$$ +\mathrm{df}_{\mathrm{resid,diag}} += n-r_X-r_{\mathrm{effects}}, +$$ + +and + +$$ +\mathrm{df}_{\mathrm{total,diag}} += n-r_{\mathrm{effects}} +$$ + +for the standardized FE model F and adjusted R². + +This is intentionally separate from the historical public `PanelOLS.df_resid`, which remains unchanged because existing covariance, t statistics, p-values, confidence intervals, and summaries depend on that compatibility convention. Hausman uses a diagnostic-only small FE covariance matrix rescaled to the standard nuisance-rank denominator; the public Stage-A BSE/CI remain unchanged. + +### Classical model F + +For OLS-style panel estimators, Stage B reports the classical homoskedastic joint-slope F statistic on the estimator's primary fit space: + +$$ +F= +\frac{(RSS_R-RSS_U)/q} + {RSS_U/\mathrm{df}_{\mathrm{resid}}}, +$$ + +where \(q\) is the effective restriction rank. A robust or clustered covariance choice does not silently convert this field into a robust Wald test. When the unrestricted regression fits exactly while the restricted regression has positive RSS, the standardized result is the limiting classical value `F=inf`, `p=0` rather than an unavailable statistic. RSS zero/nesting tolerances are relative to the RSS scale, so multiplying the response and fitted coefficients by a common unit-conversion factor does not change the dimensionless F statistic or its applicability. + +`FamaMacBeth` does not receive a residual-OLS model F or adjusted R². Its covariance is based on the time series of cross-sectional coefficient estimates, and Stage B does not relabel that beta-series inference as residual OLS. -The PR79 validation pipeline preserves prediction/RSS/rank contracts for rank-deficient cases while excluding non-identifiable coefficient-space comparisons. +## Specification Tests + +All specification tests return `PanelTestResult`. Econometrically inapplicable cases are structured results with `applicable=False` and a reason; malformed API calls still raise normal programming errors. + +### Pooling F test + +For a fitted `PanelOLS` with at least one effect: + +```python +result = fe.pooling_f_test() +# or +result = pooling_f_test(fe) +``` + +The classical poolability null is that all included fixed effects are jointly zero. The statistic compares the effect model against a nested pooled regression on the exact aligned estimation sample. + +When the FE level design has no explicit constant, the pooled null projects both y and X off the common constant before fitting the slopes. This prevents the common mean from being counted as a tested fixed effect. The numerator and denominator degrees of freedom are derived from effective nested-model ranks, not hard-coded effect counts. + +If the fixed-effects fit is exact (`RSS_FE` numerically zero) while the restricted pooled RSS is materially positive, the classical limiting result is `F=inf`, `p=0`. If both pooled and FE RSS are numerically zero, the ratio is indeterminate and the structured result is inapplicable. These zero/nesting decisions use RSS-relative tolerances rather than a unit-sized absolute floor. + +The test is classical/homoskedastic even when the fitted FE object uses a robust or clustered covariance for coefficient inference. + +### Breusch-Pagan LM for entity random effects + +Supply `entity_ids` to `PooledOLS.fit()`: + +```python +pooled = PooledOLS().fit(X, y, entity_ids=entity_ids) +result = pooled.breusch_pagan_lm_test() +# or +result = breusch_pagan_lm_test(pooled) +``` + +This is the **panel error-components Breusch-Pagan LM test**, not the cross-sectional heteroskedasticity Breusch-Pagan test. Stage B implements the one-way entity version, including the Baltagi-Li incomplete/unbalanced-panel formula used by `plm::plmtest(type="bp", effect="individual")`. + +The null is zero entity random-effect variance. At least two entities, positive pooled RSS, and at least one repeated observation within an entity are required. Without `entity_ids`, the result is structured as inapplicable rather than guessed from row order. + +### Classical Hausman FE versus RE + +```python +fe = PanelOLS(entity_effects=True, cov_type="nonrobust").fit( + X, y, entity_ids=entity_ids +) +re = RandomEffects().fit(X, y, entity_ids=entity_ids) + +result = fe.hausman_test(re) +# or +result = hausman_test(fe, re) +``` + +Stage B implements the original quadratic-form one-way entity FE-versus-RE Hausman test: + +$$ +H=(\hat\beta_{FE}-\hat\beta_{RE})^\top + (V_{FE}-V_{RE})^{-1} + (\hat\beta_{FE}-\hat\beta_{RE}). +$$ + +Applicability rules are explicit: + +- FE must be one-way entity effects only; +- the FE coefficient covariance must be classical/nonrobust; +- FE and RE must be fitted to the same aligned y/entity sample and the same canonical slope design; +- an RE-only explicit constant is allowed because entity FE absorbs the common intercept; that constant is excluded from the Hausman coefficient vector; +- row/sample compatibility uses a collision-resistant SHA-256 digest of every aligned float64 **slope-X/y** value plus the entity-code signature and canonical feature metadata, not only matching shapes or low-order moments; +- canonical slope positions retain a map to each model's original coefficient/covariance positions, so an RE intercept at position 0 does not shift `x1`, `x2`, ... into the wrong coefficients; +- a materially indefinite covariance difference is reported as inapplicable instead of being eigenvalue-clipped into a statistic. + +For array input, slopes are canonically renumbered after an RE-only constant is removed. For named/formula designs, slope names are preserved. Hausman covariance-rank and identified-range tolerances are relative to the covariance/coefficient scale, so a change of outcome units does not change applicability for the same mathematical problem. + +For GPU fits, the canonical slope full-content digest is computed through bounded chunks copied to host solely for hashing. One-way entity FE with nonrobust covariance and `RandomEffects` retain this identity because they can participate in Stage-B Hausman. Robust/clustered, time-only, and two-way FE are rejected before identity comparison and do not pay the full X/y hashing cost. Fitted models retain only the digest/index metadata, not a second CPU copy of the design. Statistical estimation, covariance construction, and fit-statistic reductions remain on the selected numerical backend. + +If the covariance difference is positive semidefinite but rank-deficient, statgpu provides a documented generalized-inverse extension: the test uses the identified range and chi-square degrees of freedom equal to the numerical rank, but only when the coefficient difference lies in that range. Metadata records `used_pinv=True` and labels this as the `singular PSD generalized-inverse Hausman` extension. Robust auxiliary-regression Hausman is not part of Stage B. ## Parameters and Fit Signatures @@ -104,9 +264,11 @@ PanelOLS( ``` ```python -model.fit(y, X, entity_ids=entity_ids, time_ids=time_ids, cluster=cluster) +model.fit(X, y, entity_ids=entity_ids, time_ids=time_ids, cluster=cluster) ``` +Formula input can also request effects through the existing pipe syntax, for example `"y ~ x1 + x2 | entity"`. Formula row filtering aligns side arrays to the retained estimation sample. + ### `PooledOLS` ```python @@ -120,10 +282,16 @@ PooledOLS( ``` ```python -model.fit(X, y, cluster=None, time_index=None) +model.fit( + X, + y, + cluster=None, + time_index=None, + entity_ids=None, +) ``` -`cluster` is required for clustered inference. `time_index` is strongly recommended for HAC inference and is used to define stable temporal ordering. +`cluster` is required for clustered inference. `time_index` defines stable temporal ordering for HAC. `entity_ids` is optional and does not change coefficients; it enables standardized within/between R² and the panel BP-LM diagnostic. ### Other models @@ -140,11 +308,13 @@ FamaMacBeth( ) ``` -## CPU and GPU Examples +`FamaMacBeth.fit(..., entity_ids=None)` accepts optional entity IDs only for Stage-B within/between R². The beta-series estimation and covariance path are unchanged. + +## CPU and GPU Example ```python import numpy as np -from statgpu.panel import PanelOLS, PooledOLS, FamaMacBeth +from statgpu.panel import PanelOLS, PooledOLS, RandomEffects n_entities, n_times = 50, 10 n = n_entities * n_times @@ -153,20 +323,20 @@ time_ids = np.tile(np.arange(n_times), n_entities) X = np.random.default_rng(0).normal(size=(n, 3)) y = X @ np.array([1.0, -0.5, 0.3]) + np.random.default_rng(1).normal(size=n) * 0.1 -# Fixed effects on CPU. -fe = PanelOLS(entity_effects=True, cov_type="robust", device="cpu") -fe.fit(y, X, entity_ids=entity_ids) +fe = PanelOLS(entity_effects=True, device="cpu").fit( + X, y, entity_ids=entity_ids +) +print(fe.fit_statistics_.rsquared_within) +print(fe.pooling_f_test()) -# HAC PooledOLS with explicit time ordering. -pooled_hac = PooledOLS(cov_type="hac", device="cpu") -pooled_hac.fit(X, y, time_index=time_ids) +pooled = PooledOLS(device="cpu").fit(X, y, entity_ids=entity_ids) +print(pooled.breusch_pagan_lm_test()) -# Fama-MacBeth on CuPy CUDA; metadata labels may remain on CPU. -fm = FamaMacBeth(cov_type="newey-west", device="cuda") -fm.fit(X, y, time_ids=time_ids) +re = RandomEffects(device="cpu").fit(X, y, entity_ids=entity_ids) +print(fe.hausman_test(re)) ``` -For Torch CUDA, pass CUDA tensors for numerical arrays and use `device="torch"`. Public prediction methods preserve the estimator backend for array inputs. +For CuPy CUDA use `device="cuda"`; for Torch CUDA use CUDA tensors and `device="torch"`. Stage-B statistical transforms and sufficient-statistic accumulation follow the selected numerical backend. Formula/label metadata, final scalars, and small covariance matrices use the CPU metadata boundary; Hausman-compatible one-way FE/RE fits additionally perform bounded chunked host copies of canonical slope X/y solely for collision-resistant identity hashing. ## Outputs @@ -174,28 +344,28 @@ Common fitted attributes include: - `coef_`; - `bse_`, `tvalues_`, `pvalues_`, `conf_int_` when coefficient-space inference is identifiable; -- `rsquared` or `rsquared_within` as applicable; +- legacy `rsquared` or `rsquared_within` where historically exposed; +- standardized `fit_statistics_`; - `nobs`, `df_resid`, and effective rank where exposed; - `betas_`, `cov_params_`, and `n_periods` for `FamaMacBeth`. -For an exactly rank-deficient `PooledOLS` design, downstream consumers must not interpret coefficient-level inference as uniquely identified. +`PanelTestResult` contains `statistic`, `pvalue`, `distribution`, `df`, `null`, `alternative`, `applicable`, `reason`, and `metadata`. ## Formula and Metadata Boundaries -Formula evaluation may drop rows with missing values. Entity, time, cluster, and other side arrays are aligned to the retained rows. String and categorical labels are factorized on CPU; the numerical transformations and regression calculations remain on the selected backend. +Formula evaluation may drop rows with missing values. Entity, time, cluster, and other side arrays are aligned to the retained rows. String and categorical labels are factorized on CPU; numerical transforms and sufficient-statistic calculations remain on the selected backend. For Hausman-compatible fits, canonical slope X/y are copied to host in bounded chunks for SHA-256 identity, after excluding any RE-only explicit constant; the fitted model stores only the digest, original coefficient-index map, feature/entity metadata, and a small covariance matrix rather than a second CPU copy of the design. ## Validation -PR #79 validated maintained panel behavior across NumPy, CuPy CUDA, and Torch CUDA. The final maintained physical-GPU suite passed **33/33** checks on a Tesla P100, including backend-preserving `PooledOLS.predict()` and the rank-deficient `NOT_COMPARABLE` contract. GitHub Actions also passed the Python 3.9–3.12 regression matrix and full CPU suite on the exact head. - -See: +Stage A / PR #119 established the shared panel framework and passed exact-head physical validation on Tesla P100 across 10 CuPy and 10 Torch cases. -- `dev/reviews/pr79_physical_gpu_validation.md`; -- `dev/tests/test_pr79_physical_gpu.py`; -- Issue #83 for cleanup of ignored legacy GPU diagnostic scripts. +Stage B adds maintained analytic and fitted-model regression tests, formula/missing-row alignment tests, Python 3.9 + Torch 2.0 CPU parity coverage, and an executable `linearmodels==7.0` external-definition gate. The physical runner contains 17 estimator cases per backend and four Hausman diagnostic cases per backend, including balanced/unbalanced RandomEffects with an explicit constant and FE-versus-RE Hausman where FE absorbs that intercept. Final promotion requires `dev/benchmarks/validate_panel_stage_b_gpu.py` to pass on an exact clean commit for both CuPy and Torch CUDA; this runner is a correctness/provenance gate rather than a performance benchmark. A separate physical benchmark measures the remaining full-content identity overhead on Hausman-compatible FE/RE fits. ## References +- Hausman, J. A. (1978). Specification tests in econometrics. +- Breusch, T. S., & Pagan, A. R. (1980). The Lagrange multiplier test and its applications to model specification in econometrics. +- Baltagi, B. H., & Li, Q. (1990). A Lagrange multiplier test for the error components model with incomplete panels. - White, H. (1980). A heteroskedasticity-consistent covariance matrix estimator. - Newey, W. K., & West, K. D. (1987). A simple, positive semi-definite, heteroskedasticity and autocorrelation consistent covariance matrix. - Fama, E. F., & MacBeth, J. D. (1973). Risk, return, and equilibrium. diff --git a/frontend/README.md b/frontend/README.md index fc5305089..f85fd36eb 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,7 +4,7 @@ Interactive benchmark dashboard for statgpu, built with Vite, TypeScript, and EC ## Current coverage -The canonical dashboard is restricted to benchmark sources dated **2026-06-01 or later**. The manifest currently registers **eight sources**, producing **1,774 normalized runs across 36 models**: +The canonical dashboard is restricted to benchmark sources dated **2026-06-01 or later**. The manifest currently registers **11 sources**, producing **1,852 normalized runs across 46 models**: - `p2_benchmark_20260617.json`; - `penalized_glm_perf_20260622.json`; @@ -13,15 +13,18 @@ The canonical dashboard is restricted to benchmark sources dated **2026-06-01 or - `loss_functions_20260623.json`; - `new_modules_full_20260624.json`; - `unsupervised_20260627.json`; -- `ordered_inference_pr74.json`. +- `ordered_inference_pr74.json`; +- `cv_benchmark_20260807.json`; +- `results/pr116_p100/cv_benchmark_pr116_p100.json`; +- `panel_stage_b_pr122_p100_20260808.json`. -Covered categories include penalized GLM and GLM, recent linear models, robust and quantile regression, survival analysis, unsupervised learning, ordered models, nonparametric methods, panel models, covariance estimation, and ANOVA. +Covered categories include penalized GLM and GLM, recent linear models, robust and quantile regression, survival analysis, unsupervised learning, ordered models, nonparametric methods, panel models, covariance estimation, ANOVA, and current cross-validation families. Survival coverage combines the dedicated Efron benchmark with the aligned Breslow rows embedded in `loss_functions_20260623.json`. Breslow contributes five scales, NumPy/CuPy/Torch and statsmodels timings, runner-reported speedups against statsmodels, and CPU/CuPy precision validation. The richer Efron source retains its light-ties and heavy-ties variants. GAM coverage exposes two distinct fixed-lambda pyGAM comparison variants at `1K×3`, `10K×5`, and `100K×10`: the ordinary source comparison and the uniform-knot precision-aligned comparison. Each variant includes NumPy, CuPy, Torch, and pyGAM timing, reported speedup, and prediction-difference validation. Other nonparametric and covariance families remain limited by available source artifacts rather than hidden frontend rows. -Panel coverage exposes both aligned linearmodels scales present in the June 24 source: `10K×10` and `100K×20`. PanelOLS and RandomEffects each include NumPy, CuPy, Torch, and linearmodels timing, runner-reported speedup, and coefficient-relative-error metrics. +Panel coverage has two complementary evidence classes. The June 24 timing source exposes aligned `10K×10` and `100K×20` PanelOLS and RandomEffects comparisons with NumPy, CuPy, Torch, and linearmodels timing, runner-reported speedup, and coefficient-relative-error metrics. PR #122 additionally registers `results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260808.json` (SHA256 `882892c6e3077fe3b9f6084212647311da795fd05d1ed9f12ec53da1e05d0d4d`) as **validation-only** P100 evidence for PooledOLS, BetweenOLS, FirstDifferenceOLS, PanelOLS, RandomEffects, and FamaMacBeth. It contributes 34 CuPy/Torch validation rows covering Stage-B fit statistics/specification diagnostics, backend provenance, and Stage-A coefficient-inference regression. No timing was collected by that physical validator, so these rows deliberately expose neither timing nor speedup metrics. Unsupervised coverage now retains all 131 rows present in the June 27 source rather than selecting one scale per estimator. This includes complete small/medium/large matrices for PCA, KMeans, GaussianMixture, NMF, TruncatedSVD, IncrementalPCA, MiniBatchKMeans, and MiniBatchNMF; both DBSCAN dimensional variants; and every feasible AgglomerativeClustering, UMAP, and t-SNE row. Large input dimensions are labelled from the arrays actually passed to fit, so capped estimators correctly show `100K×50` rather than the uncapped `100K×100` template. @@ -29,7 +32,7 @@ The PR #74 source now contributes all of its inference methods: Ordered Logit/Pr ANOVA coverage includes one-way ANOVA, two-way ANOVA, Welch ANOVA, Tukey HSD, and Bonferroni correction on NumPy, CuPy, and Torch. One-way ANOVA also includes aligned SciPy timing and F-statistic validation rows. -The linear-model category uses the June 2026 squared-error rows from `penalized_glm_perf_20260622.json` and `glm_solver_20260623.json`. April 2026 ElasticNet, LassoCV, comprehensive-validation, Cox package-comparison, and knockoff results are intentionally not registered. +The linear-model category uses the June 2026 squared-error rows from `penalized_glm_perf_20260622.json` and `glm_solver_benchmark_20260623.json`. April 2026 ElasticNet, LassoCV, comprehensive-validation, Cox package-comparison, and knockoff results are intentionally not registered. Current June-or-later sources provide external comparisons through scikit-learn, SciPy, statsmodels, linearmodels, and pyGAM. The feature-selection category remains part of Schema v1.1, but it is intentionally empty until a June 2026-or-later structured benchmark is available. A June distribution report also exists, but it remains outside the dashboard until its rounded Markdown tables are converted or rerun as a structured source with full timing and precision provenance. @@ -38,7 +41,7 @@ Current June-or-later sources provide external comparisons through scikit-learn, - Environment and category navigation. - Progressive filters for model, variant, penalty, solver, scale, backend, and external framework. - Explicit **Focused** and **Full matrix** chart views. -- Timing and speedup charts. +- Timing and speedup charts when those metrics exist for the selected runs. - A sortable and paginated overview table. - Validation, accuracy, inference, prediction, convergence, and selection panels. - Parse-report and source-inventory metadata. @@ -52,6 +55,8 @@ Speedups have two distinct meanings: - **Computed**: reference timing divided by current-run timing. The generated record contains `reference_run_id`. - **Reported by runner**: copied from a benchmark runner that already computed the speedup. These rows carry an `Ⓡ` marker and do not imply frontend recomputation. +Validation-only runs such as PR #122 do not participate in either speedup class because their source contains no timing measurements. + The speedup chart uses a dashed gray 1× parity line with a compact in-chart `1×` badge and `×` axis labels. Runner-reported bars use a subtle border instead of a patterned fill. The global headline card displays only the fastest runner-reported GPU speedup; computed ratios remain available in the chart and raw data for auditing. ## Requirements @@ -88,8 +93,11 @@ The generator automatically uses `dev/benchmarks/frontend_sources.json`. Require python -m pip install -U pytest jsonschema pytest \ dev/tests/test_benchmark_frontend_data.py \ + dev/tests/test_benchmark_catalog.py \ + dev/tests/test_benchmark_inventory_v2.py \ dev/tests/test_frontend_contracts.py \ - dev/tests/test_frontend_domain_coverage.py -v + dev/tests/test_frontend_domain_coverage.py \ + dev/tests/test_panel_stage_b_frontend_source.py -v python dev/benchmarks/generate_benchmark_data.py --check --strict-sources @@ -100,7 +108,7 @@ npx playwright install --with-deps chromium npm run test:e2e ``` -The domain-coverage suite verifies robust/quantile, survival, unsupervised, ordered, nonparametric, panel, covariance, and ANOVA runs. It specifically guards CoxPH Breslow timing/speedup/validation, both complete GAM comparison variants, both aligned Panel scales, all 131 Unsupervised rows and corrected scale labels, all PR #74 inference methods, Focused/Full matrix switching, the dashed 1× parity contract, June 2026 linear-model sources, ANOVA backend/SciPy coverage, speedup-summary semantics, and removal of pre-June framework controls. +The domain-coverage suite verifies robust/quantile, survival, unsupervised, ordered, nonparametric, panel, covariance, and ANOVA runs. It specifically guards CoxPH Breslow timing/speedup/validation, both complete GAM comparison variants, both aligned Panel timing scales, the PR #122 34-row validation-only Panel source, all 131 Unsupervised rows and corrected scale labels, all PR #74 inference methods, Focused/Full matrix switching, the dashed 1× parity contract, June 2026 linear-model sources, ANOVA backend/SciPy coverage, speedup-summary semantics, and removal of pre-June framework controls. ## Production build and staleness diff --git a/frontend/public/data/benchmark_data.json b/frontend/public/data/benchmark_data.json index bb083b1af..b7b025ba4 100644 --- a/frontend/public/data/benchmark_data.json +++ b/frontend/public/data/benchmark_data.json @@ -4,7 +4,7 @@ "meta": { "generator": "dev/benchmarks/generate_benchmark_data.py", "git_sha": "deterministic", - "generation_id": "ceac2591d688920101474d09e145c7595e6d9460ce347828b4225139fcd60644" + "generation_id": "b425b95947fcb5ee7dcd4e6e10f1b108cfe82b804f5d46428e63c1fb5ca35b1d" }, "environments": [ { @@ -27,6 +27,12 @@ "gpu": "Tesla P100-SXM2-16GB", "cpu": "x86_64", "host": "wE6lDe" + }, + { + "env_id": "remote-p100-pr122-20260809", + "label": "Tesla P100 PR #122 Panel Stage B validation — 2026-08-09", + "gpu": "Tesla P100-SXM2-16GB", + "cpu": "x86_64" } ], "categories": [ @@ -110,6 +116,15 @@ "supports_penalty": false, "supports_inference": false }, + { + "model_id": "BetweenOLS", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, { "model_id": "BonferroniCorrection", "primary_category_id": "anova", @@ -165,6 +180,24 @@ "supports_penalty": false, "supports_inference": true }, + { + "model_id": "FamaMacBeth", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, + { + "model_id": "FirstDifferenceOLS", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, { "model_id": "GAM", "primary_category_id": "nonparametric", @@ -382,6 +415,15 @@ "supports_penalty": true, "supports_inference": true }, + { + "model_id": "PooledOLS", + "primary_category_id": "panel", + "category_ids": [ + "panel" + ], + "supports_penalty": false, + "supports_inference": true + }, { "model_id": "QuantileRegression", "primary_category_id": "robust_quantile", @@ -562,6 +604,11 @@ "label": "Covariance and nonparametric benchmark — 2026-06-17", "env_id": "remote-p100" }, + { + "comparison_id": "panel-stage-b-pr122-20260809", + "label": "Panel Stage B physical validation — PR #122 — 2026-08-09", + "env_id": "remote-p100-pr122-20260809" + }, { "comparison_id": "penalized-glm-perf-20260622", "label": "Penalized GLM performance — 2026-06-22", @@ -84727,7 +84774,7 @@ "metrics": { "timing": { "fit_time_ms": 9.062319993972778, - "std_ms": 0.011520549774405635, + "std_ms": 0.011520549774, "min_ms": 9.062081575393677, "max_ms": 9.086638689041138, "sample_count": 3, @@ -84882,7 +84929,7 @@ "metrics": { "timing": { "fit_time_ms": 17.41701364517212, - "std_ms": 0.10901293932213849, + "std_ms": 0.109012939322, "min_ms": 17.355382442474365, "max_ms": 17.611205577850342, "sample_count": 3, @@ -85037,7 +85084,7 @@ "metrics": { "timing": { "fit_time_ms": 12.491881847381592, - "std_ms": 0.10357847916711392, + "std_ms": 0.103578479167, "min_ms": 12.459933757781982, "max_ms": 12.69388198852539, "sample_count": 3, @@ -85192,7 +85239,7 @@ "metrics": { "timing": { "fit_time_ms": 7.204592227935791, - "std_ms": 0.03478300871847091, + "std_ms": 0.034783008718, "min_ms": 7.1443915367126465, "max_ms": 7.226705551147461, "sample_count": 3, @@ -85347,7 +85394,7 @@ "metrics": { "timing": { "fit_time_ms": 29.17364239692688, - "std_ms": 0.11058449868330528, + "std_ms": 0.110584498683, "min_ms": 29.15489673614502, "max_ms": 29.398292303085327, "sample_count": 3, @@ -85502,7 +85549,7 @@ "metrics": { "timing": { "fit_time_ms": 76.099693775177, - "std_ms": 0.15514420258897654, + "std_ms": 0.155144202589, "min_ms": 75.85901021957397, "max_ms": 76.23404264450073, "sample_count": 3, @@ -85657,7 +85704,7 @@ "metrics": { "timing": { "fit_time_ms": 34.79743003845215, - "std_ms": 0.08212314243487934, + "std_ms": 0.082123142435, "min_ms": 34.72268581390381, "max_ms": 34.921795129776, "sample_count": 3, @@ -85812,7 +85859,7 @@ "metrics": { "timing": { "fit_time_ms": 7.392674684524536, - "std_ms": 0.06373652588749994, + "std_ms": 0.063736525887, "min_ms": 7.343053817749023, "max_ms": 7.496058940887451, "sample_count": 3, @@ -85968,7 +86015,7 @@ "metrics": { "timing": { "fit_time_ms": 76.40200853347778, - "std_ms": 0.12667643292286518, + "std_ms": 0.126676432923, "min_ms": 76.32750272750854, "max_ms": 76.62561535835266, "sample_count": 3, @@ -86125,7 +86172,7 @@ "metrics": { "timing": { "fit_time_ms": 153.78409624099731, - "std_ms": 1.2051632389990876, + "std_ms": 1.205163238999, "min_ms": 151.39129757881165, "max_ms": 154.0849506855011, "sample_count": 3, @@ -86282,7 +86329,7 @@ "metrics": { "timing": { "fit_time_ms": 122.1560537815094, - "std_ms": 1.2313895375387158, + "std_ms": 1.231389537539, "min_ms": 122.06673622131348, "max_ms": 124.722421169281, "sample_count": 3, @@ -86439,7 +86486,7 @@ "metrics": { "timing": { "fit_time_ms": 7.413893938064575, - "std_ms": 0.03394906482522582, + "std_ms": 0.033949064825, "min_ms": 7.350414991378784, "max_ms": 7.428675889968872, "sample_count": 3, @@ -86595,7 +86642,7 @@ "metrics": { "timing": { "fit_time_ms": 50.83748698234558, - "std_ms": 0.5573683115942412, + "std_ms": 0.557368311594, "min_ms": 50.72379112243652, "max_ms": 51.95888876914978, "sample_count": 3, @@ -86750,7 +86797,7 @@ "metrics": { "timing": { "fit_time_ms": 103.54551672935486, - "std_ms": 0.43010181393284636, + "std_ms": 0.430101813933, "min_ms": 102.88560390472412, "max_ms": 103.92677783966064, "sample_count": 3, @@ -87008,7 +87055,7 @@ "metrics": { "timing": { "fit_time_ms": 32.809287309646606, - "std_ms": 0.22101482164439643, + "std_ms": 0.221014821644, "min_ms": 32.615602016448975, "max_ms": 33.15025568008423, "sample_count": 3, @@ -87163,7 +87210,7 @@ "metrics": { "timing": { "fit_time_ms": 5.704343318939209, - "std_ms": 0.05382879156628554, + "std_ms": 0.053828791566, "min_ms": 5.664348602294922, "max_ms": 5.793154239654541, "sample_count": 3, @@ -87318,7 +87365,7 @@ "metrics": { "timing": { "fit_time_ms": 79.3997049331665, - "std_ms": 0.2710391095246946, + "std_ms": 0.271039109525, "min_ms": 79.18986678123474, "max_ms": 79.8402726650238, "sample_count": 3, @@ -87473,7 +87520,7 @@ "metrics": { "timing": { "fit_time_ms": 53.322941064834595, - "std_ms": 0.0964612495050475, + "std_ms": 0.096461249505, "min_ms": 53.19675803184509, "max_ms": 53.432852029800415, "sample_count": 3, @@ -87628,7 +87675,7 @@ "metrics": { "timing": { "fit_time_ms": 367.21765995025635, - "std_ms": 1.2007614908480113, + "std_ms": 1.200761490848, "min_ms": 367.20117926597595, "max_ms": 369.7565793991089, "sample_count": 3, @@ -87783,7 +87830,7 @@ "metrics": { "timing": { "fit_time_ms": 387.08221912384033, - "std_ms": 1.7942564379041452, + "std_ms": 1.794256437904, "min_ms": 385.0722312927246, "max_ms": 389.46205377578735, "sample_count": 3, @@ -87938,7 +87985,7 @@ "metrics": { "timing": { "fit_time_ms": 226.59814357757568, - "std_ms": 1.4370176592588964, + "std_ms": 1.437017659259, "min_ms": 225.08418560028076, "max_ms": 228.59317064285278, "sample_count": 3, @@ -88093,7 +88140,7 @@ "metrics": { "timing": { "fit_time_ms": 9.013503789901733, - "std_ms": 0.08388103984794355, + "std_ms": 0.083881039848, "min_ms": 8.965373039245605, "max_ms": 9.162425994873047, "sample_count": 3, @@ -88248,7 +88295,7 @@ "metrics": { "timing": { "fit_time_ms": 17.37356185913086, - "std_ms": 0.1751727379638956, + "std_ms": 0.175172737964, "min_ms": 17.317146062850952, "max_ms": 17.713725566864014, "sample_count": 3, @@ -88403,7 +88450,7 @@ "metrics": { "timing": { "fit_time_ms": 12.376457452774048, - "std_ms": 0.1469539035687877, + "std_ms": 0.146953903569, "min_ms": 12.127071619033813, "max_ms": 12.476563453674316, "sample_count": 3, @@ -88558,7 +88605,7 @@ "metrics": { "timing": { "fit_time_ms": 7.113069295883179, - "std_ms": 0.022110917652291093, + "std_ms": 0.022110917652, "min_ms": 7.089287042617798, "max_ms": 7.1433186531066895, "sample_count": 3, @@ -88713,7 +88760,7 @@ "metrics": { "timing": { "fit_time_ms": 29.22457456588745, - "std_ms": 0.07344943972222218, + "std_ms": 0.073449439722, "min_ms": 29.094338417053223, "max_ms": 29.266953468322754, "sample_count": 3, @@ -88868,7 +88915,7 @@ "metrics": { "timing": { "fit_time_ms": 76.48885250091553, - "std_ms": 0.7515277112219284, + "std_ms": 0.751527711222, "min_ms": 76.3988196849823, "max_ms": 78.03615927696228, "sample_count": 3, @@ -89023,7 +89070,7 @@ "metrics": { "timing": { "fit_time_ms": 34.95398163795471, - "std_ms": 0.08122915143822747, + "std_ms": 0.081229151438, "min_ms": 34.94828939437866, "max_ms": 35.12337803840637, "sample_count": 3, @@ -89178,7 +89225,7 @@ "metrics": { "timing": { "fit_time_ms": 7.375836372375488, - "std_ms": 0.2661149489547812, + "std_ms": 0.266114948955, "min_ms": 7.340103387832642, "max_ms": 7.92163610458374, "sample_count": 3, @@ -89334,7 +89381,7 @@ "metrics": { "timing": { "fit_time_ms": 75.77601075172424, - "std_ms": 0.45205802075521384, + "std_ms": 0.452058020755, "min_ms": 75.26275515556335, "max_ms": 76.36910676956177, "sample_count": 3, @@ -89491,7 +89538,7 @@ "metrics": { "timing": { "fit_time_ms": 153.40235829353333, - "std_ms": 0.5537958099223218, + "std_ms": 0.553795809922, "min_ms": 153.37586402893066, "max_ms": 154.56366539001465, "sample_count": 3, @@ -89648,7 +89695,7 @@ "metrics": { "timing": { "fit_time_ms": 121.93247675895691, - "std_ms": 0.2863016881758537, + "std_ms": 0.286301688176, "min_ms": 121.55237793922424, "max_ms": 122.25282192230225, "sample_count": 3, @@ -89805,7 +89852,7 @@ "metrics": { "timing": { "fit_time_ms": 7.36507773399353, - "std_ms": 0.0928811561208376, + "std_ms": 0.092881156121, "min_ms": 7.2509050369262695, "max_ms": 7.4784159660339355, "sample_count": 3, @@ -89961,7 +90008,7 @@ "metrics": { "timing": { "fit_time_ms": 51.06106400489807, - "std_ms": 0.12425235011605137, + "std_ms": 0.124252350116, "min_ms": 50.835102796554565, "max_ms": 51.12466216087341, "sample_count": 3, @@ -90116,7 +90163,7 @@ "metrics": { "timing": { "fit_time_ms": 95.80263495445251, - "std_ms": 3.1773095837367222, + "std_ms": 3.177309583737, "min_ms": 89.1546905040741, "max_ms": 95.98329663276672, "sample_count": 3, @@ -90271,7 +90318,7 @@ "metrics": { "timing": { "fit_time_ms": 49.2972731590271, - "std_ms": 2.079922258152906, + "std_ms": 2.079922258153, "min_ms": 48.696696758270264, "max_ms": 53.378403186798096, "sample_count": 3, @@ -90426,7 +90473,7 @@ "metrics": { "timing": { "fit_time_ms": 32.66507387161255, - "std_ms": 0.16058180541689593, + "std_ms": 0.160581805417, "min_ms": 32.47341513633728, "max_ms": 32.866716384887695, "sample_count": 3, @@ -90581,7 +90628,7 @@ "metrics": { "timing": { "fit_time_ms": 5.652785301208496, - "std_ms": 0.0647337375076041, + "std_ms": 0.064733737508, "min_ms": 5.633354187011719, "max_ms": 5.779355764389038, "sample_count": 3, @@ -90736,7 +90783,7 @@ "metrics": { "timing": { "fit_time_ms": 79.96299862861633, - "std_ms": 0.3447674872577324, + "std_ms": 0.344767487258, "min_ms": 79.8010528087616, "max_ms": 80.5998146533966, "sample_count": 3, @@ -90891,7 +90938,7 @@ "metrics": { "timing": { "fit_time_ms": 53.66373062133789, - "std_ms": 0.07744715297714648, + "std_ms": 0.077447152977, "min_ms": 53.63589525222778, "max_ms": 53.81232500076294, "sample_count": 3, @@ -91046,7 +91093,7 @@ "metrics": { "timing": { "fit_time_ms": 368.74711513519287, - "std_ms": 1.0174916822778004, + "std_ms": 1.017491682278, "min_ms": 367.16771125793457, "max_ms": 369.62711811065674, "sample_count": 3, @@ -91201,7 +91248,7 @@ "metrics": { "timing": { "fit_time_ms": 393.74133944511414, - "std_ms": 10.053154619538892, + "std_ms": 10.053154619539, "min_ms": 372.9921281337738, "max_ms": 394.8514759540558, "sample_count": 3, @@ -91356,7 +91403,7 @@ "metrics": { "timing": { "fit_time_ms": 229.87377643585205, - "std_ms": 0.8445346932663552, + "std_ms": 0.844534693266, "min_ms": 229.26145792007446, "max_ms": 231.27886652946472, "sample_count": 3, @@ -91413,6 +91460,3062 @@ } }, "comparison_id": "cv-benchmark-pr116-20260807" + }, + { + "run_id": "7279d751fec4ae72", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-0df334186e5af228", + "method_config_id": "method-c0fdde15c26f43a5", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "56290607cc2a0cf2", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dfd23de8fa23785c", + "method_config_id": "method-e4f7696d4033b5bd", + "variant": "balanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "695e9058534086b0", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-ef4ada9396e927aa", + "method_config_id": "method-c6458b366c6c5b4b", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "477b4803cfaac953", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-77e21b1b2625849e", + "method_config_id": "method-a891526d2820b20c", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6ca556cca3c1dc6c", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7505e931c271bead", + "method_config_id": "method-936c0cf8f8c6843c", + "variant": "entity-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "e21809c952ed9df4", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-f697cc38be66bb5b", + "method_config_id": "method-75395c8069822f63", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "ac6d5fc5e070c379", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-376492aadf7db27e", + "method_config_id": "method-af8a0d67f1b0b4e8", + "variant": "explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p3", + "n_samples": 54, + "n_features": 3, + "label": "54×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d4465ae990d85c88", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-23b6169b7248b5ff", + "method_config_id": "method-b815058aea2adbef", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "e3be78f30afda49c", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-6e32a6271b846410", + "method_config_id": "method-64417e823f34055a", + "variant": "two-way-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "4452fae4e283ef7b", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dbbafa2f82f52516", + "method_config_id": "method-0ef4a02559810006", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "056e0de8e3170092", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-3fac3845d997d0be", + "method_config_id": "method-0ce477d0912b1b99", + "variant": "unbalanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "a7069bd41b6ce3c2", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-9319e500c0d05810", + "method_config_id": "method-fd5b0f3282f7b323", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "c4014db4618f0475", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-7e6762b4b150ea72", + "method_config_id": "method-dc8c010b9bd69c0b", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "547f90cd8206fe0e", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7a6f0593ba47178d", + "method_config_id": "method-73caf204ad0ce7d3", + "variant": "entity-fe-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "3fc25da005609311", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-8c5eda16f11a344d", + "method_config_id": "method-2062c9b03141cff2", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "445759ea0b44a878", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-9a06cd1ab60b638f", + "method_config_id": "method-b230d1629d626033", + "variant": "explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p3", + "n_samples": 49, + "n_features": 3, + "label": "49×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6b0cbd5594989983", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-9ae95425684d313a", + "method_config_id": "method-2b49a6a725b5ea3c", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "83cf372c6fccda77", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-4a81e6630ee4a244", + "method_config_id": "method-4ed205ed0ad3db26", + "variant": "hausman-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "095e3a18069cd015", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-c1327c75008f9dfa", + "method_config_id": "method-6352b566ecc4def7", + "variant": "hausman-re-explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "a5ce2c8e73fdfcd3", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-528c7a3f7e02b3e3", + "method_config_id": "method-6580ac1c68723cc9", + "variant": "hausman-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "08bb5121ff32dd95", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7053bf05f9fc593f", + "method_config_id": "method-b49c53698837f046", + "variant": "hausman-re-explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "a72370d3b5ccdeef", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-0571875762c64d2c", + "method_config_id": "method-d58d685666eb74d4", + "variant": "hausman-applicable-nonzero-effect", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "cupy", + "scale": { + "scale_key": "n48_p1", + "n_samples": 48, + "n_features": 1, + "label": "48×1" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": true, + "diagnostic_fixture": "nonzero-effect-applicable", + "statistic": 1.1965942530851057, + "pvalue": 0.27400344142676447, + "df": 1.0, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + }, + { + "metric": "hausman_applicable_statistic_pvalue_df", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6cd624414dece5bf", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-0df334186e5af228", + "method_config_id": "method-c0fdde15c26f43a5", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d515d6684ce4f746", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dfd23de8fa23785c", + "method_config_id": "method-e4f7696d4033b5bd", + "variant": "balanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "6ccea7600064931d", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-ef4ada9396e927aa", + "method_config_id": "method-c6458b366c6c5b4b", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d6e9e3dbe35b8b12", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-77e21b1b2625849e", + "method_config_id": "method-a891526d2820b20c", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "1704f2b8ba0d05c5", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7505e931c271bead", + "method_config_id": "method-936c0cf8f8c6843c", + "variant": "entity-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "547b8f511ea88b95", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-f697cc38be66bb5b", + "method_config_id": "method-75395c8069822f63", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "c3b0d6f5d6fdfd93", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-376492aadf7db27e", + "method_config_id": "method-af8a0d67f1b0b4e8", + "variant": "explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p3", + "n_samples": 54, + "n_features": 3, + "label": "54×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "89b036b7f61372b0", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-23b6169b7248b5ff", + "method_config_id": "method-b815058aea2adbef", + "variant": "balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "75dadabb2c7d4a31", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-6e32a6271b846410", + "method_config_id": "method-64417e823f34055a", + "variant": "two-way-fe-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "99bb431cee9ad0b9", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-dbbafa2f82f52516", + "method_config_id": "method-0ef4a02559810006", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "4b1040cb15d08732", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PooledOLS", + "case_id": "case-3fac3845d997d0be", + "method_config_id": "method-0ce477d0912b1b99", + "variant": "unbalanced-hac-unsorted", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "bp_lm", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "metadata_sort_alignment", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "3a94049592ef4a57", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "BetweenOLS", + "case_id": "case-9319e500c0d05810", + "method_config_id": "method-fd5b0f3282f7b323", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "40b888d3411cdbec", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FirstDifferenceOLS", + "case_id": "case-7e6762b4b150ea72", + "method_config_id": "method-dc8c010b9bd69c0b", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "d102490eea35e9d7", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7a6f0593ba47178d", + "method_config_id": "method-73caf204ad0ce7d3", + "variant": "entity-fe-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "pooling_f", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "9ca6ae85a86d0dce", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-8c5eda16f11a344d", + "method_config_id": "method-2062c9b03141cff2", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "fc273a5c73d0ecea", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "RandomEffects", + "case_id": "case-9a06cd1ab60b638f", + "method_config_id": "method-b230d1629d626033", + "variant": "explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p3", + "n_samples": 49, + "n_features": 3, + "label": "49×3" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "random_effects_diagnostic_contract", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "bd74d2455189bbd5", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "FamaMacBeth", + "case_id": "case-9ae95425684d313a", + "method_config_id": "method-2b49a6a725b5ea3c", + "variant": "unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "fit_statistics", + "status": "pass" + }, + { + "metric": "diagnostic_covariance", + "status": "pass" + }, + { + "metric": "stage_a_inference", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + }, + "inference": { + "ok": true, + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "cee85afecd82d8ee", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-4a81e6630ee4a244", + "method_config_id": "method-4ed205ed0ad3db26", + "variant": "hausman-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "4712fb5193e2fb7f", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-c1327c75008f9dfa", + "method_config_id": "method-6352b566ecc4def7", + "variant": "hausman-re-explicit-constant-balanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n54_p2", + "n_samples": 54, + "n_features": 2, + "label": "54×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "b07fbe6ab06e9c8a", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-528c7a3f7e02b3e3", + "method_config_id": "method-6580ac1c68723cc9", + "variant": "hausman-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "81049fac88841b43", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-7053bf05f9fc593f", + "method_config_id": "method-b49c53698837f046", + "variant": "hausman-re-explicit-constant-unbalanced", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n49_p2", + "n_samples": 49, + "n_features": 2, + "label": "49×2" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "re-explicit-constant", + "applicable": false, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" + }, + { + "run_id": "3fd46d0bebaa4152", + "benchmark_session_id": "remote-p100-pr122-20260809-panel-stage-b-pr122", + "env_id": "remote-p100-pr122-20260809", + "category_ids": [ + "panel" + ], + "model_id": "PanelOLS", + "case_id": "case-0571875762c64d2c", + "method_config_id": "method-d58d685666eb74d4", + "variant": "hausman-applicable-nonzero-effect", + "penalty": null, + "solver": "physical_validation", + "solver_display": "Physical validation", + "solver_kind": "internal", + "framework": "statgpu", + "backend": "torch", + "scale": { + "scale_key": "n48_p1", + "n_samples": 48, + "n_features": 1, + "label": "48×1" + }, + "parameters": { + "metric_scope": "physical_validation", + "diagnostic": "hausman", + "parameterization": "standard", + "applicable": true, + "diagnostic_fixture": "nonzero-effect-applicable", + "statistic": 1.1965942530849238, + "pvalue": 0.2740034414268009, + "df": 1.0, + "measurement_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true + }, + "source": { + "file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "date": "2026-08-09", + "parser": "parse_panel_stage_b_physical_validation_v1", + "parser_version": "1.0", + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "original_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "sha256": "2056f836bfe2a708b3131becca42dbba15e519762c8bafb582000b19f81120bf" + }, + "metrics": { + "validation": { + "status": "pass", + "checks": [ + { + "metric": "hausman_backend_consistency", + "status": "pass" + }, + { + "metric": "backend_provenance", + "status": "pass" + }, + { + "metric": "hausman_applicable_statistic_pvalue_df", + "status": "pass" + } + ], + "quality": "reported", + "source_file": "panel_stage_b_pr122_p100_20260809_2701aa9f.json" + } + }, + "comparison_id": "panel-stage-b-pr122-20260809" } ] } \ No newline at end of file diff --git a/frontend/public/data/parse_report.json b/frontend/public/data/parse_report.json index 5abc6f0d0..68a6a5657 100644 --- a/frontend/public/data/parse_report.json +++ b/frontend/public/data/parse_report.json @@ -1,10 +1,10 @@ { "report_version": "2.0", - "files_seen": 10, - "files_parsed": 10, + "files_seen": 11, + "files_parsed": 11, "files_skipped": 0, - "runs_generated": 1818, - "generation_id": "ceac2591d688920101474d09e145c7595e6d9460ce347828b4225139fcd60644", + "runs_generated": 1862, + "generation_id": "b425b95947fcb5ee7dcd4e6e10f1b108cfe82b804f5d46428e63c1fb5ca35b1d", "issues": [ { "source_id": "glm-solver-20260623-1b6197d94d88", diff --git a/frontend/public/data/source_inventory.json b/frontend/public/data/source_inventory.json index 3a6f459fd..efd33ae22 100644 --- a/frontend/public/data/source_inventory.json +++ b/frontend/public/data/source_inventory.json @@ -1,22 +1,22 @@ { "inventory_version": "2.0", "catalog_version": "2.0", - "catalog_digest": "1de71937d6d994c3ada387018a087e0d1c7b146099853c6ab700f0993f25a5fe", + "catalog_digest": "c9c6655a95d2347de0344a9431f75ec3b218c0b70a8c842719b2f5b8174bed54", "coverage_matrix_version": "1.0", - "coverage_matrix_digest": "b9264e98be2d774a9251ff122a36a0acb16ab64217a1ae9bcc1ac2e11599edf0", - "discovered_json_artifacts": 89, - "classified_candidate_sources": 89, - "eligible_sources": 10, - "registered_sources": 10, - "available_registered_sources": 10, - "parsed_registered_sources": 10, + "coverage_matrix_digest": "8ed8713f8ef977ad4b33eab83a33d65227e6e12a8a5179a0d7e6dea848a85a91", + "discovered_json_artifacts": 95, + "classified_candidate_sources": 95, + "eligible_sources": 11, + "registered_sources": 11, + "available_registered_sources": 11, + "parsed_registered_sources": 11, "eligible_unregistered_sources": 0, - "not_canonical_ready_sources": 42, - "historical_or_excluded_sources": 37, - "superseded_or_duplicate_sources": 9, + "not_canonical_ready_sources": 45, + "historical_or_excluded_sources": 39, + "superseded_or_duplicate_sources": 10, "unrelated_json_artifacts": 0, "unclassified_artifacts": 0, - "catalog_policy_digest": "23cd0e6e1ccc7a563a42fd6d999a7f6f0a3590726f93030c41c95672ebe19e0a", + "catalog_policy_digest": "c32b3e90a3b6f9694bfa6052c5e19629220f9266b59eef0a8d5dd62a3705e637", "catalog_entries": [ { "path": "results/bench_large_scale.json", @@ -828,6 +828,42 @@ "issue": null, "rule_id": "manifest-registration" }, + { + "path": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809.json", + "artifact_type": "json", + "source_date": "2026-08-09", + "classification": "superseded_or_duplicate", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "complete", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "The 42-row a57efcea Stage-B canonical validation source is retained immutably for audit but is superseded by the exact-clean-head 2701aa9f P100 source that also physically validates an applicable Hausman branch.", + "superseded_by": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "issue": "#93", + "rule_id": "superseded-pr122-panel-stage-b-canonical-a57efcea" + }, + { + "path": "results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json", + "artifact_type": "json", + "source_date": "2026-08-09", + "classification": "registered_canonical", + "canonical_eligible": true, + "registered": true, + "source_id": "panel-stage-b-pr122-20260809-2056f836bfe2", + "parser": "panel_stage_b_physical_validation", + "parser_version": "1.0", + "provenance_status": "complete", + "timing_protocol_status": "accepted", + "statistical_alignment_status": "accepted", + "reason": "Registered in frontend_sources.json and protected by manifest SHA256.", + "superseded_by": null, + "issue": null, + "rule_id": "manifest-registration" + }, { "path": "results/benchmark_frontend_sources/penalized_cox_trusted_gradient_pr80_20260727.json", "artifact_type": "json", @@ -1224,6 +1260,78 @@ "issue": "#112", "rule_id": "pr116-logistic-cv-focused-validation" }, + { + "path": "results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json", + "artifact_type": "json", + "source_date": null, + "classification": "historical_or_excluded", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "incomplete_date", + "timing_protocol_status": "unknown", + "statistical_alignment_status": "unknown", + "reason": "Artifact has no deterministically recoverable result date and is excluded pending explicit provenance review.", + "superseded_by": null, + "issue": "#100", + "rule_id": "undated-json" + }, + { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "artifact_type": "json", + "source_date": null, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "superseded_by": null, + "issue": "#93", + "rule_id": "pr122-panel-stage-b-physical-validation" + }, + { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json", + "artifact_type": "json", + "source_date": null, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "superseded_by": null, + "issue": "#93", + "rule_id": "pr122-panel-stage-b-physical-validation" + }, + { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_faa95ce7.json", + "artifact_type": "json", + "source_date": null, + "classification": "not_canonical_ready", + "canonical_eligible": false, + "registered": false, + "source_id": null, + "parser": null, + "parser_version": null, + "provenance_status": "validation_evidence", + "timing_protocol_status": "not_applicable", + "statistical_alignment_status": "accepted", + "reason": "Immutable full Stage-B physical correctness/backend-provenance artifacts for PR #122 are retained for audit; only the currently SHA-protected normalized canonical source is registered in frontend_sources.json.", + "superseded_by": null, + "issue": "#93", + "rule_id": "pr122-panel-stage-b-physical-validation" + }, { "path": "results/pr74_inference_validation.json", "artifact_type": "json", @@ -1627,5 +1735,5 @@ "current_evidence_not_canonical_ready": 2, "partial_canonical": 7 }, - "generation_id": "ceac2591d688920101474d09e145c7595e6d9460ce347828b4225139fcd60644" + "generation_id": "b425b95947fcb5ee7dcd4e6e10f1b108cfe82b804f5d46428e63c1fb5ca35b1d" } \ No newline at end of file diff --git a/results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809.json b/results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809.json new file mode 100644 index 000000000..a24a9ab9a --- /dev/null +++ b/results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809.json @@ -0,0 +1,451 @@ +{ + "method": "Panel Tier-1 Stage B physical GPU validation", + "source_schema_version": "1.0", + "source_date": "2026-08-09", + "git_sha": "a57efcea29b0e87ecb89865c5a6902d5773812c6", + "working_tree_clean": true, + "status": "success", + "schema_status": "ok", + "raw_artifact": { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json", + "repository_commit": "72b3279d2028e8ec2af30e138e123aceb611ae8c", + "git_blob_sha": "254b64776bff4e3b2b642bb4a2ae1eea25f4751c", + "generated_at": "2026-08-09T03:37:01.897253Z", + "schema_version": 2 + }, + "focused_artifact": { + "path": "results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json", + "repository_commit": "72b3279d2028e8ec2af30e138e123aceb611ae8c", + "git_blob_sha": "3bda0b2040479ba8201e2722eb990ba086c3f3b9", + "generated_at": "2026-08-09T03:37:05.589811+00:00", + "schema_version": 1, + "validation": "panel_stage_b_disconnected_two_way_fe", + "status": "success" + }, + "environment": { + "env_id": "remote-p100-pr122-20260809", + "gpu": "Tesla P100-SXM2-16GB", + "cpu": "x86_64", + "platform": "Linux-3.10.0-1062.el7.x86_64-x86_64-with-glibc2.31", + "python": "3.9.16", + "packages": { + "statgpu": "0.2.4", + "numpy": "1.24.2", + "scipy": "1.10.1", + "cupy": null, + "torch": "2.0.0" + }, + "available_backends": [ + "cupy", + "torch" + ] + }, + "protocol": { + "runner": "dev/benchmarks/validate_panel_stage_b_gpu.py", + "focused_runner": "dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py", + "runner_schema_version": 2, + "focused_runner_schema_version": 1, + "reference_backend": "numpy", + "dtype": "float64", + "timing_collected": false, + "failure_policy": "fail_on_backend_mismatch_or_precision_regression", + "rtol": 5e-06, + "atol": 5e-07 + }, + "backend_times": { + "numpy": null, + "cupy": null, + "torch": null + }, + "external_baseline": { + "name": "statgpu NumPy reference", + "time": null, + "version": null + }, + "precision_vs_external": {}, + "convergence_status": {}, + "backend_precision": { + "cupy": "pass", + "torch": "pass" + }, + "compatibility_matrix": { + "cupy": { + "model_cases": "17/17", + "diagnostics": "4/4", + "executed_backend": "cupy", + "cpu_fallback": false, + "disconnected_fe": "pass" + }, + "torch": { + "model_cases": "17/17", + "diagnostics": "4/4", + "executed_backend": "torch", + "cpu_fallback": false, + "disconnected_fe": "pass" + } + }, + "inference_matrix": { + "status": "pass", + "fields": [ + "coef", + "bse", + "tvalues", + "pvalues", + "conf_int", + "nobs", + "df_resid" + ], + "reference": "statgpu NumPy", + "reported_precision": "Full Stage-B matrix passed validator tolerances on CuPy/Torch; the focused disconnected two-way FE case passed with df_resid=1, effect_rank=7, incidence_components=3, and confidence-interval differences at floating-point noise." + }, + "validation_tier": "remote-full", + "timing_scope": { + "collected": false, + "reason": "Correctness/backend-provenance validation only; no benchmark timing was measured." + }, + "reproducibility": { + "exact_git_sha": "a57efcea29b0e87ecb89865c5a6902d5773812c6", + "working_tree_clean": true, + "runner": "dev/benchmarks/validate_panel_stage_b_gpu.py", + "focused_runner": "dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py", + "raw_artifact_path": "results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json", + "raw_artifact_git_blob_sha": "254b64776bff4e3b2b642bb4a2ae1eea25f4751c", + "focused_artifact_path": "results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json", + "focused_artifact_git_blob_sha": "3bda0b2040479ba8201e2722eb990ba086c3f3b9", + "repository_commit": "72b3279d2028e8ec2af30e138e123aceb611ae8c" + }, + "uncovered_reasons": [ + "Performance timing was not collected by this correctness-only physical validation and must not be inferred from this source.", + "All standard and explicit-RE-constant Hausman validation datasets produced materially indefinite covariance differences; applicability/reason parity against NumPy is recorded.", + "The runner could not resolve an installed CuPy distribution version through importlib.metadata, so the source records the CuPy package version as null while executed_backend='cupy' proves physical backend execution.", + "The focused disconnected-FE artifact is retained as validation evidence and is not expanded into separate dashboard timing rows." + ], + "case_catalog": [ + { + "case_id": "pooled_balanced", + "model_id": "PooledOLS", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "pooled_hac_unsorted_balanced", + "model_id": "PooledOLS", + "variant": "balanced-hac-unsorted", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "metadata_sort_alignment", + "backend_provenance" + ] + }, + { + "case_id": "between_balanced", + "model_id": "BetweenOLS", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "first_difference_balanced", + "model_id": "FirstDifferenceOLS", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "panel_entity_balanced", + "model_id": "PanelOLS", + "variant": "entity-fe-balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "pooling_f", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_balanced", + "model_id": "RandomEffects", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_explicit_constant_balanced", + "model_id": "RandomEffects", + "variant": "explicit-constant-balanced", + "n_samples": 54, + "n_features": 3, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "fama_macbeth_balanced", + "model_id": "FamaMacBeth", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "panel_two_way_balanced", + "model_id": "PanelOLS", + "variant": "two-way-fe-balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "pooling_f", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "pooled_unbalanced", + "model_id": "PooledOLS", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "pooled_hac_unsorted_unbalanced", + "model_id": "PooledOLS", + "variant": "unbalanced-hac-unsorted", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "metadata_sort_alignment", + "backend_provenance" + ] + }, + { + "case_id": "between_unbalanced", + "model_id": "BetweenOLS", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "first_difference_unbalanced", + "model_id": "FirstDifferenceOLS", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "panel_entity_unbalanced", + "model_id": "PanelOLS", + "variant": "entity-fe-unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "pooling_f", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_unbalanced", + "model_id": "RandomEffects", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_explicit_constant_unbalanced", + "model_id": "RandomEffects", + "variant": "explicit-constant-unbalanced", + "n_samples": 49, + "n_features": 3, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "fama_macbeth_unbalanced", + "model_id": "FamaMacBeth", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + } + ], + "backend_results": { + "cupy": { + "status": "success", + "executed_backend": "cupy", + "model_cases": { + "pooled_balanced": "success", + "pooled_hac_unsorted_balanced": "success", + "between_balanced": "success", + "first_difference_balanced": "success", + "panel_entity_balanced": "success", + "random_effects_balanced": "success", + "random_effects_explicit_constant_balanced": "success", + "fama_macbeth_balanced": "success", + "panel_two_way_balanced": "success", + "pooled_unbalanced": "success", + "pooled_hac_unsorted_unbalanced": "success", + "between_unbalanced": "success", + "first_difference_unbalanced": "success", + "panel_entity_unbalanced": "success", + "random_effects_unbalanced": "success", + "random_effects_explicit_constant_unbalanced": "success", + "fama_macbeth_unbalanced": "success" + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + } + } + }, + "torch": { + "status": "success", + "executed_backend": "torch", + "model_cases": { + "pooled_balanced": "success", + "pooled_hac_unsorted_balanced": "success", + "between_balanced": "success", + "first_difference_balanced": "success", + "panel_entity_balanced": "success", + "random_effects_balanced": "success", + "random_effects_explicit_constant_balanced": "success", + "fama_macbeth_balanced": "success", + "panel_two_way_balanced": "success", + "pooled_unbalanced": "success", + "pooled_hac_unsorted_unbalanced": "success", + "between_unbalanced": "success", + "first_difference_unbalanced": "success", + "panel_entity_unbalanced": "success", + "random_effects_unbalanced": "success", + "random_effects_explicit_constant_unbalanced": "success", + "fama_macbeth_unbalanced": "success" + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + } + } + } + } +} diff --git a/results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json b/results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json new file mode 100644 index 000000000..8a38e5e94 --- /dev/null +++ b/results/benchmark_frontend_sources/panel_stage_b_pr122_p100_20260809_2701aa9f.json @@ -0,0 +1,545 @@ +{ + "method": "Panel Tier-1 Stage B physical GPU validation", + "source_schema_version": "1.0", + "source_date": "2026-08-09", + "git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true, + "status": "success", + "schema_status": "ok", + "raw_artifact": { + "path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "repository_commit": "0d0d654d825cea872672f27d02107a58048b345f", + "git_blob_sha": "fa3a253e6d882a4e69be29e7e3b1dce7b223b9a9", + "generated_at": "2026-08-09T14:06:57.406080Z", + "schema_version": 2 + }, + "focused_artifact": { + "path": "results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json", + "repository_commit": "72b3279d2028e8ec2af30e138e123aceb611ae8c", + "git_blob_sha": "3bda0b2040479ba8201e2722eb990ba086c3f3b9", + "generated_at": "2026-08-09T03:37:05.589811+00:00", + "schema_version": 1, + "validation": "panel_stage_b_disconnected_two_way_fe", + "status": "success", + "measurement_git_sha": "a57efcea29b0e87ecb89865c5a6902d5773812c6", + "role": "historical supplementary evidence for unchanged disconnected-FE numerical path" + }, + "environment": { + "env_id": "remote-p100-pr122-20260809", + "gpu": "Tesla P100-SXM2-16GB", + "cpu": "x86_64", + "platform": "Linux-3.10.0-1062.el7.x86_64-x86_64-with-glibc2.31", + "python": "3.9.16", + "packages": { + "statgpu": "0.2.4", + "numpy": "1.24.2", + "scipy": "1.10.1", + "cupy": null, + "torch": "2.0.0" + }, + "available_backends": [ + "cupy", + "torch" + ] + }, + "protocol": { + "runner": "dev/benchmarks/validate_panel_stage_b_gpu.py", + "focused_runner": "dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py", + "runner_schema_version": 2, + "focused_runner_schema_version": 1, + "reference_backend": "numpy", + "dtype": "float64", + "timing_collected": false, + "failure_policy": "fail_on_backend_mismatch_or_precision_regression", + "rtol": 5e-06, + "atol": 5e-07 + }, + "backend_times": { + "numpy": null, + "cupy": null, + "torch": null + }, + "external_baseline": { + "name": "statgpu NumPy reference", + "time": null, + "version": null + }, + "precision_vs_external": {}, + "convergence_status": {}, + "backend_precision": { + "cupy": "pass", + "torch": "pass" + }, + "compatibility_matrix": { + "cupy": { + "model_cases": "17/17", + "diagnostics": "5/5", + "executed_backend": "cupy", + "cpu_fallback": false, + "disconnected_fe": "pass" + }, + "torch": { + "model_cases": "17/17", + "diagnostics": "5/5", + "executed_backend": "torch", + "cpu_fallback": false, + "disconnected_fe": "pass" + } + }, + "inference_matrix": { + "status": "pass", + "fields": [ + "coef", + "bse", + "tvalues", + "pvalues", + "conf_int", + "nobs", + "df_resid" + ], + "reference": "statgpu NumPy", + "reported_precision": "Exact clean-head P100 Stage-B matrix passed validator tolerances on CuPy/Torch. The dedicated nonzero-effect Hausman fixture was applicable on both physical GPU backends with df=1 and statistic/p-value differences versus NumPy below 1.1e-13 and 2.2e-14, respectively. The historical focused disconnected two-way FE artifact remains supplementary evidence for the unchanged df_resid=1/effect_rank=7/incidence_components=3 inference boundary." + }, + "validation_tier": "remote-full", + "timing_scope": { + "collected": false, + "reason": "Correctness/backend-provenance validation only; no benchmark timing was measured." + }, + "reproducibility": { + "exact_git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true, + "runner": "dev/benchmarks/validate_panel_stage_b_gpu.py", + "focused_runner": "dev/benchmarks/validate_panel_stage_b_disconnected_fe_gpu.py", + "raw_artifact_path": "results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json", + "raw_artifact_git_blob_sha": "fa3a253e6d882a4e69be29e7e3b1dce7b223b9a9", + "repository_commit": "0d0d654d825cea872672f27d02107a58048b345f", + "focused_artifact_path": "results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json", + "focused_artifact_git_blob_sha": "3bda0b2040479ba8201e2722eb990ba086c3f3b9", + "focused_measurement_git_sha": "a57efcea29b0e87ecb89865c5a6902d5773812c6", + "focused_repository_commit": "72b3279d2028e8ec2af30e138e123aceb611ae8c" + }, + "uncovered_reasons": [ + "Performance timing was not collected by this correctness-only physical validation and must not be inferred from this source.", + "The four standard/explicit-RE-constant Hausman datasets remain structured inapplicable because their covariance differences are materially indefinite; a separate nonzero-effect fitted fixture physically exercises the applicable statistic/p-value/df branch on both GPU backends.", + "The runner could not resolve an installed CuPy distribution version through importlib.metadata, so the source records the CuPy package version as null while executed_backend='cupy' proves physical backend execution.", + "The focused disconnected-FE artifact was measured earlier at a57efcea and is retained only as supplementary validation evidence for an unchanged numerical path; it is not represented as a 2701aa9f measurement or expanded into timing rows." + ], + "case_catalog": [ + { + "case_id": "pooled_balanced", + "model_id": "PooledOLS", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "pooled_hac_unsorted_balanced", + "model_id": "PooledOLS", + "variant": "balanced-hac-unsorted", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "metadata_sort_alignment", + "backend_provenance" + ] + }, + { + "case_id": "between_balanced", + "model_id": "BetweenOLS", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "first_difference_balanced", + "model_id": "FirstDifferenceOLS", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "panel_entity_balanced", + "model_id": "PanelOLS", + "variant": "entity-fe-balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "pooling_f", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_balanced", + "model_id": "RandomEffects", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_explicit_constant_balanced", + "model_id": "RandomEffects", + "variant": "explicit-constant-balanced", + "n_samples": 54, + "n_features": 3, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "fama_macbeth_balanced", + "model_id": "FamaMacBeth", + "variant": "balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "panel_two_way_balanced", + "model_id": "PanelOLS", + "variant": "two-way-fe-balanced", + "n_samples": 54, + "n_features": 2, + "checks": [ + "fit_statistics", + "pooling_f", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "pooled_unbalanced", + "model_id": "PooledOLS", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "pooled_hac_unsorted_unbalanced", + "model_id": "PooledOLS", + "variant": "unbalanced-hac-unsorted", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "bp_lm", + "diagnostic_covariance", + "stage_a_inference", + "metadata_sort_alignment", + "backend_provenance" + ] + }, + { + "case_id": "between_unbalanced", + "model_id": "BetweenOLS", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "first_difference_unbalanced", + "model_id": "FirstDifferenceOLS", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "panel_entity_unbalanced", + "model_id": "PanelOLS", + "variant": "entity-fe-unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "pooling_f", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_unbalanced", + "model_id": "RandomEffects", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "random_effects_explicit_constant_unbalanced", + "model_id": "RandomEffects", + "variant": "explicit-constant-unbalanced", + "n_samples": 49, + "n_features": 3, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "random_effects_diagnostic_contract", + "backend_provenance" + ] + }, + { + "case_id": "fama_macbeth_unbalanced", + "model_id": "FamaMacBeth", + "variant": "unbalanced", + "n_samples": 49, + "n_features": 2, + "checks": [ + "fit_statistics", + "diagnostic_covariance", + "stage_a_inference", + "backend_provenance" + ] + } + ], + "backend_results": { + "cupy": { + "status": "success", + "executed_backend": "cupy", + "model_cases": { + "pooled_balanced": "success", + "pooled_hac_unsorted_balanced": "success", + "between_balanced": "success", + "first_difference_balanced": "success", + "panel_entity_balanced": "success", + "random_effects_balanced": "success", + "random_effects_explicit_constant_balanced": "success", + "fama_macbeth_balanced": "success", + "panel_two_way_balanced": "success", + "pooled_unbalanced": "success", + "pooled_hac_unsorted_unbalanced": "success", + "between_unbalanced": "success", + "first_difference_unbalanced": "success", + "panel_entity_unbalanced": "success", + "random_effects_unbalanced": "success", + "random_effects_explicit_constant_unbalanced": "success", + "fama_macbeth_unbalanced": "success" + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_applicable_nonzero_effect": { + "status": "success", + "max_abs_differences": { + "statistic": 7.260858581048524e-14, + "pvalue": 1.454392162258955e-14 + }, + "fit_max_abs_differences": { + "fe_coef": 0.0, + "re_coef": 2.220446049250313e-16, + "fe_covariance": 0.0, + "re_covariance": 5.421010862427522e-20 + }, + "applicable": true, + "reason": null, + "statistic": 1.1965942530851057, + "pvalue": 0.27400344142676447, + "df": 1.0, + "reference_statistic": 1.196594253085033, + "reference_pvalue": 0.274003441426779, + "reference_df": 1.0, + "reference_variance_difference": 4.6413153162319366e-05, + "fixture": { + "seed": 20260810, + "n_entities": 12, + "n_times": 4, + "entity_effect_scale": 0.005, + "noise_scale": 0.1 + } + } + } + }, + "torch": { + "status": "success", + "executed_backend": "torch", + "model_cases": { + "pooled_balanced": "success", + "pooled_hac_unsorted_balanced": "success", + "between_balanced": "success", + "first_difference_balanced": "success", + "panel_entity_balanced": "success", + "random_effects_balanced": "success", + "random_effects_explicit_constant_balanced": "success", + "fama_macbeth_balanced": "success", + "panel_two_way_balanced": "success", + "pooled_unbalanced": "success", + "pooled_hac_unsorted_unbalanced": "success", + "between_unbalanced": "success", + "first_difference_unbalanced": "success", + "panel_entity_unbalanced": "success", + "random_effects_unbalanced": "success", + "random_effects_explicit_constant_unbalanced": "success", + "fama_macbeth_unbalanced": "success" + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_applicable_nonzero_effect": { + "status": "success", + "max_abs_differences": { + "statistic": 1.092459456231154e-13, + "pvalue": 2.1871393585115584e-14 + }, + "fit_max_abs_differences": { + "fe_coef": 0.0, + "re_coef": 3.3306690738754696e-16, + "fe_covariance": 0.0, + "re_covariance": 8.131516293641283e-20 + }, + "applicable": true, + "reason": null, + "statistic": 1.1965942530849238, + "pvalue": 0.2740034414268009, + "df": 1.0, + "reference_statistic": 1.196594253085033, + "reference_pvalue": 0.274003441426779, + "reference_df": 1.0, + "reference_variance_difference": 4.6413153162319366e-05, + "fixture": { + "seed": 20260810, + "n_entities": 12, + "n_times": 4, + "entity_effect_scale": 0.005, + "noise_scale": 0.1 + } + } + } + } + } +} diff --git a/results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json b/results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json new file mode 100644 index 000000000..3bda0b204 --- /dev/null +++ b/results/pr122_p100/panel_stage_b_disconnected_fe_gpu_validation_a57efcea.json @@ -0,0 +1,246 @@ +{ + "backend_results": { + "cupy": { + "differences_vs_numpy": { + "bse": 0.0, + "coef": 2.220446049250313e-16, + "conf_int": 8.881784197001252e-16, + "fit_statistics.f_pvalue": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_within": 0.0, + "pooling_f.pvalue": 0.0, + "pooling_f.statistic": 0.0, + "pvalues": 0.0, + "tvalues": 4.440892098500626e-16 + }, + "executed_backend": "cupy", + "snapshot": { + "bse": [ + 0.5 + ], + "coef": [ + 1.5 + ], + "conf_int": [ + [ + -4.853102368087348, + 7.853102368087348 + ] + ], + "df_resid": 1, + "diagnostic_df": { + "df_resid": 1, + "df_total": 2, + "effect_rank": 7, + "incidence_components": 3, + "rank_x": 1 + }, + "fit_statistics": { + "f_df": [ + 1.0, + 1.0 + ], + "f_pvalue": 0.2048327646991338, + "f_statistic": 9.0, + "rsquared_adj": 0.8, + "rsquared_between": 0.0, + "rsquared_overall": 0.9, + "rsquared_within": 0.9 + }, + "legacy_df_resid": 0, + "nobs": 9, + "pooling_f": { + "applicable": true, + "df": [ + 6.0, + 1.0 + ], + "distribution": "F", + "pvalue": 1.0, + "reason": null, + "statistic": 0.0 + }, + "public_df_resid_basis": "component-aware", + "pvalues": [ + 0.20483276469913347 + ], + "rsquared_within": 0.9, + "tvalues": [ + 3.0 + ] + }, + "status": "success" + }, + "torch": { + "differences_vs_numpy": { + "bse": 0.0, + "coef": 4.440892098500626e-16, + "conf_int": 8.881784197001252e-16, + "fit_statistics.f_pvalue": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_within": 0.0, + "pooling_f.pvalue": 0.0, + "pooling_f.statistic": 0.0, + "pvalues": 0.0, + "tvalues": 8.881784197001252e-16 + }, + "executed_backend": "torch", + "snapshot": { + "bse": [ + 0.5 + ], + "coef": [ + 1.5000000000000002 + ], + "conf_int": [ + [ + -4.853102368087348, + 7.853102368087348 + ] + ], + "df_resid": 1, + "diagnostic_df": { + "df_resid": 1, + "df_total": 2, + "effect_rank": 7, + "incidence_components": 3, + "rank_x": 1 + }, + "fit_statistics": { + "f_df": [ + 1.0, + 1.0 + ], + "f_pvalue": 0.2048327646991338, + "f_statistic": 9.0, + "rsquared_adj": 0.8, + "rsquared_between": 0.0, + "rsquared_overall": 0.9, + "rsquared_within": 0.9 + }, + "legacy_df_resid": 0, + "nobs": 9, + "pooling_f": { + "applicable": true, + "df": [ + 6.0, + 1.0 + ], + "distribution": "F", + "pvalue": 1.0, + "reason": null, + "statistic": 0.0 + }, + "public_df_resid_basis": "component-aware", + "pvalues": [ + 0.20483276469913347 + ], + "rsquared_within": 0.9, + "tvalues": [ + 3.0000000000000004 + ] + }, + "status": "success" + } + }, + "environment": { + "gpu": "Tesla P100-SXM2-16GB", + "packages": { + "cupy": null, + "numpy": "1.24.2", + "scipy": "1.10.1", + "statgpu": "0.2.4", + "torch": "2.0.0" + }, + "platform": "Linux-3.10.0-1062.el7.x86_64-x86_64-with-glibc2.31", + "python": "3.9.16" + }, + "generated_at": "2026-08-09T03:37:05.589811+00:00", + "git_sha": "a57efcea29b0e87ecb89865c5a6902d5773812c6", + "protocol": { + "atol": 5e-07, + "expected_rank_contract": { + "component_aware_df_resid": 1, + "effect_rank": 7, + "incidence_components": 3, + "legacy_df_resid": 0, + "n_entities": 5, + "n_times": 5, + "nobs": 9, + "rank_x": 1 + }, + "fixture": "two disconnected 2x2 blocks plus one singleton", + "reference_backend": "numpy", + "requested_backends": [ + "cupy", + "torch" + ], + "rtol": 5e-06, + "timing_collected": false + }, + "reference": { + "bse": [ + 0.5 + ], + "coef": [ + 1.4999999999999998 + ], + "conf_int": [ + [ + -4.853102368087349, + 7.853102368087349 + ] + ], + "df_resid": 1, + "diagnostic_df": { + "df_resid": 1, + "df_total": 2, + "effect_rank": 7, + "incidence_components": 3, + "rank_x": 1 + }, + "fit_statistics": { + "f_df": [ + 1.0, + 1.0 + ], + "f_pvalue": 0.2048327646991338, + "f_statistic": 9.0, + "rsquared_adj": 0.8, + "rsquared_between": 0.0, + "rsquared_overall": 0.9, + "rsquared_within": 0.9 + }, + "legacy_df_resid": 0, + "nobs": 9, + "pooling_f": { + "applicable": true, + "df": [ + 6.0, + 1.0 + ], + "distribution": "F", + "pvalue": 1.0, + "reason": null, + "statistic": 0.0 + }, + "public_df_resid_basis": "component-aware", + "pvalues": [ + 0.20483276469913347 + ], + "rsquared_within": 0.9, + "tvalues": [ + 2.9999999999999996 + ] + }, + "schema_version": 1, + "status": "success", + "validation": "panel_stage_b_disconnected_two_way_fe", + "working_tree_clean": true +} \ No newline at end of file diff --git a/results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json b/results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json new file mode 100644 index 000000000..fa3a253e6 --- /dev/null +++ b/results/pr122_p100/panel_stage_b_gpu_validation_2701aa9f.json @@ -0,0 +1,896 @@ +{ + "schema_version": 2, + "generated_at": "2026-08-09T14:06:57.406080Z", + "git_sha": "2701aa9feb3796c33c94e6480fcb78c80c6a809c", + "working_tree_clean": true, + "status": "success", + "environment": { + "python": "3.9.16", + "platform": "Linux-3.10.0-1062.el7.x86_64-x86_64-with-glibc2.31", + "gpu": "Tesla P100-SXM2-16GB", + "packages": { + "statgpu": "0.2.4", + "numpy": "1.24.2", + "scipy": "1.10.1", + "cupy": null, + "torch": "2.0.0" + } + }, + "tolerances": { + "rtol": 5e-06, + "atol": 5e-07 + }, + "datasets": { + "balanced": { + "nobs": 54 + }, + "unbalanced": { + "nobs": 49 + }, + "hausman_applicable_nonzero_effect": { + "nobs": 48, + "seed": 20260810, + "n_entities": 12, + "n_times": 4, + "entity_effect_scale": 0.005, + "noise_scale": 0.1, + "reference_variance_difference": 4.6413153162319366e-05 + } + }, + "backends": { + "cupy": { + "models": { + "pooled_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 4.440892098500626e-15, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 7.771561172376096e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 1.4210854715202004e-13, + "bp_lm.pvalue": 1.0908587575630828e-35 + } + }, + "pooled_hac_unsorted_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 1.0658141036401503e-14, + "pvalues": 1.1964340379966992e-20, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.469446951953614e-18, + "bp_lm.statistic": 7.105427357601002e-14, + "bp_lm.pvalue": 4.8430367253878245e-36 + } + }, + "between_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 5.551115123125783e-17, + "tvalues": 4.440892098500626e-16, + "pvalues": 2.7755575615628914e-16, + "conf_int": 1.6653345369377348e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 3.3306690738754696e-16, + "fit_statistics.rsquared_adj": 1.1102230246251565e-16, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.7755575615628914e-17 + } + }, + "first_difference_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 6.938893903907228e-18, + "tvalues": 0.0, + "pvalues": 1.9665546849844295e-156, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080801e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.336808689942018e-19 + } + }, + "panel_entity_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 0.0, + "bse": 6.938893903907228e-18, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.336808689942018e-19, + "pooling_f.statistic": 2.1316282072803006e-14, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 8.673617379884035e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 9.71445146547012e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 4.440892098500626e-16, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 5.421010862427522e-18, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + }, + "panel_two_way_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.440892098500626e-16, + "bse": 0.0, + "tvalues": 1.0658141036401503e-14, + "pvalues": 0.0, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0657581468206416e-20, + "pooling_f.statistic": 0.0, + "pooling_f.pvalue": 0.0 + } + }, + "pooled_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 1.9984014443252818e-15, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.0842021724855044e-19, + "bp_lm.statistic": 2.842170943040401e-14, + "bp_lm.pvalue": 1.7333369499485123e-33 + } + }, + "pooled_hac_unsorted_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.996003610813204e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 5.995204332975845e-15, + "pvalues": 3.3306690738754696e-15, + "conf_int": 5.551115123125783e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.842170943040401e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0115480381963664e-18, + "bp_lm.statistic": 0.0, + "bp_lm.pvalue": 0.0 + } + }, + "between_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 2.220446049250313e-16, + "pvalues": 1.1102230246251565e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 2.220446049250313e-16, + "fit_statistics.f_statistic": 7.771561172376096e-16, + "fit_statistics.f_pvalue": 3.3306690738754696e-16, + "diagnostic_covariance": 5.551115123125783e-17 + } + }, + "first_difference_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 0.0, + "bse": 3.469446951953614e-18, + "tvalues": 7.105427357601002e-15, + "pvalues": 6.9283089726601085e-121, + "conf_int": 0.0, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.0842021724855044e-19 + } + }, + "panel_entity_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 6.938893903907228e-18, + "tvalues": 0.0, + "pvalues": 0.0, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.2737367544323206e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.2526065174565133e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 6.938893903907228e-18, + "tvalues": 5.329070518200751e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.2737367544323206e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.163336342344337e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 8.326672684688674e-17, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.469446951953614e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 5.551115123125783e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 2.220446049250313e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + } + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_applicable_nonzero_effect": { + "status": "success", + "max_abs_differences": { + "statistic": 7.260858581048524e-14, + "pvalue": 1.454392162258955e-14 + }, + "fit_max_abs_differences": { + "fe_coef": 0.0, + "re_coef": 2.220446049250313e-16, + "fe_covariance": 0.0, + "re_covariance": 5.421010862427522e-20 + }, + "applicable": true, + "reason": null, + "statistic": 1.1965942530851057, + "pvalue": 0.27400344142676447, + "df": 1.0, + "reference_statistic": 1.196594253085033, + "reference_pvalue": 0.274003441426779, + "reference_df": 1.0, + "reference_variance_difference": 4.6413153162319366e-05, + "fixture": { + "seed": 20260810, + "n_entities": 12, + "n_times": 4, + "entity_effect_scale": 0.005, + "noise_scale": 0.1 + } + } + } + }, + "torch": { + "models": { + "pooled_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.661338147750939e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 8.881784197001252e-15, + "pvalues": 1.1275609692606992e-06, + "conf_int": 1.6652982326448296e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 2.220446049250313e-16, + "fit_statistics.rsquared_between": 8.881784197001252e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 2.4158453015843406e-13, + "bp_lm.pvalue": 1.9325127127518407e-35 + } + }, + "pooled_hac_unsorted_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.661338147750939e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 1.0658141036401503e-14, + "pvalues": 1.7470054537119944e-20, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.903127820947816e-18, + "bp_lm.statistic": 1.7053025658242404e-13, + "bp_lm.pvalue": 1.3259576277275403e-35 + } + }, + "between_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 1.1102230246251565e-16, + "tvalues": 6.661338147750939e-16, + "pvalues": 2.7755575615628914e-16, + "conf_int": 1.6653345369377348e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 3.3306690738754696e-16, + "fit_statistics.f_statistic": 1.7763568394002505e-15, + "fit_statistics.f_pvalue": 3.3306690738754696e-16, + "diagnostic_covariance": 5.551115123125783e-17 + } + }, + "first_difference_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 0.0, + "bse": 1.3877787807814457e-17, + "tvalues": 1.4210854715202004e-14, + "pvalues": 8.539945807941643e-156, + "conf_int": 5.551115123125783e-17, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 3.410605131648481e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 7.589415207398531e-19 + } + }, + "panel_entity_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 3.469446951953614e-18, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 4.719586943480181e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 6.353757520116687e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.3877787807814457e-16, + "bse": 0.0, + "tvalues": 1.3322676295501878e-15, + "pvalues": 2.841572954714877e-08, + "conf_int": 1.8457834705110088e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 1.1102230246251565e-16, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 1.7763568394002505e-15, + "pvalues": 3.2526065174565133e-18, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + }, + "panel_two_way_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 3.3306690738754696e-16, + "bse": 0.0, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 4.125351171779812e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0657581468206416e-20, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "pooled_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 4.0245584642661925e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 6.327377473080986e-07, + "conf_int": 1.3248322994208195e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 9.947598300641403e-14, + "bp_lm.pvalue": 6.0426052005149525e-33 + } + }, + "pooled_hac_unsorted_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.106226635438361e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 9.2148511043888e-15, + "pvalues": 4.9960036108132044e-15, + "conf_int": 6.38378239159465e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.842170943040401e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 5.366800753803247e-18, + "bp_lm.statistic": 2.842170943040401e-14, + "bp_lm.pvalue": 1.7333369499485123e-33 + } + }, + "between_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 0.0, + "tvalues": 2.220446049250313e-16, + "pvalues": 1.1102230246251565e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 4.440892098500626e-16, + "fit_statistics.f_statistic": 1.4432899320127035e-15, + "fit_statistics.f_pvalue": 5.551115123125783e-16, + "diagnostic_covariance": 6.938893903907228e-17 + } + }, + "first_difference_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 0.0, + "bse": 6.938893903907228e-18, + "tvalues": 2.1316282072803006e-14, + "pvalues": 6.9283089726601085e-121, + "conf_int": 0.0, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.710505431213761e-19 + } + }, + "panel_entity_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 3.469446951953614e-18, + "tvalues": 1.4210854715202004e-14, + "pvalues": 0.0, + "conf_int": 2.8987834355120867e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 3.410605131648481e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 5.262852376120009e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 0.0, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 1.4210854715202004e-14, + "pvalues": 2.1985840481519858e-08, + "conf_int": 1.818750788862289e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 4.440892098500626e-16, + "bse": 8.326672684688674e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 1.6263032587282567e-19, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + } + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_applicable_nonzero_effect": { + "status": "success", + "max_abs_differences": { + "statistic": 1.092459456231154e-13, + "pvalue": 2.1871393585115584e-14 + }, + "fit_max_abs_differences": { + "fe_coef": 0.0, + "re_coef": 3.3306690738754696e-16, + "fe_covariance": 0.0, + "re_covariance": 8.131516293641283e-20 + }, + "applicable": true, + "reason": null, + "statistic": 1.1965942530849238, + "pvalue": 0.2740034414268009, + "df": 1.0, + "reference_statistic": 1.196594253085033, + "reference_pvalue": 0.274003441426779, + "reference_df": 1.0, + "reference_variance_difference": 4.6413153162319366e-05, + "fixture": { + "seed": 20260810, + "n_entities": 12, + "n_times": 4, + "entity_effect_scale": 0.005, + "noise_scale": 0.1 + } + } + } + } + } +} diff --git a/results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json b/results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json new file mode 100644 index 000000000..254b64776 --- /dev/null +++ b/results/pr122_p100/panel_stage_b_gpu_validation_a57efcea.json @@ -0,0 +1,829 @@ +{ + "schema_version": 2, + "generated_at": "2026-08-09T03:37:01.897253Z", + "git_sha": "a57efcea29b0e87ecb89865c5a6902d5773812c6", + "working_tree_clean": true, + "status": "success", + "environment": { + "python": "3.9.16", + "platform": "Linux-3.10.0-1062.el7.x86_64-x86_64-with-glibc2.31", + "gpu": "Tesla P100-SXM2-16GB", + "packages": { + "statgpu": "0.2.4", + "numpy": "1.24.2", + "scipy": "1.10.1", + "cupy": null, + "torch": "2.0.0" + } + }, + "tolerances": { + "rtol": 5e-06, + "atol": 5e-07 + }, + "datasets": { + "balanced": { + "nobs": 54 + }, + "unbalanced": { + "nobs": 49 + } + }, + "backends": { + "cupy": { + "models": { + "pooled_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 4.440892098500626e-15, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 7.771561172376096e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 1.4210854715202004e-13, + "bp_lm.pvalue": 1.0908587575630828e-35 + } + }, + "pooled_hac_unsorted_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 1.0658141036401503e-14, + "pvalues": 1.1964340379966992e-20, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.469446951953614e-18, + "bp_lm.statistic": 7.105427357601002e-14, + "bp_lm.pvalue": 4.8430367253878245e-36 + } + }, + "between_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 5.551115123125783e-17, + "tvalues": 4.440892098500626e-16, + "pvalues": 2.7755575615628914e-16, + "conf_int": 1.6653345369377348e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 3.3306690738754696e-16, + "fit_statistics.rsquared_adj": 1.1102230246251565e-16, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.7755575615628914e-17 + } + }, + "first_difference_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 6.938893903907228e-18, + "tvalues": 0.0, + "pvalues": 1.9665546849844295e-156, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080801e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.336808689942018e-19 + } + }, + "panel_entity_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 0.0, + "bse": 6.938893903907228e-18, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.336808689942018e-19, + "pooling_f.statistic": 2.1316282072803006e-14, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 8.673617379884035e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 9.71445146547012e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 4.440892098500626e-16, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 5.421010862427522e-18, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + }, + "panel_two_way_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 3.3306690738754696e-16, + "bse": 0.0, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 3.3306690738754696e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0657581468206416e-20, + "pooling_f.statistic": 0.0, + "pooling_f.pvalue": 0.0 + } + }, + "pooled_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 1.9984014443252818e-15, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.0842021724855044e-19, + "bp_lm.statistic": 2.842170943040401e-14, + "bp_lm.pvalue": 1.7333369499485123e-33 + } + }, + "pooled_hac_unsorted_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.996003610813204e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 5.995204332975845e-15, + "pvalues": 3.3306690738754696e-15, + "conf_int": 5.551115123125783e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.842170943040401e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0115480381963664e-18, + "bp_lm.statistic": 0.0, + "bp_lm.pvalue": 0.0 + } + }, + "between_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 2.220446049250313e-16, + "pvalues": 1.1102230246251565e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 2.220446049250313e-16, + "fit_statistics.f_statistic": 7.771561172376096e-16, + "fit_statistics.f_pvalue": 3.3306690738754696e-16, + "diagnostic_covariance": 5.551115123125783e-17 + } + }, + "first_difference_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 0.0, + "bse": 3.469446951953614e-18, + "tvalues": 7.105427357601002e-15, + "pvalues": 6.9283089726601085e-121, + "conf_int": 0.0, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.0842021724855044e-19 + } + }, + "panel_entity_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 6.938893903907228e-18, + "tvalues": 0.0, + "pvalues": 0.0, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.2737367544323206e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.2526065174565133e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 6.938893903907228e-18, + "tvalues": 5.329070518200751e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.2737367544323206e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.163336342344337e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 8.326672684688674e-17, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.469446951953614e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 5.551115123125783e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 2.220446049250313e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + } + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + } + } + }, + "torch": { + "models": { + "pooled_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.661338147750939e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 8.881784197001252e-15, + "pvalues": 1.1275609692606992e-06, + "conf_int": 1.6652982326448296e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 2.220446049250313e-16, + "fit_statistics.rsquared_between": 8.881784197001252e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 2.4158453015843406e-13, + "bp_lm.pvalue": 1.9325127127518407e-35 + } + }, + "pooled_hac_unsorted_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.661338147750939e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 1.0658141036401503e-14, + "pvalues": 1.7470054537119944e-20, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.903127820947816e-18, + "bp_lm.statistic": 1.7053025658242404e-13, + "bp_lm.pvalue": 1.3259576277275403e-35 + } + }, + "between_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 1.1102230246251565e-16, + "tvalues": 6.661338147750939e-16, + "pvalues": 2.7755575615628914e-16, + "conf_int": 1.6653345369377348e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 3.3306690738754696e-16, + "fit_statistics.f_statistic": 1.7763568394002505e-15, + "fit_statistics.f_pvalue": 3.3306690738754696e-16, + "diagnostic_covariance": 5.551115123125783e-17 + } + }, + "first_difference_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 0.0, + "bse": 1.3877787807814457e-17, + "tvalues": 1.4210854715202004e-14, + "pvalues": 8.539945807941643e-156, + "conf_int": 5.551115123125783e-17, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 3.410605131648481e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 7.589415207398531e-19 + } + }, + "panel_entity_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 3.469446951953614e-18, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 4.719586943480181e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 6.353757520116687e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.3877787807814457e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 2.841572954714877e-08, + "conf_int": 1.8457834149998575e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 4.440892098500626e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 1.1102230246251565e-16, + "fit_statistics.f_statistic": 1.7053025658242404e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 1.7763568394002505e-15, + "pvalues": 3.2526065174565133e-18, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + }, + "panel_two_way_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 3.3306690738754696e-16, + "bse": 6.938893903907228e-18, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 4.125351171779812e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 1.4210854715202004e-14, + "pooling_f.pvalue": 0.0 + } + }, + "pooled_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 4.0245584642661925e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 6.327377473080986e-07, + "conf_int": 1.3248322994208195e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 9.947598300641403e-14, + "bp_lm.pvalue": 6.0426052005149525e-33 + } + }, + "pooled_hac_unsorted_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.106226635438361e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 9.2148511043888e-15, + "pvalues": 4.9960036108132044e-15, + "conf_int": 6.38378239159465e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.842170943040401e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 5.366800753803247e-18, + "bp_lm.statistic": 2.842170943040401e-14, + "bp_lm.pvalue": 1.7333369499485123e-33 + } + }, + "between_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 0.0, + "tvalues": 2.220446049250313e-16, + "pvalues": 1.1102230246251565e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 4.440892098500626e-16, + "fit_statistics.f_statistic": 1.4432899320127035e-15, + "fit_statistics.f_pvalue": 5.551115123125783e-16, + "diagnostic_covariance": 6.938893903907228e-17 + } + }, + "first_difference_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 0.0, + "bse": 6.938893903907228e-18, + "tvalues": 2.1316282072803006e-14, + "pvalues": 6.9283089726601085e-121, + "conf_int": 0.0, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.710505431213761e-19 + } + }, + "panel_entity_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 3.469446951953614e-18, + "tvalues": 1.4210854715202004e-14, + "pvalues": 0.0, + "conf_int": 2.8987834355120867e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 3.410605131648481e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 5.262852376120009e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 0.0, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 1.4210854715202004e-14, + "pvalues": 2.1985840481519858e-08, + "conf_int": 1.818750788862289e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 4.440892098500626e-16, + "bse": 8.326672684688674e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 1.6263032587282567e-19, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + } + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + } + } + } + } +} diff --git a/results/pr122_p100/panel_stage_b_gpu_validation_faa95ce7.json b/results/pr122_p100/panel_stage_b_gpu_validation_faa95ce7.json new file mode 100644 index 000000000..f525ce5c8 --- /dev/null +++ b/results/pr122_p100/panel_stage_b_gpu_validation_faa95ce7.json @@ -0,0 +1,829 @@ +{ + "schema_version": 2, + "generated_at": "2026-08-08T14:49:33.405711Z", + "git_sha": "faa95ce7fb5cb204088957fbda5544c20a06fbfc", + "working_tree_clean": true, + "status": "success", + "environment": { + "python": "3.9.16", + "platform": "Linux-3.10.0-1062.el7.x86_64-x86_64-with-glibc2.31", + "gpu": "Tesla P100-SXM2-16GB", + "packages": { + "statgpu": "0.2.4", + "numpy": "1.24.2", + "scipy": "1.10.1", + "cupy": null, + "torch": "2.0.0" + } + }, + "tolerances": { + "rtol": 5e-06, + "atol": 5e-07 + }, + "datasets": { + "balanced": { + "nobs": 54 + }, + "unbalanced": { + "nobs": 49 + } + }, + "backends": { + "cupy": { + "models": { + "pooled_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 4.440892098500626e-15, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 7.771561172376096e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 1.4210854715202004e-13, + "bp_lm.pvalue": 1.0908587575630828e-35 + } + }, + "pooled_hac_unsorted_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 1.0658141036401503e-14, + "pvalues": 1.1964340379966992e-20, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.469446951953614e-18, + "bp_lm.statistic": 7.105427357601002e-14, + "bp_lm.pvalue": 4.8430367253878245e-36 + } + }, + "between_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 5.551115123125783e-17, + "tvalues": 4.440892098500626e-16, + "pvalues": 2.7755575615628914e-16, + "conf_int": 1.6653345369377348e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 3.3306690738754696e-16, + "fit_statistics.rsquared_adj": 1.1102230246251565e-16, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.7755575615628914e-17 + } + }, + "first_difference_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 6.938893903907228e-18, + "tvalues": 0.0, + "pvalues": 1.9665546849844295e-156, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080801e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.336808689942018e-19 + } + }, + "panel_entity_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 0.0, + "bse": 6.938893903907228e-18, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.336808689942018e-19, + "pooling_f.statistic": 2.1316282072803006e-14, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 8.673617379884035e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 9.71445146547012e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 4.440892098500626e-16, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 5.421010862427522e-18, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + }, + "panel_two_way_balanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 3.3306690738754696e-16, + "bse": 0.0, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 3.3306690738754696e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0657581468206416e-20, + "pooling_f.statistic": 0.0, + "pooling_f.pvalue": 0.0 + } + }, + "pooled_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 1.9984014443252818e-15, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.0842021724855044e-19, + "bp_lm.statistic": 2.842170943040401e-14, + "bp_lm.pvalue": 1.7333369499485123e-33 + } + }, + "pooled_hac_unsorted_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.996003610813204e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 5.995204332975845e-15, + "pvalues": 3.3306690738754696e-15, + "conf_int": 5.551115123125783e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.842170943040401e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 4.0115480381963664e-18, + "bp_lm.statistic": 0.0, + "bp_lm.pvalue": 0.0 + } + }, + "between_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 2.220446049250313e-16, + "pvalues": 1.1102230246251565e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 2.220446049250313e-16, + "fit_statistics.f_statistic": 7.771561172376096e-16, + "fit_statistics.f_pvalue": 3.3306690738754696e-16, + "diagnostic_covariance": 5.551115123125783e-17 + } + }, + "first_difference_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 0.0, + "bse": 3.469446951953614e-18, + "tvalues": 7.105427357601002e-15, + "pvalues": 6.9283089726601085e-121, + "conf_int": 0.0, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.0842021724855044e-19 + } + }, + "panel_entity_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 6.938893903907228e-18, + "tvalues": 0.0, + "pvalues": 0.0, + "conf_int": 2.220446049250313e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.2737367544323206e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.2526065174565133e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 6.938893903907228e-18, + "tvalues": 5.329070518200751e-15, + "pvalues": 0.0, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.2737367544323206e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 4.163336342344337e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 7.105427357601002e-15, + "pvalues": 0.0, + "conf_int": 8.326672684688674e-17, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.469446951953614e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_unbalanced": { + "status": "success", + "executed_backend": "cupy", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 5.551115123125783e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 2.220446049250313e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + } + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + } + } + }, + "torch": { + "models": { + "pooled_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.661338147750939e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 8.881784197001252e-15, + "pvalues": 1.1275609692606992e-06, + "conf_int": 1.6652982326448296e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-15, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 2.4158453015843406e-13, + "bp_lm.pvalue": 1.9325127127518407e-35 + } + }, + "pooled_hac_unsorted_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.661338147750939e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 1.0658141036401503e-14, + "pvalues": 1.7470054537119944e-20, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.4210854715202004e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 3.903127820947816e-18, + "bp_lm.statistic": 1.7053025658242404e-13, + "bp_lm.pvalue": 1.3259576277275403e-35 + } + }, + "between_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 1.6653345369377348e-16, + "tvalues": 8.881784197001252e-16, + "pvalues": 2.7755575615628914e-16, + "conf_int": 2.7755575615628914e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 2.220446049250313e-16, + "fit_statistics.f_statistic": 1.7763568394002505e-15, + "fit_statistics.f_pvalue": 3.3306690738754696e-16, + "diagnostic_covariance": 8.326672684688674e-17 + } + }, + "first_difference_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 0.0, + "bse": 1.3877787807814457e-17, + "tvalues": 1.4210854715202004e-14, + "pvalues": 8.539945807941643e-156, + "conf_int": 5.551115123125783e-17, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 3.410605131648481e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 7.589415207398531e-19 + } + }, + "panel_entity_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 3.469446951953614e-18, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 4.719586943480181e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 6.353757520116687e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.3877787807814457e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 3.552713678800501e-15, + "pvalues": 2.841572954714877e-08, + "conf_int": 1.8457834149998575e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 4.440892098500626e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 1.1102230246251565e-16, + "fit_statistics.f_statistic": 1.7053025658242404e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 5.551115123125783e-17, + "bse": 1.3877787807814457e-17, + "tvalues": 1.7763568394002505e-15, + "pvalues": 3.2526065174565133e-18, + "conf_int": 1.1102230246251565e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + }, + "panel_two_way_balanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 3.3306690738754696e-16, + "bse": 6.938893903907228e-18, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 4.125351171779812e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 5.684341886080802e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 1.4210854715202004e-14, + "pooling_f.pvalue": 0.0 + } + }, + "pooled_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 4.0245584642661925e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 6.327377473080986e-07, + "conf_int": 1.3248322994208195e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "bp_lm.statistic": 9.947598300641403e-14, + "bp_lm.pvalue": 6.0426052005149525e-33 + } + }, + "pooled_hac_unsorted_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 6.106226635438361e-16, + "bse": 2.7755575615628914e-17, + "tvalues": 9.2148511043888e-15, + "pvalues": 4.9960036108132044e-15, + "conf_int": 6.38378239159465e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 2.842170943040401e-14, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 5.366800753803247e-18, + "bp_lm.statistic": 2.842170943040401e-14, + "bp_lm.pvalue": 1.7333369499485123e-33 + } + }, + "between_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 0.0, + "tvalues": 2.220446049250313e-16, + "pvalues": 1.1102230246251565e-16, + "conf_int": 4.440892098500626e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 4.440892098500626e-16, + "fit_statistics.f_statistic": 1.4432899320127035e-15, + "fit_statistics.f_pvalue": 5.551115123125783e-16, + "diagnostic_covariance": 6.938893903907228e-17 + } + }, + "first_difference_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 0.0, + "bse": 6.938893903907228e-18, + "tvalues": 2.1316282072803006e-14, + "pvalues": 6.9283089726601085e-121, + "conf_int": 0.0, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.710505431213761e-19 + } + }, + "panel_entity_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 3.469446951953614e-18, + "tvalues": 1.4210854715202004e-14, + "pvalues": 0.0, + "conf_int": 2.8987834355120867e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 2.220446049250313e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 3.410605131648481e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 2.168404344971009e-19, + "pooling_f.statistic": 7.105427357601002e-15, + "pooling_f.pvalue": 0.0 + } + }, + "random_effects_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 1.1102230246251565e-16, + "bse": 0.0, + "tvalues": 3.552713678800501e-15, + "pvalues": 0.0, + "conf_int": 5.262852376120009e-10, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 0.0, + "fit_statistics.rsquared_overall": 0.0, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 0.0, + "random_effects_diagnostic_contract": 0.0 + } + }, + "random_effects_explicit_constant_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 2.220446049250313e-16, + "bse": 1.3877787807814457e-17, + "tvalues": 1.4210854715202004e-14, + "pvalues": 2.1985840481519858e-08, + "conf_int": 1.818750788862289e-09, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 0.0, + "fit_statistics.rsquared_between": 3.3306690738754696e-16, + "fit_statistics.rsquared_overall": 1.1102230246251565e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 1.1368683772161603e-13, + "fit_statistics.f_pvalue": 0.0, + "diagnostic_covariance": 1.734723475976807e-18, + "random_effects_diagnostic_contract": 0.0 + } + }, + "fama_macbeth_unbalanced": { + "status": "success", + "executed_backend": "torch", + "max_abs_differences": { + "coef": 4.440892098500626e-16, + "bse": 8.326672684688674e-17, + "tvalues": 5.329070518200751e-15, + "pvalues": 1.6263032587282567e-19, + "conf_int": 6.661338147750939e-16, + "nobs": 0.0, + "df_resid": 0.0, + "fit_statistics.rsquared_within": 1.1102230246251565e-16, + "fit_statistics.rsquared_between": 1.1102230246251565e-16, + "fit_statistics.rsquared_overall": 2.220446049250313e-16, + "fit_statistics.rsquared_adj": 0.0, + "fit_statistics.f_statistic": 0.0, + "fit_statistics.f_pvalue": 0.0 + } + } + }, + "diagnostics": { + "hausman_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_balanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + }, + "hausman_explicit_re_constant_unbalanced": { + "status": "success", + "max_abs_differences": { + "statistic": 0.0, + "pvalue": 0.0 + }, + "applicable": false, + "reason": "covariance difference is not positive semidefinite" + } + } + } + } +} diff --git a/statgpu/__init__.py b/statgpu/__init__.py index f5c736f02..9fcab6a11 100644 --- a/statgpu/__init__.py +++ b/statgpu/__init__.py @@ -54,6 +54,11 @@ BetweenOLS, FirstDifferenceOLS, FamaMacBeth, + PanelTestResult, + PanelFitStatistics, + hausman_test, + pooling_f_test, + breusch_pagan_lm_test, hac_covariance, ) from .backends import get_backend, NumpyBackend, CuPyBackend, TorchBackend @@ -180,6 +185,11 @@ "BetweenOLS", "FirstDifferenceOLS", "FamaMacBeth", + "PanelTestResult", + "PanelFitStatistics", + "hausman_test", + "pooling_f_test", + "breusch_pagan_lm_test", "hac_covariance", # Backends "get_backend", @@ -268,4 +278,4 @@ from ._base import refresh_public_finite_validation_contracts as _refresh_finite_contracts _refresh_finite_contracts() -del _refresh_finite_contracts +del _refresh_finite_contracts \ No newline at end of file diff --git a/statgpu/panel/__init__.py b/statgpu/panel/__init__.py index 34df10200..262f9e1c2 100644 --- a/statgpu/panel/__init__.py +++ b/statgpu/panel/__init__.py @@ -2,14 +2,16 @@ Panel data models with GPU acceleration. Provides fixed effects, random effects, pooled, between, first-difference, -and Fama-MacBeth estimators for panel/longitudinal data, along with -clustered and HAC covariance estimators. +and Fama-MacBeth estimators for panel/longitudinal data, shared structured +fit/test results, and panel covariance estimators. """ from ._fixed_effects import PanelOLS, FixedEffects from ._random_effects import RandomEffects, RandomEffectsOLS from ._covariance import clustered_covariance, two_way_clustered_covariance, hac_covariance from ._utils import PanelSummary +from ._results import PanelTestResult, PanelFitStatistics +from ._diagnostics import hausman_test, pooling_f_test, breusch_pagan_lm_test from ._pooled import PooledOLS from ._between import BetweenOLS from ._first_diff import FirstDifferenceOLS @@ -25,6 +27,11 @@ 'FirstDifferenceOLS', 'FamaMacBeth', 'PanelSummary', + 'PanelTestResult', + 'PanelFitStatistics', + 'hausman_test', + 'pooling_f_test', + 'breusch_pagan_lm_test', 'clustered_covariance', 'two_way_clustered_covariance', 'hac_covariance', diff --git a/statgpu/panel/_base.py b/statgpu/panel/_base.py index 1753de190..c7cbb96a2 100644 --- a/statgpu/panel/_base.py +++ b/statgpu/panel/_base.py @@ -96,6 +96,37 @@ def _panel_set_index_info(self, nobs, *, entity_ids=None, time_ids=None): self._panel_index_info = info return info + @property + def _panel_cov_params(self): + """Return the small covariance matrix used by Stage-B diagnostics. + + Existing Stage-A inference is computed and stored before this property is + consulted, so rescaling here cannot change public bse/t/p/CI values. + PanelOLS preserves a historical residual-df convention that is one rank + parameterization away from the standard fixed-effect model df used by + classical Hausman tests. When Stage-B fit metadata provides both the + legacy and standard diagnostic df, convert only this internal covariance + copy to the standard homoskedastic scale. + """ + raw = getattr(self, "_panel_cov_params_raw", None) + if raw is None: + return None + result = getattr(self, "fit_statistics_", None) + metadata = getattr(result, "metadata", {}) if result is not None else {} + diagnostic_df = metadata.get("diagnostic_df") + legacy_df = metadata.get("legacy_df_resid") + cov_type = str(getattr(self, "_cov_type", "nonrobust")).lower() + if ( + cov_type == "nonrobust" + and isinstance(diagnostic_df, dict) + and legacy_df is not None + ): + standard_df = diagnostic_df.get("df_resid") + if standard_df is not None and int(standard_df) > 0: + factor = float(legacy_df) / float(standard_df) + return raw * factor + return raw + def _panel_predict_linear( self, X, @@ -174,6 +205,14 @@ def _panel_store_ols_inference( allowed=allowed, hc1_correction=hc1_correction, ) + # Persist only the final k x k matrix. Observation-scale X/residual arrays + # remain on the selected backend. `_panel_cov_params` exposes the + # diagnostic version and may rescale this raw copy after fit metadata is + # available; public Stage-A inference values below always use cov_params. + self._panel_cov_params_raw = np.asarray( + _to_numpy(cov_params), dtype=np.float64 + ) + diag = xp.diag(cov_params) if diag_floor is not None: diag = xp_maximum(diag, float(diag_floor), xp) @@ -185,12 +224,21 @@ def _panel_store_ols_inference( tvalues_dev = params / denominator dist_name = "t" if str(cov_type).lower() == "nonrobust" else "norm" - distribution = get_distribution(dist_name, backend=backend.name) df = int(df_resid if distribution_df is None else distribution_df) - if dist_name == "t": + if dist_name == "t" and df == 1: + # Student-t with one residual degree of freedom is exactly a + # standard Cauchy distribution. Using this closed-form boundary + # avoids inverse-beta endpoint singularities in backend fallbacks + # while keeping the computation backend-native on NumPy/CuPy/Torch. + distribution = get_distribution("cauchy", backend=backend.name) + pvalues_dev = 2 * distribution.sf(xp.abs(tvalues_dev)) + critical = distribution.isf(float(self.alpha) / 2) + elif dist_name == "t": + distribution = get_distribution("t", backend=backend.name) pvalues_dev = 2 * distribution.sf(xp.abs(tvalues_dev), df) critical = distribution.isf(float(self.alpha) / 2, df) else: + distribution = get_distribution("norm", backend=backend.name) pvalues_dev = 2 * distribution.sf(xp.abs(tvalues_dev)) critical = distribution.isf(float(self.alpha) / 2) critical = xp_asarray( @@ -255,4 +303,4 @@ def _panel_summary( ) if print_result: print(summary) - return summary + return summary \ No newline at end of file diff --git a/statgpu/panel/_between.py b/statgpu/panel/_between.py index e417b1ef1..c0ee8ca51 100644 --- a/statgpu/panel/_between.py +++ b/statgpu/panel/_between.py @@ -17,7 +17,8 @@ class BetweenOLS(BasePanelModel): """Between-entity OLS estimator for panel data. - Collapses the data to group means and runs OLS on the collapsed data. + Collapses the data to entity means and runs OLS on the collapsed data. + An intercept is added automatically. Parameters ---------- @@ -27,11 +28,13 @@ class BetweenOLS(BasePanelModel): Significance level for confidence intervals. device : str or Device, default='auto' Computation device. + n_jobs : int or None, default=None + Optional parallelism hint retained by the shared estimator contract. Attributes ---------- coef_ : ndarray, shape (k,) - Estimated coefficients (including intercept). + Estimated coefficients, including the automatically added intercept. bse_ : ndarray, shape (k,) Standard errors. tvalues_ : ndarray, shape (k,) @@ -41,11 +44,13 @@ class BetweenOLS(BasePanelModel): conf_int_ : ndarray, shape (k, 2) Confidence intervals. rsquared : float - R-squared. + R-squared of the entity-mean regression. nobs : int - Number of observations (groups). + Number of entity-mean observations (groups). df_resid : int - Residual degrees of freedom. + Residual degrees of freedom of the legacy between regression. + fit_statistics_ : PanelFitStatistics or None + Standardized Stage-B panel fit statistics populated after ``fit``. """ def __init__( @@ -60,6 +65,7 @@ def __init__( self.alpha = alpha if self.cov_type not in ("nonrobust", "robust"): raise ValueError("cov_type must be 'nonrobust' or 'robust'") + self.fit_statistics_ = None def fit(self, X=None, y=None, entity_ids=None, time_ids=None, formula=None, data=None): """Fit the between OLS model.""" @@ -97,7 +103,6 @@ def fit(self, X=None, y=None, entity_ids=None, time_ids=None, formula=None, data ) n_orig = X_arr.shape[0] - # Add intercept exactly as before. ones = xp.ones((n_orig, 1), dtype=xp.float64) if hasattr(X_arr, "is_cuda"): @@ -150,6 +155,36 @@ def fit(self, X=None, y=None, entity_ids=None, time_ids=None, formula=None, data self.rsquared = 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan") self.nobs = n self.df_resid = df_resid + + from statgpu.panel._diagnostic_context import build_model_fit_statistics + from statgpu.panel._diagnostics import _matrix_rank + + rank_mean = _matrix_rank(X_mean, xp) + diagnostic_df = n - rank_mean + self.fit_statistics_ = build_model_fit_statistics( + y_arr, + X_full, + params, + xp=xp, + entity_codes=eids, + has_constant=True, + rss_fit=ss_res, + tss_fit=ss_tot, + df_resid=diagnostic_df, + df_total=n - 1, + f_y=y_mean, + f_X=X_mean, + f_params=params, + f_has_constant=True, + metadata={ + "fit_space": "entity-mean between regression", + "legacy_df_resid": int(df_resid), + "diagnostic_df_resid": int(diagnostic_df), + "diagnostic_rank": int(rank_mean), + "legacy_rsquared": self.rsquared, + }, + ) + self._fitted = True return self diff --git a/statgpu/panel/_diagnostic_context.py b/statgpu/panel/_diagnostic_context.py new file mode 100644 index 000000000..a065bb25f --- /dev/null +++ b/statgpu/panel/_diagnostic_context.py @@ -0,0 +1,326 @@ +"""Estimator-integration helpers for Panel Tier-1 Stage B diagnostics. + +This module keeps observation-scale sufficient-statistic work backend-native and +bridges fitted estimator arrays to the structured primitives in +``statgpu.panel._diagnostics``. +""" + +from __future__ import annotations + +import numpy as np + +from statgpu.backends import _to_float_scalar, _to_numpy +from statgpu.inference._distributions_backend import get_distribution +from statgpu.panel._diagnostics import ( + _applicable, + _build_fit_statistics, + _diagnostic_identity, + _inapplicable, + _matrix_rank, + _pooling_f_from_sums, +) +from statgpu.panel._utils import group_means, group_sizes + + +def _two_way_incidence_components( + entity_codes, + time_codes, + *, + n_entities: int, + n_times: int, +) -> int: + """Count connected components in the observed entity-time incidence graph.""" + if entity_codes is None or time_codes is None: + raise ValueError( + "two-way fixed-effect diagnostic rank requires entity and time codes" + ) + + entity = np.asarray(_to_numpy(entity_codes), dtype=np.int64).ravel() + time = np.asarray(_to_numpy(time_codes), dtype=np.int64).ravel() + if entity.shape != time.shape: + raise ValueError("entity and time codes must have the same length") + + n_entities = int(n_entities) + n_times = int(n_times) + total = n_entities + n_times + parent = np.arange(total, dtype=np.int64) + rank = np.zeros(total, dtype=np.int8) + + def find(node: int) -> int: + root = int(node) + while parent[root] != root: + root = int(parent[root]) + while parent[node] != node: + nxt = int(parent[node]) + parent[node] = root + node = nxt + return root + + def union(left: int, right: int) -> None: + left_root = find(left) + right_root = find(right) + if left_root == right_root: + return + if rank[left_root] < rank[right_root]: + left_root, right_root = right_root, left_root + parent[right_root] = left_root + if rank[left_root] == rank[right_root]: + rank[left_root] += 1 + + for entity_code, time_code in zip(entity, time): + entity_code = int(entity_code) + time_code = int(time_code) + if not (0 <= entity_code < n_entities): + raise ValueError("entity diagnostic code is out of range") + if not (0 <= time_code < n_times): + raise ValueError("time diagnostic code is out of range") + union(entity_code, n_entities + time_code) + + active = np.zeros(total, dtype=bool) + active[entity] = True + active[n_entities + time] = True + return len({find(int(node)) for node in np.flatnonzero(active)}) + + +def effect_rank_standard( + *, + n_entities: int, + n_times: int, + entity_effects: bool, + time_effects: bool, + has_constant: bool = False, + n_components: int = 1, +) -> int: + """Return the nuisance-effect rank under the standard FE parameterization.""" + n_entities = int(n_entities) + n_times = int(n_times) + n_components = int(n_components) + if not entity_effects and not time_effects: + return 0 + + if entity_effects and time_effects: + if n_components <= 0: + raise ValueError("two-way incidence component count must be positive") + full_dummy_rank = max(n_entities + n_times - n_components, 0) + if has_constant: + return int(max(full_dummy_rank - 1, 0)) + return int(full_dummy_rank) + + if has_constant: + if entity_effects: + return int(max(n_entities - 1, 0)) + return int(max(n_times - 1, 0)) + + if entity_effects: + return int(max(n_entities, 0)) + return int(max(n_times, 0)) + + +def fixed_effect_diagnostic_df( + X_transformed, + *, + xp, + nobs: int, + n_entities: int, + n_times: int, + entity_effects: bool, + time_effects: bool, + has_constant: bool = False, + entity_codes=None, + time_codes=None, +): + """Return rank-consistent Stage-B FE diagnostic df without changing legacy df.""" + rank_x = _matrix_rank(X_transformed, xp) + n_components = 1 + if entity_effects and time_effects: + n_components = _two_way_incidence_components( + entity_codes, + time_codes, + n_entities=n_entities, + n_times=n_times, + ) + effect_rank = effect_rank_standard( + n_entities=n_entities, + n_times=n_times, + entity_effects=entity_effects, + time_effects=time_effects, + has_constant=has_constant, + n_components=n_components, + ) + df_resid = int(nobs) - int(rank_x) - int(effect_rank) + df_total = int(nobs) - int(effect_rank) + return { + "rank_x": int(rank_x), + "effect_rank": int(effect_rank), + "df_resid": int(df_resid), + "df_total": int(df_total), + "incidence_components": int(n_components), + "legacy_df_unchanged": True, + } + + +def explicit_constant_column(X, *, xp): + """Return an identified explicit constant-column index, if one is present. + + The classification is relative to each column's own magnitude so multiplying + a valid design column by a nonzero unit-conversion factor does not change + whether it is recognized as an explicit constant. + """ + if int(X.shape[1]) == 0: + return None + if getattr(xp, "__name__", "") == "torch": + min_native = xp.amin(X, dim=0) + max_native = xp.amax(X, dim=0) + else: + min_native = xp.min(X, axis=0) + max_native = xp.max(X, axis=0) + col_min = np.asarray(_to_numpy(min_native), dtype=np.float64).ravel() + col_max = np.asarray(_to_numpy(max_native), dtype=np.float64).ravel() + magnitude = np.maximum(np.abs(col_min), np.abs(col_max)) + tol = 256.0 * np.finfo(np.float64).eps * magnitude + span = np.abs(col_max - col_min) + candidates = np.flatnonzero((span <= tol) & (magnitude > 0.0)) + if candidates.size == 0: + return None + return int(candidates[0]) + + +def pooling_f_from_level_arrays( + y, + X, + *, + xp, + rss_effects: float, + df_resid_effects: int, + has_constant: bool = False, +): + """Construct the nested pooled null on the exact aligned FE level sample.""" + n = int(y.shape[0]) + if has_constant: + y_pool = y + X_pool = X + constant_projection_df = 0 + else: + y_pool = y - xp.mean(y) + X_pool = X - xp.mean(X, axis=0) + constant_projection_df = 1 + + rank_pool = _matrix_rank(X_pool, xp) + beta_pool = xp.linalg.pinv(X_pool) @ y_pool + resid_pool = y_pool - X_pool @ beta_pool + rss_pool = _to_float_scalar(xp.sum(resid_pool * resid_pool)) + df_resid_pool = n - rank_pool - constant_projection_df + df_num = int(df_resid_pool) - int(df_resid_effects) + return _pooling_f_from_sums( + rss_pooled=float(rss_pool), + rss_effects=float(rss_effects), + df_num=int(df_num), + df_denom=int(df_resid_effects), + metadata={ + "rank_pooled": int(rank_pool), + "df_resid_pooled": int(df_resid_pool), + "df_resid_effects_standard": int(df_resid_effects), + "constant_projection_df": int(constant_projection_df), + "has_explicit_level_constant": bool(has_constant), + }, + ) + + +def bp_lm_from_residuals(resid, entity_codes, *, xp): + """Compute the one-way Baltagi-Li BP-LM from pooled residuals on backend.""" + null = "the entity random-effect variance is zero" + alternative = "a nonzero entity random-effect variance component is present" + if entity_codes is None: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="entity_ids were not supplied to the pooled fit", + ) + + nobs = int(resid.shape[0]) + # Entity codes are diagnostic metadata, so the common explicit metadata + # conversion is allowed. Observation-scale residuals remain backend-native. + codes_np = np.asarray(_to_numpy(entity_codes), dtype=np.int64).ravel() + n_entities = int(np.unique(codes_np).size) + meta = { + "n_entities": n_entities, + "nobs": nobs, + "definition": "Baltagi-Li one-way unbalanced Breusch-Pagan LM", + } + if n_entities < 2: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="Breusch-Pagan LM requires at least two entities", + metadata=meta, + ) + + residual_ss = _to_float_scalar(xp.sum(resid * resid)) + if residual_ss <= 0.0: + meta["residual_ss"] = float(residual_ss) + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="pooled residual sum of squares must be positive", + metadata=meta, + ) + + mean_aligned = group_means(resid, entity_codes, xp=xp) + sizes_aligned = group_sizes(entity_codes, xp=xp) + # Repeated aligned values allow scalar reductions without transferring the + # entity-level residual-sum vector to CPU. sum_i s_i^2 equals + # sum_obs mean_i^2 * T_i because each group contributes T_i copies. + sum_group_sums_sq = _to_float_scalar( + xp.sum(mean_aligned * mean_aligned * sizes_aligned) + ) + # Likewise sum_i T_i^2 = sum_obs T_i. + m11 = _to_float_scalar(xp.sum(sizes_aligned)) + if m11 <= nobs: + meta.update({"residual_ss": float(residual_ss), "M11": float(m11)}) + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="Breusch-Pagan LM requires at least one entity with repeated observations", + metadata=meta, + ) + + a1 = float(sum_group_sums_sq / residual_ss - 1.0) + lm1 = float(nobs * np.sqrt(1.0 / (2.0 * (m11 - nobs))) * a1) + statistic = float(lm1 * lm1) + dist = get_distribution("chi2", backend="numpy") + pvalue = _to_float_scalar(dist.sf(statistic, 1.0)) + meta.update( + { + "residual_ss": float(residual_ss), + "A1": a1, + "M11": float(m11), + "LM1": lm1, + } + ) + return _applicable( + statistic, + pvalue, + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + metadata=meta, + ) + + +def build_model_fit_statistics(*args, **kwargs): + """Thin estimator-facing wrapper around the shared fit-statistics builder.""" + return _build_fit_statistics(*args, **kwargs) + + +def build_diagnostic_identity(*args, **kwargs): + """Thin estimator-facing wrapper around the shared numerical fingerprint.""" + return _diagnostic_identity(*args, **kwargs) diff --git a/statgpu/panel/_diagnostics.py b/statgpu/panel/_diagnostics.py new file mode 100644 index 000000000..f7b8191e8 --- /dev/null +++ b/statgpu/panel/_diagnostics.py @@ -0,0 +1,899 @@ +"""Shared Panel Tier-1 diagnostics and fit-statistic primitives. + +Stage B of Issue #93 adds structured specification tests and parameter-based +fit statistics without changing the Stage-A estimator transformations or +covariance definitions. Observation-scale statistical accumulation stays on the +selected NumPy/CuPy/Torch backend. Hausman sample identity additionally uses a +chunked full-content SHA-256 over normalized float64 y/common-slope values so +different aligned samples cannot be accepted merely because low-order moments +collide, while an RE-only explicit intercept may be absorbed by FE. +""" + +from __future__ import annotations + +import hashlib +from typing import Any, Dict, Optional, Sequence, Tuple + +import numpy as np + +from statgpu.backends import _to_float_scalar, _to_numpy, xp_asarray +from statgpu.inference._distributions_backend import get_distribution +from statgpu.panel._results import PanelFitStatistics, PanelTestResult +from statgpu.panel._utils import group_means + +__all__ = [ + "hausman_test", + "pooling_f_test", + "breusch_pagan_lm_test", +] + + +def _inapplicable( + *, + null: str, + alternative: str, + distribution: Optional[str], + reason: str, + df=None, + metadata: Optional[Dict[str, Any]] = None, +) -> PanelTestResult: + return PanelTestResult( + statistic=None, + pvalue=None, + distribution=distribution, + df=df, + null=null, + alternative=alternative, + applicable=False, + reason=str(reason), + metadata={} if metadata is None else dict(metadata), + ) + + +def _applicable( + statistic: float, + pvalue: float, + *, + null: str, + alternative: str, + distribution: str, + df, + metadata: Optional[Dict[str, Any]] = None, +) -> PanelTestResult: + return PanelTestResult( + statistic=float(statistic), + pvalue=float(pvalue), + distribution=distribution, + df=df, + null=null, + alternative=alternative, + applicable=True, + reason=None, + metadata={} if metadata is None else dict(metadata), + ) + + +def _matrix_rank(X, xp) -> int: + return int(_to_float_scalar(xp.linalg.matrix_rank(X))) + + +def _relative_tolerance(*values: float, factor: float = 256.0) -> float: + """Return a float64 roundoff tolerance that preserves scale equivariance. + + Statistical quantities such as RSS and covariance matrices have physical + scale. Using an absolute ``max(1, scale)`` floor makes F/Hausman decisions + depend on arbitrary units. A zero scale therefore maps to an exact zero + tolerance; otherwise the tolerance scales linearly with the compared value. + """ + scale = max((abs(float(value)) for value in values), default=0.0) + return float(factor) * np.finfo(np.float64).eps * scale + + +def _safe_r2(ss_res: float, ss_tot: float) -> Tuple[float, bool]: + """Return linearmodels-style parameter R² and a degenerate-TSS flag.""" + ss_res = float(ss_res) + ss_tot = float(ss_tot) + if ss_tot <= 0.0: + return 0.0, True + return 1.0 - ss_res / ss_tot, False + + +def _demean_matrix(X, entity_codes, xp): + out = X.clone() if getattr(xp, "__name__", "") == "torch" else X.copy() + for j in range(int(X.shape[1])): + out[:, j] = X[:, j] - group_means(X[:, j], entity_codes, xp=xp) + return out + + +def _first_group_indices(entity_codes, xp, ref_arr): + codes_np = np.asarray(_to_numpy(entity_codes), dtype=np.int64).ravel() + _, first = np.unique(codes_np, return_index=True) + first.sort() + return xp_asarray(first, dtype=xp.int64, xp=xp, ref_arr=ref_arr) + + +def _parameter_r2_components( + y, + X, + params, + *, + xp, + entity_codes=None, + has_constant: bool, +) -> Tuple[Optional[float], Optional[float], float, Dict[str, bool]]: + """Compute parameter-based within, between and overall R². + + ``has_constant`` refers to an actual identified constant in the level + regressor design. Fixed effects alone do not set this flag. The pooling-F + common-constant correction is a separate nested-test rule. + """ + params = params.ravel() + overall_resid = y - X @ params + overall_center = y - xp.mean(y) if has_constant else y + overall_ss_res = _to_float_scalar(xp.sum(overall_resid * overall_resid)) + overall_ss_tot = _to_float_scalar(xp.sum(overall_center * overall_center)) + overall, deg_o = _safe_r2(overall_ss_res, overall_ss_tot) + + if entity_codes is None: + return None, None, overall, { + "within": False, + "between": False, + "overall": deg_o, + } + + y_mean_aligned = group_means(y, entity_codes, xp=xp) + X_mean_aligned = X.clone() if getattr(xp, "__name__", "") == "torch" else X.copy() + for j in range(int(X.shape[1])): + X_mean_aligned[:, j] = group_means(X[:, j], entity_codes, xp=xp) + first = _first_group_indices(entity_codes, xp, X) + y_between = y_mean_aligned[first] + X_between = X_mean_aligned[first] + between_resid = y_between - X_between @ params + between_center = y_between - xp.mean(y_between) if has_constant else y_between + between_ss_res = _to_float_scalar(xp.sum(between_resid * between_resid)) + between_ss_tot = _to_float_scalar(xp.sum(between_center * between_center)) + between, deg_b = _safe_r2(between_ss_res, between_ss_tot) + + y_within = y - y_mean_aligned + X_within = _demean_matrix(X, entity_codes, xp) + within_resid = y_within - X_within @ params + within_ss_res = _to_float_scalar(xp.sum(within_resid * within_resid)) + within_ss_tot = _to_float_scalar(xp.sum(y_within * y_within)) + within, deg_w = _safe_r2(within_ss_res, within_ss_tot) + + return within, between, overall, { + "within": deg_w, + "between": deg_b, + "overall": deg_o, + } + + +def _adjusted_r2( + *, + rss: float, + tss: float, + df_resid: int, + df_total: int, +) -> Optional[float]: + if int(df_resid) <= 0 or int(df_total) <= 0: + return None + if float(tss) <= 0.0: + return 0.0 + return 1.0 - (float(rss) / float(df_resid)) / ( + float(tss) / float(df_total) + ) + + +def _classical_model_f( + y, + X, + params, + *, + xp, + df_resid: int, + has_constant: bool, + restricted_X=None, +) -> Tuple[Optional[float], Optional[float], Optional[Tuple[float, float]], Dict[str, Any]]: + """Classical homoskedastic joint-slope F statistic in the fit space.""" + rank_u = _matrix_rank(X, xp) + rank_r = _matrix_rank(restricted_X, xp) if restricted_X is not None else ( + 1 if has_constant else 0 + ) + q = rank_u - rank_r + metadata = { + "classical_homoskedastic": True, + "rank_unrestricted": rank_u, + "rank_restricted": rank_r, + "restriction_rank": q, + } + if restricted_X is not None: + metadata["restricted_design_supplied"] = True + if q <= 0 or int(df_resid) <= 0: + metadata["unavailable_reason"] = "no estimable non-constant restrictions" + return None, None, None, metadata + + resid = y - X @ params.ravel() + rss_u = _to_float_scalar(xp.sum(resid * resid)) + if restricted_X is not None: + beta_r = xp.linalg.pinv(restricted_X) @ y + resid_r = y - restricted_X @ beta_r + rss_r = _to_float_scalar(xp.sum(resid_r * resid_r)) + elif has_constant: + y_r = y - xp.mean(y) + rss_r = _to_float_scalar(xp.sum(y_r * y_r)) + else: + rss_r = _to_float_scalar(xp.sum(y * y)) + + diff = rss_r - rss_u + tol = _relative_tolerance(rss_r, rss_u) + if diff < -tol: + metadata["unavailable_reason"] = "restricted RSS is materially below unrestricted RSS" + metadata["rss_restricted"] = float(rss_r) + metadata["rss_unrestricted"] = float(rss_u) + return None, None, None, metadata + if diff < 0.0: + diff = 0.0 + metadata["roundoff_normalized"] = True + + metadata["rss_restricted"] = float(rss_r) + metadata["rss_unrestricted"] = float(rss_u) + if rss_u <= tol: + if diff > tol: + metadata["exact_fit"] = True + return ( + float("inf"), + 0.0, + (float(q), float(df_resid)), + metadata, + ) + metadata["unavailable_reason"] = ( + "restricted and unrestricted residual sums of squares are both zero" + ) + return None, None, None, metadata + + statistic = (diff / q) / (rss_u / int(df_resid)) + dist = get_distribution("f", backend="numpy") + pvalue = _to_float_scalar(dist.sf(statistic, q, int(df_resid))) + return float(statistic), float(pvalue), (float(q), float(df_resid)), metadata + + +def _build_fit_statistics( + y, + X, + params, + *, + xp, + entity_codes=None, + has_constant: bool, + rss_fit: float, + tss_fit: float, + df_resid: int, + df_total: int, + f_y=None, + f_X=None, + f_params=None, + f_has_constant: Optional[bool] = None, + f_restricted_X=None, + metadata: Optional[Dict[str, Any]] = None, +) -> PanelFitStatistics: + within, between, overall, degenerate = _parameter_r2_components( + y, + X, + params, + xp=xp, + entity_codes=entity_codes, + has_constant=bool(has_constant), + ) + f_stat, f_pvalue, f_df, f_meta = _classical_model_f( + y if f_y is None else f_y, + X if f_X is None else f_X, + params if f_params is None else f_params, + xp=xp, + df_resid=int(df_resid), + has_constant=( + bool(has_constant) if f_has_constant is None else bool(f_has_constant) + ), + restricted_X=f_restricted_X, + ) + meta = {} if metadata is None else dict(metadata) + meta.setdefault("r2_definition", "parameter-based") + meta["degenerate_total_ss"] = degenerate + meta["rsquared_adj_basis"] = { + "df_total": int(df_total), + "df_resid": int(df_resid), + } + meta["model_f"] = f_meta + if entity_codes is None: + meta.setdefault("unavailable", {})["within_between_r2"] = ( + "entity_ids were not supplied" + ) + return PanelFitStatistics( + rsquared_within=within, + rsquared_between=between, + rsquared_overall=overall, + rsquared_adj=_adjusted_r2( + rss=float(rss_fit), + tss=float(tss_fit), + df_resid=int(df_resid), + df_total=int(df_total), + ), + f_statistic=f_stat, + f_pvalue=f_pvalue, + f_df=f_df, + metadata=meta, + ) + + +def _pooling_f_from_sums( + *, + rss_pooled: float, + rss_effects: float, + df_num: int, + df_denom: int, + metadata: Optional[Dict[str, Any]] = None, +) -> PanelTestResult: + null = "all included fixed effects are jointly zero" + alternative = "at least one included fixed effect is nonzero" + meta = {} if metadata is None else dict(metadata) + meta.update( + { + "rss_pooled": float(rss_pooled), + "rss_effects": float(rss_effects), + "classical_homoskedastic": True, + } + ) + if int(df_num) <= 0 or int(df_denom) <= 0: + return _inapplicable( + null=null, + alternative=alternative, + distribution="F", + df=(float(df_num), float(df_denom)), + reason="pooling F requires positive numerator and denominator degrees of freedom", + metadata=meta, + ) + + diff = float(rss_pooled) - float(rss_effects) + tol = _relative_tolerance(rss_pooled, rss_effects) + if diff < -tol: + return _inapplicable( + null=null, + alternative=alternative, + distribution="F", + df=(float(df_num), float(df_denom)), + reason="pooled RSS is materially below fixed-effects RSS; nested-model contract failed", + metadata=meta, + ) + if diff < 0.0: + diff = 0.0 + meta["roundoff_normalized"] = True + + if float(rss_effects) <= tol: + if diff > tol: + meta["exact_fit"] = True + return _applicable( + float("inf"), + 0.0, + null=null, + alternative=alternative, + distribution="F", + df=(float(df_num), float(df_denom)), + metadata=meta, + ) + return _inapplicable( + null=null, + alternative=alternative, + distribution="F", + df=(float(df_num), float(df_denom)), + reason="pooled and fixed-effects residual sums of squares are both zero", + metadata=meta, + ) + + statistic = (diff / int(df_num)) / (float(rss_effects) / int(df_denom)) + dist = get_distribution("f", backend="numpy") + pvalue = _to_float_scalar(dist.sf(statistic, int(df_num), int(df_denom))) + return _applicable( + statistic, + pvalue, + null=null, + alternative=alternative, + distribution="F", + df=(float(df_num), float(df_denom)), + metadata=meta, + ) + + +def _bp_lm_from_components( + *, + nobs: int, + residual_ss: float, + group_residual_sums: Sequence[float], + group_counts: Sequence[float], +) -> PanelTestResult: + null = "the entity random-effect variance is zero" + alternative = "a nonzero entity random-effect variance component is present" + sums = np.asarray(group_residual_sums, dtype=np.float64).ravel() + counts = np.asarray(group_counts, dtype=np.float64).ravel() + meta = { + "n_entities": int(counts.size), + "nobs": int(nobs), + "residual_ss": float(residual_ss), + "definition": "Baltagi-Li one-way unbalanced Breusch-Pagan LM", + } + if sums.size != counts.size or counts.size < 2: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="Breusch-Pagan LM requires at least two entities", + metadata=meta, + ) + if float(residual_ss) <= 0.0: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="pooled residual sum of squares must be positive", + metadata=meta, + ) + m11 = float(np.sum(counts * counts)) + if m11 <= int(nobs): + meta["M11"] = m11 + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + reason="Breusch-Pagan LM requires at least one entity with repeated observations", + metadata=meta, + ) + + a1 = float(np.sum(sums * sums) / float(residual_ss) - 1.0) + lm1 = int(nobs) * np.sqrt(1.0 / (2.0 * (m11 - int(nobs)))) * a1 + statistic = float(lm1 * lm1) + dist = get_distribution("chi2", backend="numpy") + pvalue = _to_float_scalar(dist.sf(statistic, 1.0)) + meta.update({"A1": a1, "M11": m11, "LM1": float(lm1)}) + return _applicable( + statistic, + pvalue, + null=null, + alternative=alternative, + distribution="chi2", + df=1.0, + metadata=meta, + ) + + +def _hausman_quadratic( + difference: Sequence[float], + covariance_difference, +) -> PanelTestResult: + null = "the random-effects estimator is consistent" + alternative = "the random-effects estimator is inconsistent" + d = np.asarray(difference, dtype=np.float64).ravel() + D = np.asarray(covariance_difference, dtype=np.float64) + if D.shape != (d.size, d.size): + raise ValueError("covariance difference shape must match coefficient difference") + if d.size == 0: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=0.0, + reason="Hausman test has no common estimable slope coefficients", + ) + + D = 0.5 * (D + D.T) + eigvals, eigvecs = np.linalg.eigh(D) + norm_D = float(np.linalg.norm(D, ord=2)) if D.size else 0.0 + tol = _relative_tolerance(norm_D, factor=256.0 * max(1, d.size)) + meta = { + "eigen_tolerance": tol, + "minimum_eigenvalue": float(eigvals.min()), + "maximum_eigenvalue": float(eigvals.max()), + } + if float(eigvals.min()) < -tol: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="covariance difference is not positive semidefinite", + metadata=meta, + ) + + positive = eigvals > tol + rank = int(np.count_nonzero(positive)) + meta["rank"] = rank + if rank == 0: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=0.0, + reason="covariance difference has zero numerical rank", + metadata=meta, + ) + + basis = eigvecs[:, positive] + projected = basis @ (basis.T @ d) + null_component = d - projected + range_tol = _relative_tolerance(np.linalg.norm(d), factor=1024.0) + meta["range_tolerance"] = float(range_tol) + meta["nullspace_component_norm"] = float(np.linalg.norm(null_component)) + if float(np.linalg.norm(null_component)) > range_tol: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=float(rank), + reason="coefficient difference has a component outside the identified covariance-difference range", + metadata=meta, + ) + + inv_eigs = 1.0 / eigvals[positive] + statistic = float((basis.T @ d).T @ (inv_eigs * (basis.T @ d))) + stat_tol = 256.0 * np.finfo(np.float64).eps * max(1.0, abs(statistic)) + if statistic < -stat_tol: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + df=float(rank), + reason="Hausman quadratic form is materially negative", + metadata=meta, + ) + if statistic < 0.0: + statistic = 0.0 + meta["roundoff_normalized"] = True + + if rank < d.size: + meta["used_pinv"] = True + meta["definition_extension"] = "singular PSD generalized-inverse Hausman" + else: + meta["used_pinv"] = False + + dist = get_distribution("chi2", backend="numpy") + pvalue = _to_float_scalar(dist.sf(statistic, float(rank))) + return _applicable( + statistic, + pvalue, + null=null, + alternative=alternative, + distribution="chi2", + df=float(rank), + metadata=meta, + ) + + +def _row_weights(n: int, xp, ref_arr): + if getattr(xp, "__name__", "") == "torch": + return xp.arange( + 1, + int(n) + 1, + dtype=xp.float64, + device=ref_arr.device, + ) + return xp.arange(1, int(n) + 1, dtype=xp.float64) + + +def _full_content_digest(X, y) -> str: + """Hash every aligned canonical slope-X/y value with bounded host transfers.""" + h = hashlib.sha256() + h.update(b"statgpu-panel-diagnostic-identity-v3") + + for label, array, shape in ( + (b"X", X, (int(X.shape[0]), int(X.shape[1]))), + (b"y", y, (int(y.shape[0]),)), + ): + h.update(label) + h.update(np.asarray(shape, dtype=" Dict[str, Any]: + """Retain audit moments plus an authoritative full-content digest.""" + n = int(X.shape[0]) + weights = _row_weights(n, xp, X) + X_sum = xp.sum(X, axis=0) + X_sq = xp.sum(X * X, axis=0) + X_weighted = xp.sum(X * weights.reshape(-1, 1), axis=0) + y_sum = xp.sum(y) + y_sq = xp.sum(y * y) + y_weighted = xp.sum(y * weights) + return { + "content_digest": _full_content_digest(X, y), + "X_sum": np.asarray(_to_numpy(X_sum), dtype=np.float64).ravel(), + "X_sq": np.asarray(_to_numpy(X_sq), dtype=np.float64).ravel(), + "X_weighted": np.asarray(_to_numpy(X_weighted), dtype=np.float64).ravel(), + "y": np.asarray( + [ + _to_float_scalar(y_sum), + _to_float_scalar(y_sq), + _to_float_scalar(y_weighted), + ], + dtype=np.float64, + ), + } + + +def _metadata_signature(codes) -> Optional[str]: + if codes is None: + return None + arr = np.asarray(_to_numpy(codes), dtype=np.int64).ravel() + return hashlib.sha256(arr.tobytes()).hexdigest() + + +def _identity_constant_index(X, *, xp, has_constant: bool) -> Optional[int]: + """Locate the constant column for canonical Hausman slope identity.""" + if not has_constant or int(X.shape[1]) == 0: + return None + if getattr(xp, "__name__", "") == "torch": + col_min_native = xp.amin(X, dim=0) + col_max_native = xp.amax(X, dim=0) + else: + col_min_native = xp.min(X, axis=0) + col_max_native = xp.max(X, axis=0) + col_min = np.asarray(_to_numpy(col_min_native), dtype=np.float64).ravel() + col_max = np.asarray(_to_numpy(col_max_native), dtype=np.float64).ravel() + magnitude = np.maximum(np.abs(col_min), np.abs(col_max)) + span = np.abs(col_max - col_min) + tol = 256.0 * np.finfo(np.float64).eps * magnitude + candidates = np.flatnonzero((span <= tol) & (magnitude > 0.0)) + return None if candidates.size == 0 else int(candidates[0]) + + +def _diagnostic_identity( + X, + y, + *, + xp, + entity_codes=None, + feature_names: Optional[Sequence[str]] = None, + has_constant: bool = False, + constant_column_index: Optional[int] = None, +) -> Dict[str, Any]: + """Build canonical sample/design identity for a Hausman slope comparison. + + FE absorbs a common intercept, while RE may estimate the same intercept as an + explicit design column. Identity therefore hashes y and only the slope + design. ``coefficient_indices`` maps canonical slope positions back to each + fitted model's original coefficient/covariance positions. + """ + k_raw = int(X.shape[1]) + constant_index = constant_column_index + if constant_index is None: + constant_index = _identity_constant_index( + X, xp=xp, has_constant=bool(has_constant) + ) + if constant_index is not None and not (0 <= int(constant_index) < k_raw): + raise ValueError("constant_column_index is out of range") + + coefficient_indices = tuple( + index for index in range(k_raw) if index != constant_index + ) + if len(coefficient_indices) == k_raw: + X_slopes = X + else: + index_dev = xp_asarray( + np.asarray(coefficient_indices, dtype=np.int64), + dtype=xp.int64, + xp=xp, + ref_arr=X, + ) + X_slopes = X[:, index_dev] + + if feature_names is None: + names = tuple(f"x{i + 1}" for i in range(len(coefficient_indices))) + else: + raw_names = tuple(feature_names) + if len(raw_names) == k_raw: + names = tuple(raw_names[index] for index in coefficient_indices) + elif len(raw_names) == len(coefficient_indices): + names = raw_names + else: + raise ValueError( + "feature_names must match the raw or canonical slope feature count" + ) + + return { + "nobs": int(X.shape[0]), + "n_features": int(len(coefficient_indices)), + "feature_names": names, + "coefficient_indices": coefficient_indices, + "has_constant": bool(has_constant), + "constant_column_index": constant_index, + "entity_signature": _metadata_signature(entity_codes), + "fingerprint": _numerical_fingerprint(X_slopes, y, xp=xp), + } + + +def _fingerprints_match(left: Dict[str, Any], right: Dict[str, Any]) -> Tuple[bool, str]: + # ``has_constant`` deliberately is not compared: FE may absorb the common + # intercept while RE estimates it explicitly. Canonical slope X/y identity + # plus entity and feature metadata is authoritative. + scalar_keys = ("nobs", "n_features", "feature_names", "entity_signature") + for key in scalar_keys: + if left.get(key) != right.get(key): + return False, f"diagnostic identity mismatch: {key}" + lf = left.get("fingerprint") + rf = right.get("fingerprint") + if not isinstance(lf, dict) or not isinstance(rf, dict): + return False, "diagnostic identity is missing numerical fingerprint metadata" + left_digest = lf.get("content_digest") + right_digest = rf.get("content_digest") + if not isinstance(left_digest, str) or not isinstance(right_digest, str): + return False, "diagnostic fingerprint is missing full-content digest" + if left_digest != right_digest: + return False, "diagnostic numerical fingerprint mismatch: content_digest" + return True, "" + + +def pooling_f_test(fe_model) -> PanelTestResult: + """Return the classical fixed-effect poolability F test.""" + from statgpu.panel._fixed_effects import PanelOLS + + if not isinstance(fe_model, PanelOLS): + raise TypeError("pooling_f_test requires a fitted PanelOLS model") + fe_model._check_is_fitted() + if not (bool(fe_model.entity_effects) or bool(fe_model.time_effects)): + return _inapplicable( + null="all included fixed effects are jointly zero", + alternative="at least one included fixed effect is nonzero", + distribution="F", + reason="pooling F requires at least one fixed-effect dimension", + ) + result = getattr(fe_model, "_pooling_f_result", None) + if result is None: + return _inapplicable( + null="all included fixed effects are jointly zero", + alternative="at least one included fixed effect is nonzero", + distribution="F", + reason="pooling F sufficient statistics were not retained by this fitted model", + ) + return result + + +def breusch_pagan_lm_test(pooled_model) -> PanelTestResult: + """Return the one-way entity error-components Breusch-Pagan LM test.""" + from statgpu.panel._pooled import PooledOLS + + if not isinstance(pooled_model, PooledOLS): + raise TypeError("breusch_pagan_lm_test requires a fitted PooledOLS model") + pooled_model._check_is_fitted() + result = getattr(pooled_model, "_bp_lm_result", None) + if result is None: + return _inapplicable( + null="the entity random-effect variance is zero", + alternative="a nonzero entity random-effect variance component is present", + distribution="chi2", + df=1.0, + reason="entity_ids were not supplied to the pooled fit", + ) + return result + + +def hausman_test(fe_model, re_model) -> PanelTestResult: + """Return the classical one-way FE-vs-RE Hausman specification test.""" + from statgpu.panel._fixed_effects import PanelOLS + from statgpu.panel._random_effects import RandomEffects + + if not isinstance(fe_model, PanelOLS): + raise TypeError("hausman_test fe_model must be a fitted PanelOLS") + if not isinstance(re_model, RandomEffects): + raise TypeError("hausman_test re_model must be a fitted RandomEffects") + fe_model._check_is_fitted() + re_model._check_is_fitted() + + null = "the random-effects estimator is consistent" + alternative = "the random-effects estimator is inconsistent" + if not bool(fe_model.entity_effects) or bool(fe_model.time_effects): + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="classical Stage-B Hausman requires one-way entity PanelOLS", + ) + if str(getattr(fe_model, "_cov_type", "nonrobust")).lower() != "nonrobust": + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="classical Hausman requires nonrobust FE covariance; robust auxiliary Hausman is not implemented in Stage B", + ) + + left_id = getattr(fe_model, "_panel_diagnostic_identity", None) + right_id = getattr(re_model, "_panel_diagnostic_identity", None) + if not isinstance(left_id, dict) or not isinstance(right_id, dict): + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="fitted models do not contain Stage-B sample/design identity metadata", + ) + matched, reason = _fingerprints_match(left_id, right_id) + if not matched: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason=reason, + ) + + fe_cov = getattr(fe_model, "_panel_cov_params", None) + re_cov = getattr(re_model, "_panel_cov_params", None) + if fe_cov is None or re_cov is None: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="fitted models do not contain the small covariance matrices required for Hausman", + ) + + fe_names = tuple(left_id.get("feature_names", ())) + re_names = tuple(right_id.get("feature_names", ())) + common = [name for name in fe_names if name in set(re_names)] + if not common: + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="Hausman test has no common estimable slope coefficients", + ) + + fe_positions = tuple( + int(value) + for value in left_id.get("coefficient_indices", range(len(fe_names))) + ) + re_positions = tuple( + int(value) + for value in right_id.get("coefficient_indices", range(len(re_names))) + ) + if len(fe_positions) != len(fe_names) or len(re_positions) != len(re_names): + return _inapplicable( + null=null, + alternative=alternative, + distribution="chi2", + reason="diagnostic identity has inconsistent coefficient-index metadata", + ) + fe_lookup = dict(zip(fe_names, fe_positions)) + re_lookup = dict(zip(re_names, re_positions)) + fe_idx = np.asarray([fe_lookup[name] for name in common], dtype=np.int64) + re_idx = np.asarray([re_lookup[name] for name in common], dtype=np.int64) + fe_coef = np.asarray(fe_model.coef_, dtype=np.float64).ravel()[fe_idx] + re_coef = np.asarray(re_model.coef_, dtype=np.float64).ravel()[re_idx] + fe_cov = np.asarray(fe_cov, dtype=np.float64)[np.ix_(fe_idx, fe_idx)] + re_cov = np.asarray(re_cov, dtype=np.float64)[np.ix_(re_idx, re_idx)] + result = _hausman_quadratic(fe_coef - re_coef, fe_cov - re_cov) + meta = dict(result.metadata) + meta["common_features"] = tuple(common) + meta["fe_coefficient_indices"] = tuple(int(value) for value in fe_idx) + meta["re_coefficient_indices"] = tuple(int(value) for value in re_idx) + meta["re_explicit_constant_excluded"] = bool( + right_id.get("constant_column_index") is not None + ) + return PanelTestResult( + statistic=result.statistic, + pvalue=result.pvalue, + distribution=result.distribution, + df=result.df, + null=result.null, + alternative=result.alternative, + applicable=result.applicable, + reason=result.reason, + metadata=meta, + ) diff --git a/statgpu/panel/_fama_macbeth.py b/statgpu/panel/_fama_macbeth.py index 3344a9274..e091873fb 100644 --- a/statgpu/panel/_fama_macbeth.py +++ b/statgpu/panel/_fama_macbeth.py @@ -19,6 +19,7 @@ ) from statgpu.covariance._empirical import _detect_backend from statgpu.panel._base import BasePanelModel +from statgpu.panel._utils import factorize_panel_labels def _stack(values, xp, axis=0): @@ -38,9 +39,9 @@ def _finite_all(x, xp): class FamaMacBeth(BasePanelModel): """Fama-MacBeth two-pass regression estimator. - The beta-series covariance remains estimator-specific. Stage A only shares - neutral formula/index/summary lifecycle; it deliberately does not route this - estimator through the residual-based OLS covariance registry. + The beta-series covariance remains estimator-specific. Stage B adds only + parameter-based panel R-squared summaries; it deliberately does not route + this estimator through residual-based OLS covariance or model-F machinery. """ def __init__( @@ -59,6 +60,7 @@ def __init__( self.min_obs_per_period = min_obs_per_period if self.cov_type not in ("nonrobust", "newey-west"): raise ValueError("cov_type must be 'nonrobust' or 'newey-west'") + self.fit_statistics_ = None def _validate_parameters(self): if self._cov_type not in ("nonrobust", "newey-west"): @@ -80,8 +82,6 @@ def _validate_parameters(self): raise ValueError("min_obs_per_period must be a positive integer") def _prepare_backend_arrays(self, X, y): - # Keep the established Fama-MacBeth backend selection because it also - # controls the backend-native prediction contract. backend_name = _detect_backend(X, self._get_compute_device()) xp = _get_xp(backend_name) ref = None @@ -106,10 +106,18 @@ def _prepare_backend_arrays(self, X, y): raise ValueError("X and y must contain only finite values") return backend_name, xp, X_arr, y_arr - def fit(self, X=None, y=None, time_ids=None, formula=None, data=None): + def fit( + self, + X=None, + y=None, + time_ids=None, + formula=None, + data=None, + entity_ids=None, + ): self._validate_parameters() # Preserve current public behavior: time_ids must be explicitly supplied; - # FamaMacBeth does not infer it from formula tokens in Stage A. + # FamaMacBeth does not infer it from formula tokens in Stage B. if time_ids is None: raise ValueError("time_ids is required for FamaMacBeth") @@ -127,9 +135,10 @@ def fit(self, X=None, y=None, time_ids=None, formula=None, data=None): X, y, model_has_intercept=True, - side_arrays={"time_ids": time_ids}, + side_arrays={"time_ids": time_ids, "entity_ids": entity_ids}, ) time_ids = aligned["time_ids"] + entity_ids = aligned["entity_ids"] backend_name, xp, X_arr, y_arr = self._prepare_backend_arrays(X_data, y_data) n_orig = int(X_arr.shape[0]) @@ -138,7 +147,19 @@ def fit(self, X=None, y=None, time_ids=None, formula=None, data=None): raise ValueError("time_ids must have one entry per observation") if np.any(np.asarray([x is None for x in tids_np], dtype=bool)): raise ValueError("time_ids must not contain missing values") - self._panel_set_index_info(n_orig, time_ids=tids_np) + + entity_codes = None + if entity_ids is not None: + entity_codes, _ = factorize_panel_labels( + entity_ids, + xp, + ref_arr=X_arr, + name="entity_ids", + expected_n=n_orig, + ) + self._panel_set_index_info( + n_orig, entity_ids=entity_ids, time_ids=tids_np + ) _, time_codes = np.unique(tids_np, return_inverse=True) counts = np.bincount(time_codes) @@ -225,6 +246,45 @@ def fit(self, X=None, y=None, time_ids=None, formula=None, data=None): self._backend_name = backend_name self._xp = xp self._fit_ref_ = X_arr + + from statgpu.panel._diagnostics import _parameter_r2_components + from statgpu.panel._results import PanelFitStatistics + + within, between, overall, degenerate = _parameter_r2_components( + y_arr, + X_design, + avg_beta, + xp=xp, + entity_codes=entity_codes, + has_constant=True, + ) + unavailable = { + "rsquared_adj": ( + "FamaMacBeth average-period adjusted R-squared is a distinct statistic " + "and is not defined in Stage B" + ), + "model_f": ( + "FamaMacBeth beta-series joint inference is not a residual-OLS model F statistic" + ), + } + if entity_codes is None: + unavailable["within_between_r2"] = "entity_ids were not supplied" + self.fit_statistics_ = PanelFitStatistics( + rsquared_within=within, + rsquared_between=between, + rsquared_overall=overall, + rsquared_adj=None, + f_statistic=None, + f_pvalue=None, + f_df=None, + metadata={ + "r2_definition": "parameter-based", + "fit_space": "average FamaMacBeth coefficient on level panel", + "degenerate_total_ss": degenerate, + "unavailable": unavailable, + }, + ) + self._fitted = True return self diff --git a/statgpu/panel/_first_diff.py b/statgpu/panel/_first_diff.py index 30fdfd27f..f19c340c6 100644 --- a/statgpu/panel/_first_diff.py +++ b/statgpu/panel/_first_diff.py @@ -34,6 +34,7 @@ def __init__( self.alpha = alpha if self.cov_type not in ("nonrobust", "robust"): raise ValueError("cov_type must be 'nonrobust' or 'robust'") + self.fit_statistics_ = None def fit(self, X=None, y=None, entity_ids=None, time_ids=None, formula=None, data=None): """Fit the first-difference OLS model.""" @@ -113,6 +114,41 @@ def fit(self, X=None, y=None, entity_ids=None, time_ids=None, formula=None, data self.rsquared = 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan") self.nobs = n self.df_resid = df_resid + + from statgpu.panel._diagnostic_context import build_model_fit_statistics + from statgpu.panel._diagnostics import _matrix_rank + + rank_diff = _matrix_rank(X_diff, xp) + diagnostic_df = n - rank_diff + # The primary FD fit has no constant, so its total fit-space df is the + # number of retained first differences. Standard within/between/overall + # R² are still evaluated using the level coefficient vector, matching the + # parameter-based panel definition rather than redefining them on Δy. + ss_tot_diag = _to_float_scalar(xp.sum(y_diff * y_diff)) + self.fit_statistics_ = build_model_fit_statistics( + y_arr, + X_arr, + params, + xp=xp, + entity_codes=eids, + has_constant=False, + rss_fit=ss_res, + tss_fit=ss_tot_diag, + df_resid=diagnostic_df, + df_total=n, + f_y=y_diff, + f_X=X_diff, + f_params=params, + f_has_constant=False, + metadata={ + "fit_space": "first-difference regression", + "legacy_df_resid": int(df_resid), + "diagnostic_df_resid": int(diagnostic_df), + "diagnostic_rank": int(rank_diff), + "legacy_rsquared": self.rsquared, + }, + ) + self._fitted = True return self @@ -166,4 +202,4 @@ def _first_diff_transform(X, y, entity_ids, time_ids, xp): y_diff = (y_sorted[1:] - y_sorted[:-1])[same_entity] if int(X_diff.shape[0]) == 0: raise ValueError("No entities with 2+ observations for differencing") - return X_diff, y_diff + return X_diff, y_diff \ No newline at end of file diff --git a/statgpu/panel/_fixed_effects.py b/statgpu/panel/_fixed_effects.py index 61dc5e7b4..192b39d82 100644 --- a/statgpu/panel/_fixed_effects.py +++ b/statgpu/panel/_fixed_effects.py @@ -57,6 +57,7 @@ def __init__( self.pvalues_ = None self.conf_int_ = None self.rsquared_within = None + self.fit_statistics_ = None self.nobs = None self.df_resid = None @@ -64,6 +65,8 @@ def __init__( self._scale = None self._entity_effects_map = {} self._time_effects_map = {} + self._pooling_f_result = None + self._panel_diagnostic_identity = None def fit( self, @@ -170,16 +173,57 @@ def fit( n_entities = len(entity_labels) if entity_labels is not None else 0 n_times = len(time_labels) if time_labels is not None else 0 + + from statgpu.panel._diagnostic_context import ( + build_diagnostic_identity, + build_model_fit_statistics, + fixed_effect_diagnostic_df, + pooling_f_from_level_arrays, + ) + + # Compute the rank-consistent FE degrees of freedom before deciding + # whether the fit is feasible. For disconnected two-way incidence + # graphs the nuisance rank is N + T - C, so the historical N/T count can + # otherwise reject an identified fit before Stage-B diagnostics run. + diagnostic_df = fixed_effect_diagnostic_df( + X_d, + xp=xp, + nobs=n, + n_entities=n_entities, + n_times=n_times, + entity_effects=self.entity_effects, + time_effects=self.time_effects, + has_constant=False, + entity_codes=entity_arr, + time_codes=time_arr, + ) + + # Preserve the legacy public df whenever it is positive. If the legacy + # count is nonpositive solely because a disconnected two-way panel has a + # lower nuisance rank, use the component-aware df instead so the valid + # fit can proceed. This keeps established connected-panel inference + # unchanged while fixing the false rejection boundary. n_effects = 0 if self.entity_effects: n_effects += n_entities - 1 if self.time_effects: n_effects += n_times - 1 - self.df_resid = n - k - n_effects - if self.df_resid <= 0: + legacy_df_resid = n - k - n_effects + standard_df_resid = int(diagnostic_df["df_resid"]) + if legacy_df_resid > 0: + self.df_resid = legacy_df_resid + public_df_basis = "legacy" + elif standard_df_resid > 0: + self.df_resid = standard_df_resid + public_df_basis = "component-aware" + else: raise ValueError( - f"Not enough observations: n={n}, k={k}, n_effects={n_effects}, " - f"df_resid={self.df_resid}. Check that N*T >> k + effects." + "Not enough observations after fixed-effect rank adjustment: " + f"n={n}, k={k}, legacy_n_effects={n_effects}, " + f"legacy_df_resid={legacy_df_resid}, " + f"effect_rank={diagnostic_df['effect_rank']}, " + f"incidence_components={diagnostic_df['incidence_components']}, " + f"df_resid={standard_df_resid}." ) y_pred = X_d @ coef @@ -257,16 +301,91 @@ def fit( diag_floor=0.0, ) + # Preserve the legacy Stage-A transformed-fit R² exactly. ss_res = _to_float_scalar(xp.sum(resid ** 2)) y_d_mean = _to_float_scalar(xp.mean(y_d)) ss_tot = _to_float_scalar(xp.sum((y_d - y_d_mean) ** 2)) self.rsquared_within = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0 + # New standardized diagnostics use the full nuisance-effect rank. This + # is intentionally separate from the historical self.df_resid used by + # covariance/t inference above, except when a disconnected two-way panel + # requires the component-aware df to avoid a false fit rejection. + ss_tot_diag = _to_float_scalar(xp.sum(y_d * y_d)) + self.fit_statistics_ = build_model_fit_statistics( + y_arr, + X_arr, + coef, + xp=xp, + entity_codes=entity_arr, + has_constant=False, + rss_fit=ss_res, + tss_fit=ss_tot_diag, + df_resid=diagnostic_df["df_resid"], + df_total=diagnostic_df["df_total"], + f_y=y_d, + f_X=X_d, + f_params=coef, + f_has_constant=False, + metadata={ + "fit_space": "fixed-effect transformed regression", + "legacy_df_resid": int(legacy_df_resid), + "public_df_resid_basis": public_df_basis, + "diagnostic_df": dict(diagnostic_df), + "legacy_rsquared_within": float(self.rsquared_within), + }, + ) + # Full-content identity is only needed for the Stage-B Hausman domain: + # one-way entity FE with classical nonrobust covariance. Robust, + # clustered, time-only, and two-way FE are rejected before identity + # comparison, so hashing their full X/y would be pure host-transfer cost. + hausman_compatible = ( + bool(self.entity_effects) + and not bool(self.time_effects) + and str(self._cov_type).lower() == "nonrobust" + ) + self._panel_diagnostic_identity = ( + build_diagnostic_identity( + X_arr, + y_arr, + xp=xp, + entity_codes=entity_arr, + feature_names=self._feature_names, + has_constant=False, + ) + if hausman_compatible + else None + ) + self._pooling_f_result = ( + pooling_f_from_level_arrays( + y_arr, + X_arr, + xp=xp, + rss_effects=ss_res, + df_resid_effects=diagnostic_df["df_resid"], + has_constant=False, + ) + if self.entity_effects or self.time_effects + else None + ) + self._params = np.asarray(self.coef_).ravel() self.coef_ = self._params self._fitted = True return self + def pooling_f_test(self): + """Return the classical test that included fixed effects are jointly zero.""" + from statgpu.panel._diagnostics import pooling_f_test + + return pooling_f_test(self) + + def hausman_test(self, random_effects_model): + """Compare one-way entity FE with a matched classical RandomEffects fit.""" + from statgpu.panel._diagnostics import hausman_test + + return hausman_test(self, random_effects_model) + def predict(self, X, entity_ids=None, time_ids=None): """Predict using the fitted model, preserving existing effect semantics.""" self._check_is_fitted() diff --git a/statgpu/panel/_pooled.py b/statgpu/panel/_pooled.py index 145b4fa9b..6fc137cb8 100644 --- a/statgpu/panel/_pooled.py +++ b/statgpu/panel/_pooled.py @@ -57,9 +57,24 @@ def __init__( raise ValueError( "cov_type must be 'nonrobust', 'robust', 'clustered', or 'hac'" ) + self.fit_statistics_ = None - def fit(self, X=None, y=None, cluster=None, time_index=None, formula=None, data=None): - """Fit the pooled OLS model.""" + def fit( + self, + X=None, + y=None, + cluster=None, + time_index=None, + formula=None, + data=None, + entity_ids=None, + ): + """Fit the pooled OLS model. + + ``entity_ids`` is optional and does not affect coefficients. When + supplied it enables Stage-B within/between R² and the one-way panel + Breusch-Pagan random-effects LM diagnostic. + """ ( y_data, X_data, @@ -74,15 +89,31 @@ def fit(self, X=None, y=None, cluster=None, time_index=None, formula=None, data= X, y, model_has_intercept=True, - side_arrays={"cluster": cluster, "time_index": time_index}, + side_arrays={ + "cluster": cluster, + "time_index": time_index, + "entity_ids": entity_ids, + }, ) cluster = aligned["cluster"] time_index = aligned["time_index"] + entity_ids = aligned["entity_ids"] backend, xp, X_arr, y_arr = self._panel_prepare_numeric(X_data, y_data) + entity_arr = None + if entity_ids is not None: + entity_arr, _ = factorize_panel_labels( + entity_ids, + xp, + ref_arr=X_arr, + name="entity_ids", + expected_n=X_arr.shape[0], + ) # HAC depends on temporal ordering. Metadata may remain on CPU, while - # the numerical arrays are reordered on their selected backend. + # the numerical arrays are reordered on their selected backend. Stage B + # carries entity diagnostic codes through the identical permutation so + # BP/R² sufficient statistics cannot become misaligned with residuals. if self._cov_type == "hac" and time_index is not None: time_values = np.asarray(_to_numpy(time_index)) if time_values.ndim != 1 or time_values.shape[0] != X_arr.shape[0]: @@ -93,6 +124,8 @@ def fit(self, X=None, y=None, cluster=None, time_index=None, formula=None, data= order = xp_asarray(order_np, dtype=xp.int64, xp=xp, ref_arr=X_arr) X_arr = X_arr[order] y_arr = y_arr[order] + if entity_arr is not None: + entity_arr = entity_arr[order] n = X_arr.shape[0] ones = xp.ones((n, 1), dtype=xp.float64) @@ -149,9 +182,43 @@ def fit(self, X=None, y=None, cluster=None, time_index=None, formula=None, data= self.nobs = n self.rank_ = rank self.df_resid = df_resid + + from statgpu.panel._diagnostic_context import ( + bp_lm_from_residuals, + build_model_fit_statistics, + ) + + self.fit_statistics_ = build_model_fit_statistics( + y_arr, + X_arr, + params, + xp=xp, + entity_codes=entity_arr, + has_constant=True, + rss_fit=ss_res, + tss_fit=ss_tot, + df_resid=df_resid, + df_total=n - 1, + metadata={ + "fit_space": "pooled level regression", + "legacy_rsquared": self.rsquared, + "diagnostic_df_resid": int(df_resid), + }, + ) + self._bp_lm_result = ( + None + if entity_arr is None + else bp_lm_from_residuals(resid, entity_arr, xp=xp) + ) self._fitted = True return self + def breusch_pagan_lm_test(self): + """Test pooled OLS against a one-way entity random-effects component.""" + from statgpu.panel._diagnostics import breusch_pagan_lm_test + + return breusch_pagan_lm_test(self) + def predict(self, X): """Predict using the fitted model.""" return self._panel_predict_linear( @@ -174,4 +241,4 @@ def get_params(self, deep=True): def set_params(self, **params): """Delegate parameter updates to the shared estimator contract.""" - return super().set_params(**params) + return super().set_params(**params) \ No newline at end of file diff --git a/statgpu/panel/_random_effects.py b/statgpu/panel/_random_effects.py index 433e23f48..e3ad27cf5 100644 --- a/statgpu/panel/_random_effects.py +++ b/statgpu/panel/_random_effects.py @@ -49,10 +49,12 @@ def __init__( self.conf_int_ = None self.theta_ = None self.variance_components_ = None + self.fit_statistics_ = None self.nobs = None self.df_resid = None self._params = None self._scale = None + self._panel_diagnostic_identity = None def fit( self, @@ -108,6 +110,28 @@ def fit( # new array-interface validation rule for it in this refactor. self._panel_set_index_info(n, entity_ids=entity_ids) + from statgpu.panel._diagnostic_context import ( + build_diagnostic_identity, + explicit_constant_column, + ) + + # Detect an explicit nonzero constant in the supplied level design. + # RandomEffects does not implicitly add an intercept, so this flag must + # describe the caller's actual X rather than the model family. + constant_index = explicit_constant_column(X_arr, xp=xp) + has_constant = constant_index is not None + + # Hausman compatibility is checked against aligned level X/y/entity + # metadata, before any Swamy-Arora transformation is applied. + self._panel_diagnostic_identity = build_diagnostic_identity( + X_arr, + y_arr, + xp=xp, + entity_codes=entity_arr, + feature_names=self._feature_names, + has_constant=has_constant, + ) + # --- Step 1: Between estimation --- y_bar_i = group_means(y_arr, entity_arr, xp=xp) X_bar_i = xp.zeros_like(X_arr) @@ -138,13 +162,45 @@ def fit( for j in range(k): X_within[:, j] = within_transform(X_arr[:, j], entity_arr, xp=xp) - XtX_w = X_within.T @ X_within - Xty_w = X_within.T @ y_within - try: - beta_within = xp.linalg.solve(XtX_w, Xty_w) - except _LINALG_ERRORS: - beta_within = xp.linalg.pinv(XtX_w) @ Xty_w - resid_within = y_within - X_within @ beta_within + # An explicit level constant is annihilated exactly by the within + # transform. Passing that structural zero column into a normal-equation + # solve makes XtX singular. NumPy reliably raises LinAlgError here, while + # GPU linalg stacks may return a value or warning instead, which can make + # sigma2_e/theta/backend coefficients diverge. Remove the known null + # column and compute the same auxiliary least-squares RSS on the slope + # subspace. The df formula below is unchanged: k includes the explicit + # constant while the (N - 1) nuisance count uses the equivalent + # parameterization, so n - k - (N - 1) = n - n_slopes - N. + if constant_index is not None: + slope_indices = np.asarray( + [j for j in range(k) if j != int(constant_index)], + dtype=np.int64, + ) + if slope_indices.size == 0: + resid_within = y_within + else: + slope_idx_dev = xp_asarray( + slope_indices, + dtype=xp.int64, + xp=xp, + ref_arr=X_arr, + ) + X_within_fit = X_within[:, slope_idx_dev] + XtX_w = X_within_fit.T @ X_within_fit + Xty_w = X_within_fit.T @ y_within + # Use the small-matrix pseudoinverse deliberately in this + # structural-rank branch so correctness does not depend on + # backend-specific singular-solve exception semantics. + beta_within = xp.linalg.pinv(XtX_w) @ Xty_w + resid_within = y_within - X_within_fit @ beta_within + else: + XtX_w = X_within.T @ X_within + Xty_w = X_within.T @ y_within + try: + beta_within = xp.linalg.solve(XtX_w, Xty_w) + except _LINALG_ERRORS: + beta_within = xp.linalg.pinv(XtX_w) @ Xty_w + resid_within = y_within - X_within @ beta_within rss_within = float(xp.sum(resid_within ** 2)) # --- Step 3: Swamy-Arora variance components --- @@ -238,11 +294,69 @@ def fit( diag_floor=0.0, ) + from statgpu.panel._diagnostic_context import build_model_fit_statistics + from statgpu.panel._diagnostics import _matrix_rank + + rank_star = _matrix_rank(X_star, xp) + diagnostic_df_resid = n - rank_star + ss_res_diag = _to_float_scalar(xp.sum(resid_gls * resid_gls)) + + # In the quasi-demeaned fit space, an explicit level intercept becomes + # the transformed intercept column X_star[:, constant_index]. On an + # unbalanced panel this is not generally a vector of ones, so both the + # adjusted-R² denominator and the restricted model F must retain that + # exact transformed column. + restricted_X = None + restricted_rank = 0 + if has_constant: + restricted_X = X_star[:, constant_index : constant_index + 1] + restricted_rank = _matrix_rank(restricted_X, xp) + restricted_params = xp.linalg.pinv(restricted_X) @ y_star + restricted_resid = y_star - restricted_X @ restricted_params + ss_tot_diag = _to_float_scalar( + xp.sum(restricted_resid * restricted_resid) + ) + else: + ss_tot_diag = _to_float_scalar(xp.sum(y_star * y_star)) + + self.fit_statistics_ = build_model_fit_statistics( + y_arr, + X_arr, + beta_gls, + xp=xp, + entity_codes=entity_arr, + has_constant=has_constant, + rss_fit=ss_res_diag, + tss_fit=ss_tot_diag, + df_resid=diagnostic_df_resid, + df_total=n - restricted_rank, + f_y=y_star, + f_X=X_star, + f_params=beta_gls, + f_has_constant=has_constant, + f_restricted_X=restricted_X, + metadata={ + "fit_space": "Swamy-Arora quasi-demeaned GLS regression", + "legacy_df_resid": int(self.df_resid), + "diagnostic_df_resid": int(diagnostic_df_resid), + "diagnostic_rank": int(rank_star), + "has_explicit_constant": bool(has_constant), + "constant_column_index": constant_index, + "restricted_rank": int(restricted_rank), + }, + ) + self._params = np.asarray(self.coef_).ravel() self.coef_ = self._params self._fitted = True return self + def hausman_test(self, fixed_effects_model): + """Compare this RE fit with a matched one-way entity PanelOLS fit.""" + from statgpu.panel._diagnostics import hausman_test + + return hausman_test(fixed_effects_model, self) + def predict(self, X): """Predict using the fitted model, preserving current NumPy output.""" self._check_is_fitted()