Ускорьте тройной вложенный For Loop in R с векторизацией - PullRequest
1 голос
/ 02 мая 2019

Обзор: Ниже приведен важный фрагмент кода R для моего сайта по баскетбольной статистике.На высоком уровне код R преобразует статистику состава, где каждая строка представляет уникальный состав (состав представляет собой комбинацию из 5 игроков, играющих вместе), в статистику включения / выключения, где каждая строка представляет общую статистику команды с определеннымигрок (а) на площадке или (б) вне площадки.

Я чувствовал, что небольшой фрагмент данных не будет работать для этого воспроизводимого примера, и поэтому я загрузилданные в Google Sheet и сделали этот лист общедоступным.Воспроизводимый код захватывает эти данные CSV, но вы также можете легко загрузить файл, посетив URL.

С учетом всего вышесказанного, вот тройной вложенный цикл, с которым я работаю, с которым я 'я сделал все возможное, чтобы прокомментировать:

# Raw Data Is Lineup Data - Each Row contains stats for a single lineup (combination of 5 basketball players)
sheets_url <- 'https://docs.google.com/spreadsheets/d/1GjDbWfZglwdwMwhNemWpX6uWjhmYfpQe-WNcCNE8EK4/export?format=csv&id=1GjDbWfZglwdwMwhNemWpX6uWjhmYfpQe-WNcCNE8EK4&gid=218640693'
raw.lineup.stats <- httr::content(httr::GET(url = sheets_url))

# Will contain the final output
on.off.stats <- c()

all_seasons <- c('1718', '1819')
# Loop each season
for(i in 1:length(all_seasons)) {
  # Filter Lineup Data to include only lineups / stats from this season
  this_season <- all_seasons[i]
  season.lineup.stats <- raw.lineup.stats %>% dplyr::filter(season == this_season)
  all_teams <- unique(season.lineup.stats$teamId)

  # Loop each team that appeared in data for this season
  for(j in 1:length(all_teams)) { 
    # Filter Lineup Data again to include only lineups / stats for this team
    print(paste0(j, ': ', all_teams[j]))
    this_team <- all_teams[j]
    team.season.lineup.stats <- season.lineup.stats %>% dplyr::filter(teamId == this_team)
    players_on_team <- unique(c(team.season.lineup.stats$onCtId1, team.season.lineup.stats$onCtId2, team.season.lineup.stats$onCtId3, team.season.lineup.stats$onCtId4, team.season.lineup.stats$onCtId5))

    # Loop each player on team j
    for(k in 1:length(players_on_team)) {
      # Identify if player is on-court or off-court - is his ID one of the 5
      this_player <- players_on_team[k]
      this.players.teams.lineup.stats <- team.season.lineup.stats %>%
        dplyr::mutate(isOnOrOff = ifelse(onCtId1 == this_player | onCtId2 == this_player | onCtId3 == this_player 
                                         | onCtId4 == this_player | onCtId5 == this_player, 'On Ct', 'Off Ct')) %>%
        dplyr::mutate(playerId = this_player) %>%
        dplyr::select(playerId, isOnOrOff, everything())

      # Convert this team' lineup data into 2 Rows: 1 for team's stats w/ player on-court, and 1 for team's stats w/ player off-court
      this.players.onoff.stats <- this.players.teams.lineup.stats %>%
        dplyr::group_by(playerId, isOnOrOff) %>%
        dplyr::mutate_at(vars(possessions:minutes), .funs = sum) %>%
        dplyr::mutate_at(vars(fieldGoalsMade:oppDefensiveReboundPct), .funs = sum) %>%
        dplyr::filter(!duplicated(isOnOrOff))

      # If player played every minute for his team, nrow(this.players.onoff.stats) == 1. If so, create needed blank off-row
      if(nrow(this.players.onoff.stats) == 1) {
        off.row <- this.players.onoff.stats %>%
          dplyr::ungroup() %>% dplyr::mutate(isOnOrOff = 'Off Ct') %>%
          dplyr::mutate_at(vars(possessions:oppPersonalFoulsPer40), .funs = function(x) return(0)) %>%

          dplyr::group_by(playerId, isOnOrOff)

        this.players.onoff.stats <- this.players.onoff.stats %>% rbind(off.row)
      }

      # And Rbind to the main container
      on.off.stats <- on.off.stats %>% base::rbind(this.players.onoff.stats)
    }
  }
}

Пожалуйста, дайте мне знать, если в этом примере нет ничего воспроизводимого.Сбор данных и циклы for, все работают на моем конце. Поток кода на высоком уровне (это все в комментариях к коду) делает это:

  1. Фильтрация данных очереди за один сезон
  2. Фильтрация данных очередидля одиночной команды
  3. Для каждого игрока в команде добавьте столбец индикатора isOnOrOff, в котором указано, является ли указанный игрок одним из 5 игроков в каждом составе / ряду.
  4. Используйте столбец isOnOrOff с group_byчтобы преобразовать статистику состава команды сезона в статистику включения / выключения для конкретного игрока.
  5. Если игрок играл каждую минуту за свою команду, добавьте пустую строку «off».
  6. rbindСтатистика включения / выключения игрока на выходной фрейм данных.

Следуя комментариям при просмотре кода, мы надеемся прояснить, как происходит преобразование данных из статистики в линейке в статистику включения / выключения.

Текущая скорость / Будущие данные: Что касается текущей скорости, этот цикл занял 1,6 минуты в последний раз, когда я его запускал.Со всей статистикой (я удалил ~ 300 столбцов в данных примера), цикл занимает 3,5 минуты.Это данные о баскетболе в колледже, и в настоящее время я использовал только ~ 40 команд при создании своего сайта.Это скоро изменится до ~ 350 команд, и с этим изменением у каждой команды будет еще ~ 50% больше составов.В целом размер данных увеличится в ~ 15 раз.

