strathmark 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strathmark/__init__.py +335 -0
- strathmark/analytics.py +267 -0
- strathmark/api.py +1789 -0
- strathmark/auth.py +457 -0
- strathmark/calculator.py +1288 -0
- strathmark/config.py +761 -0
- strathmark/consumer_contract.py +105 -0
- strathmark/contracts/shadow_consumer_v1.openapi.json +1 -0
- strathmark/contracts/shadow_consumer_v1.openapi.sha256 +1 -0
- strathmark/db.py +2139 -0
- strathmark/decay.py +355 -0
- strathmark/drift.py +476 -0
- strathmark/fairness.py +674 -0
- strathmark/fallback.py +500 -0
- strathmark/features.py +386 -0
- strathmark/identity.py +29 -0
- strathmark/ledger.py +2950 -0
- strathmark/llm.py +364 -0
- strathmark/llm_roles.py +244 -0
- strathmark/loader.py +181 -0
- strathmark/mark_optimizer.py +359 -0
- strathmark/migrations/20260504_001_add_source_tracking.sql +100 -0
- strathmark/migrations/20260504_002_ml_state_tables.sql +206 -0
- strathmark/migrations/20260504_003_rls_reframe.sql +249 -0
- strathmark/migrations/20260508_004_atomic_model_swap_and_residual_dedup.sql +88 -0
- strathmark/migrations/20260811_005_prediction_v2.sql +810 -0
- strathmark/migrations/20260813_006_prediction_hash_algorithm.down.sql +638 -0
- strathmark/migrations/20260813_006_prediction_hash_algorithm.sql +649 -0
- strathmark/migrations/20260813_007_shadow_mirror_contract.down.sql +35 -0
- strathmark/migrations/20260813_007_shadow_mirror_contract.sql +1688 -0
- strathmark/migrations/README.md +188 -0
- strathmark/migrations/prerequisites/prediction_rpc_owner.sql +48 -0
- strathmark/mirror_contract.py +6 -0
- strathmark/mnemex.py +348 -0
- strathmark/models/prediction_v2_core.json +1 -0
- strathmark/prediction_v2.py +1458 -0
- strathmark/predictor.py +2868 -0
- strathmark/provenance.py +24 -0
- strathmark/residual.py +752 -0
- strathmark/shadow.py +498 -0
- strathmark/sqlite_utils.py +25 -0
- strathmark/store.py +1744 -0
- strathmark/sync.py +492 -0
- strathmark/utils.py +185 -0
- strathmark/validation.py +417 -0
- strathmark/variance.py +770 -0
- strathmark/visualization.py +129 -0
- strathmark/wood.py +672 -0
- strathmark-2.0.0.dist-info/METADATA +243 -0
- strathmark-2.0.0.dist-info/RECORD +52 -0
- strathmark-2.0.0.dist-info/WHEEL +4 -0
- strathmark-2.0.0.dist-info/licenses/LICENSE +203 -0
strathmark/__init__.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
strathmark — Woodchopping Handicap Engine
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
A Python package that exposes the STRATHMARK handicap calculation
|
|
6
|
+
engine for use in external applications (tournament software, scoring apps, etc.).
|
|
7
|
+
|
|
8
|
+
Quick start
|
|
9
|
+
-----------
|
|
10
|
+
from strathmark import HandicapCalculator, CompetitorRecord, WoodProfile, ResultStore
|
|
11
|
+
from strathmark.predictor import HistoricalResult
|
|
12
|
+
|
|
13
|
+
store = ResultStore() # opens ~/.strathmark/results.db
|
|
14
|
+
calc = HandicapCalculator()
|
|
15
|
+
|
|
16
|
+
competitor = CompetitorRecord(
|
|
17
|
+
name="Alice Smith",
|
|
18
|
+
history=store.get_competitor_history("Alice Smith", "SB"),
|
|
19
|
+
)
|
|
20
|
+
wood = WoodProfile(species="Pine", diameter_mm=300, quality=5)
|
|
21
|
+
marks = calc.calculate([competitor], wood, "SB")
|
|
22
|
+
|
|
23
|
+
Python import API (for STRATHEX and other Python projects):
|
|
24
|
+
from strathmark import HandicapCalculator
|
|
25
|
+
from strathmark.fairness import simulate_and_assess_handicaps
|
|
26
|
+
from strathmark.variance import run_monte_carlo_simulation
|
|
27
|
+
|
|
28
|
+
HTTP REST API (for web/mobile/non-Python projects):
|
|
29
|
+
uvicorn strathmark.api:app --port 8000
|
|
30
|
+
POST http://localhost:8000/calculate
|
|
31
|
+
|
|
32
|
+
Design Rules (invariants enforced in all submodules)
|
|
33
|
+
-----------------------------------------------------
|
|
34
|
+
- Mark floor: 3 seconds (never lower, under any circumstances)
|
|
35
|
+
- Mark ceiling: system-wide 183 seconds (180s time limit + 3s minimum mark)
|
|
36
|
+
event configs may enforce a lower ceiling
|
|
37
|
+
- Variance: absolute +-3 seconds ONLY — proportional variance is forbidden
|
|
38
|
+
- Prediction engine: manual override, otherwise the validated V2 posterior
|
|
39
|
+
with an explicit deterministic rollback fallback
|
|
40
|
+
- Time-decay: exponential decay, 2-year half-life (730 days)
|
|
41
|
+
- Output: plain text only — no emojis, no ANSI color codes
|
|
42
|
+
- Style: lean and simple, no unnecessary complexity
|
|
43
|
+
|
|
44
|
+
Downstream integration:
|
|
45
|
+
Pin an immutable reviewed release or commit. STRATHEX uses direct Python for its
|
|
46
|
+
offline default and may select the REST /calculate transport explicitly.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from strathmark.auth import (
|
|
50
|
+
ACTOR_ATTESTATION_SCHEMA_VERSION,
|
|
51
|
+
SHADOW_ATTESTATION_AUDIENCE,
|
|
52
|
+
VerifiedActorAttestation,
|
|
53
|
+
shadow_auth_configuration_status,
|
|
54
|
+
sign_actor_attestation,
|
|
55
|
+
)
|
|
56
|
+
from strathmark.calculator import HandicapCalculator, process_competition_day
|
|
57
|
+
from strathmark.consumer_contract import (
|
|
58
|
+
EXPECTED_SHADOW_CONSUMER_PATHS,
|
|
59
|
+
SHADOW_CONSUMER_CONTRACT_VERSION,
|
|
60
|
+
ShadowConsumerContractIntegrityError,
|
|
61
|
+
load_shadow_consumer_contract,
|
|
62
|
+
shadow_consumer_contract_bytes,
|
|
63
|
+
shadow_consumer_contract_digest,
|
|
64
|
+
)
|
|
65
|
+
from strathmark.db import (
|
|
66
|
+
format_proam_results,
|
|
67
|
+
get_active_model_version,
|
|
68
|
+
get_competitor_bias,
|
|
69
|
+
mirror_prediction_ledger,
|
|
70
|
+
pull_competitors,
|
|
71
|
+
pull_results,
|
|
72
|
+
push_competitors,
|
|
73
|
+
push_results,
|
|
74
|
+
push_results_dicts,
|
|
75
|
+
record_calibration,
|
|
76
|
+
record_prediction,
|
|
77
|
+
record_prediction_residuals,
|
|
78
|
+
register_competitor,
|
|
79
|
+
register_model_version,
|
|
80
|
+
set_active_model,
|
|
81
|
+
settle_prediction,
|
|
82
|
+
store_features,
|
|
83
|
+
)
|
|
84
|
+
from strathmark.drift import DriftReport, evaluate_drift, is_drifting
|
|
85
|
+
from strathmark.fairness import (
|
|
86
|
+
get_ai_assessment_of_handicaps,
|
|
87
|
+
get_championship_race_analysis,
|
|
88
|
+
simulate_and_assess_handicaps,
|
|
89
|
+
)
|
|
90
|
+
from strathmark.features import (
|
|
91
|
+
ExclusionDiagnostics,
|
|
92
|
+
PriorEvidence,
|
|
93
|
+
build_prior_evidence,
|
|
94
|
+
normalize_prediction_as_of,
|
|
95
|
+
resolve_species_properties,
|
|
96
|
+
)
|
|
97
|
+
from strathmark.ledger import (
|
|
98
|
+
LEGACY_SETTLEMENT_REASON_CODES,
|
|
99
|
+
MAX_NUMERIC_RAW_TIME_SECONDS,
|
|
100
|
+
NUMERIC_OUTCOME_REASON_CODES,
|
|
101
|
+
LedgerConflictError,
|
|
102
|
+
LedgerMonitoringStatus,
|
|
103
|
+
LedgerPrediction,
|
|
104
|
+
LedgerWriteResult,
|
|
105
|
+
NumericOutcomeRevisionResult,
|
|
106
|
+
NumericSettlementRevision,
|
|
107
|
+
NumericSettlementRevisionResult,
|
|
108
|
+
PredictionLedger,
|
|
109
|
+
SettlementConflictError,
|
|
110
|
+
SettlementResult,
|
|
111
|
+
)
|
|
112
|
+
from strathmark.llm import call_ollama, check_ollama_connection
|
|
113
|
+
from strathmark.loader import load_results_for_competitor, load_woodchopping_xlsx
|
|
114
|
+
from strathmark.mark_optimizer import (
|
|
115
|
+
MarkOptimizationResult,
|
|
116
|
+
legacy_rounded_gap_marks,
|
|
117
|
+
optimize_joint_marks,
|
|
118
|
+
)
|
|
119
|
+
from strathmark.mnemex import (
|
|
120
|
+
is_mnemex_configured,
|
|
121
|
+
pull_canonical_competitors,
|
|
122
|
+
pull_canonical_results,
|
|
123
|
+
register_competitor_in_mnemex,
|
|
124
|
+
)
|
|
125
|
+
from strathmark.prediction_v2 import (
|
|
126
|
+
ChronologicalCalibrator,
|
|
127
|
+
ForecastInterval,
|
|
128
|
+
PredictionV2Model,
|
|
129
|
+
PredictionV2Request,
|
|
130
|
+
PredictiveDistribution,
|
|
131
|
+
)
|
|
132
|
+
from strathmark.predictor import (
|
|
133
|
+
CompetitorRecord,
|
|
134
|
+
FilePredictionProvider,
|
|
135
|
+
HistoricalResult,
|
|
136
|
+
PredictionBundle,
|
|
137
|
+
PredictionContext,
|
|
138
|
+
PredictionEngineProvider,
|
|
139
|
+
PredictionInterval,
|
|
140
|
+
PredictionResult,
|
|
141
|
+
StaticPredictionProvider,
|
|
142
|
+
WoodProfile,
|
|
143
|
+
get_all_predictions,
|
|
144
|
+
get_best_prediction,
|
|
145
|
+
get_prediction_provider,
|
|
146
|
+
predict_baseline,
|
|
147
|
+
select_best_prediction,
|
|
148
|
+
)
|
|
149
|
+
from strathmark.shadow import (
|
|
150
|
+
ACTIVE_INPUT_SCHEMA_VERSION,
|
|
151
|
+
IDENTITY_SCHEMA_VERSION,
|
|
152
|
+
OBSERVATION_SCHEMA_VERSION,
|
|
153
|
+
RECEIPT_CORE_SCHEMA_VERSION,
|
|
154
|
+
REQUEST_PROJECTION_SCHEMA_VERSION,
|
|
155
|
+
SHADOW_TARGET_SINGLE_ELAPSED,
|
|
156
|
+
ShadowCalculationResult,
|
|
157
|
+
ShadowFieldRequest,
|
|
158
|
+
ShadowLiveStatus,
|
|
159
|
+
ShadowPredictionService,
|
|
160
|
+
ShadowReceipt,
|
|
161
|
+
)
|
|
162
|
+
from strathmark.store import (
|
|
163
|
+
DEFAULT_MAX_SNAPSHOT_AGE_DAYS,
|
|
164
|
+
EVIDENCE_ACTIVATION_SCHEMA_VERSION,
|
|
165
|
+
EVIDENCE_HISTORY_ROW_SCHEMA_VERSION,
|
|
166
|
+
EVIDENCE_SNAPSHOT_SCHEMA_VERSION,
|
|
167
|
+
EVIDENCE_SNAPSHOT_SOURCE_SCHEMA_VERSION,
|
|
168
|
+
MAX_CAPTURE_CLOCK_SKEW_SECONDS,
|
|
169
|
+
EvidenceSnapshotConflictError,
|
|
170
|
+
EvidenceSnapshotIntegrityError,
|
|
171
|
+
EvidenceSnapshotPayload,
|
|
172
|
+
EvidenceSnapshotSelection,
|
|
173
|
+
EvidenceSnapshotSource,
|
|
174
|
+
EvidenceSnapshotStatus,
|
|
175
|
+
ResultStore,
|
|
176
|
+
canonical_evidence_source_digest,
|
|
177
|
+
)
|
|
178
|
+
from strathmark.sync import (
|
|
179
|
+
SyncResult,
|
|
180
|
+
manual_force_sync,
|
|
181
|
+
nightly_batch,
|
|
182
|
+
strathex_finalization,
|
|
183
|
+
)
|
|
184
|
+
from strathmark.utils import score_prediction_accuracy
|
|
185
|
+
from strathmark.variance import (
|
|
186
|
+
audit_mark_sheet,
|
|
187
|
+
estimate_competitor_std_dev,
|
|
188
|
+
quick_fairness_check,
|
|
189
|
+
run_monte_carlo_simulation,
|
|
190
|
+
)
|
|
191
|
+
from strathmark.visualization import (
|
|
192
|
+
generate_simulation_summary,
|
|
193
|
+
visualize_simulation_results,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
# Core calculation
|
|
198
|
+
"HandicapCalculator",
|
|
199
|
+
"process_competition_day",
|
|
200
|
+
# Data types
|
|
201
|
+
"CompetitorRecord",
|
|
202
|
+
"WoodProfile",
|
|
203
|
+
"HistoricalResult",
|
|
204
|
+
"PredictionResult",
|
|
205
|
+
"PredictionContext",
|
|
206
|
+
"PredictionInterval",
|
|
207
|
+
"PredictionBundle",
|
|
208
|
+
"PredictionEngineProvider",
|
|
209
|
+
"StaticPredictionProvider",
|
|
210
|
+
"FilePredictionProvider",
|
|
211
|
+
"PriorEvidence",
|
|
212
|
+
"ExclusionDiagnostics",
|
|
213
|
+
# Prediction API
|
|
214
|
+
"get_best_prediction",
|
|
215
|
+
"get_prediction_provider",
|
|
216
|
+
"get_all_predictions",
|
|
217
|
+
"select_best_prediction",
|
|
218
|
+
"predict_baseline",
|
|
219
|
+
"build_prior_evidence",
|
|
220
|
+
"normalize_prediction_as_of",
|
|
221
|
+
"resolve_species_properties",
|
|
222
|
+
"PredictionV2Model",
|
|
223
|
+
"PredictionV2Request",
|
|
224
|
+
"PredictiveDistribution",
|
|
225
|
+
"ForecastInterval",
|
|
226
|
+
"ChronologicalCalibrator",
|
|
227
|
+
"SHADOW_CONSUMER_CONTRACT_VERSION",
|
|
228
|
+
"EXPECTED_SHADOW_CONSUMER_PATHS",
|
|
229
|
+
"ShadowConsumerContractIntegrityError",
|
|
230
|
+
"load_shadow_consumer_contract",
|
|
231
|
+
"shadow_consumer_contract_bytes",
|
|
232
|
+
"shadow_consumer_contract_digest",
|
|
233
|
+
"MarkOptimizationResult",
|
|
234
|
+
"legacy_rounded_gap_marks",
|
|
235
|
+
"optimize_joint_marks",
|
|
236
|
+
# Persistence
|
|
237
|
+
"ResultStore",
|
|
238
|
+
"EvidenceSnapshotPayload",
|
|
239
|
+
"EvidenceSnapshotSelection",
|
|
240
|
+
"EvidenceSnapshotSource",
|
|
241
|
+
"EvidenceSnapshotStatus",
|
|
242
|
+
"EvidenceSnapshotConflictError",
|
|
243
|
+
"EvidenceSnapshotIntegrityError",
|
|
244
|
+
"canonical_evidence_source_digest",
|
|
245
|
+
"EVIDENCE_SNAPSHOT_SOURCE_SCHEMA_VERSION",
|
|
246
|
+
"EVIDENCE_SNAPSHOT_SCHEMA_VERSION",
|
|
247
|
+
"EVIDENCE_HISTORY_ROW_SCHEMA_VERSION",
|
|
248
|
+
"EVIDENCE_ACTIVATION_SCHEMA_VERSION",
|
|
249
|
+
"DEFAULT_MAX_SNAPSHOT_AGE_DAYS",
|
|
250
|
+
"MAX_CAPTURE_CLOCK_SKEW_SECONDS",
|
|
251
|
+
"PredictionLedger",
|
|
252
|
+
"LedgerPrediction",
|
|
253
|
+
"LedgerWriteResult",
|
|
254
|
+
"LedgerConflictError",
|
|
255
|
+
"LEGACY_SETTLEMENT_REASON_CODES",
|
|
256
|
+
"MAX_NUMERIC_RAW_TIME_SECONDS",
|
|
257
|
+
"NUMERIC_OUTCOME_REASON_CODES",
|
|
258
|
+
"LedgerMonitoringStatus",
|
|
259
|
+
"NumericSettlementRevision",
|
|
260
|
+
"NumericSettlementRevisionResult",
|
|
261
|
+
"NumericOutcomeRevisionResult",
|
|
262
|
+
"SettlementResult",
|
|
263
|
+
"SettlementConflictError",
|
|
264
|
+
"ShadowFieldRequest",
|
|
265
|
+
"ShadowLiveStatus",
|
|
266
|
+
"ShadowReceipt",
|
|
267
|
+
"ShadowCalculationResult",
|
|
268
|
+
"ShadowPredictionService",
|
|
269
|
+
"RECEIPT_CORE_SCHEMA_VERSION",
|
|
270
|
+
"REQUEST_PROJECTION_SCHEMA_VERSION",
|
|
271
|
+
"ACTIVE_INPUT_SCHEMA_VERSION",
|
|
272
|
+
"IDENTITY_SCHEMA_VERSION",
|
|
273
|
+
"OBSERVATION_SCHEMA_VERSION",
|
|
274
|
+
"SHADOW_TARGET_SINGLE_ELAPSED",
|
|
275
|
+
"ACTOR_ATTESTATION_SCHEMA_VERSION",
|
|
276
|
+
"SHADOW_ATTESTATION_AUDIENCE",
|
|
277
|
+
"VerifiedActorAttestation",
|
|
278
|
+
"shadow_auth_configuration_status",
|
|
279
|
+
"sign_actor_attestation",
|
|
280
|
+
# Simulation
|
|
281
|
+
"run_monte_carlo_simulation",
|
|
282
|
+
"estimate_competitor_std_dev",
|
|
283
|
+
"audit_mark_sheet",
|
|
284
|
+
"quick_fairness_check",
|
|
285
|
+
# Visualization
|
|
286
|
+
"generate_simulation_summary",
|
|
287
|
+
"visualize_simulation_results",
|
|
288
|
+
# Fairness
|
|
289
|
+
"get_ai_assessment_of_handicaps",
|
|
290
|
+
"get_championship_race_analysis",
|
|
291
|
+
"simulate_and_assess_handicaps",
|
|
292
|
+
# LLM
|
|
293
|
+
"call_ollama",
|
|
294
|
+
"check_ollama_connection",
|
|
295
|
+
# Data loading
|
|
296
|
+
"load_woodchopping_xlsx",
|
|
297
|
+
"load_results_for_competitor",
|
|
298
|
+
# Database (Supabase)
|
|
299
|
+
"push_results",
|
|
300
|
+
"push_results_dicts",
|
|
301
|
+
"pull_results",
|
|
302
|
+
"push_competitors",
|
|
303
|
+
"pull_competitors",
|
|
304
|
+
"register_competitor",
|
|
305
|
+
"format_proam_results",
|
|
306
|
+
"record_prediction_residuals",
|
|
307
|
+
"get_competitor_bias",
|
|
308
|
+
"mirror_prediction_ledger",
|
|
309
|
+
# ML state (carve-out from controlled-write rule; STRATHMARK-internal)
|
|
310
|
+
"register_model_version",
|
|
311
|
+
"set_active_model",
|
|
312
|
+
"get_active_model_version",
|
|
313
|
+
"record_calibration",
|
|
314
|
+
"store_features",
|
|
315
|
+
"record_prediction",
|
|
316
|
+
"settle_prediction",
|
|
317
|
+
# MNEMEX (canonical archive client)
|
|
318
|
+
"is_mnemex_configured",
|
|
319
|
+
"pull_canonical_results",
|
|
320
|
+
"pull_canonical_competitors",
|
|
321
|
+
"register_competitor_in_mnemex",
|
|
322
|
+
# Sync (MNEMEX -> STRATHMARK Supabase)
|
|
323
|
+
"SyncResult",
|
|
324
|
+
"nightly_batch",
|
|
325
|
+
"strathex_finalization",
|
|
326
|
+
"manual_force_sync",
|
|
327
|
+
# Drift detection
|
|
328
|
+
"DriftReport",
|
|
329
|
+
"evaluate_drift",
|
|
330
|
+
"is_drifting",
|
|
331
|
+
# Scoring / accuracy
|
|
332
|
+
"score_prediction_accuracy",
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
__version__ = "2.0.0"
|
strathmark/analytics.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytics
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
Backtesting, competitor profiling, and performance history analysis.
|
|
6
|
+
|
|
7
|
+
Public functions:
|
|
8
|
+
backtest_predictions() -- compare predicted vs actual times
|
|
9
|
+
profile_competitor() -- summarise a single competitor's history
|
|
10
|
+
summarise_performance_history() -- tournament history summary for a field
|
|
11
|
+
|
|
12
|
+
Source references (STRATHEX):
|
|
13
|
+
woodchopping/analytics/prediction_accuracy.py
|
|
14
|
+
woodchopping/analytics/competitor_profiling.py
|
|
15
|
+
woodchopping/analytics/performance_history.py
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from datetime import date
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
from strathmark.predictor import (
|
|
27
|
+
CompetitorRecord,
|
|
28
|
+
WoodProfile,
|
|
29
|
+
get_best_prediction,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Backtesting
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def backtest_predictions(
|
|
38
|
+
competitors: List[CompetitorRecord],
|
|
39
|
+
wood: WoodProfile,
|
|
40
|
+
event_code: str,
|
|
41
|
+
actuals: Dict[str, float],
|
|
42
|
+
ml_model=None,
|
|
43
|
+
results_df=None,
|
|
44
|
+
wood_df=None,
|
|
45
|
+
) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Compare predicted times against actual race times.
|
|
48
|
+
|
|
49
|
+
Uses the full prediction cascade (get_best_prediction) when ml_model or
|
|
50
|
+
results_df are provided, otherwise falls back to baseline only.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
competitors: List of CompetitorRecord objects.
|
|
54
|
+
wood: Wood profile used in the event.
|
|
55
|
+
event_code: 'SB' or 'UH'.
|
|
56
|
+
actuals: Dict mapping competitor name to actual cutting time (seconds).
|
|
57
|
+
ml_model: Optional trained MLModel for ML cascade level.
|
|
58
|
+
results_df: Optional historical results DataFrame.
|
|
59
|
+
wood_df: Optional wood properties DataFrame.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict with keys:
|
|
63
|
+
'results' -- List of per-competitor dicts
|
|
64
|
+
'mae' -- Mean Absolute Error (seconds)
|
|
65
|
+
'rmse' -- Root Mean Squared Error (seconds)
|
|
66
|
+
'bias' -- Mean signed error (positive = predictions too high)
|
|
67
|
+
'within_3s_pct' -- % of predictions within 3 seconds of actual
|
|
68
|
+
"""
|
|
69
|
+
results = []
|
|
70
|
+
errors = []
|
|
71
|
+
|
|
72
|
+
for record in competitors:
|
|
73
|
+
if record.name not in actuals:
|
|
74
|
+
continue
|
|
75
|
+
actual = actuals[record.name]
|
|
76
|
+
pred_result = get_best_prediction(
|
|
77
|
+
record,
|
|
78
|
+
wood,
|
|
79
|
+
event_code,
|
|
80
|
+
wood_data_df=wood_df,
|
|
81
|
+
results_df=results_df,
|
|
82
|
+
ml_model=ml_model,
|
|
83
|
+
)
|
|
84
|
+
if pred_result is None:
|
|
85
|
+
continue
|
|
86
|
+
predicted = pred_result.value
|
|
87
|
+
error = predicted - actual
|
|
88
|
+
abs_error = abs(error)
|
|
89
|
+
errors.append(error)
|
|
90
|
+
results.append(
|
|
91
|
+
{
|
|
92
|
+
"name": record.name,
|
|
93
|
+
"predicted": predicted,
|
|
94
|
+
"actual": actual,
|
|
95
|
+
"error": error,
|
|
96
|
+
"abs_error": abs_error,
|
|
97
|
+
"confidence": pred_result.confidence,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not errors:
|
|
102
|
+
return {
|
|
103
|
+
"results": results,
|
|
104
|
+
"mae": None,
|
|
105
|
+
"rmse": None,
|
|
106
|
+
"bias": None,
|
|
107
|
+
"within_3s_pct": None,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
errors_arr = np.array(errors)
|
|
111
|
+
within_3s = sum(1 for e in errors if abs(e) <= 3.0)
|
|
112
|
+
return {
|
|
113
|
+
"results": results,
|
|
114
|
+
"mae": float(np.mean(np.abs(errors_arr))),
|
|
115
|
+
"rmse": float(np.sqrt(np.mean(errors_arr**2))),
|
|
116
|
+
"bias": float(np.mean(errors_arr)),
|
|
117
|
+
"within_3s_pct": within_3s / len(errors) * 100,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def rolling_origin_analysis(
|
|
122
|
+
evidence: pd.DataFrame,
|
|
123
|
+
*,
|
|
124
|
+
target_start: date | None = None,
|
|
125
|
+
target_end_exclusive: date | None = None,
|
|
126
|
+
min_training_rows: int = 30,
|
|
127
|
+
minimum_global_rows: int = 100,
|
|
128
|
+
minimum_cohort_rows: int = 30,
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""Return an honest prior-only V2 rolling-origin accuracy summary.
|
|
131
|
+
|
|
132
|
+
``claim_eligible`` is explicit so a small sample cannot accidentally be
|
|
133
|
+
presented as a validated accuracy or subgroup claim.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
from strathmark.validation import chronological_backtest
|
|
137
|
+
|
|
138
|
+
report = chronological_backtest(
|
|
139
|
+
evidence,
|
|
140
|
+
target_start=target_start,
|
|
141
|
+
target_end_exclusive=target_end_exclusive,
|
|
142
|
+
min_training_rows=min_training_rows,
|
|
143
|
+
)
|
|
144
|
+
predictions = report.predictions
|
|
145
|
+
metrics: Dict[str, Any] = dict(report.metrics)
|
|
146
|
+
count = int(metrics.get("count", 0))
|
|
147
|
+
metrics["claim_eligible"] = count >= minimum_global_rows
|
|
148
|
+
metrics["sample_label"] = (
|
|
149
|
+
"global_claim_eligible" if metrics["claim_eligible"] else "insufficient_global_sample"
|
|
150
|
+
)
|
|
151
|
+
if not predictions.empty and {"core_lower", "core_upper"}.issubset(predictions.columns):
|
|
152
|
+
actual = predictions["actual_time"].to_numpy(dtype=float)
|
|
153
|
+
lower = predictions["core_lower"].to_numpy(dtype=float)
|
|
154
|
+
upper = predictions["core_upper"].to_numpy(dtype=float)
|
|
155
|
+
metrics["coverage_90"] = float(np.mean((lower <= actual) & (actual <= upper)))
|
|
156
|
+
metrics["mean_interval_width"] = float(np.mean(upper - lower))
|
|
157
|
+
|
|
158
|
+
cohorts: Dict[str, Dict[str, Any]] = {}
|
|
159
|
+
for name, values in report.cohort_metrics.items():
|
|
160
|
+
cohort = dict(values)
|
|
161
|
+
cohort_count = int(cohort.get("count", 0))
|
|
162
|
+
cohort["claim_eligible"] = cohort_count >= minimum_cohort_rows
|
|
163
|
+
cohort["sample_label"] = (
|
|
164
|
+
"cohort_claim_eligible" if cohort["claim_eligible"] else "insufficient_cohort_sample"
|
|
165
|
+
)
|
|
166
|
+
cohorts[name] = cohort
|
|
167
|
+
return {
|
|
168
|
+
"metrics": metrics,
|
|
169
|
+
"cohorts": cohorts,
|
|
170
|
+
"predictions": predictions,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Competitor profiling
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def profile_competitor(
|
|
180
|
+
record: CompetitorRecord,
|
|
181
|
+
event_code: Optional[str] = None,
|
|
182
|
+
) -> Dict[str, Any]:
|
|
183
|
+
"""
|
|
184
|
+
Summarise a competitor's historical performance.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
record: Competitor record with history populated.
|
|
188
|
+
event_code: Optional filter ('SB' or 'UH'). None analyses all events.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict with:
|
|
192
|
+
'name', 'division', 'total_results', 'events_contested',
|
|
193
|
+
'mean_time', 'std_dev', 'best_time', 'worst_time',
|
|
194
|
+
'most_recent_date', 'activity_level'
|
|
195
|
+
"""
|
|
196
|
+
history = record.history
|
|
197
|
+
if event_code is not None:
|
|
198
|
+
history = [r for r in history if r.event_code.upper() == event_code.upper()]
|
|
199
|
+
|
|
200
|
+
if not history:
|
|
201
|
+
return {
|
|
202
|
+
"name": record.name,
|
|
203
|
+
"division": record.division,
|
|
204
|
+
"total_results": 0,
|
|
205
|
+
"events_contested": [],
|
|
206
|
+
"mean_time": None,
|
|
207
|
+
"std_dev": None,
|
|
208
|
+
"best_time": None,
|
|
209
|
+
"worst_time": None,
|
|
210
|
+
"most_recent_date": None,
|
|
211
|
+
"activity_level": "inactive",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
times = [r.time_seconds for r in history]
|
|
215
|
+
events = sorted({r.event_code for r in history})
|
|
216
|
+
dates = [r.result_date for r in history if r.result_date is not None]
|
|
217
|
+
most_recent = max(dates) if dates else None
|
|
218
|
+
|
|
219
|
+
today = date.today()
|
|
220
|
+
recent_count = sum(1 for d in dates if d is not None and (today - d).days <= 730)
|
|
221
|
+
if recent_count >= 5:
|
|
222
|
+
activity = "active"
|
|
223
|
+
elif recent_count >= 2:
|
|
224
|
+
activity = "moderate"
|
|
225
|
+
else:
|
|
226
|
+
activity = "inactive"
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"name": record.name,
|
|
230
|
+
"division": record.division,
|
|
231
|
+
"total_results": len(times),
|
|
232
|
+
"events_contested": events,
|
|
233
|
+
"mean_time": float(np.mean(times)),
|
|
234
|
+
"std_dev": float(np.std(times, ddof=1)) if len(times) >= 2 else None,
|
|
235
|
+
"best_time": float(min(times)),
|
|
236
|
+
"worst_time": float(max(times)),
|
|
237
|
+
"most_recent_date": most_recent,
|
|
238
|
+
"activity_level": activity,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Performance history summary
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def summarise_performance_history(
|
|
248
|
+
competitors: List[CompetitorRecord],
|
|
249
|
+
event_code: Optional[str] = None,
|
|
250
|
+
) -> List[Dict[str, Any]]:
|
|
251
|
+
"""
|
|
252
|
+
Return a ranked performance summary for a field of competitors.
|
|
253
|
+
|
|
254
|
+
Sorted by mean_time ascending (fastest first).
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
competitors: List of CompetitorRecord objects.
|
|
258
|
+
event_code: Optional filter.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of profile dicts (see profile_competitor()), sorted fastest first.
|
|
262
|
+
"""
|
|
263
|
+
profiles = [profile_competitor(c, event_code) for c in competitors]
|
|
264
|
+
with_data = [p for p in profiles if p["mean_time"] is not None]
|
|
265
|
+
without_data = [p for p in profiles if p["mean_time"] is None]
|
|
266
|
+
with_data.sort(key=lambda p: p["mean_time"])
|
|
267
|
+
return with_data + without_data
|