broom and dplyr

While broom is useful for summarizing the result of a single analysis in a consistent format, it is really designed for high-throughput applications, where you must combine results from multiple analyses. These could be subgroups of data, analyses using different models, bootstrap replicates, permutations, and so on. In particular, it plays well with the group_by and do functions in dplyr.

Let's try this on a simple dataset, the built-in Orange data.frame.

library(broom)
library(dplyr)
data(Orange)

dim(Orange)
## [1] 35  3
head(Orange)
##   Tree  age circumference
## 1    1  118            30
## 2    1  484            58
## 3    1  664            87
## 4    1 1004           115
## 5    1 1231           120
## 6    1 1372           142

This contains 35 observations of three variables: Tree, age, and circumference. Tree is a factor with five levels describing five trees. As might be expected, age and circumference are correlated:

cor(Orange$age, Orange$circumference)
## [1] 0.9135189
library(ggplot2)
ggplot(Orange, aes(age, circumference, color = Tree)) + geom_line()

plot of chunk unnamed-chunk-1

Suppose you want to test for correlations individually within each tree. You can do this with dplyr's group_by:

Orange %>% group_by(Tree) %>% summarize(correlation = cor(age, circumference))
## Source: local data frame [5 x 2]
## 
##   Tree correlation
## 1    3   0.9881766
## 2    1   0.9854675
## 3    5   0.9877376
## 4    2   0.9873624
## 5    4   0.9844610

(Note that the correlations are much higher than the aggregated one, and furthermore we can now see it is similar across trees).

Suppose that instead of simply estimating a correlation, we want to perform a hypothesis test with cor.test:

cor.test(Orange$age, Orange$circumference)
## 
##  Pearson's product-moment correlation
## 
## data:  Orange$age and Orange$circumference
## t = 12.9002, df = 33, p-value = 1.932e-14
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.8342364 0.9557955
## sample estimates:
##       cor 
## 0.9135189

This contains multiple values we could want in our output. Some are vectors of length 1, such as the p-value and the estimate, and some are longer, such as the confidence interval. broom's tidy S3 method, combined with dplyr's do, makes it easy to summarize the information about each test:

Orange %>% group_by(Tree) %>% do(tidy(cor.test(.$age, .$circumference)))
## Source: local data frame [5 x 7]
## Groups: Tree
## 
##   Tree  estimate statistic      p.value parameter  conf.low conf.high
## 1    3 0.9881766  14.41188 2.901046e-05         5 0.9189858 0.9983260
## 2    1 0.9854675  12.97258 4.851902e-05         5 0.9012111 0.9979400
## 3    5 0.9877376  14.14686 3.177093e-05         5 0.9160865 0.9982635
## 4    2 0.9873624  13.93129 3.425041e-05         5 0.9136142 0.9982101
## 5    4 0.9844610  12.53575 5.733090e-05         5 0.8946782 0.9977964

This becomes even more useful when applied to regressions, which give more than one row of output within each model:

Orange %>% group_by(Tree) %>% do(tidy(lm(age ~ circumference, data=.)))
## Source: local data frame [10 x 6]
## Groups: Tree
## 
##    Tree          term    estimate  std.error  statistic      p.value
## 1     3   (Intercept) -209.512321 85.2682904 -2.4570954 5.743323e-02
## 2     3 circumference   12.038885  0.8353445 14.4118806 2.901046e-05
## 3     1   (Intercept) -264.673437 98.6205569 -2.6837553 4.362351e-02
## 4     1 circumference   11.919245  0.9188029 12.9725813 4.851902e-05
## 5     5   (Intercept)  -54.484097 76.8862788 -0.7086323 5.102144e-01
## 6     5 circumference    8.787132  0.6211365 14.1468610 3.177093e-05
## 7     2   (Intercept) -132.439725 83.1314146 -1.5931369 1.720092e-01
## 8     2 circumference    7.795225  0.5595479 13.9312907 3.425041e-05
## 9     4   (Intercept)  -76.513671 88.2943757 -0.8665747 4.257969e-01
## 10    4 circumference    7.169842  0.5719516 12.5357484 5.733090e-05

