ECHO Enforcement Cases
Environmental enforcement actions and penalties from the EPA's ECHO database.
echo_enforcement_cases — the dataset name to pass to the Obscura API.
What one row means
One EPA enforcement case activity (one ACTIVITY_ID) from EPA ECHO's `case_downloads.zip` -> `CASE_ENFORCEMENTS.csv`: a single formal or informal enforcement action (an administrative order, judicial referral, penalty, etc.) taken against a regulated facility under EPA or a delegated state/local program, as of EPA ECHO's latest weekly full-refresh bulk extract.
One row per (activity_id).
Point-in-time availability
Every Obscura dataset carries available_date: the calendar day the publisher made the row
available, day-of, with no session rounding. It is the one column a backtest filters on, and it means the
same thing on every dataset in the catalog.
For echo_enforcement_cases: published: available_date = status_date + 14 days, EPA ECHO's ~2-week weekly-refresh lag from a case's status-date to its appearance in the public bulk extract.
Refresh cadence
Obscura refreshes echo_enforcement_cases daily — the most frequent scheduled job that re-collects or re-exports it. This is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when a row became public is recorded per row in available_date.
Schema — 20 columns
The full public column list for echo_enforcement_cases, with the meaning of every field. The same
schema is served unauthenticated at https://api.obscura.trade/v1/catalog/echo_enforcement_cases.
| Column | Type | Description |
|---|---|---|
| activity_id | text · not null | EPA's unique identifier for this enforcement case activity, from the `CASE_ENFORCEMENTS.csv` `ACTIVITY_ID` column. Primary key; rows without one are dropped at parse time. Stable across weekly refreshes — the upsert conflict target. |
| status_date | date · not null | The case's own status/conclusion date (`CASE_STATUS_DATE`, falling back to `ACTIVITY_STATUS_DATE`) — e.g. when a Final Order was issued or a case was administratively closed. A period/event date, rehomed away from available_date; NOT the date EPA disclosed that fact publicly. |
| case_number | text | EPA's human-readable case docket number from `CASE_NUMBER`, formatted as `{EPA-region}-{fiscal-year}-{sequence}`. A secondary, source-facing identifier distinct from the surrogate activity_id primary key; nullable when EPA has not yet assigned one. |
| case_name | text | Free-text case title from `CASE_NAME`, typically embedding the respondent facility/company name and the enforcement program. Display + fuzzy name-matching only — there is no facility_id/registry_id foreign key on this table linking a case to a specific echo_facilities row. |
| state_code | text | Two-letter US state postal abbreviation of where the enforcement action's subject facility/respondent is located, from `STATE_CODE`. Frequently blank in the source extract. |
| region_code | text | EPA administrative region number (01-10) that owns/leads the case, from `REGION_CODE`. An EPA organizational/geographic grouping, not a full facility address — see echo_facilities for lat/long. |
| fiscal_year | integer | US federal fiscal year (Oct-Sep) the case activity is attributed to, from `FISCAL_YEAR`. Not a calendar-year date and not the same fiscal year implied by status_date, which can fall in an adjacent FY near the Sep/Oct boundary. |
| activity_type_desc | text | Category of enforcement activity from `ACTIVITY_TYPE_DESC`, e.g. "Administrative - Formal", "Judicial", distinguishing formal/informal administrative actions from judicial referrals. |
| activity_status_desc | text | Current lifecycle status of the case activity from `ACTIVITY_STATUS_DESC`, e.g. "Closed". The row is upserted in place as status progresses through a case's life, so a re-refresh can flip this in place under the same activity_id. |
| lead | text | Lead enforcement agency for the case from `LEAD`, e.g. "EPA", distinguishing EPA-led actions from those led by a delegated state/local co-regulator under its authorized program. |
| enf_outcome_desc | text | Formal enforcement outcome/resolution type from `ENF_OUTCOME_DESC`, e.g. "Final Order With Penalty". Independent of whether a penalty dollar amount was actually assessed (see total_penalty_assessed_amt). |
| total_penalty_assessed_amt | double precision | Total monetary penalty assessed against the respondent in this case, in USD, from `TOTAL_PENALTY_ASSESSED_AMT`. NULL for cases still in process or with no monetary penalty component. |
| total_cost_recovery_amt | double precision | Total dollar amount EPA recovered for its own cleanup/response costs in this case, in USD, from `TOTAL_COST_RECOVERY_AMT`. Distinct from total_penalty_assessed_amt (a punitive penalty) and total_comp_action_amt (compliance-action cost). |
| total_comp_action_amt | double precision | Total dollar amount the respondent was required to spend on compliance actions/injunctive relief (e.g. pollution-control equipment, remediation) as part of the case resolution, in USD, from `TOTAL_COMP_ACTION_AMT`. |
| voluntary_self_disclosure_flag | text | 'Y'/'N' text flag from `VOLUNTARY_SELF_DISCLOSURE_FLAG` indicating the violation was voluntarily self-disclosed by the respondent under EPA's Audit Policy. Stored as source text rather than boolean. |
| multimedia_flag | text | 'Y'/'N' text flag from `MULTIMEDIA_FLAG` indicating the case spans multiple environmental media/statutes rather than a single program. Stored as source text rather than boolean. |
| enf_summary_text | text | Free-text case narrative from `ENF_SUMMARY_TEXT` describing the enforcement action — violations, respondents, and remedy. ~66% populated; the richest NLP/entity-extraction signal in the file, not derivable from any other kept column. |
| doj_docket_nmbr | text | Department of Justice docket number from `DOJ_DOCKET_NMBR` for judicially-referred cases (~2.7% of rows). A court/legal cross-reference identifier distinct from case_number (the EPA docket). |
| hq_division | text | EPA HQ division/program code from `HQ_DIVISION` (e.g. `CER`, `TOX`), ~90% populated. Encodes the lead environmental program/media, a categorical dimension finer than multimedia_flag. |
| available_date | date | PUBLIC-availability date = status_date + 14 days, EPA ECHO's weekly-refresh lag between a case's status-date and its appearance in the public bulk extract. DB-generated (STORED), read-only; the point-in-time column to filter/join on — NEVER status_date itself. |
Access echo_enforcement_cases
Two delivery paths, one identifier. Both require an Obscura account and an active subscription; the catalog entry and the schema above are public.
import obscura
client = obscura.Client("obs_live_…")
df = client.query(
dataset="echo_enforcement_cases",
start="2024-01-01",
)
Create a free account Browse all 95 datasets
Frequently asked questions
What is in the echo_enforcement_cases dataset?
Environmental enforcement actions and penalties from the EPA's ECHO database. One EPA enforcement case activity (one ACTIVITY_ID) from EPA ECHO's `case_downloads.zip` -> `CASE_ENFORCEMENTS.csv`: a single formal or informal enforcement action (an administrative order, judicial referral, penalty, etc.) taken against a regulated facility under EPA or a delegated state/local program, as of EPA ECHO's latest weekly full-refresh bulk extract.
How do I avoid look-ahead bias with echo_enforcement_cases?
Filter on echo_enforcement_cases.available_date, the day the publisher made the row public. For this dataset that date is derived as follows — published: available_date = status_date + 14 days, EPA ECHO's ~2-week weekly-refresh lag from a case's status-date to its appearance in the public bulk extract. A query of the form WHERE available_date <= '<as-of date>' never sees a row before it existed.
In what formats can I get echo_enforcement_cases?
As a Parquet bulk export (POST https://api.obscura.trade/v1/download) or as JSON from the typed query API (POST https://api.obscura.trade/v1/query), both with dataset="echo_enforcement_cases". The column schema is public at https://api.obscura.trade/v1/catalog/echo_enforcement_cases.
How often is echo_enforcement_cases updated?
Obscura refreshes echo_enforcement_cases on a daily schedule — that is the most frequent scheduled job that re-collects or re-exports the table. It is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when the publisher makes a row available is described by the availability rule above, and is recorded per row in available_date.