Skip to main content

atspm-report

Unit Tests PyPI version codecov Python versions License: MIT

Turns the output tables of the atspm package into per-region PDF reports of new traffic signal issues. Repeat alerts are suppressed against history you store between runs, so each report shows what changed, not what is already known.

Example Report

pip install atspm-report

What it does

ReportGenerator(config).generate(...) takes DataFrames (pandas or Ibis), runs the detectors, suppresses repeats, and returns PDFs as BytesIO plus the alert tables behind them.

It does not fetch data, read or write files, send email, or schedule anything. You supply the inputs, save the PDFs, and persist the returned history for the next run.

Alert types

Section Input Method
Phase termination (max-out) terminations CUSUM on daily percent max-out per phase
Detector detector_health CUSUM on daily share of anomalous bins
Pedestrian pedestrian + terminations Drop in ped services relative to the phase's own median, normalized within region
Missing data has_data CUSUM on daily share of missing 15-min bins
System outage has_data Region-wide missing data >= 30% for a day
Phase skip phase_wait Skips summed per phase over the retention window
Clearance intervals timeline Yellow/red durations vs. each phase's median and absolute minimums
Controller alarms timeline Alarms that fired again today, with six-week totals
Preempt frequency timeline Two-sided Poisson CUSUM of recent daily call counts vs. baseline
Phase / overlap conflicts timeline Interval overlap of conflicting indications (opt-in)

Pass only the inputs you have; every argument except signals is optional and a missing input just leaves its section out.

Example charts

Phase termination Phase termination

Detector Detector

Pedestrian Pedestrian

Phase skip Phase skip

Inputs

All inputs except signals come straight from atspm's output tables. DeviceId may be int or string in any table; it is cast to string internally.

signals (required, you provide this) one row per signal:

Column Notes
DeviceId Unique controller id, must match the atspm tables
Name Display name, e.g. 04100-Pacific at Hill
Region Grouping key, one PDF per region
group_name Not read here, but atspm's detector_health aggregation needs it

atspm tables and how much history to pass each run:

Argument atspm table Window
terminations terminations ~21 days
detector_health detector_health ~21 days
has_data has_data ~21 days
pedestrian full_ped ~21 days
phase_wait phase_wait 14 days (phase_skip_retention_days)
coordination_agg coordination_agg same as phase_wait, chart decoration only
timeline timeline exactly 1 day

The CUSUM detectors compute each entity's baseline from everything you pass, so keep the window consistent. timeline must be a single day: the clearance, alarm, preempt, and conflict checks all assume it, and alarm and preempt history is accumulated across runs for you.

detector_health needs the prediction and anomaly columns, which come from atspm's detector_health aggregation, not the plain actuations one.

Usage

from pathlib import Path
import pandas as pd
from atspm_report import ReportGenerator

STATE = Path('state')
STATE.mkdir(exist_ok=True)

def load(name):
    path = STATE / f'{name}.parquet'
    return pd.read_parquet(path) if path.exists() else pd.DataFrame()

config = {'verbosity': 1}   # every key has a default, see below

result = ReportGenerator(config).generate(
    signals=signals,                 # your own table
    terminations=terminations,       # atspm outputs, pandas or Ibis
    detector_health=detector_health,
    has_data=has_data,
    pedestrian=full_ped,
    phase_wait=phase_wait,
    coordination_agg=coordination_agg,
    timeline=timeline,
    # history from the previous run (empty on the first run)
    past_alerts={k: load(f'past_{k}') for k in ReportGenerator.ALERT_TYPES},
    alarm_history=load('alarm_history'),
    preempt_history=load('preempt_history'),
)

# 1. Persist history for the next run. Store it verbatim.
for alert_type, df in result['updated_past_alerts'].items():
    df.to_parquet(STATE / f'past_{alert_type}.parquet', index=False)
result['updated_alarm_history'].to_parquet(STATE / 'alarm_history.parquet', index=False)
result['updated_preempt_history'].to_parquet(STATE / 'preempt_history.parquet', index=False)

