Repository: cornell-dti/cureviews
Pull request #514: course eval data processing
Author: login: mjaydenkim
Created: 2025-03-02T01:43:48Z | Merged: 2025-03-25T20:55:42Z

# Summary

This PR works on converting raw Course Eval JSON data to a clean interface and putting it in the database to be pulled by the frontend.

## PR Type

- [x] 🍕 Feature
- [ ] 🐛 Bug Fix
- [x] 📝 Documentation Update
- [ ] 🎨 Style
- [ ] 🧑‍💻 Code Refactor
- [ ] 🔥 Performance Improvements
- [x] ✅ Test
- [ ] 🤖 Build
- [ ] 🔁 CI
- [ ] 📦 Chore (Release)
- [ ] ⏩ Revert

## Mobile + Desktop Screenshots & Recordings
N/A

## QA - Test Plan
Wrote tests in the server folder.

## Breaking Changes & Notes
N/A

## Added to documentation?

- [ ] 📜 README.md
- [ ] 📓 notion
- [ ] 🍕 ...
- [x] 📕 Course evaluation interfaces
- [ ] 🙅 no documentation needed

## What GIF represents this PR?

[gif](https://www.icegif.com/wp-content/uploads/tom-and-jerry-icegif-4.gif)



Diff:
diff --git a/.gitignore b/.gitignore
index d3d3eb39..f2c148e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ mongodb-binaries/
 mongo/
 *error.log
 debug.log
+server/scripts/course_eval_data/*
\ No newline at end of file
diff --git a/client/src/modules/Admin/Components/DevTools.tsx b/client/src/modules/Admin/Components/DevTools.tsx
index 4a947049..cfc00c2c 100644
--- a/client/src/modules/Admin/Components/DevTools.tsx
+++ b/client/src/modules/Admin/Components/DevTools.tsx
@@ -24,6 +24,7 @@ export default function AdminTools({ token }: AdminToolsProps) {
     description: 'Course description data successfully added',
     similarity: 'Similarity data successfully added',
     summarize: 'All courses successfully summarized',
+    courseEval: 'Course evaluations successfully updated',
     failure: 'API failed'
   };
 
@@ -35,9 +36,8 @@ export default function AdminTools({ token }: AdminToolsProps) {
   ) => {
     setUpdating(true);
     try {
-      const response = await axios.post(endpoint, { token });
-      if (response.status === 200) setUpdated(successState);
-      else setUpdated('failure');
+      await axios.post(endpoint, { token });
+      setUpdated(successState);
     } catch {
       setUpdated('failure');
     } finally {
@@ -83,6 +83,13 @@ export default function AdminTools({ token }: AdminToolsProps) {
         >
           Add New Semester
         </button>
+        <button
+          onClick={() => handleApiCall('/api/admin/courses/add-course-evals', 'courseEval')}
+          disabled={updating}
+          className={styles.adminButtons}
+        >
+          Add Course Evaluations
+        </button>
         <button
           onClick={() =>
             handleApiCall('/api/admin/professors/add', 'professors')
diff --git a/client/src/modules/Course/Components/Course.tsx b/client/src/modules/Course/Components/Course.tsx
index b005ecfb..643658de 100644
--- a/client/src/modules/Course/Components/Course.tsx
+++ b/client/src/modules/Course/Components/Course.tsx
@@ -21,7 +21,7 @@ import SimilarCoursesSection from './SimilarCourses';
 
 import type { NewReview } from '../../../types';
 
-import { Class, Recommendation, Review } from 'common';
+import { Class, Recommendation, Review, CourseEvaluation } from 'common';
 import { Session } from '../../../session-store';
 
 import { useAuthOptionalLogin } from '../../../auth/auth_utils';
@@ -39,6 +39,7 @@ export const Course = () => {
 
   const [selectedClass, setSelectedClass] = useState<Class>();
   const [courseReviews, setCourseReviews] = useState<Review[]>();
+  const [courseEval, setCourseEval] = useState<CourseEvaluation>();
   const [similarCourses, setSimilarCourses] = useState<Recommendation[]>();
   const [pageStatus, setPageStatus] = useState<PageStatus>(PageStatus.Loading);
   const [scrolled, setScrolled] = useState(false);
@@ -164,6 +165,12 @@ export const Course = () => {
           const recommendations = course.recommendations;
           setSimilarCourses(recommendations);
 
+          const courseEvalObject = await axios.post(
+            "/api/courses/get-course-eval",
+            { classSub: course.classSub.toUpperCase(), classNum: course.classNum }
+          )
+          setCourseEval(courseEvalObject.data.result === 0 ? null : courseEvalObject.data.result);
+
           setPageStatus(PageStatus.Success);
         } else {
           setPageStatus(PageStatus.Error);
diff --git a/common/index.d.ts b/common/index.d.ts
index 189bfb2b..41219f36 100644
--- a/common/index.d.ts
+++ b/common/index.d.ts
@@ -72,3 +72,41 @@ interface Recommendation {
   tags: string[];
   similarityScore: number;
 }
+/** Processed course evaluation data for a single course. */
+interface CourseEvaluation {
+  _id: string;
+  courseName: string;
+  subject: string; // derived from courseName -- should be type Subject eventually
+  courseNumber: string; // derived from courseName
+  semester: string;
+  totalEvals: number; // derived by summing over grade levels
+  courseOverall: number;
+  profTeachingSkill: number;
+  profKnowledge: number;
+  profClimate: number;
+  profOverall: number;
+  numA: number;
+  numB: number;
+  numC: number;
+  numD: number;
+  numF: number;
+  numS: number;
+  numU: number;
+  numGradeNA: number;
+  numFresh: number;
+  numSoph: number;
+  numJr: number;
+  numSr: number;
+  numAg: number;
+  numHumec: number;
+  numArch: number;
+  numILR: number;
+  numArts: number;
+  numEng: number;
+  numHotel: number;
+  numOther: number;
+  numMajorReq: number;
+  numReputation: number;
+  numInterest: number;
+  sentiments: [number, number][];
+}
diff --git a/server/db/schema.ts b/server/db/schema.ts
index 440bcc07..dd872bb5 100644
--- a/server/db/schema.ts
+++ b/server/db/schema.ts
@@ -1,5 +1,5 @@
 import mongoose, { Schema } from 'mongoose';
-import { Class, Student, Subject, Review, Professor } from 'common';
+import { Class, Student, Subject, Review, Professor, CourseEvaluation } from 'common';
 
 /*
 
@@ -209,3 +209,88 @@ export const GlobalMetadata = mongoose.model<GlobalMetadataDocument>(
   'globalnMetadata',
   GlobalMetadataSchema
 );
+
+/**
+ * Course Evaluation Collection
+ * Stores course evaluation data
+ */
+const CourseEvaluationSchema = new Schema({
+  _id: { type: String, required: true },
+  courseName: { type: String, required: true },
+  subject: { type: String, required: true },
+  courseNumber: { type: String, required: true },
+  semester: { type: String, required: true },
+  totalEvals: { type: Number, default: 0 },
+  courseOverall: { type: Number },
+  profTeachingSkill: { type: Number },
+  profKnowledge: { type: Number },
+  profClimate: { type: Number },
+  profOverall: { type: Number },
+  numA: { type: Number, default: 0 },
+  numB: { type: Number, default: 0 },
+  numC: { type: Number, default: 0 },
+  numD: { type: Number, default: 0 },
+  numF: { type: Number, default: 0 },
+  numS: { type: Number, default: 0 },
+  numU: { type: Number, default: 0 },
+  numGradeNA: { type: Number, default: 0 },
+  numFresh: { type: Number, default: 0 },
+  numSoph: { type: Number, default: 0 },
+  numJr: { type: Number, default: 0 },
+  numSr: { type: Number, default: 0 },
+  numAg: { type: Number, default: 0 },
+  numHumec: { type: Number, default: 0 },
+  numArch: { type: Number, default: 0 },
+  numILR: { type: Number, default: 0 },
+  numArts: { type: Number, default: 0 },
+  numEng: { type: Number, default: 0 },
+  numHotel: { type: Number, default: 0 },
+  numOther: { type: Number, default: 0 },
+  numMajorReq: { type: Number, default: 0 },
+  numReputation: { type: Number, default: 0 },
+  numInterest: { type: Number, default: 0 },
+  sentiments: { type: [[Number, Number]], default: [] }
+});
+
+interface CourseEvaluationDocument extends mongoose.Document, CourseEvaluation {
+  _id: string,
+  courseName: string;
+  subject: string;
+  courseNumber: string;
+  semester: string;
+  totalEvals: number;
+  courseOverall: number;
+  profTeachingSkill: number;
+  profKnowledge: number;
+  profClimate: number;
+  profOverall: number;
+  numA: number;
+  numB: number;
+  numC: number;
+  numD: number;
+  numF: number;
+  numS: number;
+  numU: number;
+  numGradeNA: number;
+  numFresh: number;
+  numSoph: number;
+  numJr: number;
+  numSr: number;
+  numAg: number;
+  numHumec: number;
+  numArch: number;
+  numILR: number;
+  numArts: number;
+  numEng: number;
+  numHotel: number;
+  numOther: number;
+  numMajorReq: number;
+  numReputation: number;
+  numInterest: number;
+  sentiments: [number, number][];
+}
+
+export const CourseEvaluations = mongoose.model<CourseEvaluationDocument>(
+  "courseEvaluations",
+  CourseEvaluationSchema,
+);
\ No newline at end of file
diff --git a/server/scripts/populate-course-evals.ts b/server/scripts/populate-course-evals.ts
new file mode 100644
index 00000000..6e2ed060
--- /dev/null
+++ b/server/scripts/populate-course-evals.ts
@@ -0,0 +1,305 @@
+import { CourseEvaluations } from '../db/schema';
+// import courseData from './course_eval_data/fa_sp_24_course_eval.json';
+import shortid from 'shortid';
+import { CourseEvaluation } from 'common';
+
+/** Given raw course evaluation data, return an object with its course code as the key
+ * and merged course data from the course evaluations JSON file as an object value. */
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  const evals: CourseEvaluations = Object.entries(data)
+    .reduce<CourseEvaluations>((acc, [key, value]) => {
+      if (value.courseName.split(' ')[2] !== 'LEC') return acc;
+
+      const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+      const numFresh = parseInt(value.numFresh, 10) || 0;
+      const numSoph = parseInt(value.numSoph, 10) || 0;
+      const numJr = parseInt(value.numJr, 10) || 0;
+      const numSr = parseInt(value.numSr, 10) || 0;
+      const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+      const courseNameSplit = value.courseName.split(' ')
+
+      acc[key] = {
+        ...value,
+        _id: shortid.generate(),
+        subject: courseNameSplit[0],
+        courseNumber: courseNameSplit[1],
+        courseName: courseNameSplit[0] + " " + courseNameSplit[1],
+        courseOverall: parseFloat(value.courseOverall) || 0,
+        profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+        profKnowledge: parseFloat(value.profKnowledge) || 0,
+        profClimate: parseFloat(value.profClimate) || 0,
+        profOverall: parseFloat(value.profOverall) || 0,
+        numA: parseInt(value.numA, 10) || 0,
+        numB: parseInt(value.numB, 10) || 0,
+        numC: parseInt(value.numC, 10) || 0,
+        numD: parseInt(value.numD, 10) || 0,
+        numF: parseInt(value.numF, 10) || 0,
+        numS: parseInt(value.numS, 10) || 0,
+        numU: parseInt(value.numU, 10) || 0,
+        numGradeNA: numGradeNA,
+        numFresh: numFresh,
+        numSoph: numSoph,
+        numJr: numJr,
+        numSr: numSr,
+        totalEvals: totalEvals,
+        numAg: parseInt(value.numAg, 10) || 0,
+        numHumec: parseInt(value.numHumec, 10) || 0,
+        numArch: parseInt(value.numArch, 10) || 0,
+        numILR: parseInt(value.numILR, 10) || 0,
+        numArts: parseInt(value.numArts, 10) || 0,
+        numEng: parseInt(value.numEng, 10) || 0,
+        numHotel: parseInt(value.numHotel, 10) || 0,
+        numOther: parseInt(value.numOther, 10) || 0,
+        numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+        numReputation: parseInt(value.numReputation, 10) || 0,
+        numInterest: parseInt(value.numInterest, 10) || 0,
+        sentiments: [...Array(18).keys()].map(x =>
+          [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+        ),
+      }
+      return acc
+    }, {})
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
+    Object.entries(mergedCourseEvals)
+      .map(([key, value]): [string, CourseEvaluation] => {
+        return [key, sortTopStatements(value)]
+      })
+  );
+
+  return sortedSentimentEvals;
+};
+
+/** Given two numbers and two corresponding weights, return the weighted average. */
+const weightedAvg = (
+  n1: number, n2: number, w1: number, w2: number
+): number => {
+  if (n1 == 0 || w1 == 0) return n2
+  if (n2 == 0 || w2 == 0) return n1
+  if (w1 + w2 <= 0) throw new Error('Invalid argument: negative weight')
+  return Math.floor((((n1 * w1) + (n2 * w2)) / (w1 + w2)) * 100) / 100
+};
+
+/** Given a course evaluation, sort the sentiments array to be in order of
+ * rating and return the modified eval. */
+const sortTopStatements = (
+  courseEval: CourseEvaluation
+): CourseEvaluation => {
+  return {...courseEval,
+    sentiments: courseEval.sentiments
+      .map((x): [number, number] => [x[0], Math.floor(x[1] * 100) / 100])
+      .sort((a, b) => b[1] - a[1])
+  };
+};
+
+/** Given two course evaluations that should represent the same lecture section
+ * but different professors (e.g. the same students are represented
+ * by both evals), average all relevant data. */
+const mergeCourseLecEvaluations = (
+  eval1: CourseEvaluation,
+  eval2: CourseEvaluation
+): CourseEvaluation => {
+  const w1 = eval1.totalEvals || 0;
+  const w2 = eval2.totalEvals || 0;
+
+  return {
+    // note for potential edge case: in some cases,
+    // discrepancies between the two evals (likely errors)
+    // are overwritten here.
+    ...eval1,
+    courseOverall: weightedAvg(eval1.courseOverall, eval2.courseOverall, w1, w2),
+    profTeachingSkill: weightedAvg(eval1.profTeachingSkill, eval2.profTeachingSkill, w1, w2),
+    profKnowledge: weightedAvg(eval1.profKnowledge, eval2.profKnowledge, w1, w2),
+    profClimate: weightedAvg(eval1.profClimate, eval2.profClimate, w1, w2),
+    profOverall: weightedAvg(eval1.profOverall, eval2.profOverall, w1, w2),
+    sentiments: eval1.sentiments.map((item, index) => {
+      const statementNum = item[0];
+      return [
+        statementNum,
+        weightedAvg(eval1.sentiments[index][1], eval2.sentiments[index][1], w1, w2)
+      ];
+    }),
+  };
+};
+
+/** Given two course evaluations that should represent the same course
+ * but different sections (e.g. different students are represented
+ * in each eval), sum all relevant data. */
+const mergeCourseEvaluations = (
+  eval1: CourseEvaluation,
+  eval2: CourseEvaluation
+): CourseEvaluation => {
+  const totalEvals1 = eval1.totalEvals || 0;
+  const totalEvals2 = eval2.totalEvals || 0;
+  const combinedTotalEvals = totalEvals1 + totalEvals2;
+  return {
+    ...mergeCourseLecEvaluations(eval1, eval2),
+    numA: eval1.numA + eval2.numA,
+    numB: eval1.numB + eval2.numB,
+    numC: eval1.numC + eval2.numC,
+    numD: eval1.numD + eval2.numD,
+    numF: eval1.numF + eval2.numF,
+    numS: eval1.numS + eval2.numS,
+    numU: eval1.numU + eval2.numU,
+    numGradeNA: eval1.numGradeNA + eval2.numGradeNA,
+    numFresh: eval1.numFresh + eval2.numFresh,
+    numSoph: eval1.numSoph + eval2.numSoph,
+    numJr: eval1.numJr + eval2.numJr,
+    numSr: eval1.numSr + eval2.numSr,
+    totalEvals: combinedTotalEvals,
+    numAg: eval1.numAg + eval2.numAg,
+    numHumec: eval1.numHumec + eval2.numHumec,
+    numArch: eval1.numArch + eval2.numArch,
+    numILR: eval1.numILR + eval2.numILR,
+    numArts: eval1.numArts + eval2.numArts,
+    numEng: eval1.numEng + eval2.numEng,
+    numHotel: eval1.numHotel + eval2.numHotel,
+    numOther: eval1.numOther + eval2.numOther,
+    numMajorReq: eval1.numMajorReq + eval2.numMajorReq,
+    numReputation: eval1.numReputation + eval2.numReputation,
+    numInterest: eval1.numInterest + eval2.numInterest,
+    sentiments: eval1.sentiments.map((item, index) => {
+      const statementNum = item[0];
+      return [
+        statementNum,
+        weightedAvg(
+          eval1.sentiments[index][1], eval2.sentiments[index][1], totalEvals1, totalEvals2
+        )
+      ];
+    }),
+  };
+};
+
+/**
+ * Adds all course eval data from a particular semester/web-scraping iteration
+ *
+ * @returns true if operation was successful, false otherwise
+ * @param data course eval json data
+ */
+export const addCourseEvalsFromJson = async (
+  data: CourseEvaluationsRaw
+): Promise<boolean> => {
+  const parsedData: CourseEvaluations = parseEval(data)
+
+  const v1 = await Promise.all(
+    Object.entries(parsedData)
+      .map(async ([_, value]) => {
+        const cEval = value
+        const cEvalIfExists = await CourseEvaluations.findOne({
+          subject: cEval.subject,
+          courseNumber: cEval.courseNumber
+        }).exec();
+
+        if (!cEvalIfExists) {
+          console.log(`Adding new course eval for course ${cEval.courseName}`)
+          const res = await new CourseEvaluations({
+            ...cEval
+          }).save().catch((err) => {
+            console.log(err);
+            return null;
+          });
+          // db operation was not successful
+          if (!res) {
+            throw new Error();
+          }
+        }
+        return true;
+      })).catch((_) => null);
+
+  if (!v1) {
+    console.log('Something went wrong while updating subjects!');
+    return false;
+  }
+  return v1;
+};
+
+/** Adds course evaluations to database.
+ *  !!! UNCOMMENT WHEN YOU NEED TO ADD COURSE EVALS !!! */
+// export const addCurrCourseEvals = async () => {
+//   await addCourseEvalsFromJson(courseData)
+//   return true
+// };
+
+/** Raw course evaluation data for a single course (e.g. as imported from web scraping). */
+interface CourseEvaluationRaw {
+  courseName: string;
+  semester: string;
+  courseOverall: string;
+  profTeachingSkill: string;
+  profKnowledge: string;
+  profClimate: string;
+  profOverall: string;
+  numA: string;
+  numB: string;
+  numC: string;
+  numD: string;
+  numF: string;
+  numS: string;
+  numU: string;
+  numGradeNA: string;
+  numFresh: string;
+  numSoph: string;
+  numJr: string;
+  numSr: string;
+  numAg: string;
+  numHumec: string;
+  numArch: string;
+  numILR: string;
+  numArts: string;
+  numEng: string;
+  numHotel: string;
+  numOther: string;
+  numMajorReq: string;
+  numReputation: string;
+  numInterest: string;
+  statement8Score: string;
+  statement9Score: string;
+  statement10Score: string;
+  statement11Score: string;
+  statement12Score: string;
+  statement13Score: string;
+  statement14Score: string;
+  statement15Score: string;
+  statement16Score: string;
+  statement17Score: string;
+  statement18Score: string;
+  statement19Score: string;
+  statement20Score: string;
+  statement21Score: string;
+  statement22Score: string;
+  statement23Score: string;
+  statement24Score: string;
+  statement25Score: string;
+}
+
+/** Raw course evaluation data for a single JSON file. */
+interface CourseEvaluationsRaw {
+  [key: string]: CourseEvaluationRaw;
+}
+
+/** Processed list of course evaluations. */
+interface CourseEvaluations {
+  [key: string]: CourseEvaluation;
+}
diff --git a/server/scripts/populate-courses.ts b/server/scripts/populate-courses.ts
index 3785f407..fd2e8a09 100644
--- a/server/scripts/populate-courses.ts
+++ b/server/scripts/populate-courses.ts
@@ -171,7 +171,7 @@ export const fetchClassesForSubject = async (
 };
 
 /**
- * Adds all new subjects, courses, and updates professors for a particular semeester
+ * Adds all new subjects, courses, and updates professors for a particular semester
  *
  * @param {string} endpoint: base url for fetching courses in a particular subject from Course API
  * @param {string} semester: course roster semester for (i.e FA23)
diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts
index 6d1ab8d9..95d8b44c 100644
--- a/server/src/admin/admin.controller.ts
+++ b/server/src/admin/admin.controller.ts
@@ -46,6 +46,7 @@ import {
   addAllSimilarityData
 } from '../../scripts';
 import { fetchAddSubjects } from '../../scripts/populate-subjects';
+// import { addCurrCourseEvals } from '../../scripts/populate-course-evals';
 
 /**
  * Verifies that the token passed in an admin.
@@ -406,6 +407,23 @@ export const addNewSemDb = async ({ auth, semester }: AdminAddSemesterType) => {
   return result;
 };
 
+/**
+ * Adds all course evals based on hard-coded files (since this operation won't happen often).
+ *
+ * @returns true if operation was successful, false if operations was not successful, null if token not admin
+ */
+export const addNewCourseEvals = async ({ auth }: VerifyAdminType) => {
+  const userIsAdmin = verifyTokenAdmin({ auth });
+  if (!userIsAdmin) {
+    return null;
+  }
+
+  // UNCOMMENT WHEN USING FUNCTION!!!!!!
+  // return await addCurrCourseEvals();
+
+  return false;
+};
+
 /**
  * Adds all course descriptions to the database after updating the courses for the most recent semester.
  *
diff --git a/server/src/admin/admin.router.ts b/server/src/admin/admin.router.ts
index d2315472..df681360 100644
--- a/server/src/admin/admin.router.ts
+++ b/server/src/admin/admin.router.ts
@@ -26,7 +26,7 @@ import {
   approveReviews,
   addCourseDescriptionsDb,
   addSimilarityDb,
-  drawRaffle
+  drawRaffle, addNewCourseEvals
 } from './admin.controller';
 
 export const adminRouter = express.Router();
@@ -376,6 +376,37 @@ adminRouter.post('/semester/add', async (req, res) => {
   }
 });
 
+/** Reachable at POST /api/admin/courses/add-course-evals
+ * @body token: a session's current token
+ * Adds all course evals to the db based on hard-coded JSON files. For admins only
+ */
+adminRouter.post('/courses/add-course-evals', async (req, res) => {
+  const { token }: AdminRequestType = req.body;
+  try {
+    const auth = new Auth({ token });
+    const result = await addNewCourseEvals({ auth });
+
+    if (result === null) {
+      return res.status(401).json({
+        error: `User is unauthenticated and unauthorized as admin, please sign in.`
+      });
+    }
+
+    if (result) {
+      res.status(200);
+      res.set('Connection', 'close');
+      res.json({ result: true });
+      return res;
+    }
+
+    return res.status(400).json({
+      result: false
+    });
+  } catch (err) {
+    return res.status(500).json({ error: `Internal Server Error: ${err}` });
+  }
+});
+
 /** Reachable at POST /api/admin/professors/add
  * @body token: a session's current token
  * Adds all professors to the db for the given semester. For admins only
diff --git a/server/src/course/course.controller.ts b/server/src/course/course.controller.ts
index ab1e3b32..6df33f49 100644
--- a/server/src/course/course.controller.ts
+++ b/server/src/course/course.controller.ts
@@ -2,12 +2,14 @@ import {
   findCourseById,
   findCourseByInfo,
   findRecommendationByInfo,
-  findGlobalMetadata
+  findGlobalMetadata,
+  findCourseEval
 } from './course.data-access';
 import {
   CourseIdRequestType,
   CourseInfoRequestType,
-  CourseDescriptionRequestType
+  CourseDescriptionRequestType,
+  CourseEvalRequestType
 } from './course.type';
 import { preprocess, tfidf, cosineSimilarity, idf } from './course.recalgo';
 
@@ -158,4 +160,11 @@ export const getSimilarity = async () => {
       .slice(0, 5);
   }
   return topSimilarities;
+}
+
+export const getCourseEval =
+  async ({ classSub, classNum }: CourseEvalRequestType) => {
+    const courseEval = await findCourseEval(classSub, classNum);
+    if (!courseEval) return null;
+    return courseEval;
 };
diff --git a/server/src/course/course.data-access.ts b/server/src/course/course.data-access.ts
index 446f6ace..7db9be53 100644
--- a/server/src/course/course.data-access.ts
+++ b/server/src/course/course.data-access.ts
@@ -1,8 +1,5 @@
-import {
-  Classes,
-  RecommendationMetadata,
-  GlobalMetadata
-} from '../../db/schema';
+import { Classes, RecommendationMetadata, GlobalMetadata } from "../../db/schema";
+import { CourseEvaluations } from '../../db/schema';
 
 export const findCourseById = async (courseId: string) =>
   await Classes.findOne({ _id: courseId }).exec();
@@ -25,5 +22,7 @@ export const findRecommendationByInfo = async (
     classNum: courseNumber
   }).exec();
 
-export const findGlobalMetadata = async () =>
-  await GlobalMetadata.find().exec();
+export const findGlobalMetadata = async () => await GlobalMetadata.find().exec();
+
+export const findCourseEval = async (subject: string, courseNumber: string) =>
+  await CourseEvaluations.findOne({ subject, courseNumber }).exec();
diff --git a/server/src/course/course.router.ts b/server/src/course/course.router.ts
index 20f01ee6..6381aea9 100644
--- a/server/src/course/course.router.ts
+++ b/server/src/course/course.router.ts
@@ -3,7 +3,8 @@ import express from 'express';
 import {
   CourseIdRequestType,
   CourseInfoRequestType,
-  CourseDescriptionRequestType
+  CourseDescriptionRequestType,
+  CourseEvalRequestType
 } from './course.type';
 import {
   getCourseByInfo,
@@ -11,7 +12,8 @@ import {
   getRecommendationData,
   getProcessedDescription,
   getSimilarity,
-  getGlobalMetadata
+  getGlobalMetadata,
+  getCourseEval
 } from './course.controller';
 
 import { getCourseById } from '../utils';
@@ -138,3 +140,29 @@ courseRouter.post('/mock-similarity', async (req, res) => {
   const similarity = await getSimilarity();
   return res.status(200).json({ result: similarity });
 });
+
+/** Reachable at POST /api/courses/get-course-eval
+ * @body courseNumber: a course's number
+ * @body courseSubject: a course's subject code
+ * Gets the course evaluation object tied to the given course
+ */
+courseRouter.post('/get-course-eval', async (req, res) => {
+  try {
+    const { classSub, classNum }: CourseEvalRequestType = req.body;
+
+    const courseEvalDoc = await getCourseEval({
+      classSub,
+      classNum,
+    });
+
+    if (courseEvalDoc === null) {
+      return res.status(200).json({ result: 0 });
+    }
+
+    return res.status(200).json({ result: courseEvalDoc });
+  } catch (error) {
+    return res
+      .status(500)
+      .json({ error: `Internal Server Error: ${error.message}` });
+  }
+});
diff --git a/server/src/course/course.type.ts b/server/src/course/course.type.ts
index 3784d171..07ba66a1 100644
--- a/server/src/course/course.type.ts
+++ b/server/src/course/course.type.ts
@@ -10,3 +10,8 @@ export interface CourseIdRequestType {
 export interface CourseDescriptionRequestType {
   description: string;
 }
+
+export interface CourseEvalRequestType {
+  classSub: string;
+  classNum: string;
+}
diff --git a/server/tests/Course.test.ts b/server/tests/Course.test.ts
index 80777931..2f039118 100644
--- a/server/tests/Course.test.ts
+++ b/server/tests/Course.test.ts
@@ -2,7 +2,7 @@ import { expect, test, describe, beforeAll, afterAll } from 'vitest';
 
 import axios from 'axios';
 
-import { testClasses, testReviews } from './mocks/InitMockDb';
+import { testClasses, testReviews, testCourseEvals } from './mocks/InitMockDb';
 import { testPort, testServer } from './mocks/MockServer';
 import { Reviews } from '../db/schema';
 
@@ -13,7 +13,8 @@ beforeAll(async () => {
     undefined,
     testClasses,
     undefined,
-    undefined
+    undefined,
+    testCourseEvals
   );
 });
 
