Structuring code that is robust to updates in the data, changes in methodological specfications, etc., and can get to presentation-ready results in an automated way can mitigate errors caused by painful, tedious tasks–it can also be a huge time saver. The cheese
package was designed with this philosophy in mind, heavily influenced by (and explicit dependencies on) tidy
concepts. Its intention is to provide general tools for working with data and results during statistical analysis, in addition to a collection of functions designated for specific statistical tasks.
Let's take a closer look:
#Load the package
require(cheese)
## Loading required package: cheese
This data comes from the UCI Machine Learning Repository, containing a collection of demographic and clinical characteristics from 303 patients. It was subsequently processed and cleaned into a format suitable for analysis–details of which can be found here.
#Look at the data
heart_disease
## # A tibble: 303 x 9
## Age Sex ChestPain BP Cholesterol BloodSugar MaximumHR
## <dbl> <fct> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 63 Male Typical … 145 233 TRUE 150
## 2 67 Male Asymptom… 160 286 FALSE 108
## 3 67 Male Asymptom… 120 229 FALSE 129
## 4 37 Male Non-angi… 130 250 FALSE 187
## 5 41 Fema… Atypical… 130 204 FALSE 172
## 6 56 Male Atypical… 120 236 FALSE 178
## 7 62 Fema… Asymptom… 140 268 FALSE 160
## 8 57 Fema… Asymptom… 120 354 FALSE 163
## 9 63 Male Asymptom… 130 254 FALSE 147
## 10 53 Male Asymptom… 140 203 TRUE 155
## # … with 293 more rows, and 2 more variables: ExerciseInducedAngina <fct>,
## # HeartDisease <fct>
The functions that will be introduced are roughly in order of their development, as many build off of one another. Selection of columns is done with non-standard evaluation (NSE) or with tidyselect::select_helpers
, where applicable.
divide()
is used to split up a data frame into a list of subsets. Suppose you want to split the example dataset by sex and heart disease status:
div_dat <-
heart_disease %>%
divide(
Sex,
HeartDisease
)
div_dat
## $Female
## $Female$No
## # A tibble: 72 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 41 Atypical … 130 204 FALSE 172 No
## 2 57 Asymptoma… 120 354 FALSE 163 Yes
## 3 56 Atypical … 140 294 FALSE 153 No
## 4 48 Non-angin… 130 275 FALSE 139 No
## 5 58 Typical a… 150 283 TRUE 162 No
## 6 50 Non-angin… 120 219 FALSE 158 No
## 7 58 Non-angin… 120 340 FALSE 172 No
## 8 66 Typical a… 150 226 FALSE 114 No
## 9 69 Typical a… 140 239 FALSE 151 No
## 10 71 Atypical … 160 302 FALSE 162 No
## # … with 62 more rows
##
## $Female$Yes
## # A tibble: 25 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 62 Asymptoma… 140 268 FALSE 160 No
## 2 65 Asymptoma… 150 225 FALSE 114 No
## 3 61 Asymptoma… 130 330 FALSE 169 No
## 4 51 Asymptoma… 130 305 FALSE 142 Yes
## 5 62 Asymptoma… 160 164 FALSE 145 No
## 6 60 Asymptoma… 150 258 FALSE 157 No
## 7 61 Asymptoma… 145 307 FALSE 146 Yes
## 8 43 Asymptoma… 132 341 TRUE 136 Yes
## 9 62 Non-angin… 130 263 FALSE 97 No
## 10 63 Asymptoma… 150 407 FALSE 154 No
## # … with 15 more rows
##
##
## $Male
## $Male$No
## # A tibble: 92 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 63 Typical a… 145 233 TRUE 150 No
## 2 37 Non-angin… 130 250 FALSE 187 No
## 3 56 Atypical … 120 236 FALSE 178 No
## 4 57 Asymptoma… 140 192 FALSE 148 No
## 5 44 Atypical … 120 263 FALSE 173 No
## 6 52 Non-angin… 172 199 TRUE 162 No
## 7 57 Non-angin… 150 168 FALSE 174 No
## 8 54 Asymptoma… 140 239 FALSE 160 No
## 9 49 Atypical … 130 266 FALSE 171 No
## 10 64 Typical a… 110 211 FALSE 144 Yes
## # … with 82 more rows
##
## $Male$Yes
## # A tibble: 114 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 67 Asymptoma… 160 286 FALSE 108 Yes
## 2 67 Asymptoma… 120 229 FALSE 129 Yes
## 3 63 Asymptoma… 130 254 FALSE 147 No
## 4 53 Asymptoma… 140 203 TRUE 155 Yes
## 5 56 Non-angin… 130 256 TRUE 142 Yes
## 6 48 Atypical … 110 229 FALSE 168 No
## 7 58 Atypical … 120 284 FALSE 160 No
## 8 58 Non-angin… 132 224 FALSE 173 No
## 9 60 Asymptoma… 130 206 FALSE 132 Yes
## 10 40 Asymptoma… 110 167 FALSE 114 Yes
## # … with 104 more rows
The default behavior is to continually split the data in a hierarchical structure based on the order of the input columns, and to remove them from the result. The keep
argument can be used to retain the column(s) in the data frames.
fasten()
allows you to take a list structure that contains data frames at an arbitrary depth, and roll them back up into a single data frame. This is useful when a statistical process needs to be mapped to many subsets, and you want to be able to easily collect the results without repeatedly binding data. You can call the function with the divided data from above:
div_dat %>%
fasten()
## # A tibble: 303 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 41 Atypical … 130 204 FALSE 172 No
## 2 57 Asymptoma… 120 354 FALSE 163 Yes
## 3 56 Atypical … 140 294 FALSE 153 No
## 4 48 Non-angin… 130 275 FALSE 139 No
## 5 58 Typical a… 150 283 TRUE 162 No
## 6 50 Non-angin… 120 219 FALSE 158 No
## 7 58 Non-angin… 120 340 FALSE 172 No
## 8 66 Typical a… 150 226 FALSE 114 No
## 9 69 Typical a… 140 239 FALSE 151 No
## 10 71 Atypical … 160 302 FALSE 162 No
## # … with 293 more rows
It was binded back to the original number of rows, but it lost the columns it was split by. The into
argument can be used to handle this:
div_dat %>%
fasten(
into = c("Sex", "HeartDisease")
)
## # A tibble: 303 x 9
## Sex HeartDisease Age ChestPain BP Cholesterol BloodSugar
## <chr> <chr> <dbl> <fct> <dbl> <dbl> <lgl>
## 1 Fema… No 41 Atypical… 130 204 FALSE
## 2 Fema… No 57 Asymptom… 120 354 FALSE
## 3 Fema… No 56 Atypical… 140 294 FALSE
## 4 Fema… No 48 Non-angi… 130 275 FALSE
## 5 Fema… No 58 Typical … 150 283 TRUE
## 6 Fema… No 50 Non-angi… 120 219 FALSE
## 7 Fema… No 58 Non-angi… 120 340 FALSE
## 8 Fema… No 66 Typical … 150 226 FALSE
## 9 Fema… No 69 Typical … 140 239 FALSE
## 10 Fema… No 71 Atypical… 160 302 FALSE
## # … with 293 more rows, and 2 more variables: MaximumHR <dbl>,
## # ExerciseInducedAngina <fct>
The positions of the into
values always lineup with the level of the list hierarchy. So, for example, if you don't care about retaining the heart disease status, you can do this:
div_dat %>%
fasten(
into = "Sex"
)
## # A tibble: 303 x 8
## Sex Age ChestPain BP Cholesterol BloodSugar MaximumHR
## <chr> <dbl> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 Fema… 41 Atypical… 130 204 FALSE 172
## 2 Fema… 57 Asymptom… 120 354 FALSE 163
## 3 Fema… 56 Atypical… 140 294 FALSE 153
## 4 Fema… 48 Non-angi… 130 275 FALSE 139
## 5 Fema… 58 Typical … 150 283 TRUE 162
## 6 Fema… 50 Non-angi… 120 219 FALSE 158
## 7 Fema… 58 Non-angi… 120 340 FALSE 172
## 8 Fema… 66 Typical … 150 226 FALSE 114
## 9 Fema… 69 Typical … 140 239 FALSE 151
## 10 Fema… 71 Atypical… 160 302 FALSE 162
## # … with 293 more rows, and 1 more variable: ExerciseInducedAngina <fct>
In contrast, if you want to forgo the sex indicator, empty strings will need to be used as placeholders so the names are applied at the correct levels.
div_dat %>%
fasten(
into = c("", "HeartDisease")
)
## # A tibble: 303 x 8
## HeartDisease Age ChestPain BP Cholesterol BloodSugar MaximumHR
## <chr> <dbl> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 No 41 Atypical… 130 204 FALSE 172
## 2 No 57 Asymptom… 120 354 FALSE 163
## 3 No 56 Atypical… 140 294 FALSE 153
## 4 No 48 Non-angi… 130 275 FALSE 139
## 5 No 58 Typical … 150 283 TRUE 162
## 6 No 50 Non-angi… 120 219 FALSE 158
## 7 No 58 Non-angi… 120 340 FALSE 172
## 8 No 66 Typical … 150 226 FALSE 114
## 9 No 69 Typical … 140 239 FALSE 151
## 10 No 71 Atypical… 160 302 FALSE 162
## # … with 293 more rows, and 1 more variable: ExerciseInducedAngina <fct>
Obviously, the classes of the split columns are not retained from the original data since the splitting and binding processes are independent.
As shown above, the default behavior for these functions is to split or bind “as much as possible”. However, it can be useful to have control over the shape of the split. This is where the depth
argument comes in. Suppose you want the same data frames at the leaves of the list, but only one level deep:
heart_disease %>%
divide(
Sex,
HeartDisease,
depth = 1
)
## $`Female|No`
## # A tibble: 72 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 41 Atypical … 130 204 FALSE 172 No
## 2 57 Asymptoma… 120 354 FALSE 163 Yes
## 3 56 Atypical … 140 294 FALSE 153 No
## 4 48 Non-angin… 130 275 FALSE 139 No
## 5 58 Typical a… 150 283 TRUE 162 No
## 6 50 Non-angin… 120 219 FALSE 158 No
## 7 58 Non-angin… 120 340 FALSE 172 No
## 8 66 Typical a… 150 226 FALSE 114 No
## 9 69 Typical a… 140 239 FALSE 151 No
## 10 71 Atypical … 160 302 FALSE 162 No
## # … with 62 more rows
##
## $`Male|No`
## # A tibble: 92 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 63 Typical a… 145 233 TRUE 150 No
## 2 37 Non-angin… 130 250 FALSE 187 No
## 3 56 Atypical … 120 236 FALSE 178 No
## 4 57 Asymptoma… 140 192 FALSE 148 No
## 5 44 Atypical … 120 263 FALSE 173 No
## 6 52 Non-angin… 172 199 TRUE 162 No
## 7 57 Non-angin… 150 168 FALSE 174 No
## 8 54 Asymptoma… 140 239 FALSE 160 No
## 9 49 Atypical … 130 266 FALSE 171 No
## 10 64 Typical a… 110 211 FALSE 144 Yes
## # … with 82 more rows
##
## $`Female|Yes`
## # A tibble: 25 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 62 Asymptoma… 140 268 FALSE 160 No
## 2 65 Asymptoma… 150 225 FALSE 114 No
## 3 61 Asymptoma… 130 330 FALSE 169 No
## 4 51 Asymptoma… 130 305 FALSE 142 Yes
## 5 62 Asymptoma… 160 164 FALSE 145 No
## 6 60 Asymptoma… 150 258 FALSE 157 No
## 7 61 Asymptoma… 145 307 FALSE 146 Yes
## 8 43 Asymptoma… 132 341 TRUE 136 Yes
## 9 62 Non-angin… 130 263 FALSE 97 No
## 10 63 Asymptoma… 150 407 FALSE 154 No
## # … with 15 more rows
##
## $`Male|Yes`
## # A tibble: 114 x 7
## Age ChestPain BP Cholesterol BloodSugar MaximumHR ExerciseInduced…
## <dbl> <fct> <dbl> <dbl> <lgl> <dbl> <fct>
## 1 67 Asymptoma… 160 286 FALSE 108 Yes
## 2 67 Asymptoma… 120 229 FALSE 129 Yes
## 3 63 Asymptoma… 130 254 FALSE 147 No
## 4 53 Asymptoma… 140 203 TRUE 155 Yes
## 5 56 Non-angin… 130 256 TRUE 142 Yes
## 6 48 Atypical … 110 229 FALSE 168 No
## 7 58 Atypical … 120 284 FALSE 160 No
## 8 58 Non-angin… 132 224 FALSE 173 No
## 9 60 Asymptoma… 130 206 FALSE 132 Yes
## 10 40 Asymptoma… 110 167 FALSE 114 Yes
## # … with 104 more rows
You now have list with four elements containing the subsets–the names of which are the concatenated levels of the split columns (see the sep
argument).
This argument also works when binding data. Going back to the original divided data frame, you may only want to bind the internal lists.
div_dat %>%
fasten(
into = "HeartDisease",
depth = 1
)
## $Female
## # A tibble: 97 x 8
## HeartDisease Age ChestPain BP Cholesterol BloodSugar MaximumHR
## <chr> <dbl> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 No 41 Atypical… 130 204 FALSE 172
## 2 No 57 Asymptom… 120 354 FALSE 163
## 3 No 56 Atypical… 140 294 FALSE 153
## 4 No 48 Non-angi… 130 275 FALSE 139
## 5 No 58 Typical … 150 283 TRUE 162
## 6 No 50 Non-angi… 120 219 FALSE 158
## 7 No 58 Non-angi… 120 340 FALSE 172
## 8 No 66 Typical … 150 226 FALSE 114
## 9 No 69 Typical … 140 239 FALSE 151
## 10 No 71 Atypical… 160 302 FALSE 162
## # … with 87 more rows, and 1 more variable: ExerciseInducedAngina <fct>
##
## $Male
## # A tibble: 206 x 8
## HeartDisease Age ChestPain BP Cholesterol BloodSugar MaximumHR
## <chr> <dbl> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 No 63 Typical … 145 233 TRUE 150
## 2 No 37 Non-angi… 130 250 FALSE 187
## 3 No 56 Atypical… 120 236 FALSE 178
## 4 No 57 Asymptom… 140 192 FALSE 148
## 5 No 44 Atypical… 120 263 FALSE 173
## 6 No 52 Non-angi… 172 199 TRUE 162
## 7 No 57 Non-angi… 150 168 FALSE 174
## 8 No 54 Asymptom… 140 239 FALSE 160
## 9 No 49 Atypical… 130 266 FALSE 171
## 10 No 64 Typical … 110 211 FALSE 144
## # … with 196 more rows, and 1 more variable: ExerciseInducedAngina <fct>
Note that the positions of the into
values are directly related to how shallow/deep you want the result to be.
The depth can also be controlled relative to the leaves by using negative integers. This can be useful if it is unclear how deep the list will be (or is). In the example above, suppose you didn't know how deep the list was, but you knew the heart disease status was the most internal split, and thus wanted to bind it.
div_dat %>%
fasten(
into = "HeartDisease",
depth = -1
)
## $Female
## # A tibble: 97 x 8
## HeartDisease Age ChestPain BP Cholesterol BloodSugar MaximumHR
## <chr> <dbl> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 No 41 Atypical… 130 204 FALSE 172
## 2 No 57 Asymptom… 120 354 FALSE 163
## 3 No 56 Atypical… 140 294 FALSE 153
## 4 No 48 Non-angi… 130 275 FALSE 139
## 5 No 58 Typical … 150 283 TRUE 162
## 6 No 50 Non-angi… 120 219 FALSE 158
## 7 No 58 Non-angi… 120 340 FALSE 172
## 8 No 66 Typical … 150 226 FALSE 114
## 9 No 69 Typical … 140 239 FALSE 151
## 10 No 71 Atypical… 160 302 FALSE 162
## # … with 87 more rows, and 1 more variable: ExerciseInducedAngina <fct>
##
## $Male
## # A tibble: 206 x 8
## HeartDisease Age ChestPain BP Cholesterol BloodSugar MaximumHR
## <chr> <dbl> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 No 63 Typical … 145 233 TRUE 150
## 2 No 37 Non-angi… 130 250 FALSE 187
## 3 No 56 Atypical… 120 236 FALSE 178
## 4 No 57 Asymptom… 140 192 FALSE 148
## 5 No 44 Atypical… 120 263 FALSE 173
## 6 No 52 Non-angi… 172 199 TRUE 162
## 7 No 57 Non-angi… 150 168 FALSE 174
## 8 No 54 Asymptom… 140 239 FALSE 160
## 9 No 49 Atypical… 130 266 FALSE 171
## 10 No 64 Typical … 110 211 FALSE 144
## # … with 196 more rows, and 1 more variable: ExerciseInducedAngina <fct>
The new depth is one less than the input. In this case, the same result was achieved because of the symmetry.
The next set of functions are rooted in functional programming (i.e. purrr
). In each instance, given a pre-defined function, you can easily control the scope of the data in which it is evaluated on. This is an incredibly useful and efficient philosphy for generating reusable code that is non-repetitive.
Often times a statistical analysis is concerned with mulitple outcomes/targets, though each one potentially uses the same set of explanatory variables. You could of course hard code the specific analysis steps for each outcome (e.g. formulas, etc.), but then the code becomes repetitive. You could also make separate datasets for each outcome with the associated predictors and then apply the same analysis process to each dataset, but then you waste resources by storing multiple copies of the same data in memory. dish()
allows you to distribute a function to various pairwise sets of columns from the same dataset in a single call. The motivation described above is merely that–this is a generalizable function that can be applied to any context.
As a simple first example, lets compute the Pearson correlation of blood pressure and cholesterol with all numeric columns:
heart_disease %>%
dish(
f = cor,
left = c(BP, Cholesterol),
right = is.numeric
)
## $BP
## $BP$Age
## [1] 0.2849459
##
## $BP$BP
## [1] 1
##
## $BP$Cholesterol
## [1] 0.1301201
##
## $BP$MaximumHR
## [1] -0.04535088
##
##
## $Cholesterol
## $Cholesterol$Age
## [1] 0.2089503
##
## $Cholesterol$BP
## [1] 0.1301201
##
## $Cholesterol$Cholesterol
## [1] 1
##
## $Cholesterol$MaximumHR
## [1] -0.003431832
The left
argument controls which columns are entered in the first argument of f
, and the right
argument controls which columns are entered into the second. In the example, you'll notice that the left
columns were also included in the set of right
columns, because they are, in fact, numeric()
. If you didn't want this, you can omit the right
argument, which will then include everything not in left
as the second argument of f
:
heart_disease %>%
dplyr::select_if(is.numeric) %>% #Need to do this so only numeric columns are evaluated
dish(
f = cor,
left = c(BP, Cholesterol)
)
## $BP
## $BP$Age
## [1] 0.2849459
##
## $BP$MaximumHR
## [1] -0.04535088
##
##
## $Cholesterol
## $Cholesterol$Age
## [1] 0.2089503
##
## $Cholesterol$MaximumHR
## [1] -0.003431832
Now lets suppose you want to regress both blood pressure and cholesterol on all other variables in the dataset. The each_
arguments allow you to control whether the column sets are entered into the function individually as vectors, or together in a single data frame. The former is the default, so you'll need to use this argument here:
heart_disease %>%
dish(
f = function(y, x) lm(y ~ ., data = x),
left = c(BP, Cholesterol),
each_right = FALSE
)
## $BP
##
## Call:
## lm(formula = y ~ ., data = x)
##
## Coefficients:
## (Intercept) Age
## 97.09264 0.50748
## SexMale ChestPainAtypical angina
## -3.95940 -9.84788
## ChestPainNon-anginal pain ChestPainAsymptomatic
## -9.56350 -10.11788
## BloodSugarTRUE MaximumHR
## 6.70106 0.09688
## ExerciseInducedAnginaYes HeartDiseaseYes
## 1.72906 6.00819
##
##
## $Cholesterol
##
## Call:
## lm(formula = y ~ ., data = x)
##
## Coefficients:
## (Intercept) Age
## 127.5108 1.2471
## SexMale ChestPainAtypical angina
## -23.6185 8.8270
## ChestPainNon-anginal pain ChestPainAsymptomatic
## 5.7660 8.0778
## BloodSugarTRUE MaximumHR
## -0.7327 0.3491
## ExerciseInducedAnginaYes HeartDiseaseYes
## 7.8039 12.5303
stratiply()
streamlines the familiar split()
then purrr::map()
approach by making it simple to select the stratification columns, and apply a function to each subset. Let's run a multiple logistic regression model using heart disease as the outcome, stratified by sex:
heart_disease %>%
stratiply(
f = glm,
by = Sex,
formula = HeartDisease ~ . -ChestPain,
family = "binomial"
)
## $Female
##
## Call: .f(formula = ..1, family = "binomial", data = .x[[i]])
##
## Coefficients:
## (Intercept) Age
## -6.95485 0.03362
## BP Cholesterol
## 0.04366 0.00354
## BloodSugarTRUE MaximumHR
## 0.47028 -0.02465
## ExerciseInducedAnginaYes
## 2.12039
##
## Degrees of Freedom: 96 Total (i.e. Null); 90 Residual
## Null Deviance: 110.7
## Residual Deviance: 76.21 AIC: 90.21
##
## $Male
##
## Call: .f(formula = ..1, family = "binomial", data = .x[[i]])
##
## Coefficients:
## (Intercept) Age
## 1.877142 0.024853
## BP Cholesterol
## 0.007710 0.008718
## BloodSugarTRUE MaximumHR
## -0.390310 -0.042231
## ExerciseInducedAnginaYes
## 1.106387
##
## Degrees of Freedom: 205 Total (i.e. Null); 199 Residual
## Null Deviance: 283.2
## Residual Deviance: 211.2 AIC: 225.2
Adding multiple stratification columns will produce a deeper list:
heart_disease %>%
stratiply(
f = glm,
by = c(Sex, ExerciseInducedAngina),
formula = HeartDisease ~ . -ChestPain,
family = "binomial"
)
## $Female
## $Female$No
##
## Call: .f(formula = ..1, family = "binomial", data = .x[[i]])
##
## Coefficients:
## (Intercept) Age BP Cholesterol
## -9.58123 0.01027 0.07213 0.00406
## BloodSugarTRUE MaximumHR
## 0.65807 -0.02540
##
## Degrees of Freedom: 74 Total (i.e. Null); 69 Residual
## Null Deviance: 62.53
## Residual Deviance: 49.93 AIC: 61.93
##
## $Female$Yes
##
## Call: .f(formula = ..1, family = "binomial", data = .x[[i]])
##
## Coefficients:
## (Intercept) Age BP Cholesterol
## 3.850121 0.049450 0.012214 0.001458
## BloodSugarTRUE MaximumHR
## 0.945153 -0.056538
##
## Degrees of Freedom: 21 Total (i.e. Null); 16 Residual
## Null Deviance: 28.84
## Residual Deviance: 23.36 AIC: 35.36
##
##
## $Male
## $Male$No
##
## Call: .f(formula = ..1, family = "binomial", data = .x[[i]])
##
## Coefficients:
## (Intercept) Age BP Cholesterol
## 2.728309 0.053058 -0.003653 0.005387
## BloodSugarTRUE MaximumHR
## -1.106214 -0.042036
##
## Degrees of Freedom: 128 Total (i.e. Null); 123 Residual
## Null Deviance: 174
## Residual Deviance: 142.3 AIC: 154.3
##
## $Male$Yes
##
## Call: .f(formula = ..1, family = "binomial", data = .x[[i]])
##
## Coefficients:
## (Intercept) Age BP Cholesterol
## 0.70070 -0.01348 0.03561 0.01342
## BloodSugarTRUE MaximumHR
## 1.09404 -0.04531
##
## Degrees of Freedom: 76 Total (i.e. Null); 71 Residual
## Null Deviance: 75.94
## Residual Deviance: 60.53 AIC: 72.53
typly()
allows you to apply a function to columns of a data frame whose type inherits at least one (or none) of the specified candidates. It is similar to purrr::map_if()
, but uses methods::is()
for determining types (see some_type()
), and only returns the results for the selected columns:
heart_disease %>%
typly(
f = mean,
types = c("numeric", "logical")
)
## $Age
## [1] 54.43894
##
## $BP
## [1] 131.6898
##
## $Cholesterol
## [1] 246.6931
##
## $BloodSugar
## [1] 0.1485149
##
## $MaximumHR
## [1] 149.6073
You can use the negated
argument to apply the function to columns that are not any of the listed types:
heart_disease %>%
typly(
f = table,
types = c("numeric", "logical"),
negated = TRUE,
useNA = "always"
)
## $Sex
##
## Female Male <NA>
## 97 206 0
##
## $ChestPain
##
## Typical angina Atypical angina Non-anginal pain Asymptomatic
## 23 50 86 144
## <NA>
## 0
##
## $ExerciseInducedAngina
##
## No Yes <NA>
## 204 99 0
##
## $HeartDisease
##
## No Yes <NA>
## 164 139 0
absorb()
allows you to create collections of strings that use keys as placeholders, which are then populated by the values. This is useful for setting up results of your analysis to be presented in a specific format that is independent of the dataset at hand. Here's a simple example:
absorb(
key = c("mean", "sd", "var"),
value = c("10", "2", "4"),
text =
c(
"MEAN: mean, SD: sd",
"VAR: var = sd^2",
MEAN = "mean"
)
)
## MEAN
## "MEAN: 10, SD: 2" "VAR: 4 = 2^2" "10"
The input text
is scanned to look for the keys (interpreted as regular expressions) and are replaced with the corresponding values.
Let's look at a more useful example. Suppose we have a collection summary statistics for patient age by angina and heart disease status:
age_stats <-
heart_disease %>%
dplyr::group_by(
ExerciseInducedAngina,
HeartDisease
) %>%
dplyr::summarise(
mean = round(mean(Age), 2),
sd = round(sd(Age), 2),
median = median(Age),
iqr = IQR(Age)
) %>%
dplyr::ungroup() %>%
tidyr::gather(
key = "key",
value = "value",
-ExerciseInducedAngina,
-HeartDisease
)
age_stats
## # A tibble: 16 x 4
## ExerciseInducedAngina HeartDisease key value
## <fct> <fct> <chr> <dbl>
## 1 No No mean 52.4
## 2 No Yes mean 57.1
## 3 Yes No mean 53.6
## 4 Yes Yes mean 56.2
## 5 No No sd 9.68
## 6 No Yes sd 7.56
## 7 Yes No sd 8.54
## 8 Yes Yes sd 8.27
## 9 No No median 52
## 10 No Yes median 58
## 11 Yes No median 53
## 12 Yes Yes median 57
## 13 No No iqr 15
## 14 No Yes iqr 10
## 15 Yes No iqr 12.5
## 16 Yes Yes iqr 8.25
Next, you can specify the string format you want to nicely summarise the information in each group:
age_stats %>%
dplyr::group_by(
ExerciseInducedAngina,
HeartDisease
) %>%
dplyr::summarise(
Summary =
absorb(
key = key,
value = value,
text =
c(
"mean (sd) | median (iqr)"
)
)
) %>%
dplyr::ungroup()
## # A tibble: 4 x 3
## ExerciseInducedAngina HeartDisease Summary
## <fct> <fct> <chr>
## 1 No No 52.42 (9.68) | 52 (15)
## 2 No Yes 57.1 (7.56) | 58 (10)
## 3 Yes No 53.61 (8.54) | 53 (12.5)
## 4 Yes Yes 56.24 (8.27) | 57 (8.25)
Of course, this is much more useful when the summary data is already in the format of age_stats
.
In the case where the key pattern found in the string template is repeated, its values are collapsed into a concatenated string:
absorb(
key = age_stats$key,
value = age_stats$value,
text = c("(mean) / (sd)", "median")
)
## [1] "(52.42_57.1_53.61_56.24) / (9.68_7.56_8.54_8.27)"
## [2] "52_58_53_57"
The sep
argument can be used to customize how values are separated.
It may be useful in some cases to evaluate the resulting string as an expression. You can setup the prior example so that the result contains valid expressions:
absorb(
key = age_stats$key,
value = age_stats$value,
text = c("(mean) / (sd)", "median"),
sep = "+",
evaluate = TRUE,
trace = TRUE
)
## (mean) / (sd) median
## (mean) / (sd) median
## (52.42+57.1+53.61+56.24) / (sd) median
## (52.42+57.1+53.61+56.24) / (sd) 52+58+53+57
## [[1]]
## [1] 6.442584
##
## [[2]]
## [1] 220
These statistics are not entirely useful here, but you get the point.
depths()
and depths_string()
are used for traversing a list structure to find the elements that satisfy a predicate. The former produces a vector of the unique depths that contain at least one element where predicate
is true, and the latter creates a string representation of the traversal with the actual positions.
#Make a divided data frame
div_dat1 <-
heart_disease %>%
divide(
Sex,
HeartDisease
)
#Find the depths of the data frames
div_dat1 %>%
depths(
predicate = is.data.frame
)
## [1] 2
div_dat1 %>%
depths_string(
predicate = is.data.frame
)
## [1] "{1{-1,-2},2{-1,-2}}"
In the string result, the brackets represent the level of the list, and the integers represent the index at that level, which are negative if predicate
is true for that element.
By default, the algorithm continues when rlang::is_bare_list()
so that the traversal can end with list-like objects. However, the bare
argument can be used to traverse deeper into the list:
div_dat1 %>%
depths_string(
predicate = is.data.frame,
bare = FALSE
)
## [1] "{1{-1{1,2,3,4,5,6,7},-2{1,2,3,4,5,6,7}},2{-1{1,2,3,4,5,6,7},-2{1,2,3,4,5,6,7}}}"
Now it also evaluated the columns of each data frame in the list, though it still found the correct positions where the predicate held.
muddle()
can be used to randomly shuffle columns in a data frame. This is a convenient way to remove confounding when exploring the effects of a variable on an outcome.
set.seed(123)
heart_disease %>%
muddle()
## # A tibble: 303 x 9
## Age Sex ChestPain BP Cholesterol BloodSugar MaximumHR
## <dbl> <fct> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 43 Fema… Non-angi… 130 214 FALSE 120
## 2 44 Male Asymptom… 130 204 FALSE 143
## 3 68 Fema… Asymptom… 128 242 FALSE 125
## 4 35 Fema… Asymptom… 120 269 FALSE 163
## 5 45 Fema… Atypical… 138 240 FALSE 169
## 6 54 Male Asymptom… 128 209 FALSE 182
## 7 61 Male Asymptom… 100 258 FALSE 152
## 8 57 Fema… Asymptom… 152 229 FALSE 142
## 9 67 Male Non-angi… 110 283 FALSE 138
## 10 51 Fema… Atypical… 125 204 TRUE 161
## # … with 293 more rows, and 2 more variables: ExerciseInducedAngina <fct>,
## # HeartDisease <fct>
All columns are permuted by default. Use the at
argument to only shuffle certain ones:
heart_disease %>%
muddle(
at = Age
)
## # A tibble: 303 x 9
## Age Sex ChestPain BP Cholesterol BloodSugar MaximumHR
## <dbl> <fct> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 47 Male Typical … 145 233 TRUE 150
## 2 49 Male Asymptom… 160 286 FALSE 108
## 3 59 Male Asymptom… 120 229 FALSE 129
## 4 35 Male Non-angi… 130 250 FALSE 187
## 5 44 Fema… Atypical… 130 204 FALSE 172
## 6 29 Male Atypical… 120 236 FALSE 178
## 7 45 Fema… Asymptom… 140 268 FALSE 160
## 8 59 Fema… Asymptom… 120 354 FALSE 163
## 9 46 Male Asymptom… 130 254 FALSE 147
## 10 54 Male Asymptom… 140 203 TRUE 155
## # … with 293 more rows, and 2 more variables: ExerciseInducedAngina <fct>,
## # HeartDisease <fct>
Additional arguments can be passed to sample()
as well. For example, you might want five random values from each column:
heart_disease %>%
muddle(
size = 5
)
## # A tibble: 5 x 9
## Age Sex ChestPain BP Cholesterol BloodSugar MaximumHR
## <dbl> <fct> <fct> <dbl> <dbl> <lgl> <dbl>
## 1 54 Fema… Asymptom… 136 275 TRUE 163
## 2 62 Male Asymptom… 125 271 FALSE 195
## 3 52 Male Non-angi… 150 286 FALSE 105
## 4 64 Male Asymptom… 106 273 FALSE 163
## 5 54 Male Atypical… 120 199 FALSE 182
## # … with 2 more variables: ExerciseInducedAngina <fct>, HeartDisease <fct>
stretch()
is for spanning keys and values across the columns. It is similar to tidyr::spread()
, but allows for any number of keys and values to be selected. It's functionality is more so targeted at setting up your results to be presented in a table, rather than for general data manipulation. Thus, the resulting column names and orderings are setup to make it a seamless transition.
To illustrate, lets first collect the parameter estimates and standard errors from multiple regression models using blood pressure and cholesterol as outcomes, stratified by sex:
mod_results <-
heart_disease %>%
#For each sex
stratiply(
by = Sex,
f =
~.x %>%
#Collect coefficients and standard errors for model estimates on multiple outcomes
dish(
f = function(y, x) {
model <- lm(y ~ ., data = x)
tibble::tibble(
Parameter = names(model$coefficients),
Estimate = model$coefficients,
SE = sqrt(diag(vcov(model)))
)
},
left = c(BP, Cholesterol),
each_right = FALSE
)
) %>%
#Gather results into a single data frame
fasten(
into = c("Sex", "Outcome")
)
mod_results
## # A tibble: 36 x 5
## Sex Outcome Parameter Estimate SE
## <chr> <chr> <chr> <dbl> <dbl>
## 1 Female BP (Intercept) 93.7 24.7
## 2 Female BP Age 0.533 0.210
## 3 Female BP ChestPainAtypical angina -16.3 9.73
## 4 Female BP ChestPainNon-anginal pain -15.3 9.15
## 5 Female BP ChestPainAsymptomatic -14.2 9.64
## 6 Female BP BloodSugarTRUE 6.92 5.53
## 7 Female BP MaximumHR 0.123 0.0993
## 8 Female BP ExerciseInducedAnginaYes 8.00 4.89
## 9 Female BP HeartDiseaseYes 12.1 5.21
## 10 Female Cholesterol (Intercept) -12.1 91.6
## # … with 26 more rows
Now lets span the estimates and standard errors across the columns for both outcomes:
straught1 <-
mod_results %>%
stretch(
key = Outcome,
value = c(Estimate, SE)
)
straught1
## # A tibble: 18 x 6
## Sex Parameter BP_Estimate BP_SE Cholesterol_Est… Cholesterol_SE
## <chr> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 Female (Intercept) 93.7 24.7 -12.1 91.6
## 2 Female Age 0.533 0.210 2.30 0.779
## 3 Female BloodSugarTR… 6.92 5.53 24.6 20.6
## 4 Female ChestPainAsy… -14.2 9.64 34.0 35.8
## 5 Female ChestPainAty… -16.3 9.73 22.7 36.1
## 6 Female ChestPainNon… -15.3 9.15 34.1 34.0
## 7 Female ExerciseIndu… 8.00 4.89 8.14 18.2
## 8 Female HeartDisease… 12.1 5.21 5.88 19.3
## 9 Female MaximumHR 0.123 0.0993 0.720 0.369
## 10 Male (Intercept) 105. 14.7 151. 38.7
## 11 Male Age 0.480 0.142 0.765 0.374
## 12 Male BloodSugarTR… 4.96 3.12 -6.85 8.23
## 13 Male ChestPainAsy… -10.0 4.25 3.38 11.2
## 14 Male ChestPainAty… -8.61 4.68 9.55 12.3
## 15 Male ChestPainNon… -6.99 4.31 -0.802 11.4
## 16 Male ExerciseIndu… -1.08 2.78 6.88 7.33
## 17 Male HeartDisease… 3.07 2.80 13.4 7.39
## 18 Male MaximumHR 0.0392 0.0601 0.239 0.158
The sep
argument controls how the new column names are concatenated.
Now suppose you wanted to also span sex across the columns. It can just be added to the keys:
straught2 <-
mod_results %>%
stretch(
key = c(Sex, Outcome),
value = c(Estimate, SE)
)
straught2
## # A tibble: 9 x 9
## Parameter Female_BP_Estim… Female_BP_SE Female_Choleste… Female_Choleste…
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Interce… 93.7 24.7 -12.1 91.6
## 2 Age 0.533 0.210 2.30 0.779
## 3 BloodSug… 6.92 5.53 24.6 20.6
## 4 ChestPai… -14.2 9.64 34.0 35.8
## 5 ChestPai… -16.3 9.73 22.7 36.1
## 6 ChestPai… -15.3 9.15 34.1 34.0
## 7 Exercise… 8.00 4.89 8.14 18.2
## 8 HeartDis… 12.1 5.21 5.88 19.3
## 9 MaximumHR 0.123 0.0993 0.720 0.369
## # … with 4 more variables: Male_BP_Estimate <dbl>, Male_BP_SE <dbl>,
## # Male_Cholesterol_Estimate <dbl>, Male_Cholesterol_SE <dbl>
Notice how the resulting columns are positioned. They are sequentially sorted starting with the outer-most key in a hierarchical manner. Since there are multiple value
columns in this example, their names are also appended to the result in the order in which they were requested.
grable()
stands for “graded table”. It is used to stack headers into a knitr::kable()
in an automated fashion by iteratively parsing the column names by the specified delimiter. The result of stretch()
was purposely made to flow directly into this function.
straught1 %>%
grable(
at = -c(Sex, Parameter)
)
Sex | Parameter | Estimate | SE | Estimate | SE |
---|---|---|---|---|---|
Female | (Intercept) | 93.7187493 | 24.6536900 | -12.1297785 | 91.6044593 |
Female | Age | 0.5330329 | 0.2096016 | 2.2975419 | 0.7788061 |
Female | BloodSugarTRUE | 6.9178015 | 5.5329199 | 24.5796821 | 20.5583885 |
Female | ChestPainAsymptomatic | -14.1810178 | 9.6423236 | 34.0195680 | 35.8274902 |
Female | ChestPainAtypical angina | -16.3247037 | 9.7289213 | 22.6736038 | 36.1492568 |
Female | ChestPainNon-anginal pain | -15.3471449 | 9.1541813 | 34.1239627 | 34.0137249 |
Female | ExerciseInducedAnginaYes | 7.9952541 | 4.8947727 | 8.1371920 | 18.1872573 |
Female | HeartDiseaseYes | 12.0913966 | 5.2058240 | 5.8795266 | 19.3430149 |
Female | MaximumHR | 0.1226586 | 0.0992801 | 0.7201695 | 0.3688901 |
Male | (Intercept) | 105.2923383 | 14.6933205 | 150.9743256 | 38.7467039 |
Male | Age | 0.4800104 | 0.1417168 | 0.7654107 | 0.3737111 |
Male | BloodSugarTRUE | 4.9616363 | 3.1197639 | -6.8540853 | 8.2269061 |
Male | ChestPainAsymptomatic | -10.0474949 | 4.2547960 | 3.3795322 | 11.2200180 |
Male | ChestPainAtypical angina | -8.6148546 | 4.6798629 | 9.5540927 | 12.3409315 |
Male | ChestPainNon-anginal pain | -6.9875791 | 4.3056911 | -0.8015539 | 11.3542299 |
Male | ExerciseInducedAnginaYes | -1.0827067 | 2.7780388 | 6.8846691 | 7.3257672 |
Male | HeartDiseaseYes | 3.0740495 | 2.8029917 | 13.4020228 | 7.3915687 |
Male | MaximumHR | 0.0391572 | 0.0600935 | 0.2387579 | 0.1584683 |
The at
argument specifies which columns should be spanned in the header. Let's try it with the two-key example from above:
straught2 %>%
grable(
at = -Parameter
)
Parameter | Estimate | SE | Estimate | SE | Estimate | SE | Estimate | SE |
---|---|---|---|---|---|---|---|---|
(Intercept) | 93.7187493 | 24.6536900 | -12.1297785 | 91.6044593 | 105.2923383 | 14.6933205 | 150.9743256 | 38.7467039 |
Age | 0.5330329 | 0.2096016 | 2.2975419 | 0.7788061 | 0.4800104 | 0.1417168 | 0.7654107 | 0.3737111 |
BloodSugarTRUE | 6.9178015 | 5.5329199 | 24.5796821 | 20.5583885 | 4.9616363 | 3.1197639 | -6.8540853 | 8.2269061 |
ChestPainAsymptomatic | -14.1810178 | 9.6423236 | 34.0195680 | 35.8274902 | -10.0474949 | 4.2547960 | 3.3795322 | 11.2200180 |
ChestPainAtypical angina | -16.3247037 | 9.7289213 | 22.6736038 | 36.1492568 | -8.6148546 | 4.6798629 | 9.5540927 | 12.3409315 |
ChestPainNon-anginal pain | -15.3471449 | 9.1541813 | 34.1239627 | 34.0137249 | -6.9875791 | 4.3056911 | -0.8015539 | 11.3542299 |
ExerciseInducedAnginaYes | 7.9952541 | 4.8947727 | 8.1371920 | 18.1872573 | -1.0827067 | 2.7780388 | 6.8846691 | 7.3257672 |
HeartDiseaseYes | 12.0913966 | 5.2058240 | 5.8795266 | 19.3430149 | 3.0740495 | 2.8029917 | 13.4020228 | 7.3915687 |
MaximumHR | 0.1226586 | 0.0992801 | 0.7201695 | 0.3688901 | 0.0391572 | 0.0600935 | 0.2387579 | 0.1584683 |
The tables can then be piped through subsequent customized processing with kableExtra
tools.
descriptives()
produces a data frame in long format with a collection of descriptive statistics.
heart_disease %>%
descriptives()
## # A tibble: 111 x 9
## fun_eval fun_key col_ind col_lab val_ind val_lab val_dbl val_chr val_cbn
## <chr> <chr> <int> <chr> <int> <chr> <dbl> <chr> <chr>
## 1 all availa… 1 Age 1 <NA> 303 <NA> 303
## 2 all availa… 2 Sex 1 <NA> 303 <NA> 303
## 3 all availa… 3 ChestP… 1 <NA> 303 <NA> 303
## 4 all availa… 4 BP 1 <NA> 303 <NA> 303
## 5 all availa… 5 Choles… 1 <NA> 303 <NA> 303
## 6 all availa… 6 BloodS… 1 <NA> 303 <NA> 303
## 7 all availa… 7 Maximu… 1 <NA> 303 <NA> 303
## 8 all availa… 8 Exerci… 1 <NA> 303 <NA> 303
## 9 all availa… 9 HeartD… 1 <NA> 303 <NA> 303
## 10 all class 1 Age 1 <NA> NA numeric numeric
## # … with 101 more rows
univariate_associations()
is a special case of dish()
specifically for computing association metrics of one or more responses
with a collection of predictors
.
heart_disease %>%
univariate_associations(
f = list(AIC = function(y, x) AIC(lm(y ~ x))),
responses = c(BP, Cholesterol)
)
## # A tibble: 14 x 3
## response predictor AIC
## <chr> <chr> <dbl>
## 1 BP Age 2577.
## 2 BP Sex 2602.
## 3 BP ChestPain 2598.
## 4 BP BloodSugar 2593.
## 5 BP MaximumHR 2602.
## 6 BP ExerciseInducedAngina 2602.
## 7 BP HeartDisease 2596.
## 8 Cholesterol Age 3243.
## 9 Cholesterol Sex 3244.
## 10 Cholesterol ChestPain 3259.
## 11 Cholesterol BloodSugar 3257.
## 12 Cholesterol MaximumHR 3257.
## 13 Cholesterol ExerciseInducedAngina 3256.
## 14 Cholesterol HeartDisease 3255.
univariate_table()
allows you to create a custom descriptive table for a dataset. It uses almost every function in the package across its span of capabilities.
heart_disease %>%
univariate_table()
Variable | Level | Summary |
---|---|---|
Age | 56 (48, 61) | |
Sex | Female | 97 (32.01%) |
Male | 206 (67.99%) | |
ChestPain | Typical angina | 23 (7.59%) |
Atypical angina | 50 (16.5%) | |
Non-anginal pain | 86 (28.38%) | |
Asymptomatic | 144 (47.52%) | |
BP | 130 (120, 140) | |
Cholesterol | 241 (211, 275) | |
MaximumHR | 153 (133.5, 166) | |
ExerciseInducedAngina | No | 204 (67.33%) |
Yes | 99 (32.67%) | |
HeartDisease | No | 164 (54.13%) |
Yes | 139 (45.87%) |
See vignette("describe")
for a more in-depth introduction to these functions.