# 2. Do something with the PDFs. Empty dict means nothing new today.
for region, pdf in result['reports'].items():
    Path(f'report_{region}.pdf').write_bytes(pdf.getvalue())

Ibis tables work anywhere a DataFrame does, so a DuckDB, Polars, or Spark backend can do the heavy lifting:

import ibis
con = ibis.duckdb.connect('atspm.duckdb')
result = ReportGenerator(config).generate(
    signals=con.table('signals'),
    terminations=con.table('terminations').filter(ibis._.TimeStamp >= start),
    # ...
)

Returns

Key Contents
reports {region: BytesIO} PDFs, only for regions with content
alerts {alert_type: DataFrame} new alerts shown in the PDFs
ongoing_alerts Suppressed repeats with an OngoingSince column (only when include_ongoing_issues)
updated_past_alerts Persist. Next run's past_alerts
updated_alarm_history Persist. Next run's alarm_history
updated_preempt_history Persist. Next run's preempt_history
alarms Controller alarms listed this run

State between runs

generate() never writes anything. Save the three updated_* values and pass them back unchanged next run. Every retention and suppression rule is already applied inside, so do not filter, dedupe, or reshape them. Dropping them does not error; it silently resets suppression, alarm totals, or the preempt baseline.

Configuration

Pass a plain dict; every key has a default.

Key Default Effect
alert_suppression_days 21 A matching alert this recent in history is held back as ongoing, not reported as new
alert_retention_weeks 104 How long updated_past_alerts keeps history
alert_flagging_days 7 Maximum age of a reportable alert
include_ongoing_issues False Add an "Ongoing Issues" subsection under each section
phase_skip_alert_threshold 1 Aggregated skips must exceed this
phase_skip_retention_days 14 Trailing days of phase-skip data kept
maxout_cusum_threshold / maxout_zscore_threshold / maxout_percent_threshold / maxout_min_services 0.25 / 4.0 / 0.2 / 30 Phase termination thresholds, all must be exceeded
clearance_yellow_min_seconds / clearance_red_min_seconds / clearance_tolerance_seconds 3.5 / 0.5 / 0.1 Clearance interval limits
overlap_dual_indications_enabled + overlap_dual_indication_phases False, [] Phase green concurrent with same-numbered overlap yellow/red. Never suppressed
general_phase_conflicts_enabled False Standard conflicting phases both green
overlap_conflicts_enabled + overlap_conflict_numbers False, [] Phase/overlap conflicts for the listed overlaps
same_movement_color_conflicts_enabled False One movement showing two colors at once
*_excluded_signals / *_excluded_device_ids [] Skip signals with non-standard phasing from the conflict checks
figures_per_device 3 Charts per alert section
max_table_rows 10 Row cap per report table
custom_logo_path None Logo for the PDF header
verbosity 1 0 silent, 1 info, 2 debug

The full list with clearance noise filters and chart options is in the ReportGenerator docstring.

License

MIT, see LICENSE. Contributions welcome; open an issue for problems or help.

Release files for atspm-report 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for atspm-report 1.0.0
File Size Uploaded
atspm_report-1.0.0.tar.gz 133.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for atspm-report 1.0.0
File Interpreter ABI Platform
atspm_report-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 251.4 kB

Release files / atspm_report-1.0.0.tar.gz

Download URL atspm_report-1.0.0.tar.gz
Size 133.6 kB
Tags Source
SHA-256 checksum
How to use checksums
0f0a4dd06b5b2f751467f9d171b5805a366bdead8e1dcf5f981310c623926306
BLAKE2b-256 checksum
How to use checksums
56d744e079c6c6bf9c1ffbe4354e270567ee01b9f691bd31ff0f8ea764dcdc8c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / atspm_report-1.0.0-py3-none-any.whl

Download URL atspm_report-1.0.0-py3-none-any.whl
Size 117.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe9713bbf0c2dba8c35934c227c389294d823885539c6cd90566d0c9d0a1a6ae
BLAKE2b-256 checksum
How to use checksums
88dda101c7e7545b3705ce7778eb9907842a6a2002b111f853b183f0806bae57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

1.2.0

2 release files

1.1.0

2 release files

This release

1.0.0 This release

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page