ltc-code 0.2.24__tar.gz → 0.2.25__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 (70) hide show
  1. {ltc_code-0.2.24 → ltc_code-0.2.25}/PKG-INFO +1 -1
  2. {ltc_code-0.2.24 → ltc_code-0.2.25}/pyproject.toml +1 -1
  3. {ltc_code-0.2.24 → ltc_code-0.2.25}/pyproject.toml.orig +1 -1
  4. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/build_nsc_outcomes.py +317 -202
  5. ltc_code-0.2.25/src/ltc_code/nsc/dhs_stem/dhs_stem_cip_additions_2024.csv +252 -0
  6. ltc_code-0.2.25/src/ltc_code/nsc/dhs_stem/extract_dhs_stem_cips.R +50 -0
  7. ltc_code-0.2.25/src/ltc_code/nsc/dhs_stem/stemList2024.pdf +0 -0
  8. {ltc_code-0.2.24 → ltc_code-0.2.25}/README.md +0 -0
  9. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/aspire.py +0 -0
  10. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/check_cmo_apps.do +0 -0
  11. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/christel_house.py +0 -0
  12. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/democracy_prep.py +0 -0
  13. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/green_dot.py +0 -0
  14. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/helpers.py +0 -0
  15. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/ilt.py +0 -0
  16. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/kipp_nj.py +0 -0
  17. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/main.py +0 -0
  18. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/mappings.py +0 -0
  19. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/rocketship.py +0 -0
  20. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260614_new_build_scripts_update/yes_prep.py +0 -0
  21. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/aspire.py +0 -0
  22. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/christel_house.py +0 -0
  23. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/democracy_prep.py +0 -0
  24. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/green_dot.py +0 -0
  25. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/ilt.py +0 -0
  26. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/kipp.py +0 -0
  27. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/kipp_nj.py +0 -0
  28. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/rocketship.py +0 -0
  29. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260630_census_disclosure/yes_prep.py +0 -0
  30. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/BALANCE.do +0 -0
  31. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/FS.do +0 -0
  32. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/ITT.do +0 -0
  33. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/TOT.do +0 -0
  34. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/apps_helpers.py +0 -0
  35. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/harmony.py +0 -0
  36. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260706_ceprscripts/main.py +0 -0
  37. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260712_uncommon_scripts/main.py +0 -0
  38. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260712_uncommon_scripts/mappings.py +0 -0
  39. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/20260712_uncommon_scripts/uncommon.py +0 -0
  40. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/__init__.py +0 -0
  41. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/aspire.py +0 -0
  42. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/check_cmo_apps.do +0 -0
  43. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/christel_house.py +0 -0
  44. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/green_dot.py +0 -0
  45. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/helpers.py +0 -0
  46. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/june13.py +0 -0
  47. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/june2.py +0 -0
  48. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/june30.py +0 -0
  49. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/june5.py +0 -0
  50. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/june7.py +0 -0
  51. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/kipp_nj.py +0 -0
  52. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/kipp_tx.py +0 -0
  53. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/main.py +0 -0
  54. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/make_summary_stats_table.py +0 -0
  55. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/mappings.py +0 -0
  56. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/may27.py +0 -0
  57. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/__init__.py +0 -0
  58. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/input/.gitkeep +0 -0
  59. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/naics.py +0 -0
  60. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/output/.gitkeep +0 -0
  61. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/raw/ipeds_data.dta +0 -0
  62. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/nsc/run_nsc_outcomes.py +0 -0
  63. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/polars_dates.py +0 -0
  64. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/rocketship.py +0 -0
  65. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/schema_mapping.py +0 -0
  66. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/school_name_xwalk/__init__.py +0 -0
  67. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/school_name_xwalk/all_schools_with_ccd.csv +0 -0
  68. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/school_name_xwalk/merge_school_ccd.py +0 -0
  69. {ltc_code-0.2.24 → ltc_code-0.2.25}/src/ltc_code/signal_var_calcs.py +0 -0
  70. {ltc_code-0.2.24 → ltc_code-0.2.25}/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.24
3
+ Version: 0.2.25
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.24"
3
+ version = "0.2.25"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ltc-code"
3
- version = "0.2.24"
3
+ version = "0.2.25"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -76,6 +76,12 @@ OUTCOMES = OUTPUT_NSC / "nsc_outcomes.parquet"
76
76
  # Final student-level NSC outcomes file.
77
77
 
78
78
 
79
+ DHS_STEM_CIP_ADDITIONS_FILE = (
80
+ PACKAGE_NSC / "dhs_stem" / "dhs_stem_cip_additions_2024.csv"
81
+ )
82
+ # Exact six-digit additions in the fixed July 22, 2024 DHS STEM list.
83
+
84
+
79
85
  ###########################################################
80
86
  # Define run settings
81
87
  ###########################################################
@@ -84,11 +90,128 @@ N_YEARS_OUT = 8
84
90
  NSC_OBSERVATION_YEAR = 2026
85
91
  NSC_CUTOFF_DATE = date(2026, 12, 31)
86
92
  RANDOM_SEED = 3852804
87
- STEM_CIP_FAMILIES = {11, 14, 15, 26, 27, 40, 41}
93
+ SARAH_STEM_CIP_FAMILIES = {"11", "14", "15", "26", "27", "40", "41"}
94
+ DHS_STEM_CIP_FAMILIES = {"14", "26", "27", "40"}
95
+ STEM_DEFINITIONS = ["sarah", "dhs"]
88
96
  COLLEGE_TYPES = ["any", "4yr", "2yr", "elite"]
89
97
  AGE_ATTENDANCE_TYPES = ["any", "4yr", "2yr"]
