Улучшение производительности плохих / потенциально ненужных Применить в R - PullRequest
0 голосов
/ 03 мая 2019

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

Обзор: У меня есть данные о баскетболе, где каждый ряд является моментом в баскетбольном матче и включает в себя 10 игроков на площадке, их команды, игру, а также сколько минут в игре.игра (1 - 40), в которой находится ряд.Используя эти данные, я рассчитываю для каждого игрока процент игр своей команды, которые они провели на площадке в течение 1–40 минут.

Например, если команда Джо сыграла 20 игр, иесли в 13 из этих игр Джо был замечен в данных на 5-й минуте игры, то мы бы сказали, что Джо был замечен на корте на 5-й минуте 65% игр его команды.Я вычисляю это для каждого игрока, для каждого сезона, для каждой из 1-40 минут, в моих не очень маленьких данных, и сталкиваюсь с проблемами производительности.Вот функция, которую я в настоящее время имею для этого:

library(dplyr)

# Raw Data Is Play-By-Play Data - Each Row contains stats for a pl (combination of 5 basketball players)
sheets_url <- 'https://docs.google.com/spreadsheets/d/1xmzaF6tpzVpjOmgfwHwFM_JE8LUszofjj25A5P0P21o/export?format=csv&id=1xmzaF6tpzVpjOmgfwHwFM_JE8LUszofjj25A5P0P21o&gid=630752085'
on.ct.data <- httr::content(httr::GET(url = sheets_url))

