Skip to content

Selection-based government residential schools in India

India runs a network of public schools that select children on academic merit, board them free, and serve poor and tribal families. Nobody had counted it. This folder does, state by state and system by system, with a source attached to every figure.

The count exists because a grant application needed a number for “how big is the thing you want to scale into”. It turned out to be a research question with no published answer.

Across the systems surveyed, filtering in the order that matters:

Cut Schools
Government schools in the systems examined 23,738
…that admit by competitive selection 5,311
…and are residential 4,727
…and run to Class 12 3,442

Selection is the filter that cuts; residential is nearly free. Going from 5,311 selective schools to 4,727 selective-and-residential loses only 11%. In India, “selective government school” and “residential government school” are close to the same set, which is a structural fact about how these systems were built, not a coincidence.

A third of the residential set never reaches Class 12 (4,727 → 3,442). Mostly new Eklavya schools that open at Class 6 and add a grade a year, so they unblock over time rather than never. For anything keyed to a Class-12 outcome, that cut is not optional.

Against about 784 districts, the surface is roughly six selective residential schools per district, four or five of them running to Class 12. That framing survives the choice of district count: on the narrower 694-district frame in our own PLFS geography table it is 6.8 and 5.0.

File What it is
census.yaml The registry. 117 systems, one block each, every figure carrying the source it came from. In git, human-editable, the source of truth.
census_io.py Loads the registry. Reads the YAML so a fresh clone works with no GCS access.
verify_census.py Re-fetches every cited source and reports which no longer resolve.
build_census.py Provenance only, not part of the build. How the registry was first assembled from the research-fleet journals in one Claude session transcript. Refuses to run without --from-journals, and writes a comparison CSV rather than touching census.yaml.
jnv_roster_rate.json The per-school rate every non-JNV system is scaled by: Avanti’s JNV roster, Class 12, over 662 functional JNVs, with the query, the read date and the counting rule.
build_surface.py The selection-first funnel, the state ranking, and the penetration table against UDISE. Re-runs the roster query and reports drift from the committed rate.
build_facts.py One tidy fact table, grain = (state x system x metric), every row carrying its own provenance.
schemes.json The 100 full scheme records the research fleet returned (selection mechanism, target population, exam prep, source URLs). A one-time snapshot of the session journals, so enrich_facts.py runs anywhere.
enrich_facts.py Adds official URLs, a prose blurb per system, and the per-state Sainik split. Reads schemes.json.

Read this before treating any of it as derived output.

Which systems exist came from a one-time research fleet that swept central schemes plus nine regional groups against live web sources in August 2026. That sweep is not reproducible from this repo. Treat census.yaml as a curated registry to be revised by hand or by re-running a fleet, not as something regenerated on demand.

The figures are each attributed to a real source, mostly the operating body’s own directory or a PIB release. Those are re-verifiable:

Terminal window
python3 verify_census.py # check every cited source resolves
python3 verify_census.py --limit 20 # spot-check

It deliberately does not re-extract the numbers. Government pages are inconsistent enough that automated extraction would manufacture false confidence; it tells you which citations to open by hand.

The measurement problem, and how it is handled

Section titled “The measurement problem, and how it is handled”

No system in this census publishes Class-12 enrolment. Not one of the 117. NVS does not publish class-wise strength centrally, NESTS does not, and none of the state welfare societies do. So student counts here are school counts multiplied by a rate, and the rate comes from the one system where both numbers are known:

JNV: 662 functional schools, 42,920 Class-12 students on Avanti’s own JNV roster in 2026-27 (41,358 online TP-Async + 1,562 physical TP-Phy), so about 65 students per school.

The rate comes from the roster, not from UDISE+, because UDISE+ undercounts JNV. UDISE+ AY2024-25 has 33,813 JNV Class-12 students (51 a school), 27% fewer than the roster. The roster is the one that checks out against an outside count: its 2025-26 JNV Class 11 online roster (42,303) is 95% of the 2025 JNV Class 10 board cohort (44,433), where UDISE’s JNV Class 11 (34,193) would be 77%. This was decided in a model review on 2026-10-15; until then this folder used 51, which put the Class-12 total at about 176,000 rather than 223,000.

The derivation is committed in jnv_roster_rate.json: fact_student_enrollment_journey, enrollment_system = 'JNV', enrollment_academic_year = '2026-2027', Class 12, COUNT(DISTINCT enrollment_user_id) over every enrollment status, read 2026-10-15. Counting only students still marked Enrolled gives 40,547 (61 a school); dividing by the 689 sanctioned JNVs rather than the 662 functional gives 62. Neither moves the answer out of the 60s.