90
98
  AGE_ATTENDANCE_RANGE = range(18, 27)
91
99
 
100
+ # These ordered rules exactly reproduce Sarah's Stata replacements. Every rule
101
+ # checks the original CIP slot, so the last matching rule wins when names overlap.
102
+ MAJOR_TO_CIP_RULES = [
103
+ ("ECONOMICS", "4506"),
104
+ ("BUSINESS|FINANCE", "52"),
105
+ ("OPERATIONS MANAG", "52"),
106
+ ("MARKETING", "5214"),
107
+ ("MANAGEMENT", "5213"),
108
+ ("ANTHRO|ARCHEO|CRIMINO|DEMOGRA|POLITIC|SOCIOLO|HUMANI|GOVERN|GEOGRA", "45"),
109
+ ("VISUAL|PERFORM|ART|DANCE|PHOTO|FILM|MUSIC|MEDIA|ENTERTAIN|DRAMA|CRAFT|THEATRE|THEATER", "50"),
110
+ ("HISTORY", "54"),
111
+ ("PSYCHOLOGY", "42"),
112
+ ("PHILOSO", "38"),
113
+ ("LIBERAL|GENERAL STUDIES", "24"),
114
+ ("ENGLISH|ENGLISH LANGUAGE", "23"),
115
+ ("ENGLISH LIERATURE|WRITING", "23"),
116
+ ("LEGAL|LAW", "22"),
117
+ ("FOREIGN LANGUAGE|LANGUAGES|SPAN|JAPAN|CHIN|COMPARATIVE LING|FRENCH", "16"),
118
+ ("EDUCATION", "13"),
119
+ ("COMMUNI", "09"),
120
+ ("ETHNIC|CULTURAL|GENDER|WOMEN", "05"),
121
+ ("ENGI", "14"),
122
+ ("COMPUT|INFORMATION TECH", "11"),
123
+ ("EDUC|CURRIC|EARLY CHI", "13"),
124
+ ("SCHOOL COUN|STUDENT COUN", "13"),
125
+ ("LIBRA", "25"),
126
+ ("BIOLOG|NEURO|BIOTECH", "26"),
127
+ ("MATH", "27"),
128
+ ("LIBRA", "25"),
129
+ ("RELIG", "38"),
130
+ ("THEOLOG", "39"),
131
+ ("ASTRON|ATMOS|GEOLOG|CHEMI|PHYSIC|METEO", "40"),
132
+ ("PUBLIC ADMIN|PUBLIC POL|SOCIAL WORK|COMMUNITY ORG|SOCIAL SERVICE|HUMAN SERV", "44"),
133
+ ("BIOCHEM|BIOPHY|PHYSIO|BOTANY|ANATOMY|ZOO|GENETIC|GENOM|PHARMA|TOXICO|ECOLOG|EPIDEM|WILDLIFE", "26"),
134
+ ("VETERINARY|DENT|HEALTH SCI", "51"),
135
+ ("SPORT MANAGEMENT|SPORTS MANAGEMENT|KINESIO|EXER", "31"),
136
+ ("PUBLIC RELA|ADVERTIS|RADIO|TELEV|JOURNA", "09"),
137
+ ("PUBLIC HEALTH|REHAB|PHYSICAL THER|PREMED|PRE-MED|PRE-VET|NUR|MEDIC|HEALTH CARE ADM", "51"),
138
+ ("PLANT|SOIL|AGRI|TURF|HORTIC|FOOD SCI|ANIMAL", "01"),
139
+ ("ARCHI", "04"),
140
+ ("DESIGN", "50"),
141
+ ("HUMAN RESO|HOSPITALITY|HOTEL", "52"),
142
+ ("FOREST|ENVIRONMENTAL STUD|ENVIRONMENTAL SCI", "03"),
143
+ ("HUMAN RESO|HOSPITALITY|HOTEL|FASHION MERCH", "52"),
144
+ ("FOODS|NUTRITION", "19"),
145
+ ("CRIMINAL JUS|CRIME & JUST", "43"),
146
+ ("CLASSICS", "16"),
147
+ ("ACCOUNTING", "5203"),
148
+ ("AUTOMOTIVE TECH|AUTOMOB", "47"),
149
+ ("CLINICAL LAB", "51"),
150
+ ("HUMAN DEV", "19"),
151
+ ]
152
+
153
+ DHS_STEM_CIP_ADDITIONS = set(
154
+ pl.read_csv(
155
+ DHS_STEM_CIP_ADDITIONS_FILE,
156
+ schema_overrides={"cip6": pl.String},
157
+ )["cip6"].to_list()
158
+ )
159
+
160
+ if len(DHS_STEM_CIP_ADDITIONS) != 251:
161
+ raise ValueError(
162
+ "Expected 251 exact six-digit additions in the 2024 DHS STEM lookup, "
163
+ f"found {len(DHS_STEM_CIP_ADDITIONS)}"
164
+ )
165
+
166
+
167
+ def impute_cip_from_major(cip_column, major_column):
168
+ """Fill a missing CIP with Sarah's ordered major-name replacement rules."""
169
+ cip = pl.col(cip_column).cast(pl.String).str.strip_chars()
170
+ major = pl.col(major_column).cast(pl.String).str.to_uppercase().fill_null("")
171
+ imputed = pl.lit(None, dtype=pl.String)
172
+
173
+ for pattern, cip_value in MAJOR_TO_CIP_RULES:
174
+ imputed = (
175
+ pl.when(major.str.contains(pattern, literal=False))
176
+ .then(pl.lit(cip_value))
177
+ .otherwise(imputed)
178
+ )
179
+
180
+ return (
181
+ pl.when(cip.is_null() | cip.is_in(["", "."]))
182
+ .then(imputed)
183
+ .otherwise(cip)
184
+ .alias(cip_column)
185
+ )
186
+
187
+
188
+ def stem_flag_from_cips(cip_columns, definition):
189
+ """Classify a row as STEM when any of its four imputed CIP slots qualify."""
190
+ cip_digits = [
191
+ pl.col(column)
192
+ .cast(pl.String)
193
+ .str.strip_chars()
194
+ .str.replace(r"\.0$", "")
195
+ .str.replace_all(r"\D", "")
196
+ for column in cip_columns
197
+ ]
198
+
199
+ if definition == "sarah":
200
+ conditions = [
201
+ cip.str.slice(0, 2).is_in(SARAH_STEM_CIP_FAMILIES)
202
+ for cip in cip_digits
203
+ ]
204
+ elif definition == "dhs":
205
+ conditions = [
206
+ cip.str.slice(0, 2).is_in(DHS_STEM_CIP_FAMILIES)
207
+ | ((cip.str.len_chars() == 6) & cip.is_in(DHS_STEM_CIP_ADDITIONS))
208
+ for cip in cip_digits
209
+ ]
210
+ else:
211
+ raise ValueError(f"Unknown STEM definition: {definition}")
212
+
213
+ return pl.any_horizontal(conditions).fill_null(False).cast(pl.Int8)
214
+
92
215
 
