Offensive Power Rating (OPR) is an estimate of how many points an individual FIRST Robotics Competition (FRC) team can be expected to contribute to an alliance score. OPR is calculated from prior match scores.

Here are some links:

Data Source

I’ll demonstrate how I use R to calculate OPR using match data uploaded by Chief Delphi user Ether. The files contains match data for 8921 matches for 2696 teams.

Time Required for Calculation

When I ran the OPR calculation on a Surface 4 with an i5 processor and 8Gb of memory, Here’s how long it took:

> wOpr <- CalcOpr8Col("8col.dat")
[1] "Preparing Data 2016-10-16 15:18:56"
[1] "Preparing B Vector 2016-10-16 15:18:56"
[1] "Preparing A Matrix 2016-10-16 15:18:57"
[1] "Checking A Matrix 2016-10-16 15:19:06"
[1] "Solving for OPR 2016-10-16 15:19:17"
[1] "Calculations Complete 2016-10-16 15:19:20"

Overall, the calculations took 24 seconds. I could easily cut out the 11 seconds that it takes to check that the A matrix is valid, which would bring the total elapsed time to 13 seconds. Note that it only takes three seconds to solve for OPR once the A and B matrices are prepared – most of the time is required to build the A matrix itself. I suspect my algorithm for preparing the A matrix could be improved. The algorithm uses a nested for loop to read all of the match data. For loops have a reputation for being slow in R, so I’ll see if I can modify the algorithm to use functionals.

Reading the Match Data and Building the Data Frame

The data file is a text file with data fields separated by spaces. Each row contains both the blue and red match scores for one match. The column order is red1, red2, red3, blue1, blue2, blue3, red_score, blue_score.

First, we’ll read the text file into an R data frame and shape the data frame such that there are two rows per alliance, with three columns for team numbers and one column for the alliance score.

# This is the working directory on my system. Modify it for your configuration.
setwd("C:/Users/stacy/OneDrive/Projects/FIRST_API/OPR")
source("opr.R")
matches.df <- Shape8colFile("8col.dat")

# Display first 5 rows
matches.df[1:5, ]
##   teamNumber.1 teamNumber.2 teamNumber.3 scoreFinal matchNumber
## 1         3103         4063          457         26           1
## 2         2158         2985         4670          1           2
## 3         3037         2805          653         26           3
## 4         2468         2966         3008        137           4
## 5         3561         2721         5052         28           5

Next, we’ll extract an ordered list of teams from the match data. The list of teams will be used several times in subsequent calculations.

teams <- GetTeams(matches.df)

# Display first 5 rows
teams[1:5]
## [1]  1  4  8 11 16

Calculating the A and B Matrices

Now it’s time to calculate the B vector, which is the sum of all alliance scores for all 2696 teams.

B <- CalcB(matches.df, teams)

# Display first 5 rows
B[1:5]
##    1    4    8   11   16 
## 2093 1688 1746 6879 5704

Now for the A matrix.

A <- CalcA1(matches.df, teams)

# Display the upper left corner of the A matrix
A[1:20, 1:20]
##     1  4  8 11 16 20 21 25 27 28 31 33 34 41 45 48 51 53 56 57
## 1  24  0  0  0  0  0  0  0  0  0  0  1  0  0  0  0  1  0  0  0
## 4   0 21  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
## 8   0  0 22  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
## 11  0  0  0 58  0  0  0  0  0  0  0  0  0  1  0  0  0  0  1  0
## 16  0  0  0  0 34  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
## 20  0  0  0  0  0 36  0  0  0  0  0  0  0  0  0  0  0  1  0  0
## 21  0  0  0  0  0  0 22  0  0  0  0  0  0  0  0  0  0  0  0  0
## 25  0  0  0  0  0  0  0 46  0  0  0  0  0  0  0  1  0  0  1  0
## 27  0  0  0  0  0  0  0  0 56  0  0  0  0  0  0  0  0  0  0  0
## 28  0  0  0  0  0  0  0  0  0 21  0  0  0  0  0  0  0  0  0  0
## 31  0  0  0  0  0  0  0  0  0  0 10  0  0  0  0  0  0  0  0  0
## 33  1  0  0  0  0  0  0  0  0  0  0 58  0  0  0  0  0  0  0  0
## 34  0  0  0  0  0  0  0  0  0  0  0  0 10  0  0  0  0  0  0  0
## 41  0  0  0  1  0  0  0  0  0  0  0  0  0 36  0  0  0  0  1  0
## 45  0  0  0  0  0  0  0  0  0  0  0  0  0  0 34  0  0  0  0  0
## 48  0  0  0  0  0  0  0  1  0  0  0  0  0  0  0 43  0  0  0  0
## 51  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0 46  0  0  0
## 53  0  0  0  0  0  1  0  0  0  0  0  0  0  0  0  0  0 23  0  0
## 56  0  0  0  1  0  0  0  1  0  0  0  0  0  1  0  0  0  0 46  0
## 57  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0 20

Checking the A Matrix

I like to be confident that the A matrix is formed correctly. The CheckA function verifies the following:

# CheckA will throw an error if A is not valid.
CheckA(A, matches.df, teams)

Solving for x (OPR)

Finally, we’ll use R’s built in solve functio to calculate the OPRs.

OPRs <- solve(A, B)

# Save OPRs to a CSV file.
write.csv(OPRs, "World_OPRs.csv")

# Show the first 20 OPRs.
OPRs[1:20]
##         1         4         8        11        16        20        21 
## 24.618922 20.281847 24.085335 46.192437 92.226272 67.936762 37.613890 
##        25        27        28        31        33        34        41 
## 55.747588 98.842209 29.636078  7.105654 86.658450 17.586410  4.424738 
##        45        48        51        53        56        57 
## 56.456220 54.986947 76.075287 22.561491 54.855071 15.016499
# Show the Issaquah Robotics Society's OPR
OPRs["1318"]
##     1318 
## 76.59617

