MUSA Graderv0.1.0

User guide

Assignments

The dataset decides what can be checked. Each assignment gives the grader a different situation, and the rubric is designed around it.


Assignment 1: The Donut Effect#

rubrics/hw1.yamlassignments/hw1.pydata required

Students download their own Zillow ZHVI extract, and Zillow revises and extends that file every month. Row counts, ZIP counts and final percentages therefore differ legitimately between submissions, so the grader never compares against a stored answer. It asks whether each step in the instructions worked, and checks it against the student's own data:

InstructionHow it is checked
Load the dataA pandas import, a read_* call, a relative path, and a wide frame with many month columns
Trim to PhiladelphiaEvery ZIP in the subset is a Philadelphia ZIP, and City/State hold no other place
Melt to tidyRow count equals this student's own unique ZIPs × unique dates, with values in a column named ZHVI
Split Center CityThe two frames partition this student's own Philadelphia ZIP set, using the template's ZIP list
Percent increase functionCalled on a frame the grader builds, so the right answer is known whatever data was loaded. See Hidden tests.
Compare the two groupsBoth averages exist, are plausible, and point in the Donut Effect direction

The only fixed values in the rubric are the ones the assignment itself fixes: the Center City ZIP list printed in the student template, and the two comparison dates (2020-03-31 and 2022-03-31). A regression test confirms this: the same notebook graded against a shorter, smaller, rescaled ZHVI file still scores 100/100.

ItemTypePoints
Notebook executes successfullyexecution10
Data loaded correctlystructural10
Data trimmed to Philadelphiadeterministic15
Data melted into tidy formatdeterministic20
Center City split correctlydeterministic15
Percent increase function workshidden_test20
Center City vs. rest of Philadelphia compareddeterministic10
Total100

Assignment 2: Exploratory Visualization#

rubrics/hw2.yamlassignments/hw2.pyno data

Students pick their own dataset and hand in only the notebook. The grader has no copy of the data, so it cannot re-run anything: every submission dies at read_csv. The content of a chart was never checkable, and this assignment doesn't ask for it. It asks whether the student can produce the required elements. So the rubric splits the assignment in two.

Facts, graded automatically (70 points)#

An Altair chart compiles to a Vega-Lite specification, and that specification is stored in the notebook's own saved output. The grader reads mean/count/bin transformations, interval brushes and transform_filter() cross-filters out of the spec instead of guessing from the source text. That is how it tells apart the three things students most often confuse:

Student wroteVega-Lite saysVerdict
alt.selection_interval() + add_paramsparams: [{select: {type: interval}}]brush ✓
.interactive()the same, plus bind: "scales"pan/zoom, not a brush
alt.selection_point()select: {type: point}a selection, half credit
transform_filter("datum.x > 5")filter with no parama subset, not a transformation
transform_filter(brush) across hconcatfilter: {param: …} + two viewscross-filter ✓ extra credit

Judgement, graded by a person (30 points)#

The assignment grades the matplotlib chart on "colour choices and clarity" and asks for written reasoning under each chart. The quality of a picture and the substance of a paragraph can't be checked by a machine. Those items always come back as manual_review, whatever they score. Each carries a provisional score built from real signals (labelling and colour calls extracted from the source, word counts, the position of each markdown cell) plus the text itself. A TA confirms or adjusts a number instead of starting from a blank box.

The grader records, per chart, where the evidence came from. If an instructor does supply the dataset, a freshly executed chart replaces the saved one automatically and confidence rises from 0.85 to 0.95.

ItemTypePoints
Notebook was run and submitted with its outputsexecution10
Dataset loaded from a relative pathstructural10
Matplotlib chart present and rendereddeterministic10
Reasoning for using matplotlibqualitative5
Matplotlib chart aesthetics (colour and clarity)qualitative5
Seaborn chart present and rendereddeterministic10
Motivation for the seaborn plot typequalitative5
Three Altair chartsdeterministic15
Altair transformation useddeterministic8
Brush selection useddeterministic7
Written conclusion below each chartqualitative15
Extra credit: cross-filtered two-chart Altair dashboarddeterministic0
Total100

The extra-credit dashboard is worth 0 points in the rubric so that it never inflates the maximum. When it is found, the item is flagged with the suggested bonus. A manual override on it adds to the student's total without changing what they are graded out of.


Assignment 3: Evictions and the NDVI#

rubrics/hw3.yamlassignments/hw3.pydata optional

