通过自动化生成大量变量(具有特定模式)

编程语言 2026-07-09

我有一个脚本,在一周内的若干天里,当温度超过某个阈值时,会生成指示变量(例如,连续两天或以上超过温度的第90百分位数)。

temp <- temp %>%
  mutate(
    warm=if_else(month < 3 | month > 8, 1, 0),
    per_warm=case_when(
      warm == 0 ~ 0,
      warm == 1 & month < 3 ~ year - 1991,
      warm == 1 & month >= 9 ~ year - 1990
    )
  )

## 2 consecutive days over the 90th percentile
temp <- temp %>%
  group_by(per_warm, comuna) %>%
  mutate(
    p90=round(as.numeric(quantile(tmax, 90/100, na.rm=TRUE)), 1)
    ) %>%
  ungroup()

temp <- temp %>%
  arrange(comuna, year, month, day)

## Heat day: tmax > percentile threshold
temp <- temp %>%
  mutate(
    hd90=if_else(tmax > p90, 1, 0),
    cd90=if_else(tmax > p90, 1, 0)
  )

## Consecutive days
temp <- temp %>%
  arrange(comuna, year, month, day) %>%
  group_by(comuna) %>%
  mutate(
    cd90=fcumsum(x=hd90, g=fcumsum(!hd90)),
    cd90=if_else(warm == 0, 0, cd90),
    hd90=if_else(warm == 0, 0, hd90)
  ) %>%
  ungroup()

## Heat week
## In the past 7 days, did the pregnant woman have at least 3
## consecutive days over p90?
## Is the delivery day at least the third consecutive day over p90?
## Important: does this 7-day window include the present day?

## Number of consecutive days over threshold
last <- 2

temp <- temp %>%
  group_by(comuna) %>%
  mutate(hk90=if_else(cd90 >= last, 1, 0)) %>%
  ungroup()

temp <- temp %>%
  group_by(comuna) %>%
  mutate(hk90=if_else(lag(cd90, 1, default=0) >= last, 1, hk90)) %>%
  ungroup()

temp <- temp %>%
  group_by(comuna) %>%
  mutate(hk90=if_else(lag(cd90, 2, default=0) >= last, 1, hk90)) %>%
  ungroup()

temp <- temp %>%
  group_by(comuna) %>%
  mutate(hk90=if_else(lag(cd90, 3, default=0) >= last, 1, hk90)) %>%
  ungroup()

temp <- temp %>%
  group_by(comuna) %>%
  mutate(hk90=if_else(lag(cd90, 4, default=0) >= last, 1, hk90)) %>%
  ungroup()

## Same for p95 and p98
#...

现在的问题在于,当我尝试修改参数时会出现问题(把 tmax 改为 tmean,或者把 2 天改为 3,或者把每周的计数天数改为1天)。

我可以把脚本复制粘贴多次,改动其中的具体部分,然后运行,以获得所有组合,总共18种:2个温度、计数天数为1到3天、阈值对应3个百分位数。我的想法是得到18个变量,如此排列:“tmax_p90_l1 tmax_p90_l2 tmax_p90_l3 .... tmean_p98_l1 tmean_p98_l2 tmean_p98_l3”。

但我完全相信,这可以用一种“优雅”和“高效”的方式来完成。

有什么建议?

(我编辑了帖子,使之更容易理解)

解决方案

这是一个基于 data.table 的函数,能够实现这一点,按照评论中建议的运行长度编码来实现。

library(data.table)

