The Woodstock Hill: Elevation Map of Reed College

Elevation contour map of the area surrounding Reed College. Filled contour bands shade the land from dark purple at the lowest elevations to bright yellow at the highest, and a legend bar is labeled "Elevation Contours, 60 ft to 250 ft." The land is lowest in the west and southwest, along SE 28th Ave, and rises steadily toward the east, reaching its highest point past SE Campesinos Blvd.

Looks better in color, huh?

It’s actually surprisingly difficult to convey all the information you’d like to in just black and white and in a small format. I tried grayscale on the map but it didn’t read well, so I just left contour lines, but the colored image shows what’s going on much better.

black and white contour map of reed college

Chavez Campesinos Blvd

The biggest surprise in making this map was that Chavez has been officially renamed in Oregon’s databases as Campesinos Blvd. They are going to begin changing signs soon. The decision was made official on September 9th by a unanimous city council vote.

How this map was made

To create the contours on this map, I used a file from Oregon Metro’s Regional Land Information System (RLIS). They host hundreds of publicly available datasets that cover a wide range of topics including geographical maps, but files about Oregon demographics, climate and environmental data, civil buildings, transportation, and more. It’s a great resource for many projects.

I used the contour map with five feet intervals to get elevations for the Portland area. This came in the form of shape files (.shp), which initially just looks like a series of lines, not shaded areas between lines.

I wanted an area around Reed, not just Reed itself, so I added a rectangular polygon around the area I wanted. I set it to be white and moved it behind the contour lines.

Because I wanted to include Reed College on my map, I also downloaded another shape file that included names and boundaries for different tax lots in Portland. I opened that as a layer in QGIS and filtered the names to just find the boundary of Reed College and remove all other tax lots. Then I overlaid this Reed layer onto the map.

Then I converted the contour lines into polygons so that they could be shaded in by elevation height. There’s a tool in QGIS called “Contour Polygons” that will do this for you. Then I changed the symbology of that layer so that every five feet difference in elevation appeared as a different color along a gradient. I chose five feet because that is the resolution of the original contour file, but I could have chosen a higher number for a less fine-grained graphic.

Lastly, I wanted to add some roads so that it would be easier to orient oneself. QGIS has a Open Streets Map plugin that allows you to add all kinds of things like bike lanes, horse paths, stop lights, and more. From that layer, I selected the streets that I wanted to add to my map. Then I fiddled with the coloring a bit to make things more legible.

To determine that the height of the Woodstock hill is 240 feet, I used the “identify features” tool to see the information for the contour line that overlapped 39th street.

I did the same for an area on the quad. I identified the quad by adding on a layer of Reed buildings, but I thought the map looked too busy with those included, so I removed them for the final graphic. The point of my graphic was to look at the steepness of the Woodstock hill, so the buildings distracted rather than added to the map.

Posted in Uncategorized | Leave a comment

Who’s the fattest of them all?

Two fat bears: 435 Holly (left) and 747 (right)

If you haven’t heard, Fat Bear Week is an annual online contest run by Katmai National Park and Preserve in Alaska. The public votes on which brown bear has done the best job of fattening up for winter. The site Explore.org runs live webcams at along the Brooks River where the bears hunt salmon, so people all over the world can watch the bears fish throughout the season.

The contest of bear fatness started in 2014 as a day of single voting, but has expanded into a bracket style tournament that lasts a week. The contest continues until September 29th, so go vote for the fattest bear!

Our graphic this week shows the history of past champions and the correlation between votes in the contest and money raised for the nature preserve.

Where did we find the data?

There’s no official Fat Bear Week repository, so honestly getting the data was kind of a pain. Vote totals and winners were compiled by hand from the National Park Service, Explore.org (which hosts the bear cams and the voting), and news coverage of each year’s contest. The revenue of the Katmai Conservancy was much easier to find and comes from their IRS Form 990 filings. All nonprofits (including schools like Reed) have to file these publicly, which makes them a good source for data. You can find them on ProPublica’s website.

Some information was not possible to find. I couldn’t find the full list of bears entered in the competition for years before 2020, only the champion and runner up were available. The vote counts are also rough estimates that were reported from news sites, not hard numbers from Explore.org. I also couldn’t find any estimates for 2015-2017, but 1700 votes were cast in the first year of the contest.

If you’re interested in looking at the raw data, you can find csv files with data for each year on the Quest repository of the Data@Reed Github page. The ‘summary.csv’ file contains the information used to make the graph, and the other files contain information about individual bears, their relation to one another, and the sources I used to find this data.

