# NJDOE SPR Discipline and Safety

```toml ergo
[dataset]
ergo = "0.4"
slug = "spr-discipline-safety"
title = "NJDOE SPR Discipline and Safety"
publisher = "NJ Department of Education, Office of Performance Reports"
subject = "https://www.nj.gov/education/spr/"
source_url = "https://www.nj.gov/education/sprreports/download/DataFiles/2024-2025/Database_DistrictStateDetail.xlsx"
pitfall = "These are separate 2024-25 records of reported incidents and responses, not a combined safety score or evidence of cause, effectiveness, or budget performance."
status = "live"
confidence = "A"
updated = "2026-07-30"
implementation = "https://github.com/lavallee/njschooldata"

[dataset.coverage]
period = "2024-25 School Performance Report edition"
grain = "district headline x metric; district x HIB nature x metric; district x All Students publisher row x metric; district x student group x metric and district x grade x metric"
entities = "666 district reporting units; 576 of 587 rendered budget districts match by exact code"

[dataset.access]
keys = ["school_year", "district_code", "tab_id", "dimension_label", "metric_id"]
```

## Consumer question and boundary

AQ-062 adds one product-owned question to the district report:

> What did the 2024-25 SPR record about this district's reported incidents,
> HIB investigations, removals, police notifications, and arrests before a
> reader treats those records as proof of safety, cause, or budget
> performance?

Aquifer owns official-workbook custody, exact source literals and locators,
publisher identifiers and defects, three source grains, value states,
immutable releases, and offline snapshots. This repo owns the district-report
question, the bounded three-table mart, exact-code join, wording, placement,
static UAT, and rollback.

On 2026-07-30 a second product-owned question joined it, on the district
profile rather than the budget report:

> Which students in this district were suspended, referred to police and
> arrested — and where New Jersey did not say, what kind of not-saying was it?

The panels do not rank districts, construct a safety score, add incident-type
counts, treat a police notification as an arrest, reverse engineer a
suppressed denominator, compute a disproportionality ratio or risk index
between two student groups, or align the SPR record to a later UFB budget
year. Headline incident rows, HIB-nature rows, and the publisher's
`All Students` student-group rows remain separate marts. Student-group and
grade rows are now projected, in their own additive table, at district level
only — the release's school coverage is 259 school codes and nothing renders
on a school profile.

## Why the count/percent choice went to the percent

Both a headcount and a percentage exist for every removal, police-notification
and arrest measure. The surface leads with the **percentage**, and the reason
is measured rather than aesthetic.

The percentage is a **within-group rate**: the share of that group's own
students, against that group's own denominator, not the group's share of the
district total. South Orange-Maplewood's Black students carry 25 students and
1.60%, which implies about 1,563 Black students — the district's own Black
enrollment, not its total.

And the two columns are not suppressed alike. For male and female students
NJDOE withholds 12,467 of 12,654 district headcounts each (98.522%) while
withholding only 2,040 of the matching percentages (16.121%). In 8,328 male
and 8,506 female cells the withheld headcount sits beside a **published exact
zero** percentage. A count-led surface would print "withheld" over cells where
the state plainly said nobody, and would lose sex entirely in 98.5% of
districts. A rate-led surface keeps it in 83.9%. The headcount is still shown
wherever NJDOE published it, and its absence is stated where it did not.

## How holey the student-group surface actually is

Measured on what the module renders — three measures across twelve student
groups in 666 districts, 23,976 cells — reading the headcount and the
percentage together:

| state | cells | share |
| --- | ---: | ---: |
| a published rate | 7,167 | 29.89% |
| a published exact zero | 7,392 | 30.83% |
| `<5` — headcount masked below five (often a zero) | 2,253 | 9.40% |
| withheld outright | 7,164 | 29.88% |

Per student group, the withheld share runs inversely to group size:

| student group | rate | zero | `<5` | withheld | withheld % |
| --- | ---: | ---: | ---: | ---: | ---: |
| Non-Binary/Undesignated Gender | 3 | 30 | 6 | 1,959 | **98.05** |
| American Indian or Alaska Native | 39 | 125 | 31 | 1,803 | **90.24** |
| Asian, Native Hawaiian, or Pacific Islander | 342 | 533 | 201 | 922 | 46.15 |
| Two or More Races | 340 | 571 | 221 | 866 | 43.34 |
| Black or African American | 595 | 585 | 281 | 537 | 26.88 |
| Female | 755 | 850 | 6 | 387 | 19.37 |
| Male | 815 | 790 | 6 | 387 | 19.37 |
| White | 713 | 781 | 366 | 138 | 6.91 |
| Economically Disadvantaged Students | 789 | 738 | 366 | 105 | 5.26 |
| Hispanic/Latino | 771 | 798 | 384 | 45 | 2.25 |
| Students with Disabilities | 798 | 800 | 385 | 15 | 0.75 |
| All Students | 1,207 | 791 | 0 | 0 | 0.00 |