f_dt <- \(temp, last=2, tx='tmax', qs=c(0.90, 0.95, 0.98), round_p=FALSE) {
  qs <- setNames(qs, qs*100)
  qn <- names(qs)
  p_cols <- paste0("p", qn)
  hd_cols <- paste0("hd", qn)
  cd_cols <- paste0("cd", qn)
  hk_cols <- paste0("hk", qn)
  base_cols <- names(temp)
  heat_week <- \(cd, last) {
    z <- cd >= last
    as.integer(Reduce(`|`, shift(z, 0:4, fill=FALSE)))
  }
  dt <- as.data.table(temp)
  dt[, warm:=as.integer(month < 3 | month > 8)]
  dt[, per_warm:=0L]
  dt[warm == 1L & month < 3, per_warm:=year - 1991L]
  dt[warm == 1L & month >= 9, per_warm:=year - 1990L]
  setorder(dt, comuna, year, month, day)
  if (round_p) {  ## rounding here (not ideal; kept for compatibility)
    dt[, (p_cols) := as.list(
      round(
        unname(stats::quantile(get(tx), qs, na.rm=TRUE)),
        1
      )
    ), by=.(per_warm, comuna)]
  } else {
    dt[, (p_cols) := as.list(
      unname(stats::quantile(get(tx), qs, na.rm=TRUE))
    ), by=.(per_warm, comuna)]
  }
  for (j in seq_along(qn)) {
    p_col <- p_cols[j]
    hd_col <- hd_cols[j]
    cd_col <- cd_cols[j]
    hk_col <- hk_cols[j]
    dt[, (hd_col):=as.integer(get(tx) > get(p_col))]
    dt[, (cd_col):={
      x <- get(hd_col)
      rid <- rleid(x)
      cd <- sequence(tabulate(rid))
      cd[x == 0L] <- 0L
      cd
    }, by=comuna]
    dt[warm == 0L, c(hd_col, cd_col):=0L]
    dt[, (hk_col):=heat_week(get(cd_col), last), by=comuna]
  }
  setcolorder(dt, c(
    base_cols, "warm", "per_warm",
    p_cols, hd_cols, cd_cols, hk_cols
  ))
  dt
}

用法

> f_dt(temp) |> head(3)  ## using defaults
   comuna  year month   day     tmax    tmean  warm per_warm      p90      p95      p98  hd90  hd95  hd98  cd90  cd95  cd98  hk90  hk95  hk98
   <char> <int> <int> <int>    <num>    <num> <int>    <int>    <num>    <num>    <num> <int> <int> <int> <int> <int> <int> <int> <int> <int>
1:      a  1990     1     1 27.37383 17.15873     1       -1 31.57008 33.46768 35.40864     0     0     0     0     0     0     0     0     0
2:      a  1990     1     1 18.29882 21.79981     1       -1 31.57008 33.46768 35.40864     0     0     0     0     0     0     0     0     0
3:      a  1990     1     1 30.20806 19.31374     1       -1 31.57008 33.46768 35.40864     0     0     0     0     0     0     0     0     0
> f_dt(temp, last=3, tx='tmean', qs=c(0.75, 0.90)) |> head(3)  ## customize
   comuna  year month   day     tmax    tmean  warm per_warm      p75      p90  hd75  hd90  cd75  cd90  hk75  hk90
   <char> <int> <int> <int>    <num>    <num> <int>    <int>    <num>    <num> <int> <int> <int> <int> <int> <int>
1:      a  1990     1     1 27.37383 17.15873     1       -1 21.39404 24.57328     0     0     0     0     0     0
2:      a  1990     1     1 18.29882 21.79981     1       -1 21.39404 24.57328     1     0     1     0     0     0
3:      a  1990     1     1 30.20806 19.31374     1       -1 21.39404 24.57328     0     0     0     0     0     0

检查

> temp_OP <- f_OP(temp)  ## your code
> temp_dt <- f_dt(temp, round_p=TRUE)
> 
> stopifnot(all.equal(
+   as.data.frame(temp_dt),
+   as.data.frame(temp_OP[, sub("\\.", "", names(temp_dt))])
+ ))
> 
> microbenchmark::microbenchmark(
+   f_OP(temp),
+   f_dt(temp),
+   times=10L
+ )
Unit: milliseconds
       expr      min       lq     mean   median       uq      max neval cld
 f_OP(temp) 400.6320 423.1116 521.9866 514.2539 621.2008 660.8288    10  a 
 f_dt(temp) 168.6245 178.8905 184.7490 184.2289 194.0279 195.9787    10   b

它的执行速度也略快一些。


数据:

set.seed(42)

n <- 1e6

temp <- data.frame(
  comuna=sample(letters[1:5], n, replace=TRUE),
  year=sample(1990:1995, n, replace=TRUE),
  month=sample(1:12, n, replace=TRUE),
  day=sample(1:28, n, replace=TRUE),
  tmax=rnorm(n, mean=25, sd=5),
  tmean=rnorm(n, mean=18, sd=5)
)
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章