Skip to content

Add grade distributions from the records request - #137

Merged
KadyrovAkram merged 1 commit into
mainfrom
feature/grade-distributions
Sep 15, 2026
Merged

KadyrovAkram merged 1 commit into
mainfrom
feature/grade-distributions

Conversation

@KadyrovAkram

Copy link
Copy Markdown
Collaborator

Five academic years of per-section grade distributions, obtained under R.C. 149.43 because OSU publishes nothing like them, folded into one curve per instructor per course and drawn in the detail pane under the ratings.

data/grades.json is not in this branch. The records request is still pending. loadGrades reads a 404 as "not published yet" — settled, never retried, distinct from the 500 that stays retryable — so the block does not render and nothing else on the page changes. Everything here is written and tested against fixtures, so the day the spreadsheet arrives the work is a column-name check, not a build.

The join does not guess

Every other join in Finder does. js/ratings.js matches RateMyProfessors on a first and last name and returns nothing when two people share one, because a wrong rating is worse than no rating — which is why both Alan Reeds get nothing.

content.osu.edu/v2 already publishes an instructor's address beside every section, and the records request asked for the same field. Verified against the live API while building:

class 5168  {"displayName": "Paolo Bucci", "role": "PI", "email": "bucci.2@osu.edu"}

So a curve keys on bucci.2 and lands on one person or on nobody. Name matching survives as a fallback for rows that come back without an address, inheriting the same refusal to choose between two people with one name.

What the pane shows

Leads with the mean and the share who got an A or A-, because the two disagree exactly where it matters: a section that is half A and half E averages the same as a section that is entirely B, and only one of them is a coin flip.

Withdrawals sit beside the curve rather than inside it. Nobody who withdrew has a grade, so they cannot move a mean, which means a course a third of the class drops posts a flattering one. The number that explains it belongs next to it.

Every way a curve can be absent reads as absent rather than as a number. A course whose sections were all too small to publish, a pass-fail course, and an instructor with no record are three different sentences; gradesStatus keeps them apart instead of printing one blank for all three.

Two bugs the tests caught

The header normaliser collapsed A- into A. Field names have their punctuation stripped so catalog_nbr and Catalog Nbr are one spelling. Applied to grade columns that turns A- into a, which is also what A turns into, so both resolved to whichever came first and every A-minus in five years would have been read out of the A column — silently, with no error and a completely normal-looking output. Grade headers now keep their sign, which is why markSlug exists next to slug.

A negative count was reported as a pass-fail course. It has the right eleven columns, so a length-only check waved it through into a branch meaning "students finished on marks carrying no points". The validity rule now lives in one place that both the status and the mean read.

The import

scripts/fetch-grades.mjs fetches nothing; the records office sends a file once.

node scripts/fetch-grades.mjs records.csv

The column names are not known in advance. Every field resolves through alias tables, and an unresolved one aborts printing the headers the file actually had, because the failure being avoided is silent — a file whose B- column went unrecognised would write a curve missing every B-minus and look fine doing it. Same write gate as the other three snapshotters via scripts/guards.mjs: an instructor floor, a drop against the committed count, and an unreadable-row rate. Staff, TBA and Unassigned are dropped, since folding five years of unstaffed sections into one key would invent the most prolific instructor at the university.

Verified

730 tests pass, 0 fail, up from 654 on main. Rendered against the live OSU API in both themes with no console errors: 2.77 average GPA, 35% got A or A-, 860 graded, beside Bucci's 3.0 rating.

Deliberately not included

No minimum-GPA filter and no grade sort. Both need real data to pick thresholds against; a cutoff tuned on fixtures is a guess.

One judgement call worth a second opinion: the rating spread runs red→green (1 to 5) and the grade curve runs green→red (A to E), stacked adjacent in the pane. Each is in its own scale's natural order and each carries a direction caption, so I left it alone.

Docs in docs/osu-grades.md; README carries the new source, the layout line and a re-counted network audit (grades.js is a nineteenth module, so the page load is 30 requests).

🤖 Generated with Claude Code

Five academic years of per-section grade distributions, obtained under
R.C. 149.43 because OSU publishes nothing like them, folded into one curve
per instructor per course and drawn in the detail pane under the ratings.

This is the one join in Finder that does not guess. RateMyProfessors has to
be matched on a name, which is why two professors called Alan Reed get
nothing; the records request asked for the instructor's OSU address, which
content.osu.edu already publishes beside every section, so a curve keys on
bucci.2 and lands on one person or on nobody.

The pane leads with the mean and the share who got an A or A-, because the
two disagree where it matters: half A and half E averages the same as
entirely B. Withdrawals sit beside the curve rather than inside it, since
nobody who withdrew has a grade to move a mean with.

Every way a curve can be absent reads as absent rather than as a number. A
course whose sections were all too small to publish, a pass-fail course and
an instructor with no record are three different sentences, and gradesStatus
keeps them apart.

data/grades.json is not in the tree until the request is fulfilled. A 404
reads as unpublished and is never retried; a 500 stays retryable.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@KadyrovAkram
KadyrovAkram merged commit 3f01d49 into main Sep 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant