-
Notifications
You must be signed in to change notification settings - Fork 79
/
Supporting_additional_objects.Rmd
192 lines (147 loc) · 6.43 KB
/
Supporting_additional_objects.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
---
title: "Supporting additional objects"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Supporting additional objects}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
## Introduction
The `skim()` function summarizes data types contained within data frames. It
comes with a set of default summary functions for a wide variety of data types,
but this is not comprehensive. Package authors can add support for skimming
their specific data types in their packages, and they can provide different
defaults in their own summary functions.
This example will illustrate this by creating support for the `sf` object
produced by the "sf: Simple Features for R" package. For any object this
involves two required elements and one optional element.
- experiment with interactive changes
- create methods to `get_skimmers` for different objects within this package
- if needed, define any custom statistics
If you are adding skim support to a package you will also need to add `skimr`
to the list of imports. Note that to run the code in this vignette you will
need to install the `sf` package. We suggest not doing that, and instead
substitute whatever package you are working with.
```{r}
library(skimr)
library(sf)
nc <- st_read(system.file("shape/nc.shp", package = "sf"))
```
```{r}
class(nc)
class(nc$geometry)
```
Unlike the example of having a new type of data in a column of a simple data
frame in the "Using skimr" vignette, this is a different type of object
with special attributes.
In this object there is also a column of a class that does not have default
skimmers. By default, skimr falls back to use the sfl for character variables.
```{r}
skim(nc$geometry)
```
## Experiment interactively
`skimr` has an opinionated list of functions for each class (e.g. numeric,
factor) of data. The core package supports many commonly used classes,
but there are many others. You can investigate these defaults by calling
`get_default_skimmer_names()`.
What if your data type isn't covered by defaults? `skimr` usually falls
back to treating the type as a character, which isn't necessarily helpful. In
this case, you're best off adding your data type with `skim_with()`.
Before we begin, we'll be using the following custom summary statistic
throughout. The function gets the geometry's crs and combines it into a string.
```{r}
get_crs <- function(column) {
crs <- sf::st_crs(column)
paste0("epsg: ", crs[["epsg"]], " proj4string: '", crs[["proj4string"]], "'")
}
```
This function, like all summary functions used by `skimr` has two notable
features.
* It accepts a vector as its single argument
* It returns a scalar
There are a lot of functions that fulfill these criteria:
* existing functions from base, stats, or other packages,
* lambda's created using the Tidyverse-style syntax
* custom functions that have been defined in the `skimr` package
* custom functions that you have defined.
Not fulfilling the two criteria can lead to some very confusing behavior within
`skimr`. Beware! An example of this issue is the base `quantile()` function in
default `skimr` percentiles are returned by using `quantile()` five
times.
Next, we create a custom skimming function. To do this, we need to think about
the many specific classes of data in the `sf` package. From above, you can see
the geometry column has two classes: 1st the specific geometry type (e.g.
`sfc_MULTIPOLYGON` `sfc_LINESTRING`, `sfc_POLYGON`, `sfc_MULTIPOINT`) and 2nd
the general sfc class. Skimr will try to find a sfl() helper function for the
classes in the order they appear in `class(.)` (see S3 classes for more detail
[*Advanced R*](https://adv-r.hadley.nz/s3.html)). The following example will
build support for `sfc`, which encompasses all `sf` objects: `sfc_MULTIPOLYGON`
`sfc_LINESTRING`, `sfc_POLYGON`, `sfc_MULTIPOINT`. If we want custom skim_with
functions we can write `sfl()` helper functions for the geometry type.
```{r}
skim_sf <- skim_with(
sfc = sfl(
n_unique = n_unique,
valid = ~ sum(sf::st_is_valid(.)),
crs = get_crs
)
)
```
The example above creates a new *function*, and you can call that function on
a specific column with `sfc` data to get the appropriate summary
statistics. The `skim_with` factory also uses the default skimrs for things
like factors, characters, and numerics. Therefore our `skim_sf` is like the regular
`skim` function with the added ability to summarize `sfc` columns.
```{r}
skim_sf(nc$geometry)
```
While this works for any data type and you can also include it within any
package (assuming your users load skimr), there is an even better approach in
this case. To take full advantage of `skimr`, we'll dig a bit into its API.
## Adding new methods
`skimr` has a lookup mechanism, based on the function `get_skimmers()`, to
find default summary functions for each class. This is based on the S3 class
system. You can learn more about it in
[*Advanced R*](https://adv-r.hadley.nz/s3.html).
This requires that you add `skimr` to your list of dependencies.
To export a new set of defaults for a data type, create a method for the generic
function `get_skimmers`. Each of those methods returns an `sfl`, a `skimr`
function list. This is the same list-like data structure used in the
`skim_with()` example above. But note! There is one key difference. When adding
a generic we also want to identify the `skim_type` in the `sfl`. You will
probably want to use `skimr::get_skimmers.sfc()` but that will not work in a
vignette.
```{r}
#' @importFrom skimr get_skimmers
#' @export
get_skimmers.sfc <- function(column) {
sfl(
skim_type = "sfc",
n_unique = n_unique,
valid = ~ sum(sf::st_is_valid(.)),
crs = get_crs
)
}
```
The same strategy follows for other data types.
* Create a method
* return an `sfl`
* make sure that the `skim_type` is there
Users of your package should load `skimr` to get the `skim()` function
(although you could import and reexport it). Once
loaded, a call to `get_default_skimmer_names()` will return defaults for your
data types as well!
```{r}
get_default_skimmer_names()
```
They will then be able to use `skim()` directly.
```{r}
skim(nc)
```
## Conclusion
This is a very simple example. For a package such as `sf` the custom statistics
will likely be much more complex. The flexibility of `skimr` allows you to
manage that.
Thanks to Jakub Nowosad, Tiernan Martin, Edzer Pebesma, Michael Sumner, and
Kyle Butts for inspiring and helping with the development of this code.