@@ -134,4 +135,34 @@ describe('Course functionality unit tests', () => {
       classOfReviews.map((r) => '')
     );
   });
+  test('Getting course eval from course characteristics', async () => {
+    const res = await axios.post(
+      `http://localhost:${testPort}/api/courses/get-course-eval`,
+      { classSub: 'AEM', classNum: '1200'}
+    );
+
+    expect(res.status).toBe(200);
+    expect(res.data.result.courseOverall).toBe(4.5);
+    expect(res.data.result.numInterest).toBe(55);
+
+    const res2 = await axios.post(
+      `http://localhost:${testPort}/api/courses/get-course-eval`,
+      { classSub: 'BEE', classNum: '5330'}
+    );
+
+    expect(res2.status).toBe(200);
+    expect(res2.data.result.courseOverall).toBe(4.15);
+    expect(res2.data.result.numInterest).toBe(10);
+  });
+
+  test('Getting nonexistent course eval', async () => {
+    const res = await axios.post(
+      `http://localhost:${testPort}/api/courses/get-course-eval`,
+      { classSub: 'CS', classNum: '2110'}
+    );
+
+    expect(res.status).toBe(200);
+    expect(res.data.result).toBe(0);
+  });
+
 });
diff --git a/server/tests/TestServer.ts b/server/tests/TestServer.ts
index b54e85cc..648c2aca 100644
--- a/server/tests/TestServer.ts
+++ b/server/tests/TestServer.ts
@@ -1,9 +1,9 @@
 import { MongoMemoryServer } from 'mongodb-memory-server';
 import mongoose from 'mongoose';
 import express from 'express';
