ltc-code 0.2.20__tar.gz → 0.2.21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {ltc_code-0.2.20 → ltc_code-0.2.21}/PKG-INFO +1 -1
  2. {ltc_code-0.2.20 → ltc_code-0.2.21}/pyproject.toml +1 -1
  3. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/build_nsc_outcomes.py +195 -11
  4. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/.DS_Store +0 -0
  5. ltc_code-0.2.21/src/ltc_code/signal_var_calcs.py +41 -0
  6. {ltc_code-0.2.20 → ltc_code-0.2.21}/README.md +0 -0
  7. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/.DS_Store +0 -0
  8. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/aspire.py +0 -0
  9. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/check_cmo_apps.do +0 -0
  10. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/christel_house.py +0 -0
  11. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/democracy_prep.py +0 -0
  12. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/green_dot.py +0 -0
  13. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/helpers.py +0 -0
  14. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/ilt.py +0 -0
  15. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/kipp_nj.py +0 -0
  16. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/main.py +0 -0
  17. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/mappings.py +0 -0
  18. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/rocketship.py +0 -0
  19. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/yes_prep.py +0 -0
  20. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/aspire.py +0 -0
  21. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/christel_house.py +0 -0
  22. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/democracy_prep.py +0 -0
  23. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/green_dot.py +0 -0
  24. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/ilt.py +0 -0
  25. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/kipp.py +0 -0
  26. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/kipp_nj.py +0 -0
  27. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/rocketship.py +0 -0
  28. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/yes_prep.py +0 -0
  29. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/BALANCE.do +0 -0
  30. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/FS.do +0 -0
  31. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/ITT.do +0 -0
  32. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/TOT.do +0 -0
  33. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/apps_helpers.py +0 -0
  34. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/harmony.py +0 -0
  35. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/main.py +0 -0
  36. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260712_uncommon_scripts/main.py +0 -0
  37. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260712_uncommon_scripts/mappings.py +0 -0
  38. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/20260712_uncommon_scripts/uncommon.py +0 -0
  39. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/__init__.py +0 -0
  40. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/aspire.py +0 -0
  41. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/check_cmo_apps.do +0 -0
  42. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/christel_house.py +0 -0
  43. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/green_dot.py +0 -0
  44. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/helpers.py +0 -0
  45. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/june13.py +0 -0
  46. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/june2.py +0 -0
  47. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/june30.py +0 -0
  48. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/june5.py +0 -0
  49. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/june7.py +0 -0
  50. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/kipp_nj.py +0 -0
  51. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/kipp_tx.py +0 -0
  52. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/main.py +0 -0
  53. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/make_summary_stats_table.py +0 -0
  54. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/mappings.py +0 -0
  55. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/may27.py +0 -0
  56. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/.DS_Store +0 -0
  57. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/__init__.py +0 -0
  58. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/input/.gitkeep +0 -0
  59. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/naics.csv +0 -0
  60. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/naics.py +0 -0
  61. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/naics_raw.xlsx +0 -0
  62. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/output/.gitkeep +0 -0
  63. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/CREDENTIAL_LEVEL_LOOKUP_TABLE.xlsx +0 -0
  64. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/IPEDS_IC_2013.csv +0 -0
  65. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/IPEDS_IC_manual.xlsx +0 -0
  66. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/chetty/mrc_table11.dta +0 -0
  67. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/chetty/mrc_table2.dta +0 -0
  68. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/college_crosswalk.xls +0 -0
  69. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/directory.dta +0 -0
  70. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/ipeds_data.dta +0 -0
  71. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/nsc/run_nsc_outcomes.py +0 -0
  72. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/polars_dates.py +0 -0
  73. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/rocketship.py +0 -0
  74. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/schema_mapping.py +0 -0
  75. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/school_name_xwalk/__init__.py +0 -0
  76. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/school_name_xwalk/all_schools_with_ccd.csv +0 -0
  77. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/school_name_xwalk/merge_school_ccd.py +0 -0
  78. {ltc_code-0.2.20 → ltc_code-0.2.21}/src/ltc_code/yes_prep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ltc-code
3
- Version: 0.2.20
3
+ Version: 0.2.21
4
4
  Summary: Add your description here
5
5
  Requires-Dist: fastexcel>=0.16,<0.20
6
6
  Requires-Dist: polars>=1.36.1,<1.42
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ltc-code"
3
- version = "0.2.20"
3
+ version = "0.2.21"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -235,11 +235,13 @@ chetty_college_outcomes = (
235
235
  missing_string_as_null=True,
236
236
  value_labels_as_strings=False,
237
237
  )
