通过向量化加快R中的三重嵌套For循环

时间:2019-05-02 03:10:08

标签: r for-loop vectorization apply

概述:以下是我的篮球统计网站的R代码的重要部分。在较高的层次上,R代码将阵容统计信息转换为开/关统计数据,其中每一行代表一个唯一的阵容(一个阵容是5个玩家一起玩的组合),其中每一行代表团队的整体统计信息,其中包含特定的(a)场上或(b)场外球员。

我觉得一小段数据对这个可重现的示例不起作用,因此我将数据上传到了Google表格中,并公开了该表格。可复制的代码捕获了CSV数据,但是您可以通过访问url来轻松下载文件。

话虽如此,这是我正在使用的三重嵌套for循环,我已尽力清楚地发表了看法:

# 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. 如果玩家每分钟为球队打球,请添加空白的“关闭”行。
  6. 将播放器的开/关统计信息绑定到输出数据框中。

在检查代码时遵循注释将希望清楚代码将如何将数据从阵容统计转换为开/关统计。

当前速度/将来的数据:就当前速度而言,上一次运行此循环耗时1.6分钟。使用所有统计信息(我在示例数据中删除了约300列),循环需要3.5分钟。这是大学篮球数据,目前我在建立网站时只使用了约40个团队。很快将更改为约350个团队,并且随着这一更改,每个团队将增加约50%的阵容。总体而言,数据大小将增加约15倍。

鉴于我使用的是for循环,我希望整个数据集至少可以减慢15倍(如果不是更多的话)(15循环,但每个循环在使用较大的整体数据集时可能会变慢)。我还需要每次运行代码两次而不是一次调用此循环。总的来说,我估计未来的运行时间为3.5 *更多的团队15倍* 2次代码运行==〜105分钟。这太长了。我的代码必须每天运行,而这个三重for循环只是更大脚本中的一小部分。

关闭:非常感谢您提供的任何帮助。我知道这不是最简单的for循环矢量化处理,因此我计划奖励这篇文章以及任何需要的超级有用答案。

编辑:关于我的方法的快速分享想法。我觉得我必须使用这种嵌套的for-loop方法,因为非常重要的group_by必须仅在团队的阵容统计上完成。我不在乎球员是否在场外,如果阵容是完全不同的球队/这个球员甚至没有参加过大学篮球的赛季。

编辑2:如果我可以简单地在j季节和i团队中同时在j for循环内运行代码( i的每个j团队,确定该团队中的球员,循环该团队中的球员,计算每个球员的开/关统计数据,完成),这很可能会完成工作,对吧?

1 个答案:

答案 0 :(得分:1)

通过利用gathergroup_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管道,因为我认为它有助于解决问题(并且因为我认为很多tidyverse函数确实很方便),但是如果您愿意,可以加快速度转换为基数R。
  • 似乎您的代码中存在一些错误,这些错误与您要进行的加速操作无关-这使您难以对照输出进行验证,因为有时您的输出是错误的。例如,根据JamesBolden043bWVU仅在1718赛季为球队raw.lineup.stats效力,但是您的on.off.stats最终输出是他在{{1 }}。我也很确定您的1819summarise命令没有完全给您您想要的东西。
  • 如果您想为场上/场下的球员统计数据,按照每5人一次的场上配置,您需要做一个额外的分组,mutate。 (当我查看数据时,这对我来说更有意义,但当然是您的电话。)

我认为剩下的就是语法调整和错误查找。此代码更新背后的主要直觉应该可以助您一臂之力。另一项调整:在球员有100%的情况下在场上的情况下,您需要添加缺少的行-但您也不需要for循环。