93
216
  ###########################################################
94
217
  # Read college crosswalk
@@ -621,6 +744,10 @@ nsc_new = (
621
744
  "Enrollment CIP 1": "enrollmentcip1",
622
745
  "Enrollment Major 2": "enrollmentmajor2",
623
746
  "Enrollment CIP 2": "enrollmentcip2",
747
+ "Enrollment Major 3": "enrollmentmajor3",
748
+ "Enrollment CIP 3": "enrollmentcip3",
749
+ "Enrollment Major 4": "enrollmentmajor4",
750
+ "Enrollment CIP 4": "enrollmentcip4",
624
751
  "Graduated?": "graduated",
625
752
  "Graduation Date": "graduationdate",
626
753
  "Degree Title": "degreetitle",
@@ -635,7 +762,7 @@ nsc_new = (
635
762
  "College Sequence": "collegesequence",
636
763
  "Student Age at start of term": "studentageatstartofterm",
637
764
 
638
- })
765
+ }, strict=False)
639
766
  .with_row_index("origsrt", offset=1)
640
767
  .filter(pl.col("studyid").is_not_null())
641
768
  .with_columns(
@@ -689,6 +816,15 @@ nsc = (
689
816
  pl.concat([nsc_old, nsc_new], how="diagonal_relaxed")
690
817
  )
691
818
 
819
+ # Older NSC extracts may omit some enrollment-major slots entirely. Add those
820
+ # slots as nulls so enrollment and degree majors always retain distinct names.
821
+ for column in [
822
+ *[f"enrollmentcip{index}" for index in range(1, 5)],
823
+ *[f"enrollmentmajor{index}" for index in range(1, 5)],
824
+ ]:
825
+ if column not in nsc.columns:
826
+ nsc = nsc.with_columns(pl.lit(None).cast(pl.String).alias(column))
827
+
692
828
  required_nsc_columns = [
693
829
  "sid_cepr",
694
830
  "ID_FSC",
@@ -708,6 +844,14 @@ required_nsc_columns = [
708
844
  "degreemajor2",
709
845
  "degreemajor3",
710
846
  "degreemajor4",
847
+ "enrollmentcip1",
848
+ "enrollmentcip2",
849
+ "enrollmentcip3",
850
+ "enrollmentcip4",
851
+ "enrollmentmajor1",
852
+ "enrollmentmajor2",
853
+ "enrollmentmajor3",
854
+ "enrollmentmajor4",
711
855
  "NSCdatayear",
712
856
  "searchbeginyear",
713
857
  "collegesequence",
@@ -768,6 +912,32 @@ nsc = (
768
912
  )
769
913
  )
770
914
 
915
+ # Impute every missing enrollment and degree CIP slot independently from its
916
+ # corresponding major name, using the same ordered rules Sarah uses for degree 1.
917
+ for cip_prefix, major_prefix in [
918
+ ("enrollmentcip", "enrollmentmajor"),
919
+ ("degreecip", "degreemajor"),
920
+ ]:
921
+ nsc = nsc.with_columns(
922
+ [
923
+ impute_cip_from_major(
924
+ f"{cip_prefix}{index}",
925
+ f"{major_prefix}{index}",
926
+ )
927
+ for index in range(1, 5)
928
+ ]
929
+ )
930
+
931
+ # A row is STEM when any of its four imputed enrollment-major CIPs qualify.
932
+ enrollment_cip_columns = [f"enrollmentcip{index}" for index in range(1, 5)]
933
+ nsc = nsc.with_columns(
934
+ [
935
+ stem_flag_from_cips(enrollment_cip_columns, definition)
936
+ .alias(f"_enrollment_stem_{definition}")
937
+ for definition in STEM_DEFINITIONS
938
+ ]
939
+ )
940
+
771
941
 
772
942
  ###########################################################
773
943
  # Build enrollment outcomes
@@ -895,6 +1065,8 @@ enroll_weeks = (
895
1065
  "week_end",
896
1066
  "_term_att",
897
1067
  "_enrollment_score",
1068
+ "_enrollment_stem_sarah",
1069
+ "_enrollment_stem_dhs",
898
1070
  "NSCdatayear",
899
1071
  "searchbeginyear",
900
1072
  "collegesequence",
@@ -925,6 +1097,14 @@ enroll_weeks = (
925
1097
  (pl.col("week") != pl.col("week").shift(1) + 1)
926
1098
  | (pl.col("sid_cepr") != pl.col("sid_cepr").shift(1))
927
1099
  | (pl.col("ID_FSC") != pl.col("ID_FSC").shift(1))
1100
+ | (
1101
+ pl.col("_enrollment_stem_sarah")
1102
+ != pl.col("_enrollment_stem_sarah").shift(1)
1103
+ )
1104
+ | (
1105
+ pl.col("_enrollment_stem_dhs")
1106
+ != pl.col("_enrollment_stem_dhs").shift(1)
1107
+ )
928
1108
  )
929
1109
  .fill_null(True)
930
1110
  .cast(pl.Int64)
@@ -949,6 +1129,8 @@ enroll = (
949
1129
  pl.col("k_mean").drop_nulls().first(),
950
1130
  pl.col("completion_rate_150pct_ip").drop_nulls().first(),
951
1131
  pl.col("_term_att").max(),
1132
+ pl.col("_enrollment_stem_sarah").max(),
1133
+ pl.col("_enrollment_stem_dhs").max(),
952
1134
  pl.col("week").min().alias("week_start"),
953
1135
  pl.col("week").max().alias("week_end"),
954
1136
  pl.col("_enrollment_score").max(),
@@ -1019,6 +1201,32 @@ for year in range(1, N_YEARS_OUT + 1):
1019
1201
  .alias(f"att_{college_type}_{label}")
1020
1202
  for college_type in COLLEGE_TYPES
1021
1203
  ]
1204
+ + [
1205
+ expression
1206
+ for definition in STEM_DEFINITIONS
1207
+ for expression in [
1208
+ (
1209
+ pl.col("_overlaps")
1210
+ * pl.col(f"_enrollment_stem_{definition}")
1211
+ )
1212
+ .max()
1213
+ .alias(f"att_stem_{definition}_{label}"),
1214
+ (
1215
+ pl.col("_overlaps")
1216
+ * pl.col("college_4yr").fill_null(0).cast(pl.Int8)
1217
+ * pl.col(f"_enrollment_stem_{definition}")
1218
+ )
1219
+ .max()
1220
+ .alias(f"att_4yr_stem_{definition}_{label}"),
1221
+ (
1222
+ pl.col("_overlaps")
1223
+ * pl.col("college_2yr").fill_null(0).cast(pl.Int8)
1224
+ * pl.col(f"_enrollment_stem_{definition}")
1225
+ )
1226
+ .max()
1227
+ .alias(f"att_2yr_stem_{definition}_{label}"),
1228
+ ]
1229
+ ]
1022
1230
  )
1023
1231
  )
1024
1232
 
@@ -1053,6 +1261,32 @@ for age in AGE_ATTENDANCE_RANGE:
1053
1261
  .alias(f"att_{college_type}_{age}")
1054
1262
  for college_type in AGE_ATTENDANCE_TYPES
1055
1263
  ]
