• Alex Maldonado
  • About
  • Materials
  • Teaching
  • Research
  • Blog

BIOSC 1000

2025 Fall

Published

December 2025

metadata = FileAttachment("/teaching/evals/course/biosc1000-25f/meta.json").json()
quant = FileAttachment("/teaching/evals/course/biosc1000-25f/quant.json").json()
grades = FileAttachment("/teaching/evals/course/biosc1000-25f/grades.json").json()
segments = FileAttachment("/teaching/evals/course/biosc1000-25f/segments.json").json()
themeSet = FileAttachment("/teaching/evals/course/biosc1000-25f/themes.json").json()
promptSet = FileAttachment("/teaching/evals/course/biosc1000-25f/prompts.json").json()
demographics = FileAttachment("/teaching/evals/course/biosc1000-25f/demographics.json").json()
omet = import("/static/js/omets.js")
// One row per segment
segRows = omet.flattenSegments(segments)
// Display labels for the course themes present in the data
themeLabels = Object.fromEntries(
  [...new Set(segRows.map(r => r.theme))].map(k => [k, omet.prettyTheme(k)])
)
themeColors = Generators.observe(change => {
  const el = document.querySelector(".omet") || document.body;
  const read = () => {
    const cs = getComputedStyle(el);
    const g = (n, fb) => (cs.getPropertyValue(n).trim() || fb);
    change({
      ink: g("--omet-ink", "#1a2233"),
      bg: g("--omet-bg", "#faf9f6"),
      muted: g("--omet-muted", "#6a737b"),
      line: g("--omet-line", "#e0ddd4"),
      accent: g("--omet-accent", "#39729e"),
      pos: g("--omet-pos", "#2f7d5a"),
      mix: g("--omet-mix", "#b9832b"),
      sug: g("--omet-sug", "#3a77a6"),
      crit: g("--omet-crit", "#b23a2e"),
      na: g("--omet-na", "#8a8f98"),
      lk: [1, 2, 3, 4, 5].map(i => g("--omet-lk-" + i, "#999999")),
    });
  };
  read();
  // Paired-theme toggles swap stylesheets; the new --bs-* values may not be
  // live on the first frame, so re-read now, next frame, and after a beat.
  const reread = () => { read(); requestAnimationFrame(read); setTimeout(read, 120); };
  const mo = new MutationObserver(reread);
  mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-bs-theme"] });
  mo.observe(document.body, { attributes: true, attributeFilter: ["class", "data-bs-theme"] });
  return () => mo.disconnect();
})
html`<div class="omet-stats">
  <div class="cell"><b>${metadata.responseRate}%</b><span>response rate</span></div>
  <div class="cell"><b>${segments.length}</b><span>written comments</span></div>
  <div class="cell"><b>${segRows.length}</b><span>segments extracted</span></div>
  <div class="cell"><b>${metadata.overallMean}</b><span>overall mean (of 5)</span></div>
</div>`

These are the complete, unedited results of the student survey for . Every rating and every one of the written comments appear exactly as students submitted them. Around that raw feedback, I’ve added a systematic reading of it and my response.

Note

For complete transparency, you can also download the original document containing all student responses. If you find any discrepancy, please send me an email.

I am publishing it in full because teaching is the center of my current role. I would rather show the feedback whole—including the parts that are hard to read—than let it be stood in for by a single number, and because working through criticism in public is part of how I try to get better at it. It’s written for four readers at once: me, a student deciding whether to take the course, colleagues and reviewers, and anyone curious how one instructor reads and uses feedback.

Of students, responded (a % response rate), well above the 50% Pitt asks instructors to aim for, which makes this an unusually complete sample. The written comments answer two prompts: “What did you like best about how the course was taught?” and “If you were teaching this course, what would you do differently?” The second asks for criticism by design, so that column runs heavier on problems. Remember, that’s the prompt working, not a verdict. Rating items run from 1 (strongly disagree) to 5 (strongly agree); “N/A” is left out of each mean, and 3 is neutral.

Per the University’s recommendation, I had to offer a small reward to achieve this response rate. This semesters reward was that, upon certain thresholds being met, I would release a select few questions from the final exam before they take it. The selected questions were the hardest on the exam, and no answers were provided—they had to work it out themselves.