-import { Student, Class, Professor, Review, Subject } from 'common';
+import { Student, Class, Professor, Review, Subject, CourseEvaluation } from 'common';
 import * as http from 'http';
-import { Classes, Students, Subjects, Professors, Reviews } from '../db/schema';
+import { Classes, Students, Subjects, Professors, Reviews, CourseEvaluations } from '../db/schema';
 import { configure } from '../endpoints';
 
 /**
@@ -28,7 +28,8 @@ export default class TestingServer {
     students: Student[] = [],
     classes: Class[] = [],
     professors: Professor[] = [],
-    subjects: Subject[] = []
+    subjects: Subject[] = [],
+    courseEvals: CourseEvaluation[] = [],
   ) => {
     // setup db
     await this.mongoServer.start();
@@ -55,6 +56,8 @@ export default class TestingServer {
 
     await Promise.all(subjects.map(async (c) => await new Subjects(c).save()));
 
+    await Promise.all(courseEvals.map(async (c) => await new CourseEvaluations(c).save()));
+
     await Promise.all(
       professors.map(async (c) => await new Professors(c).save())
     );
diff --git a/server/tests/mocks/InitMockDb.ts b/server/tests/mocks/InitMockDb.ts
index 7ebb13c5..6cdcecde 100644
--- a/server/tests/mocks/InitMockDb.ts
+++ b/server/tests/mocks/InitMockDb.ts
@@ -1,4 +1,11 @@
-import { Class, Professor, Review, Student, Subject } from 'common';
+import {
+  Class,
+  CourseEvaluation,
+  Professor,
+  Review,
+  Student,
+  Subject
+} from 'common';
 
 export const testSubjects: Subject[] = [
   {
@@ -252,3 +259,118 @@ export const testClasses: Class[] = [
     crossList: []
   }
 ];
+
+export const testCourseEvals: CourseEvaluation[] = [
+  {
+    _id: 'WTd1ddnkoAV7',
+    courseName: 'AEM 1200',
+    subject: 'AEM',
+    courseNumber: '1200',
+    semester: 'SPRING 2024',
+    totalEvals: 85,
+    courseOverall: 4.5,
+    profTeachingSkill: 4.78,
+    profKnowledge: 4.8,
+    profClimate: 4.62,
+    profOverall: 4.73,
+    numA: 62,
+    numB: 23,
+    numC: 0,
+    numD: 0,
+    numF: 0,
+    numS: 0,
+    numU: 0,
+    numGradeNA: 0,
+    numFresh: 27,
+    numSoph: 36,
+    numJr: 15,
+    numSr: 7,
+    numAg: 34,
+    numHumec: 6,
+    numArch: 3,
+    numILR: 1,
+    numArts: 27,
+    numEng: 12,
+    numHotel: 0,
+    numOther: 2,
+    numMajorReq: 17,
+    numReputation: 12,
+    numInterest: 55,
+    sentiments: [
+      [8, 4.88],
+      [12, 4.84],
+      [11, 4.8],
+      [19, 4.76],
+      [10, 4.75],
+      [17, 4.72],
+      [13, 4.71],
+      [25, 4.71],
+      [24, 4.7],
+      [14, 4.67],
+      [16, 4.67],
+      [9, 4.63],
+      [18, 4.63],
+      [21, 4.48],
+      [20, 4.4],
+      [15, 4.38],
+      [23, 4.38],
+      [22, 4.09]
+    ]
+  },
+  {
+    _id: 'l5Ucpq8wO8wK',
+    courseName: 'BEE 5330',
+    subject: 'BEE',
+    courseNumber: '5330',
+    semester: 'SPRING 2024',
+    totalEvals: 14,
+    courseOverall: 4.15,
+    profTeachingSkill: 4.28,
+    profKnowledge: 4.69,
+    profClimate: 4.27,
+    profOverall: 4.23,
+    numA: 12,
+    numB: 0,
+    numC: 0,
+    numD: 0,
+    numF: 0,
+    numS: 1,
+    numU: 0,
+    numGradeNA: 2,
+    numFresh: 0,
+    numSoph: 0,
+    numJr: 1,
+    numSr: 11,
+    numAg: 3,
+    numHumec: 0,
+    numArch: 0,
+    numILR: 0,
+    numArts: 0,
+    numEng: 11,
+    numHotel: 0,
+    numOther: 0,
+    numMajorReq: 2,
+    numReputation: 1,
+    numInterest: 10,
+    sentiments: [
+      [16, 4.84],
+      [11, 4.69],
+      [25, 4.69],
+      [12, 4.62],
+      [24, 4.62],
+      [8, 4.57],
+      [19, 4.46],
+      [14, 4.38],
+      [21, 4.23],
+      [9, 4.21],
+      [23, 4.15],
+      [17, 4.08],
+      [10, 4],
+      [18, 4],
+      [20, 3.92],
+      [13, 3.85],
+      [15, 3.85],
+      [22, 3.85]
+    ]
+  }
+];
diff --git a/server/tsconfig.json b/server/tsconfig.json
index 7edfa96e..23a2ffa9 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -6,7 +6,8 @@
     "lib": ["ES2017", "DOM"],
     "esModuleInterop": true,
     "downlevelIteration": true,
-    "outDir": "dist"
+    "outDir": "dist",
+    "resolveJsonModule": true
   },
   "exclude": ["./tests/**/*"]
 }