1264
+ + [
1265
+ expression
1266
+ for definition in STEM_DEFINITIONS
1267
+ for expression in [
1268
+ (
1269
+ pl.col("_overlaps")
1270
+ * pl.col(f"_enrollment_stem_{definition}")
1271
+ )
1272
+ .max()
1273
+ .alias(f"att_stem_{definition}_{age}"),
1274
+ (
1275
+ pl.col("_overlaps")
1276
+ * pl.col("college_4yr").fill_null(0).cast(pl.Int8)
1277
+ * pl.col(f"_enrollment_stem_{definition}")
1278
+ )
1279
+ .max()
1280
+ .alias(f"att_4yr_stem_{definition}_{age}"),
1281
+ (
1282
+ pl.col("_overlaps")
1283
+ * pl.col("college_2yr").fill_null(0).cast(pl.Int8)
1284
+ * pl.col(f"_enrollment_stem_{definition}")
1285
+ )
1286
+ .max()
1287
+ .alias(f"att_2yr_stem_{definition}_{age}"),
1288
+ ]
1289
+ ]
1056
1290
  )
1057
1291
  )
1058
1292
 
@@ -1065,7 +1299,17 @@ for piece in enrollment_pieces[1:]:
1065
1299
  # Sarah makes these categories disjoint: a 4-year enrollment wins over 2-year.
1066
1300
  for column in enrollment_outcomes.columns:
1067
1301
  if column.startswith("att_2yr_"):