Making the Graphic

The code to the graphic is in ‘scripts/analysis.R’ on the Github page, but I will copy the relevant portions of it below. There’s an additional script that pulls together data for the summary file and another that compiles the sources, but these aren’t really material to the analysis.

The final figure is two charts stacked on top of each other and sharing one year axis. I used four packages besides the tidyverse: {ggstar} for star-shaped markers, {patchwork} for stacking plots, {ggtext} for the boxed subtitle and caption, and {scales} for axis labels.

The first bit of the script just to see which bears had the most appearances in the contest so that we could only graph those. When I did this, bear 409 was not included because the data about entries wasn’t great for the first few years. So I forced the appearances to include Beadnose because she was a two-time champion. Then I added some labeling columns so that things would appear nicely on the graph and set how the years would be displayed on the x-axis in order to save room.

Click to see the wrangling R script

library(tidyverse)
library(ggstar)
library(patchwork)
library(ggtext)

# --- Update summary.csv with labeled entries/champion/runner_up ---------
appearances <- read_csv("data/appearances.csv", show_col_types = FALSE)
summary_df <- read_csv("data/summary.csv", show_col_types = FALSE)
bears <- read_csv("data/bears.csv", show_col_types = FALSE)

bear_names 
  mutate(bear_id = as.character(bear_id)) |>
  select(bear_id, name)

# " " labels, falling back to just the bear_id
label_bears 
    left_join(bear_names, by = "bear_id") |>
    mutate(label = if_else(is.na(name), bear_id, str_c(bear_id, name, sep = " "))) |>
    pull(label)
}

# Per-year entry list (2014-2020 = finalists only)
entries_per_year 
  mutate(bear_label = label_bears(bear_id)) |>
  arrange(bear_label) |>
  summarize(entries = str_flatten(bear_label, collapse = ", "), .by = year)

summary_out 
  select(-any_of("entries")) |>
  left_join(entries_per_year, by = "year") |>
  relocate(entries, .after = year) |>
  mutate(
    year = as.integer(year),
    champion = label_bears(champion),
    runner_up = label_bears(runner_up)
  )

write_csv(summary_out, "data/summary.csv")

# --- Per-bear contest chart ---------------------------------------------
# TODO: bump a bear now that 409 is force-included (8 shown)
min_appearances <- 4
force_include <- c("409")

completed_appearances 
  filter(year != 2026)

qualifying 
  count(bear_id, name = "n_appearances") |>
  filter(n_appearances >= min_appearances | bear_id %in% force_include)

contest_data 
  inner_join(qualifying, by = "bear_id") |>
  mutate(
    label = label_bears(bear_id),
    status = case_when(
      result == "champion" ~ "Champion",
      result == "runner_up" ~ "Runner-up",
      TRUE ~ "Entered"
    ) |> factor(levels = c("Entered", "Runner-up", "Champion"))
  ) |>
  mutate(first_year = min(year), .by = bear_id) |>
  mutate(label = fct_reorder(label, -first_year, .fun = min))

contest_span 
  summarize(min_year = min(year), max_year = max(year), .by = c(bear_id, label))

contest_years <- min(contest_data$year):max(contest_data$year)

</details>

Next I started arranging the plot. I created each component of the chart separately and then used the package {patchwork} to arrange them into one figure. I first started with just the title and the boxed text that I wanted to appear at the top and bottom.

Show the graph headings code

title_text <- "It's Fat Bear Week!"
subtitle_text <- "Below are the historical top competitors for fattest bear in Katmai National Park Alaska, which has occurred annually since 2014. All bears have a numerical ID and some bears also have a name."
title_style <- element_text(face = "bold", size = 28, hjust = 0.5)

# Shared boxed-text style for subtitle/caption
boxed_text <- function(outer_margin) {
  element_textbox_simple(
    size = 11, face = "bold", color = "black", hjust = 0.5, halign = 0.5,
    margin = outer_margin, padding = margin(5, 8, 5, 8),
    linetype = 1, box.color = "grey60", linewidth = 0.4, fill = NA
  )
}
</details>

Then I made the top chart by using the geom_segment() command. A start shape isn’t an option within basic ggplot, so that’s where the {ggstar} package came in. Then I just did a lot of adjusting the theme to get things to look exactly how I wanted them. That’s one of the greatest things about R is you can really customize things down to a very fine level.

Show code to make the contest chart

