Skip to content

Commit

Permalink
plot.emfx (#54)
Browse files Browse the repository at this point in the history
* add plot.emfx method

* catch edge cases

- including for heterogenous treatment

* more edge cases catching

* docs and namespace

* update vignette

* docs

* news

* fix pkgdown
  • Loading branch information
grantmcdermott authored Dec 16, 2024
1 parent 793f26d commit 081d5d8
Show file tree
Hide file tree
Showing 12 changed files with 761 additions and 406 deletions.
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
cran-comments.md
^CRAN-SUBMISSION$
^_TESTING/
^SCRATCH/
README.html
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Imports:
stats,
data.table,
Formula,
marginaleffects (>= 0.24.0)
marginaleffects (>= 0.24.0),
tinyplot (>= 0.2.0)
Suggests:
did,
broom,
modelsummary (>= 2.2.0),
ggplot2,
knitr,
Expand Down
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand

S3method(plot,emfx)
export(emfx)
export(etwfe)
importFrom(Formula,as.Formula)
Expand All @@ -10,6 +11,11 @@ importFrom(data.table,setDT)
importFrom(fixest,demean)
importFrom(fixest,feglm)
importFrom(fixest,feols)
importFrom(graphics,abline)
importFrom(graphics,par)
importFrom(graphics,plot)
importFrom(marginaleffects,slopes)
importFrom(stats,reformulate)
importFrom(stats,setNames)
importFrom(tinyplot,tinyplot)
importFrom(utils,modifyList)
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ with the topline `emfx(..., type = <aggregration_type>)` argument. (#49)
- `emfx(..., lean = TRUE)` ensures a light return object by default, by
stripping away data-heavy attributes that are unlikely to be needed afterwards.
(#51)
- Native `plot.emfx()` method (via a **tinyplot** backend) for visualizing
`emfx` objects. (#54)

## Documentation

Expand Down
184 changes: 94 additions & 90 deletions R/emfx.R
Original file line number Diff line number Diff line change
@@ -1,93 +1,93 @@
##' Post-estimation aggregation of ETWFE results
##'
##' @md
##' @description
##' Companion function to `etwfe`, enabling the recovery of aggregate treatment
##' effects along different dimensions of interest (e.g, an event study of
##' dynamic average treatment effects). `emfx` is a light wrapper around the
##' \code{\link[marginaleffects]{slopes}} function from the **marginaleffects**
##' package.
##'
##' @param object An `etwfe` model object.
##' @param type Character. The desired type of post-estimation aggregation.
##' @param by_xvar Logical. Should the results account for heterogeneous
##' treatment effects? Only relevant if the preceding `etwfe` call included a
##' specified `xvar` argument, i.e. interacted categorical covariate. The
##' default behaviour (`"auto"`) is to automatically estimate heterogeneous
##' treatment effects for each level of `xvar` if these are detected as part
##' of the underlying `etwfe` model object. Users can override by setting to
##' either `FALSE` or `TRUE.` See the "Heterogeneous treatment effects"
##' section below.
##' @param collapse Logical. Collapse the data by (period by cohort) groups
##' before calculating marginal effects? This trades off a loss in estimate
##' accuracy (typically around the 1st or 2nd significant decimal point) for a
##' substantial improvement in estimation time for large datasets. The default
##' behaviour (`"auto"`) is to automatically collapse if the original dataset
##' has more than 500,000 rows. Users can override by setting either `FALSE` or
##' `TRUE`. Note that collapsing by group is only valid if the preceding `etwfe`
##' call was run with `"ivar = NULL"` (the default). See the "Performance
##' tips" section below.
##' @param predict Character. The type (scale) of prediction used to compute the
##' marginal effects. If `"response"` (the default), then the output is at the
##' level of the response variable, i.e. it is the expected predictor
##' \eqn{E(Y|X)}. If `"link"`, the value returned is the linear predictor of
##' the fitted model, i.e. \eqn{X\cdot \beta}. The difference should only
##' matter for nonlinear models. (Note: This argument is typically called
##' `type` when use in \code{\link[stats]{predict}} or
##' \code{\link[marginaleffects]{slopes}}, but we rename it here to avoid a
##' clash with the top-level `type` argument above.)
##' @param post_only Logical. Drop pre-treatment ATTs? Only evaluated if (a)
##' `type = "event"` and (b) the original `etwfe` model object was estimated
##' using the default `"notyet"` treated control group. If conditions (a) and
##' (b) are met then the pre-treatment effects will be zero as a mechanical
##' result of ETWFE's estimation setup. The default behaviour (`TRUE`) is
##' thus to drop these nuisance rows from the dataset. The `post_only` argument
##' recognises that you may still want to keep them for presentation purposes
##' (e.g., plotting an event study). Nevertheless, be forewarned that enabling
##' that behaviour via `FALSE` is _strictly_ performative: the "zero" treatment
##' effects for any pre-treatment periods is purely an artefact of the
##' estimation setup.
##' @param lean Logical. Enforces a lean return object; namely a simple
##' data.frame of the main results, stripped of ancillary attributes. Defaults
##' to `TRUE`, in which case `options(marginaleffects_lean = TRUE)` is set
##' internally at the start of the `emfx` call, before being reverted upon
##' exit. Note that this will disable some advanced `marginaleffects`
##' post-processing features, but those are unlikely to be used in the `emfx`
##' context and means that we can dramatically reduce the size of the return
##' object.
##' @param ... Additional arguments passed to
##' [`marginaleffects::slopes`]. For example, you can pass `vcov =
##' FALSE` to dramatically speed up estimation times of the main marginal
##' effects (but at the cost of not getting any information about standard
##' errors; see Performance tips below). Another potentially useful
##' application is testing whether heterogeneous treatment effects (i.e., the
##' levels of any `xvar` covariate) are equal by invoking the `hypothesis`
##' argument, e.g. `hypothesis = "b1 = b2"`.
##' @return A `data.frame` of aggregated treatment effects along the
##' dimension(s) of interested. Note that this data.frame will have been
##' overloaded with the \code{\link[marginaleffects]{slopes}} class, and so
##' will come with a special print method. But the underlying columns will
##' usually include:
##'
##' - `term`
##' - `contrast`
##' - `<type>` (i.e., the name of your `type` string)
##' - `estimate`
##' - `std.error`
##' - `statistic`
##' - `p.value`
##' - `s.value`
##' - `conf.low`
##' - `conf.high`
##' @seealso [marginaleffects::slopes] which does the heavily lifting behind the
##' scenes. [`etwfe`] is the companion estimating function that should be run
##' before `emfx`.
##' @inherit etwfe examples
##' @inheritSection etwfe Performance tips
##' @inheritSection etwfe Heterogeneous treatment effects
##' @importFrom data.table as.data.table setDT .N .SD
##' @importFrom marginaleffects slopes
##' @export
#' Post-estimation aggregation of ETWFE results
#'
#' @md
#' @description
#' Companion function to `etwfe`, enabling the recovery of aggregate treatment
#' effects along different dimensions of interest (e.g, an event study of
#' dynamic average treatment effects). `emfx` is a light wrapper around the
#' \code{\link[marginaleffects]{slopes}} function from the **marginaleffects**
#' package.
#'
#' @param object An `etwfe` model object.
#' @param type Character. The desired type of post-estimation aggregation.
#' @param by_xvar Logical. Should the results account for heterogeneous
#' treatment effects? Only relevant if the preceding `etwfe` call included a
#' specified `xvar` argument, i.e. interacted categorical covariate. The
#' default behaviour (`"auto"`) is to automatically estimate heterogeneous
#' treatment effects for each level of `xvar` if these are detected as part
#' of the underlying `etwfe` model object. Users can override by setting to
#' either `FALSE` or `TRUE.` See the "Heterogeneous treatment effects"
#' section below.
#' @param collapse Logical. Collapse the data by (period by cohort) groups
#' before calculating marginal effects? This trades off a slight loss in
#' precision (typically around the 1st or 2nd significant decimal point) for a
#' substantial improvement in estimation time for large datasets. The default
#' behaviour (`"auto"`) is to automatically collapse if the original dataset
#' has more than 500,000 rows. Users can override by setting either `FALSE` or
#' `TRUE`. Note that collapsing by group is only valid if the preceding `etwfe`
#' call was run with `"ivar = NULL"` (the default). See the "Performance
#' tips" section below.
#' @param predict Character. The type (scale) of prediction used to compute the
#' marginal effects. If `"response"` (the default), then the output is at the
#' level of the response variable, i.e. it is the expected predictor
#' \eqn{E(Y|X)}. If `"link"`, the value returned is the linear predictor of
#' the fitted model, i.e. \eqn{X\cdot \beta}. The difference should only
#' matter for nonlinear models. (Note: This argument is typically called
#' `type` when use in \code{\link[stats]{predict}} or
#' \code{\link[marginaleffects]{slopes}}, but we rename it here to avoid a
#' clash with the top-level `type` argument above.)
#' @param post_only Logical. Drop pre-treatment ATTs? Only evaluated if (a)
#' `type = "event"` and (b) the original `etwfe` model object was estimated
#' using the default `"notyet"` treated control group. If conditions (a) and
#' (b) are met then the pre-treatment effects will be zero as a mechanical
#' result of ETWFE's estimation setup. The default behaviour (`TRUE`) is
#' thus to drop these nuisance rows from the dataset. The `post_only` argument
#' recognises that you may still want to keep them for presentation purposes
#' (e.g., plotting an event study). Nevertheless, be forewarned that enabling
#' that behaviour via `FALSE` is _strictly_ performative: the "zero" treatment
#' effects for any pre-treatment periods is purely an artefact of the
#' estimation setup.
#' @param lean Logical. Enforces a lean return object; namely a simple
#' data.frame of the main results, stripped of ancillary attributes. Defaults
#' to `TRUE`, in which case `options(marginaleffects_lean = TRUE)` is set
#' internally at the start of the `emfx` call, before being reverted upon
#' exit. Note that this will disable some advanced `marginaleffects`
#' post-processing features, but those are unlikely to be used in the `emfx`
#' context and means that we can dramatically reduce the size of the return
#' object.
#' @param ... Additional arguments passed to
#' [`marginaleffects::slopes`]. For example, you can pass `vcov =
#' FALSE` to dramatically speed up estimation times of the main marginal
#' effects (but at the cost of not getting any information about standard
#' errors; see Performance tips below). Another potentially useful
#' application is testing whether heterogeneous treatment effects (i.e., the
#' levels of any `xvar` covariate) are equal by invoking the `hypothesis`
#' argument, e.g. `hypothesis = "b1 = b2"`.
#' @return A `data.frame` of aggregated treatment effects along the
#' dimension(s) of interested. Note that this data.frame will have been
#' overloaded with the \code{\link[marginaleffects]{slopes}} class, and so
#' will come with a special print method. But the underlying columns will
#' usually include:
#'
#' - `term`
#' - `contrast`
#' - `<type>` (i.e., the name of your `type` string)
#' - `estimate`
#' - `std.error`
#' - `statistic`
#' - `p.value`
#' - `s.value`
#' - `conf.low`
#' - `conf.high`
#' @seealso [marginaleffects::slopes] which does the heavily lifting behind the
#' scenes. [`etwfe`] is the companion estimating function that should be run
#' before `emfx`.
#' @inherit etwfe examples
#' @inheritSection etwfe Performance tips
#' @inheritSection etwfe Heterogeneous treatment effects
#' @importFrom data.table as.data.table setDT .N .SD
#' @importFrom marginaleffects slopes
#' @export
emfx = function(
object,
type = c("simple", "group", "calendar", "event"),
Expand All @@ -114,6 +114,7 @@ emfx = function(
type = match.arg(type)
predict = match.arg(predict)
etwfe_attr = attr(object, "etwfe")
etwfe_attr[["type"]] = type
gvar = etwfe_attr[["gvar"]]
tvar = etwfe_attr[["tvar"]]
ivar = etwfe_attr[["ivar"]]
Expand Down Expand Up @@ -227,6 +228,9 @@ emfx = function(
for (a in setdiff(atts, c("names", "row.names", "class", "by", "conf_level", "lean"))) attr(mfx, a) = NULL
}

class(mfx) = c("emfx", class(mfx))
attr(mfx, "etwfe") = etwfe_attr

return(mfx)
}

Loading

0 comments on commit 081d5d8

Please sign in to comment.