【问题标题】:Tornado / both-sided horizontal bar-plot in R with chart axes crosses at a given value (instead of crossing at zero)R中的龙卷风/双面水平条形图,图表轴在给定值处交叉(而不是在零处交叉)
【发布时间】:2023-03-09 08:48:01
【问题描述】:

我想在 R 中绘制一个龙卷风图(双面水平条形图),用于确定性灵敏度分析,我尝试了一些代码但没有得到所需的输出。

我想实现:-

  1. 绘图应按敏感参数的降序排列(即最宽的区间应显示在图表顶部 - 为了获得灵敏度,我们首先计算下限值和上限值的绝对差值我在我的数据框代码中命名为“UL_Difference”)。

  2. 中心不应为零,而应为给定值,称为“基本情况”或我的结果表的核心/最终结果(我们要检查不同固定参数的影响)使用参数的下限值和上限值,并为下限值和上限值生成核心结果)。 Excel VBA 中的示例代码是

  3. 该图的标题应为“药物 A 与 P 的龙卷风图”。

我尝试了很多代码。下面是一个例子,它给了我一个龙卷风图,但不完全是我想从 R 生成的。

Base_Result <- results.table[5,4] # Base/Core result (which I have not used in my codes below yet)

Drug_AP <- seq(1, 48, 4)
D_AP <- data.frame(OWSA[Drug_AP,]) # OWSA[] is a 10x3 matrix with 'Lower_Bound', 'Upper_Bound' and Absolute Difference of the LB and UB termed as 'UL_Difference' (row names are parameters)
DSA_Drug_AP <- D_AP[order(D_AP$UL_Difference, decreasing = T),] # Ordering the data.frame above in Descending order of 'UL_Difference'
cat("DSA Table: Drug A vs P \n")
library(formattable)
print(accounting(as.matrix(DSA_Drug_AP), digits = 0, format = "f", big.mark = ","), right = T) # Just printing the above data.frame

我尝试了以下代码来绘制龙卷风:-

(我不确定是否应该制作下面的数据框,也许这是我没有得到所需输出的原因之一)

dat <- data.frame(Group = c(rep("Lower_Bound", 12), rep("Upper_Bound", 12)), 
                  Parameters = rep(rownames(DSA_Drug_AP), 2), 
                  UL = c(-DSA_Drug_AP[,1], DSA_Drug_AP[,2]))

(最后我用“ggplot”绘制了上面的数据框,如下图)

library(ggplot2)
ggplot(dat, aes(x = Parameters, y = UL, fill = Group)) + 
    coord_flip() + 
    geom_bar(stat = "identity", position = "identity", width = 0.525) +
    theme(legend.position="top", axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.5, size = 10))

得到如下输出:-