computeOnCourtByMinutePcts <- function(on.ct.data) {

  # Create Dataframe With Number Of Games Played By Team Each Season
  num.home.team.games <- on.ct.data %>%
    dplyr::group_by(homeTeamId, season) %>%
    dplyr::summarise(count = length(unique(gameId)))

  num.away.team.games <- on.ct.data %>%
    dplyr::group_by(awayTeamId, season) %>%
    dplyr::summarise(count = length(unique(gameId)))

  num.team.games <- num.home.team.games %>%
    dplyr::full_join(num.away.team.games, by = c('homeTeamId'='awayTeamId', 'season'='season')) %>%
    dplyr::mutate(gamesPlayed = rowSums(cbind(count.x, count.y), na.rm = TRUE)) %>%
    dplyr::rename(teamId = homeTeamId) %>%
    dplyr::mutate(season = as.character(season)) %>%
    dplyr::select(teamId, season, gamesPlayed)

  # Create Dataframe With Players By Season - Seems kind of bulky as well
  all.player.season.apperances <- rbind(
    on.ct.data %>% dplyr::select(homeTeamId, onCtHomeId1, season) %>% dplyr::rename(playerId = onCtHomeId1, teamId = homeTeamId),
    on.ct.data %>% dplyr::select(homeTeamId, onCtHomeId2, season) %>% dplyr::rename(playerId = onCtHomeId2, teamId = homeTeamId),
    on.ct.data %>% dplyr::select(homeTeamId, onCtHomeId3, season) %>% dplyr::rename(playerId = onCtHomeId3, teamId = homeTeamId),
    on.ct.data %>% dplyr::select(homeTeamId, onCtHomeId4, season) %>% dplyr::rename(playerId = onCtHomeId4, teamId = homeTeamId),
    on.ct.data %>% dplyr::select(homeTeamId, onCtHomeId5, season) %>% dplyr::rename(playerId = onCtHomeId5, teamId = homeTeamId),
    on.ct.data %>% dplyr::select(awayTeamId, onCtAwayId1, season) %>% dplyr::rename(playerId = onCtAwayId1, teamId = awayTeamId),
    on.ct.data %>% dplyr::select(awayTeamId, onCtAwayId2, season) %>% dplyr::rename(playerId = onCtAwayId2, teamId = awayTeamId),
    on.ct.data %>% dplyr::select(awayTeamId, onCtAwayId3, season) %>% dplyr::rename(playerId = onCtAwayId3, teamId = awayTeamId),
    on.ct.data %>% dplyr::select(awayTeamId, onCtAwayId4, season) %>% dplyr::rename(playerId = onCtAwayId4, teamId = awayTeamId),
    on.ct.data %>% dplyr::select(awayTeamId, onCtAwayId5, season) %>% dplyr::rename(playerId = onCtAwayId5, teamId = awayTeamId)) %>%
    dplyr::distinct(teamId, playerId, season) %>%
    dplyr::filter(!is.na(playerId))

  # For Each Player-Season, Compute Number Of Games On Court at each minute in game - this is the bad Apply
  playing.time.breakdowns <- apply(X = all.player.season.apperances, MARGIN = 1, FUN = function(thisRow) {

    # Set Player / Season Variables
    thisPlayerId = thisRow[2]
    thisSeason = thisRow[3]

    # Filter for each unique minute of each game with this player on court
    on.court.df = on.ct.data %>% 
      dplyr::filter(onCtHomeId1 == thisPlayerId | onCtHomeId2 == thisPlayerId | onCtHomeId3 == thisPlayerId | onCtHomeId4 == thisPlayerId | onCtHomeId5 == thisPlayerId |
                      onCtAwayId1 == thisPlayerId | onCtAwayId2 == thisPlayerId | onCtAwayId3 == thisPlayerId | onCtAwayId4 == thisPlayerId | onCtAwayId5 == thisPlayerId) %>%
      dplyr::filter(season == thisSeason) %>%
      dplyr::filter(!duplicated(paste0(gameId, minNumIntoGame)))

    # Turn This Into a table of minutes on court by game
    thisTable <- table(on.court.df$minNumIntoGame)

    this.player.distrubution.df <- data.frame(
      playerId = thisRow[2],
      teamId = thisRow[1],
      season = thisRow[3],
      minNumIntoGame = as.integer(names(thisTable)),
      numGamesAtMinNum = unname(thisTable) %>% as.vector(),
      stringsAsFactors = FALSE
    )

    # 40 minutes in basketball game, so previous dataframe needs 40 rows
    if(length(which(!(1:40 %in% this.player.distrubution.df$minNumIntoGame))) > 0) {
      zero.mins.played.df <- data.frame(
        playerId = thisRow[2],
        teamId = thisRow[1],
        season = thisRow[3],
        minNumIntoGame = which(!(1:40 %in% this.player.distrubution.df$minNumIntoGame)),
        numGamesAtMinNum = 0,
        stringsAsFactors = FALSE
      )

      this.player.distrubution.df <- plyr::rbind.fill(this.player.distrubution.df, zero.mins.played.df) %>% dplyr::arrange(minNumIntoGame)
    }

    # and return
    return(this.player.distrubution.df)
  })

  # Combine the output into one dataframe
  playing.time.breakdowns <- playing.time.breakdowns %>% do.call("rbind", .)

  # Join on Team-Games played
  playing.time.breakdowns <- playing.time.breakdowns %>%
    dplyr::left_join(num.team.games, by = c("teamId"="teamId", "season"="season")) %>%
    dplyr::rename(teamGamesPlayed = gamesPlayed)

  # Compute pct of games played
  playing.time.breakdowns <- playing.time.breakdowns %>%
    dplyr::mutate(pctMinNumPlayed = round(numGamesAtMinNum / teamGamesPlayed, 3))

  # Handle OT (minNumIntoGame > 40) needs a lower gamesPlayed denominator...

  # And Return
  return(playing.time.breakdowns);
}
on.ct.by.min <- computeOnCourtByMinutePcts(on.ct.data)

Таким образом, код выполняет следующее:

  1. Создает начальные кадры данных всех уникальных сезонов игроков и командныхсезоны.Для командных сезонов используйте данные pbp для расчета сыгранных игр.
  2. Применить - для каждого игрового сезона: (a) найдите каждый экземпляр игрока на площадке (в одном из столбцов 10 onCt)) для каждой минуты каждой игры, (b) конвертируйте это в таблицу, которая показывает количество игр, в которых игрок находился на корте в каждую из 1-40 минут.
  3. Польский подъем и возврат.Соедините несколько таблиц вместе и вычислите соответствующие проценты.

Обратите внимание, что может быть проще следовать функции apply, запустив ее вручную для одной строки all.player.season.appearances.Установите thisRow на любую строку в кадре данных и постройте код построчно для большей ясности.

