ggplot新的scale_x函数无法正常工作

6
为了在ggplot中摆脱非交易日(即周末)的影响,我使用数据中的行数来代替日期,并添加间隔和标签。下面的代码 "works" 可以实现这一目的,并画出 quantmod 包中的 chartSeries 数据图。 ggplot 会根据绘制的图表类型添加本不存在的信息或显示空白。对于处理股票价格这样的问题不方便。因此,works 部分可以解决该问题。
但由于这只是一个标签问题,使用轴转换函数会更加逻辑清晰和简单易用。我尝试创建一个scale_x_finance函数(见 does not work 部分),但我可能误解了反函数的含义,因为我只得到了一个日期而非整个时间序列的情况。
我阅读了多篇Stack Overflow的问题,例如这个这个 ,但目前还没有找到解决方法。
我知道存在一个叫做 bdscale 的包,但它已经超过六年没有更新了,而且它创建的间隔 / 标签并不符合我的需求。
使用 scale_x_finance 的结果应该和 works 部分的图表一样。我想知道我漏掉了什么。
我在问题底部添加了一些测试数据。

works

library(ggplot2)

# get the start date and the last days of the month for breaks and label positions
get_breaks <- function(x) {
  out <- c(1, which(ave(as.numeric(x),format(x,"%Y%m"), FUN = function(x) x == max(x)) == 1))
}

# use 1:nrow to be able to use scale_x_continuous
ggplot(test_data, aes(x = 1:nrow(test_data))) + 
  geom_line(aes(y = close)) +
  scale_x_continuous(name = "date",
                     breaks = get_breaks(test_data$date),
                     labels = test_data$date[get_breaks(test_data$date)])

这里输入图像描述

无法工作

scale_x_finance <- function (...,
                             dates,
                             breaks = get_breaks(dates)){
  
  my_transformer <- function(dates, breaks = get_breaks(dates)) {
    
    transform <- function(dates) seq_along(dates) 
    inverse <- function(nums) dates[nums] 
    
    scales::trans_new(name = "date",
                      transform = transform,
                      inverse = inverse,
                      breaks = breaks,
                      domain = range(dates))
  }
  
  scale_x_continuous(name = "date",
                     trans = my_transformer(dates = dates, breaks = breaks),
                     ...)
}


ggplot(test_data, aes(x = date)) + 
  geom_line(aes(y = close)) +
  scale_x_finance(dates = test_data$date) 

图片描述放在这里

数据:

test_data <- structure(list(date = structure(c(18995, 18996, 18997, 18998, 
                                               18999, 19002, 19003, 19004, 19005, 19006, 19010, 19011, 19012, 
                                               19013, 19016, 19017, 19018, 19019, 19020, 19023, 19024, 19025, 
                                               19026, 19027, 19030, 19031, 19032, 19033, 19034, 19037, 19038, 
                                               19039, 19040, 19041, 19045, 19046, 19047, 19048, 19051, 19052, 
                                               19053, 19054, 19055, 19058, 19059, 19060, 19061, 19062, 19065, 
                                               19066, 19067, 19068, 19069, 19072, 19073, 19074, 19075, 19076, 
                                               19079, 19080, 19081, 19082), class = "Date"), 
                            close = c(182.009995, 179.699997, 174.919998, 172, 172.169998, 172.190002, 175.080002, 
                                      175.529999, 172.190002, 173.070007, 169.800003, 166.229996, 164.509995, 
                                      162.410004, 161.619995, 159.779999, 159.690002, 159.220001, 170.330002, 
                                      174.779999, 174.610001, 175.839996, 172.899994, 172.389999, 171.660004, 
                                      174.830002, 176.279999, 172.119995, 168.639999, 168.880005, 172.789993, 
                                      172.550003, 168.880005, 167.300003, 164.320007, 160.070007, 162.740005, 
                                      164.850006, 165.119995, 163.199997, 166.559998, 166.229996, 163.169998, 
                                      159.300003, 157.440002, 162.949997, 158.520004, 154.729996, 150.619995, 
                                      155.089996, 159.589996, 160.619995, 163.979996, 165.380005, 168.820007, 
                                      170.210007, 174.070007, 174.720001, 175.600006, 178.960007, 177.770004, 
                                      174.610001)), row.names = c(NA, 62L), class = "data.frame")

1
也许 Hadley Wickham 的这个 对话 会有所帮助? - Quinten
1
@Quinten,感谢您的建议。我已经阅读了那篇文章以及解决了使用此对话作为输入的SO问题。不幸的是,这并没有帮助我解决我的问题。我可以反转日期,这不是问题。但是使用一种与日期相关的scale_x_continuous。自动假设如果在x轴上绘制日期,则在没有Y值时添加日期是问题所在。我有一个解决方法,但需要每个要制作的图形类型都需要一个包装器函数,而不是调用“简单”的比例。 - phiver
1个回答

4
问题在于你的my_transformer对象。它需要能够处理不在你的数据中的日期并适当地转换它们。例如,当ggplot计算绘图限制时,它可能会传递一个包含不属于你的dates向量的两个日期的向量。 transform函数将任何两个日期的向量转换为向量c(1,2),这不是你想要的 - 你需要根据你的dates向量插值任意日期。
类似的概念也适用于inverse函数,它将处理任意数字并将其反向转换为日期。
我认为处理这个问题最简单的方法是确保所有日期在my_transformer内部都被视为数值,然后通过scale_x_continuous调用在最后进行标记。
因此,你的转换器可以是这样的:
library(ggplot2)

my_transformer <- function(dates) {
  dates <- as.numeric(dates)
  pos   <- seq_along(dates) - 1
  
  transform <- function(x) {
    if(all(is.na(x))) return(x)
    x <- as.numeric(x)
    y <- numeric(length(x))
    in_range <- x >= min(dates) & x <= max(dates)
    y[in_range] <- approx(dates, pos, x[in_range])$y
    y[x < min(dates)] <- x[x < min(dates)] - min(dates)
    y[x > max(dates)] <- x[x > max(dates)] - max(dates) + max(pos)
    y
  }
  
  inverse <- function(x) {
    if(all(is.na(x))) return(x)
    x <- as.numeric(x)
    y <- numeric(length(x))
    y[is.na(x)] <- NA
    in_range <- x >= 0 & x <= max(pos) & !is.na(x)
    y[in_range] <- approx(pos, dates, x[in_range])$y
    y[x < 0] <- x[x < 0] + min(dates)
    y[x > max(pos)] <- max(dates) + x[x > max(pos)] - max(pos)
    y
  }
  
    scales::trans_new(name = "date",
                      transform = transform,
                      inverse   = inverse)
}

scale_x_finance 就会像这样:

scale_x_finance <- function (dates, ...) {
  
  scale_x_continuous(name = "date",  ..., 
                     trans = my_transformer(dates),
                     labels = ~ as.Date(.x, origin = "1970-01-01"))
}

这样,您的绘图调用只需:

ggplot(test_data, aes(x = date, y = close)) + 
  geom_line(aes(y = close)) +
  scale_x_finance(dates = test_data$date)

为了演示数据中的缺口只是移除x轴上的空间(这似乎是最终目标),我们可以移除一周的数据,看看缺失日期两侧的日期是否会更加接近:

图片说明文字请在此输入

test_data <- test_data[-(25:31),]

ggplot(test_data, aes(x = date, y = close)) + 
  geom_line(aes(y = close)) +
  scale_x_finance(dates = test_data$date)

enter image description here


Allen,谢谢你。它在我为这个问题保留的数据集上运行良好。奇怪的是,在一个记录更多的数据集上,我遇到了一个错误 :-(。y[in_range] <- approx(pos, dates, x[in_range])$y 中的错误:子脚本赋值中不允许NAs。经过一些测试,当使用74条或更多记录时,错误就会开始出现。我要进行调试,看看能否找到这个错误的来源。 - phiver
找到了问题。必须设置断点,否则在 ggplot 中添加 50 天到 inverse 部分的限制中并超过 in_range 值,这会创建一个 NA 值作为进入 approx 函数的最后一个值。当然,设置断点后,我有了一个图,但现在标签丢失了。有时我想知道 ggplot 是否创造了更多问题而不是解决问题。 - phiver
@phiver 不需要breaks - 只需调整以处理NA值 - 我已经编辑过以处理NA值并在一年的数据上进行了测试。 - Allan Cameron
我成功地让我的图表中的间隔看起来像我想要的样子。有没有关于ggplot内部这个部分的合适文档,还是只能靠试错?ggplot2的书籍并没有涉及到这种细节。 - phiver
1
@phiver 不是很确定。我曾经编写过使用大量内部操作的ggplot包,这主要是凭借反复试错(除了扩展ggplot文档)。即使回答这个问题,我也不得不广泛地使用browser()来查找ggplot如何使用trans对象。我通过在这里回答问题学到了很多。最终,这个问题更多地与trans对象有关,而不是ggplot内部操作。 - Allan Cameron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接