【问题标题】:Draw random day of the month depending on the month of the year根据一年中的月份随机抽取月份中的某一天
【发布时间】:2022-02-23 23:28:38
【问题描述】:

我想根据一年中的月份随机生成月份中的某一天。我当前的代码是:

  df$new_day = case_when(
  df$new_month == 2 ~ (floor(runif(1, min=1, max=28))),
  df$new_month == 1 ~ floor(runif(1, min=1, max=31)),
  df$new_month == 3 ~ floor(runif(1, min=1, max=31)),
  df$new_month == 5 ~ floor(runif(1, min=1, max=31)),
  df$new_month == 7 ~ floor(runif(1, min=1, max=31)),
  df$new_month == 8 ~ floor(runif(1, min=1, max=31)),
  df$new_month == 10 ~ floor(runif(1, min=1, max=31)),
  df$new_month == 12 ~ floor(runif(1, min=1, max=31)),
  TRUE ~ floor(runif(1, min=1, max=30))
)

但是,我在给定月份的每一天都是一样的。例如,2 月的所有日期都是 23 日。

如何才能真正随机化每个月内的日期?

【问题讨论】:

    标签: r date random


    【解决方案1】:

    您每次都明确调用 1 个随机数:runif(1, ...)。相反,请使用runif(n(), ...)。意识到它不是为每一行调用一次,而是为满足该条件的所有行运行一次。在下面的示例中,5 月份有三行,但 runif 被称为 runif(1,..),并且该单个数字应用于所有三行。

    样本数据:

    set.seed(42)
    df <- data.frame(day = as.Date("2022-01-01") + sample(364, size=10)) %>%
      arrange(day) %>%
      mutate(month = as.POSIXlt(day)$mon + 1L)
    df
    #           day month
    # 1  2022-02-19     2
    # 2  2022-03-16     3
    # 3  2022-05-03     5
    # 4  2022-05-09     5
    # 5  2022-05-27     5
    # 6  2022-06-03     6
    # 7  2022-08-17     8
    # 8  2022-10-31    10
    # 9  2022-11-18    11
    # 10 2022-12-31    12
    

    破碎:

    library(dplyr)
    set.seed(42)
    df %>%
      mutate(
        new_day = case_when(
          month == 2                ~ floor(runif(1, 1, 28)), 
          month %in% c(9, 4, 6, 11) ~ floor(runif(1, 1, 30)), 
          TRUE                      ~ floor(runif(1, 1, 31))
        )
      )
    #           day month new_day
    # 1  2022-02-19     2      25
    # 2  2022-03-16     3       9
    # 3  2022-05-03     5       9
    # 4  2022-05-09     5       9
    # 5  2022-05-27     5       9
    # 6  2022-06-03     6      28
    # 7  2022-08-17     8       9
    # 8  2022-10-31    10       9
    # 9  2022-11-18    11      28
    # 10 2022-12-31    12       9
    

    为了证明runif 被调用一次 对于满足每个条件的所有行,我将添加message 到每个。如果我们可以依赖runif(1,..),那么我们应该会看到"30d" 打印到控制台7 次和"31d" 两次,但我们没有。

    set.seed(42)
    df %>%
      mutate(
        new_day = case_when(
          month == 2                ~ { message("Feb: ", length(month)); floor(runif(1, 1, 28)); }, 
          month %in% c(9, 4, 6, 11) ~ { message("30d: ", length(month)); floor(runif(1, 1, 30)); }, 
          TRUE                      ~ { message("31d: ", length(month)); floor(runif(1, 1, 31)); }
        )
      )
    # Feb: 10
    # 30d: 10
    # 31d: 10
    #           day month new_day
    # 1  2022-02-19     2      25
    # 2  2022-03-16     3       9
    # 3  2022-05-03     5       9
    # 4  2022-05-09     5       9
    # 5  2022-05-27     5       9
    # 6  2022-06-03     6      28
    # 7  2022-08-17     8       9
    # 8  2022-10-31    10       9
    # 9  2022-11-18    11      28
    # 10 2022-12-31    12       9
    
    

    这表明,当我们在其中一个条件的 RHS 内时,它是对框架所有行的调用。请注意,每次我们调用runif,它都会看到month所有 值(df 中有 10 行)。


    改为使用n()(每次调用的行数):

    set.seed(42)
    df %>%
      mutate(
        new_day = case_when(
          month == 2                ~ floor(runif(n(), 1, 28)), 
          month %in% c(9, 4, 6, 11) ~ floor(runif(n(), 1, 30)), 
          TRUE                      ~ floor(runif(n(), 1, 31))
        )
      )
    #           day month new_day
    # 1  2022-02-19     2      25
    # 2  2022-03-16     3       5
    # 3  2022-05-03     5      30
    # 4  2022-05-09     5      29
    # 5  2022-05-27     5       3
    # 6  2022-06-03     6      28
    # 7  2022-08-17     8      12
    # 8  2022-10-31    10      28
    # 9  2022-11-18    11      14
    # 10 2022-12-31    12      26
    

    这意味着我们在case_when 中抽取 30 个随机数,每个条件抽取 10 个。虽然这不是问题这里(大量提取熵可能会很慢),但您可以通过预先提取随机数据然后相应地进行缩放来缓解。

    set.seed(42)
    df %>%
      mutate(
        rand = runif(n(), 0, 1),
        new_day = case_when(
          month == 2                ~ ceiling(rand*28), 
          month %in% c(9, 4, 6, 11) ~ ceiling(rand*30), 
          TRUE                      ~ ceiling(rand*31)
        )
      )
    #           day month      rand new_day
    # 1  2022-02-19     2 0.9148060      26
    # 2  2022-03-16     3 0.9370754      30
    # 3  2022-05-03     5 0.2861395       9
    # 4  2022-05-09     5 0.8304476      26
    # 5  2022-05-27     5 0.6417455      20
    # 6  2022-06-03     6 0.5190959      16
    # 7  2022-08-17     8 0.7365883      23
    # 8  2022-10-31    10 0.1346666       5
    # 9  2022-11-18    11 0.6569923      20
    # 10 2022-12-31    12 0.7050648      22
    

    (注意从floorceiling 的转变)。代码可以通过其他方式进行重构,但我认为这通常已经足够了。

    【讨论】:

    • 哦,哇,我从中学到了很多东西。谢谢!!!
    • 查看我的编辑,还有一个减少熵过度拉动的建议。通常不是问题,尤其是对于这么小的数据集,但如果你的真实代码变得“大”,那么减少过度拉取随机数可能会提高性能......如果你有其他更昂贵的代码,这可能确实有帮助,从不太可能的case_when-conditions 中删除昂贵的代码。
    • 欣赏。我的数据集是 1GB,所以这次不是问题,但我过去在大型数据集 (20Gb+) 上遇到过类似的问题,因此非常感谢您的编辑。
    • 作为一个侧面的想法,因为你有更大的数据:考虑用left_join(., data.frame(month=1:12, ndays=c(31,28,31,30,31,30,31,31,30,31,30,31)), by="month") %&gt;% mutate(new_day = ceiling(rand*ndays))替换你的case_when。这可能会表现得更好,我还没有对其进行基准测试。仅供参考,这仍然容易出现闰年问题;反驳这一点并不难,但这将是另一个电话或两个电话。
    • 最少天数和最多天数都需要加半小数,否则四舍五入表示第一天和最后一天的代表性不足,例如runif(n(), 0.5, 28.5)
    【解决方案2】:

    sampl来自seq.Date 调用,该调用利用了存储在POSIXlt 中的值。我们可以很容易地替换天并增加月份(但减去一天)。这会自动考虑到闰年等

    f <- \(x) {
      sample(with(as.POSIXlt(x),
                  seq.Date(as.Date(ISOdate(year + 1900, mon + 1, 1, 0)),
                           as.Date(ISOdate(year + 1900, mon + 2, 1, 0)) - 1, 'day')),
             1)
    }
    
    res <- transform(df, new_date=do.call(c, lapply(df$date, f)))
    res
    #            x       date   new_date
    # 1  0.9148060 2021-06-17 2021-06-22
    # 2  0.9370754 2022-08-13 2022-08-18
    # 3  0.2861395 2020-08-23 2020-08-13
    # 4  0.8304476 2022-07-30 2022-07-28
    # 5  0.6417455 2021-07-20 2021-07-05
    # 6  0.5190959 2021-09-23 2021-09-04
    # 7  0.7365883 2020-09-12 2020-09-02
    # 8  0.1346666 2022-05-20 2022-05-24
    # 9  0.6569923 2021-05-09 2021-05-18
    # 10 0.7050648 2019-09-16 2019-09-03
    # 11 0.4577418 2022-08-30 2022-08-24
    # 12 0.7191123 2020-04-25 2020-04-23
    # 13 0.9346722 2022-08-14 2022-08-17
    # 14 0.2554288 2019-01-24 2019-01-21
    # 15 0.4622928 2022-03-27 2022-03-26
    # 16 0.9400145 2019-10-26 2019-10-18
    # 17 0.9782264 2020-02-10 2020-02-06
    # 18 0.1174874 2019-11-10 2019-11-06
    # 19 0.4749971 2022-08-08 2022-08-02
    # 20 0.5603327 2021-04-15 2021-04-20
    

    不确定您是否需要日期或数字。如果您希望新的月份和日期显示为数字,您可以这样做

    within(res, {
      new_date <- do.call(c, lapply(df$date, f))
      month <- strftime(new_date, '%m')
      day <- strftime(new_date, '%d')
      }) |>
      type.convert(as.is=TRUE)
    #            x       date   new_date day month
    # 1  0.9148060 2021-06-17 2021-06-03   3     6
    # 2  0.9370754 2022-08-13 2022-08-22  22     8
    # 3  0.2861395 2020-08-23 2020-08-21  21     8
    # 4  0.8304476 2022-07-30 2022-07-02   2     7
    # 5  0.6417455 2021-07-20 2021-07-23  23     7
    # 6  0.5190959 2021-09-23 2021-09-06   6     9
    # 7  0.7365883 2020-09-12 2020-09-26  26     9
    # 8  0.1346666 2022-05-20 2022-05-10  10     5
    # 9  0.6569923 2021-05-09 2021-05-08   8     5
    # 10 0.7050648 2019-09-16 2019-09-05   5     9
    # 11 0.4577418 2022-08-30 2022-08-01   1     8
    # 12 0.7191123 2020-04-25 2020-04-17  17     4
    # 13 0.9346722 2022-08-14 2022-08-07   7     8
    # 14 0.2554288 2019-01-24 2019-01-04   4     1
    # 15 0.4622928 2022-03-27 2022-03-13  13     3
    # 16 0.9400145 2019-10-26 2019-10-10  10    10
    # 17 0.9782264 2020-02-10 2020-02-09   9     2
    # 18 0.1174874 2019-11-10 2019-11-29  29    11
    # 19 0.4749971 2022-08-08 2022-08-12  12     8
    # 20 0.5603327 2021-04-15 2021-04-20  20     4
    

    数据:

    df <- structure(list(x = c(0.914806043496355, 0.937075413297862, 0.286139534786344, 
    0.830447626067325, 0.641745518893003, 0.519095949130133, 0.736588314641267, 
    0.13466659723781, 0.656992290401831, 0.705064784036949, 0.45774177624844, 
    0.719112251652405, 0.934672247152776, 0.255428824340925, 0.462292822543532, 
    0.940014522755519, 0.978226428385824, 0.117487361654639, 0.474997081561014, 
    0.560332746244967), date = structure(c(18795, 19217, 18497, 19203, 
    18828, 18893, 18517, 19132, 18756, 18155, 19234, 18377, 19218, 
    17920, 19078, 18195, 18302, 18210, 19212, 18732), class = "Date")), class = "data.frame", row.names = c(NA, 
    -20L))
    

    【讨论】:

      【解决方案3】:

      你可以创建一个小助手函数,它会返回每个月的天数。

      month_days <- function(x) case_when(
        x == 2 ~ 28,
        x %in% c(1,3,5,7,8,10) ~ 31,
        TRUE ~ 30
      )
      

      然后您可以使用max=runif 中矢量化这一事实来一次获取所有值。请注意,由于您正在执行floor(),因此您需要将最大值加 1,以便您有机会观察到该值

      set.seed(22)
      # test data
      N <- 50
      dd <- data.frame(new_month = sample(1:12, N, replace=TRUE))
      
      dd$new_day <- floor( runif( length(dd$new_month), min=1, max=month_days(dd$new_month) + 1 ) )
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-08
        • 1970-01-01
        • 1970-01-01
        • 2021-10-19
        • 1970-01-01
        相关资源
        最近更新 更多