WarningRead for patterns, not outliers

The most useful thing a set of evaluations can tell you is where students agree. Pitt’s own guidance is explicit that student surveys should never be the sole or primary measure of teaching, and that bias is well documented. One glowing or one furious comment is a single voice; a theme repeated across dozens is a signal. This page is built to help you tell those apart.

Who’s in the course

Before the ratings, it is important to understand who gave them. Evaluations read differently depending on the population behind them: why students are enrolled, what other coursework they are measured against, and who is actually in the room. It’s compiled from the enrollment and Canvas rosters for the enrolled students invited to the survey.

omet.demographicsStats(demographics, {html})

Why they’re taking it

For most students, this is a required course on a pre-health or life-science path, not an elective. This directly shapes what they want from it and how they judge it. The first chart shows every field of study students have declared (this includes majors and minors). Because many declare more than one, the bars count declarations and sum to more than the headcount. The second shows how many fields each student is spread across.

Nearly seven in ten students major in a life science or chemistry, and about a fifth carry the pre-med Conceptual Foundations of Medicine plan. For most of them, biochemistry is a prerequisite on the way to medical or health school, not a free choice. That matters for reading the feedback: a high-stakes, required course for a grade-conscious, pre-professional audience runs hot on anything that touches grading, workload, or GPA.

omet.barChart(omet.topN(demographics.fieldsOfStudy, 14, {restLabel: "Other fields"}), {Plot, colors: themeColors}, {marginLeft: 210, xLabel: "students declaring this field (a student may declare several)"})
omet.barChart(
  omet.mapLabel(demographics.declaredPlansPerStudent, d => d.plans + (d.plans === 1 ? " field" : " fields")),
  {Plot, colors: themeColors}, {marginLeft: 90, xLabel: "students"})

Who’s answering

Pronouns (from the Canvas roster) are the only signal here that bears on gender, and they’re an imperfect proxy. They’re worth showing because the literature Pitt’s own guidance points to finds student evaluations carry a well-documented gender (and other demographic) bias, and the makeup of the class is part of interpreting that.

The class skews toward she/her students (about 53%, against 32% he/him), with a few using they/them or she/they and about one in ten undisclosed. Read this next to the “patterns, not outliers” note above: because the direction and size of evaluation bias depend on both the instructor’s and the students’ gender, the composition here is context for the scores, not a correction to apply to them.

omet.barChart(demographics.pronouns, {Plot, colors: themeColors}, {marginLeft: 110, xLabel: "students"})
html`<p class="omet-aside">${demographics.note}</p>`

The numbers

Nine rating items, ranked by mean. Color marks relative strength; the dark point is the overall average.

TODO

omet.meansChart(quant.means, {Plot, d3, colors: themeColors})
omet.likertChart(quant.likert, {Plot, d3, colors: themeColors})
omet.barChart(quant.standards, {Plot, colors: themeColors}, {marginLeft: 150, xLabel: "standards the course set (count of students)"})
omet.barChart(quant.hours, {Plot, colors: themeColors}, {marginLeft: 150, xLabel: "hours per week spent outside class"})

Exam scores and final grades

The survey is what students felt; this is what the gradebook recorded. These are final scores, so they include all adjustments including curves, the dropped exam, and the optional cumulative final.

In-person assessments

TODO

omet.examChart(grades, {Plot, colors: themeColors},
  {order: ["Exam 1", "Exam 2", "Exam 3", "Exam 4", "Final Exam", "Amino Acid Quiz", "Metabolism Quiz"]})

Each box spans the middle half of the class (25th–75th percentile), the line inside is the median, the whiskers reach the rest, and the ◆ marks the mean. Zeros are non-attempts (a dropped exam, or the optional final not taken) and are excluded; n is how many students each row reflects.

Final grades

Of students, finished with an A or A-, earned a B or higher, and did not pass.

TODO

gradeStats = {
  const g = grades.finalGrades, has = xs => g.filter(v => xs.includes(v)).length;
  return {
    n: g.length,
    a: has(["A+", "A", "A-"]),
    bplus: has(["A+", "A", "A-", "B+", "B"]),
    f: has(["F"])
  };
}
omet.gradeChart(grades.finalGrades, {Plot, colors: themeColors})