下面是我想要实现的输出(点#1和#2都实现了;图表是从excel生成的)。

# Also, the data I'm using is shown below: -

Base_Result <- 9,504  # Value of results.table[5,4] on which I get 'lower' and 'upper' limit values below (and want tornado with the origin at this base_result).

# My data.frame "D_AP" will look like (I just renamed my parameters to 1(to)12)

           Lower_Bound  Upper_Bound UL_Difference
Parameter_01     8,074      11,181   3,108 
Parameter_02     8,177      11,007   2,831 
Parameter_03     8,879      10,188   1,308 
Parameter_04     4,358      18,697   14,339 
Parameter_05     9,073      10,087   1,013 
Parameter_06     12,034      7,572   4,462 
Parameter_07     11,357      7,933   3,423 
Parameter_08     9,769       9,202   567 
Parameter_09     8,833      10,403   1,570 
Parameter_10     13,450      4,219   9,231 
Parameter_11     10,691      7,915   2,776 
Parameter_12     10,036      8,792   1,244 

# Once, I did sort in descending order then it will be data.frame "DSA_Drug_AP" as below: -

            Lower_Bound Upper_Bound UL_Difference
Parameter_04     4,358      18,697   14,339 
Parameter_10     13,450      4,219   9,231 
Parameter_06     12,034      7,572   4,462 
Parameter_07     11,357      7,933   3,423 
Parameter_01     8,074      11,181   3,108 
Parameter_02     8,177      11,007   2,831 
Parameter_11     10,691      7,915   2,776 
Parameter_09     8,833      10,403   1,570 
Parameter_03     8,879      10,188   1,308 
Parameter_12     10,036      8,792   1,244 
Parameter_05     9,073      10,087   1,013 
Parameter_08     9,769       9,202   567 

# Please note that I need to plot the 1st and 2nd column of values 
# (shown in above table in order of 3rd column as a tornado plot).
# The parameter-## names will come to the left vertical line of plot.

提前谢谢你!

【问题讨论】:

    标签: r ggplot2 bar-chart


    【解决方案1】:

    前段时间我尝试使用geom_bar() 进行此操作,但并不好玩。 geom_bar() 默认堆叠以零为参考的列。我必须在列中创建空白部分才能获得(某种)我想要的效果。

    更好的方法是使用geom_rect()。你只需要稍微调整一下你的数据框,就可以获得xminxmaxyminymax 所需的美学(比尝试解决geom_bar() 的问题要少得多)

    由于您没有发布您的数据集,我创建了一个非常简单的数据集。但希望结构与您的结构足够接近


    编辑:我更改了代码以在您的示例中包含数据框。

    library(ggplot2)
    library(plyr)
    library(dplyr)
    library(tidyverse)
    
    # this is throwing some warnings in my computer, but it is reading the data frame correctly
    df <- '
    Parameter Lower_Bound Upper_Bound UL_Difference
    Parameter01 8074 11181 3108 
    Parameter02 8177 11007 2831 
    Parameter03 8879 10188 1308 
    Parameter04 4358 18697 14339 
    Parameter05 9073 10087 1013 
    Parameter06 12034 7572 4462 
    Parameter07 11357 7933 3423 
    Parameter08 9769 9202 567 
    Parameter09 8833 10403 1570 
    Parameter10 13450 4219 9231 
    Parameter11 10691 7915 2776 
    Parameter12 10036 8792 1244
    ' %>% read_table2()
    
    # original value of output
    base.value <- 9504
    
    # get order of parameters according to size of intervals
    # (I use this to define the ordering of the factors which I then use to define the positions in the plot)
    order.parameters <- df %>% arrange(UL_Difference) %>%
      mutate(Parameter=factor(x=Parameter, levels=Parameter)) %>%
      select(Parameter) %>% unlist() %>% levels()
    
    # width of columns in plot (value between 0 and 1)
    width <- 0.95
    
    # get data frame in shape for ggplot and geom_rect
    df.2 <- df %>% 
      # gather columns Lower_Bound and Upper_Bound into a single column using gather
      gather(key='type', value='output.value', Lower_Bound:Upper_Bound) %>%
      # just reordering columns
      select(Parameter, type, output.value, UL_Difference) %>%
      # create the columns for geom_rect
      mutate(Parameter=factor(Parameter, levels=order.parameters),
             ymin=pmin(output.value, base.value),
             ymax=pmax(output.value, base.value),
             xmin=as.numeric(Parameter)-width/2,
             xmax=as.numeric(Parameter)+width/2)
    
    # create plot
    # (use scale_x_continuous to change labels in y axis to name of parameters)
    png(width = 960, height = 540)
    ggplot() + 
      geom_rect(data = df.2, 
                aes(ymax=ymax, ymin=ymin, xmax=xmax, xmin=xmin, fill=type)) +
      theme_bw() + 
      theme(axis.title.y=element_blank(), legend.position = 'bottom',
            legend.title = element_blank()) + 
      geom_hline(yintercept = base.value) +
      scale_x_continuous(breaks = c(1:length(order.parameters)), 
                         labels = order.parameters) +
      coord_flip()
    dev.off()
    

    【讨论】:

    • 感谢 Kikoralston 对帖子的回复。我意识到我应该提供一些虚拟数据,这也有助于使我拥有和想要绘制的输出更加清晰。感谢您的代码,但我检查了我没有任何 input.value 我正在获取 output.value。但我只有输出(下限和上限)值。我已经编辑了我的问题并将数据与 base_result(龙卷风的起源)一起添加。我希望对您修改代码有所帮助,以便获得所需的输出。提前致谢。
    • @user11334975:您能否提供有关数据集中这些值的更多详细信息?这就是我理解龙卷风图的方式。您有一个 Base_result 值,您使用模型模拟了所有参数的典型值。然后你选择一个参数。 (例如Parameter-01)并将其值更改为-10%和+ 10%(例如)。这为您的结果提供了两个新值(Lower_BoundUpper_Bound)。我看到它Base_Result 的方式应该在[Lower boundUpper bound] 范围内。但在您的数据框中并非如此。我的解释有误?
    • 太棒了!您在理解问题方面处于正确的道路/轨道上。感谢您指出错误。为了保持机密性,我对数据进行了太多修改,甚至我都没有注意到 base_result 现在超出了 LB 和 UB 值(这是不可能的)。我已经为场景运行编辑了问题中的数据。
    • 我很高兴能帮上忙!关于您的问题:1)我使用tidyverse 只是为了使用read_table2()函数并且能够将字符串作为数据框读取(在您的情况下,您可能删除了这部分); 2)(见下一条评论)
    • 2) 我使用png() 函数将绘图保存为 png 文件(而不是将其绘制到 R studio 绘图窗口)。 png 函数允许您定义文件的大小/分辨率(在我的情况下为 960x540 像素)。将绘图保存为文件时,R studio 中的导出功能可能会使用其他设置。您还可以使用其他重要设置(例如point sizeres)。我承认我并没有很好地控制这部分,我总是尝试尝试错误的方法,直到我得到我想要的外观。一个很好的资源是this link
    【解决方案2】:

    这是一个使用 ggplot2::geom_col() 生成龙卷风图的函数以及一些使用示例。希望对您有所帮助....

    # Tornado Plot using ggplot2(), 2019/05/19.
    # See Wikipedia: ["Tornado diagram"](https://en.wikipedia.org/wiki/Tornado_diagram).
    
    library( magrittr )
    library( tidyverse )
    
    # Function tornado_plot() produces a "tornado plot" given the sensitivity
    # analysis results in data_frame df. It plots green bars indicating the levels
    # of the response variable when each **x input variable** is moved to its maximum
    # level while holding all other variables constant.  Similarly, the red bars are
    # the outputs when each **x input variable** is moved to its minimum value while
    # holding all other variables constant. The input variable to which the output
    # is most sensitive is shown at the top of the plot. And the bars are stacked
    # from most sensitive to least sensitive, fancifully yielding the shape of a
    # tornado.
    tornado_plot <-
      function(
        df,
        var_names_col,
        min_level_col,
        min_output_col,
        max_level_col,
        max_output_col,
        base_level_col,
        baseline_output,
        title_str      = "Tornado Plot",
        subtitle_str   = "",
        caption_str    = "",
        ylab_str       = "output",
        baseline_label = "",
        scale_breaks   = NULL,
        limits         = NULL
      ) {
        # + The argument df must be a tidyverse::tibble with columns referred to by all of the
        #   other arguments having "col" in their names.
        # + The var_names_col argument must be an unquoted column name that contains characters
        #    naming the variables that were varied in the sensitivity analysis.
        # + The level column arguments -- min_level_col, max_level_col and
        #   base_level_col -- must be unquoted column names that contain characters to be
        #   used in forming labels for each variable bar of the plot.
        # + The output column arguments -- min_output_col and max_output_col -- must
        #   be unquoted column names that contain numerical values to be plotted as the
        #   extents of the bars in the plot.
        # + The baseline_output argument is the numeric value of the output (response) variable
        #   produced by setting all of the variables to their base levels.
    
        var_names_col  <- enquo( var_names_col )
        min_level_col  <- enquo( min_level_col )
        max_level_col  <- enquo( max_level_col )
        base_level_col <- enquo( base_level_col )
        min_output_col <- enquo( min_output_col )
        max_output_col <- enquo( max_output_col )
    
        have_custom_y_breaks <- !any( is.null(scale_breaks) )
    
        # Create a generic tibble as the data source for plotting.
        # Sorts variables from the one to which the output was least sensitive
        # to the one to which the output was most sensitive.
        # Then creates labels for each variable capturing the min, base, and max
        # levels of that variable.
        # Finally, it centers all outputs around the baseline output so thta the
        # ggplot2::geom_col() function can still work with zero-based bars.
        plt_df <- df %>% 
          mutate(del = abs(!!max_output_col - !!min_output_col) ) %>% 
          arrange(del) %>% 
          mutate(
            names = sprintf(
              "%s\n(min=%s; base=%s; max=%s)",
              !!var_names_col,
              !!min_level_col,
              !!base_level_col,
              !!max_level_col
            ),
            names = factor(names,names),
            min   = !!min_output_col,
            max   = !!max_output_col
          ) %>% 
          dplyr::select(names,min,max) %>% 
          gather( key = Level, value = output, -names) %>% 
          mutate( output = output - baseline_output, Level = factor(Level,c("min","max")) ) #%T>% print()
    
        # Generate the tornado plot.
        plt <- plt_df %>% 
          {
            ggplot(., aes( fill = Level, x = names, y = output )) + 
              geom_hline(yintercept = 0, linetype = 1, size = 2, color = "darkgray") +
              geom_col( alpha = 0.4, width = 0.98) + 
              coord_flip() + #*** NOTE THE COORDINATE FLIP ***
              geom_text(aes(y = 0, label = names), size = 4, fontface = "bold" ) +
              scale_x_discrete( expand = expand_scale(add = 1 ) ) +
              scale_fill_manual(values = c(min = "red", max = "green") ) +
              ylab( ylab_str ) +
              theme( # **Hmmm, references the ACTUAL plotted (post-flipped) x-y axes. **
                axis.ticks.y = element_blank(),
                axis.text.y  = element_blank(),
                axis.title.y = element_blank(),
                panel.grid.major.y = element_blank(), # Remove horizontal grid lines
                panel.grid.minor.y = element_blank(),
                axis.text.x  = element_text( size = 14 ),
                axis.title.x = element_text( size = 16 ),
                title        = element_text( size = 18 ),
                legend.position = "bottom"
              ) +
              labs( title = title_str, subtitle = subtitle_str, caption = caption_str )
          }
        # Set the pre-flipped y-axis (which gets flipped to be the x-axis in the final plot).
        if( !is.null(limits) ){
          y_limits = limits
        } else {
          y_limits = c(-max(abs(plt_df$output)),max(abs(plt_df$output)))
        }
        if( have_custom_y_breaks ){
          plt <- plt + scale_y_continuous(
            limits = y_limits,
            breaks = scale_breaks,
            labels = names(scale_breaks)
          )
        } else {
          plt <- plt + scale_y_continuous(
            limits = y_limits,
            labels = function(x) baseline_output + x
          )
        }
        # Add the baseline output label, if any
        if(baseline_label != ""){
          return(
            plt + 
              geom_label(
                data = tibble( x = 0.25, y = 0, label = baseline_label),
                mapping = aes( x = x, y = y, label = label),
                fontface = "bold",
                show.legend = FALSE,
                inherit.aes = FALSE
              )
          )
        } else {
          return( plt )
        }
      }
    
    #--------------------------------------------------------------------------------     
    
    # USAGE EXAMPLE:
    # Hypothetical Investment Strategy Analysis:
    # These are data from a sensitivity analysis on an investment strategy that invests in an
    # an S&P 500 index fund and a "safety" value-store (a 0%-real-return investment); 
    # protecting winnings from market with transfer to safety when strategy criteria are met. 
    # Disregards taxes and fees. Real values (i.e., inflation-adjusted).
    sensitivity_df <- tribble(
      ~variable,                            ~min,  ~base,   ~max, ~Total_at_min, ~Total_base, ~Total_at_max,             ~Time_period,
      "Start Value",                           0,   2000, 100000,        239600,      245900,        554800, "start: 1980, end: 2005",
      "Monthly Investment",                    0,    500,   1000,          6300,      245900,        485600, "start: 1980, end: 2005",
      "Allocation to Safety",                  0,    0.3,    0.5,        277800,      245900,        224700, "start: 1980, end: 2005",
      "Annual Increase in Mo. Investment",     0,   0.01,   0.03,        222700,      245900,        303800, "start: 1980, end: 2005",
      "Protection Rate",                       0, 0.0025,   0.03,        310300,      245900,        199500, "start: 1980, end: 2005",
    
      "Start Value",                           0,   2000, 100000,        174300,      175900,        253300, "start: 1910, end: 1935",
      "Monthly Investment",                    0,    500,   1000,          1600,      175900,        350100, "start: 1910, end: 1935",
      "Allocation to Safety",                  0,    0.3,    0.5,        177700,      175900,        174600, "start: 1910, end: 1935",
      "Annual Increase in Mo. Investment",     0,   0.01,   0.03,        155600,      175900,        227100, "start: 1910, end: 1935",
      "Protection Rate",                       0, 0.0025,   0.03,        171800,      175900,        176000, "start: 1910, end: 1935"
    ) %>%  # Add x-input level labels (overwriting reals min, base, max with character values through mutate_at()).
      mutate_at(vars(contains("Total")), ~{100*round(./100)}) %>%
      mutate_at(
        vars( min, base, max), 
        ~ { 
          ifelse(
            abs(.) >= 1000,
            paste0("$",formatC(.,big.mark = ",",format = "f",digits = 0)),
            sprintf(
              c( "$%.0f", "$%.0f", "%.0f%%", "%.1f%%", "%.2f%%" ), 
              . * c(1,1,100,100,100)
            )
          )
        } 
      )
    
    # Generate the tornado plot with generic labeling and axis.
    sensitivity_df %>%
      filter( grepl("1980.+2005", Time_period ) ) %>%
      tornado_plot(
        var_names_col   = variable,
        min_level_col   = min,
        min_output_col  = Total_at_min,
        max_level_col   = max,
        max_output_col  = Total_at_max,
        base_level_col  = base,
        baseline_output = .$Total_base[[1]]
      ) %>% print()
    
    
    # Generate the tornado plot with customized labeling and axis.
    scl_limits = c(0, 6.0e5 )
    sensitivity_df %>%
      filter( grepl("1980.+2005", Time_period ) ) %>%
      tornado_plot(
        var_names_col   = variable,
        min_level_col   = min,
        min_output_col  = Total_at_min,
        max_level_col   = max,
        max_output_col  = Total_at_max,
        base_level_col  = base,
        baseline_output = .$Total_base[[1]],
        title_str       = "Sensitivity of Total Value to Strategy Variables",
        subtitle_str    = sprintf( "Time period %s", .$Time_period[[1]] ),
        caption_str     = "Assuming S&P 500 index & 0%-real-return 'safe harbor'",
        ylab_str        = "Total Value",
        baseline_label  = paste0("Base Case:\n$",format(100*round(.$Total_base[[1]]/100,0),big.mark = ",")),
        scale_breaks    = setNames(
          seq(min(scl_limits), max(scl_limits), 1e5) - .$Total_base[[1]], 
          paste0("$",formatC(
            seq(min(scl_limits), max(scl_limits), 1e5),big.mark = ",",format = "f",digits = 0)
          )
        ),
        limits          = scl_limits - .$Total_base[[1]]
      ) %>% print()
    
    # Generate the tornado plot for another time period, with scaling
    # to be comparable with the first time period.
    sensitivity_df %>%
      filter( grepl("1910.+1935", Time_period ) ) %>%
      tornado_plot(
        var_names_col   = variable,
        min_level_col   = min,
        min_output_col  = Total_at_min,
        max_level_col   = max,
        max_output_col  = Total_at_max,
        base_level_col  = base,
        baseline_output = .$Total_base[[1]],
        title_str       = "Sensitivity of Total Value to Strategy Variables",
        subtitle_str    = sprintf( "Time period %s", .$Time_period[[1]] ),
        caption_str     = "Assuming S&P 500 index & 0%-real-return 'safe harbor'",
        ylab_str        = "Total Value",
        baseline_label  = paste0("Base Case:\n$",format(100*round(.$Total_base[[1]]/100,0),big.mark = ",")),
        scale_breaks    = setNames(
          seq(min(scl_limits), max(scl_limits), 1e5) - .$Total_base[[1]], 
          paste0("$",formatC(
            seq(min(scl_limits), max(scl_limits), 1e5),big.mark = ",",format = "f",digits = 0)
          )
        ),
        limits          = scl_limits - .$Total_base[[1]]
      ) %>% print()
    
    

    Generic tornado plot

    Tornado plot with custom labels, axis

    Tornado plot w/custom labels and on same scale as previous one

    【讨论】:

      猜你喜欢
      • 2017-10-16
      • 2016-08-31
      • 2016-12-23
      • 2017-09-18
      • 2013-11-22
      • 2018-10-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多