title_text <- "It's Fat Bear Week!"
subtitle_text <- "Below are the historical top competitors for fattest bear in Katmai National Park Alaska, which has occurred annually since 2014. All bears have a numerical ID and some bears also have a name."
title_style <- element_text(face = "bold", size = 28, hjust = 0.5)

# Shared boxed-text style for subtitle/caption
boxed_text <- function(outer_margin) {
  element_textbox_simple(
    size = 11, face = "bold", color = "black", hjust = 0.5, halign = 0.5,
    margin = outer_margin, padding = margin(5, 8, 5, 8),
    linetype = 1, box.color = "grey60", linewidth = 0.4, fill = NA
  )
}
</details>

Next I made the revenue plot with the votes displayed on the same figure as an overlay. To do this, the y-axis on the left was set to show revenue and the y-axis on the right shows total vote. I got lucky here because these numbers scale almost identically. That often doesn’t happen, so then you’d need to use transformations to make your it work on the same figure, or just separate the two data sources to their own figures.

Show code to make the revenue and votes graph

title_text <- "It's Fat Bear Week!"
subtitle_text <- "Below are the historical top competitors for fattest bear in Katmai National Park Alaska, which has occurred annually since 2014. All bears have a numerical ID and some bears also have a name."
title_style <- element_text(face = "bold", size = 28, hjust = 0.5)

# Shared boxed-text style for subtitle/caption
boxed_text <- function(outer_margin) {
  element_textbox_simple(
    size = 11, face = "bold", color = "black", hjust = 0.5, halign = 0.5,
    margin = outer_margin, padding = margin(5, 8, 5, 8),
    linetype = 1, box.color = "grey60", linewidth = 0.4, fill = NA
  )
}
</details>

There’s a little bit of code in there that also runs cor() which calculates the Pearson’s correlation coefficient to test the linear correlation between the two datasets. Note that this isn’t saying anything about the cause of the trends, just that they do go up at a very similar rate, 0.93, where 1 is a perfect relationship.

Then I added code to stack the figures and display only one x-axis at the bottom of the graph.

Show code to make the contest chart

# --- Combined stacked figure -----------------------------------------
# Drop top chart's x-axis (shown below instead)
contest_chart_top <- contest_chart +
  theme(
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    plot.margin = margin(b = 2)
  )

# Shared year axis between the two panels
overlay_chart_bottom <- overlay_chart +
  scale_x_continuous(
    breaks = contest_years, labels = abbreviate_years, limits = range(contest_years),
    position = "top"
  ) +
  theme(
    axis.text.x.top = element_text(color = "black", size = 12, face = "bold"),
    axis.ticks.x.top = element_blank(),
    plot.margin = margin(t = 2)
  )

contest_and_overlay <- contest_chart_top / overlay_chart_bottom +
  plot_layout(heights = c(3, 1.5), axis_titles = "collect") +
  plot_annotation(
    title = title_text,
    subtitle = subtitle_text,
    caption = caption_text,
    theme = theme(
      plot.title = title_style,
      plot.subtitle = boxed_text(margin(t = 4, b = 6)),
      plot.caption = boxed_text(margin(t = 6, b = 2))
    )
  )
contest_and_overlay
</details>

Adding the text boxes was the fiddliest bit. Honestly, that’s something that probably could have been done easier in an imaging program like PowerPoint or Adobe, but I get stubborn when I know something can be done in R, so I pushed until everything was tweaked just how I wanted it.

Things you could do with this data

Inside the spreadsheets, there’s more data about each bear. There’s information on their family members, their sex, and the year they were born. A more thorough internet search could probably reveal even more than is already in there. Here are a few ideas of other things you could do with this data:

  • Who’s fatter: mama bears or papa bears?
  • Does a bear do a better job of fattening up over time or are they the fattest when they’re young and able to exert more energy hunting?
  • You could trace the lineage of many of the bears to see if champions beget champions or if fatness is maybe more environmental.
  • You could look up data about salmon numbers in Alaska for the same years as the bear data. Then you could see if there was any correlation between the winners and the salmon abundance. For that, you’d probably need to have a fat score for each bear, which you could subjectively do based off of pictures.

I’ll leave you with a couple pictures of Otis, the four-time champion. RIP, you fat bear!

a fat brown bear sitting midstream and facing the camera
a fat brown bear in the river with a bit of fish in his mouth and the rest in his paw
Posted in Uncategorized | Tagged | Leave a comment

Commons’ Common Meals

