1+ const fs = require ( 'fs' ) ;
2+ const https = require ( 'https' ) ;
3+ const csv = require ( 'csv-parser' ) ;
4+
5+ const GPA_URL = 'https://raw.githubusercontent.com/wadefagen/datasets/master/gpa/uiuc-gpa-dataset.csv' ;
6+
7+ function download ( ) {
8+ return new Promise ( ( resolve , reject ) => {
9+ const file = fs . createWriteStream ( 'data/gpa-raw.csv' ) ;
10+ https . get ( GPA_URL , res => {
11+ res . pipe ( file ) ;
12+ file . on ( 'finish' , ( ) => {
13+ file . close ( ) ;
14+ resolve ( ) ;
15+ } ) ;
16+ } ) . on ( 'error' , reject ) ;
17+ } ) ;
18+ }
19+
20+ function calculateGPA ( row ) {
21+ // as defined by UIUC
22+ // for some reason theres an average gpa in each term but not overall so i have to recalc?? what the shibal??
23+ const gradePoints = {
24+ 'A+' : 4.0 , 'A' : 4.0 , 'A-' : 3.67 ,
25+ 'B+' : 3.33 , 'B' : 3.0 , 'B-' : 2.67 ,
26+ 'C+' : 2.33 , 'C' : 2.0 , 'C-' : 1.67 ,
27+ 'D+' : 1.33 , 'D' : 1.0 , 'D-' : 0.67 , 'F' : 0.0
28+ } ;
29+
30+ let points = 0 , students = 0 ;
31+
32+ for ( const [ grade , value ] of Object . entries ( gradePoints ) ) {
33+ const count = parseInt ( row [ grade ] ) || 0 ;
34+ points += count * value ;
35+ students += count ;
36+ }
37+
38+ return students > 0 ? ( points / students ) . toFixed ( 2 ) : null ;
39+ }
40+
41+ function calcRate ( row ) {
42+ const aPlus = parseInt ( row [ 'A+' ] ) || 0 ;
43+ const a = parseInt ( row [ 'A' ] ) || 0 ;
44+ const total = Object . keys ( row ) . filter ( k =>
45+ [ 'A+' , 'A' , 'A-' , 'B+' , 'B' , 'B-' , 'C+' , 'C' , 'C-' , 'D+' , 'D' , 'D-' , 'F' ] . includes ( k )
46+ ) . reduce ( ( sum , grade ) => sum + ( parseInt ( row [ grade ] ) || 0 ) , 0 ) ;
47+
48+ return total > 0 ? ( ( aPlus + a ) / total * 100 ) . toFixed ( 1 ) : null ;
49+ }
50+
51+ async function process ( ) {
52+ const courses = { } ;
53+ const currentYear = new Date ( ) . getFullYear ( ) ;
54+ const threeYearsAgo = currentYear - 3 ;
55+
56+ await new Promise ( resolve => {
57+ fs . createReadStream ( 'data/gpa-raw.csv' )
58+ . pipe ( csv ( ) )
59+ . on ( 'data' , row => {
60+ const id = `${ row . Subject } ${ row . Number } ` ;
61+
62+ if ( ! courses [ id ] ) {
63+ courses [ id ] = {
64+ subject : row . Subject ,
65+ number : row . Number ,
66+ title : row [ 'Course Title' ] ,
67+ semesters : [ ]
68+ } ;
69+ }
70+
71+ courses [ id ] . semesters . push ( {
72+ year : parseInt ( row . Year ) ,
73+ term : row . Term ,
74+ gpa : calculateGPA ( row ) ,
75+ rate : calcRate ( row ) ,
76+ instructor : row [ 'Primary Instructor' ] ,
77+ students : row . Students
78+ } ) ;
79+ } )
80+ . on ( 'end' , resolve ) ;
81+ } ) ;
82+
83+ // calc averages (last 3 years only) and 4.0 rate
84+ for ( const course of Object . values ( courses ) ) {
85+ const recentSemesters = course . semesters . filter ( s => s . year >= threeYearsAgo ) ;
86+
87+ const gpas = recentSemesters . map ( s => parseFloat ( s . gpa ) ) . filter ( g => ! isNaN ( g ) ) ;
88+ course . avgGPA = gpas . length ? ( gpas . reduce ( ( a , b ) => a + b ) / gpas . length ) . toFixed ( 2 ) : null ;
89+
90+ const rates = recentSemesters . map ( s => parseFloat ( s . rate ) ) . filter ( r => ! isNaN ( r ) ) ;
91+ course . avgRate = rates . length ? ( rates . reduce ( ( a , b ) => a + b ) / rates . length ) . toFixed ( 1 ) : null ;
92+ }
93+
94+ // ok i think this is how quartz works?
95+ if ( ! fs . existsSync ( 'quartz/static' ) ) fs . mkdirSync ( 'quartz/static' , { recursive : true } ) ;
96+ fs . writeFileSync ( 'quartz/static/gpa-processed.json' , JSON . stringify ( courses , null , 2 ) ) ;
97+ }
98+
99+ ( async ( ) => {
100+ if ( ! fs . existsSync ( 'data' ) ) fs . mkdirSync ( 'data' ) ;
101+ await download ( ) ;
102+ await process ( ) ;
103+ console . log ( 'done' ) ;
104+ } ) ( ) ;
0 commit comments