Load data

library(tidyverse)
library(DT) # library to create tables
library(scales) # library to format dollars
library(RColorBrewer)

is_outlier <- function(x) {
  return(x < quantile(x, 0.25, na.rm = T) - 1.5 * IQR(x, na.rm = T) | x > quantile(x, 0.75, na.rm = T) + 1.5 * IQR(x, na.rm = T))
}
daily_full <- read.csv("./output/kwh_daily_2026-03-16.csv") %>%
  mutate(date = as_date(date)) # convert back to date

joined_full <- read.csv("./output/kwh_annual_2026-03-16.csv") 

joined_acad <- read.csv("./output/kwh_academic_2026-03-16.csv") 

total <- joined_full %>%
  filter(meter != "Submeter" & type != "Res Hall - U" & type != "Production") %>%
  summarize(kwh = sum(kwh, na.rm = T),
            kwh_corr = sum(kwh_corr, na.rm = T),
            sqft = sum(sqft, na.rm = T)) %>%
  mutate(kwh_sqft = kwh_corr/sqft,
         meter = "Total")

total_acad <- joined_acad %>%
  filter(meter != "Submeter" & type != "Res Hall - U" & type != "Production") %>%
  summarize(kwh = sum(kwh, na.rm = T),
            kwh_corr = sum(kwh_corr, na.rm = T),
            sqft = sum(sqft, na.rm = T)) %>%
  mutate(kwh_sqft = kwh_corr/sqft,
         meter = "Total")

buildings <- read.csv("./keys/fy25_building_list_updated.csv") %>%
  mutate(meter = ifelse(weis_meter == 1, "Weis Meter",
                               ifelse(main_meter == 0 & weis_meter == 0, "Individually Metered", "Main Meter")))

# store conversion factors
dollars_kwh <- 0.08138507
co2_kg_kwh <- 0.30082405
# summarize # buildings by meter status
bldg_sum <- buildings %>%
  group_by(meter) %>%
  summarize(number = n())

bldg_tot <- bldg_sum %>%
  summarize(number = sum(number)) %>%
  mutate(meter = "Total")

bldg_comb <- rbind(bldg_sum, bldg_tot)

# generate summary for Main, Weis, and Individual meters
joined_agg <- joined_full %>%
  ungroup() %>%
  filter(meter != "Submeter" & type != "Res Hall - U" & type != "Production") %>%
  group_by(meter) %>%
  summarize(kwh = sum(kwh, na.rm = T),
            kwh_corr = sum(kwh_corr, na.rm = T),
            sqft = sum(sqft, na.rm = T)) %>%
  mutate(kwh_sqft = kwh_corr/sqft) %>%
  rbind(total) %>%
  mutate(dollars = kwh_corr*dollars_kwh,
          ghg_MTCO2 = (kwh_corr*co2_kg_kwh)/1000) %>%
  select(-kwh) %>%
  arrange(kwh_corr)

joined_agg$meter <- factor(joined_agg$meter,
                           levels = c("Main Meter - Total",
                                      "Weis Meter - Total",
                                      "Individual", "Total"),
                           labels = c("Main Meter", "Weis Meter",
                                      "Individually Metered","Total"))

joined_tot <- joined_agg %>%
  left_join(bldg_comb, by = "meter")
# generate summary by building category for individually metered buildings
joined_cat <- joined_full %>%
  filter(meter %in% c("Individual","Submeter")) %>%
    mutate(kwh_sqft = kwh_corr/sqft, # calculate kwh per sqft
         kwh_person = kwh_corr/occupants,
         dollars = kwh_corr*dollars_kwh,
         ghg_kgCO2 = kwh_corr*co2_kg_kwh) %>% 
  group_by(type) %>%
  summarize(n = n(),
            kwh = sum(kwh_corr),
            dollars = sum(dollars),
            ghg_kgCO2 = sum(ghg_kgCO2),
            sqft = median(sqft, na.rm = T),
            med_kwh_sqft = median(kwh_sqft, na.rm = T),
            kwh_sqft_25 = quantile(kwh_sqft, .25, na.rm = T),
            kwh_sqft_75 = quantile(kwh_sqft, .75, na.rm = T)) %>%
  arrange(-kwh)

Electricity use summary