The most commonly served meal was Fish & Chips at 14 times! It was closely followed by Fried Tofu & Chips at 12 times.

This week we wanted to know what types of meals were being served at commons. We found out that a ton of data on that is available and the chart in the Quest just skims the surface. If you want to learn more about the data, or use it yourself, keep reading for a walk-through or email data@reed.edu.

plot of common main ingredients served by Commons

Where did we find the data?

Every day Bon Appetit’s menu is posted on the Commons homepage. Since we wanted to get access to past menus, the first thing to do was see if that information was cached somewhere. When you go to any Commons’ menu that is not todays, you will see the URL look something like this: https://reed.cafebonappetit.com/cafe/commons-cafe/2026-09-11/ The important thing there is that it ends in the specific date. That means each menu was theoretically stored as its own page that we could access using a web-scraping script.

How did we web scrape the data?

The individual URL dates indicate that each day’s data exists as an inline JS object in the page’s HTML code. So we can write a script to access that and pull out the code. I prefer working in R, so I wrote this script in R. But it could have been written in Python or a number of other languages.

There is actually a lot of data contained in each day’s page. To make the graph simpler (because it has to print in black and white and be legible when small), I decided to only pull dinner menus and only from the SimplyOasis and Classics stations. But more data than this is available if you want to use it!

Here’s the script. The main things it relies on are the packages {httr} and {jsonlite}. {httr} handles the HTTP request — downloading each day’s page — while {jsonlite} takes the JSON-formatted menu data embedded in that page and turns it into something R can actually work with.

Show the script


# load libraries
library(httr)
library(jsonlite)
library(stringr)
library(readr)

####  set selections for scraping
###############################

# get url for menu
cafe_url <- "https://reed.cafebonappetit.com/cafe/commons-cafe/%s/"

# select what meal (can be single value or vector)
daypart <- "Dinner"
# select station (can be single value or vector)
station_wanted <- c("SimplyOASIS", "Classics")
# select date range
dates <- seq(as.Date("2026-01-26"), as.Date("2026-05-14"), by = "day")


####  create functions to:
####  fetch webpage,  
####  extract the menu,
####  extract the correct part of day

# downloads one date's menu page as raw HTML
fetch_page <- function(d) {
  url <- sprintf(cafe_url, format(d, "%Y-%m-%d"))
  resp <- GET(url, timeout(30))
  stop_for_status(resp)
  content(resp, as = "text", encoding = "UTF-8")
}

# pulls the menu_items from the JavaScript and parses it as JSON
extract_menu_items <- function(html) {
  pattern <- regex("Bamco\\.menu_items\\s*=\\s*(\\{.*?\\});", dotall = TRUE)
  match <- str_match(html, pattern)
  fromJSON(match[1, 2], simplifyVector = FALSE)
}

# pulls the relevant daypart and returns as a named list
extract_dayparts <- function(html) {
  pattern <- regex(
    "Bamco\\.dayparts\\['(\\d+)'\\]\\s*=\\s*(\\{.*?\\});\\s*\\n\\s*\\}\\)\\(\\);",
    dotall = TRUE
  )
  matches <- str_match_all(html, pattern)[[1]]
  dayparts <- list()
  for (i in seq_len(nrow(matches))) {
    dayparts[[matches[i, 2]]] <- fromJSON(matches[i, 3], simplifyVector = FALSE)
  }
  dayparts
}


####  create function extract the actual meals
###############################

# helper function for pulling menu
# if a is NULL, return b; otherwise return a 
# (can't get combinable rows if NULL values are present)
`%||%` <- function(a, b) if (is.null(a)) b else a

# extracts meals based on previous parameters
get_meal_items <- function(d) {
  html <- fetch_page(d)
  menu_items <- extract_menu_items(html)
  dayparts <- extract_dayparts(html)

  # create empty list for output
  rows <- list()
  for (dp in dayparts) {
    # only do this for the correct meal time
    if (!(dp$label %in% daypart)) next

    # find every station that matches the station(s)_wanted
    # using Filter because it's lists not dataframes
    stations <- Filter(\(s) str_trim(str_remove_all(s$label, "<[^>]+>")) %in% station_wanted, dp$stations)

    # make sure there's something there to find
    if (length(stations) == 0) next

    # for every matching station, grab data for each of its items
    for (station in stations) {
      for (item_id in station$items) {
        # get the item's full list of things
        item <- menu_items[[item_id]]
        # make a dataframe out of the following
        rows[[length(rows) + 1]] <- data.frame(
          date = as.character(d),
          meal = dp$label,
          item_name = item$label,
          description = item$description %||% "", # put "" instead of NULL
          price = as.character(item$price %||% ""), # put "" instead of NULL
          dietary_tags = paste(unlist(item$cor_icon), collapse = ", ")
        )
      }
    }
  }
  # bind everything together
  do.call(rbind, rows)
}