Чтобы выделить проблемы с медленным кодом, я загрузил большой кусок play-by-play / on-Сортируйте данные на листы Google, сделайте их общедоступными и включите ссылку для загрузки данных в приведенном выше коде.В Google Sheets хранится ~ 1/2 моих текущих данных, однако, как ожидается, общий объем данных в ближайшем будущем увеличится в 10 раз, и в настоящее время выполнение кода на моем компьютере занимает ~ 8 минут.Это сценарий, который нужно запускать ежедневно и довольно быстро, и я не могу позволить, чтобы эта одна функция занимала 80 минут.

Такое ощущение, что мой apply() вызов выполнен неправильно, как будто его нетбыстрее, чем обычный цикл.Я не уверен, что подать заявку нужно вообще, и на самом деле, я не думаю, что это так.Но последние 24 часа я боролась с мыслями о том, как улучшить эту функцию, но не повезло.Здесь должен быть лучший подход!

Редактировать: У меня есть небольшая ошибка в воспроизводимом примере, над которым я сейчас работаю.Edit2: исправлена ​​ошибка, из-за которой создавались NA в кадре данных num.team.games.Я просто запустил код, и он, кажется, работает правильно.Есть ~ 600 строк вывода, где teamId равен NA, и не о чем беспокоиться.

Edit3: Похоже, что каждая итерация применения занимает 0,06 секунды, а количество 5312строк в кадре данных, что составляет ~ 8 минут времени выполнения.Должен ли я пытаться уменьшить это 0,06 до <0,01 или отказаться от всего этого подхода?Это основной вопрос, в котором я не уверен ... </p>

1 Ответ

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

Я думаю, что к этому можно подойти проще путем преобразования данных в длинную форму и подсчета комбинаций игрок-минута-команда-сезон. (Это займет около 5 секунд, чтобы запустить на этом старом компьютере с 2008 года, и это большая часть расчета.)

library(tidyverse)
on.ct.data %>%
  gather(spot, name, onCtHomeId1:onCtAwayId5) %>%
  mutate(team = if_else(spot %>% str_detect("Away"),
                        awayTeamId, homeTeamId)) %>%
  select(-spot) %>%  # For this part, I only care about person and minute of game.
  distinct() %>%  # Drop dupes and instances where they were repositioned within one minute.
  drop_na()  %>%
  select(-c(gameId:awayTeamId)) %>%
  count(minNumIntoGame, name, team, season)

# A tibble: 140,581 x 5
   minNumIntoGame name              team  season     n
            <dbl> <chr>             <chr>  <dbl> <int>
 1              1 AahmaneSantos387c JAC     1819     1
 2              1 AamirSimmseef9    CLEM    1819    13
 3              1 AarenEdmead9cd6   NCAT    1718     1
 4              1 AarenEdmead9cd6   NCAT    1819     1
 5              1 AaronBrennanbee2  IUPU    1718     1
 6              1 AaronCalixtea11d  OKLA    1819    11
 7              1 AaronCarver9cfa   ODU     1819     2
 8              1 AaronClarke3d67   SHU     1819     1
 9              1 AaronFalzon213b   NW      1718     1
10              1 AaronHolidayfce6  UCLA    1718    11

Теперь, когда у нас это есть, мы можем проверить, как выглядит наша игровая вселенная для каждой команды. В скольких играх каждого сезона каждая команда играла определенную минуту?

on.ct.data.team.minutes <- on.ct.data.minute.counts %>%
  count(season, team, minNumIntoGame, gameId) %>%  
  count(season, team, minNumIntoGame) 

ggplot(on.ct.data.team.minutes %>% slice(1:1000),
       aes(minNumIntoGame, team, fill = n)) + 
  geom_tile() + facet_wrap(~season) + 
  labs(title = "# times each team played each minute (excerpt)")

enter image description here

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

# How many games each season did each player play a given minute for each team?
on.ct.data.player.minutes <- on.ct.data.minute.counts %>%
  count(season, team, name, minNumIntoGame) %>%
  rename(player_n = n) %>%
  left_join(on.ct.data.team.minutes) %>%
  rename(team_n = n) %>% 
  mutate(player_time = player_n / team_n)

ggplot(on.ct.data.player.minutes %>% filter(name %>% str_detect("Can")),
       aes(minNumIntoGame, player_time, color = name)) +
  geom_line() + facet_wrap(~season) +
  scale_y_continuous(labels = scales::percent_format(accuracy = 1))

enter image description here

...