joined_pretty_tot <- joined_tot %>%
  mutate(kwh = round(kwh_corr, digits = 0),
         dollars = paste("$",round(dollars, digits = 0)),
         ghg_MTCO2 = round(ghg_MTCO2, digits = 0),
         sqft = round(sqft, digits = 0),
         kwh_sqft = round(kwh_sqft, digits = 1)) %>%
  select(meter, number, sqft, kwh, dollars, ghg_MTCO2, kwh_sqft) %>%
  arrange(kwh)

joined_pretty_cat <- joined_cat %>%
  mutate(kwh = round(kwh, digits = 0),
         dollars = paste("$",round(dollars, digits = 0)),
         ghg_MTCO2 = round(ghg_kgCO2/1000, digits = 0),
         sqft = round(sqft, digits = 0),
         med_kwh_sqft = round(med_kwh_sqft, digits = 1),
         kwh_sqft_25 = round(kwh_sqft_25, digits = 1),
         kwh_sqft_75 = round(kwh_sqft_75, digits = 1)) %>%
    select(type, n, sqft, kwh, dollars, ghg_MTCO2, med_kwh_sqft, kwh_sqft_25, kwh_sqft_75) %>% 
  arrange(desc(med_kwh_sqft))

Aggregate meters

datatable(joined_pretty_tot,
          rownames = FALSE,
          colnames = c("Meter status","Buildings", "Square\nfootage", "kWh", "Est. cost", 
                       "CO2e\n(MT)",  "kWh per\nsqft"),
          filter = "none",
          class = "compact",
          options = list(pageLength = 4, autoWidth = TRUE, dom = 't'),
          caption = "Table 1. Totals for annual electricity use by meter type. Annual estimates for each meter were adjusted to account for missing days of electricity data.")

Individually metered buildings

datatable(joined_pretty_cat,
          rownames = FALSE,
          colnames = c("Building\ntype","Buildings\nwith data", "Median\nsquare\nfootage", "kWh", 
                       "Est. cost", "CO2e\n(MT)", "Median\nkWh\nper sqft",
                       "25th Perc.", "75th Perc."),
          filter = "none",
          class = "compact",
          options = list(pageLength = 11, autoWidth = TRUE, dom = 't'),
          caption = "Table 2. Descriptive statistics for annual electricity use by building type for buildings with individual electricity use data. Annual estimates for each meter were adjusted to account for missing days of electricity data.")

Electricity use over the year

daily_graph <- daily_full %>%
  mutate(date = as_date(date),
         month = month(date, label = TRUE),
         day = wday(date, label = TRUE),
         type_brief = recode(type,
                       'Res Hall - U' = 'Residential',
                       'Res Hall - S' = 'Residential',
                       'Res Hall - M' = 'Residential',
                       'Res Hall - L' = 'Residential')) %>%
  filter(!is.na(month))

ggplot(filter(daily_graph, meter != "Submeter"),
       aes(x = month, y = kwh/10^6, fill = reorder(type_brief, kwh, FUN = sum))) +
  geom_col(position = "stack") +
  scale_fill_brewer(type = "qual", palette = "Paired") +
  theme_bw() +
  labs(x = "", y = "Electricity use (million kWh)", fill = "",
       title = "Figure 1. Fiscal Year 2025",
       subtitle = "Monthly patterns in electricity use")

Version Author Date
cd016a6 maggiedouglas 2026-03-07
92fcd5c maggiedouglas 2026-03-04
ggplot(filter(daily_graph, meter != "Submeter"),
       aes(x = month, y = kwh/10^3, fill = reorder(type_brief, kwh, FUN = sum))) +
  geom_col(position = "stack") +
  facet_wrap(. ~ type_brief, scales = "free") +
  scale_fill_brewer(type = "qual", palette = "Paired") +
  theme_bw() +
  theme(legend.position = "none") +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)) +
  labs(x = "", y = "Electricity use (1000 kWh)", fill = "",
       title = "Figure 2. Fiscal Year 2025",
       subtitle = "Monthly patterns in electricity use by category",
       caption = "Note: Data only includes buildings with individual meters. Most electricity use is accounted for on the Main Meter.")

Electricity intensity

intensity <- joined_full %>%
  filter(!(type %in% c("Res Hall - U","Production", "Non-building"))) %>%
  mutate(kwh_sqft = kwh_corr/sqft,
         meter_type = ifelse(type %in% c("Main Meter","Weis Meter"), "Aggregate", "Individual Meter"),
         outlier = case_when(
           NAME == "King House" ~ "King House",
           NAME == "Kisner - Woodward" ~ "K-W",
           NAME == "Factory Apts." ~ "Factory",
           NAME == "Rector Science Center" ~ "Rector",
           NAME == "Quarry, The " ~ "Quarry",
           NAME == "27 W. High St." ~ "27 W. High"
         )) 