####  actually pull the data
###############################

# create empty list for things to go in
rows <- list()
# get meal for each date
for (i in seq_along(dates)) {
  d <- dates[i]
  rows[[i]] <- get_meal_items(d) # this is the line that actually runs everything

  # prints each date just to show progress 
  cat(as.character(d), "\n")
  # small pause between requests (technically don't need if it's too slow)
  Sys.sleep(0.1)
}

all_meals <- do.call(rbind, rows)


####  write file
###############################

write_csv(all_meals, "data/dinner_jan26_may14.csv")

</details>

Out of this script, I get a csv file that looks like this:

Wrangling the data and graphing

The way I designed the scraping leaves the data pretty clean, so not a lot needs to be done to work with it. The only thing I need to do is decide what things I want to pull from it. After looking through it, I decided that the best thing would be to look at main ingredients from the item name. I decided to focus on proteins (and mushrooms). Full disclosure, I used Claude to generate this list. I have strong and mixed feelings about AI, but this is one of the uses where it excels and it was much faster than trying to just identify things by most common word because things like “pasta” or “seasoned” came up a lot.

  • Meat Substitute: Beyond, Gardein, Chik, Impossible
  • Chicken: Chicken, Pollo
  • Beef: Beef, Steak, Brisket, Tri-Tip
  • Pork: Pork, Sausage, Bratwurst
  • Turkey: Turkey
  • Fish: Fish, Salmon
  • Tofu: Tofu
  • Mushroom: Mushroom, Portobello
  • Chickpea: Chickpea, Garbanzo
  • Soy Tempeh: Tempeh (tempeh not described as chickpea or lentil)
  • Lentil: Lentil
  • Bean: Bean

I organized the data so that each dish had a column classifying it as the above or “other”. Then the script just counted up the frequency of each. I also classified as “meat”, “veggie”, or “mixed” (because other contained meat and veggie dishes).

Then I made a bar graph with flipped coordinates so the foods would be on the y-axis. When you have a lot of bars to show or things with long names, this is a good thing to do. People can see the difference in bars just as easily, and it makes reading the names simpler. So, consider doing this with your bar graphs.

I colored the bars by meat/veggie/other. I made other striped and to do this I needed to add a bit of more specialized code with the package {ggpattern}. Then I made adjustments to the axis labels, the theme background, and the display of the gridlines.

Show the script


# load library
library(tidyverse)
library(ggpattern)

# load data
raw_menu <- read_csv("data/dinner_jan26_may14.csv")

# most popular single meal
most_popular <- raw_menu |> 
  group_by(item_name) |> 
  summarize(total = n()) |> 
  arrange(desc(total)) |> 
  slice(1) |>
  pull(item_name)

# search item_name to tag main ingredient
# group into categories
# order is hierarchical (helps with sausage vs fake sausage, diff kinds of tempeh)
menu <- raw_menu %>%
  mutate(main_ingredient = case_when(
    str_detect(item_name, regex("Beyond|Gardein|Chik|Impossible", ignore_case = TRUE)) ~ "Meat Substitute",
    str_detect(item_name, regex("Chicken|Pollo", ignore_case = TRUE)) ~ "Chicken",
    str_detect(item_name, regex("Beef|Steak|Brisket|Tri-Tip", ignore_case = TRUE)) ~ "Beef",
    str_detect(item_name, regex("Pork|Sausage|Bratwurst", ignore_case = TRUE)) ~ "Pork",
    str_detect(item_name, regex("Turkey", ignore_case = TRUE)) ~ "Turkey",
    str_detect(item_name, regex("Fish|Salmon", ignore_case = TRUE)) ~ "Fish",
    str_detect(item_name, regex("Tofu", ignore_case = TRUE)) ~ "Tofu",
    str_detect(item_name, regex("Mushroom|Portobello", ignore_case = TRUE)) ~ "Mushroom",
    str_detect(item_name, regex("Tempeh", ignore_case = TRUE)) &
      str_detect(item_name, regex("Chickpea|Garbanzo", ignore_case = TRUE)) ~ "Chickpea",
    str_detect(item_name, regex("Tempeh", ignore_case = TRUE)) &
      !str_detect(item_name, regex("Lentil", ignore_case = TRUE))~ "Soy Tempeh",
    str_detect(item_name, regex("Chickpea|Garbanzo", ignore_case = TRUE)) ~ "Chickpea",
    str_detect(item_name, regex("Lentil", ignore_case = TRUE)) ~ "Lentil",
    str_detect(item_name, regex("Bean", ignore_case = TRUE)) ~ "Bean",
    # real dishes with other main ingredient 
    item_name %in% c(
      "Garlic Roasted Lamb",
      "Stuffed Peppers with Smashed Potatoes",
      "Eggplant Stew over Polenta",
      "Seared Yams over Polenta",
      "Vegetable Pozole",
      "Pozole Chile Verde"
    ) ~ "Other",
    TRUE ~ NA_character_
  )) %>%
  # drop things like ice cream desserts that aren't dinner
  filter(!is.na(main_ingredient)) 