238
- .select("super_opeid", "tier", "tier_name", "k_mean")
238
+ .select("super_opeid", "tier", "tier_name", "iclevel", "count", "k_mean")
239
239
  .collect()
240
240
  .with_columns(
241
241
  pl.col("super_opeid").cast(pl.Int64, strict=False),
242
242
  pl.col("tier").cast(pl.Int8, strict=False),
243
+ pl.col("iclevel").cast(pl.Int8, strict=False),
244
+ pl.col("count").cast(pl.Float64, strict=False),
243
245
  pl.col("k_mean").cast(pl.Float64, strict=False),
244
246
  )
245
247
  .drop_nulls("super_opeid")
@@ -253,6 +255,38 @@ k_mean_insuffdata = chetty_college_outcomes.filter(
253
255
  pl.col("super_opeid") == -1
254
256
  ).item(0, "k_mean")
255
257
 
258
+ # Collapse MRC college-specific earnings to national student-weighted means.
259
+ # MRC count is the mean number of children per cohort represented by each row.
260
+ mrc_sector_means = (
261
+ chetty_college_outcomes
262
+ .filter(
263
+ (pl.col("super_opeid") > 0)
264
+ & pl.col("iclevel").is_in([1, 2, 3])
265
+ & (pl.col("count") > 0)
266
+ & pl.col("k_mean").is_not_null()
267
+ )
268
+ .with_columns(
269
+ pl.when(pl.col("iclevel") == 1)
270
+ .then(pl.lit("4yr"))
271
+ .otherwise(pl.lit("2yr_or_less"))
272
+ .alias("college_sector")
273
+ )
274
+ .group_by("college_sector")
275
+ .agg(
276
+ (
277
+ (pl.col("k_mean") * pl.col("count")).sum()
278
+ / pl.col("count").sum()
279
+ ).alias("k_mean_coarse")
280
+ )
281
+ )
282
+
283
+ k_mean_4yr_coarse = mrc_sector_means.filter(
284
+ pl.col("college_sector") == "4yr"
285
+ ).item(0, "k_mean_coarse")
286
+ k_mean_2yr_coarse = mrc_sector_means.filter(
287
+ pl.col("college_sector") == "2yr_or_less"
288
+ ).item(0, "k_mean_coarse")
289
+
256
290
  chetty_ope_crosswalk = (
257
291
  scan_readstat(
258
292
  CHETTY_MRC_TABLE11,
@@ -275,24 +309,102 @@ chetty_by_opeid = (
275
309
  .unique("opeid", keep="first")
276
310
  )
277
311
 
278
- # IPEDS reports completion within 150% of normal time: six years at four-year
279
- # colleges and three years at two-year colleges.
312
+ # The IPEDS 150%-of-normal-time rate is six-year completion at four-year
313
+ # institutions and three-year completion at two-year institutions.
280
314
  ipeds_completion = (
281
315
  scan_readstat(
282
316
  IPEDS_DATA,
283
317
  missing_string_as_null=True,
284
318
  value_labels_as_strings=False,
285
319
  )
286
- .select("unitid", "completion_rate_150pct_ip")
320
+ .select(
321
+ "unitid",
322
+ "completion_rate_150pct_ip",
323
+ "cohort_adj_150pct_ip",
324
+ "completers_150pct_ip",
325
+ )
287
326
  .collect()
288
327
  .with_columns(
289
328
  pl.col("unitid").cast(pl.Int64, strict=False),
290
329
  pl.col("completion_rate_150pct_ip").cast(pl.Float64, strict=False),
330
+ pl.col("cohort_adj_150pct_ip").cast(pl.Float64, strict=False),
331
+ pl.col("completers_150pct_ip").cast(pl.Float64, strict=False),
291
332
  )
292
333
  .drop_nulls("unitid")
293
334
  .unique("unitid", keep="first")
294
335
  )
295
336
 
337
+ # Use the graduation cohort itself as the student weight. Summing completers and
338
+ # cohorts is equivalent to weighting each college's rate by its IPEDS denominator.
339
+ ipeds_sector_rates = (
340
+ ipeds_completion
341
+ .join(
342
+ ipeds_2013.select("unitid", "years_ipeds_2013"),
343
+ on="unitid",
344
+ how="left",
345
+ validate="1:1",
346
+ )
347
+ .join(
348
+ ipeds_manual.select("unitid", "years_manual"),
349
+ on="unitid",
350
+ how="left",
351
+ validate="1:1",
352
+ )
353
+ .join(
354
+ directory.select("unitid", "years_directory"),
355
+ on="unitid",
356
+ how="left",
357
+ validate="1:1",
358
+ )
359
+ .with_columns(
360
+ pl.coalesce(
361
+ [
362
+ pl.col("years_manual"),
363
+ pl.col("years_ipeds_2013"),
364
+ pl.col("years_directory"),
365
+ ]
366
+ ).alias("college_years")
367
+ )
368
+ .filter(
369
+ pl.col("college_years").is_in([1, 2, 4])
370
+ & (pl.col("cohort_adj_150pct_ip") > 0)
371
+ & pl.col("completers_150pct_ip").is_not_null()
372
+ )
373
+ .with_columns(
374
+ pl.when(pl.col("college_years") == 4)
375
+ .then(pl.lit("4yr"))
376
+ .otherwise(pl.lit("2yr_or_less"))
377
+ .alias("college_sector")
378
+ )
379
+ .group_by("college_sector")
380
+ .agg(
381
+ pl.col("completers_150pct_ip").sum().alias("completers"),
382
+ pl.col("cohort_adj_150pct_ip").sum().alias("cohort"),
383
+ )
384
+ .with_columns((pl.col("completers") / pl.col("cohort")).alias("cmp_rate_coarse"))
385
+ )
386
+
387
+ cmp_rate_4yr_coarse = ipeds_sector_rates.filter(
388
+ pl.col("college_sector") == "4yr"
389
+ ).item(0, "cmp_rate_coarse")
390
+ cmp_rate_2yr_coarse = ipeds_sector_rates.filter(
391
+ pl.col("college_sector") == "2yr_or_less"
392
+ ).item(0, "cmp_rate_coarse")
393
+
394
+ for label, value in {
395
+ "four-year k_mean": k_mean_4yr_coarse,
396
+ "two-year-or-less k_mean": k_mean_2yr_coarse,
397
+ }.items():
398
+ if value is None or value <= 0:
399
+ raise ValueError(f"Invalid national student-weighted {label}: {value}")
400
+
401
+ for label, value in {
402
+ "four-year completion rate": cmp_rate_4yr_coarse,
403
+ "two-year-or-less completion rate": cmp_rate_2yr_coarse,
404
+ }.items():
405
+ if value is None or not 0 <= value <= 1:
406
+ raise ValueError(f"Invalid national student-weighted {label}: {value}")
407
+
296
408
 
297
409
  ###########################################################
298
410
  # Build college reference data
@@ -331,7 +443,7 @@ college_ref = (
331
443
  )
332
444
  .with_columns(
333
445
  (pl.col("college_years") == 4).cast(pl.Int8).alias("college_4yr"),
334
- (pl.col("college_years") == 2).cast(pl.Int8).alias("college_2yr"),
446
+ pl.col("college_years").is_in([1, 2]).cast(pl.Int8).alias("college_2yr"),
335
447
  pl.col("tier").is_in([1, 2]).cast(pl.Int8).alias("college_elite"),
336
448
  )
337
449
  .with_columns(
@@ -939,7 +1051,6 @@ first_institution = (
939
1051
  .first()
940
1052
  .alias("college_name_firstinst"),
941
1053
  pl.col("college_years").first().alias("college_years_firstinst"),
942
- pl.col("k_mean").first().alias("k_mean_firstinst"),
943
1054
  pl.col("completion_rate_150pct_ip")
944
1055
  .first()
945
1056
  .alias("completion_rate_150pct_firstinst"),
@@ -1370,6 +1481,14 @@ nsc_outcomes = (
1370
1481
  .then(k_mean_neverattend)
1371
1482
  .otherwise(pl.col("k_mean"))
1372
1483
  .alias("k_mean"),
1484
+ pl.when(pl.col("ID_FSC_firstinst").is_null())
1485
+ .then(k_mean_neverattend)
1486
+ .when(pl.col("college_years_firstinst") == 4)
1487
+ .then(k_mean_4yr_coarse)
1488
+ .when(pl.col("college_years_firstinst").is_in([1, 2]))
1489
+ .then(k_mean_2yr_coarse)
1490
+ .otherwise(k_mean_insuffdata)
1491
+ .alias("k_mean_coarse"),
1373
1492
  )
1374
1493
  )
1375
1494
 
@@ -1479,10 +1598,18 @@ nsc_outcomes = nsc_outcomes.with_columns(
1479
1598
  # Sarah creates cmp_rate as a copy of the restricted raw completion rate, then
1480
1599
  # fills missing cmp_rate values with the late-enrollee/control-group mean.
1481
1600
  nsc_outcomes = nsc_outcomes.with_columns(
1482
- pl.col("completion_rate_150pct_ip").alias("cmp_rate")
1601
+ pl.col("completion_rate_150pct_ip").alias("cmp_rate"),
1602
+ pl.when(pl.col("att_any_byY2") == 0)
1603
+ .then(None)
1604
+ .when(pl.col("college_years_firstinst") == 4)
1605
+ .then(cmp_rate_4yr_coarse)
1606
+ .when(pl.col("college_years_firstinst").is_in([1, 2]))
1607
+ .then(cmp_rate_2yr_coarse)
1608
+ .otherwise(None)
1609
+ .alias("cmp_rate_coarse"),
1483
1610
  )
1484
1611
 
1485
- late_enrollee_mean = (
1612
+ late_enrollee_means = (
1486
1613
  nsc_outcomes
1487
1614
  .filter(
1488
1615
  (pl.col("att_4yr_inY2") == 0)
@@ -1494,15 +1621,29 @@ late_enrollee_mean = (
1494
1621
  )
1495
1622
  & (pl.col("offer") == 0)
1496
1623
  )
1497
- .select(pl.col("cmp_rate").mean())
1498
- .item()
1624
+ .select(
1625
+ pl.col("cmp_rate").mean().alias("cmp_rate"),
1626
+ pl.col("cmp_rate_coarse").mean().alias("cmp_rate_coarse"),
1627
+ )
1499
1628
  )
1500
1629
 
1630
+ late_enrollee_mean = late_enrollee_means.item(0, "cmp_rate")
1631
+ late_enrollee_coarse_mean = late_enrollee_means.item(0, "cmp_rate_coarse")
1632
+
1633
+ if late_enrollee_mean is None or late_enrollee_coarse_mean is None:
1634
+ raise ValueError(
1635
+ "The late-enrollee control donor group has no usable completion-rate values."
1636
+ )
1637
+
1501
1638
  nsc_outcomes = nsc_outcomes.with_columns(
1502
1639
  pl.when(pl.col("cmp_rate").is_null())
1503
1640
  .then(pl.lit(late_enrollee_mean, dtype=pl.Float64))
1504
1641
  .otherwise(pl.col("cmp_rate"))
1505
- .alias("adj_cmp_rate")
1642
+ .alias("adj_cmp_rate"),
1643
+ pl.when(pl.col("cmp_rate_coarse").is_null())
1644
+ .then(pl.lit(late_enrollee_coarse_mean, dtype=pl.Float64))
1645
+ .otherwise(pl.col("cmp_rate_coarse"))
1646
+ .alias("adj_cmp_rate_coarse"),
1506
1647
  ).with_columns(
1507
1648
  pl.when(pl.col("att_4yr_byY2").is_null())
1508
1649
  .then(None)
@@ -1512,6 +1653,14 @@ nsc_outcomes = nsc_outcomes.with_columns(
1512
1653
  .then(None)
1513
1654
  .otherwise(pl.col("adj_cmp_rate"))
1514
1655
  .alias("adj_cmp_rate"),
1656
+ pl.when(pl.col("att_4yr_byY2").is_null())
1657
+ .then(None)
1658
+ .otherwise(pl.col("cmp_rate_coarse"))
1659
+ .alias("cmp_rate_coarse"),
1660
+ pl.when(pl.col("att_4yr_byY2").is_null())
1661
+ .then(None)
1662
+ .otherwise(pl.col("adj_cmp_rate_coarse"))
1663
+ .alias("adj_cmp_rate_coarse"),
1515
1664
  # Preserve the package's existing name as an alias for Sarah's adjusted rate.
1516
1665
  pl.when(pl.col("att_4yr_byY2").is_null())
1517
1666
  .then(None)
@@ -1551,9 +1700,12 @@ nsc_outcomes = nsc_outcomes.with_columns(
1551
1700
  keep_columns = [
1552
1701
  "sid_cepr",
1553
1702
  "k_mean",
1703
+ "k_mean_coarse",
1554
1704
  "completion_rate_150pct_ip",
1555
1705
  "cmp_rate",
1556
1706
  "adj_cmp_rate",
1707
+ "cmp_rate_coarse",
1708
+ "adj_cmp_rate_coarse",
1557
1709
  "predicted_completion",
1558
1710
  "ID_FSC_firstinst",
1559
1711
  "college_name_firstinst",
@@ -1591,6 +1743,38 @@ print(
1591
1743
  f"{first_college_coverage['has_completion_rate'].sum()}/"
1592
1744
  f"{first_college_coverage.height} with IPEDS completion rate"
1593
1745
  )
1746
+ print(
1747
+ "National student-weighted coarse values: "
1748
+ f"k_mean 4yr={k_mean_4yr_coarse:.2f}, "
1749
+ f"k_mean 2yr-or-less={k_mean_2yr_coarse:.2f}, "
1750
+ f"completion 4yr={cmp_rate_4yr_coarse:.4f}, "
1751
+ f"completion 2yr-or-less={cmp_rate_2yr_coarse:.4f}, "
1752
+ f"nonattender completion imputation={late_enrollee_mean:.4f}, "
1753
+ f"coarse nonattender imputation={late_enrollee_coarse_mean:.4f}"
1754
+ )
1755
+
1756
+ observable_y2 = nsc_outcomes.filter(pl.col("att_4yr_byY2").is_not_null())
1757
+ coarse_audit = observable_y2.select(
1758
+ pl.len().alias("students"),
1759
+ pl.col("k_mean_coarse").null_count().alias("k_mean_coarse_missing"),
1760
+ pl.col("k_mean_coarse").min().alias("k_mean_coarse_min"),
1761
+ pl.col("k_mean_coarse").max().alias("k_mean_coarse_max"),
1762
+ pl.col("adj_cmp_rate_coarse")
1763
+ .null_count()
1764
+ .alias("adj_cmp_rate_coarse_missing"),
1765
+ pl.col("adj_cmp_rate_coarse").min().alias("adj_cmp_rate_coarse_min"),
1766
+ pl.col("adj_cmp_rate_coarse").max().alias("adj_cmp_rate_coarse_max"),
1767
+ )
1768
+
1769
+ if coarse_audit.item(0, "k_mean_coarse_missing") > 0:
1770
+ raise ValueError("k_mean_coarse is unexpectedly missing for observable students.")
1771
+ if coarse_audit.item(0, "adj_cmp_rate_coarse_missing") > 0:
1772
+ raise ValueError(
1773
+ "adj_cmp_rate_coarse is unexpectedly missing for observable students."
1774
+ )
1775
+
1776
+ print("Coarse outcome audit:")
1777
+ print(coarse_audit)
1594
1778
  print(
1595
1779
  nsc_outcomes.select("sid_cepr", "cohort_lottery", "recovered_nsc_outcome").sort(
1596
1780
  "sid_cepr"
@@ -0,0 +1,41 @@
1
+ # --- Import necessary packages ---
2
+ import polars as pl
3
+
4
+
5
+ SIGNAL_DEP_VARS = ["wages25", "college20"]
6
+
7
+
8
+ def calculate_signal_variance(results: pl.DataFrame) -> pl.DataFrame:
9
+ """Calculate signal variance and its implied 90-10 spread by outcome."""
10
+ return (
11
+ results
12
+ .filter(pl.col("depvar").is_in(SIGNAL_DEP_VARS))
13
+ .drop_nulls(["sample", "depvar", "coef", "se"])
14
+ .group_by("depvar")
15
+ .agg(
16
+ pl.col("sample").n_unique().alias("n_cmos"),
17
+ pl.col("coef").mean().alias("mean_te"),
18
+ pl.col("coef").var().alias("raw_variance"),
19
+ (pl.col("se") ** 2).mean().alias("noise_variance"),
20
+ )
21
+ .with_columns(
22
+ (
23
+ pl.col("raw_variance")
24
+ - pl.col("noise_variance")
25
+ ).alias("signal_variance_raw")
26
+ )
27
+ # Retain the raw estimate, but use a nonnegative value for the spread.
28
+ .with_columns(
29
+ pl.col("signal_variance_raw")
30
+ .clip(lower_bound=0)
31
+ .alias("signal_variance")
32
+ )
33
+ .with_columns(
34
+ pl.col("signal_variance").sqrt().alias("signal_sd")
35
+ )
36
+ # Under normality, P90 - P10 equals 2.563103 standard deviations.
37
+ .with_columns(
38
+ (2.563103 * pl.col("signal_sd")).alias("spread_90_10")
39
+ )
40
+ .sort("depvar")
41
+ )
File without changes