diff --git a/.Rbuildignore b/.Rbuildignore index 808efb8..43e3b0e 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -7,3 +7,5 @@ ^docs$ ^pkgdown$ ^\.github$ +^TESTING$ +^.vscode$ diff --git a/DESCRIPTION b/DESCRIPTION index a7f7be9..e84492a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,28 +1,36 @@ Package: ggiplot -Title: ggplot2 Equivalents of fixest::iplot() -Version: 0.0.1.9010 +Title: ggplot2 equivalents of fixest's `iplot` and `coefplot` functions +Version: 0.0.1.9011 Authors@R: - person(given = "Grant", - family = "McDermott", - role = c("aut", "cre"), - email = "grantmcd@uoregon.edu", - comment = c(ORCID = "0000-0001-7883-8573")) -Description: Provides ggplot2 equivalents of fixest::iplot() for producing - "event study" plots. Enables some additional functionality and convenience - features, including grouped mutli_fixest object facetting and programatic - updating of existing plots (e.g. themes and aesthetics). + c(person(given = "Grant", + family = "McDermott", + role = c("aut", "cre"), + email = "gmcd@amazon.com", + comment = c(ORCID = "0000-0001-7883-8573")), + person(given = "Laurent", + family = "Berge", + role = "ctb", + email = "laurent.berge@u-bordeaux.fr") + ) +Description: Provides ggplot2 equivalents of fixest::iplot and fixest::coefplot + for producing nice "event study" and coefficient plots, respectively. Enables + some additional functionality and convenience features, including grouped + multi-fixest object faceting and programatic updating of existing plots + (e.g., themes and aesthetics). License: MIT + file LICENSE Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.2.3.9000 URL: http://grantmcdermott.com/ggiplot/ BugReports: https://github.com/grantmcdermott/ggiplot/issues Remotes: lrberge/fixest Depends: ggplot2 (>= 2.2.0) Imports: - fixest (>= 0.11.0), + fixest (>= 0.11.2), + dreamerr, + ggh4x, scales, utils, marginaleffects (>= 0.10.0), diff --git a/NAMESPACE b/NAMESPACE index 1d59f9a..9b4f067 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand export(aggr_es) +export(ggcoefplot) export(ggiplot) export(iplot_data) import(ggplot2) diff --git a/NEWS.md b/NEWS.md index 65b43ff..bfaf571 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,8 @@ -# ggiplot 0.0.1.9010 (development version) +# ggiplot 0.0.1.9011 (development version) ## New features +- Support for `ggcoefplot`, a ggplot equivalent of `coefplot` (#28). - Support `keep` and `drop` arguments for subsetting coefficients (#22). ## Bug fixes and breaking changes diff --git a/R/ggiplot.R b/R/ggiplot.R index fb9501d..8defd4a 100644 --- a/R/ggiplot.R +++ b/R/ggiplot.R @@ -1,15 +1,21 @@ -#' @title ggplots confidence intervals and point estimates +#' @title Draw coefficient plots and interaction plots from `fixest` regression +#' objects. #' -#' @description Plots the `ggplot2` equivalent of `fixest::iplot()`. Many of the -#' arguments are the same. As per the latter's description: -#' This function plots the results of estimations (coefficients and confidence -#' intervals). The function restricts the output to variables created with -#' `i`, either interactions with factors or raw factors. +#' @description Draws the `ggplot2` equivalents of `fixest::coefplot` and +#' `fixest::iplot`. These "gg" versions do their best to recycle the same +#' arguments and plotting logic as their original base counterparts. But they +#' also support additional features via the `ggplot2` API and infrastructure. +#' The overall goal remains the same as the original functions. To wit: +#' `ggcoefplot` plots the results of estimations (coefficients and confidence +#' intervals). The function `ggiplot` restricts the output to variables +#' created with `i`, either interactions with factors or raw factors. #' @md #' @param object A model object of class `fixest` or `fixest_multi`, or a list #' thereof. #' @param geom_style Character string. One of `c('pointrange', 'errorbar', 'ribbon')` -#' describing the preferred geometric representation of the coefficients. +#' describing the preferred geometric representation of the coefficients. Note +#' that ribbon plots not supported for `ggcoefplot`, since we cannot guarantee +#' a continuous relationship among the coefficients. #' @param multi_style Character string. One of `c('dodge', 'facet')`, defining #' how multi-model objects should be presented. #' @param aggr_eff A character string indicating whether the aggregated mean @@ -24,10 +30,12 @@ #' @param theme ggplot2 theme. Defaults to `theme_linedraw()` with some minor #' adjustments, such as centered plot title. Can also be defined on an #' existing ggiplot object to redefine theme elements. See examples. -#' @param ... Arguments passed down, or equivalent, to the corresponding -#' `fixest::iplot()` arguments. Note that some of these require list objects. -#' Currently used are: -#' * `keep` and `drop` for subsetting variables using regular expressions. +#' @param ... Arguments passed down to, or equivalent to, the corresponding +#' `fixest::coefplot`/`fixest::iplot` arguments. Note that some of these +#' require list objects. Currently used are: +#' * `keep` and `drop` for subsetting variables using regular expressions. The `fixest::iplot` help page includes more detailed examples, but these should generally work as you expect. One useful regexp trick worth mentioning briefly for event studies with many pre-/post-periods is `drop = "[[:digit:]]{2}"`. This will cause the plot to zoom in around single digit pre-/post-periods. +#' * `group` a list indicating variables to group over. Each element of the list reports the coefficients to be grouped while the name of the element is the group name. Each element of the list can be either: i) a character vector of length 1, ii) of length 2, or iii) a numeric vector. Special patterns such as "^^var_start" can be used to more appealing plotting, where group labels are separated from their subsidiary labels. This can be especially useful for plotting interaction terms. See the Details section of `fixest::coefplot` for more information. +#' * `i.select` Integer scalar, default is 1. In `ggiplot`, used to select which variable created with `i()` to select. Only used when there are several variables created with `i`. See the Details section of `fixest::iplot` for more information. #' * `main`, `xlab`, and `ylab` for setting the plot title, x- and y-axis labels, respectively. #' * `zero` and `zero.par` for defining or adjusting the zero line. For #' example, `zero.par = list(col = 'orange')`. @@ -45,108 +53,225 @@ #' channel. For example, we can make the CI band lighter with #' `ci.fill.par = list(alpha = 0.2)` (the default alpha is 0.3). #' * `dict` a dictionary for overriding coefficient names. -#' @details This function generally tries to mimic the functionality and (where -#' appropriate) arguments of `fixest::iplot()` as closely as possible. -#' However, by leveraging the ggplot2 API and infrastructure, it is able to -#' support some more complex plot arrangements out-of-the-box that would be -#' more difficult to achieve using the base `iplot()` alternative. -#' @seealso [fixest::iplot()]. +#' @details These functions generally try to mimic the functionality and (where +#' appropriate) arguments of `fixest::coefplot` and `fixest::iplot` as +#' closely as possible. However, by leveraging the ggplot2 API and +#' infrastructure, they are able to support some more complex plot +#' arrangements out-of-the-box that would be more difficult to achieve using +#' the base `coefplot`/`iplot` alternatives. +#' @seealso [fixest::coefplot()], [fixest::iplot()]. #' @return A ggplot2 object. #' @import ggplot2 #' @export #' @examples -#' # We'll also load fixest to estimate the actual models that we're plottig. +#' # We'll also load fixest to estimate the actual models that we're plotting. #' library(fixest) #' library(ggiplot) #' -#' # These examples borrow from the fixest::iplot() documentation and the -#' # introductory package vignette. +#' ## +#' # Author note: The examples that follow deliberately follow the original +#' # examples from the coefplot/iplot help pages. A few "gg-" specific +#' # features are sprinkled within, with the final set of examples in +#' # particular highlighting unique features of this package. +#' +#' +#' # +#' # Example 1: Basic use and stacking two sets of results on the same graph +#' # +#' +#' # Estimation on Iris data with one fixed-effect (Species) +#' est = feols(Petal.Length ~ Petal.Width + Sepal.Length + Sepal.Width | Species, iris) +#' +#' ggcoefplot(est) +#' +#' # Show multiple CIs +#' ggcoefplot(est, ci_level = c(0.8, 0.95)) +#' +#' # By default, fixest model standard errors are clustered by the first fixed +#' # effect (here: Species). +#' # But we can easily switch to "regular" standard-errors +#' est_std = summary(est, se = "iid") +#' +#' # You can plot both results at once in the same plot frame... +#' ggcoefplot(list("Clustered" = est, "IID" = est_std)) +#' # ... or as separate facets +#' ggcoefplot(list("Clustered" = est, "IID" = est_std), multi_style = "facet") + +#' theme(legend.position = "none") +#' #' #' # -#' ## Example 1: Vanilla TWFE +#' # Example 2: Interactions #' # #' +#' +#' # Now we estimate and plot the "yearly" treatment effects +#' #' data(base_did) #' base_inter = base_did #' -#' est_did = feols(y ~ x1 + i(period, treat, 5) | id+period, base_inter) +#' # We interact the variable 'period' with the variable 'treat' +#' est_did = feols(y ~ x1 + i(period, treat, 5) | id + period, base_inter) +#' +#' # In the estimation, the variable treat is interacted +#' # with each value of period but 5, set as a reference +#' +#' # ggcoefplot will show all the coefficients: +#' ggcoefplot(est_did) +#' +#' +#' # Note that the grouping of the coefficients is due to 'group = "auto"' +#' +#' # If you want to keep only the coefficients +#' # created with i() (ie the interactions), use ggiplot #' ggiplot(est_did) #' -#' # Comparison with iplot defaults -#' iplot(est_did) -#' ggiplot(est_did, geom = 'errorbar') # closer iplot original +#' # We can see that the graph is different from before: +#' # - only interactions are shown, +#' # - the reference is present, +#' # => this is fully flexible #' -#' # Many of the arguments work the same as in iplot() -#' iplot(est_did, pt.join = TRUE) -#' ggiplot(est_did, pt.join = TRUE, geom_style = 'errorbar') +#' ggiplot(est_did, ci_level = c(0.8, 0.95)) +#' ggiplot(est_did, ref.line = FALSE, pt.join = TRUE, geom_style = "errorbar") +#' ggiplot(est_did, geom_style = "ribbon", col = "orange") +#' # etc #' -#' # Plots can be customized and tweaked easily -#' ggiplot(est_did, geom_style = 'ribbon') -#' ggiplot(est_did, geom_style = 'ribbon', col = 'orange') +#' # We can also use a dictionary to replace label values. The dicionary should +#' # take the form of a named vector or list, e.g. c("old_lab1" = "new_lab1", ...) #' -#' # Unlike base iplot, multiple confidence interval levels are supported -#' ggiplot(est_did, ci_level = c(.8, .95)) +#' # Let's create a "month" variable +#' all_months = c("aug", "sept", "oct", "nov", "dec", "jan", +#' "feb", "mar", "apr", "may", "jun", "jul") +#' # Turn into a dictionary by providing the old names +#' # Note the implication that treatment occured here in December (5 month in our series) +#' dict = all_months; names(dict) = 1:12 +#' # Pass our new dictionary to our ggiplot call +#' ggiplot(est_did, pt.join = TRUE, geom_style = "errorbar", dict = dict) #' -#' # Another new feature (i.e. unsupported in base iplot) is adding aggregated -#' # post- and/or pre-treatment effects to your plots. Here's an example that -#' # builds on the previous plot, by adding the mean post-treatment effect. -#' ggiplot(est_did, ci_level = c(.8, .95), -#' aggr_eff = "post", aggr_eff.par = list(col="orange")) # default is grey +#' # +#' # What if the interacted variable is not numeric? +#' +#' # let's re-use our all_months vector from the previous example, but add it +#' # directly to the dataset +#' base_inter$period_month = all_months[base_inter$period] +#' +#' # The new estimation +#' est = feols(y ~ x1 + i(period_month, treat, "oct") | id+period, base_inter) +#' # Since 'period_month' of type character, iplot/coefplot both sort it +#' ggiplot(est) +#' +#' # To respect a plotting order, use a factor +#' base_inter$month_factor = factor(base_inter$period_month, levels = all_months) +#' est = feols(y ~ x1 + i(month_factor, treat, "oct") | id + period, base_inter) +#' ggiplot(est) +#' +#' # dict -> c("old_name" = "new_name") +#' dict = all_months; names(dict) = 1:12; dict +#' ggiplot(est_did, dict = dict) #' #' # -#' # Example 2: Multiple estimation (i) +#' # Example 3: Setting defaults #' # #' -#' # We'll demonstrate using the staggered treatment example from the -#' # introductory fixest vignette. +#' # The customization logic of ggcoefplot/ggiplot works differently than the +#' # original base fixest counterparts, so we don't have "gg" equivalents of +#' # setFixest_coefplot and setFixest_iplot. However, you can still invoke some +#' # global fixest settings like setFixest_dict(). SImple example: #' -#' data(base_stagg) -#' est_twfe = feols(y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | id + year, base_stagg) -#' est_sa20 = feols(y ~ x1 + sunab(year_treated, year) | id + year, base_stagg) +#' base_inter$letter = letters[base_inter$period] +#' est_letters = feols(y ~ x1 + i(letter, treat, 'e') | id+letter, base_inter) +#' +#' # Set global dictionary for capitalising the letters +#' dict = LETTERS[1:10]; names(dict) = letters[1:10] +#' setFixest_dict(dict) #' -#' ggiplot(list('TWFE' = est_twfe, 'Sun & Abraham (2020)' = est_sa20), -#' main = 'Staggered treatment', ref.line = -1, pt.join = TRUE) +#' ggiplot(est_letters) #' -#' # If you don't like the presentation of 'dodged' models in a single frame, -#' # then it easy to facet them instead using multi_style = 'facet'. -#' ggiplot(list('TWFE' = est_twfe, 'Sun & Abraham (2020)' = est_sa20), -#' main = 'Staggered treatment', ref.line = -1, pt.join = TRUE, -#' multi_style = 'facet') +#' setFixest_dict() # reset #' #' # -#' # Example 3: Multiple estimation (ii) +#' # Example 4: group + cleaning #' # #' -#' # An area where ggiplot shines is in complex multiple estimation cases, such -#' # as lists of fixest_multi objects. To illustrate, let's add a split variable +#' # You can use the argument group to group variables +#' # You can further use the special character "^^" to clean +#' # the beginning of the coef. name: particularly useful for factors +#' +#' est = feols(Petal.Length ~ Petal.Width + Sepal.Length + +#' Sepal.Width + Species, iris) +#' +#' # No grouping: +#' ggcoefplot(est) +#' +#' # now we group by Sepal and Species +#' ggcoefplot(est, group = list(Sepal = "Sepal", Species = "Species")) +#' +#' # now we group + clean the beginning of the names using the special character ^^ +#' ggcoefplot(est, group = list(Sepal = "^^Sepal.", Species = "^^Species")) +#' +#' +#' # +#' # Example 5: Some more ggcoefplot/ggiplot extras +#' # +#' +#' # We'll demonstrate using the staggered treatment example from the +#' # introductory fixest vignette. +#' +#' data(base_stagg) +#' est_twfe = feols( +#' y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | id + year, +#' base_stagg +#' ) +#' est_sa20 = feols( +#' y ~ x1 + sunab(year_treated, year) | id + year, +#' data = base_stagg +#' ) +#' +#' # Plot both regressions in a faceted plot +#' ggiplot( +#' list('TWFE' = est_twfe, 'Sun & Abraham (2020)' = est_sa20), +#' main = 'Staggered treatment', ref.line = -1, pt.join = TRUE +#' ) +#' +#' # So far that's no different than base iplot (automatic legend aside). But an +#' # area where ggiplot shines is in complex multiple estimation cases, such as +#' # lists of fixest_multi objects. To illustrate, let's add a split variable #' # (group) to our staggered dataset. #' base_stagg_grp = base_stagg #' base_stagg_grp$grp = ifelse(base_stagg_grp$id %% 2 == 0, 'Evens', 'Odds') #' #' # Now re-run our two regressions from earlier, but splitting the sample to #' # generate fixest_multi objects. -#' est_twfe_grp = feols(y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | -#' id + year, base_stagg_grp, split = ~ grp) -#' est_sa20_grp = feols(y ~ x1 + sunab(year_treated, year) | -#' id + year, base_stagg_grp, split = ~ grp) +#' est_twfe_grp = feols( +#' y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | id + year, +#' data = base_stagg_grp, split = ~ grp +#' ) +#' est_sa20_grp = feols( +#' y ~ x1 + sunab(year_treated, year) | id + year, +#' data = base_stagg_grp, split = ~ grp +#' ) #' -#' # ggiplot combines with list of multi-estimation objects without a problem... +#' # ggiplot combines the list of multi-estimation objects without a problem... #' ggiplot(list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), -#' ref.line = -1, main = 'Staggered treatment: Split multi-sample') +#' ref.line = -1, main = 'Staggered treatment: Split multi-sample') #' -#' # ... but is even better when we use faceting instead of dodged errorbars. +#' # ... but is even better when we use facets instead of dodged errorbars. #' # Let's use this an opportunity to construct a fancy plot that invokes some #' # additional arguments and ggplot theming. -#' ggiplot(list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), -#' ref.line = -1, -#' main = 'Staggered treatment: Split multi-sample', -#' xlab = 'Time to treatment', -#' multi_style = 'facet', -#' geom_style = 'ribbon', -#' theme = theme_minimal() + -#' theme(text = element_text(family = 'HersheySans'), -#' plot.title = element_text(hjust = 0.5), -#' legend.position = 'none')) +#' ggiplot( +#' list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), +#' ref.line = -1, +#' main = 'Staggered treatment: Split multi-sample', +#' xlab = 'Time to treatment', +#' multi_style = 'facet', +#' geom_style = 'ribbon', +#' facet_args = list(labeller = labeller(id = \(x) gsub(".*: ", "", x))), +#' theme = theme_minimal() + +#' theme( +#' text = element_text(family = 'HersheySans'), +#' plot.title = element_text(hjust = 0.5), +#' legend.position = 'none' +#' ) +#' ) #' #' # #' # Aside on theming and scale adjustments @@ -155,33 +280,14 @@ #' # Setting the theme inside the `ggiplot()` call is optional and not strictly #' # necessary, since the ggplot2 API allows programmatic updating of existing #' # plots. E.g. -#' last_plot() + labs(caption = 'Note: Super fancy plot brought to you by ggiplot') -#' last_plot() + theme_void() + scale_colour_brewer(palette = 'Set1') +#' last_plot() + +#' labs(caption = 'Note: Super fancy plot brought to you by ggiplot') +#' last_plot() + +#' theme_grey() + +#' theme(legend.position = 'none') + +#' scale_fill_brewer(palette = 'Set1', aesthetics = c("colour", "fill")) #' # etc. #' -#' # -#' # Aside on dictionaries -#' # -#' -#' # Dictionaries work similarly to iplot. Simple example: -#' -#' base_inter$letter = letters[base_inter$period] -#' est_letters = feols(y ~ x1 + i(letter, treat, 'e') | id+letter, base_inter) -#' -#' ggiplot(est_letters) # No dictionary -#' -#' # Dictionary for capitalising the letters -#' dict = LETTERS[1:10]; names(dict) = letters[1:10] -#' -#' # You can either set the dictionary directly in the plot call. -#' ggiplot(est_letters, dict=dict) -#' -#' # Or, set it globally using the setFixest_dict macro -#' setFixest_dict(dict) -#' ggiplot(est_letters) -#' -#' setFixest_dict() # reset -#' ggiplot = function( object, geom_style = c('pointrange', 'errorbar', 'ribbon'), @@ -200,8 +306,11 @@ ggiplot = function( dots = list(...) ## Defaults + is_iplot = if (!is.null(dots[["is_iplot"]])) dots[["is_iplot"]] else TRUE + group = if (!is.null(dots[["group"]])) dots[["group"]] else "auto" keep = if (!is.null(dots[["keep"]])) dots[["keep"]] else NULL drop = if (!is.null(dots[["drop"]])) dots[["drop"]] else NULL + i.select = if (!is.null(dots[["i.select"]])) dots[["i.select"]] else 1 ci_level = if (!is.null(dots[["ci_level"]])) dots[["ci_level"]] else 0.95 ci.width = if (!is.null(dots[["ci.width"]])) dots[["ci.width"]] else 0.2 ci.fill.par = list(col = 'lightgray', alpha = 0.3) ## Note: The col arg is going be ignored anyway @@ -213,27 +322,29 @@ ggiplot = function( col = if (!is.null(dots[['col']])) dots[['col']] else NULL pt.pch = if (!is.null(dots[['pt.pch']])) dots[['pt.pch']] else NULL pt.join = if (!is.null(dots[['pt.join']])) dots[['pt.join']] else FALSE - zero = if (!is.null(dots[['zero']])) dots[['zero']] else TRUE - zero.par = list(col = 'black', lty = 1, lwd = 0.3) - if (!is.null(dots[['zero.par']])) zero.par = utils::modifyList(zero.par, dots[['zero.par']]) + ## hold off deciding zero line until we have seen the data + # zero = if (!is.null(dots[['zero']])) dots[['zero']] else TRUE + # zero.par = list(col = 'black', lty = 1, lwd = 0.3)i + # if (!is.null(dots[['zero.par']])) zero.par = utils::modifyList(zero.par, dots[['zero.par']]) ref.line = if (!is.null(dots[['ref.line']])) dots$ref.line else 'auto' + if (isFALSE(ref.line)) ref.line = NULL ref.line.par = list(col = "black", lty = 2, lwd = 0.3) - if (!is.null(dots[["ref.line.par"]])) ref.line.par = utils::modifyList(ref.line.par, dots[["ref.line.par"]]) - # The next few blocks grab the underlying iplot data, contingent on the + # The next few blocks grab the underlying iplot/coefplot data, contingent on the # object that was passed into the function (i.e. fixest, fixest_multi, or # list) + iplot_data_func = ifelse(isTRUE(is_iplot), iplot_data, coefplot_data) if (inherits(object, c("fixest", "fixest_multi"))) { if (length(ci_level) == 1) { - data = iplot_data(object, .ci_level = ci_level, .dict = dict, .aggr_es = aggr_eff, .keep = keep, .drop = drop) + data = iplot_data_func(object, .ci_level = ci_level, .dict = dict, .aggr_es = aggr_eff, .keep = keep, .drop = drop, .group = group, .i.select = i.select) } else { data = lapply( ci_level, - function(ci_l) iplot_data(object, .ci_level = ci_l, .dict = dict, .aggr_es = aggr_eff, .keep = keep, .drop = drop) + function(ci_l) iplot_data_func(object, .ci_level = ci_l, .dict = dict, .aggr_es = aggr_eff, .keep = keep, .drop = drop, .group = group, .i.select = i.select) ) data = do.call("rbind", data) } @@ -252,14 +363,14 @@ ggiplot = function( if (inherits(object, "list")) { if (length(ci_level) == 1) { data = lapply( - object, iplot_data, - .ci_level = ci_level, .dict = dict, .aggr_es = aggr_eff + object, iplot_data_func, + .ci_level = ci_level, .dict = dict, .aggr_es = aggr_eff, .group = group, .i.select = i.select ) } else { data = lapply(ci_level, function(ci_l) { - lapply(object, iplot_data, + lapply(object, iplot_data_func, .ci_level = ci_l, - .dict = dict, .aggr_es = aggr_eff + .dict = dict, .aggr_es = aggr_eff, .group = group, .i.select = i.select ) }) data = do.call(function(...) Map("rbind", ...), data) @@ -293,11 +404,35 @@ ggiplot = function( if (is.null(facet_args$ncol)) facet_args$ncol = length(unique(data$group)) } + # Prep data for nested grouping + has_groups = (!is.null(attributes(data)[["has_groups"]]) && isTRUE(attributes(data)[["has_groups"]])) + if (isTRUE(has_groups)) { + data[["x"]] = interaction( + data[["x"]], data[["group_var"]], + sep = "___", drop = TRUE#, lex.order = TRUE + ) + data[["group_var"]] = NULL + } + + yrange = range(c(data[["ci_low"]], data[["ci_high"]]), na.rm = TRUE) + spans_zero = any(yrange > 0) && any(yrange < 0) + zero = if (!is.null(dots[['zero']])) { + dots[['zero']] } + else if (is_iplot || spans_zero) { + TRUE + } else { + FALSE + } + zero.par = list(col = 'black', lty = 1, lwd = 0.3) + if (!is.null(dots[['zero.par']])) zero.par = utils::modifyList(zero.par, dots[['zero.par']]) + + + if (multi_style == "dodge") ci.width = ci.width * n_fcts - if (is.null(xlab)) xlab = sub("::.*", "", data$estimate_names_raw[1]) + if (is.null(xlab) & isTRUE(is_iplot)) xlab = sub("::.*", "", data$estimate_names_raw[1]) if (!is.null(ref.line)) { - if (ref.line == "auto") ref.line = data$x[which(data$is_ref)[1]] + if (ref.line == "auto" && isTRUE(is_iplot)) ref.line = data$x[which(data$is_ref)[1]] } if (is.null(ylab)) ylab = paste0("Estimate and ", oxford(paste0(ci_level * 100, "%")), " Conf. Int.") if (is.null(main)) main = paste0("Effect on ", oxford(unique(data$lhs))) @@ -541,14 +676,20 @@ ggiplot = function( } + { if (is.numeric(data$x)) { + labels_func = ifelse(is_iplot, function(x) dict_apply(x, dict = dict), waiver) if (length(unique(data$x)) == 2) { # nicer gridlines for 2x2 case - scale_x_continuous(breaks = unique(data$x)) + scale_x_continuous(breaks = unique(data$x), labels = labels_func) } else { - scale_x_continuous(breaks = scales::pretty_breaks()) + scale_x_continuous(breaks = scales::breaks_pretty(), labels = labels_func) } } } + labs(x = xlab, y = ylab, title = main) + { + if (has_groups) { + scale_x_discrete(guide = ggh4x::guide_axis_nested(delim = "___")) + } + } + + { if (multi_style == "facet") { facet_wrap( facets = facet_args$facets, @@ -579,6 +720,44 @@ ggiplot = function( ) } + if (has_groups) { + gg = gg + + theme(ggh4x.axis.nestline = element_line(linewidth = 0.5)) + } + return(gg) } + + + + +#' @describeIn ggiplot This function plots the results of estimations +#' (coefficients and confidence intervals). The function `ggiplot` restricts +#' the output to variables created with i, either interactions with factors or +#' raw factors. +#' @export +ggcoefplot = function( + object, + geom_style = c('pointrange', 'errorbar'), + multi_style = c('dodge', 'facet'), + facet_args = NULL, + theme = NULL, + ... + ) { + + + geom_style = match.arg(geom_style) + multi_style = match.arg(multi_style) + + ggiplot( + object = object, + geom_style = geom_style, + multi_style = multi_style, + facet_args = facet_args, + theme = theme, + is_iplot = FALSE, + ... + ) + + } diff --git a/R/iplot_data.R b/R/iplot_data.R index 9b23786..7ab4497 100644 --- a/R/iplot_data.R +++ b/R/iplot_data.R @@ -13,10 +13,31 @@ #' @param .drop Character vector used to subset the coefficients of interest #' (complement of `.keep`). Passed down to `fixest::iplot(..., drop = .drop)` #' and should take the form of an acceptable regular expression. +#' @param .group A list, default is missing. Each element of the list reports +#' the coefficients to be grouped while the name of the element is the group +#' name. Passed down to `fixest::coefplot(..., group = .group)`. Example of +#' valid uses: +#' ⁠group=list(group_name=\"pattern\")⁠, +#' ⁠group=list(group_name=c(\"var_start\", \"var_end\"))⁠, +#' ⁠group=list(group_name=1:2)) +#' See the Details section of `?fixest::coefplot` for more. #' @param .dict A dictionary (i.e. named character vector or a logical scalar). #' Used for changing coefficient names. Defaults to the values in -#' `getFixest_dict()`. See the `?fixest::iplot` documentation for more -#' information. +#' `getFixest_dict()`. See the `?fixest::coefplot` documentation for more +#' information. Note: This argument applies dictionary changes directly to the +#' return object for `coefplot_data`. However, it is ignored for `iplot_data`, +#' since we want to preserve the numeric ordering for potential event study +#' plots. (And imposing an ordered factor would create its own downstream +#' problems in the case of continuous plot features like ribbons.) Instead, any +#' dictionary replacement for `ggiplot` is deferred to the actual plot call and +#' applied directly to the labels. +#' @param .internal.only.i Logical variable used for some internal function +#' handling when passing on to coefplot/iplot. +#' @param .i.select Integer scalar, default is 1. In (gg)iplot, used to select +#' which variable created with i() to select. Only used when there are several +#' variables created with i. This is an index, just try increasing numbers to +#' hopefully obtain what you want. Passed down to +#' `fixest::iplot(..., i.select = .i.select)` #' @param .aggr_es A character string indicating whether the aggregated mean #' post- (and/or pre-) treatment effect should be added as a column to the #' returned data frame. Passed to `aggr_es(..., aggregation = "mean")` and @@ -47,15 +68,31 @@ #' iplot_data(est_split) # The wrapper provided by this package #' iplot_data = function( - object, - .ci_level = 0.95, - .keep = NULL, - .drop = NULL, - .dict = fixest::getFixest_dict(), - .aggr_es = c("none", "post", "pre", "both") + object, + .ci_level = 0.95, + .keep = NULL, + .drop = NULL, + .dict = fixest::getFixest_dict(), + .internal.only.i = TRUE, + .i.select = 1, + .aggr_es = c("none", "post", "pre", "both"), + .group = "auto" ) { - .aggr_es = match.arg(.aggr_es) - p = fixest::iplot(object, only.params = TRUE, ci_level = .ci_level, dict = .dict, keep = .keep, drop = .drop) + + .aggr_es = match.arg(.aggr_es) + if (isFALSE(.internal.only.i)) { + ## No pre/post aggregation allowed for coefplot + if (.aggr_es!="none") warning("The .aggr_es argument will be ignored with (gg)coefplot calls.\n") + .aggr_es = "none" + } + + if (isTRUE(.internal.only.i)) { + # No grouping permitted for iplot + if (!is.null(.group) && .group!="auto") warning("The .group argument will be ignored with (gg)iplot calls.\n") + .group = NULL + } + + p = fixest::coefplot(object, only.params = TRUE, ci_level = .ci_level, dict = .dict, keep = .keep, drop = .drop, internal.only.i = .internal.only.i, i.select = .i.select) d = p$prms if (inherits(object, "fixest_multi")) { @@ -100,22 +137,288 @@ iplot_data = function( d$ci_level = .ci_level + # + ## Grouping variables with (gg)coefplot + ## + ## GM note: The next section of code, which is quite lengthy, is adapted from + ## fixest::coefplot plotting internals. We need to implement those changes a + ## bit further upstream to ensure that they are recorded in the return data + ## frame object for this coefplot_data() function. (Put differently: Because + ## ggplot2 understands data frames, we need to make sure that the grouping + ## info and adjustments are encoded in the data frame before plot time.) + # + + x_labels = d$estimate_names + # x_labels_raw = d$estimate_names_raw + x_labels_raw = d$estimate_names + group = .group + dict = .dict + + # group = auto #### + is_iplot = .internal.only.i + + # The value of group = "auto" => renaming the labels + if(identical(group, "auto") && is_iplot == FALSE){ + # we change the names of interactions + qui = grepl(":", x_labels_raw) + if(any(qui)){ + x_inter = gsub(":.+", "", x_labels_raw[qui]) + tx_inter = table(x_inter) + qui_auto = names(tx_inter)[tx_inter >= 2] + + group = list() + + for(i in seq_along(qui_auto)){ + var_left = qui_auto[i] + qui_select = substr(x_labels_raw, 1, nchar(var_left)) == var_left + + x_select = x_labels_raw[qui_select] + + is_inter = TRUE + n_trim = 0 + group_regex = NULL + if(all(grepl("[^:]:[^:].*::", x_select))){ + # treat:period::1 + first_part = strsplit(x_select[1], "::")[[1]][1] + var_right = gsub(".+:", "", first_part) + n_max = nchar(first_part) + 2 + + } else if(all(grepl("::.*[^:]:[^:]", x_select))){ + # period::1:treat + # Note that we always put 'treat' on the left + second_part = strsplit(x_select[1], "::")[[1]][2] + var_right = var_left + var_left = gsub(".+:", "", second_part) + n_max = nchar(var_right) + 2 + n_trim = nchar(var_left) + 1 + + group_regex = paste0("%", escape_regex(var_right), "::.+:", escape_regex(var_left)) + + } else if(all(grepl("::", x_select))) { + is_inter = FALSE + # This is a fixest factor + n_max = nchar(var_left) + 2 + + } else { + # we need to find out by ourselves... + n = min(nchar(x_select[1:2])) + x_split = strsplit(substr(x_select[1:2], 1, n), "") + start_diff = which.max(x_split[[1]] != x_split[[2]]) + + ok = FALSE + for(n_max in n:(nchar(var_left) + 2)){ + if(all(grepl(substr(x_select[1], 1, n_max), x_select, fixed = TRUE))){ + ok = TRUE + break + } + } + + if(!ok) next + + var_right = gsub(".+:", "", substr(x_select[1], 1, n_max)) + } + + if(is_inter){ + v_name = dict_apply(c(var_left, var_right), dict) + group_name =replace_and_make_callable("__x__ %*% (__y__ == ldots)", list(x = v_name[1], y = v_name[2]), text_as_expr = TRUE) + + if(is.null(group_regex)){ + group[[group_name]] = escape_regex(paste0("%", var_left, ":", var_right)) + } else { + group[[group_name]] = group_regex + } + + } else { + v_name = dict_apply(var_left, dict) + group_name =replace_and_make_callable("__x__", list(x = v_name), text_as_expr = TRUE) + + group[[group_name]] = paste0("%^", escape_regex(var_left)) + + } + + # We update the labels + x_labels[qui_select] = substr(x_select, n_max + 1, nchar(x_select) - n_trim) + } + } + ## Skip this iplot dictionary step and defer to actual plotting step. + ## Reason: We want to preserve the numeric x labs for correct order in + ## later plot (and factors won't work since this makes lines and ribbon + ## connections problematic). + # } else if (is_iplot) { + # # apply dictionary to iplot x values + # d[["x"]] = dict_apply(d[["x"]], dict = dict) + } + + IS_GROUP = !identical(group, "auto") && !missing(group) && !is.null(group) && length(group) > 0 && !is.null(x_labels) + + + # This means there are groups! + if(IS_GROUP){ + d[["group_var"]] = "" + + # We perform basic checks on the group + if(!is.list(group)) stop("Argument 'group' must be a list.") + + for(i in seq_along(group)){ + my_group = group[[i]] + + # We just take care of the special group + cleaning case + # when group starts with ^^ + if(length(my_group) == 1 && is.character(my_group) && substr(my_group, 1, 2) == "^^") { + + + if(grepl("^%", my_group)){ + qui = grepl(gsub("^%", "", my_group), x_labels_raw) + } else { + qui = grepl(my_group, x_labels) + } + + + if(!any(qui)){ + warning("In argument 'group', the pattern: \"", my_group, "\", did not match any coefficient name.") + group[[i]] = NULL + if(length(group) == 0) IS_GROUP = FALSE + next + } + + group[[i]] = range(which(qui)) + x_labels = gsub(my_group, "", x_labels) + } + ## end of ^^ special case check + + group_name = names(group)[i] + my_group = group[[i]] # reset my_group var for checking and formatting + + # formatting properly + if(is.numeric(my_group)){ + g_start = min(my_group) + g_end = max(my_group) + + if(any(my_group %% 1 != 0)){ + stop("The elements of the argument 'group' must be integers (e.g. group=list(group_name=1:2)). Currently they are numeric but not integers. Alternatively, you could use coefficient names (see details).") + } + + if(g_start < 1){ + warning("The elements of the argument 'group' must be integers ranging from 1 to the number of coefficients. The value of ", g_start, " has been replaced by 1.") + } + + # if(g_end > nrow(prms)){ + if(g_end > nrow(d)){ ## GM changed + warning("The elements of the argument 'group' must be integers ranging from 1 to the number of coefficients (here ", g_end, "). The value of ", g_end, " has been replaced by ", g_end, ".") + } + + } else { + + if(!is.character(my_group)){ + stop("The elements of the argument 'group' must be either: i) a character string of length 1 or 2, or ii) integers. Currently it is not character nor numeric.\nExample of valid use: group=list(group_name=\"pattern\"), group=list(group_name=c(\"var_start\", \"var_end\")), group=list(group_name=1:2))") + } + + if(!length(my_group) %in% 1:2){ + stop("The elements of the argument 'group' must be either: i) a character string of length 1 or 2, or ii) integers. Currently it is a character of length ", length(my_group), ".\nExample of valid use: group=list(group_name=\"pattern\"), group=list(group_name=c(\"var_start\", \"var_end\")), group=list(group_name=1:2))") + } + + if(length(my_group) == 1){ + # This is a pattern + if(grepl("^%", my_group)){ + qui = grepl(gsub("^%", "", my_group), x_labels_raw) + } else { + qui = grepl(my_group, x_labels) + } + + if(!any(qui)){ + warning("In argument 'group', the pattern: \"", my_group, "\", did not match any coefficient name.") + next + } + + } else { + # This is a pattern + check = c(FALSE, FALSE) + qui = rep(FALSE, length(x_labels)) + for(i in 1:2){ + val = my_group[i] + if(grepl("^%", val)){ + val = gsub("^%", "", val) + check = val %in% x_labels_raw + qui = qui | x_labels_raw %in% val + } else { + check = val %in% x_labels + qui = qui | x_labels %in% val + } + } + + check = my_group %in% x_labels + if(!all(check)){ + warning("In argument 'group', the value", dreamerr::enumerate_items(my_group[check], "s.quote"), ", did not match any coefficient name.") + next + } + + } + + qui_nb = which(qui) + + g_start = min(qui_nb) + g_end = max(qui_nb) + } + + if(!is.null(group_name)){ + if(grepl("^&", group_name)){ + group_name = eval(str2lang(gsub("^&", "", group_name))) + group_var = gsub("==", "=", gsub("\\%\\*\\%", "\u00D7", gsub("ldots", "...", gsub("\"", "", sprintf("%s", deparse1(group_name)))))) + } else { + group_var = group_name + } + + d[["group_var"]][qui] = group_var + } else { + d[["group_var"]][qui] = strrep(" ", i) + } + + } + + d[["x"]] = factor(x_labels, levels = unique(x_labels)) + d[["group_var"]] = factor(d[["group_var"]], levels = unique(d[["group_var"]])) + attr(d, "has_groups") = TRUE + + } + + if (.aggr_es != "none") { - ea = aggr_es(object, period = .aggr_es) - ref_idx = which(d$is_ref) - d$aggr_eff = 0 - if (.aggr_es %in% c("post", "both")) { - if (ref_idx < nrow(d)) { - d$aggr_eff[(ref_idx + 1):nrow(d)] = ea$estimate[which(ea$term == "post-treatment (mean)")] - } - } - if (.aggr_es %in% c("pre", "both")) { - if (ref_idx > 1) { - d$aggr_eff[1:(ref_idx - 1)] = ea$estimate[which(ea$term == "pre-treatment (mean)")] - } - } + ea = aggr_es(object, period = .aggr_es) + ref_idx = which(d$is_ref) + d$aggr_eff = 0 + if (.aggr_es %in% c("post", "both")) { + if (ref_idx < nrow(d)) { + d$aggr_eff[(ref_idx + 1):nrow(d)] = ea$estimate[which(ea$term == "post-treatment (mean)")] + } + } + if (.aggr_es %in% c("pre", "both")) { + if (ref_idx > 1) { + d$aggr_eff[1:(ref_idx - 1)] = ea$estimate[which(ea$term == "pre-treatment (mean)")] + } + } } + row.names(d) = NULL + return(d) } + + + +#' @describeIn iplot_data Internal function for grabbing and preparing coefplot data +coefplot_data = function( + object, + .ci_level = 0.95, + .keep = NULL, + .drop = NULL, + .group = "auto", + .dict = fixest::getFixest_dict(), + .internal.only.i = FALSE, + .i.select = 1, + .aggr_es = "none" +) { + + iplot_data(object, .ci_level = .ci_level, .dict = .dict, .keep = .keep, .drop = .drop, .internal.only.i = .internal.only.i, .group = .group) + + } diff --git a/R/utils.R b/R/utils.R index 35854f5..0770947 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,3 +1,4 @@ +#' @keywords internal oxford = function(some_vec) { if (length(some_vec)>2) { paste0("[", paste(utils::head(some_vec, -1), collapse = ", "), ", & ", utils::tail(some_vec, 1), "]") @@ -7,3 +8,169 @@ oxford = function(some_vec) { paste(some_vec) } } + +## +## Unexported functions borrowed from fixest + +#' @keywords internal +escape_regex = function(x){ + # escape special characters in regular expressions => to make it as "fixed" + + res = gsub("((?<=[^\\\\])|(?<=^))(\\$|\\.|\\+|\\(|\\)|\\[|\\]|\\?|\\^)", "\\\\\\2", x, perl = TRUE) + res +} + + +#' @keywords internal +dict_apply = function(x, dict = NULL){ + + dreamerr::check_arg(dict, "NULL named character vector no na", .message = "The argument `dict` must be a dictionnary, ie a named vector (eg dict=c(\"old_name\"=\"new_name\")") + + if(missing(dict) || length(dict) == 0){ + return(x) + } + + # We make the dictionary names space agnostic, adds a handful of us only + if(any(grepl(" ", x, fixed = TRUE))){ + x_tmp = gsub(" ", "", x, fixed = TRUE) + } else { + x_tmp = x + } + + if(any(grepl(" ", names(dict), fixed = TRUE))){ + names(dict) = gsub(" ", "", names(dict), fixed = TRUE) + if(anyDuplicated(names(dict))){ + dup = duplicated(names(dict)) + stop("The dictionary `dict` contains several items with the same names, it concerns ", + dreamerr::enumerate_items(names(dict)[dup]), " (note that spaces are ignored).") + } + } + + who = x_tmp %in% names(dict) + x[who] = dict[as.character(x_tmp[who])] + x +} + + +#' @keywords internal +replace_and_make_callable = function(text, varlist, text_as_expr = FALSE){ + # used to make a character string callable and substitutes variables inside text with the variables + # in varlist + # ex: "Interacted with __var__" becomes "Interacted with x_beta" + # or: "&paste(\"Interacted with \", x[beta])" + + if(length(text) > 1) stop("Internal problem: length of text should not be greater than 1.") + + text_split = strsplit(paste0(text, " "), "__")[[1]] + + if(length(text_split) < 3){ + # Nothing to be done! + return(text) + } else { + # We need to replace the variables + is_var = seq_along(text_split) %% 2 == 0 + my_variables = text_split[is_var] + + if(length(varlist) == 0 || any(!my_variables %in% names(varlist))){ + info = "No special variable is available for this estimation." + if(length(varlist) > 0){ + info = paste0("In this estimation, the only special variable", dreamerr::enumerate_items(paste0("__", names(varlist), "__"), "s.is.start"), ". ") + } + + # warning(info, enumerate_items(paste0("__", setdiff(my_variables, names(varlist)), "__"), "is"), " not valid, thus ignored.", call. = FALSE) + + return("") + + not_var = !my_variables %in% names(varlist) + is_var[is_var][not_var] = FALSE + my_variables = intersect(my_variables, names(varlist)) + if(length(my_variables) == 0){ + return(text) + } + } + + my_variables_values = varlist[my_variables] + + if(any(lengths(varlist[my_variables]) > 1)){ + qui = which(lengths(varlist[my_variables]) > 1)[1] + n_val = lengths(varlist[my_variables])[qui] + warning("The special variable __", my_variables[qui], "__ takes ", n_val, " values, only the first is used.", call. = FALSE) + my_variables_values = sapply(my_variables_values, function(x) x[1]) + } else { + my_variables_values = unlist(my_variables_values) + } + + # we prepare the result (we drop the last space) + n = length(text_split) + text_new = text_split + text_new[n] = gsub(" $", "", text_new[n]) + if(nchar(text_new[n]) == 0){ + text_new = text_new[-n] + is_var = is_var[-n] + } + + + # Do the variables contain expressions? + is_expr = grepl("^&", my_variables_values) + if(any(is_expr)){ + + expr_drop = function(x){ + if(grepl("^&", x)){ + res = gsub("^&", "", x) + if(grepl("^expression\\(", res)){ + res = gsub("(^expression\\()|(\\)$)", "", res) + } else if(grepl("^substitute\\(", res)){ + res = deparse(eval(str2lang(res))) + } + } else { + res = x + } + res + } + + my_vars = sapply(my_variables_values, expr_drop) + + if(text_as_expr){ + text_new[is_var][is_expr] = my_vars[is_expr] + + if(all(is_expr)){ + text_new = paste0("&expression(", paste(text_new, collapse = " "), ")") + } else { + my_var_no_expr = paste0('"', my_vars, '"')[!is_expr] + new_names = paste0("x___", seq_along(my_var_no_expr)) + text_new[is_var][!is_expr] = new_names + text_new = paste0("&substitute(", paste(text_new, collapse = " "), ", list(", paste0(new_names, "=", my_var_no_expr, collapse = ", "), "))") + } + } else { + text_new = paste0('"', text_new, '"') + text_new[is_var][is_expr] = my_vars[is_expr] + + if(all(is_expr)){ + text_new = paste0("&expression(paste(", paste(text_new, collapse = ", "), "))") + } else { + my_var_no_expr = paste0('"', my_vars, '"')[!is_expr] + new_names = paste0("x___", seq_along(my_var_no_expr)) + text_new[is_var][!is_expr] = new_names + text_new = paste0("&substitute(paste(", paste(text_new, collapse = ", "), "), list(", paste0(new_names, "=", my_var_no_expr, collapse = ", "), "))") + } + + } + + return(text_new) + } else { + # They don't contain expressions => fine, we just replace with the variables + if(text_as_expr){ + my_vars = paste0('"', my_variables_values, '"') + new_names = paste0("x___", seq_along(my_vars)) + text_new[is_var] = new_names + text_new = paste0("&substitute(", paste(text_new, collapse = ""), ", list(", paste0(new_names, "=", my_vars, collapse = ", "), "))") + } else { + text_new[is_var] = my_variables_values + text_new = paste(text_new, collapse = "") + } + + return(text_new) + } + + } +} diff --git a/README.Rmd b/README.Rmd index 988c900..175efbb 100644 --- a/README.Rmd +++ b/README.Rmd @@ -22,11 +22,14 @@ knitr::opts_chunk$set( [![R-CMD-check](https://github.com/grantmcdermott/ggiplot/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/grantmcdermott/ggiplot/actions/workflows/R-CMD-check.yaml) -This package provides a **ggplot2** equivalent of the base -[`fixest::iplot()`](https://lrberge.github.io/fixest/reference/coefplot.html) -function. The goal of **ggiplot** is to produce nice -[event study](https://theeffectbook.net/ch-EventStudies.html) plots with minimal -effort, but with lots of scope for further customization. +This package provides **ggplot2** equivalents of the (base) +[`fixest::coefplot`](https://lrberge.github.io/fixest/reference/coefplot.html) +and +[`fixest::iplot`](https://lrberge.github.io/fixest/reference/coefplot.html) +functions. The goal of **ggiplot** is to produce nice coefficient and +interaction plots (including +[event study](https://theeffectbook.net/ch-EventStudies.html) plots) with +minimal effort, but with lots of scope for further customization. ## Installation @@ -38,28 +41,64 @@ install.packages("ggiplot", repos = "https://grantmcdermott.r-universe.dev") ## Quickstart -A detailed -[introductory vignette](http://grantmcdermott.com/ggiplot/articles/ggiplot.html) -with many examples is provided on the package homepage (or, by typing -`vignette("ggiplot")` in your R console). But here are a few quickstart examples -to whet your appetite. First, a basic event study plot. +The [package website]([introductory vignette](http://grantmcdermott.com/ggiplot) +provides a number of examples in the help documentation. (Also available by +typing `?ggcoefplot` or `?ggiplot` in your R console.) But here are a few +quickstart examples to whet your appetite. -```{r example1, message=FALSE} -library(ggiplot) +Start by loading the **ggiplot** and **fixest** packages. Note that +**ggiplot** _only_ supports **fixest** model objects, so the latter must be +loaded alongside the former. + +```{r pkgload, message=FALSE} library(fixest) +library(ggiplot) +``` + +### Coefficient plots + +Use `ggcoefplot` to draw basic coefficient plots. + +```{r coefplot1, message=FALSE} +est = feols( + Petal.Length ~ Petal.Width + Sepal.Length + Sepal.Width + Species, + data = iris +) + +# coefplot(est) ## base version +ggcoefplot(est) ## this package +``` + +The above plot call and output should look very familiar to regular **fixest** +users. Like its base equivalent, `ggcoefplot` can be heavily customized and +contains various shortcuts for common operations. For example, we can use regex +the control the coefficient grouping logic. + +```{r coefplot2, message=FALSE} +ggcoefplot(est, group = list(Sepal = "^^Sepal.", Species = "^^Species")) +``` +### Event study plots + +The `ggiplot` function is a special case of `ggocoefplot` that only plots +coefficients with factor levels or interactions (specifically, those created +with the [`i()`](https://lrberge.github.io/fixest/reference/i.html) +operator). This is especially useful for producing event study plots in a +difference-in-differences (DiD) setup. + +```{r es1, message=FALSE} est_did = feols(y ~ x1 + i(period, treat, 5) | id+period, base_did) # iplot(est_did) ## base version ggiplot(est_did) ## this package ``` -The above plot call and output should look very familiar to regular **fixest** -users. But note that `ggiplot()` supports several features that are not -available in the base `iplot()` version. For example, plotting multiple +Again, the above plot call and output should look very familiar to regular +**fixest** users. But note that `ggiplot` supports several features that are +not available in the base `iplot` version. For example, plotting multiple confidence intervals and aggregate treatments effects. -```{r example2} +```{r es2} ggiplot( est_did, ci_level = c(.8, .95), @@ -71,7 +110,7 @@ ggiplot( And you can get quite fancy, combining lists of complex multiple estimation objects with custom themes, and so on. -```{r example3} +```{r es3} base_stagg_grp = base_stagg base_stagg_grp$grp = ifelse(base_stagg_grp$id %% 2 == 0, 'Evens', 'Odds') @@ -101,3 +140,7 @@ ggiplot( ) ) ``` + +For more `ggiplot` examples and comparisons with its base counterpart, see the +detailed [vignette](http://grantmcdermott.com/ggiplot/articles/ggiplot.html) on +the package homepage (or, by typing `vignette("ggiplot")` in your R console). diff --git a/README.md b/README.md index c16b34f..3d6e319 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,13 @@ badge](https://grantmcdermott.r-universe.dev/badges/ggiplot)](https://grantmcder [![R-CMD-check](https://github.com/grantmcdermott/ggiplot/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/grantmcdermott/ggiplot/actions/workflows/R-CMD-check.yaml) -This package provides a **ggplot2** equivalent of the base -[`fixest::iplot()`](https://lrberge.github.io/fixest/reference/coefplot.html) -function. The goal of **ggiplot** is to produce nice [event -study](https://theeffectbook.net/ch-EventStudies.html) plots with +This package provides **ggplot2** equivalents of the (base) +[`fixest::coefplot`](https://lrberge.github.io/fixest/reference/coefplot.html) +and +[`fixest::iplot`](https://lrberge.github.io/fixest/reference/coefplot.html) +functions. The goal of **ggiplot** is to produce nice coefficient and +interaction plots (including [event +study](https://theeffectbook.net/ch-EventStudies.html) plots) with minimal effort, but with lots of scope for further customization. ## Installation @@ -27,28 +30,71 @@ install.packages("ggiplot", repos = "https://grantmcdermott.r-universe.dev") ## Quickstart -A detailed [introductory -vignette](http://grantmcdermott.com/ggiplot/articles/ggiplot.html) with -many examples is provided on the package homepage (or, by typing -`vignette("ggiplot")` in your R console). But here are a few quickstart -examples to whet your appetite. First, a basic event study plot. +The \[package website\]([introductory +vignette](http://grantmcdermott.com/ggiplot) provides a number of +examples in the help documentation. (Also available by typing +`?ggcoefplot` or `?ggiplot` in your R console.) But here are a few +quickstart examples to whet your appetite. + +Start by loading the **ggiplot** and **fixest** packages. Note that +**ggiplot** *only* supports **fixest** model objects, so the latter must +be loaded alongside the former. ``` r -library(ggiplot) library(fixest) +library(ggiplot) +``` + +### Coefficient plots + +Use `ggcoefplot` to draw basic coefficient plots. + +``` r +est = feols( + Petal.Length ~ Petal.Width + Sepal.Length + Sepal.Width + Species, + data = iris +) + +# coefplot(est) ## base version +ggcoefplot(est) ## this package +``` + + +The above plot call and output should look very familiar to regular +**fixest** users. Like its base equivalent, `ggcoefplot` can be heavily +customized and contains various shortcuts for common operations. For +example, we can use regex the control the coefficient grouping logic. + +``` r +ggcoefplot(est, group = list(Sepal = "^^Sepal.", Species = "^^Species")) +``` + + + +### Event study plots + +The `ggiplot` function is a special case of `ggocoefplot` that only +plots coefficients with factor levels or interactions (specifically, +those created with the +[`i()`](https://lrberge.github.io/fixest/reference/i.html) operator). +This is especially useful for producing event study plots in a +difference-in-differences (DiD) setup. + +``` r est_did = feols(y ~ x1 + i(period, treat, 5) | id+period, base_did) # iplot(est_did) ## base version ggiplot(est_did) ## this package ``` - + -The above plot call and output should look very familiar to regular -**fixest** users. But note that `ggiplot()` supports several features -that are not available in the base `iplot()` version. For example, -plotting multiple confidence intervals and aggregate treatments effects. +Again, the above plot call and output should look very familiar to +regular **fixest** users. But note that `ggiplot` supports several +features that are not available in the base `iplot` version. For +example, plotting multiple confidence intervals and aggregate treatments +effects. ``` r ggiplot( @@ -58,7 +104,7 @@ ggiplot( ) ``` - + And you can get quite fancy, combining lists of complex multiple estimation objects with custom themes, and so on. @@ -94,4 +140,10 @@ ggiplot( ) ``` - + + +For more `ggiplot` examples and comparisons with its base counterpart, +see the detailed +[vignette](http://grantmcdermott.com/ggiplot/articles/ggiplot.html) on +the package homepage (or, by typing `vignette("ggiplot")` in your R +console). diff --git a/_pkgdown.yml b/_pkgdown.yml index 3faaa17..ca6ded5 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -2,3 +2,15 @@ url: http://grantmcdermott.com/ggiplot/ template: bootstrap: 5 +navbar: + type: default + left: + - text: Vignettes + menu: + - text: Comparing ggiplot with iplot + href: articles/ggiplot.html + - text: News + href: news/index.html + - text: Reference + href: reference/index.html + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_did.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_did.svg new file mode 100644 index 0000000..0a9176a --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_did.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +5 + + + + + + + + + + + + +x1 +1 +2 +3 +4 +6 +7 +8 +9 +10 + +treat × (period = ...) +Estimate and 95% Conf. Int. +Effect on y + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_group_names_prefix.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_group_names_prefix.svg new file mode 100644 index 0000000..d181636 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_group_names_prefix.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-1 +0 +1 +2 + + + + + + + + + + +Constant +Petal.Width +Length +Width +versicolor +virginica + + +Sepal +Species +Estimate and 95% Conf. Int. +Effect on Petal.Length + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_group_nonames.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_group_nonames.svg new file mode 100644 index 0000000..76c9dd8 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_group_nonames.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-1 +0 +1 +2 + + + + + + + + + + +Constant +Petal.Width +Sepal.Length +Sepal.Width +Speciesversicolor +Speciesvirginica + + + + +Estimate and 95% Conf. Int. +Effect on Petal.Length + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_group_none.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_group_none.svg new file mode 100644 index 0000000..f4e4baa --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_group_none.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-1 +0 +1 +2 + + + + + + + + + + +Constant +Petal.Width +Sepal.Length +Sepal.Width +Speciesversicolor +Speciesvirginica +Estimate and 95% Conf. Int. +Effect on Petal.Length + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_groupnames.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_groupnames.svg new file mode 100644 index 0000000..a9860f6 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_groupnames.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-1 +0 +1 +2 + + + + + + + + + + +Constant +Petal.Width +Sepal.Length +Sepal.Width +Speciesversicolor +Speciesvirginica + + +Sepal +Species +Estimate and 95% Conf. Int. +Effect on Petal.Length + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_interactions.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_interactions.svg new file mode 100644 index 0000000..06db9c5 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_interactions.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.00 +0.05 +0.10 +0.15 + + + + + + + + + +0 +1 +4:wt +6:wt +8:wt + + +hp × (am = ...) +disp × (cyl = ...) +Estimate and 95% Conf. Int. +Effect on mpg + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_interactions_multici.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_interactions_multici.svg new file mode 100644 index 0000000..f66f4c1 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_interactions_multici.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.00 +0.05 +0.10 +0.15 + + + + + + + + + +0 +1 +4:wt +6:wt +8:wt + + +hp × (am = ...) +disp × (cyl = ...) +Estimate and [80% & 95%] Conf. Int. +Effect on mpg + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_multi.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_multi.svg new file mode 100644 index 0000000..2de3595 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_multi.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +100 +200 + + + + + + + +Constant +4 +6 +8 + +wt × (cyl = ...) +Estimate and 95% Conf. Int. + +group + + + + + + +lhs: mpg +lhs: hp +Effect on [mpg & hp] + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_multi_facet.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_multi_facet.svg new file mode 100644 index 0000000..bfd7e38 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_multi_facet.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +hp + + + + + + + + + + +mpg + + + + + + +Constant +4 +6 +8 + +wt × (cyl = ...) + + + + +Constant +4 +6 +8 + +wt × (cyl = ...) +0 +100 +200 + + + +Estimate and 95% Conf. Int. + +group + + + + + + +lhs: mpg +lhs: hp +Effect on [mpg & hp] + + diff --git a/inst/tinytest/_tinysnapshot/ggcoefplot_simple.svg b/inst/tinytest/_tinysnapshot/ggcoefplot_simple.svg new file mode 100644 index 0000000..a72fc09 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ggcoefplot_simple.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1.0 +1.5 +2.0 +2.5 + + + + + + + + +Constant +Petal.Width +versicolor +virginica + +Species +Estimate and 95% Conf. Int. +Effect on Petal.Length + + diff --git a/inst/tinytest/test_ggcoefplot.R b/inst/tinytest/test_ggcoefplot.R new file mode 100644 index 0000000..f152c76 --- /dev/null +++ b/inst/tinytest/test_ggcoefplot.R @@ -0,0 +1,58 @@ +source("tinysnapshot_helpers.R") +using("tinysnapshot") +if (Sys.info()["sysname"] != "Linux") exit_file("Linux snapshots") + +library(ggiplot) +library(tinytest) + + +# NB: 1st test runs will fail, but write the targets to file. 2nd run(s) should +# pass. + +# +## Simple ggcoeflot ---- + +est = feols(Petal.Length ~ Petal.Width + i(Species), iris) +p = ggcoefplot(est) +expect_snapshot_plot(p, label = "ggcoefplot_simple") + +# +## i-based Interactions ---- + +est_i = feols(mpg ~ 0 + i(cyl, wt):disp + i(am, hp), mtcars) +p_i1 = ggcoefplot(est_i) +expect_snapshot_plot(p_i1, label = "ggcoefplot_interactions") +p_i2 = ggcoefplot(est_i, ci_level = c(.8, .95)) +expect_snapshot_plot(p_i2, label = "ggcoefplot_interactions_multici") + +# +## Multi estimation (with interactions) ---- + +est_multi = feols(c(mpg, hp) ~ i(cyl, wt), mtcars) +p_multi = ggcoefplot(est_multi) +expect_snapshot_plot(p_multi, label = "ggcoefplot_multi") +p_multi_facet = ggcoefplot(est_multi, multi_style = "facet") +expect_snapshot_plot(p_multi_facet, label = "ggcoefplot_multi_facet") + + +# +## Grouping ---- + +est_grp = feols(Petal.Length ~ Petal.Width + Sepal.Length + Sepal.Width + Species, iris) + +p_grp1 = ggcoefplot(est_grp) # no groups +expect_snapshot_plot(p_grp1, label = "ggcoefplot_group_none") +p_grp2 = ggcoefplot(est_grp, group = list("Sepal", "Species")) # group, no names +expect_snapshot_plot(p_grp2, label = "ggcoefplot_group_nonames") +p_grp3 = ggcoefplot(est_grp, group = list(Sepal = "Sepal", Species = "Species")) # group + names +expect_snapshot_plot(p_grp3, label = "ggcoefplot_groupnames") +p_grp4 = ggcoefplot(est_grp, group = list(Sepal = "^^Sepal.", Species = "^^Species")) # group + ^^names +expect_snapshot_plot(p_grp4, label = "ggcoefplot_group_names_prefix") + +# +## DiD (mostly to check auto grouping) ---- + +data("base_did", package = "fixest") +est_did = feols(y ~ x1 + i(period, treat, 5) | id + period, base_did) +p_did = ggcoefplot(est_did) +expect_snapshot_plot(p_did, label = "ggcoefplot_did") diff --git a/man/figures/README-coefplot1-1.png b/man/figures/README-coefplot1-1.png new file mode 100644 index 0000000..6aac334 Binary files /dev/null and b/man/figures/README-coefplot1-1.png differ diff --git a/man/figures/README-coefplot2-1.png b/man/figures/README-coefplot2-1.png new file mode 100644 index 0000000..4adeadb Binary files /dev/null and b/man/figures/README-coefplot2-1.png differ diff --git a/man/figures/README-example1-1.png b/man/figures/README-es1-1.png similarity index 100% rename from man/figures/README-example1-1.png rename to man/figures/README-es1-1.png diff --git a/man/figures/README-example2-1.png b/man/figures/README-es2-1.png similarity index 100% rename from man/figures/README-example2-1.png rename to man/figures/README-es2-1.png diff --git a/man/figures/README-example3-1.png b/man/figures/README-es3-1.png similarity index 100% rename from man/figures/README-example3-1.png rename to man/figures/README-es3-1.png diff --git a/man/ggiplot.Rd b/man/ggiplot.Rd index 66149d4..7376e4d 100644 --- a/man/ggiplot.Rd +++ b/man/ggiplot.Rd @@ -2,7 +2,9 @@ % Please edit documentation in R/ggiplot.R \name{ggiplot} \alias{ggiplot} -\title{ggplots confidence intervals and point estimates} +\alias{ggcoefplot} +\title{Draw coefficient plots and interaction plots from \code{fixest} regression +objects.} \usage{ ggiplot( object, @@ -14,13 +16,24 @@ ggiplot( theme = NULL, ... ) + +ggcoefplot( + object, + geom_style = c("pointrange", "errorbar"), + multi_style = c("dodge", "facet"), + facet_args = NULL, + theme = NULL, + ... +) } \arguments{ \item{object}{A model object of class \code{fixest} or \code{fixest_multi}, or a list thereof.} \item{geom_style}{Character string. One of \code{c('pointrange', 'errorbar', 'ribbon')} -describing the preferred geometric representation of the coefficients.} +describing the preferred geometric representation of the coefficients. Note +that ribbon plots not supported for \code{ggcoefplot}, since we cannot guarantee +a continuous relationship among the coefficients.} \item{multi_style}{Character string. One of \code{c('dodge', 'facet')}, defining how multi-model objects should be presented.} @@ -41,11 +54,13 @@ E.g. \code{facet_args = list(ncol = 2, scales = 'free_y')}. Only used if adjustments, such as centered plot title. Can also be defined on an existing ggiplot object to redefine theme elements. See examples.} -\item{...}{Arguments passed down, or equivalent, to the corresponding -\code{fixest::iplot()} arguments. Note that some of these require list objects. -Currently used are: +\item{...}{Arguments passed down to, or equivalent to, the corresponding +\code{fixest::coefplot}/\code{fixest::iplot} arguments. Note that some of these +require list objects. Currently used are: \itemize{ -\item \code{keep} and \code{drop} for subsetting variables using regular expressions. +\item \code{keep} and \code{drop} for subsetting variables using regular expressions. The \code{fixest::iplot} help page includes more detailed examples, but these should generally work as you expect. One useful regexp trick worth mentioning briefly for event studies with many pre-/post-periods is \code{drop = "[[:digit:]]{2}"}. This will cause the plot to zoom in around single digit pre-/post-periods. +\item \code{group} a list indicating variables to group over. Each element of the list reports the coefficients to be grouped while the name of the element is the group name. Each element of the list can be either: i) a character vector of length 1, ii) of length 2, or iii) a numeric vector. Special patterns such as "^^var_start" can be used to more appealing plotting, where group labels are separated from their subsidiary labels. This can be especially useful for plotting interaction terms. See the Details section of \code{fixest::coefplot} for more information. +\item \code{i.select} Integer scalar, default is 1. In \code{ggiplot}, used to select which variable created with \code{i()} to select. Only used when there are several variables created with \code{i}. See the Details section of \code{fixest::iplot} for more information. \item \code{main}, \code{xlab}, and \code{ylab} for setting the plot title, x- and y-axis labels, respectively. \item \code{zero} and \code{zero.par} for defining or adjusting the zero line. For example, \code{zero.par = list(col = 'orange')}. @@ -69,112 +84,240 @@ channel. For example, we can make the CI band lighter with A ggplot2 object. } \description{ -Plots the \code{ggplot2} equivalent of \code{fixest::iplot()}. Many of the -arguments are the same. As per the latter's description: -This function plots the results of estimations (coefficients and confidence -intervals). The function restricts the output to variables created with -\code{i}, either interactions with factors or raw factors. +Draws the \code{ggplot2} equivalents of \code{fixest::coefplot} and +\code{fixest::iplot}. These "gg" versions do their best to recycle the same +arguments and plotting logic as their original base counterparts. But they +also support additional features via the \code{ggplot2} API and infrastructure. +The overall goal remains the same as the original functions. To wit: +\code{ggcoefplot} plots the results of estimations (coefficients and confidence +intervals). The function \code{ggiplot} restricts the output to variables +created with \code{i}, either interactions with factors or raw factors. } \details{ -This function generally tries to mimic the functionality and (where -appropriate) arguments of \code{fixest::iplot()} as closely as possible. -However, by leveraging the ggplot2 API and infrastructure, it is able to -support some more complex plot arrangements out-of-the-box that would be -more difficult to achieve using the base \code{iplot()} alternative. +These functions generally try to mimic the functionality and (where +appropriate) arguments of \code{fixest::coefplot} and \code{fixest::iplot} as +closely as possible. However, by leveraging the ggplot2 API and +infrastructure, they are able to support some more complex plot +arrangements out-of-the-box that would be more difficult to achieve using +the base \code{coefplot}/\code{iplot} alternatives. } +\section{Functions}{ +\itemize{ +\item \code{ggcoefplot()}: This function plots the results of estimations +(coefficients and confidence intervals). The function \code{ggiplot} restricts +the output to variables created with i, either interactions with factors or +raw factors. + +}} \examples{ -# We'll also load fixest to estimate the actual models that we're plottig. +# We'll also load fixest to estimate the actual models that we're plotting. library(fixest) library(ggiplot) -# These examples borrow from the fixest::iplot() documentation and the -# introductory package vignette. +## +# Author note: The examples that follow deliberately follow the original +# examples from the coefplot/iplot help pages. A few "gg-" specific +# features are sprinkled within, with the final set of examples in +# particular highlighting unique features of this package. + # -## Example 1: Vanilla TWFE +# Example 1: Basic use and stacking two sets of results on the same graph # +# Estimation on Iris data with one fixed-effect (Species) +est = feols(Petal.Length ~ Petal.Width + Sepal.Length + Sepal.Width | Species, iris) + +ggcoefplot(est) + +# Show multiple CIs +ggcoefplot(est, ci_level = c(0.8, 0.95)) + +# By default, fixest model standard errors are clustered by the first fixed +# effect (here: Species). +# But we can easily switch to "regular" standard-errors +est_std = summary(est, se = "iid") + +# You can plot both results at once in the same plot frame... +ggcoefplot(list("Clustered" = est, "IID" = est_std)) +# ... or as separate facets +ggcoefplot(list("Clustered" = est, "IID" = est_std), multi_style = "facet") + + theme(legend.position = "none") + + +# +# Example 2: Interactions +# + + +# Now we estimate and plot the "yearly" treatment effects + data(base_did) base_inter = base_did -est_did = feols(y ~ x1 + i(period, treat, 5) | id+period, base_inter) +# We interact the variable 'period' with the variable 'treat' +est_did = feols(y ~ x1 + i(period, treat, 5) | id + period, base_inter) + +# In the estimation, the variable treat is interacted +# with each value of period but 5, set as a reference + +# ggcoefplot will show all the coefficients: +ggcoefplot(est_did) + + +# Note that the grouping of the coefficients is due to 'group = "auto"' + +# If you want to keep only the coefficients +# created with i() (ie the interactions), use ggiplot ggiplot(est_did) -# Comparison with iplot defaults -iplot(est_did) -ggiplot(est_did, geom = 'errorbar') # closer iplot original +# We can see that the graph is different from before: +# - only interactions are shown, +# - the reference is present, +# => this is fully flexible + +ggiplot(est_did, ci_level = c(0.8, 0.95)) +ggiplot(est_did, ref.line = FALSE, pt.join = TRUE, geom_style = "errorbar") +ggiplot(est_did, geom_style = "ribbon", col = "orange") +# etc + +# We can also use a dictionary to replace label values. The dicionary should +# take the form of a named vector or list, e.g. c("old_lab1" = "new_lab1", ...) + +# Let's create a "month" variable +all_months = c("aug", "sept", "oct", "nov", "dec", "jan", + "feb", "mar", "apr", "may", "jun", "jul") +# Turn into a dictionary by providing the old names +# Note the implication that treatment occured here in December (5 month in our series) +dict = all_months; names(dict) = 1:12 +# Pass our new dictionary to our ggiplot call +ggiplot(est_did, pt.join = TRUE, geom_style = "errorbar", dict = dict) + +# +# What if the interacted variable is not numeric? -# Many of the arguments work the same as in iplot() -iplot(est_did, pt.join = TRUE) -ggiplot(est_did, pt.join = TRUE, geom_style = 'errorbar') +# let's re-use our all_months vector from the previous example, but add it +# directly to the dataset +base_inter$period_month = all_months[base_inter$period] -# Plots can be customized and tweaked easily -ggiplot(est_did, geom_style = 'ribbon') -ggiplot(est_did, geom_style = 'ribbon', col = 'orange') +# The new estimation +est = feols(y ~ x1 + i(period_month, treat, "oct") | id+period, base_inter) +# Since 'period_month' of type character, iplot/coefplot both sort it +ggiplot(est) -# Unlike base iplot, multiple confidence interval levels are supported -ggiplot(est_did, ci_level = c(.8, .95)) +# To respect a plotting order, use a factor +base_inter$month_factor = factor(base_inter$period_month, levels = all_months) +est = feols(y ~ x1 + i(month_factor, treat, "oct") | id + period, base_inter) +ggiplot(est) -# Another new feature (i.e. unsupported in base iplot) is adding aggregated -# post- and/or pre-treatment effects to your plots. Here's an example that -# builds on the previous plot, by adding the mean post-treatment effect. -ggiplot(est_did, ci_level = c(.8, .95), - aggr_eff = "post", aggr_eff.par = list(col="orange")) # default is grey +# dict -> c("old_name" = "new_name") +dict = all_months; names(dict) = 1:12; dict +ggiplot(est_did, dict = dict) # -# Example 2: Multiple estimation (i) +# Example 3: Setting defaults # -# We'll demonstrate using the staggered treatment example from the -# introductory fixest vignette. +# The customization logic of ggcoefplot/ggiplot works differently than the +# original base fixest counterparts, so we don't have "gg" equivalents of +# setFixest_coefplot and setFixest_iplot. However, you can still invoke some +# global fixest settings like setFixest_dict(). SImple example: -data(base_stagg) -est_twfe = feols(y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | id + year, base_stagg) -est_sa20 = feols(y ~ x1 + sunab(year_treated, year) | id + year, base_stagg) +base_inter$letter = letters[base_inter$period] +est_letters = feols(y ~ x1 + i(letter, treat, 'e') | id+letter, base_inter) + +# Set global dictionary for capitalising the letters +dict = LETTERS[1:10]; names(dict) = letters[1:10] +setFixest_dict(dict) + +ggiplot(est_letters) + +setFixest_dict() # reset + +# +# Example 4: group + cleaning +# -ggiplot(list('TWFE' = est_twfe, 'Sun & Abraham (2020)' = est_sa20), - main = 'Staggered treatment', ref.line = -1, pt.join = TRUE) +# You can use the argument group to group variables +# You can further use the special character "^^" to clean +# the beginning of the coef. name: particularly useful for factors + +est = feols(Petal.Length ~ Petal.Width + Sepal.Length + + Sepal.Width + Species, iris) + +# No grouping: +ggcoefplot(est) + +# now we group by Sepal and Species +ggcoefplot(est, group = list(Sepal = "Sepal", Species = "Species")) + +# now we group + clean the beginning of the names using the special character ^^ +ggcoefplot(est, group = list(Sepal = "^^Sepal.", Species = "^^Species")) -# If you don't like the presentation of 'dodged' models in a single frame, -# then it easy to facet them instead using multi_style = 'facet'. -ggiplot(list('TWFE' = est_twfe, 'Sun & Abraham (2020)' = est_sa20), - main = 'Staggered treatment', ref.line = -1, pt.join = TRUE, - multi_style = 'facet') # -# Example 3: Multiple estimation (ii) +# Example 5: Some more ggcoefplot/ggiplot extras # -# An area where ggiplot shines is in complex multiple estimation cases, such -# as lists of fixest_multi objects. To illustrate, let's add a split variable +# We'll demonstrate using the staggered treatment example from the +# introductory fixest vignette. + +data(base_stagg) +est_twfe = feols( + y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | id + year, + base_stagg +) +est_sa20 = feols( + y ~ x1 + sunab(year_treated, year) | id + year, + data = base_stagg +) + +# Plot both regressions in a faceted plot +ggiplot( + list('TWFE' = est_twfe, 'Sun & Abraham (2020)' = est_sa20), + main = 'Staggered treatment', ref.line = -1, pt.join = TRUE +) + +# So far that's no different than base iplot (automatic legend aside). But an +# area where ggiplot shines is in complex multiple estimation cases, such as +# lists of fixest_multi objects. To illustrate, let's add a split variable # (group) to our staggered dataset. base_stagg_grp = base_stagg base_stagg_grp$grp = ifelse(base_stagg_grp$id \%\% 2 == 0, 'Evens', 'Odds') # Now re-run our two regressions from earlier, but splitting the sample to # generate fixest_multi objects. -est_twfe_grp = feols(y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | - id + year, base_stagg_grp, split = ~ grp) -est_sa20_grp = feols(y ~ x1 + sunab(year_treated, year) | - id + year, base_stagg_grp, split = ~ grp) +est_twfe_grp = feols( + y ~ x1 + i(time_to_treatment, treated, ref = c(-1, -1000)) | id + year, + data = base_stagg_grp, split = ~ grp +) +est_sa20_grp = feols( + y ~ x1 + sunab(year_treated, year) | id + year, + data = base_stagg_grp, split = ~ grp +) -# ggiplot combines with list of multi-estimation objects without a problem... +# ggiplot combines the list of multi-estimation objects without a problem... ggiplot(list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), - ref.line = -1, main = 'Staggered treatment: Split multi-sample') + ref.line = -1, main = 'Staggered treatment: Split multi-sample') -# ... but is even better when we use faceting instead of dodged errorbars. +# ... but is even better when we use facets instead of dodged errorbars. # Let's use this an opportunity to construct a fancy plot that invokes some # additional arguments and ggplot theming. -ggiplot(list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), - ref.line = -1, - main = 'Staggered treatment: Split multi-sample', - xlab = 'Time to treatment', - multi_style = 'facet', - geom_style = 'ribbon', - theme = theme_minimal() + - theme(text = element_text(family = 'HersheySans'), - plot.title = element_text(hjust = 0.5), - legend.position = 'none')) +ggiplot( + list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), + ref.line = -1, + main = 'Staggered treatment: Split multi-sample', + xlab = 'Time to treatment', + multi_style = 'facet', + geom_style = 'ribbon', + facet_args = list(labeller = labeller(id = \(x) gsub(".*: ", "", x))), + theme = theme_minimal() + + theme( + text = element_text(family = 'HersheySans'), + plot.title = element_text(hjust = 0.5), + legend.position = 'none' + ) +) # # Aside on theming and scale adjustments @@ -183,34 +326,15 @@ ggiplot(list('TWFE' = est_twfe_grp, 'Sun & Abraham (2020)' = est_sa20_grp), # Setting the theme inside the `ggiplot()` call is optional and not strictly # necessary, since the ggplot2 API allows programmatic updating of existing # plots. E.g. -last_plot() + labs(caption = 'Note: Super fancy plot brought to you by ggiplot') -last_plot() + theme_void() + scale_colour_brewer(palette = 'Set1') +last_plot() + + labs(caption = 'Note: Super fancy plot brought to you by ggiplot') +last_plot() + + theme_grey() + + theme(legend.position = 'none') + + scale_fill_brewer(palette = 'Set1', aesthetics = c("colour", "fill")) # etc. -# -# Aside on dictionaries -# - -# Dictionaries work similarly to iplot. Simple example: - -base_inter$letter = letters[base_inter$period] -est_letters = feols(y ~ x1 + i(letter, treat, 'e') | id+letter, base_inter) - -ggiplot(est_letters) # No dictionary - -# Dictionary for capitalising the letters -dict = LETTERS[1:10]; names(dict) = letters[1:10] - -# You can either set the dictionary directly in the plot call. -ggiplot(est_letters, dict=dict) - -# Or, set it globally using the setFixest_dict macro -setFixest_dict(dict) -ggiplot(est_letters) - -setFixest_dict() # reset - } \seealso{ -\code{\link[fixest:coefplot]{fixest::iplot()}}. +\code{\link[fixest:coefplot]{fixest::coefplot()}}, \code{\link[fixest:coefplot]{fixest::iplot()}}. } diff --git a/man/iplot_data.Rd b/man/iplot_data.Rd index e2909ac..96eef63 100644 --- a/man/iplot_data.Rd +++ b/man/iplot_data.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/iplot_data.R \name{iplot_data} \alias{iplot_data} +\alias{coefplot_data} \title{Internal function for grabbing and preparing iplot data} \usage{ iplot_data( @@ -10,7 +11,22 @@ iplot_data( .keep = NULL, .drop = NULL, .dict = fixest::getFixest_dict(), - .aggr_es = c("none", "post", "pre", "both") + .internal.only.i = TRUE, + .i.select = 1, + .aggr_es = c("none", "post", "pre", "both"), + .group = "auto" +) + +coefplot_data( + object, + .ci_level = 0.95, + .keep = NULL, + .drop = NULL, + .group = "auto", + .dict = fixest::getFixest_dict(), + .internal.only.i = FALSE, + .i.select = 1, + .aggr_es = "none" ) } \arguments{ @@ -31,13 +47,37 @@ and should take the form of an acceptable regular expression.} \item{.dict}{A dictionary (i.e. named character vector or a logical scalar). Used for changing coefficient names. Defaults to the values in -\code{getFixest_dict()}. See the \code{?fixest::iplot} documentation for more -information.} +\code{getFixest_dict()}. See the \code{?fixest::coefplot} documentation for more +information. Note: This argument applies dictionary changes directly to the +return object for \code{coefplot_data}. However, it is ignored for \code{iplot_data}, +since we want to preserve the numeric ordering for potential event study +plots. (And imposing an ordered factor would create its own downstream +problems in the case of continuous plot features like ribbons.) Instead, any +dictionary replacement for \code{ggiplot} is deferred to the actual plot call and +applied directly to the labels.} + +\item{.internal.only.i}{Logical variable used for some internal function +handling when passing on to coefplot/iplot.} + +\item{.i.select}{Integer scalar, default is 1. In (gg)iplot, used to select +which variable created with i() to select. Only used when there are several +variables created with i. This is an index, just try increasing numbers to +hopefully obtain what you want. Passed down to +\code{fixest::iplot(..., i.select = .i.select)}} \item{.aggr_es}{A character string indicating whether the aggregated mean post- (and/or pre-) treatment effect should be added as a column to the returned data frame. Passed to \code{aggr_es(..., aggregation = "mean")} and should be one of "none" (the default), "post", "pre", or "both".} + +\item{.group}{A list, default is missing. Each element of the list reports +the coefficients to be grouped while the name of the element is the group +name. Passed down to \code{fixest::coefplot(..., group = .group)}. Example of +valid uses: +⁠group=list(group_name=\"pattern\")⁠, +⁠group=list(group_name=c(\"var_start\", \"var_end\"))⁠, +⁠group=list(group_name=1:2)) +See the Details section of \code{?fixest::coefplot} for more.} } \value{ A data frame consisting of estimate values, confidence intervals, @@ -54,6 +94,11 @@ This function is a wrapper around to better facilitate plotting with \code{ggplot2} and handling of complex object types (e.g. lists of fixest_multi models) } +\section{Functions}{ +\itemize{ +\item \code{coefplot_data()}: Internal function for grabbing and preparing coefplot data + +}} \examples{ library(fixest) diff --git a/vignettes/ggiplot.Rmd b/vignettes/ggiplot.Rmd index 511a8ff..a295c63 100644 --- a/vignettes/ggiplot.Rmd +++ b/vignettes/ggiplot.Rmd @@ -1,8 +1,8 @@ --- -title: "Comparing ggiplot with fixest::iplot" +title: "Comparing ggiplot with iplot" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{Comparing ggiplot with fixest::iplot} + %\VignetteIndexEntry{Comparing ggiplot with iplot} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- @@ -252,7 +252,7 @@ last_plot() + last_plot() + theme_grey() + theme(legend.position = 'none') + - scale_colour_brewer(palette = 'Set1', aesthetics = c('colour', 'fill')) + scale_fill_brewer(palette = 'Set1', aesthetics = c('colour', 'fill')) ``` etc.