Estimated Class-12 students in the qualifying set: about 223,000 (3,442 Class-12-reaching schools x 64.8). The residential set at the same rate is about 306,000 (4,727 x 64.8), which is roughly where the Class-12 figure heads as new Eklavya schools reach Class 12. Both are ASSUMED, orders of magnitude, and about 1% of a single-year cohort.

JNV is the right benchmark because it is structurally the closest analogue: government-run, wholly residential, admission by a national entrance test, running to Class 12. Applying it to another system assumes that system is sized like a JNV, which is an assumption and is labelled as one wherever it is used.

build_facts.py keeps this honest by tagging every row:

  • REAL — measured and published by the named source
  • DERIVED — arithmetic on REAL inputs, no judgement
  • ASSUMED — carries a judgement call, spelled out in the row’s assumptions field

Every ASSUMED row carries its assumption. Not a footnote elsewhere, the actual sentence on the row.

Telangana is the largest addressable block in the country. Five societies (TGSWREIS, TGTWREIS, MJPTBCWREIS, TGREIS, TGMREIS) run roughly 900 to 950 wholly residential institutions, and all of them admit through one common entrance test, TGCET. One door into five systems.

Karnataka’s KREIS is the largest single body, 821 institutions, bigger than JNV.

Tamil Nadu is structurally different. It is the only state that never took the Navodaya scheme, so it has zero JNVs and almost no selective government boarding. It built VETRI Palligal instead, a coaching programme inside 236 government day schools going to 414. Any Tamil Nadu strategy has to be a day-school strategy.

Two systems that look qualifying and are not. Kasturba Gandhi Balika Vidyalaya is 5,269 functional schools but is not merit-selected by design, and historically capped at Class 8. The “New Sainik Schools” are 87 approved under a PPP badge, but the named list is overwhelmingly private schools (Delhi Public School, Adani World School, Khalsa College Public School) with only a minimum 40% of seats filled from the AISSEE merit list. Both are the easiest things in this space to miscount as government selective-residential.

Sainik Schools: 33, not 35. The Society’s own directory lists 33; Wikipedia lists 35 because it counts Morena and Gorakhpur. Verified against the Society, split per state.

  • The state coverage is uneven. Nine regional sweeps were launched; the first run lost five to API failures and a second run recovered them, but the depth per state varies. Punjab and Delhi in particular came back thin relative to their real footprint.
  • The two largest claims rest on society websites, not audited directories: KREIS at 821 and the Telangana societies at ~950. Both deserve the same primary-source audit that corrected the Sainik numbers.
  • The penetration table’s JNV component is still UDISE+. It needs a per-state JNV count, and it uses UDISE’s, which is low (33,813 nationally against the roster’s 42,920). Every state’s selective share is understated by that much on its JNV part.
  • The registry’s first assembly is not reproducible. build_census.py read the research-fleet journals in one Claude session transcript; it is kept for provenance only. enrich_facts.py reads the committed schemes.json snapshot of the same journals.

The routine build needs only files in git (census.yaml, schemes.json, jnv_roster_rate.json), plus one small input in GCS that build_facts.py fetches itself through analysis/gcs_inputs.py: gs://avantifellows-private-data/analysis/selection-residential-schools/sources/moe_science_share_by_state_category_2024.csv. Nothing reads ~/avanti-data or a Claude transcript.

Terminal window
python3 verify_census.py # re-check the cited sources
python3 build_surface.py # -> funnel, state ranking, penetration (needs BigQuery)
python3 build_facts.py # -> output/surface_facts.parquet + .csv (UDISE rows need build_surface's cache)

build_surface.py reads UDISE+ and MoE board results from BigQuery over the REST API with curl -4. That is deliberate: the bq CLI hangs indefinitely on networks whose IPv6 path black-holes, which cost a day. Results cache to output/_cache_*.csv.

Outputs are gitignored. The published copies live in gs://avantifellows-grant-applications/mulago-rainer-2026-08/ (public, aggregate only, no PII): the fact table, the funnel, the state ranking, the penetration table, and the per-school rates. Those copies were built on the old UDISE rate of 51 (they are the figures the Mulago application used) and have not been replaced; the roster-rate outputs are local until someone republishes them.

grant-applications/Mulago/ — the Rainer Arnhold Fellowship application, whose argument rests on this count.