1068
- four_year_column = column.replace("att_2yr_", "att_4yr_")
1302
+ if "_stem_" in column:
1303
+ definition = next(
1304
+ item for item in STEM_DEFINITIONS if f"_stem_{item}_" in column
1305
+ )
1306
+ four_year_column = column.replace(
1307
+ f"att_2yr_stem_{definition}_",
1308
+ "att_4yr_",
1309
+ )
1310
+ else:
1311
+ four_year_column = column.replace("att_2yr_", "att_4yr_")
1312
+
1069
1313
  enrollment_outcomes = enrollment_outcomes.with_columns(
1070
1314
  pl.when(pl.col(four_year_column) == 1)
1071
1315
  .then(0)
@@ -1211,211 +1455,21 @@ degrees = (
1211
1455
  .otherwise(pl.col("_degree"))
1212
1456
  .alias("_degree")
1213
1457
  )
1214
- .with_columns(
1215
- pl.col("degreemajor1").cast(pl.String).str.to_uppercase().alias("_major1_upper"),
1216
- pl.col("degreecip1").cast(pl.String).str.strip_chars().alias("_degreecip1_clean"),
1217
- )
1218
- .with_columns(
1219
- pl.when(
1220
- pl.col("_degreecip1_clean").is_null()
1221
- | (pl.col("_degreecip1_clean") == "")
1222
- | (pl.col("_degreecip1_clean") == ".")
1223
- )
1224
- .then(
1225
- pl.when(pl.col("_major1_upper").str.contains("ECONOMICS", literal=False))
1226
- .then(pl.lit("4506"))
1227
- .when(
1228
- pl.col("_major1_upper").str.contains(
1229
- "BUSINESS|FINANCE|OPERATIONS MANAG", literal=False
1230
- )
1231
- )
1232
- .then(pl.lit("52"))
1233
- .when(pl.col("_major1_upper").str.contains("MARKETING", literal=False))
1234
- .then(pl.lit("5214"))
1235
- .when(pl.col("_major1_upper").str.contains("MANAGEMENT", literal=False))
1236
- .then(pl.lit("5213"))
1237
- .when(
1238
- pl.col("_major1_upper").str.contains(
1239
- "ANTHRO|ARCHEO|CRIMINO|DEMOGRA|POLITIC|SOCIOLO|HUMANI|GOVERN|GEOGRA",
1240
- literal=False,
1241
- )
1242
- )
1243
- .then(pl.lit("45"))
1244
- .when(
1245
- pl.col("_major1_upper").str.contains(
1246
- "VISUAL|PERFORM|ART|DANCE|PHOTO|FILM|MUSIC|MEDIA|ENTERTAIN|DRAMA|CRAFT|THEATRE|THEATER|DESIGN",
1247
- literal=False,
1248
- )
1249
- )
1250
- .then(pl.lit("50"))
1251
- .when(pl.col("_major1_upper").str.contains("HISTORY", literal=False))
1252
- .then(pl.lit("54"))
1253
- .when(pl.col("_major1_upper").str.contains("PSYCHOLOGY", literal=False))
1254
- .then(pl.lit("42"))
1255
- .when(pl.col("_major1_upper").str.contains("PHILOSO|RELIG", literal=False))
1256
- .then(pl.lit("38"))
1257
- .when(
1258
- pl.col("_major1_upper").str.contains(
1259
- "LIBERAL|GENERAL STUDIES", literal=False
1260
- )
1261
- )
1262
- .then(pl.lit("24"))
1263
- .when(
1264
- pl.col("_major1_upper").str.contains(
1265
- "ENGLISH|ENGLISH LANGUAGE|ENGLISH LIERATURE|WRITING",
1266
- literal=False,
1267
- )
1268
- )
1269
- .then(pl.lit("23"))
1270
- .when(pl.col("_major1_upper").str.contains("LEGAL|LAW", literal=False))
1271
- .then(pl.lit("22"))
1272
- .when(
1273
- pl.col("_major1_upper").str.contains(
1274
- "FOREIGN LANGUAGE|LANGUAGES|SPAN|JAPAN|CHIN|COMPARATIVE LING|FRENCH|CLASSICS",
1275
- literal=False,
1276
- )
1277
- )
1278
- .then(pl.lit("16"))
1279
- .when(
1280
- pl.col("_major1_upper").str.contains(
1281
- "EDUCATION|EDUC|CURRIC|EARLY CHI|SCHOOL COUN|STUDENT COUN",
1282
- literal=False,
1283
- )
1284
- )
1285
- .then(pl.lit("13"))
1286
- .when(
1287
- pl.col("_major1_upper").str.contains(
1288
- "COMMUNI|PUBLIC RELA|ADVERTIS|RADIO|TELEV|JOURNA",
1289
- literal=False,
1290
- )
1291
- )
1292
- .then(pl.lit("09"))
1293
- .when(
1294
- pl.col("_major1_upper").str.contains(
1295
- "ETHNIC|CULTURAL|GENDER|WOMEN", literal=False
1296
- )
1297
- )
1298
- .then(pl.lit("05"))
1299
- .when(pl.col("_major1_upper").str.contains("ENGI", literal=False))
1300
- .then(pl.lit("14"))
1301
- .when(
1302
- pl.col("_major1_upper").str.contains(
1303
- "COMPUT|INFORMATION TECH", literal=False
1304
- )
1305
- )
1306
- .then(pl.lit("11"))
1307
- .when(pl.col("_major1_upper").str.contains("LIBRA", literal=False))
1308
- .then(pl.lit("25"))
1309
- .when(
1310
- pl.col("_major1_upper").str.contains(
1311
- "BIOLOG|NEURO|BIOTECH|BIOCHEM|BIOPHY|PHYSIO|BOTANY|ANATOMY|ZOO|GENETIC|GENOM|PHARMA|TOXICO|ECOLOG|EPIDEM|WILDLIFE",
1312
- literal=False,
1313
- )
1314
- )
1315
- .then(pl.lit("26"))
1316
- .when(pl.col("_major1_upper").str.contains("MATH", literal=False))
1317
- .then(pl.lit("27"))
1318
- .when(pl.col("_major1_upper").str.contains("THEOLOG", literal=False))
1319
- .then(pl.lit("39"))
1320
- .when(
1321
- pl.col("_major1_upper").str.contains(
1322
- "ASTRON|ATMOS|GEOLOG|CHEMI|PHYSIC|METEO", literal=False
1323
- )
1324
- )
1325
- .then(pl.lit("40"))
1326
- .when(
1327
- pl.col("_major1_upper").str.contains(
1328
- "PUBLIC ADMIN|PUBLIC POL|SOCIAL WORK|COMMUNITY ORG|SOCIAL SERVICE|HUMAN SERV",
1329
- literal=False,
1330
- )
1331
- )
1332
- .then(pl.lit("44"))
1333
- .when(
1334
- pl.col("_major1_upper").str.contains(
1335
- "VETERINARY|DENT|HEALTH SCI|PUBLIC HEALTH|REHAB|PHYSICAL THER|PREMED|PRE-MED|PRE-VET|NUR|MEDIC|HEALTH CARE ADM|CLINICAL LAB",
1336
- literal=False,
1337
- )
1338
- )
1339
- .then(pl.lit("51"))
1340
- .when(
1341
- pl.col("_major1_upper").str.contains(
1342
- "SPORT MANAGEMENT|SPORTS MANAGEMENT|KINESIO|EXER", literal=False
1343
- )
1344
- )
1345
- .then(pl.lit("31"))
1346
- .when(
1347
- pl.col("_major1_upper").str.contains(
1348
- "PLANT|SOIL|AGRI|TURF|HORTIC|FOOD SCI|ANIMAL", literal=False
1349
- )
1350
- )
1351
- .then(pl.lit("01"))
1352
- .when(pl.col("_major1_upper").str.contains("ARCHI", literal=False))
1353
- .then(pl.lit("04"))
1354
- .when(
1355
- pl.col("_major1_upper").str.contains(
1356
- "HUMAN RESO|HOSPITALITY|HOTEL|FASHION MERCH", literal=False
1357
- )
1358
- )
1359
- .then(pl.lit("52"))
1360
- .when(
1361
- pl.col("_major1_upper").str.contains(
1362
- "FOREST|ENVIRONMENTAL STUD|ENVIRONMENTAL SCI", literal=False
1363
- )
1364
- )
1365
- .then(pl.lit("03"))
1366
- .when(
1367
- pl.col("_major1_upper").str.contains("FOODS|NUTRITION|HUMAN DEV", literal=False)
1368
- )
1369
- .then(pl.lit("19"))
1370
- .when(
1371
- pl.col("_major1_upper").str.contains(
1372
- "CRIMINAL JUS|CRIME & JUST", literal=False
1373
- )
1374
- )
1375
- .then(pl.lit("43"))
1376
- .when(pl.col("_major1_upper").str.contains("ACCOUNTING", literal=False))
1377
- .then(pl.lit("5203"))
1378
- .when(
1379
- pl.col("_major1_upper").str.contains(
1380
- "AUTOMOTIVE TECH|AUTOMOB", literal=False
1381
- )
1382
- )
1383
- .then(pl.lit("47"))
1384
- .otherwise(pl.col("_degreecip1_clean"))
1385
- )
1386
- .otherwise(pl.col("_degreecip1_clean"))
1387
- .alias("degreecip1_imputed")
1388
- )
1389
1458
  .with_columns(
1390
1459
  [
1391
- pl.when(i == 1)
1392
- .then(pl.col("degreecip1_imputed"))
1393
- .otherwise(pl.col(f"degreecip{i}"))
1394
- .cast(pl.String)
1395
- .str.replace_all(r"\D", "")
1396
- .str.slice(0, 2)
1397
- .cast(pl.Int64, strict=False)
1398
- .alias(f"_cip_family{i}")
1399
- for i in range(1, 5)
1460
+ stem_flag_from_cips(
1461
+ [f"degreecip{index}" for index in range(1, 5)],
1462
+ definition,
1463
+ ).alias(f"degree_stem_{definition}")
1464
+ for definition in STEM_DEFINITIONS
1400
1465
  ]
1401
1466
  )
1402
1467
  .with_columns(
1403
- pl.any_horizontal(
1404
- [pl.col(f"_cip_family{i}").is_in(STEM_CIP_FAMILIES) for i in range(1, 5)]
1405
- )
1406
- .fill_null(False)
1407
- .cast(pl.Int8)
1408
- .alias("degree_stem"),
1409
1468
  pl.coalesce(
1410
- [pl.col("degreecip1_imputed")]
1411
- + [pl.col(f"degreecip{i}") for i in range(2, 5)]
1469
+ [pl.col(f"degreecip{index}") for index in range(1, 5)]
1412
1470
  ).alias("degreecip"),
1413
1471
  pl.coalesce(
1414
- [
1415
- pl.col(column)
1416
- for column in nsc.columns
1417
- if column.startswith("degreemajor")
1418
- ]
1472
+ [pl.col(f"degreemajor{index}") for index in range(1, 5)]
1419
1473
  ).alias("degreemajor"),
1420
1474
  )
1421
1475
  )
