Temporal analysis counts gaps between distinct calendar days, measures interval regularity, and checks sortedness using adjacent values. Date analysis requires at least 30 distinct days.
Modal cadence and gaps¶
The pass sorts and deduplicates the observed days, then measures the gaps between consecutive distinct days. The modal gap (most frequent) is the column’s assumed cadence; the median gap is reported alongside it. Gap semantics depend on the cadence:
daily cadence (modal 1): missing weekdays are counted, and a pure weekend bridge is not a gap. Holidays are not modeled — the finding’s reference says so explicitly (
weekdays, sourceinferred).weekly cadence (modal 7): gaps are counted in weeks once more than ten days elapse.
other cadences: a gap is flagged when the elapsed time exceeds 1.5× the modal interval and the modal interval plus two days; the count is expressed in modal-interval units and marked approximate.
At most 10 gap events are reported, ordered by missing count. Each
gaps finding carries its reference — the cadence or weekday rule that
defines “missing” — because an empty interval is only meaningful against
an expectation.
Irregular intervals¶
The coefficient of variation (CV) of the day gaps measures variation in
observed spacing. A CV above 0.5, with at least ten gaps, produces an
intervals statement containing the modal interval, median interval, and
CV. Irregular spacing can affect resampling and lag features that assume
a regular time index.
Sortedness¶
The scan compares adjacent non-null values and requires at least 100
values. It reports strictly increasing or non-decreasing order when those
properties hold. These ordering statements describe the relationship
between row order and column values. The pass does not emit a finding for
an unsorted column.
import datetime as dt
import random
import polars as pl
import onceover
rng = random.Random(51)
# Frame A: a daily snapshot calendar with a two-weekday hole.
base = dt.date(2024, 1, 1)
skip = {dt.date(2024, 2, 8), dt.date(2024, 2, 9)}
business, i = [], 0
while len(business) < 3_000:
d = base + dt.timedelta(days=i // 10)
if d not in skip:
business.append(d)
i += 1
daily = pl.DataFrame({"row_id": range(len(business)), "snapshot_date": business})
report = onceover.profile(daily)
for f in report.findings:
if f["category"] in {"gaps", "ordering"}:
m = f["evidence"][0]["metrics"]
print(f"[{f['category']:9s}] {f['claim']}")
if f["category"] == "gaps":
print(f" reference={m.get('reference')} cadence_days={m.get('cadence_days')}")
# Frame B: a bursty event log — an uneven clock with silences.
events, day = [], 0
while len(events) < 3_000:
for _ in range(5): # five consecutive active days
day += 1
events.append(day)
day += rng.randrange(9, 15) # then a silence
log_dates = [dt.date(2024, 1, 1) + dt.timedelta(days=d) for d in events]
rng.shuffle(log_dates) # logs arrive out of order
log = pl.DataFrame({"event_id": range(len(log_dates)), "event_day": log_dates})
log_report = onceover.profile(log)
log_gaps = [f for f in log_report.findings if f["category"] == "gaps"]
print("gap events in the event log:", len(log_gaps))
for f in log_gaps[:2]:
print(f"[gaps ] {f['claim']}")
for f in log_report.findings:
if f["category"] == "intervals":
print(f"[intervals ] {f['claim']}")[gaps ] column snapshot_date is missing 2 business days at 2024-02-07..2024-02-10 (3 days elapsed)
reference=weekdays cadence_days=1
[ordering ] column row_id is strictly increasing with row order
[ordering ] column snapshot_date is non-decreasing with row order
gap events in the event log: 10
[gaps ] column event_day is missing 10 business days at 2024-01-21..2024-02-04 (14 days elapsed)
[gaps ] column event_day is missing 10 business days at 2024-06-01..2024-06-15 (14 days elapsed)
[intervals ] column event_day has irregular spacing (modal 1 days, coefficient of variation 1.41)
The snapshot calendar is missing two weekdays under the inferred weekday reference. Its dates are non-decreasing in row order. The event log has irregular intervals: gaps are one day within bursts and ten or more days between bursts. Those longer intervals also produce gap events against the daily modal interval. The report retains the ten largest gap events; the example prints two of them. The shuffled event-log date column receives no sortedness finding.
Datetime columns¶
Datetime analysis uses day granularity. It does not detect time-of-day patterns, intraday gaps, or session boundaries (Directions). Gap counts use all distinct days, but their interpretation depends on the inferred cadence or weekday reference.
The breaks time axis¶
The breaks pass scans each numeric series along one time axis, chosen in this order: a column declared with hints={"time": "<column>"} when it is a date, datetime, or integer column; otherwise the first date or datetime column; otherwise a year-like integer column — every present value in 1900..=2100 with at least 8 distinct values. Panels keyed by integer years engage through the third rule with no declaration needed. Breakpoints on an integer axis are reported as the raw axis values. With no qualifying column the pass records why it skipped in coverage and emits nothing.
Transient excursions¶
The breaks pass looks for change inside a single time-ordered series.
Its sustained types — level_shift, trend_change, variance_change
— describe a series that moves and stays moved. The same pass also
scans for the opposite shape: an excursion, a stretch of points that
departs from the local baseline and returns to it — a one-day demand
collapse, a three-hour latency failure. Excursions are reported as
changes findings of type transient_excursion (form excursion),
with the event’s start, end, and peak in the evidence.
The baseline is the series’ own recent past. When the median interval between timestamps divides a week evenly and the series spans at least two and a half weeks of periods, the baseline for a point is the median of the values at the same time of week in the three neighbouring weeks on each side — matched by timestamp, not by position, so a missing or sampled-out grid point does not shift the phase. Otherwise the baseline is a centered rolling median over 97 points. Deviations are scaled by the median absolute residual (× 1.4826); points beyond 8 robust standard units are grouped into events, bridging short unflagged gaps. An event’s span is capped — one period on the seasonal baseline, half the window on the rolling one — so a sustained level shift, whose run of deviating points exceeds the cap, is left to the level-shift scan rather than reported as a very long excursion. An event at the end of the series counts: no return to baseline is required. At most ten events per series are reported, ranked by peak deviation.
The example plants a single one-day collapse in eight weeks of hourly requests with a weekly pattern. The scan reports that day, at a peak of −13.2 robust units, and nothing else.
import datetime as dt
import random
import polars as pl
import onceover
rng = random.Random(3)
start = dt.datetime(2026, 1, 5, 0, 0)
ts, vals = [], []
for h in range(24 * 7 * 8):
t = start + dt.timedelta(hours=h)
weekly = 100 + 30 * (1 if t.weekday() < 5 else 0.4)
v = weekly + rng.gauss(0, 4)
if dt.datetime(2026, 2, 11) <= t < dt.datetime(2026, 2, 12):
v -= 55 # the planted one-day collapse
ts.append(t)
vals.append(round(v, 2))
report = onceover.profile(pl.DataFrame({"ts": ts, "requests": vals}))
for f in report.findings:
if f.get("type") == "transient_excursion":
print(f["claim"])requests dips between 2026-02-11 00:00:00 and 2026-02-11 23:00:00: peak value 70.49 at 2026-02-11 08:00:00, 13.2 robust z from the local baseline