# NCES CCD school lunch and membership

Source and use guide for the eleven school-level NCES Common Core of Data
Lunch Program Eligibility (`033`) editions and their Membership (`052`)
denominators. This is a separate federal record from the New Jersey Fall
Enrollment free/reduced-price lunch fields.

```toml ergo
[dataset]
ergo = "0.4"
slug = "nces-ccd-school-lunch"
title = "NCES Common Core of Data school lunch and membership"
publisher = "National Center for Education Statistics"
subject = "https://nces.ed.gov/ccd/files.asp"
source_url = "https://nces.ed.gov/ccd/files.asp"
pitfall = "The lunch and membership files stop covering the same schools: in 2024-25 the New Jersey lunch file has 2,461 schools and membership has 2,552, so a rate may be computed only after a same-school, same-year join that reports the 91 exclusions."
status = "live"
confidence = "A"
updated = "2026-08-29"
implementation = "https://github.com/lavallee/njschooldata"

[dataset.coverage]
years = "2014-15 → 2024-25 (11 school years)"
grain = "school year × school; lunch and membership universe state retained separately"
entities = "New Jersey schools appearing in either the NCES 033 or 052 file"

[dataset.access]
keys = ["district_code", "school_code", "school_year", "nces_school_id"]
```

## What this record is

NCES publishes one School Lunch Program Eligibility file and one school
Membership file for each school year from 2014-15 through 2024-25. The lunch
file supplies free-lunch, reduced-price-lunch, and combined totals. The
membership file is the only denominator used here. A profile percentage exists
only when both values are reported for the same school in the same edition.

The counts are not exact school administrative totals. NCES says it perturbs
lunch values for disclosure protection. This record is therefore useful for a
source-bounded federal trend and coverage audit, not as a replacement for the
NJDOE Fall Enrollment fields printed immediately above it on a school profile.

The consumer reads an exact offline pin:

- release: `nces-ccd-school-lunch-2026-07-31-aq-033-052-all-editions`
- manifest SHA-256: `9b970530e8c6fc1d60bb2288d88f0abadeaf7cd7fdc85e19574efb488c544268`
- artifact SHA-256: `a9ee38f6a70e92eb68b2ad2384e9e8e93ff12918f40589eafd37c2720d68b823`
- artifact rows: 6,579,293; New Jersey consumer rows: 28,185

## The format boundary

| Years | NCES source shape | Consumer treatment |
|---|---|---|
| 2014-15, 2015-16 | Wide: one row per school, counts in columns | Preserved as `data_format = wide`; negative exception codes become typed missing states, never counts |
| 2016-17 through 2024-25 | Long: one row per school × data group × category | Preserved as `data_format = long`; `DMS_FLAG` determines the value state |

The profile table places a visible divider at 2016-17. The adapter reads the
two source grains separately before projecting them into the consumer table; it
does not `UNION` the source rows as though their keys or blanks meant the same
thing.

```toml ergo
[issue]
id = "format-regime-changes-at-2016-17"
title = "The source changes from one wide school row to long category rows at 2016-17"
effect = "corrupts"
type = "format"
status = "mitigated"
discovered = "2026-07"
detection = "The 2015-16 lunch file stores TOTFRL, FRELCH, and REDLCH as columns on one school row. The 2016-17 file stores five rows per school keyed by data group, lunch program, and total indicator."
misuse = "Appending the raw rows into one series, which changes the grain, key, and meaning of a blank at the boundary."
instead = "Keep the source formats separate during extraction and expose data_format on every projected row and in the reader-facing table."

[issue.scope]
years = ["2014-2015", "2015-2016", "2016-2017"]
tables = ["nces_school_lunch_history"]
```

## The universe boundary

The two files describe different publisher universes. In New Jersey they match
for the first four editions, then membership-only schools appear. The gap grows
sharply in the last three editions.

| School year | Lunch schools | Membership schools | In both | Membership only |
|---|---:|---:|---:|---:|
| 2014-15 | 2,571 | 2,571 | 2,571 | 0 |
| 2015-16 | 2,569 | 2,569 | 2,569 | 0 |
| 2016-17 | 2,571 | 2,571 | 2,571 | 0 |
| 2017-18 | 2,573 | 2,573 | 2,573 | 0 |
| 2018-19 | 2,566 | 2,573 | 2,566 | 7 |
| 2019-20 | 2,553 | 2,553 | 2,553 | 0 |
| 2020-21 | 2,558 | 2,565 | 2,558 | 7 |
| 2021-22 | 2,550 | 2,558 | 2,550 | 8 |
| 2022-23 | 2,511 | 2,548 | 2,511 | 37 |
| 2023-24 | 2,453 | 2,552 | 2,453 | 99 |
| 2024-25 | 2,461 | 2,552 | 2,461 | 91 |

Those 91 current membership-only schools are retained as records with
`lunch_in_universe = 0`; they are not discarded by an inner join. Their rate is
`NULL`, and the profile says “No lunch row.”