# median and IQR come from EIA (2022) table, annual kWh per square foot for Colleges/Universities
# https://www.eia.gov/consumption/commercial/data/2018/ce/pdf/c22.pdf

ggplot(intensity,
       aes(x = reorder(type, kwh_sqft, FUN = "median"),
                        y = kwh_sqft, fill = type)) +
  annotate("rect", xmin = -Inf, xmax = Inf, ymin = 7.4, ymax = 14.3, color = "lightgray", alpha = 0.3) +
  geom_hline(yintercept = 10.3, linetype = "dashed", color = "white") +
  facet_grid(meter_type ~ ., scales = "free_y", space = "free_y") +
  geom_boxplot() +
  geom_label(aes(label = outlier), na.rm = TRUE, nudge_x = -0.25, nudge_y = 0.5, 
             color = "black", fill = "white", size = 2, alpha = 0, label.size = NA) +
  coord_flip() +
  theme_bw() +
  theme(legend.position = "none") +
  labs(x = "", y = "kWh per sqft per year", 
       title = "Figure 3. Fiscal Year 2025")

Version Author Date
9145690 maggiedouglas 2026-03-12
cd016a6 maggiedouglas 2026-03-07
92fcd5c maggiedouglas 2026-03-04
intensity_acad <- joined_acad %>%
  filter(!(type %in% c("Res Hall - U","Production", "Non-building"))) %>%
    mutate(kwh_sqft = kwh_corr/sqft,
         meter_type = ifelse(type %in% c("Main Meter","Weis Meter"), "Aggregate", "Individual Meter"))

ggplot(intensity_acad,
       aes(x = reorder(type, kwh_sqft, FUN = "median"),
                        y = kwh_sqft, fill = type)) +
  annotate("rect", xmin = -Inf, xmax = Inf, ymin = 7.4, ymax = 14.3, color = "lightgray", alpha = 0.3) +
  geom_hline(yintercept = 10.3, linetype = "dashed", color = "white") +
  facet_grid(meter_type ~ ., scales = "free_y", space = "free_y") +
  geom_boxplot() +
  coord_flip() +
  theme_bw() +
  theme(legend.position = "none") +
  labs(x = "", y = "kWh per sqft per year", 
       title = "Figure 4. Academic Year 2024/2025")


sessionInfo()
R version 4.5.2 (2025-10-31)
Platform: x86_64-apple-darwin20
Running under: macOS Ventura 13.7.8

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] RColorBrewer_1.1-3 scales_1.4.0       DT_0.34.0          lubridate_1.9.5   
 [5] forcats_1.0.1      stringr_1.6.0      dplyr_1.2.0        purrr_1.2.1       
 [9] readr_2.2.0        tidyr_1.3.2        tibble_3.3.1       ggplot2_4.0.2     
[13] tidyverse_2.0.0   

loaded via a namespace (and not attached):
 [1] sass_0.4.10       generics_0.1.4    stringi_1.8.7     hms_1.1.4        
 [5] digest_0.6.39     magrittr_2.0.4    timechange_0.4.0  evaluate_1.0.5   
 [9] grid_4.5.2        fastmap_1.2.0     rprojroot_2.1.1   workflowr_1.7.2  
[13] jsonlite_2.0.0    whisker_0.4.1     promises_1.5.0    crosstalk_1.2.2  
[17] jquerylib_0.1.4   cli_3.6.5         rlang_1.1.7       withr_3.0.2      
[21] cachem_1.1.0      yaml_2.3.12       otel_0.2.0        tools_4.5.2      
[25] tzdb_0.5.0        httpuv_1.6.16     vctrs_0.7.1       R6_2.6.1         
[29] lifecycle_1.0.5   git2r_0.36.2      htmlwidgets_1.6.4 fs_1.6.7         
[33] pkgconfig_2.0.3   pillar_1.11.1     bslib_0.10.0      later_1.4.8      
[37] gtable_0.3.6      glue_1.8.0        Rcpp_1.1.1        xfun_0.56        
[41] tidyselect_1.2.1  rstudioapi_0.18.0 knitr_1.51        farver_2.1.2     
[45] htmltools_0.5.9   labeling_0.4.3    rmarkdown_2.30    compiler_4.5.2   
[49] S7_0.2.1