You can just as easily perform multiple regressions within each group, as shown here on the mtcars dataset. We group the data into automatic and manual cars (the am column), then perform the regression within each.

data(mtcars)
head(mtcars)
##                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
## Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1
mtcars %>% group_by(am) %>% do(tidy(lm(wt ~ mpg + qsec + gear, .)))
## Source: local data frame [8 x 6]
## Groups: am
## 
##   am        term    estimate  std.error   statistic      p.value
## 1  0 (Intercept)  4.91754623 1.39665675  3.52094116 0.0030879519
## 2  0         mpg -0.19188914 0.04428329 -4.33321746 0.0005909953
## 3  0        qsec  0.09191361 0.09832067  0.93483509 0.3646797728
## 4  0        gear  0.14653754 0.36819363  0.39799041 0.6962441554
## 5  1 (Intercept)  4.28307028 3.45859958  1.23838281 0.2469014834
## 6  1         mpg -0.10098320 0.02943409 -3.43082488 0.0074984578
## 7  1        qsec  0.03983165 0.15112135  0.26357393 0.7980436972
## 8  1        gear -0.02288330 0.34878226 -0.06560912 0.9491232955

What if you want not just the tidy output, but the augment and glance outputs as well, while still performing each regression only once? First, save the modeling result into a column fit.

regressions <- mtcars %>% group_by(cyl) %>%
    do(fit = lm(wt ~ mpg + qsec + gear, .))
regressions
## Source: local data frame [3 x 2]
## Groups: <by row>
## 
##   cyl     fit
## 1   4 <S3:lm>
## 2   6 <S3:lm>
## 3   8 <S3:lm>

This creates a rowwise data frame. Tidying methods are designed to work seamlessly with rowwise data frames, grouping them and performing tidying on each row:

regressions %>% tidy(fit)
## Source: local data frame [12 x 6]
## Groups: cyl
## 
##    cyl        term     estimate  std.error    statistic     p.value
## 1    4 (Intercept) -0.772662398 2.22788026 -0.346815047 0.738922522
## 2    4         mpg -0.081831569 0.02381787 -3.435721470 0.010900258
## 3    4        qsec  0.216651715 0.07590773  2.854145649 0.024542352
## 4    4        gear  0.267469618 0.24454173  1.093758587 0.310264464
## 5    6 (Intercept) -7.785808290 3.35484932 -2.320762438 0.103012430
## 6    6         mpg  0.043283282 0.05196724  0.832895538 0.466010439
## 7    6        qsec  0.421998315 0.09136817  4.618657640 0.019102466
## 8    6        gear  0.638320019 0.20524143  3.110093347 0.052877671
## 9    8 (Intercept)  0.005971578 4.27460511  0.001396989 0.998912840
## 10   8         mpg -0.176924561 0.05570852 -3.175897889 0.009888094
## 11   8        qsec  0.369405813 0.19309553  1.913072859 0.084773637
## 12   8        gear  0.142762416 0.31664127  0.450864838 0.661705480
regressions %>% augment(fit)
## Source: local data frame [32 x 12]
## Groups: cyl
## 
##    cyl    wt  mpg  qsec gear  .fitted    .se.fit        .resid       .hat
## 1    4 2.320 22.8 18.61    4 2.463345 0.14185713 -0.1433447361 0.19729750
## 2    4 3.190 24.4 20.00    4 2.633560 0.11995523  0.5564398892 0.14107744
## 3    4 3.150 22.8 22.90    4 3.392781 0.29892232 -0.2427805952 0.87606415
## 4    4 2.200 32.4 19.47    4 1.864082 0.17384682  0.3359178471 0.29631436
## 5    4 1.615 30.4 18.52    4 1.821926 0.14754013 -0.2069261605 0.21342216
## 6    4 1.835 33.9 19.90    4 1.834495 0.20986098  0.0005049623 0.43179998
## 7    4 2.465 21.5 20.01    3 2.605569 0.24938708 -0.1405685584 0.60977149
## 8    4 1.935 27.3 18.90    4 2.157932 0.10457128 -0.2229316749 0.10721217
## 9    4 2.140 26.0 16.70    5 2.055149 0.21764285  0.0848514414 0.46441691
## 10   4 1.513 30.4 16.90    5 1.738420 0.20098449 -0.2254199999 0.39604478
## 11   4 2.780 21.4 18.60    4 2.575742 0.16489346  0.2042575850 0.26657908
## 12   6 2.620 21.0 16.46    4 2.622513 0.06977405 -0.0025129716 0.63887606
## 13   6 2.875 21.0 17.02    4 2.858832 0.04582155  0.0161679722 0.27552963
## 14   6 3.215 21.4 19.44    3 3.259061 0.07881413 -0.0440612436 0.81514886
## 15   6 3.460 18.1 20.22    3 3.445385 0.07971081  0.0146149026 0.83380242
## 16   6 3.440 19.2 18.30    4 3.321080 0.04906607  0.1189200376 0.31593034
## 17   6 3.440 17.8 18.90    4 3.513682 0.06297695 -0.0736823560 0.52046551
## 18   6 2.770 19.7 15.50    5 2.799446 0.06763176 -0.0294463411 0.60024719
## 19   8 3.440 18.7 17.02    3 3.413056 0.25023886  0.0269435247 0.24010420
## 20   8 3.570 14.3 15.84    3 3.755626 0.29826372 -0.1856256827 0.34110739
## 21   8 4.070 16.4 17.40    3 3.960357 0.17508288  0.1096428267 0.11753782
## 22   8 3.730 17.3 17.60    3 3.875006 0.21816235 -0.1450062314 0.18249453
## 23   8 3.780 15.2 18.00    3 4.394310 0.22251508 -0.6143101336 0.18984936
## 24   8 5.250 10.4 17.98    3 5.236160 0.32575214  0.0138400920 0.40687856
## 25   8 5.424 10.4 17.82    3 5.177055 0.31428013  0.2469450220 0.37872512
## 26   8 5.345 14.7 17.42    3 4.268517 0.15736423  1.0764829575 0.09495161
## 27   8 3.520 15.5 16.87    3 3.923804 0.15764239 -0.4038041971 0.09528758
## 28   8 3.435 15.2 17.30    3 4.135726 0.15093719 -0.7007260647 0.08735400
## 29   8 3.840 13.3 15.41    3 3.773706 0.38738008  0.0662942561 0.57539333
## 30   8 3.845 19.2 17.05    3 3.335676 0.27317862  0.5093236306 0.28614335
## 31   8 3.170 15.8 14.50    5 3.280760 0.36186300 -0.1107598851 0.50208658
## 32   8 3.570 15.0 14.60    5 3.459240 0.36186300  0.1107598851 0.50208658
## Variables not shown: .sigma (dbl), .cooksd (dbl), .std.resid (dbl)
regressions %>% glance(fit)
## Source: local data frame [3 x 12]
## Groups: cyl
## 
##   cyl r.squared adj.r.squared      sigma statistic     p.value df
## 1   4 0.7799131     0.6855901 0.31936726  8.268539 0.010597767  4
## 2   6 0.9699947     0.9399893 0.08729425 32.327394 0.008743763  4
## 3   8 0.6521278     0.5477661 0.51068708  6.248728 0.011614605  4
## Variables not shown: logLik (dbl), AIC (dbl), BIC (dbl), deviance (dbl),
##   df.residual (int)

By combining the estimates and p-values across all groups into the same tidy data frame (instead of, for example, a list of output model objects), a new class of analyses and visualizations becomes straightforward. This includes

In each of these cases, we can easily filter, facet, or distinguish based on the term column. In short, this makes the tools of tidy data analysis available for the results of data analysis and models, not just the inputs.