```toml ergo
[issue]
id = "lunch-and-membership-cover-different-universes"
title = "The lunch and membership files diverge, reaching 91 membership-only New Jersey schools in 2024-25"
effect = "corrupts"
core = true
type = "universe"
status = "mitigated"
discovered = "2026-07"
detection = "New Jersey distinct schools by file: 2,453 lunch against 2,552 membership in 2023-24, and 2,461 against 2,552 in 2024-25."
misuse = "Dividing a lunch numerator over all membership schools, which silently treats schools absent from the numerator file as zero."
instead = "Join by NCESSCH within a school year, calculate only for schools in both files, retain the exceptions, and print the excluded count beside the series."

[issue.scope]
all = true
tables = ["nces_school_lunch_history", "nces_school_lunch_edition"]
```

## Missing, suppressed, zero

A numeric zero is a reported number. It is not interchangeable with any of
these states:

- `suppressed`: NCES withheld the value.
- `not-collected`: the row exists but the publisher says the state did not
  report a value.
- `structural-absence`: the field does not apply in that source shape.
- `unresolved-unknown`: the wide file's `-1` exception code.
- `outside-lunch-universe`: membership has the school and lunch does not.
- `not-published`: the wide edition has no such field.

The adapter never uses `COALESCE(count, 0)` and the profile gives these states
words, not zeroes.

```toml ergo
[issue]
id = "blank-and-zero-are-different-facts"
title = "Wide negative codes and long blank counts carry typed reasons that must never become zero"
effect = "corrupts"
type = "suppression"
status = "mitigated"
discovered = "2026-07"
detection = "Wide editions use -1, -2, and -9; long editions pair a blank STUDENT_COUNT with DMS_FLAG values including Not reported and Suppressed."
misuse = "Replacing every absent count with zero, which makes a withheld or uncollected value look like no students."
instead = "Carry the source value state, display it literally, and calculate a rate only from two reported numeric cells."

[issue.scope]
all = true
columns = ["total_frl_count", "membership_count", "direct_certification_count"]
```

## Direct certification is not available for New Jersey

The wide editions do not publish a direct-certification field. The long
editions do create one row per school, but New Jersey reports no numeric values:
the rows are `Not reported`, apart from small suppressed sets in 2019-20,
2020-21, and 2021-22. The resulting New Jersey direct-certification count is
unknown, not zero.

This blocks a tempting decomposition. The file can support a same-school,
same-year total-lunch-to-membership rate. It cannot tell a New Jersey reader how
many of those students were matched through direct certification rather than a
household application.

```toml ergo
[issue]
id = "new-jersey-direct-certification-is-not-collected"
title = "NCES publishes no numeric direct-certification count for a New Jersey school in any held edition"
effect = "corrupts"
core = true
type = "coverage"
status = "open"
discovered = "2026-07"
detection = "The two wide editions have no direct-certification field. Every New Jersey direct-certification row in the nine long editions is Not reported or Suppressed; zero rows are numeric."
misuse = "Summing the empty rows and reporting that New Jersey directly certified no students."
instead = "Say the value is not collected in this source. A New Jersey direct-certification series requires a different state source."

[issue.scope]
all = true
columns = ["direct_certification_count", "direct_certification_state"]
```

## Publisher status and perturbation

The companion workbooks mark 2014-15 through 2022-23 **Provisional** and the
2023-24 and 2024-25 editions **Final**. The file API's version label does not
show this distinction, so publisher status comes from the companion workbook
and is printed in the profile table.

NCES also states that lunch counts are perturbed for disclosure protection.
The site therefore prints the count as reported but does not describe it as an
exact number of students.

```toml ergo
[issue]
id = "publisher-status-requires-the-companion-workbook"
title = "Nine editions are provisional even though the file API version label looks like a final edition's"
effect = "misleads"
type = "revision"
status = "mitigated"
discovered = "2026-07"
detection = "The companion Introduction sheets say Provisional through 2022-23 and Final for 2023-24 and 2024-25; the API label cannot distinguish them."
misuse = "Calling every released file final because its API version is above zero."
instead = "Use the companion workbook status and print it with each edition."

[issue.scope]
all = true
columns = ["lunch_publisher_provisional", "membership_publisher_provisional"]
```

### Disclosure protection

```toml ergo
[issue]
id = "lunch-counts-are-perturbed"
title = "NCES perturbs school lunch counts for disclosure protection"
effect = "context"
type = "uncertainty"
status = "monitor"
discovered = "2026-07"
detection = "NCES documentation identifies National School Lunch Program eligibility counts as subject to perturbation."
misuse = "Treating a printed school count as an exact administrative tally or using a tiny difference as evidence of change."
instead = "Describe the values as NCES-reported, disclosure-protected counts and avoid precision claims the source cannot support."

[issue.scope]
all = true
columns = ["free_lunch_count", "reduced_price_count", "total_frl_count"]
```

## Consumer acceptance rules

The release is accepted only when all of these remain true:

1. The exact manifest and 2.7 GB artifact hashes match the lock.
2. The projection contains 28,185 New Jersey school-year rows and 11 editions.
3. 2015-16 remains `wide` and 2016-17 remains `long`.
4. The 2024-25 New Jersey universe remains 2,461 lunch, 2,552 membership,
   and 91 membership-only schools.
5. No New Jersey direct-certification count is numeric.
6. Every percentage has reported lunch and membership cells from the same
   school-year and a positive denominator.
7. The family writes atomically and cannot replace the prior file after pin or
   semantic validation fails.

```toml ergo
[change]
date = "2026-08-29"
note = "Pinned the eleven-edition Aquifer release, added an isolated consumer family, retained both publisher universes, and exposed the source-format and publisher-status boundaries on school profiles."
```