Assignment 3 hands out assignment3-data.zip, and everyone works from it. That one difference changes the whole design: for the first time the right answers exist, so the grader checks results rather than inferring intent.

Fact about the shipped dataValue
Philadelphia census tracts in PA-tracts.geojson384
Rows of li_violations.csv in the fifteen listed types (of 434,052)34,108
Rows in the tidy eviction frame5,376
Evictions in that frame149,472
Median NDVI inside the city limits0.2025
Median NDVI in the suburbs0.3748
Street trees sampled / their median NDVI2,480 / 0.178

Every value was recomputed under the variations students legitimately produce (crop=True and crop=False, masked and filled arrays, sjoin(how="inner") and how="left") before it was written into the rubric. Where a step has two defensible answers, both are listed rather than one being called wrong. For example, the optional unstack(fill_value=0).stack() fill gives 5,535 groups instead of 4,000.

Because the answers are fixed, a matched expected value scores confidence 1.0. Grading combines two readings, the saved outputs and the re-run, so the data is optional:

The correct demo submission scores
With all five data files supplied99.5 / 100
With no data supplied at all99.5 / 100

Supply the data and 19 of the 22 items come back confirmed by both readings. Leave it out and the grade comes from the submitted notebook alone, at one confidence step lower (0.9 rather than 1.0).

Plots are graded on the objects#

Two thirds of Part 1 uses hvplot, and an hvplot call returns a holoviews object that knows what it is:

The instructions ask forWhat the object saysVerdict
groupby="year", dynamic=FalseHoloMap, kdims [year], 14 frames of Polygonswidget choropleth ✓
The same, dynamic=False omittedDynamicMap, 0 framesnothing renders. Named, 60% credit
One map per violation typeHoloMap, kdims [violationdescription], 15 frames✓
Two maps side by sideLayout, shape (1, 2), children [Polygons, Polygons]✓
The same, panels never trimmedLayout whose children are HoloMapstill carries its widget. 75%
The yearly eviction trendCurve, 14 points summing to 149,472graded on the numbers plotted

Named mistakes, not just wrong answers#

What the student didHow it is recognisedResult
points_from_xy(lat, lng)The data spans 39.8–40.1 east, not −75.2Coordinate swap, 30%
Joined before trimmingThe join has 434,052 rows, not 34,108Right step, wrong order: 70%
Joined across two CRSsThe join lost rows, and none should be lostNamed, 50%
NDVI from bands 3 and 4Medians come back at −0.02, not 0.20Off-by-one band index, 50%
np.median on a masked arrayThe medians are NaNThe NaN trap, 40%
Suburbs = the envelopeIts area is city + suburbsdifference() never applied, 50%
Tracts left in EPSG:4326The polygon's area is 0.037, not 369 km²Degrees vs metres, 40%

Where the 100 points go#

ItemPoints
Notebook executes10
1.1.1Eviction data loaded with geopandas3
1.1.2Trimmed to Philadelphia (384 tracts)5
1.1.3Melted to tidy format (5,376 rows)6
1.1.4Yearly eviction trend with hvplot5
1.1.5Year-by-year choropleth with a widget5
1.2.1Violations loaded as a point GeoDataFrame4
1.2.2Trimmed to the fifteen types (34,108 rows)4
1.2.3Hex bin map with tracts overlaid4
1.2.4Spatial join to census tracts5
1.2.5Counted per type and tract5
1.2.6Merged back onto tract geometries3
1.2.7Choropleth per violation type with a widget5
1.3Evictions and violations side by side4
2.1.1Landsat scene opened with rasterio3
2.1.2City and suburb polygons5
2.1.3Masked, and the NDVI computed6
2.1.4Median NDVI compared4
2.2.1Street tree data loaded3
2.2.2NDVI sampled at the tree locations4
2.2.3Histogram and mapped tree points4
2.2.3Figure styling (judged by a person)3
1.4Extra credit (0 points, flagged, suggested 5)0
Total100

Execution is 10 points, Part 1 is 58 across thirteen steps, and Part 2 is 32 across eight. Within each part, the weight follows how much the step matters: the melt, the spatial join and the NDVI masking are where the assignment's teaching happens, while the loading steps are hard to get wrong and cheap to fix.


Assignments 4–6#

Their student templates are in assignment_template/, but they are not graded yet. Each needs a rubric YAML and a grader class. See Adding an assignment.