如何在apply()的自定义函数中使用dplyr::pull()按列名引用变量?
问题描述
我正在用apply()函数遍历数据框的每一行,对不同列执行特定操作,但希望能引用原始列名(而非基于索引子集)并保持变量的原始类型,偏好使用dplyr的语法。
背景与数据集
我有两个数据集:
- 广播电视广告投放数据,用于分析广告投放前10分钟的时段:
spot_DMA spot_time before_spot 1 LOS ANGELES 2022-01-31 10:02:00 2022-01-31 09:52:00 2 NEW YORK 2022-02-01 14:22:00 2022-02-01 14:12:00 3 ATLANTA 2022-02-02 08:20:00 2022-02-02 08:10:00 4 AUSTIN 2022-02-03 03:16:00 2022-02-03 03:06:00
生成代码:
spot.time <- c("2022-01-31 10:02:00", "2022-02-01 14:22:00", "2022-02-02 08:20:00", "2022-02-03 03:16:00") spot.DMA <- c("LOS ANGELES", "NEW YORK", "ATLANTA", "AUSTIN") time_before <- 10 spot_df <- data.frame(spot_DMA = spot.DMA, spot_time = spot.time) %>% mutate(spot_time = as.POSIXct(spot_time)) %>% mutate(before_spot = spot_time - minutes(time_before)) # 计算广告投放前10分钟的时间点
- 网站流量数据,用于分析广告投放前后的会话数:
web_time web_DMA web_sessions 1 2022-01-31 09:55:00 LOS ANGELES 2 2 2022-01-31 10:15:00 LOS ANGELES 5 3 2022-01-31 10:18:00 LOS ANGELES 3 4 2022-01-31 10:20:00 LOS ANGELES 5 5 2022-01-31 10:24:00 LOS ANGELES 4 6 2022-01-31 10:28:00 LOS ANGELES 4 7 2022-02-02 08:11:00 ATLANTA 6 8 2022-02-02 08:22:00 ATLANTA 7 9 2022-02-02 08:23:00 ATLANTA 8 10 2022-02-02 08:42:00 ATLANTA 4 11 2022-02-02 08:43:00 ATLANTA 3 12 2022-02-02 08:45:00 ATLANTA 1
生成代码:
web.time <- c("2022-01-31 09:55:00", "2022-01-31 10:15:00", "2022-01-31 10:18:00", "2022-01-31 10:20:00", "2022-01-31 10:24:00", "2022-01-31 10:28:00", "2022-02-02 08:11:00", "2022-02-02 08:22:00", "2022-02-02 08:23:00", "2022-02-02 08:42:00", "2022-02-02 08:43:00", "2022-02-02 08:45:00") web.DMA <- c(rep("LOS ANGELES", 6), rep("ATLANTA", 6)) web.sessions <- c(2, 5, 3, 5, 4, 4, 6, 7, 8, 4, 3, 1) web_df <- data.frame(web_time = web.time, web_DMA = web.DMA, web_sessions = web.sessions) %>% mutate(web_time = as.POSIXct(web_time))
当前可行但不满意的实现(用列索引)
通过列索引提取值可以正常运行,但无法使用列名:
# 此代码可行,但依赖列索引 spot_time_function_subset <- function(df){ spot_dma_extract <- df[[1]] spot_time_extract <- df[[2]] spot_before_extract <- df[[3]] web_before <- web_df %>% # 筛选当前DMA的网站会话 filter(web_DMA == spot_dma_extract) %>% # 筛选广告投放前10分钟到投放时的会话 filter(between(web_time, as.POSIXct(spot_before_extract), as.POSIXct(spot_time_extract))) %>% # 求和会话数 summarise(Total = sum(web_sessions)) %>% pull(Total) return(web_before/time_before) # 计算每分钟平均会话数 } web_session_avg <- apply(spot_df, 1, spot_time_function_subset) spot_df_bind <- bind_cols(spot_df, web_session_avg)
尝试的写法(无法与apply配合)
希望传递列名给函数,但apply()无法正常工作,因为函数会拿到整列而非单行值:
# 此语法无法与apply()配合 spot_time_function_pull <- function(df, spot_DMA, spot_time, before_spot){ spot_dma_extract <- df %>% pull({{spot_DMA}}) spot_time_extract <- df %>% pull({{spot_time}}) spot_before_extract <- df %>% pull({{before_spot}}) # 此处会出错:上面三个变量是整列数据,而非单行值 web_before <- web_df %>% # 筛选当前DMA的网站会话 filter(web_DMA == spot_dma_extract) %>% # 筛选广告投放前10分钟到投放时的会话 filter(between(web_time, as.POSIXct(spot_before_extract), as.POSIXct(spot_time_extract))) %>% # 求和会话数 summarise(Total = sum(web_sessions)) %>% pull(Total) return(web_before/time_before) # 计算每分钟平均会话数 } spot_time_function_pull(spot_df, spot_DMA, spot_time, before_spot) # apply无法正常运行 apply(spot_df, 1, spot_time_function_pull)
核心需求
希望用apply()遍历每行时,能引用原始列名并保持变量类型,偏好dplyr语法,是否可行?
解决方案
首先明确:apply()按行处理数据框时,会把每行转换成原子向量,丢失列名和原始类型(比如POSIXct时间会被转成字符),所以直接用apply()很难实现你要的效果。更推荐用dplyr或purrr的工具,既保留列名和类型,又符合tidyverse风格:
方案1:用dplyr::rowwise() + mutate()
这是最符合dplyr习惯的写法,直接按行处理,保留所有列的类型和名称:
library(dplyr) spot_df_result <- spot_df %>% rowwise() %>% mutate( web_session_avg = { # 直接引用当前行的列名,无需额外转换 web_df_sub <- web_df %>% filter(web_DMA == spot_DMA, between(web_time, before_spot, spot_time)) %>% summarise(total = sum(web_sessions, na.rm = TRUE)) %>% pull(total) web_df_sub / time_before } ) %>% ungroup() # 取消按行分组,避免后续操作受影响 print(spot_df_result)
方案2:用purrr::pmap()
pmap可以按行传递列作为参数,完美匹配你想要的函数写法:
library(purrr) library(dplyr) # 定义接收单行参数的函数,直接用列名作为参数名 calc_avg_sessions <- function(spot_DMA, spot_time, before_spot) { web_df %>% filter(web_DMA == spot_DMA, between(web_time, before_spot, spot_time)) %>% summarise(total = sum(web_sessions, na.rm = TRUE)) %>% pull(total) %>% `/`(time_before) } # 按行传递列参数,pmap_dbl确保返回数值向量 spot_df_result <- spot_df %>% mutate(web_session_avg = pmap_dbl(., calc_avg_sessions)) print(spot_df_result)
为什么你的apply()写法不行?
apply(spot_df, 1, fun)会把每行转成一个没有列名的向量,并且所有元素类型会被统一(比如POSIXct时间会变成字符)。当你把这个向量传给函数时,函数无法识别列名,还要手动转换类型,完全失去了用列名的意义。
如果一定要用apply()(不推荐),可以手动给行向量命名并转换类型,但步骤繁琐:
spot_time_function_apply <- function(row) { # 给行向量命名,匹配原数据框列名 names(row) <- colnames(spot_df) # 手动转换类型 spot_dma <- row["spot_DMA"] spot_time <- as.POSIXct(row["spot_time"]) before_spot <- as.POSIXct(row["before_spot"]) web_before <- web_df %>% filter(web_DMA == spot_dma, between(web_time, before_spot, spot_time)) %>% summarise(Total = sum(web_sessions)) %>% pull(Total) return(web_before/time_before) } web_session_avg <- apply(spot_df, 1, spot_time_function_apply) spot_df_bind <- bind_cols(spot_df, web_session_avg)
显然,前两种方案更简洁,也符合tidyverse的设计理念,完全满足你用列名、保留类型的需求。
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

