|
15 | 15 | # COMMAND ---------- |
16 | 16 |
|
17 | 17 | # MAGIC %md |
18 | | -# MAGIC # Setup |
| 18 | +# MAGIC # setup |
19 | 19 |
|
20 | 20 | # COMMAND ---------- |
21 | 21 |
|
22 | 22 | # MAGIC %sh python --version |
23 | 23 |
|
24 | 24 | # COMMAND ---------- |
25 | 25 |
|
26 | | -# install dependencies, most of which should come through our 1st-party SST package |
| 26 | +# install dependencies, most/all of which should come through our 1st-party SST package |
| 27 | +# NOTE: it's okay to use 'develop' or a feature branch while developing this nb |
| 28 | +# but when it's finished, it's best to pin to a specific version of the package |
| 29 | +# %pip install "student-success-tool == 0.1.0" |
27 | 30 | # %pip install git+https://github.com/datakind/student-success-tool.git@develop |
28 | 31 |
|
29 | 32 | # COMMAND ---------- |
|
33 | 36 | # COMMAND ---------- |
34 | 37 |
|
35 | 38 | import logging |
36 | | -import os |
37 | 39 | import sys |
38 | 40 |
|
39 | 41 | import matplotlib.pyplot as plt |
|
44 | 46 | from databricks.connect import DatabricksSession |
45 | 47 | from databricks.sdk.runtime import dbutils |
46 | 48 |
|
| 49 | +from student_success_tool import configs |
47 | 50 | from student_success_tool.analysis import pdp |
48 | 51 |
|
49 | 52 | # COMMAND ---------- |
50 | 53 |
|
51 | | -logging.basicConfig(level=logging.INFO) |
| 54 | +logging.basicConfig(level=logging.INFO, force=True) |
52 | 55 | logging.getLogger("py4j").setLevel(logging.WARNING) # ignore databricks logger |
53 | 56 |
|
54 | 57 | try: |
55 | | - spark_session = DatabricksSession.builder.getOrCreate() |
| 58 | + spark = DatabricksSession.builder.getOrCreate() |
56 | 59 | except Exception: |
57 | 60 | logging.warning("unable to create spark session; are you in a Databricks runtime?") |
58 | 61 | pass |
59 | 62 |
|
60 | 63 | # COMMAND ---------- |
61 | 64 |
|
62 | 65 | # MAGIC %md |
63 | | -# MAGIC ## `student-success-intervention` hacks |
| 66 | +# MAGIC ## import school-specific code |
64 | 67 |
|
65 | 68 | # COMMAND ---------- |
66 | 69 |
|
|
69 | 72 |
|
70 | 73 | # COMMAND ---------- |
71 | 74 |
|
72 | | -# HACK: insert our 1st-party (school-specific) code into PATH |
| 75 | +# insert our 1st-party (school-specific) code into PATH |
73 | 76 | if "../" not in sys.path: |
74 | 77 | sys.path.insert(1, "../") |
75 | 78 |
|
76 | | -# TODO: specify school's subpackage |
| 79 | +# TODO: specify school's subpackage here |
77 | 80 | from analysis import * # noqa: F403 |
78 | 81 |
|
79 | 82 | # COMMAND ---------- |
80 | 83 |
|
81 | | -# MAGIC %md |
82 | | -# MAGIC ## unity catalog config |
83 | | - |
84 | | -# COMMAND ---------- |
85 | | - |
86 | | -catalog = "sst_dev" |
87 | | - |
88 | | -# configure where data is to be read from / written to |
89 | | -inst_name = "SCHOOL" # TODO: fill in school's name in Unity Catalog |
90 | | -read_schema = f"{inst_name}_bronze" |
91 | | -write_schema = f"{inst_name}_silver" |
92 | | - |
93 | | -path_volume = os.path.join( |
94 | | - "/Volumes", catalog, read_schema, f"{inst_name}_bronze_file_volume" |
95 | | -) |
96 | | -path_table = f"{catalog}.{read_schema}" |
97 | | -print(f"{path_table=}") |
98 | | -print(f"{path_volume=}") |
| 84 | +# project configuration should be stored in a config file in TOML format |
| 85 | +# it'll start out with just basic info: institution_id, institution_name |
| 86 | +# but as each step of the pipeline gets built, more parameters will be moved |
| 87 | +# from hard-coded notebook variables to shareable, persistent config fields |
| 88 | +cfg = configs.load_config("./config-v2-TEMPLATE.toml", configs.PDPProjectConfigV2) |
| 89 | +cfg |
99 | 90 |
|
100 | 91 | # COMMAND ---------- |
101 | 92 |
|
102 | 93 | # MAGIC %md |
103 | | -# MAGIC # Read and Validate Raw Data |
| 94 | +# MAGIC # read and validate raw data |
104 | 95 |
|
105 | 96 | # COMMAND ---------- |
106 | 97 |
|
|
109 | 100 |
|
110 | 101 | # COMMAND ---------- |
111 | 102 |
|
112 | | -# TODO: fill in school's name; may not be same as in the schemas above |
113 | | -fpath_course = os.path.join(path_volume, "SCHOOL_COURSE_AR_DEID_DTTM.csv") |
| 103 | +# TODO: fill in the actual path to school's raw course file |
| 104 | +# okay to add it to project config now or later, whatever you prefer |
| 105 | +raw_course_file_path = cfg.datasets["labeled"].raw_course.file_path |
| 106 | +# raw_course_file_path = "/Volumes/CATALOG/INST_NAME_bronze/INST_NAME_bronze_file_volume/SCHOOL_COURSE_AR_DEID_DTTM.csv" |
114 | 107 |
|
115 | 108 | # COMMAND ---------- |
116 | 109 |
|
117 | 110 | # read without any schema validation, so we can look at the data "raw" |
118 | 111 | df_course_raw = pdp.dataio.read_raw_pdp_course_data_from_file( |
119 | | - fpath_course, schema=None, dttm_format="%Y%m%d.0" |
| 112 | + raw_course_file_path, schema=None, dttm_format="%Y%m%d.0" |
120 | 113 | ) |
121 | 114 | print(f"rows x cols = {df_course_raw.shape}") |
122 | 115 | df_course_raw.head() |
|
127 | 120 |
|
128 | 121 | # COMMAND ---------- |
129 | 122 |
|
| 123 | +df_course_raw["course_begin_date"].describe() |
| 124 | + |
| 125 | +# COMMAND ---------- |
| 126 | + |
130 | 127 | # MAGIC %md |
131 | 128 | # MAGIC Quick checks: |
132 | 129 | # MAGIC - [ ] data exists where it should |
|
137 | 134 |
|
138 | 135 | # try to read data while validating with the "base" PDP schema |
139 | 136 | df_course = pdp.dataio.read_raw_pdp_course_data_from_file( |
140 | | - fpath_course, schema=pdp.schemas.RawPDPCourseDataSchema, dttm_format="%Y%m%d.0" |
| 137 | + raw_course_file_path, |
| 138 | + schema=pdp.schemas.RawPDPCourseDataSchema, |
| 139 | + dttm_format="%Y%m%d.0", |
141 | 140 | ) |
142 | 141 | df_course |
143 | 142 |
|
144 | 143 | # COMMAND ---------- |
145 | 144 |
|
146 | 145 | # MAGIC %md |
147 | | -# MAGIC If the above command works, and `df_course` is indeed a `pd.DataFrame` containing the validated + parsed PDP cohort dataset, then you're all set, and can skip ahead to the next section. If not, and this is instead a json blob of schema errors, then you'll need to iteratively develop school-specific overrides. There are existing examples you can refer to in the `student-success-intervention` repo. |
| 146 | +# MAGIC If the above command works, and `df_course` is indeed a `pd.DataFrame` containing the validated + parsed PDP cohort dataset, then you're all set, and can skip ahead to the next section. If not, and this is instead a json blob of schema errors, then you'll need to inspect those errors and iteratively develop school-specific overrides to handle them. There are existing examples you can refer to in the `student-success-intervention` repo if you're unsure. |
148 | 147 | # MAGIC |
149 | 148 | # MAGIC This will involve some ad-hoc exploratory work, depending on the schema errors. For example: |
150 | 149 | # MAGIC |
|
199 | 198 | # MAGIC ``` |
200 | 199 | # MAGIC |
201 | 200 | # MAGIC At this point, `df_course` should be a properly validated and parsed data frame, ready for exploratory data analysis. |
202 | | - |
| 201 | +# MAGIC |
203 | 202 |
|
204 | 203 | # COMMAND ---------- |
205 | 204 |
|
|
208 | 207 |
|
209 | 208 | # COMMAND ---------- |
210 | 209 |
|
211 | | - |
212 | | -# TODO: fill in school's name; may not be same as in the schemas above |
213 | | -fpath_cohort = os.path.join(path_volume, "SCHOOL_COHORT_AR_DEID_DTTM.csv") |
| 210 | +# TODO: fill in the actual path to school's raw cohort file |
| 211 | +# okay to add it to project config now or later, whatever you prefer |
| 212 | +raw_cohort_file_path = cfg.datasets["labeled"].raw_cohort.file_path |
| 213 | +# raw_cohort_file_path = "/Volumes/CATALOG/INST_NAME_bronze/INST_NAME_bronze_file_volume/SCHOOL_COHORT_AR_DEID_DTTM.csv" |
214 | 214 |
|
215 | 215 | # COMMAND ---------- |
216 | 216 |
|
217 | 217 | # read without any schema validation, so we can look at the data "raw" |
218 | | -df_cohort_raw = pdp.dataio.read_raw_pdp_cohort_data_from_file(fpath_cohort, schema=None) |
| 218 | +df_cohort_raw = pdp.dataio.read_raw_pdp_cohort_data_from_file( |
| 219 | + raw_cohort_file_path, schema=None |
| 220 | +) |
219 | 221 | print(f"rows x cols = {df_cohort_raw.shape}") |
220 | 222 | df_cohort_raw.head() |
221 | 223 |
|
222 | 224 | # COMMAND ---------- |
223 | 225 |
|
224 | 226 | # try to read data while validating with the "base" PDP schema |
225 | 227 | df_cohort = pdp.dataio.read_raw_pdp_cohort_data_from_file( |
226 | | - fpath_cohort, schema=pdp.schemas.base.RawPDPCohortDataSchema |
| 228 | + raw_cohort_file_path, schema=pdp.schemas.base.RawPDPCohortDataSchema |
227 | 229 | ) |
228 | 230 | df_cohort |
229 | 231 |
|
|
242 | 244 | # COMMAND ---------- |
243 | 245 |
|
244 | 246 | # MAGIC %md |
245 | | -# MAGIC ## save validated data |
| 247 | +# MAGIC ## STOP HERE! |
| 248 | + |
| 249 | +# COMMAND ---------- |
| 250 | + |
| 251 | +# MAGIC %md |
| 252 | +# MAGIC Before continuing on to EDA, now's a great time to do a couple things: |
| 253 | +# MAGIC |
| 254 | +# MAGIC - Copy any school-specific raw dataset schemas into a `schemas.py` file in the current working directory |
| 255 | +# MAGIC - Copy any school-specific preprocessing functions needed to coerce the raw data into a standardized form into a `dataio.py` file in the current working directory |
| 256 | +# MAGIC - **Optional:** If you want easy access to outputs from every (sub-)step of the data transformation pipeline, save the validated datasets into this school's "silver" schema in Unity Catalog. |
246 | 257 |
|
247 | 258 | # COMMAND ---------- |
248 | 259 |
|
249 | 260 | pdp.dataio.write_data_to_delta_table( |
250 | 261 | df_course, |
251 | | - f"{catalog}.{write_schema}.course_dataset_validated", |
252 | | - spark_session=spark_session, |
| 262 | + "CATALOG.INST_NAME_silver.course_dataset_validated", |
| 263 | + spark_session=spark, |
253 | 264 | ) |
254 | 265 |
|
255 | 266 | # COMMAND ---------- |
256 | 267 |
|
257 | 268 | pdp.dataio.write_data_to_delta_table( |
258 | 269 | df_cohort, |
259 | | - f"{catalog}.{write_schema}.cohort_dataset_validated", |
260 | | - spark_session=spark_session, |
| 270 | + "CATALOG.INST_NAME_silver.cohort_dataset_validated", |
| 271 | + spark_session=spark, |
261 | 272 | ) |
262 | 273 |
|
263 | 274 | # COMMAND ---------- |
264 | 275 |
|
265 | 276 | # MAGIC %md |
266 | | -# MAGIC # Exploratory Data Analysis |
| 277 | +# MAGIC # exploratory data analysis |
267 | 278 |
|
268 | 279 | # COMMAND ---------- |
269 | 280 |
|
270 | | -# MAGIC %md |
271 | 281 | # MAGIC %md |
272 | 282 | # MAGIC ## read validated data |
273 | 283 | # MAGIC |
274 | | -# MAGIC (so you don't have to execute the validation process more than once) |
| 284 | +# MAGIC (optional, so you don't have to execute the validation process more than once) |
275 | 285 |
|
276 | 286 | # COMMAND ---------- |
277 | 287 |
|
278 | 288 | # use base or school-specific schema, as needed |
279 | 289 | df_course = pdp.schemas.RawPDPCourseDataSchema( |
280 | 290 | pdp.dataio.read_data_from_delta_table( |
281 | | - f"{catalog}.{write_schema}.course_dataset_validated", |
282 | | - spark_session=spark_session, |
| 291 | + "CATALOG.INST_NAME_silver.course_dataset_validated", |
| 292 | + spark_session=spark, |
283 | 293 | ) |
284 | 294 | ) |
285 | 295 | df_course.shape |
|
288 | 298 |
|
289 | 299 | df_cohort = pdp.schemas.RawCohortDataSchema( |
290 | 300 | pdp.dataio.read_data_from_delta_table( |
291 | | - f"{catalog}.{write_schema}.cohort_dataset_validated", |
292 | | - spark_session=spark_session, |
| 301 | + "CATALOG.INST_NAME_silver.cohort_dataset_validated", |
| 302 | + spark_session=spark, |
293 | 303 | ) |
294 | 304 | ) |
295 | 305 | df_cohort.shape |
|
307 | 317 | # COMMAND ---------- |
308 | 318 |
|
309 | 319 | # specific follow-ups, for example |
| 320 | +# df_course["academic_year"].value_counts(normalize=True, dropna=False) |
| 321 | +# df_course["academic_term"].value_counts(normalize=True, dropna=False) |
310 | 322 | # df_course["grade"].value_counts(normalize=True, dropna=False) |
311 | 323 | # df_course["delivery_method"].value_counts(normalize=True, dropna=False) |
| 324 | +# df_course["course_name"].value_counts(normalize=True, dropna=False).head(10) |
312 | 325 |
|
313 | 326 | # COMMAND ---------- |
314 | 327 |
|
|
317 | 330 | # COMMAND ---------- |
318 | 331 |
|
319 | 332 | # specific follow-ups, for example |
320 | | -# df_course["cohort"].value_counts(normalize=True, dropna=False) |
321 | | -# df_course["enrollment_type"].value_counts(normalize=True, dropna=False) |
| 333 | +# df_cohort["cohort"].value_counts(normalize=True, dropna=False) |
| 334 | +# df_cohort["enrollment_type"].value_counts(normalize=True, dropna=False) |
322 | 335 |
|
323 | 336 | # COMMAND ---------- |
324 | 337 |
|
|
509 | 522 |
|
510 | 523 | # COMMAND ---------- |
511 | 524 |
|
| 525 | +df_pre_cohort["enrollment_type"].value_counts() |
| 526 | + |
| 527 | +# COMMAND ---------- |
| 528 | + |
512 | 529 | # MAGIC %md |
513 | 530 | # MAGIC ### filter invalid rows(?) |
514 | 531 |
|
515 | 532 | # COMMAND ---------- |
516 | 533 |
|
517 | 534 | # this is probably a filter you'll want to apply |
518 | 535 | # these courses known to be an issue w/ PDP data |
519 | | -df_course_valid = df_course.loc[df_course["course_number"].notna(), :] |
520 | | -df_course_valid |
| 536 | +df_course_filtered = df_course.loc[df_course["course_number"].notna(), :] |
| 537 | +df_course_filtered.shape |
521 | 538 |
|
522 | 539 | # COMMAND ---------- |
523 | 540 |
|
|
527 | 544 | # COMMAND ---------- |
528 | 545 |
|
529 | 546 | # MAGIC %md |
530 | | -# MAGIC **Note:** You'll probably want to use the "valid" dataframes for most of these plots, but not necessarily for all. For simplicity, all these example plots will just use the base data w/o extra data validation filtering applied. It's your call! |
| 547 | +# MAGIC **Note:** You'll probably want to use the filtered dataframes for most of these plots, but not necessarily for all. Sometimes comparing the two can be instructive. For simplicity, all these example plots will just use the base data w/o extra data validation filtering applied. It's your call! |
531 | 548 |
|
532 | 549 | # COMMAND ---------- |
533 | 550 |
|
|
574 | 591 |
|
575 | 592 | ax = sb.histplot( |
576 | 593 | df_course.sort_values(by="academic_year"), |
| 594 | + # df_course_filtered.sort_values(by="academic_year"), |
577 | 595 | y="academic_year", |
578 | 596 | hue="academic_term", |
579 | 597 | multiple="stack", |
|
645 | 663 | ax = sb.histplot( |
646 | 664 | pd.merge( |
647 | 665 | df_course.groupby("student_guid") |
| 666 | + # df_course_filtered.groupby("student_guid") |
648 | 667 | .size() |
649 | 668 | .rename("num_courses_enrolled") |
650 | 669 | .reset_index(drop=False), |
|
667 | 686 | df_course.groupby("student_guid").agg( |
668 | 687 | {"number_of_credits_attempted": "sum", "number_of_credits_earned": "sum"} |
669 | 688 | ), |
| 689 | + # df_course_filtered.groupby("student_guid").agg( |
| 690 | + # {"number_of_credits_attempted": "sum", "number_of_credits_earned": "sum"} |
| 691 | + # ), |
670 | 692 | x="number_of_credits_attempted", |
671 | 693 | y="number_of_credits_earned", |
672 | 694 | kind="hex", |
|
764 | 786 | # COMMAND ---------- |
765 | 787 |
|
766 | 788 | # MAGIC %md |
767 | | -# MAGIC # Wrap-up |
| 789 | +# MAGIC # wrap-up |
768 | 790 |
|
769 | 791 | # COMMAND ---------- |
770 | 792 |
|
771 | 793 | # MAGIC %md |
772 | | -# MAGIC - [ ] Add school-specific data schemas and/or preprocessing functions into the appropriate directory in the [`student-success-intervention` repository](https://github.com/datakind/student-success-intervention) |
773 | | -# MAGIC - ... |
| 794 | +# MAGIC - [ ] If you haven't already, add school-specific data schemas and/or preprocessing functions into the appropriate directory in the [`student-success-intervention` repository](https://github.com/datakind/student-success-intervention) |
| 795 | +# MAGIC - [ ] Add file paths for the raw course/cohort datasets to the project config file's `datasets["labeled"].raw_course` and `datasets["labeled"].raw_cohort` blocks |
| 796 | +# MAGIC - [ ] Submit a PR including this notebook and any school-specific files added in order to run it |
774 | 797 |
|
775 | 798 | # COMMAND ---------- |
0 commit comments