@@ -1452,6 +1506,33 @@ for year in range(1, N_YEARS_OUT + 1):
1452
1506
  )
1453
1507
  .max()
1454
1508
  .alias(f"cmp_elite_byY{year}"),
1509
+ *[
1510
+ expression
1511
+ for definition in STEM_DEFINITIONS
1512
+ for expression in [
1513
+ (
1514
+ pl.col("_by_end")
1515
+ * pl.col("_degree").is_in(["AA", "BA"]).cast(pl.Int8)
1516
+ * pl.col(f"degree_stem_{definition}")
1517
+ )
1518
+ .max()
1519
+ .alias(f"cmp_stem_{definition}_byY{year}"),
1520
+ (
1521
+ pl.col("_by_end")
1522
+ * (pl.col("_degree") == "AA").cast(pl.Int8)
1523
+ * pl.col(f"degree_stem_{definition}")
1524
+ )
1525
+ .max()
1526
+ .alias(f"cmp_AA_stem_{definition}_byY{year}"),
1527
+ (
1528
+ pl.col("_by_end")
1529
+ * (pl.col("_degree") == "BA").cast(pl.Int8)
1530
+ * pl.col(f"degree_stem_{definition}")
1531
+ )
1532
+ .max()
1533
+ .alias(f"cmp_BA_stem_{definition}_byY{year}"),
1534
+ ]
1535
+ ],
1455
1536
  )
1456
1537
  )
1457
1538
 
@@ -1494,7 +1575,8 @@ degree_details = (
1494
1575
  pl.col("graduated_date").first().alias("degree_date"),
1495
1576
  pl.col("degreecip").first().alias("degreecip"),
1496
1577
  pl.col("degreemajor").first().alias("degreemajor"),
1497
- pl.col("degree_stem").max().alias("degree_stem"),
1578
+ pl.col("degree_stem_sarah").max().alias("degree_stem_sarah"),
1579
+ pl.col("degree_stem_dhs").max().alias("degree_stem_dhs"),
1498
1580
  pl.col("ID_FSC").first().alias("ID_FSC_degree"),
1499
1581
  pl.col("college_name").first().alias("college_name_degree"),
1500
1582
  pl.col("college_years").first().alias("college_years_degree"),
@@ -1727,6 +1809,39 @@ outcome_columns = [
1727
1809
  "cmp_elite_byY4", "cmp_elite_byY5", "cmp_elite_byY6", "cmp_elite_byY7", "cmp_elite_byY8",
1728
1810
  ]
1729
1811
 
1812
+ stem_outcome_columns = []
1813
+ for year in range(1, N_YEARS_OUT + 1):
1814
+ for prefix in ["in", "by"]:
1815
+ for term in ["", "fall", "spring"]:
1816
+ label = f"{prefix}Y{year}{term}"
1817
+ for definition in STEM_DEFINITIONS:
1818
+ stem_outcome_columns.extend(
1819
+ [
1820
+ f"att_stem_{definition}_{label}",
1821
+ f"att_4yr_stem_{definition}_{label}",
1822
+ f"att_2yr_stem_{definition}_{label}",
1823
+ ]
1824
+ )
1825
+
1826
+ for definition in STEM_DEFINITIONS:
1827
+ stem_outcome_columns.extend(
1828
+ [
1829
+ f"cmp_stem_{definition}_byY{year}",
1830
+ f"cmp_AA_stem_{definition}_byY{year}",
1831
+ f"cmp_BA_stem_{definition}_byY{year}",
1832
+ ]
1833
+ )
1834
+
1835
+ for age in AGE_ATTENDANCE_RANGE:
1836
+ for definition in STEM_DEFINITIONS:
1837
+ stem_outcome_columns.extend(
1838
+ [
1839
+ f"att_stem_{definition}_{age}",
1840
+ f"att_4yr_stem_{definition}_{age}",
1841
+ f"att_2yr_stem_{definition}_{age}",
1842
+ ]
1843
+ )
1844
+
1730
1845
  nsc_outcomes = nsc_outcomes.with_columns(
1731
1846
  pl.any_horizontal([pl.col(column).fill_null(0) == 1 for column in outcome_columns])
1732
1847
  .cast(pl.Int8)
@@ -1747,7 +1862,7 @@ keep_columns = [
1747
1862
  "college_name_firstinst",
1748
1863
  "tier_firstinst",
1749
1864
  "completion_rate_150pct_firstinst",
1750
- ] + outcome_columns
1865
+ ] + outcome_columns + stem_outcome_columns
1751
1866
 
1752
1867
  nsc_outcomes_final = nsc_outcomes.select(keep_columns)
1753
1868
 
@@ -0,0 +1,252 @@
1
+ cip6
2
+ 010308
3
+ 010901
4
+ 010902
5
+ 010903
6
+ 010904
7
+ 010905
8
+ 010906
9
+ 010907
10
+ 010999
11
+ 011001
12
+ 011002
13
+ 011099
14
+ 011101
15
+ 011102
16
+ 011103
17
+ 011104
18
+ 011105
19
+ 011106
20
+ 011199
21
+ 011201
22
+ 011202
23
+ 011203
24
+ 011299
25
+ 018105
26
+ 018106
27
+ 018107
28
+ 018108
29
+ 018109
30
+ 018110
31
+ 018111
32
+ 030101
33
+ 030103
34
+ 030104
35
+ 030199
36
+ 030204
37
+ 030205
38
+ 030210
39
+ 030501
40
+ 030502
41
+ 030508
42
+ 030509
43
+ 030510
44
+ 030601
45
+ 040601
46
+ 040902
47
+ 090702
48
+ 100304
49
+ 110101
50
+ 110102
51
+ 110103
52
+ 110104
53
+ 110105
54
+ 110199
55
+ 110201
56
+ 110202
57
+ 110203
58
+ 110299
59
+ 110301
60
+ 110401
61
+ 110501
62
+ 110701
63
+ 110801
64
+ 110802
65
+ 110803
66
+ 110804
67
+ 110899
68
+ 110901
69
+ 110902
70
+ 111001
71
+ 111002
72
+ 111003
73
+ 111004
74
+ 111005
75
+ 111006
76
+ 111099
77
+ 130501
78
+ 130601
79
+ 130603
80
+ 130608
81
+ 150000
82
+ 150101
83
+ 150201
84
+ 150303
85
+ 150304
86
+ 150305
87
+ 150306
88
+ 150399
89
+ 150401
90
+ 150403
91
+ 150404
92
+ 150405
93
+ 150406
94
+ 150407
95
+ 150499
96
+ 150501
97
+ 150506
98
+ 150507
99
+ 150508
100
+ 150599
101
+ 150607
102
+ 150611
103
+ 150612
104
+ 150613
105
+ 150614
106
+ 150615
107
+ 150616
108
+ 150617
109
+ 150699
110
+ 150701
111
+ 150702
112
+ 150703
113
+ 150704
114
+ 150799
115
+ 150801
116
+ 150803
117
+ 150805
118
+ 150899
119
+ 150901
120
+ 150903
121
+ 150999
122
+ 151001
123
+ 151102
124
+ 151103
125
+ 151199
126
+ 151201
127
+ 151202
128
+ 151203
129
+ 151204
130
+ 151299
131
+ 151301
132
+ 151302
133
+ 151303
134
+ 151304
135
+ 151305
136
+ 151306
137
+ 151399
138
+ 151401
139
+ 151501
140
+ 151502
141
+ 151503
142
+ 151599
143
+ 151601
144
+ 151701
145
+ 151703
146
+ 159999
147
+ 280501
148
+ 280502
149
+ 280505
150
+ 290201
151
+ 290202
152
+ 290203
153
+ 290204
154
+ 290205
155
+ 290206
156
+ 290207
157
+ 290299
158
+ 290301
159
+ 290302
160
+ 290303
161
+ 290304
162
+ 290305
163
+ 290306
164
+ 290307
165
+ 290399
166
+ 290401
167
+ 290402
168
+ 290403
169
+ 290404
170
+ 290405
171
+ 290406
172
+ 290407
173
+ 290408
174
+ 290409
175
+ 290499
176
+ 299999
177
+ 300101
178
+ 300601
179
+ 300801
180
+ 301001
181
+ 301701
182
+ 301801
183
+ 301901
184
+ 302501
185
+ 302701
186
+ 303001
187
+ 303101
188
+ 303201
189
+ 303301
190
+ 303401
191
+ 303501
192
+ 303801
193
+ 303901
194
+ 304101
195
+ 304301
196
+ 304401
197
+ 304801
198
+ 304901
199
+ 305001
200
+ 307001
201
+ 307101
202
+ 307102
203
+ 307103
204
+ 307104
205
+ 307199
206
+ 410000
207
+ 410101
208
+ 410204
209
+ 410205
210
+ 410299
211
+ 410301
212
+ 410303
213
+ 410399
214
+ 419999
215
+ 422701
216
+ 422702
217
+ 422703
218
+ 422704
219
+ 422705
220
+ 422706
221
+ 422707
222
+ 422708
223
+ 422709
224
+ 422710
225
+ 422799
226
+ 422804
227
+ 430403
228
+ 430406
229
+ 430407
230
+ 450102
231
+ 450301
232
+ 450501
233
+ 450603
234
+ 450702
235
+ 490101
236
+ 511002
237
+ 511005
238
+ 511401
239
+ 512003
240
+ 512004
241
+ 512005
242
+ 512006
243
+ 512007
244
+ 512009
245
+ 512010
246
+ 512202
247
+ 512205
248
+ 512706
249
+ 521301
250
+ 521302
251
+ 521304
252
+ 521399
@@ -0,0 +1,50 @@
1
+ # Rebuild the fixed DHS six-digit additions lookup from the July 22, 2024 PDF.
2
+ # Run from this directory with: Rscript extract_dhs_stem_cips.R
3
+
4
+ # Source: https://www.ice.gov/doclib/sevis/pdf/stemList2024.pdf
5
+
6
+ args <- commandArgs(trailingOnly = FALSE)
7
+ file_arg <- grep("^--file=", args, value = TRUE)
8
+ script_dir <- if (length(file_arg) == 1) {
9
+ dirname(normalizePath(sub("^--file=", "", file_arg)))
10
+ } else {
11
+ getwd()
12
+ }
13
+
14
+ pdf_path <- file.path(script_dir, "stemList2024.pdf")
15
+ output_path <- file.path(script_dir, "dhs_stem_cip_additions_2024.csv")
16
+
17
+ pdftotext <- Sys.which("pdftotext")
18
+ if (pdftotext == "") {
19
+ stop("Install Poppler so the pdftotext executable is available on PATH.")
20
+ }
21
+
22
+ text_path <- tempfile(fileext = ".txt")
23
+ on.exit(unlink(text_path), add = TRUE)
24
+ status <- system2(pdftotext, c("-layout", shQuote(pdf_path), shQuote(text_path)))
25
+ if (status != 0) {
26
+ stop("pdftotext could not extract the DHS PDF.")
27
+ }
28
+
29
+ pdf_text <- paste(readLines(text_path, warn = FALSE), collapse = "\n")
30
+ cip_codes <- unique(regmatches(
31
+ pdf_text,
32
+ gregexpr("(?<![0-9])[0-9]{2}\\.[0-9]{4}(?![0-9])", pdf_text, perl = TRUE)
33
+ )[[1]])
34
+
35
+ core_families <- c("14", "26", "27", "40")
36
+ cip_codes <- sort(cip_codes[!substr(cip_codes, 1, 2) %in% core_families])
37
+ cip_codes <- gsub("\\.", "", cip_codes)
38
+
39
+ stopifnot(length(cip_codes) == 251)
40
+ stopifnot(!any(substr(cip_codes, 1, 2) %in% core_families))
41
+ stopifnot(all(grepl("^[0-9]{6}$", cip_codes)))
42
+
43
+ write.csv(
44
+ data.frame(cip6 = cip_codes),
45
+ output_path,
46
+ row.names = FALSE,
47
+ quote = FALSE
48
+ )
49
+
50
+ message("Wrote ", length(cip_codes), " DHS six-digit additions to ", output_path)
File without changes