Grade is nearly complete by comparison — 0.60% to 2.93% withheld — but that
completeness is mostly published zeros, not published rates: Grade PK is
78.6% zero and 0.33% rate. **The early grades are a substantive finding about
who gets suspended, not a data gap.**

By district enrollment band, on the same surface:

| enrollment | districts | cells | withheld % | rate % |
| --- | ---: | ---: | ---: | ---: |
| 0–249 | 81 | 2,916 | 51.27 | 6.31 |
| 250–499 | 118 | 4,248 | 46.45 | 12.95 |
| 500–999 | 134 | 4,824 | 40.61 | 23.32 |
| 1,000–2,499 | 173 | 6,228 | 36.71 | 38.34 |
| 2,500–4,999 | 91 | 3,276 | 29.52 | 47.56 |
| 5,000–9,999 | 52 | 1,872 | 30.02 | 53.74 |
| 10,000+ | 16 | 576 | 27.43 | 60.24 |

In 182 of 666 districts no student group carries a published suspension rate
at all. In most of those the state published zeros rather than withholding —
which is why the surface must render a printed zero and a withheld cell as
visibly different things.

## Exact offline pin and mart

The committed pin is
`contracts/aquifer/nj-spr-discipline-safety.lock.json`:

- release: `nj-spr-discipline-safety-aq062-refresh2`;
- manifest SHA-256:
  `d5e418cc02eb9ba1461d9582351a8ada885dd9ea50b80b514aa742849477d9ad`;
- SQLite SHA-256:
  `77d7eb7d26f084031bdd7b698f138edb1900ca1fdc4d7d3245339dcdcd35c91e`;
- byte count: `2,355,728,384`.

The consumer verifies the entire pin before cloning its existing warehouse.
It checks all 207,547 physical rows, 2,344,237 typed values, 54 metric
definitions, six tab populations, and the complete producer value-state
receipt. It then projects only district records needed by the owned question,
hashes every unmanaged table, and atomically replaces the clone:

| Consumer table | Rows | Deterministic row SHA-256 |
| --- | ---: | --- |
| `discipline_safety_district_headline` | 8,658 | `be24b7d4f478d168e8844171e86c69fb3c5c36dadebde2a48f47eda3e4f7ff47` |
| `discipline_safety_district_hib_nature` | 15,984 | `5de748b5e3cd4cb71d6c48a11b0709c99855a39bfc7032b20d37f26d968ec0df` |
| `discipline_safety_district_all_students` | 25,308 | `60d4fe95427b6b6f632ea272ea15640da996d77d34b93ca6c9ce3cf905b2811b` |
| `discipline_safety_district_group_grade` | 570,342 | `62633307e686472fced4bbed37e2e37a5b7ad352b619a5cc778a5eb1add49f17` |
| `discipline_safety_metric_catalog` | 54 | `0fc5c2d84b10c96e40f7ac081c457d262194fea280a4faba942690c157eb5e13` |

The headline mart retains 4,637 reported and 4,021 exact-zero values. The
HIB-nature mart retains 6,713 reported, 8,605 exact-zero, and 666 structural
`n/a` values. The `All Students` mart retains 8,401 reported and 16,907
exact-zero values.

The first three tables' hashes are asserted by the adapter itself
(`LEGACY_ROW_SHA256`), so the 2026-07-30 widening is provably additive: it
cannot change a byte of what was already there.

The widened `discipline_safety_district_group_grade` table holds 325,103
exact-zero, 62,047 reported and **183,192 suppressed** values — every
district-level suppression the pinned artifact records. Before it existed the
consumer warehouse had a suppression rate of exactly 0.000%, because the three
narrow marts kept precisely the slices NJDOE never suppresses. That was an
artefact of the projection, not a property of the data.

It is deliberately narrower in columns than its siblings: at 570,342 rows the
repeated metric label, definition, publisher defect and source URL would add
several hundred megabytes of identical text, so those live once per metric in
`discipline_safety_metric_catalog`. What is never trimmed is `value_state` and
`reported_literal`, carried verbatim so `0`, `0.00%`, `*`, `<5` and `n/a`
remain five distinguishable publisher acts.

