summaryrefslogtreecommitdiff
path: root/Content/posts/2024-07-01-cu-boulder-class-grades.md
blob: dabcc1cda90089391707ec94a876e117f5612440 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
---
date: 2024-07-01 20:36 
description: Creating a WebUI to search for the perfect gen ed class 
tags: HTML, JavaScript, SQL, Data-Science
---

# CU Boulder Grades Search

In every student's journey, there comes a time when they need to take classes that complete their general education requirements. Or, sometimes you need to decide between two different professors teaching the same course, and Rate My Professor isn't helping you at all. The [Data & Analytics](https://www.colorado.edu/oda/student-data/courses) group at CU Boulder provides grade distributions of all main campus courses. You can already see where I am going with this.

![Screenshot of the Web UI](/assets/buffs-eda/main-ss.png)

## Exploring and Exporting the Data

The data is provided as a spreadsheet and contains data of main campus courses with 10+ grades (including passes), from Fall 2006 to Spring 2024. This spreadsheet is updated after both fall and spring terms.

![Screenshot of the spreadsheet opened in Microsoft Excel](/assets/buffs-eda/excel-ss.png)

Excel is amazing. But, sometimes you just want a simple search tool where you don't have to manually use any formulae. Enter SQLite, the best thing since sliced bread. I exported the spreadsheet as a CSV and imported it into SQLite with a custom table schema.

```sql
CREATE TABLE IF NOT EXISTS "raw_data"
  (
     "yearterm"             INTEGER,
     "crspbadept"           TEXT,
     "crspbacoll"           TEXT,
     "crspbadiv"            TEXT,
     "subject"              TEXT,
     "course"               INTEGER,
     "coursetitle"          TEXT,
     "level"                TEXT,
     "crslvlnum"            TEXT,
     "activity_type"        TEXT,
     "instruction_mode"     TEXT,
     "hours"                REAL,
     "n_eot"                INTEGER,
     "n_enroll"             INTEGER,
     "n_grade"              REAL,
     "pct_grade"            TEXT,
     "avg_grd"              REAL,
     "pct_a"                TEXT,
     "pct_b"                TEXT,
     "pct_c"                TEXT,
     "pct_d"                TEXT,
     "pct_f"                TEXT,
     "pct_c_minus_or_below" TEXT,
     "pct_df"               TEXT,
     "pct_dfw"              TEXT,
     "pct_wdraw"            TEXT,
     "pct_incomp"           TEXT,
     "n_pass"               INTEGER,
     "n_nocred"             INTEGER,
     "n_incomp"             INTEGER,
     "rap"                  INTEGER,
     "honors"               INTEGER,
     "insname1"             TEXT,
     "insgrp1"              TEXT,
     "insttl1"              TEXT,
     "insname2"             TEXT,
     "insgrp2"              TEXT,
     "insttl2"              TEXT,
     "insname3"             TEXT,
     "insgrp3"              TEXT,
     "insttl3"              TEXT,
     "section"              TEXT,
     "ncomb"                INTEGER,
     "subject_label"        TEXT
  );
```


```bash
sqlite> .mode csv
sqlite> .import data.csv raw_data
```

Since I wanted the database to be small in size to be able to load it on shitty cellular connections, I said goodbye to pre-2006 data.

```bash
sqlite> DELETE from raw_data where YearTerm < 20161
sqlite> VACUUM;
```

### Making Sense of the Data

I focussed on three things:

1. Searching by Instructor - Given a professor is there a way to see the classes they have taught, and the average grades in those classes?
2. Searching by Class Code - If there are multiple professors teaching a class, which professor should I go with?
3. Searching by A&S Division that class fulfils - Can I find classes that fulfil certain requirements and are either upper or lower division?

#### By Instructor

```sql
SELECT subject
       || ' '
       || course                           AS "Class",
       coursetitle                         AS "Title",
       Group_concat(DISTINCT yearterm)     AS YearTerms,
       Round(Avg(avg_grd), 2)              AS "Average Grade",
       Round(Avg(pct_a), 2)                AS "% A",
       Round(Avg(pct_b), 2)                AS "% B",
       Round(Avg(pct_c_minus_or_below), 2) AS "% C- and below",
       Round(Avg(n_grade))                 AS "Average No. of Students"
FROM   raw_data AS main
WHERE  insname1 = "$VARIABLE"
        OR insname2 = "$VARIABLE"
        OR insname3 = "$VARIABLE"
GROUP  BY coursetitle
```

The database takes the instructor name in the format `Last Name, First Name`

#### By Class Code

```sql
SELECT yearterm             AS "YearTerms",
       insname1             AS Instructor,
       avg_grd              AS "Average Grade",
       pct_a                AS "% A",
       pct_b                AS "% B",
       pct_c_minus_or_below AS "% C- and below",
       n_grade              AS "No. Graded"
FROM   raw_data
WHERE  subject
       || ' '
       || course = "$VARIABLE";
```

Where the variable is the class code we are search for.

#### By A&S Division

```sql
SELECT DISTINCT subject
                                || ' '
                                || course AS "Class Code",
                coursetitle               AS "Title",
                Avg(avg_grd)              AS "Average Grade",
                Avg(pct_a)                AS "% A",
                Avg(pct_b)                AS "% B",
                Avg(pct_c_minus_or_below) AS "% C- and below",
                Avg(n_grade)              AS "No. Graded"
FROM            raw_data
WHERE           (
                                crspbadiv = "$VARIABLE"
                AND             course "$VARIABLE" )
AND             subject != 'APPM'
GROUP BY        "Class Code",
                "Title"
```

Here the first variable represents the Arts & Science division:
* `AH` - Arts & Humanities
* `SS` - Social Sciences
* `NS` - Natural Sciences

The second variable is used to craft the class code range. If we only wanted to see upper division courses we would substitute it with `BETWEEN 3000 AND 4000`


## Web-ifying the Data

Now now now, I could have obviously stopped here since I know how to craft SQL queries. But where is the fun in that? 

I created a simple interface using Bulma. To run SQLite in the browser I used [SQL.js](https://sql.js.org)

### Loading the database

```html
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.js" onload="" integrity="sha512-PAyD/84QEfxE1X/H3RDJY9kqXtJObyGq6qA93+LnkMNWdTLHjcTKHsDOcz6Y5xAdsaMGItRP5vNs4vtj3/FRuw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<script>
let SQL = undefined
let db = undefined

async function fetchDatabase(url) {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error('Great Buffaloes! HTTP Error with status: ${response.status}');
    }

    const blob = await response.blob();
    const buffer = await blob.arrayBuffer();
    const uInt8Array = new Uint8Array(buffer);

    db = new SQL.Database(uInt8Array)

}

async function loadDatabase() {
    SQL = await initSqlJs({
        locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        });
    fetchDatabase("./grades.sqlite")
}

loadDatabase();
</script>
...
```

This loads up the database.

### Adding autocomplete 

The `<datalist>` element is perfect for creating a lightweight autocomplete for `<input>` elements. For the pre-defined queries I populate the elements with all possible inputs.

```html
<script>
async function fethDatabase(url) {
    ...
    // One example of populating the datalist with all possible class codes
    let classCodeQuery = "SELECT DISTINCT Subject || ' ' || Course AS ClassCode FROM raw_data"
    const classCodeResults = db.exec(classCodeQuery);

    const classCodeResult = classCodeResults[0];
    const classCodeColumns = classCodeResult.columns;
    const classCodeRows = classCodeResult.values;

    let classCodeDatalist = document.getElementById("classCodes");

    for (let i = 0; i < classCodeRows.length; i++) {
        let option = document.createElement("option");
        option.value = classCodeRows[i][0];
        classCodeDatalist.appendChild(option);
    }
}
</script>
...
```

![Screenshot of datalist using Arc Browser on macOS](/assets/buffs-eda/arc-datalist.png)

![Screenshot of datalist using Safari on iOS](/assets/buffs-eda/safari-datalist.jpeg)

### Making the table sortable

I found this amazing library called [sorttable.js](https://github.com/stuartlangridge/sorttable) to make the resulting table I was generating to be sortable. All I had to do was add the `sorttable` class to the table element and then call the library

```javascript
...
const results = db.exec(query);
createTable(results, "table-container", query);
sorttable.makeSorable(document.getElementById("table-container").children[0]);
...
```

![Screenshot of results table sorted by average grades](/assets/buffs-eda/sorted-ss.png)

The full code is available on [GitHub](https://github.com/navanchauhan/BuffClassesEDA).

## A Word of Caution: The Fine Print

If you came here to learn how to do this with another database or 

### The "Easy A" Mirage

Sure, that PHIL 1400 class might have an average grade that makes it look easy. But remember, just because something's labelled "easy" doesn't mean it is right for you. It's like picking classes based solely on their proximity to the campus coffee shop - tempting but not always wise. 

### Challenge by Choice

College is about growth, learning, and the occasional existential dread where you ask why you are doing something even though we are all going to die one day and nothing matters. Sometimes, the class with the lower average grade might be the one that actually helps you learn and grow.

### Past Performance != Future Results

Just because a class was super easy during Covid with a particular professor does not mean it will be easy now that the professor is teaching that class again. Remember, there's more to a class than its GPA. Like, you know, actually learning stuff. Crazy concept, I know.


### In Conclusion: The TL;DR

This is just a fun tool to inform your decisions, not to make them for you. Also, don't underestimate how stupid some people can be.