Function Definitions

Here are the the definitions for the functions used above.

Shape8colFile <- function(file) {
  # Read match scores from text file and assign column names.
  matches <- read.table(file)
  names(matches) <- c("Red1", "Red2", "Red3", "Blue1", "Blue2", "Blue3",
                      "RedScore", "BlueScore")
  
  # Split red and blue alliance scores into separte data frames
  matches.red <- matches[, c("Red1", "Red2", "Red3", "RedScore")]
  matches.blue <- matches[, c("Blue1", "Blue2", "Blue3", "BlueScore")]
  
  # Add a column with match numbers
  matches.red$matchNumber <- 1:nrow(matches.red)
  matches.blue$matchNumber <- 1:nrow(matches.blue)  
  
  # Make the column names the same in both data frames and combine into a single
  #   data frame sorted by matchNumber
  col.names <- c("teamNumber.1", "teamNumber.2", "teamNumber.3", "scoreFinal",
                 "matchNumber")
  names(matches.red) <- col.names
  names(matches.blue) <- col.names
  matches <- rbind(matches.red, matches.blue)
}

GetTeams <- function(matches) {
  teams <- c(matches$teamNumber.1, matches$teamNumber.2, matches$teamNumber.3)
  teams <- sort(unique(teams))
  return(teams)
}


CalcB <- function(matches, teams = NULL) {
  if(is.null(teams)) teams <- getTeams(matches)
  
  B <- vapply(teams,
              function(tm) {
                sum(matches[(matches$teamNumber.1 == tm |
                               matches$teamNumber.2 == tm |
                               matches$teamNumber.3 == tm),
                            "scoreFinal"])
              },
              integer(1))
  names(B) <- as.character(teams)
  return(B)
}


CalcA1 <- function(matches, teams = NULL) {
  if(is.null(teams)) teams <- getTeams(matches)
  
  # Initialize the A matrix to all zeros
  A <- matrix(0, length(teams), length(teams), dimnames = list(teams, teams))
  
  # Create matrix of all single and two-team combinations
  matchups <- cbind(c(1, 1), c(2, 2), c (3,3), combn(1:3, 2, simplify = TRUE))
  
  # Fill in the A matrix
  for(match.num in 1:nrow(matches)) {
    for(matchup in 1:ncol(matchups)) {
      tm.col1 <- paste("teamNumber", matchups[1, matchup], sep = ".")
      tm.col2 <- paste("teamNumber", matchups[2, matchup], sep = ".")
      tm.num1 <- as.character(matches[match.num, tm.col1])
      tm.num2 <- as.character(matches[match.num, tm.col2])
      A[tm.num1, tm.num2] = A[tm.num1, tm.num2] + 1
      if(tm.num1 != tm.num2)
        A[tm.num2, tm.num1] = A[tm.num2, tm.num1] + 1
    }
  }
  return(A)
}

CheckA <- function(A, matches, teams = NULL) {
  if(is.null(teams)) teams <- getTeams(matches)
  
  # Matrix diagonal value equal to number of matches played by that team.
  num.matches <- vapply(teams,
                        function(tm, mtch) {
                          nrow(mtch[(mtch$teamNumber.1 == tm |
                                          mtch$teamNumber.2 == tm |
                                          mtch$teamNumber.3 == tm), ])},
                        FUN.VALUE = integer(1),
                        matches)
  stopifnot(num.matches == diag(A))
  
  # Sums of columns and rows are 3 times number of matches played. 
  stopifnot(colSums(A) == 3 * diag(A))
  stopifnot(rowSums(A) == 3 * diag(A))
  
  # Symmetric and positive definite.
  stopifnot(isSymmetric(A))
  stopifnot(matrixcalc::is.positive.definite(A))
}

The following function can be used to calculate OPRs from data frames returned by firstapiR::GetMatchResults(session, event_code, expand_cols = FALSE. It is used in place of the Shape8colFile function. It accepts one or more MatchResults data frames.

ShapeDf <- function(...) {
  # Combine all arguments into one dataframe, with unique matchNumber fields
  SetMatchNumbers <- function(matches, num) {
    matches$matchNumber <- paste(num, matches$matchNumber, sep = "-")
    return(matches)
  }
  all.matches <- Map(SetMatchNumbers, list(...), 1:length(list(...)))
  matches <- do.call(rbind, all.matches)
  
  # Drop extra columns and split red and blue alliances into separate data frames.
  match.cols <- c("matchNumber", "teamNumber", "station", "scoreFinal")
  matches.blue <- matches[substr(matches$station, 1, 4) == "Blue", match.cols]
  matches.red <- matches[substr(matches$station, 1, 3) == "Red", match.cols]
  
  # Drop "Red" and "Blue" from station values
  matches.blue$station <- with(matches.blue,
                            substr(station, nchar(station), nchar(station)))
  matches.red$station <- with(matches.blue,
                            substr(station, nchar(station), nchar(station)))
  
  # Compress data frames to one row per alliance score, with team numbers in
  #   separate columns.
  matches.blue.wide <- reshape(matches.blue, direction = "wide",
                               idvar = "matchNumber", timevar = "station",
                               v.names = "teamNumber")
  matches.red.wide <- reshape(matches.red, direction = "wide",
                              idvar = "matchNumber", timevar = "station",
                              v.names = "teamNumber")
  
  # Combine all data back into single data frame
  matches <- merge(matches.blue.wide, matches.red.wide, all = TRUE)
  return(matches)
}