---
title: "standardize.interactive"
output: html_document
date: "2026-01-06"
---

## What this script does

This script standardizes votes so that everyone's opinion is measured on the
same scale, regardless of whether they tend to score high, low, or somewhere
in between.

For each voter, it takes every score they gave and re-centers it around their
own personal average, then scales it by their own personal spread (how much
their scores vary). This is called a z-score.

**What this means in practice:**

- A voter who consistently gives out 9s and 10s to almost everyone gets
  brought down. Since a 9 or 10 is normal for them, it barely counts as a
  standout vote once standardized.
- A voter who is more critical overall (mostly gives 5s and 6s) but gives an
  occasional 9 or 10 gets that vote weighted heavily. Since a high score is
  rare for them, it stands out as genuine enthusiasm and carries real weight.
- Voters who only submitted one vote, or who gave the exact same score to
  every PNM (no personal spread to measure), are standardized against the
  whole chapter's average and spread instead of their own, so their vote
  still carries real signal rather than being dropped or forced to zero.
- Each PNM's final score is the average of everyone's standardized votes for
  them, so the score reflects relative enthusiasm across voters, not raw
  numbers that can be skewed by generous or stingy voting habits.

## How to run this each round

Follow these steps in order every round. The short version: **the only thing
you ever rename is this round's fresh export from MyVote. Nothing else -
not column headers, not the previous round's file, not the output.**

**1. Export your votes from MyVote as a .csv.**
Don't rename the columns inside it. It needs to come in with these exact
column names (MyVote already exports them this way, so you shouldn't need to
touch this):
  - vote value as `Value`
  - ADPi who voted as `Author`
  - PNM first name as `PNM First Name`
  - PNM last name as `PNM Last Name`
  - PNM id as `PNM.ID`
  - the round number as `Round`

**2. Rename that exported file to exactly `current_round_votes.csv`.**
This is the *only* file you ever rename by hand, and it's a file rename, not
a column rename — don't open the CSV and start editing headers. Put the
renamed file in the same folder as this script. This is what
`df <- read.csv(...)` below reads in as this round's fresh votes.

**3. That's it — leave `prev.file` alone.**
The script automatically finds last round's saved results on its own (it
looks for files named like `round3_scores.csv` and picks the most recent
one that's older than this round). You never set or edit `prev.file` by
hand. If no previous round file exists yet, it's treated as the first round
automatically.

**4. Old round files: keep them, don't delete or rename them.**
Each round's saved output already has that round's history folded into it
(see the merge section further down), and the auto-detect step in step 3
depends on the filenames staying exactly as this script saved them. Deleting
or renaming an old round file won't break anything going forward, but
renaming one could cause it to stop being found automatically.

**5. To run: click the "Run" button in the top right and choose "Run All."**

**Testing this script:** the folder has one raw, untouched vote export kept
from each past round, named and ordered clearly for testing:
`round1-open-invite-votes.csv`, `round2-philo-votes.csv`,
`round3-sisterhood-votes.csv`, `round4-pref-votes.csv`. These are originals,
not something this script reads directly — to test a round, copy one, rename
the copy to `current_round_votes.csv`, and run. Do this in order
(open invite, then philo, then sisterhood, then pref) to test the full
multi-round chain the same way it'll actually be used. There are currently no
old merged/output files left in this folder — every `roundN_scores.csv` file
you see going forward was generated by this script, never edited by hand.

## Upload Exported File from MyVote (as .csv)

```{r setup, message=FALSE}
library(dplyr)
df <- read.csv("current_round_votes.csv")  # this filename should never change - always rename this round's export to match it (see step 2 above)

# figure out what round this is from the vote data itself
current_round <- suppressWarnings(max(df$Round, na.rm = TRUE))
if (!is.finite(current_round)) current_round <- "current"

# auto-detect the previous round's saved output - no manual editing needed.
# looks for every file this script has already saved (roundN_scores.csv) and
# picks the highest round number that comes before this round.
existing_outputs <- list.files(pattern = "^round[0-9]+_scores\\.csv$")

if (length(existing_outputs) > 0) {
  existing_round_numbers <- as.numeric(gsub("^round([0-9]+)_scores\\.csv$", "\\1", existing_outputs))
  eligible <- if (is.numeric(current_round)) {
    existing_outputs[existing_round_numbers < current_round]
  } else {
    existing_outputs
  }
  if (length(eligible) > 0) {
    eligible_numbers <- as.numeric(gsub("^round([0-9]+)_scores\\.csv$", "\\1", eligible))
    prev.file <- eligible[which.max(eligible_numbers)]
  } else {
    prev.file <- NA
  }
} else {
  prev.file <- NA
}

if (!is.na(prev.file) && file.exists(prev.file)) {
  prev.df <- read.csv(prev.file)
  has_previous <- TRUE
  message("Found and using previous round file: ", prev.file)
} else {
  has_previous <- FALSE
  message("No previous round file found - treating this as the first round.")
}
```

Z-score standardization: for each voter (Author), subtract their personal average and divide by their personal standard deviation. This ensures every voter's scores have the same center (0) AND the same spread (1), so a "this is my top pick" vote counts equally no matter who cast it.

Voters who only submitted one vote can't have a personal std dev computed, so their vote is standardized using the global mean and SD across all votes instead. This keeps their vote in the results while putting it on the same scale as everyone else.

For each PNM, average their standardized scores (automatically accounts for unequal vote counts).

```{r standardize-scores}
# compute global mean and SD as a fallback for single-vote authors
global_avg <- mean(df$Value, na.rm = TRUE)
global_sd  <- sd(df$Value,   na.rm = TRUE)

df_standardized <- df %>%
  # compute each author's personal mean, SD, and vote count
  group_by(Author) %>%
  mutate(
    author_avg   = mean(Value, na.rm = TRUE),
    author_sd    = sd(Value,   na.rm = TRUE),
    author_votes = n()
  ) %>%
  ungroup() %>%

  # z-score using personal stats if author has >1 vote AND actually varies their
  # scores, else fall back to global mean/SD. This covers two cases the same way:
  #   - authors with only 1 vote (no personal SD can be computed)
  #   - authors whose personal SD is 0 (gave every PNM the exact same score, so
  #     (Value - use_avg) / use_sd would divide by zero)
  # Falling back to global stats (instead of forcing standardized_value to 0)
  # means these voters' scores still carry real signal — "this is a high score
  # relative to the whole pool" — rather than going completely flat. That matters
  # most when a PNM was only rated by one of these voters: they still get a
  # meaningful score instead of landing at exactly 0 with no information.
  mutate(
    use_avg            = ifelse(author_votes > 1 & author_sd > 0, author_avg, global_avg),
    use_sd             = ifelse(author_votes > 1 & author_sd > 0, author_sd,  global_sd),
    standardized_value = (Value - use_avg) / use_sd
  ) %>%

  # compute per-PNM average standardized score
  group_by(PNM.First.Name, PNM.Last.Name) %>%
  mutate(
    num_votes              = n(),
    avg_standardized_score = mean(standardized_value, na.rm = TRUE)
  ) %>%
  ungroup() %>%

  # sort by your existing ID
  arrange(PNM.ID)
```

Don't need to show all of the columns since Author and comments don't matter anymore.
distinct() collapses to one row per PNM (since the standardized scores were computed row-by-row).

```{r Remove Unwanted Columns}
final_output <- df_standardized %>%
  select(
    PNM.ID,
    PNM.First.Name,
    PNM.Last.Name,
    num_votes,
    avg_standardized_score
  ) %>%
  distinct()
```

## Merge previous round scores on PNM id into one file

This section is built to handle any number of past rounds chained together, not
just one. Each round's scores are labeled with that round's number (score_round1,
score_round2, score_round3, ...) instead of a generic "current"/"previous" label,
so nothing gets overwritten or lost as you go from round 2 to round 3 to round 4
and beyond.

If there is no previous round file, the current round's scores are used as-is.

```{r group}
# current_round was already figured out back in the setup step above -
# reused here to label this round's columns so re-running this across many
# rounds never reuses the same column name twice
votes_col <- paste0("num_votes_round", current_round)
score_col <- paste0("score_round", current_round)

current <- final_output %>%
  rename(
    !!votes_col := num_votes,
    !!score_col := avg_standardized_score
  )

if (has_previous) {
  if (all(c("num_votes", "avg_standardized_score") %in% names(prev.df))) {
    # legacy single-round file with no round-labeling of its own
    previous <- prev.df %>%
      select(PNM.ID, PNM.First.Name, PNM.Last.Name, num_votes, avg_standardized_score) %>%
      rename(
        num_votes_previous = num_votes,
        score_previous      = avg_standardized_score
      )
  } else {
    # already a merged, round-labeled file from a prior run of this script -
    # carry all of its round columns forward untouched
    previous <- prev.df
  }
  output <- left_join(current, previous, by = c("PNM.ID", "PNM.First.Name", "PNM.Last.Name"))
} else {
  output <- current
}
```

## Save File

This automatically names the output file after the current round (e.g.
`round4_scores.csv`), so you never have to come up with or type a filename
yourself. Next round, set `prev.file` above to whatever filename gets printed
below - don't rename this file afterward.

```{r save-merged-csv}
output_file <- paste0("round", current_round, "_scores.csv")
write.csv(output, output_file, row.names = FALSE)
message("Saved this round's results to: ", output_file)
message("Next round, set prev.file <- \"", output_file, "\"")
```