Structural `n/a` does not occur in this slice — all 666 of the release's
district-level structural absences sit in the HIB-nature mart, under
`No Identified Nature`. The adapter asserts that as an exact expectation rather
than assuming it.

## Issues

### Two suppression markers that do not mean the same thing

```toml ergo
[issue]
id = "two-suppression-markers"
title = "Suppressed cells carry either `*` or `<5`, and `<5` bounds the value where `*` says nothing at all"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "Release-wide the 750,782 suppressed values split 661,604 `*` and 89,178 `<5`. In the district student-group and grade slice the split is 148,999 `*` and 34,193 `<5`. No suppressed cell carries a numeric value."
misuse = "Coalescing both markers to NULL or to a single 'suppressed' label. `<5` bounds the headcount below five; `*` states only that the publisher withheld the cell. Collapsing them discards a bound the publisher chose to give. Note the bound runs from zero, not one — see less-than-five-includes-zero."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["reported_literal", "value_state"]
rows = "value_state = 'suppressed'"
```

The mart carries `reported_literal` verbatim so the two stay separable, and the
profile module renders them as two different words — **Fewer than 5** and
**Withheld**.

### `<5` is a headcount marker only

```toml ergo
[issue]
id = "less-than-five-is-count-only"
title = "`<5` never appears on a percentage metric; the percentage partner of a `<5` headcount is always a bare `*`"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "All 89,178 `<5` literals in the release sit on the students-or-incidents unit; the percent unit carries only `*` (322,625 cells). Joining each count metric to its percent partner on the same source row gives the pair ('<5','*') in every student group and every grade."
misuse = "Resolving a cell from its percentage alone. The percentage says only `*`, so a reader who never looks at the headcount column throws away the below-five bound on 34,193 district cells."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["reported_literal", "unit"]
rows = "reported_literal = '<5'"
```

Every cell on the profile module is resolved from the count/percent **pair**
rather than from either column alone. That is the only way the one-to-four
bound survives to the page.

### `<5` includes zero, so it is not evidence that anything happened

```toml ergo
[issue]
id = "less-than-five-includes-zero"
title = "The `<5` marker bounds a headcount at zero to four, not one to four — most `<5` cells are masked zeros"
effect = "corrupts"
core = true
type = "suppression"
status = "mitigated"
detection = "Grades are mutually exclusive and partition a district, so the published grade headcounts plus the `<5` grade cells must reconcile to the All Students headcount. Among the 574 districts with a published All Students any-suspension count and no `*` grade cell, 96 have an All Students total smaller than their number of `<5` grade cells — district 0070 suspended one student in total and carries ten `<5` grade cells, district 0080 one student and nine. Those cells cannot all contain at least one student, so at least nine of ten are exactly zero."
misuse = "Reading `<5` as 'between one and four students', which turns a masked zero into an incident. In a small district that inverts the whole grade table: ten grades each reading 'fewer than 5' suggests discipline spread across every grade when the district suspended one child."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["reported_literal"]
rows = "reported_literal = '<5'"
```

The module says **0 to 4 students** rather than "between one and four", and its
legend calls the marker a masked headcount that is often a zero. This is the
mirror image of the published-zero problem: there, a zero gets read as a gap;
here, a gap gets read as an incident.

### The small-cell rule is applied inconsistently

```toml ergo
[issue]
id = "small-cell-rule-is-inconsistent"
title = "NJDOE masks 34,193 small district headcounts as `<5` but publishes 13,939 headcounts of one to four outright, so a published rate can rest on a single student"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "In the district student-group and grade slice, 13,939 reported headcounts fall between 1 and 4 while 34,193 comparable cells are masked `<5`; 15,079 reported headcounts are 5 or more. Nine districts publish an American Indian or Two or More Races rate of exactly 10.00% off a headcount of 1. On the three-measure student-group surface, 1,960 of 7,167 published rates (27.3%) rest on a headcount below five."
misuse = "Treating a published rate as evidence of a pattern. One student in a group of ten prints as 10%, which is arithmetic and not a finding; and treating the `<5` mask as a guarantee that every small cell is protected."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["numeric_value"]
rows = "value_state = 'reported' AND unit = 'students-or-incidents' AND numeric_value < 5"
```

The module states a floor and enforces it in the chart: a rate whose published
headcount is one to four keeps its table row, with the headcount beside it, and
is shown on the dot plot as a labelled row with no dot. Every rate that does
carry a dot is shown with its headcount, or with a note that the headcount was
withheld.

### Male and female headcounts are withheld while their rates are published