Учитывая, что я использую цикл for, я ожидаю как минимум 15-кратного замедления, если не больше (15-кратных циклов, но каждый цикл может работать медленнее с большим общим набором данных) с полным набором данных,Я также должен вызывать этот цикл дважды при каждом запуске кода, а не один раз.В целом, я оцениваю время выполнения в 3,5 * 15 раз больше команд * 2 прогона кода == ~ 105 минут.Это слишком долго.Этот мой код нужно будет запускать ежедневно, и этот тройной цикл for является лишь небольшой частью гораздо большего сценария.

Закрыть: любая помощь в этом очень ценится.Я знаю, что это не самый простой цикл для векторизации, и я планирую увеличить этот пост и любые супер полезные ответы, если это будет необходимо.

РЕДАКТИРОВАТЬ: Быстрый общий доступдумал о моем подходе.Я чувствовал, что должен использовать этот подход с вложенным циклом, потому что очень важный group_by должен быть сделан только для статистики команды.Мне все равно, если игрок вне площадки, если состав для совершенно другой команды / для сезона, когда игрок даже не играл в баскетбол в колледже.

РЕДАКТИРОВАТЬ 2: Если бы я мог просто запустить код внутри цикла j for для i сезонов и j команд одновременно (для каждого i сезона, j команды, определите игроков в этой командеЦикл игроков в команде, вычисление статистики включения / выключения каждого игрока, готово), что, вероятно, сделало бы работу, верно?

1 Ответ

1 голос
/ 02 мая 2019

Вы можете добиться значительного ускорения, используя gather и group_by сводные / агрегатные операции.

Начиная с raw.lineup.stats, вот проход, который должен пройти вас большую часть пути, по крайней мере, грубыми ударами. См. Примечания ниже.

library(tidyverse)

all_seasons <- c('1718', '1819')

# make a list of unique players per team, per season
players <- raw.lineup.stats %>%
  filter(season %in% all_seasons) %>% 
  gather(position, player, starts_with("onCtId")) %>%
  select(season, teamId, player) %>%
  group_by(season, teamId) %>%
  distinct(player, .keep_all = TRUE) %>%
  ungroup()

# cartesian join with the full df
# use lineupId to determine on/off court (on_ct)
# group_by and aggregate, then use distinct to drop duplicate rows
on_off <- inner_join(
    players, raw.lineup.stats, 
    by = c("season" = "season", "teamId" = "teamId")
  ) %>%
  mutate(on_ct = stringr::str_detect(lineupId, player)) %>% 
  group_by(season, teamId, player, on_ct) %>%
  mutate_at(vars(possessions:minutes, fieldGoalsMade:oppDefensiveReboundPct), 
            list(~sum)) %>%
  ungroup() %>%
  distinct(player, on_ct, .keep_all = TRUE) 

Вот несколько тестовых сравнений запуска вашего кода с обновленным кодом:

# new code
> on_off[on_off$teamId == "WVU" & on_off$season == "1819", 
+        c("player", "on_ct", "possessions", "minutes")] %>% 
arrange(player) 
                 player on_ct possessions    minutes
1      AndrewGordon4009  TRUE        86.5  46.133333
2      AndrewGordon4009 FALSE       689.0 374.650000
3    BrandonKnappercbd1  TRUE       225.5 123.233333
4    BrandonKnappercbd1 FALSE       550.0 297.550000
5       ChaseHarler8a7e  TRUE       369.5 201.900000
6       ChaseHarler8a7e FALSE       406.0 218.883333
...

# old code
> on.off.stats[on.off.stats$teamId == "WVU" & on.off.stats$season == "1819", 
c("playerId", "isOnOrOff", "possessions", "minutes")] %>% 
arrange(playerId) 
               playerId isOnOrOff possessions    minutes
1      AndrewGordon4009     On Ct        86.5  46.133333
2      AndrewGordon4009    Off Ct       689.0 374.650000
3    BrandonKnappercbd1     On Ct       225.5 123.233333
4    BrandonKnappercbd1    Off Ct       550.0 297.550000
5       ChaseHarler8a7e     On Ct       369.5 201.900000
6       ChaseHarler8a7e    Off Ct       406.0 218.883333
...

Примечания:

  • Я все еще использую magrittr каналы, потому что я думаю, что это полезно для обхода проблемы (и потому что я думаю, что многие функции обратного хода действительно удобны), но вы можете получить некоторое ускорение, если хотите преобразовать в база Р.
  • Похоже, в вашем коде есть несколько ошибок, которые не связаны с этой операцией ускорения, о которой вы спрашиваете - это усложняло проверку по вашим выводам, так как иногда ваш вывод был неверным. Например, JamesBolden043b играет за команду WVU только в сезоне 1718, в соответствии с raw.lineup.stats, но на вашем финальном выводе on.off.stats он также играет в сезоне 1819. Я также уверен, что ваши команды summarise vs mutate не дают вам именно то, что вы хотите.
  • Если вы хотите получить статистику игрока для площадки вкл / выкл, для каждой конфигурации на площадке на 5 человек, есть дополнительный уровень группировки, с lineupId, который вам нужно сделать. (Это имело больше смысла для меня, когда я просматривал данные, но твой звонок, конечно.)

Я думаю, что осталось изменить синтаксис и поиск ошибок; основная интуиция этого обновления кода поможет вам в этом. Еще одна поправка: вам нужно добавить строки, которые отсутствуют в тех случаях, когда игрок находится на корте 100% времени - но для этого вам также не нужен цикл for.

...