compute_rasa()
stat_express()
, with StatTemp defined
within the function
call.stat_group()
and
stat_panel()
devtools::create(".")
β¦ creating new stats is one of the most useful ways to extend the capabilities of ggplot2.β β ggplot2: Elegant Graphics for Data Analysis Yes!
Current methods for defining new user-facing stat and geom functions may be considered prohibitive for in-script, on-the-fly-use. statexpress attempts to make concise definition possible - almost as concise as the pre-processing of data that you might have to do in absence of the proposed statexpress functionality. With statexpress, extending ggplot2βs capabilities by creating under-the-hood statisical transformation routines could happen not only as development activity, but also in data-analytic contexts.
Because creating new stats is so useful and can contribute so much to userβs expressiveness w/ ggplot2 and because users are likely to intuit how Stats and Geoms work together at a high level, perhaps itβs an activity that should hold a special place in the extension world - where not so much is expected of extenders as is suggested by some resources (e.g.Β Internals Chapter of ggplot2 which follows):
Maybe not? For a ggplot2 developer who hopes to design extensions, however, β¦ understanding [how ggplot2 translates this plot specification into an image] is paramount. (ggplot2 internals)
β¦ the ease with which extensions can be made means that writing one-off extensions to solve a particular plotting challenge is also viable. (More true with shortcut?)
A few approaches have been combine here.
Letβs go!
compute_rasa()
See Campitelli 2018 which creates statRasa and stat_rasa, and
https://eliocamp.github.io/codigo-r/en/2018/05/how-to-make-a-generic-stat-in-ggplot2/
We pull his function internal to StatRasa that defines compute_rasa. We can use in a few different places β now to define user-facing stat_group() and stat_panel() and stat_panel_sf().
knitrExtra:::chunk_to_r("a_compute_rasa")
#> It seems you are currently knitting a Rmd/Qmd file. The parsing of the file will be done in a new R session.
compute_rasa <- function(data, scales, fun, fun.args) {
# Change default arguments of the function to the
# values in fun.args
args <- formals(fun)
for (i in seq_along(fun.args)) {
if (names(fun.args[i]) %in% names(fun.args)) {
args[[names(fun.args[i])]] <- fun.args[[i]]
}
}
formals(fun) <- args
# Apply function to data
fun(data)
}
stat_express()
, with StatTemp defined within the function call.knitrExtra:::chunk_to_r("b_stat_express")
#> It seems you are currently knitting a Rmd/Qmd file. The parsing of the file will be done in a new R session.
# stat function used in ggplot - right now putting fun and geom first
# with eye to positional augmenting
stat_express <- function(fun = NULL,
geom = "point",
mapping = NULL,
data = NULL,
position = "identity",
required_aes = NULL,
default_aes = NULL,
dropped_aes = NULL,
...,
show.legend = NA,
inherit.aes = TRUE,
computation_scope = "group"
) {
# Check arguments
if (!is.function(fun)) stop("fun must be a function")
# Pass dotted arguments to a list
fun.args <- match.call(expand.dots = FALSE)$`...`
StatTemp <- ggplot2::ggproto(
`_class` = "StatTemp",
`_inherit` = ggplot2::Stat,
)
if(!is.null(required_aes)){StatTemp$required_aes <- required_aes}
if(!is.null(default_aes)){StatTemp$default_aes <- default_aes}
if(!is.null(dropped_aes)){StatTemp$dropped_aes <- dropped_aes}
if(computation_scope == "group"){StatTemp$compute_group <- compute_rasa}
if(computation_scope == "panel"){StatTemp$compute_panel <- compute_rasa}
ggplot2::layer(
data = data,
mapping = mapping,
stat = StatTemp,
geom = geom,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
check.aes = FALSE,
check.param = FALSE,
params = list(
fun = fun,
fun.args = fun.args,
na.rm = FALSE,
...
)
)
}
stat_group()
and stat_panel()
knitrExtra:::chunk_to_r("c_stat_group_panel")
#> It seems you are currently knitting a Rmd/Qmd file. The parsing of the file will be done in a new R session.
I think unified approach works fine, but commenting out predecessor.
#' Title
#'
#' @param ...
#'
#' @return
#' @export
#'
#' @examples
stat_group <- function(...){stat_express(...)}
#' Title
#'
#' @param ...
#'
#' @return
#' @export
#'
#' @examples
stat_panel <- function(...){stat_express(computation_scope = "panel", ...)}
# stat function used in ggplot - we reorder from conventional
# stat_panel <- function(fun = NULL,
# geom = "point",
# mapping = NULL, data = NULL,
# position = "identity",
# required_aes = NULL,
# default_aes = NULL,
# dropped_aes = NULL,
# ...,
# show.legend = NA,
# inherit.aes = TRUE) {
#
# StatTemp <-
# ggplot2::ggproto("StatTemp",
# ggplot2::Stat,
# compute_panel = compute_rasa)
#
# # Check arguments
# if (!is.function(fun)) stop ("fun must be a function")
#
# # Pass dotted arguments to a list
# fun.args <- match.call(expand.dots = FALSE)$`...`
#
# if(!is.null(required_aes)){StatTemp$required_aes <- required_aes}
# if(!is.null(default_aes)){StatTemp$default_aes <- default_aes}
# if(!is.null(dropped_aes)){StatTemp$dropped_aes <- dropped_aes}
#
# ggplot2::layer(
# data = data,
# mapping = mapping,
# stat = StatTemp,
# geom = geom,
# position = position,
# show.legend = show.legend,
# inherit.aes = inherit.aes,
# check.aes = FALSE,
# check.param = FALSE,
# params = list(
# fun = fun,
# fun.args = fun.args,
# na.rm = FALSE,
# ...
# )
# )
# }
knitrExtra:::chunk_to_r("d_stat_panel_sf")
#> It seems you are currently knitting a Rmd/Qmd file. The parsing of the file will be done in a new R session.
stat_panel_sf()
will use StatTemp and compute_rasa, but with
layer_sf under the hood instead of layer. You should be able to declare
crs (not convinced weβve got that working. ) Additionally, the βfunβ for
the panel will populate automatically if a geom_ref_argument is
provided and the fun argument is not provided.
# stat function used in ggplot - we reorder from conventional
stat_panel_sf <- function(geo_ref_data = NULL,
fun = NULL,
geom = "sf",
mapping = NULL,
data = NULL,
position = "identity",
required_aes = NULL,
default_aes = NULL,
dropped_aes = NULL,
...,
show.legend = NA,
inherit.aes = TRUE,
na.rm = FALSE,
crs = "NAD27" # "NAD27", 5070, "WGS84", "NAD83", 4326 , 3857
) {
StatTemp <-
ggplot2::ggproto("StatTemp",
ggplot2::Stat,
compute_panel = compute_rasa)
if(!is.null(geo_ref_data) & is.null(fun)){
fun <- function(data, keep_id = NULL,
drop_id = NULL,
stamp = FALSE){
if(!stamp){data <- dplyr::inner_join(data, geo_ref_data)}
if( stamp){data <- geo_ref_data }
if(!is.null(keep_id)){ data <- dplyr::filter(data, id_col %in% keep_id) }
if(!is.null(drop_id)){ data <- dplyr::filter(data, !(id_col %in% drop_id))}
data
}
}
# Check arguments
if (!is.function(fun)) stop ("fun must be a function")
# Pass dotted arguments to a list
fun.args <- match.call(expand.dots = FALSE)$`...`
if(!is.null(required_aes)){StatTemp$required_aes <- required_aes}
if(!is.null(default_aes)){StatTemp$default_aes <- default_aes}
if(!is.null(dropped_aes)){StatTemp$dropped_aes <- dropped_aes}
c(ggplot2::layer_sf(
data = data,
mapping = mapping,
stat = StatTemp, # proto object from step 2
geom = geom, # inherit other behavior
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
check.aes = FALSE,
check.param = FALSE,
params = rlang::list2(
fun = fun,
fun.args = fun.args,
na.rm = na.rm, ...)
),
ggplot2::coord_sf(crs = crs,
default_crs = sf::st_crs(crs),
datum = crs,
default = TRUE)
)
}
library(tidyverse)
group_means <- function(data){
data |>
summarise(x = mean(x),
y = mean(y))
}
geom_means <- function(...){
stat_group(fun = group_means, ...)
}
mtcars |>
ggplot() +
aes(x = wt,
y = mpg) +
geom_point() +
geom_means(size = 6)
last_plot() +
aes(color = factor(cyl))
group_label_at_center <- function(data){
data %>%
group_by(label) %>%
summarise(x = mean(x, na.rm = T),
y = mean(y, na.rm = T))
}
geom_center_label <- function(...){
stat_group(fun = group_label_at_center, geom = GeomLabel, ...)
}
palmerpenguins::penguins |>
ggplot() +
aes(x = bill_length_mm,
y = bill_depth_mm) +
geom_point() +
aes(label = "All") +
geom_center_label()
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
last_plot() +
aes(color = species, label = species)
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
geom_center_text <- function(...){
stat_group(fun = group_label_at_center, geom = GeomText, ...)
}
palmerpenguins::penguins |>
ggplot() +
aes(x = bill_length_mm,
y = bill_depth_mm) +
geom_point(aes(color = bill_length_mm)) +
aes(color = species) +
geom_center_text(color = "Black",
aes(label = species),
alpha = .8,
size = 5,
fontface = "bold")
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
layer_data(i = 2)
#> label x y PANEL group colour size angle hjust vjust alpha
#> 1 Adelie 38.79139 18.34636 1 -1 Black 5 0 0.5 0.5 0.8
#> 2 Chinstrap 48.83382 18.42059 1 -1 Black 5 0 0.5 0.5 0.8
#> 3 Gentoo 47.50488 14.98211 1 -1 Black 5 0 0.5 0.5 0.8
#> family fontface lineheight
#> 1 bold 1.2
#> 2 bold 1.2
#> 3 bold 1.2
compute_post <- function(data){
data %>%
mutate(xend = x,
yend = 0)
}
geom_post <- function(...){
stat_group(fun = compute_post, geom = "segment", ...)
}
data.frame(outcome = 0:1, prob = c(.4, .6)) |>
ggplot() +
aes(x = outcome,
y = prob) +
geom_post() +
geom_point() +
labs(title = "probability by outcome")
compute_xmean <- function(data){
data %>%
summarize(xintercept = mean(x))
}
geom_xmean <- function(...){
stat_group(fun = compute_xmean, geom = "vline",
dropped_aes = c("x", "y"), ...)
}
mtcars |>
ggplot() +
aes(x = wt,
y = mpg) +
geom_point() +
geom_xmean(linetype = "dashed")
last_plot() +
aes(color = factor(cyl))
compute_xy_quantile <- function(data, q = .5){
data %>%
summarise(x = quantile(x, q),
y = quantile(y, q))
}
geom_quantile <- function(...){
stat_group(fun = compute_xy_quantile, "point", ...) #adding point means that aes is automatically figured out! ok
}
mtcars |>
ggplot() +
aes(x = wt,
y = mpg) +
geom_point() +
geom_quantile(size = 8, aes(color = "q = .5"), q = .5) +
geom_quantile(size = 8, q = 1, aes(color = "q = 1")) +
geom_quantile(aes(color = "q = .9"), size = 8, q = .9)
geom_highlight()
compute_panel_highlight <- function(data, which_id = NULL){
data %>%
arrange(highlight_condition) %>%
mutate(group = fct_inorder(grouping))
}
stat_highlight <- function(geom = "line", ...){
stat_panel(fun = compute_panel_highlight,
geom = geom,
default_aes = aes(color = after_stat(highlight_condition)),
...)
}
gapminder::gapminder %>%
filter(continent == "Americas") %>%
ggplot() +
aes(x = year, y = lifeExp,
grouping = country,
highlight_condition =
country == "Bolivia") +
geom_highlight(linewidth = 3)
#> Error in geom_highlight(linewidth = 3): could not find function "geom_highlight"
gapminder::gapminder %>%
filter(year == 2002) %>%
ggplot() +
aes(x = gdpPercap, y = lifeExp,
grouping = continent,
highlight_condition =
continent == "Europe") +
geom_highlight(geom = "point") +
scale_x_log10() +
scale_color_manual(values = c("grey", "darkolivegreen"))
#> Error in geom_highlight(geom = "point"): could not find function "geom_highlight"
Since were organize with variable function input in first position and geom in section position, and we can do one-liners (or two) use positioning for arguments.
geom_xmean_line()
in 137 characterslibrary(tidyverse)
geom_xmean_line <- function(...){stat_group(function(df) df |> summarize(xintercept = mean(x)), "vline", dropped_aes = c("x", "y"), ...)}
ggplot(cars) +
aes(speed, dist) +
geom_point() +
geom_xmean_line(linetype = 'dashed')
last_plot() +
aes(color = dist > 50)
ggplot(cars) +
aes(speed, dist) +
geom_point() +
geom_xmean_line(linetype = 'dashed',
data = . %>% tail,
aes(color = dist > 50))
ggplot(cars) +
aes(speed, dist) +
geom_point() +
geom_xmean_line(data = . %>% filter(speed < 10))
geom_xmean
in 99 charactersgeom_xmean <- function(...){stat_group(function(df) df |> summarize(x = mean(x), y = I(.05)), "point", ...)}
ggplot(cars) +
aes(speed, dist) +
geom_point() +
geom_xmean(size = 8, shape = "diamond")
last_plot() +
aes(color = dist > 50)
geom_post()
in 101 characters, stat_expectedvalue()
in 113, geom_expectedvalue_label()
171β¦May I buy a visually enhanced probability lesson for 400 characters? Yes please.
geom_post <- function(...){stat_group(function(df) df |> mutate(xend = x, yend = 0), "segment", ...)}
data.frame(prob = c(.4,.6), outcome = c(0, 1)) %>%
ggplot(data = .) +
aes(outcome, prob) +
geom_post() +
geom_point()
stat_expectedvalue <- function(geom = "point", ...){
stat_group(function(df) df |> summarise(x = sum(x*y), y = 0),
geom = geom,
default_aes = aes(label = after_stat(round(x, 2))),
...)
} # point is default geom
last_plot() +
stat_expectedvalue()
last_plot() +
stat_expectedvalue(geom = "text", vjust = 0) +
stat_expectedvalue(geom = "text", label = "The Expected Value",
vjust = 1)
geom_proportion()
and geom_proportion_label()
rep(1, 15) |>
c(0) %>%
data.frame(outcome = .) |>
ggplot() +
aes(x = outcome) +
geom_dotplot()
#> Bin width defaults to 1/30 of the range of the data. Pick better value with
#> `binwidth`.
geom_proportion <- function(...){stat_panel(function(df) df |> summarise(x = sum(x)/length(x), y = 0), ...)} # this should work for T/F too when rasa_p is in play
last_plot() +
geom_proportion(shape = "triangle")
#> Bin width defaults to 1/30 of the range of the data. Pick better value with
#> `binwidth`.
geom_proportion_label <- function(...){stat_panel(function(df) df |> summarise(x = sum(x)/length(x), y = 0) |> mutate(label = round(x,2)), vjust = 0, "text", ...)} # this should work for T/F too when rasa_p is in play
last_plot() +
geom_proportion_label()
#> Bin width defaults to 1/30 of the range of the data. Pick better value with
#> `binwidth`.
# last_plot() +
# geom_proportion_label() +
# ggsample::facet_bootstrap()
#
# layer_data(i = 2)
#
#
#
# rep(0:1, 10000) %>% # very large 50/50 sample
# data.frame(outcome = .) |>
# ggplot() +
# aes(x = outcome) +
# geom_dotplot() +
# geom_proportion() +
# geom_proportion_label() +
# ggsample::facet_sample(n_facets = 25,
# n_sampled = 16) ->
# p; p
#
#
# layer_data(p, i = 2) |>
# ggplot() +
# aes(x = x) +
# geom_rug() +
# geom_histogram()
geom_means
in 131 charactersgeom_means <- function(...){stat_group(function(df) df |> summarize(x = mean(x, na.rm = T), y = mean(y, na.rm = T)), "point", ...)}
palmerpenguins::penguins %>%
ggplot() +
aes(bill_length_mm, bill_depth_mm) +
geom_point() +
geom_means(size = 7)
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
last_plot() +
geom_means(size = 8, shape = "diamond", aes(color = bill_depth_mm > 17)) # why does aes work here w/o mapping =
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
#> Warning: Removed 1 row containing missing values or values outside the scale range
#> (`geom_point()`).
last_plot() +
geom_means(size = 8,
shape = "square",
data = . %>% filter(species == "Chinstrap"),
aes(color = bill_depth_mm > 17),
alpha = .6)
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
#> Removed 1 row containing missing values or values outside the scale range
#> (`geom_point()`).
geom_grouplabel_at_means()
geom_grouplabel_at_means <- function(...){stat_group(function(df) df |> group_by(label) |> summarize(x = mean(x, na.rm = T), y = mean(y, na.rm = T)), "label", ...)}
palmerpenguins::penguins %>%
ggplot() +
aes(bill_length_mm, bill_depth_mm, label = species, group = species) +
geom_point() +
geom_grouplabel_at_means(size = 7)
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).
nc <- sf::st_read(system.file("shape/nc.shp", package="sf"))
geo_reference_nc <- nc |>
dplyr::select(county_name = NAME, fips = FIPS) |>
sf2stat:::sf_df_prep_for_stat(id_col_name = "county_name")
#> Warning in st_point_on_surface.sfc(sf::st_zm(dplyr::pull(sf_df, geometry))):
#> st_point_on_surface may not give correct results for longitude/latitude data
#> Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if
#> `.name_repair` is omitted as of tibble 2.0.0.
#> βΉ Using compatibility `.name_repair`.
#> βΉ The deprecated feature was likely used in the sf2stat package.
#> Please report the issue to the authors.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
stat_nc_counties <- function(geom = "sf", ...){
stat_panel_sf(geo_ref_data = geo_reference_nc,
fun = NULL,
geom = geom,
default_aes =
ggplot2::aes(label =
after_stat(id_col)),
...)
}
nc %>%
sf::st_drop_geometry() %>%
ggplot() +
aes(fips = FIPS) +
stat_nc_counties() +
stat_nc_counties(geom = "text", check_overlap = T)
#> Joining with `by = join_by(fips)`
#> Joining with `by = join_by(fips)`
nc %>%
sf::st_drop_geometry() %>%
ggplot() +
aes(fips = FIPS, fill = SID74) +
stat_nc_counties() +
stat_nc_counties(geom = "text",
data = . %>% head(),
aes(label = SID74),
color = "grey85")
#> Joining with `by = join_by(fips)`
#> Joining with `by = join_by(fips)`
To build a minimal working package, 1) weβll need to document dependencies 2) send functions to .R files in the R folder, 3) do a package check (this will A. Document our functions and B. this will help us identify problems - for example if weβve failed to declare a dependency) and 4) install the package locally.
Then weβll write up a quick advertisement for what our package is able to do in the βtraditional readmeβ section. This gives us a chance to test out the package.
### Bit 2a: in the function(s) you wrote above make sure dependencies to functions using '::' syntax to pkg functions
usethis::use_package("ggplot2") # Bit 2b: document dependencies, w hypothetical ggplot2
# Bit 4: document functions and check that package is minimally viable
devtools::check(pkg = ".")
# Bit 5: install package locally
devtools::install(pkg = ".", upgrade = "never")
#> ββ R CMD build βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#> * checking for file β/Users/evangelinereynolds/Google Drive/r_packages/statexpress/DESCRIPTIONβ ... OK
#> * preparing βstatexpressβ:
#> * checking DESCRIPTION meta-information ... OK
#> * checking for LF line-endings in source and make files and shell scripts
#> * checking for empty or unneeded directories
#> * building βstatexpress_0.0.0.9000.tar.gzβ
#>
#> Running /Library/Frameworks/R.framework/Resources/bin/R CMD INSTALL \
#> /var/folders/zy/vfmj60bs3zv6r_2dsk18_vj00000gn/T//RtmpIstacq/statexpress_0.0.0.9000.tar.gz \
#> --install-tests
#> * installing to library β/Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/libraryβ
#> * installing *source* package βstatexpressβ ...
#> ** using staged installation
#> ** R
#> ** byte-compile and prepare package for lazy loading
#> ** help
#> *** installing help indices
#> ** building package indices
#> ** testing if installed package can be loaded from temporary location
#> ** testing if installed package can be loaded from final location
#> ** testing if installed package keeps a record of temporary installation path
#> * DONE (statexpress)
The goal of the {statExpress} package is to β¦
Install package with:
remotes::install_github("EvaMaeRey/statExpress")
Once functions are exported you can remove go to two colons, and when things are are really finalized, then go without colons (and rearrange your readmeβ¦)
rm(list = ls())
library(statexpress)
library(ggplot2)
library(tidyverse)
geom_xmean <- function(...){stat_group(function(df) df |> summarize(x = mean(x), y = I(.05)), ...)}
ggplot(cars) +
aes(speed, dist) +
geom_point() +
geom_xmean(size = 8, shape = "diamond")
last_plot() +
aes(color = dist > 50)
last_plot() +
geom_xmean(mapping = aes(size = dist > 75))
#> Warning: Using size for a discrete variable is not advised.
# for quick knit (exiting early) change eval to TRUE
fs::dir_tree(recurse = T)
#> .
#> βββ DESCRIPTION
#> βββ NAMESPACE
#> βββ R
#> β βββ a_compute_rasa.R
#> β βββ b_stat_express.R
#> β βββ b_stat_group.R
#> β βββ c_stat_group_panel.R
#> β βββ c_stat_panel.R
#> β βββ d_stat_panel_sf.R
#> βββ README.Rmd
#> βββ README.md
#> βββ README_files
#> β βββ figure-gfm
#> β βββ cars-1.png
#> β βββ cars-2.png
#> β βββ cars-3.png
#> β βββ unnamed-chunk-10-1.png
#> β βββ unnamed-chunk-10-2.png
#> β βββ unnamed-chunk-11-1.png
#> β βββ unnamed-chunk-11-2.png
#> β βββ unnamed-chunk-12-1.png
#> β βββ unnamed-chunk-12-2.png
#> β βββ unnamed-chunk-12-3.png
#> β βββ unnamed-chunk-13-1.png
#> β βββ unnamed-chunk-13-2.png
#> β βββ unnamed-chunk-13-3.png
#> β βββ unnamed-chunk-13-4.png
#> β βββ unnamed-chunk-14-1.png
#> β βββ unnamed-chunk-14-2.png
#> β βββ unnamed-chunk-14-3.png
#> β βββ unnamed-chunk-14-4.png
#> β βββ unnamed-chunk-15-1.png
#> β βββ unnamed-chunk-15-2.png
#> β βββ unnamed-chunk-15-3.png
#> β βββ unnamed-chunk-16-1.png
#> β βββ unnamed-chunk-16-2.png
#> β βββ unnamed-chunk-16-3.png
#> β βββ unnamed-chunk-17-1.png
#> β βββ unnamed-chunk-17-2.png
#> β βββ unnamed-chunk-17-3.png
#> β βββ unnamed-chunk-18-1.png
#> β βββ unnamed-chunk-18-2.png
#> β βββ unnamed-chunk-19-1.png
#> β βββ unnamed-chunk-19-2.png
#> β βββ unnamed-chunk-21-1.png
#> β βββ unnamed-chunk-21-2.png
#> β βββ unnamed-chunk-21-3.png
#> β βββ unnamed-chunk-22-1.png
#> β βββ unnamed-chunk-22-2.png
#> β βββ unnamed-chunk-22-3.png
#> β βββ unnamed-chunk-23-1.png
#> β βββ unnamed-chunk-23-2.png
#> β βββ unnamed-chunk-23-3.png
#> β βββ unnamed-chunk-7-1.png
#> β βββ unnamed-chunk-7-2.png
#> β βββ unnamed-chunk-8-1.png
#> β βββ unnamed-chunk-8-2.png
#> β βββ unnamed-chunk-8-3.png
#> β βββ unnamed-chunk-9-1.png
#> β βββ unnamed-chunk-9-2.png
#> β βββ unnamed-chunk-9-3.png
#> βββ man
#> β βββ stat_group.Rd
#> β βββ stat_panel.Rd
#> βββ statexpress.Rproj
knitr::knit_exit()