```toml ergo
[issue]
id = "sex-count-withheld-rate-published"
title = "For Male and Female alone, NJDOE suppresses 98.522% of district headcounts and only 16.121% of the matching percentages"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "Male and Female each carry 12,654 district count cells and 12,654 percent cells. 12,467 count cells are suppressed against 2,040 percent cells. Every other student group suppresses the two columns at identical rates to three decimals. Pairing each count with its percent on the same source row, 8,328 Male and 8,506 Female cells carry a suppressed `*` count beside a published `0.00%` percentage. `spr.discipline.arrests.any-count` is suppressed for 659 of 666 districts for both."
misuse = "Building a student-group surface on headcounts. Sex becomes unreportable in 98.5% of districts, and in the 8,328/8,506 zero-percentage cells a count-led reading prints 'withheld' over a cell where the publisher stated an exact zero."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["value_state", "reported_literal"]
rows = "conformed_dimension_label IN ('Male','Female')"
```

The adapter asserts the four suppression counts as a fail-closed expectation
(`EXPECTED_SEX_ASYMMETRY`) so a refreshed release that changes NJDOE's
disclosure behaviour breaks the build rather than the page. The module leads
with the percentage, shows the headcount wherever it exists, and says in place
that the number was withheld where it does not.

### Suppression runs inversely to student-group size

```toml ergo
[issue]
id = "suppression-tracks-group-size"
title = "The smallest student groups are withheld most: 98.522% of Non-Binary cells and 92.327% of American Indian cells against 16.603% for Students with Disabilities"
effect = "misleads"
core = true
type = "coverage"
status = "mitigated"
detection = "Each student group carries 25,308 district cells. Suppressed counts run Non-Binary/Undesignated Gender 24,934; American Indian or Alaska Native 23,366; Male 14,507; Female 14,507; Asian/NHPI 14,068; Two or More Races 13,388; Black or African American 9,920; White 5,614; Economically Disadvantaged 5,232; Hispanic/Latino 4,572; Students with Disabilities 4,202; All Students 0. Non-Binary carries 14 reported values of 25,308 (0.055%) and American Indian 194 (0.767%)."
misuse = "Rendering only the cells that were published. Because the holes are patterned by group size, a chart of the surviving cells redraws discipline as a White, Hispanic, Black, economically-disadvantaged and disabled-student phenomenon — those being the groups whose cells survive. Equally: computing any ratio, risk index or 'X times more likely' statement between two groups censored at different rates."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["value_state"]
rows = "dimension_kind = 'student-group'"
```

Every group keeps its row on the module, in the table and on the dot plot, with
the reason for its absence written where the value would be. No ratio between
two groups is computed anywhere on the site.

### A published zero is not a gap, and the early grades are mostly zeros

```toml ergo
[issue]
id = "published-zero-is-not-a-gap"
title = "`0` and `0.00%` are exact publisher zeros, not missing values, and they are the majority state in the early grades and the small districts"
effect = "corrupts"
type = "suppression"
status = "mitigated"
detection = "The district student-group and grade slice holds 325,103 exact-reconciliation-zero cells (`0` 154,130 and `0.00%` 170,973) against 183,192 suppressed and 62,047 reported. Grade PK is 78.6% published zero and 0.33% published rate. Across the whole district level, 97.733% of cells in districts under 250 students are either a suppression marker or a published zero, and the Pearson r between log10 enrollment and district suppression rate is only -0.2756 (n=665) — suppression is near-flat at 29-31% above 250 students while the reported rate rises 13.7x across the range."
misuse = "Rendering a published zero as 'no data', or a `*`/`<5` as zero. The two errors reverse a finding in opposite directions. Also: writing that small districts are censored more heavily — they are not; they are recorded as zero rather than published as a number."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["value_state", "reported_literal"]
rows = "value_state IN ('exact-reconciliation-zero','suppressed')"
```