# summarize and arrange by count
summary_menu <- menu |> 
  group_by(main_ingredient) |> 
  summarize(count = n()) |> 
  arrange(desc(count))
summary_menu


# add meat vs veggie
# other has lamb, so it's a combo of both
food_type <- c(
  "Chicken" = "Meat",
  "Beef" = "Meat",
  "Pork" = "Meat",
  "Turkey" = "Meat",
  "Fish" = "Meat",
  "Mushroom" = "Veggie",
  "Tofu" = "Veggie",
  "Meat Substitute" = "Veggie",
  "Soy Tempeh" = "Veggie",
  "Chickpea" = "Veggie",
  "Lentil" = "Veggie",
  "Bean" = "Veggie",
  "Other" = "Mixed"
)

summary_menu <- summary_menu |>
  mutate(food = food_type[main_ingredient],
         food_pattern = case_when(food == "Mixed" ~ "Y",
                                  TRUE ~ "N"))

# horizontal bar plot
ingredient_plot <- summary_menu |>
  ggplot(aes(x = reorder(main_ingredient, count), y = count, fill = food, pattern = food_pattern)) +
  geom_col_pattern(
    color = NA,
    pattern_fill = "black",
    pattern_color = NA,
    pattern_density = 0.3,
    pattern_spacing = 0.03,
    pattern_angle = 45
  ) +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
  scale_fill_manual(
    values = c("Meat" = "gray25", "Veggie" = "gray65", "Mixed" = "gray65"),
    breaks = c("Meat", "Veggie", "Mixed"),
    name = NULL
  ) +
  scale_pattern_manual(values = c("N" = "none", "Y" = "stripe")) +
  guides(
    pattern = "none",
    # this forces only "Both" to look striped
    fill = guide_legend(override.aes = list(pattern = c("none", "none", "stripe")))
  ) +
  labs(
    x = NULL, y = "Number of Times Served",
    title = "What's for Dinner?") +
  theme_minimal(base_size = 16) +
  theme(
    plot.title = element_text(face = "bold", size = 28, hjust = 0.5),
    plot.subtitle = element_text(size = 13, color = "black", margin = margin(b = 10), hjust = 0.5),
    axis.text.x = element_text(color = "black", size = 12),
    axis.text.y = element_text(color = "black", size = 12, margin = margin(r = 0)),
    axis.title.x = element_text(face = "bold", size = 14),
    legend.text = element_text(size = 14),
    legend.position = "top",
    panel.grid.minor.y = element_blank(),
    panel.grid.major.y = element_blank()
  )

ingredient_plot

# save image and a vector copy for print 
ggsave("ingredient_plot.png", ingredient_plot, width = 8, height = 6)
ggsave("ingredient_plot.svg", ingredient_plot, width = 8, height = 6)

Things you could do with these scripts

So there’s a lot more to the data than just what I got. I only looked at last semester, but the data goes back to 2014 (I think, you’d have to double check). The JSON data has meals from all times of day and all stations present in commons. It also has prices and dietary tags, like gluten free. Here are a few ideas:

  • Does the menu change when there are holidays approaching? More pie at Thanksgiving or more halal food during Ramadan?
  • How many new dishes are added each semester or is the menu consistent over time?
  • How do the Farm to Fork offerings change with seasons?
  • Was there any shift after Covid?
If you’re interested in exploring any of these, you can email data@reed.edu or come by the DataLab!
Posted in Uncategorized | Tagged | Leave a comment