Metadata-Version: 2.4
Name: honestclock
Version: 0.1.0
Summary: Point-in-time correctness for regulatory panel data: separate what was knowable from what merely happened.
License: MIT
Keywords: point-in-time,as-of,data-leakage,panel-data,regulatory-data,epa,echo,npdes,feature-engineering
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.5
Requires-Dist: numpy>=1.21
Provides-Extra: parquet
Requires-Dist: pyarrow>=10.0; extra == "parquet"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: hypothesis>=6.60; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: pyarrow>=10.0; extra == "dev"
Dynamic: license-file

# honestclock

[![CI](https://github.com/JonathanC27/honestclock/actions/workflows/ci.yml/badge.svg)](https://github.com/JonathanC27/honestclock/actions/workflows/ci.yml)

**Point-in-time correctness for regulatory panel data.** Separate what was
*knowable* at a forecast date from what merely *happened* — and make the
distinction a property of your types rather than a convention a reviewer has
to check.

```bash
pip install honestclock
```

## The problem

Records describing the same period arrive on different schedules.

In EPA's discharge-monitoring data, an effluent exceedance becomes visible a
median of **20 days** after the monitoring period, because the report that
reveals it is the report that carries it. A *missing* report generates no row
at all, and the non-compliance is not recorded for a median of **59 days**.

Between those two dates, a facility whose compliance has already broken down
still looks compliant to any filter written against the monitoring month.

The consequence is not a metric correction. It changes **who is eligible**: a
pool that looks clean at the forecast date already contains failures. One
dataset holds two prediction problems — detecting breakdowns already underway,
and forecasting those still to come — and conflating them inflates what a
precision number means.

## What this package does

```python
from honestclock import AsOfClock, PointInTimePanel, features as F

panel = PointInTimePanel(
    df,
    entity_keys=["permit_id", "parameter_code"],
    reference_col="month",          # the period the row describes
    availability_col="received_at", # when the row became knowable
)

clock = AsOfClock("2025-07", lookback=9, horizon=3)
visible = panel.visible_at(clock)          # cannot contain future data
labels  = panel.labels_at(clock, "exceedance")

X = F.assemble(
    visible,
    F.reporting_gap(visible),
    F.event_count(visible, "exceedance"),
    F.value_stats(visible, "ratio", clip_upper=5.0),
)
```

Three guarantees:

| | |
|---|---|
| **`AsOfClock`** | Derives a feature window and a label window from a forecast time and refuses to let them overlap. |
| **`VisibleFrame`** | *Cannot be constructed* holding an observation that postdates its clock. The constructor validates and raises `LookaheadError`. Feature builders accept only this type. |
| **`audit_lookahead`** | Tests a whole pipeline for dependence on unavailable data — catching leaks that never place a future row in a feature. |

## The audit is the part you probably need

A type can guarantee a frame holds no future rows. It cannot guarantee the
*pipeline* is honest. An eligibility filter, a join against a "latest"
snapshot, or a groupby computed before the cut can each admit information that
did not exist at forecast time, without any future row ever appearing.

`audit_lookahead` answers the question empirically, without reading your code:

> Does this function's output change when future rows are removed?

```python
from honestclock import audit_lookahead

report = audit_lookahead(df, build_features, clocks, "received_at")
report.raise_for_status()   # AssertionError if the output depends on the future
```

It reports how many future rows were withheld at each forecast time, so a
clean result cannot be mistaken for a strong one — an audit that withheld
nothing proves nothing, and says so (`report.vacuous`).

## A trap worth knowing about

`event_share` and `event_count` describe the same history and are **not**
interchangeable as ranking signals. A share is bounded in `[0, 1]` and
saturates: thousands of entities tie at `1.0`, a top-*k* cut lands inside the
tie group, and realised precision collapses toward the group's base rate.

In the study this package came from, a silence baseline built as a share
scored **17.1%** at a national top-100; the same history built as a count
scored **45.2%**. A 28-point difference that was an artefact of the data type,
not the signal. Any comparison between a model and a one-column heuristic must
build the heuristic as a count, or it is measuring representations.

## Scope, honestly

**This package is the point-in-time layer, not a model.** It gives you a
leak-resistant feature/label split and an auditor. It does not contain the
Circuit Rider AI model, its trained weights, or any facility-level score, and
it deliberately publishes no per-facility rankings.

**The ECHO adapter is a schema contract and a panel builder**
(`honestclock.echo`), not a downloader. EPA's bulk files are large and their
schema shifts between vintages; the contract is pinned in one module so a
change surfaces as one failing test rather than as a quietly wrong number.

**Domain-neutral core.** Nothing outside `honestclock.echo` knows about EPA.
Any panel with a reference time and an availability time works — drinking
water, air permits, workplace safety, clinical registries, credit files.

## Development

```bash
pip install -e ".[dev,parquet]"
pytest                                  # 153 tests, 100% branch coverage
ruff check src tests examples
ruff format --check src tests examples
mypy src/honestclock
python examples/quickstart.py
```

The suite includes property-based tests (via Hypothesis) asserting the
availability invariant over thousands of generated panels, and adversarial
tests in which deliberately leaky feature functions — modelled on real defects
— must be caught by the auditor.

### What is verified, and where

Because this package is about not making unsupported claims, here is the
status of its own:

| Claim | Verified by |
|---|---|
| 153 tests, 100% statement and branch coverage | local + CI (`--cov-fail-under=100`) |
| Annotations are accurate (`py.typed`) | `mypy` at default strictness, in CI |
| Works on the declared floor, `pandas>=1.5` | run against pandas 1.5.3 locally and in CI |
| Works on `pandas` 2.x | pandas 2.3.3 locally, 2.0 and latest in CI |
| Installs and works as a built wheel | clean-venv install, suite re-run against the artifact |
| The ECHO adapter parses real bulk data | run against a real 4.5M-row FY2026 DMR extract: 1.67M pair-months, invariants held, median availability lag 16 days |
| `requires-python >=3.9` | 3.9–3.13 all green in CI (9-job matrix) |

`mypy --strict` is *not* clean: pandas' stubs return `Any` from most
operations, and satisfying it would mean scattering casts that obscure the
logic. Default strictness is the level this package claims.

## Provenance

Extracted from the evaluation pipeline of *Circuit Rider AI: Assistance-Oriented
Compliance Triage for Small Wastewater Systems from Public Data*. The
20-day/59-day latency figures, the share-versus-count artefact, and the two
leak shapes the auditor models are findings from that work.

## License

MIT.