The module gives a printed zero its own word (**None**, with "the state
published a zero" beneath it) and its own styling, distinct from **Withheld**
and from **Fewer than 5**.

### Three dimensions exist in the schema and are effectively never published

```toml ergo
[issue]
id = "dimensions-effectively-never-published"
title = "Non-Binary/Undesignated Gender, American Indian or Alaska Native and Grade PK are present in every district row and almost never carry a value"
effect = "misleads"
type = "coverage"
status = "mitigated"
detection = "District-level reported cells: Non-Binary/Undesignated Gender 14 of 25,308 (0.055%), and 34 of 120,650 across all organization levels (0.028%); American Indian or Alaska Native 194 of 25,308 (0.767%); Grade PK 22 of 19,228 (0.114%). On the three-measure module surface Non-Binary is withheld in 98.05% of cells and American Indian in 90.24%."
misuse = "Reading these rows as zero, or as evidence that these students are never disciplined. The rows are withheld, not empty."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
rows = "conformed_dimension_label IN ('Non-Binary/Undesignated Gender','American Indian or Alaska Native','Grade PK')"
```

### The arrest columns are named after police notifications

```toml ergo
[issue]
id = "arrest-columns-are-named-police"
title = "The source fields behind the arrest measures are named PoliceCount and PolicePercent while the published layout defines them as all incidents leading to arrest"
effect = "misleads"
type = "definitional"
status = "mitigated"
detection = "metric_definitions carries publisher_defect = 'Source field says Police; layout defines all incidents leading to arrest.' on spr.discipline.arrests.any-count and spr.discipline.arrests.any-percent, and 'Source field is OtherIncidents; layout defines a total unique count.' on spr.discipline.police-notifications.unique-count."
misuse = "Reconciling the arrest column against the separate police-notification column, or treating one as a subset of the other. They are different tabs with differently-named source fields, and the site never adds or nets them."

[issue.scope]
tables = ["discipline_safety_metric_catalog", "discipline_safety_district_group_grade"]
columns = ["publisher_defect"]
rows = "metric_id LIKE 'spr.discipline.arrests.any-%' OR metric_id = 'spr.discipline.police-notifications.unique-count'"
```

The defect is carried into `discipline_safety_metric_catalog` and printed on the
module beneath the table, as published and unreconciled.

### These are reported incidents, so they measure reporting practice too

```toml ergo
[issue]
id = "reported-incidents-measure-reporting"
title = "Every measure here is an administrative record of what a district recorded and referred, so a diligent recorder looks worse than a district that handles the same behaviour informally"
effect = "misleads"
core = true
type = "definitional"
status = "open"
detection = "All 54 metrics carry value_origin = 'reported' and universe text describing publisher-reported counts and percentages. There is no independent incident audit in the file and no adjustment for reporting practice."
misuse = "Reading any of these numbers as behaviour, crime, safety, school quality or the effect of a budget decision; combining removals, police notifications and arrests into a single index or safety score; or ranking districts on any of them."

[issue.scope]
tables = ["discipline_safety_district_group_grade", "discipline_safety_district_headline", "discipline_safety_district_all_students"]
```

Stated on the module beside the numbers rather than in distant methodology
text, and the three measures are never combined.

## Static rendering and omissions

The canonical profile build writes 587 district budget reports. Exactly 576
join the mart by zero-padded NJDOE district code and render the panel. These 11
exact-code nonmatches omit it instead of falling back to names:

| Code | District route |
| --- | --- |
| `0130` | `atlantic-highlands` |
| `0200` | `bass-river` |
| `0695` | `camden-co-ed-serv-comm` |
| `0810` | `chesilhurst` |
| `2120` | `henry-hudson-regional-monmouth-2120` |
| `2160` | `highlands` |
| `2305` | `hunterdon-county-educational-services-commission` |
| `4700` | `sea-isle` |
| `4720` | `seaside-park` |
| `5450` | `warren-co-special-service` |
| `5490` | `washington-burlington` |

South Orange-Maplewood renders 77 unique reported incidents and 1.1 incidents
per 100 students; 38 students receiving any suspension and a reported 0.55%;
13 students involved in an incident leading to police notification and a
reported 0.19%; and zero students on the arrest row as an exact reported zero.
All eight HIB-nature rows remain visible, including structural `n/a` rather
than zero for confirmed HIB under `No Identified Nature`. Every primary value
shows its exact workbook locator.

## Changelog

```toml ergo
[change]
date = "2026-07-30"
note = "Widened the district projection from the three never-suppressed slices to every student group and every grade (49,950 to 620,292 consumer rows), carrying all 183,192 district-level suppressions into the warehouse for the first time; added a 54-row metric catalog; asserted the three pre-existing marts byte-identical; shipped the district-profile module and registered the ten issues a student-group discipline surface has to get right, including that `<5` bounds a headcount at zero rather than one."
issues = ["two-suppression-markers", "less-than-five-is-count-only", "less-than-five-includes-zero", "small-cell-rule-is-inconsistent", "sex-count-withheld-rate-published", "suppression-tracks-group-size", "published-zero-is-not-a-gap", "dimensions-effectively-never-published", "arrest-columns-are-named-police", "reported-incidents-measure-reporting"]
```