Reviews:
user: login: qiandrewj
body: Looks really excellent, Matt! Super clean - I think the only thing is to document it a bit, through function docstrings first and maybe later in Notion. I don't think I had anything to add, just some thoughts and things I noticed that I left comments on. Thanks for the hard work :)
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-02T03:09:50Z
user: login: leihelen
body: This looks really great Matthew! The code is divided up clearly and the function names are very intuitive which makes it very easy to read. The logic for the sentiment score merging looks good and having this feature will be really helpful in evaluating the different courses for students. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T21:56:57Z
user: login: leihelen
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T21:57:55Z
user: login: qiandrewj
body: Looks great, Matt - I think one thing is could you put the raw data into the git-ignore so it does not get pushed? Sorry, I should've told you to do that earlier - may need to delete it and then gitignore it. I think most of it looked great! We can talk about the router and course-eval.ts file in work session, but TYSM :)))
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T19:57:59Z
user: login: mjaydenkim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T20:29:47Z
user: login: qiandrewj
body: Thanks for the changes, Matt - looks amazing now! Excited to see how front-end comes along, so def merge when you get the chance.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-18T21:29:54Z

Review comments:
diff_hunk: @@ -0,0 +1,502 @@
+{
path: server/scripts/course_eval_data/test_data.json
user: login: qiandrewj
body: This works, but if you have time in the future we could move some testing data and a short script into the backend tests folder. It doesn't necessarily need to use routing or anything, could just serve as a proof-of-concept for our desired evals behavior
created_at: 2025-03-02T02:45:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
path: server/scripts/populate-course-evals.ts
user: login: qiandrewj
body: The separation in course-eval data between fall and spring was mostly for my scraping tool, so eventually I actually think that it would make the most sense to just copy the spring into the fall doc and have one file only
created_at: 2025-03-02T02:46:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
path: server/scripts/populate-course-evals.ts
user: login: qiandrewj
body: great :)
created_at: 2025-03-02T02:47:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+
+        return [
+          key,
+          {
+            ...value,
+            subject: value.courseName.split(' ')[0],
+            courseNumber: value.courseName.split(' ')[1],
+            courseOverall: parseFloat(value.courseOverall) || 0,
+            profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+            profKnowledge: parseFloat(value.profKnowledge) || 0,
+            profClimate: parseFloat(value.profClimate) || 0,
+            profOverall: parseFloat(value.profOverall) || 0,
+            numA: parseInt(value.numA, 10) || 0,
+            numB: parseInt(value.numB, 10) || 0,
+            numC: parseInt(value.numC, 10) || 0,
+            numD: parseInt(value.numD, 10) || 0,
+            numF: parseInt(value.numF, 10) || 0,
+            numS: parseInt(value.numS, 10) || 0,
+            numU: parseInt(value.numU, 10) || 0,
+            numGradeNA: numGradeNA,
+            numFresh: numFresh,
+            numSoph: numSoph,
+            numJr: numJr,
+            numSr: numSr,
+            totalEvals: totalEvals,
+            numAg: parseInt(value.numAg, 10) || 0,
+            numHumec: parseInt(value.numHumec, 10) || 0,
+            numArch: parseInt(value.numArch, 10) || 0,
+            numILR: parseInt(value.numILR, 10) || 0,
+            numArts: parseInt(value.numArts, 10) || 0,
+            numEng: parseInt(value.numEng, 10) || 0,
+            numHotel: parseInt(value.numHotel, 10) || 0,
+            numOther: parseInt(value.numOther, 10) || 0,
+            numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+            numReputation: parseInt(value.numReputation, 10) || 0,
+            numInterest: parseInt(value.numInterest, 10) || 0,
+            sentiments: [...Array(18).keys()].map(x =>
+              [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+            ),
+          }
+      ]})
+  );
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
+    Object.entries(mergedCourseEvals)
+      .map(([key, value]): [string, CourseEvaluation] => {
+        return [key, sortTopStatements(value)]
+      })
+  );
+
+  return sortedSentimentEvals;
+};
+
+const weightedAvg = (
+  n1: number, n2: number, w1: number, w2: number
+): number => {
+  if (n1 == 0 || w1 == 0) return n2
+  if (n2 == 0 || w2 == 0) return n1
+  return Math.floor((((n1 * w1) + (n2 * w2)) / (w1 + w2)) * 100) / 100
+};
+
+const sortTopStatements = (
+  courseEval: CourseEvaluation
+): CourseEvaluation => {
+  return {...courseEval,
+    sentiments: courseEval.sentiments
+      .map((x): [number, number] => [x[0], Math.floor(x[1] * 100) / 100])
+      .sort((a, b) => b[1] - a[1])
+  };
+};
+
+const mergeCourseLecEvaluations = (
path: server/scripts/populate-course-evals.ts
user: login: qiandrewj
body: This looks good to me! Would like some documentation I think, because the sub-function names are a bit similar -- ex. my understanding of this one is that it takes a weighted average of sentiments for the same _lecture_, whereas `mergeCourseLecEvaluations` then will add all the non-rating numbers together
created_at: 2025-03-02T02:57:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+
+        return [
+          key,
+          {
+            ...value,
+            subject: value.courseName.split(' ')[0],
+            courseNumber: value.courseName.split(' ')[1],
+            courseOverall: parseFloat(value.courseOverall) || 0,
+            profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+            profKnowledge: parseFloat(value.profKnowledge) || 0,
+            profClimate: parseFloat(value.profClimate) || 0,
+            profOverall: parseFloat(value.profOverall) || 0,
+            numA: parseInt(value.numA, 10) || 0,
+            numB: parseInt(value.numB, 10) || 0,
+            numC: parseInt(value.numC, 10) || 0,
+            numD: parseInt(value.numD, 10) || 0,
+            numF: parseInt(value.numF, 10) || 0,
+            numS: parseInt(value.numS, 10) || 0,
+            numU: parseInt(value.numU, 10) || 0,
+            numGradeNA: numGradeNA,
+            numFresh: numFresh,
+            numSoph: numSoph,
+            numJr: numJr,
+            numSr: numSr,
+            totalEvals: totalEvals,
+            numAg: parseInt(value.numAg, 10) || 0,
+            numHumec: parseInt(value.numHumec, 10) || 0,
+            numArch: parseInt(value.numArch, 10) || 0,
+            numILR: parseInt(value.numILR, 10) || 0,
+            numArts: parseInt(value.numArts, 10) || 0,
+            numEng: parseInt(value.numEng, 10) || 0,
+            numHotel: parseInt(value.numHotel, 10) || 0,
+            numOther: parseInt(value.numOther, 10) || 0,
+            numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+            numReputation: parseInt(value.numReputation, 10) || 0,
+            numInterest: parseInt(value.numInterest, 10) || 0,
+            sentiments: [...Array(18).keys()].map(x =>
+              [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+            ),
+          }
+      ]})
+  );
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
+    Object.entries(mergedCourseEvals)
+      .map(([key, value]): [string, CourseEvaluation] => {
+        return [key, sortTopStatements(value)]
+      })
+  );
+
+  return sortedSentimentEvals;
+};
+
+const weightedAvg = (
+  n1: number, n2: number, w1: number, w2: number
+): number => {
+  if (n1 == 0 || w1 == 0) return n2
+  if (n2 == 0 || w2 == 0) return n1
+  return Math.floor((((n1 * w1) + (n2 * w2)) / (w1 + w2)) * 100) / 100
+};
+
+const sortTopStatements = (
+  courseEval: CourseEvaluation
+): CourseEvaluation => {
+  return {...courseEval,
+    sentiments: courseEval.sentiments
+      .map((x): [number, number] => [x[0], Math.floor(x[1] * 100) / 100])
+      .sort((a, b) => b[1] - a[1])
+  };
+};
+
+const mergeCourseLecEvaluations = (
+  eval1: CourseEvaluation,
+  eval2: CourseEvaluation
+): CourseEvaluation => {
+  const w1 = eval1.totalEvals || 0;
+  const w2 = eval2.totalEvals || 0;
+
+  return {
+    ...eval1,
+    ...eval2,
path: server/scripts/populate-course-evals.ts
user: login: qiandrewj
body: Use of the spread operator here looks great! One thing that I will note (a tiny bug that I don't even think is worth fixing), is that the numbers in eval2 then overwrite the numbers in eval1. This was something I only noticed when looking at the test case for AEM 1101, where the count of students for LEC2 is different across professors. Honestly, this is probably inevitable that people make mistakes in the evals, but I think it would just be helpful to note it down as a possible edge case in the function spec
created_at: 2025-03-02T03:02:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+
+        return [
+          key,
+          {
+            ...value,
+            subject: value.courseName.split(' ')[0],
+            courseNumber: value.courseName.split(' ')[1],
+            courseOverall: parseFloat(value.courseOverall) || 0,
+            profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+            profKnowledge: parseFloat(value.profKnowledge) || 0,
+            profClimate: parseFloat(value.profClimate) || 0,
+            profOverall: parseFloat(value.profOverall) || 0,
+            numA: parseInt(value.numA, 10) || 0,
+            numB: parseInt(value.numB, 10) || 0,
+            numC: parseInt(value.numC, 10) || 0,
+            numD: parseInt(value.numD, 10) || 0,
+            numF: parseInt(value.numF, 10) || 0,
+            numS: parseInt(value.numS, 10) || 0,
+            numU: parseInt(value.numU, 10) || 0,
+            numGradeNA: numGradeNA,
+            numFresh: numFresh,
+            numSoph: numSoph,
+            numJr: numJr,
+            numSr: numSr,
+            totalEvals: totalEvals,
+            numAg: parseInt(value.numAg, 10) || 0,
+            numHumec: parseInt(value.numHumec, 10) || 0,
+            numArch: parseInt(value.numArch, 10) || 0,
+            numILR: parseInt(value.numILR, 10) || 0,
+            numArts: parseInt(value.numArts, 10) || 0,
+            numEng: parseInt(value.numEng, 10) || 0,
+            numHotel: parseInt(value.numHotel, 10) || 0,
+            numOther: parseInt(value.numOther, 10) || 0,
+            numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+            numReputation: parseInt(value.numReputation, 10) || 0,
+            numInterest: parseInt(value.numInterest, 10) || 0,
+            sentiments: [...Array(18).keys()].map(x =>
+              [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+            ),
+          }
+      ]})
+  );
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
path: server/scripts/populate-course-evals.ts
user: login: qiandrewj
body: Looks great! Love the way you broke it down into steps
created_at: 2025-03-02T03:06:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+
+        return [
+          key,
+          {
+            ...value,
+            subject: value.courseName.split(' ')[0],
+            courseNumber: value.courseName.split(' ')[1],
+            courseOverall: parseFloat(value.courseOverall) || 0,
+            profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+            profKnowledge: parseFloat(value.profKnowledge) || 0,
+            profClimate: parseFloat(value.profClimate) || 0,
+            profOverall: parseFloat(value.profOverall) || 0,
+            numA: parseInt(value.numA, 10) || 0,
+            numB: parseInt(value.numB, 10) || 0,
+            numC: parseInt(value.numC, 10) || 0,
+            numD: parseInt(value.numD, 10) || 0,
+            numF: parseInt(value.numF, 10) || 0,
+            numS: parseInt(value.numS, 10) || 0,
+            numU: parseInt(value.numU, 10) || 0,
+            numGradeNA: numGradeNA,
+            numFresh: numFresh,
+            numSoph: numSoph,
+            numJr: numJr,
+            numSr: numSr,
+            totalEvals: totalEvals,
+            numAg: parseInt(value.numAg, 10) || 0,
+            numHumec: parseInt(value.numHumec, 10) || 0,
+            numArch: parseInt(value.numArch, 10) || 0,
+            numILR: parseInt(value.numILR, 10) || 0,
+            numArts: parseInt(value.numArts, 10) || 0,
+            numEng: parseInt(value.numEng, 10) || 0,
+            numHotel: parseInt(value.numHotel, 10) || 0,
+            numOther: parseInt(value.numOther, 10) || 0,
+            numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+            numReputation: parseInt(value.numReputation, 10) || 0,
+            numInterest: parseInt(value.numInterest, 10) || 0,
+            sentiments: [...Array(18).keys()].map(x =>
+              [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+            ),
+          }
+      ]})
+  );
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
+    Object.entries(mergedCourseEvals)
path: server/scripts/populate-course-evals.ts
user: login: qiandrewj
body: It's kind of up to you if you want to do this in the preprocessing step or in the database step, but it may be best here to just take the top 5 statements (after sorting) for ease of your next step. Would also work doing it later though
created_at: 2025-03-02T03:09:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+
+        return [
+          key,
+          {
+            ...value,
+            subject: value.courseName.split(' ')[0],
+            courseNumber: value.courseName.split(' ')[1],
+            courseOverall: parseFloat(value.courseOverall) || 0,
+            profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+            profKnowledge: parseFloat(value.profKnowledge) || 0,
+            profClimate: parseFloat(value.profClimate) || 0,
+            profOverall: parseFloat(value.profOverall) || 0,
+            numA: parseInt(value.numA, 10) || 0,
+            numB: parseInt(value.numB, 10) || 0,
+            numC: parseInt(value.numC, 10) || 0,
+            numD: parseInt(value.numD, 10) || 0,
+            numF: parseInt(value.numF, 10) || 0,
+            numS: parseInt(value.numS, 10) || 0,
+            numU: parseInt(value.numU, 10) || 0,
+            numGradeNA: numGradeNA,
+            numFresh: numFresh,
+            numSoph: numSoph,
+            numJr: numJr,
+            numSr: numSr,
+            totalEvals: totalEvals,
+            numAg: parseInt(value.numAg, 10) || 0,
+            numHumec: parseInt(value.numHumec, 10) || 0,
+            numArch: parseInt(value.numArch, 10) || 0,
+            numILR: parseInt(value.numILR, 10) || 0,
+            numArts: parseInt(value.numArts, 10) || 0,
+            numEng: parseInt(value.numEng, 10) || 0,
+            numHotel: parseInt(value.numHotel, 10) || 0,
+            numOther: parseInt(value.numOther, 10) || 0,
+            numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+            numReputation: parseInt(value.numReputation, 10) || 0,
+            numInterest: parseInt(value.numInterest, 10) || 0,
+            sentiments: [...Array(18).keys()].map(x =>
+              [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+            ),
+          }
+      ]})
+  );
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
+    Object.entries(mergedCourseEvals)
+      .map(([key, value]): [string, CourseEvaluation] => {
+        return [key, sortTopStatements(value)]
+      })
+  );
+
+  return sortedSentimentEvals;
+};
+
+const weightedAvg = (
+  n1: number, n2: number, w1: number, w2: number
+): number => {
+  if (n1 == 0 || w1 == 0) return n2
path: server/scripts/populate-course-evals.ts
user: login: leihelen
body: could add a check here for w1 + s2 == 0 just to make sure there is no division by zero
created_at: 2025-03-02T21:51:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
path: server/scripts/populate-course-evals.ts
user: login: leihelen
body: maybe could try to combine filter and map into only one loop to optimize
created_at: 2025-03-02T21:56:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +41,9 @@ export default function AdminTools({ token }: AdminToolsProps) {
     } finally {
       setUpdating(false);
     }
+
path: client/src/modules/Admin/Components/DevTools.tsx
user: login: qiandrewj
body: Before pushing, we should get rid of this test case and can just replace it with the success message - can still use Postman to test the success of different classes
created_at: 2025-03-11T19:46:00Z
author_association: COLLABORATOR
diff_hunk: @@ -6,6 +6,7 @@ import { reviewRouter } from './src/review';
 import { courseRouter } from './src/course';
 import { adminRouter } from './src/admin';
 import aiRouter from './src/ai/ai.router';
+import { courseEvalRouter } from './src/course-eval';
path: server/endpoints.ts
user: login: qiandrewj
body: I think we may want the courseEvalRouter to just be a part of the courses router, similarly to how Cornellians Say and Similar Courses are nested under courses
created_at: 2025-03-11T19:50:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,13 @@
+import { findCourseEval } from './course-eval.data-access';
+import { CourseEvalRequestType } from './course-eval.type';
+
+export const getCourseEval = async ({
+  classSub,
+  classNum
+}: CourseEvalRequestType) => {
+  console.log(classSub);
path: server/src/course-eval/course-eval.controller.ts
user: login: qiandrewj
body: Remove console.logs
created_at: 2025-03-11T19:51:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,9 @@
+import { CourseEvaluations } from '../../db/schema';
+
+export const findCourseEval = async (subject: string, courseNumber: string) => {
+  console.log("subj" + subject)
path: server/src/course-eval/course-eval.data-access.ts
user: login: qiandrewj
body: Also remove console.logs here
created_at: 2025-03-11T19:52:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,9 @@
+import { CourseEvaluations } from '../../db/schema';
+
+export const findCourseEval = async (subject: string, courseNumber: string) => {
+  console.log("subj" + subject)
+  console.log("num" + courseNumber)
+  const cEval = await CourseEvaluations.findOne({ subject, courseNumber }).exec();
path: server/src/course-eval/course-eval.data-access.ts
user: login: qiandrewj
body: LOL so nitpicky sorry but for some reason `cEval` is not sitting right with me -- can you change it to `courseEval` 😢 
created_at: 2025-03-11T19:52:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import express from 'express';
+
+import { CourseEval } from './course-eval';
+
+import {
+  getCourseEval,
+} from './course-eval.controller';
+import { CourseEvalRequestType } from './course-eval.type';
+
+export const courseEvalRouter = express.Router();
+
+/** Reachable at POST /api/course-eval/get-course-eval
+ * @body courseNumber: a course's number
+ * @body courseSubject: a course's subject code
+ * Gets the course evaluation object tied to the given course
+ */
+courseEvalRouter.post('/get-course-eval', async (req, res) => {
+  try {
+    const { classSub, classNum }: CourseEvalRequestType = req.body;
+    const courseEval: CourseEval = new CourseEval({ classSub, classNum });
+    console.log(classSub)
path: server/src/course-eval/course-eval.router.ts
user: login: qiandrewj
body: Remove console.logs
created_at: 2025-03-11T19:53:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import express from 'express';
+
+import { CourseEval } from './course-eval';
+
+import {
+  getCourseEval,
+} from './course-eval.controller';
+import { CourseEvalRequestType } from './course-eval.type';
+
+export const courseEvalRouter = express.Router();
+
+/** Reachable at POST /api/course-eval/get-course-eval
+ * @body courseNumber: a course's number
+ * @body courseSubject: a course's subject code
+ * Gets the course evaluation object tied to the given course
+ */
+courseEvalRouter.post('/get-course-eval', async (req, res) => {
+  try {
+    const { classSub, classNum }: CourseEvalRequestType = req.body;
+    const courseEval: CourseEval = new CourseEval({ classSub, classNum });
path: server/src/course-eval/course-eval.router.ts
user: login: qiandrewj
body: Here, it should be the controller function I think?
created_at: 2025-03-11T19:54:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import express from 'express';
+
+import { CourseEval } from './course-eval';
+
+import {
+  getCourseEval,
+} from './course-eval.controller';
+import { CourseEvalRequestType } from './course-eval.type';
+
+export const courseEvalRouter = express.Router();
+
+/** Reachable at POST /api/course-eval/get-course-eval
+ * @body courseNumber: a course's number
+ * @body courseSubject: a course's subject code
+ * Gets the course evaluation object tied to the given course
+ */
+courseEvalRouter.post('/get-course-eval', async (req, res) => {
+  try {
+    const { classSub, classNum }: CourseEvalRequestType = req.body;
+    const courseEval: CourseEval = new CourseEval({ classSub, classNum });
+    console.log(classSub)
+    console.log(classNum)
+    const validClassSub: string = courseEval.getClassSub();
path: server/src/course-eval/course-eval.router.ts
user: login: qiandrewj
body: And I'm not entirely sure what's going on here
created_at: 2025-03-11T19:54:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+import joi from 'joi';
path: server/src/course-eval/course-eval.ts
user: login: qiandrewj
body: Hmmm... looking at this code and comparing it to other components, it seems that this may be deprecated and stuff we do not use anymore. I will ask a bit more about it though.
created_at: 2025-03-11T19:57:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,299 @@
+import courseData from './course_eval_data/test_data.json';
+
+// Given raw course evaluation data, return an object with its course code as the key
+// and merged course data from the course evaluations JSON file as an object value.
+const parseEval = (data: CourseEvaluationsRaw): CourseEvaluations => {
+  let evals: CourseEvaluations = Object.fromEntries(
+    Object.entries(data)
+      .filter(([_, value]) => value.courseName.split(' ')[2] === 'LEC')
+      .map(([key, value]): [string, CourseEvaluation] => {
+        const numGradeNA = parseInt(value.numGradeNA, 10) || 0;
+        const numFresh = parseInt(value.numFresh, 10) || 0;
+        const numSoph = parseInt(value.numSoph, 10) || 0;
+        const numJr = parseInt(value.numJr, 10) || 0;
+        const numSr = parseInt(value.numSr, 10) || 0;
+        const totalEvals = numGradeNA + numFresh + numSoph + numJr + numSr;
+
+        return [
+          key,
+          {
+            ...value,
+            subject: value.courseName.split(' ')[0],
+            courseNumber: value.courseName.split(' ')[1],
+            courseOverall: parseFloat(value.courseOverall) || 0,
+            profTeachingSkill: parseFloat(value.profTeachingSkill) || 0,
+            profKnowledge: parseFloat(value.profKnowledge) || 0,
+            profClimate: parseFloat(value.profClimate) || 0,
+            profOverall: parseFloat(value.profOverall) || 0,
+            numA: parseInt(value.numA, 10) || 0,
+            numB: parseInt(value.numB, 10) || 0,
+            numC: parseInt(value.numC, 10) || 0,
+            numD: parseInt(value.numD, 10) || 0,
+            numF: parseInt(value.numF, 10) || 0,
+            numS: parseInt(value.numS, 10) || 0,
+            numU: parseInt(value.numU, 10) || 0,
+            numGradeNA: numGradeNA,
+            numFresh: numFresh,
+            numSoph: numSoph,
+            numJr: numJr,
+            numSr: numSr,
+            totalEvals: totalEvals,
+            numAg: parseInt(value.numAg, 10) || 0,
+            numHumec: parseInt(value.numHumec, 10) || 0,
+            numArch: parseInt(value.numArch, 10) || 0,
+            numILR: parseInt(value.numILR, 10) || 0,
+            numArts: parseInt(value.numArts, 10) || 0,
+            numEng: parseInt(value.numEng, 10) || 0,
+            numHotel: parseInt(value.numHotel, 10) || 0,
+            numOther: parseInt(value.numOther, 10) || 0,
+            numMajorReq: parseInt(value.numMajorReq, 10) || 0,
+            numReputation: parseInt(value.numReputation, 10) || 0,
+            numInterest: parseInt(value.numInterest, 10) || 0,
+            sentiments: [...Array(18).keys()].map(x =>
+              [x += 8, (parseFloat(value['statement' + x + 'Score']) || 0)]
+            ),
+          }
+      ]})
+  );
+
+  // Merge objects with the same lecture
+  const mergedLecEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(evals)) {
+    if (mergedLecEvals[value.courseName]) {
+      mergedLecEvals[value.courseName] =
+        mergeCourseLecEvaluations(mergedLecEvals[value.courseName], value);
+    } else {
+      mergedLecEvals[value.courseName] = value;
+    }
+  }
+
+  // Merge objects with the same course
+  const mergedCourseEvals: CourseEvaluations = {};
+  for (const [_, value] of Object.entries(mergedLecEvals)) {
+    const courseName = value.courseName.split(' ').slice(0, 2).join(' ')
+    if (mergedCourseEvals[courseName]) {
+      mergedCourseEvals[courseName] = mergeCourseEvaluations(mergedCourseEvals[courseName], value);
+    } else {
+      mergedCourseEvals[courseName] = value;
+    }
+  }
+
+  const sortedSentimentEvals: CourseEvaluations = Object.fromEntries(
+    Object.entries(mergedCourseEvals)
path: server/scripts/populate-course-evals.ts
user: login: mjaydenkim
body: i kinda wanna do it later, since i think it's helpful to have as much data as possible (e.g. if we wanna expand it later on to include a menu with all sentiments)
created_at: 2025-03-13T20:29:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: dti-github-bot
created_at: 2025-03-02T01:44:03Z
body: [diff-counting] Significant lines: 722.
author_association: MEMBER
user: login: mjaydenkim
created_at: 2025-03-14T02:18:00Z
body: just pushed all the fixes to all of this, i think it's a lot cleaner now, ty!!!!!!! a note, though -- the hard-coded data etc means that now the build fails when the json file can't load. not sure what to do about that
author_association: CONTRIBUTOR
user: login: mjaydenkim
created_at: 2025-03-18T22:11:10Z
body: thank you!!!!!! wait what should we do about the undefined import, if anything? should i just comment out all relevant code so the build succeeds?
author_association: CONTRIBUTOR
user: login: qiandrewj
created_at: 2025-03-23T19:20:35Z
body: Hmm... yeah maybe comment it out for now then. thanks!
author_association: COLLABORATOR