What students said

NoteThe theme codebook
omet.themeCodebook(themeSet, {html})

Sentiment within each category: where the praise and the criticism actually land. The top row is every segment; each row below is one category:

omet.sentimentChart(segRows, {Plot, d3, colors: themeColors}, {groupBy: "category"})

The same segments grouped by the course-specific themes (the finer, hand-curated layer; Unthemed covers segments no theme fit):

omet.barChart(omet.themeCounts(segRows, themeLabels), {Plot, colors: themeColors}, {marginLeft: 260, xLabel: "segments under this theme"})

What worked

TODO

What to change

TODO

My response

TODO

All comments

Every written comment is here, exactly as submitted (original spelling and all). Switch between whole comments (each shown in full, with its segments highlighted in place) and individual segments (every matching span stands on its own). Filter by category, theme, or sentiment, narrow further with the specificity, actionability, and register flags, and search the text. The same filters drive both views, so a category from the charts above is the category you read here: pick one in whole comments to read the comments it appears in, or in individual segments to see every span in it at once.

The comments answer two prompts. Every comment id (and every segment id within it) begins with the letter of the prompt it responded to, so you can read the two apart with the Prompt filter below.

omet.promptList(promptSet, segments, {html})
viewof fView = Inputs.radio(
  new Map([["Whole comments", "comments"], ["Individual segments", "segments"]]),
  {value: "comments", label: "Show"}
)
viewof fQuery = Inputs.text({label: "Search", placeholder: "Search the text…", submit: false})
viewof fPrompt = Inputs.checkbox(
  new Map(omet.promptCounts(segments, promptSet).map(d => [`${d.key} · ${d.question ?? d.key} (${d.n})`, d.key])),
  {label: "Prompt"}
)
viewof fCat = Inputs.checkbox(
  new Map(omet.categoryCounts(segRows).map(d => [`${d.label} (${d.n})`, d.key])),
  {label: "Category"}
)
viewof fTheme = Inputs.checkbox(
  new Map(omet.themeCounts(segRows, themeLabels).map(d => [`${d.label} (${d.n})`, d.key])),
  {label: "Course theme"}
)
viewof fSent = Inputs.checkbox(
  new Map(omet.sentimentCounts(segRows).map(d => [`${d.label} (${d.n})`, d.key])),
  {label: "Sentiment"}
)
viewof fSpec = Inputs.checkbox(
  new Map(omet.attrCounts(segRows, "specificity", omet.SPECIFICITY_LABEL).map(d => [`${d.label} (${d.n})`, d.key])),
  {label: "Specificity"}
)
viewof fAct = Inputs.checkbox(
  new Map(omet.attrCounts(segRows, "actionability", omet.ACTIONABILITY_LABEL).map(d => [`${d.label} (${d.n})`, d.key])),
  {label: "Actionability"}
)
viewof fCiv = Inputs.checkbox(
  new Map(omet.attrCounts(segRows, "register", omet.REGISTER_LABEL).map(d => [`${d.label} (${d.n})`, d.key])),
  {label: "Register"}
)
viewof fVerified = Inputs.toggle({label: "Only human-verified segments"})
// One filter object, read by both views.
filters = ({
  prompts: fPrompt,
  categories: fCat, themes: fTheme, sentiments: fSent,
  specificity: fSpec, actionability: fAct, register: fCiv, onlyVerified: fVerified
})
// Whole comments containing a matching segment (matchIds records which ones).
shownComments = omet.selectComments(segments, filters, fQuery)
// Individual segments that match, from the flattened table.
shownSegments = omet.selectSegments(segRows, filters, fQuery)
md`**Showing ${fView === "segments"
  ? `${shownSegments.length} of ${segRows.length} segments`
  : `${shownComments.length} of ${segments.length} comments`}.**`
fView === "segments"
  ? omet.segmentCards(shownSegments, {html, query: fQuery})
  : omet.commentCards(shownComments, {html, query: fQuery})
  • © 2026 Alex Maldonado
  • CC BY-NC-SA 4.0