--- title: "Filtering in relational data models" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Technical: Filtering in relational data models} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: console author: Katharina Brunner --- ``````{r setup, include = FALSE} source("setup/setup.R") `````` The {dm} package offers functions to work with relational data models in R. This document introduces you to filtering functions, and shows how to apply them to the data that is separated into multiple tables. Our example data is drawn from the [{nycflights13}](https://github.com/tidyverse/nycflights13) package that contains five inter-linked tables. First, we will load the packages that we need: ```{r message=FALSE, warning=FALSE} library(nycflights13) library(dm) ``` ## Data: nycflights13 To explore filtering with {dm}, we'll use the {nycflights13} data with its `flights`, `planes`, `airlines`, `airports` and `weather` tables. This dataset contains information about the 336 776 flights that departed from New York City in 2013, with 3322 different planes and 1458 airports involved. The data comes from the US Bureau of Transportation Statistics, and is documented in `?nycflights13::flights`. To start with our exploration, we have to create a `dm` object from the {nycflights13} data. The built-in `dm::dm_nycflights13()` function takes care of this. By default it only uses a subset of the complete data though: only the flights on the 10th of each month are considered, reducing the number of rows in the `flights` table to 11 227. A [data model object](https://dm.cynkra.com/articles/tech-dm-class.html) contains data from the source tables, and metadata about the tables. If you would like to create a `dm` object from tables other than the example data, you can use the `new_dm()`, `dm()` or `as_dm()` functions. See `vignette("howto-dm-df")` for details. ```{r} dm <- dm_nycflights13() ``` The console output of the 'dm` object shows its data and metadata, and is colored for clarity: ```{r} dm ``` Now we know that there are five tables in our `dm` object. But how are they connected? These relations are best displayed as a visualization of the entity-relationship model: ```{r} dm_draw(dm) ``` You can look at a single table with `tbl`. To print the `airports` table, call ```{r} tbl(dm, "airports") ``` ## Filtering a `dm` object {#filter} `dm_filter()` allows you to select a subset of a `dm` object. ### How it works Filtering a `dm` object is not that different from filtering a dataframe or tibble with `dplyr::filter()`. The corresponding {dm} function is `dm::dm_filter()`. With this function one or more filtering conditions can be set for one of the tables of the `dm` object. These conditions are immediately evaluated for their respective tables and for all related tables. For each resulting table, all related tables (directly or indirectly) with a filter condition them are taken into account in the following way: - filtering semi-joins are successively performed along the paths from each of the filtered tables to the requested table, each join reducing the left-hand side tables of the joins to only those of their rows with key values that have corresponding values in key columns of the right-hand side tables of the join. - eventually the requested table is returned, containing only the the remaining rows after the filtering joins Currently, this only works if the graph induced by the foreign key relations is cycle free. Fortunately, this is the default for `dm_nycflights13()`. ### Filtering Examples Let's see filtering in action: **We only want the data that is related to John F. Kennedy International Airport.** ```{r} filtered_dm <- dm %>% dm_filter(airports = (name == "John F Kennedy Intl")) filtered_dm ``` You can get the numbers of rows of each table with `dm_nrow()`. ```{r} rows_per_table <- filtered_dm %>% dm_nrow() rows_per_table sum(rows_per_table) ``` ```{r echo = FALSE, eval = TRUE} sum_nrow <- NA sum_nrow_filtered <- NA ``` ```{r} sum_nrow <- sum(dm_nrow(dm)) sum_nrow_filtered <- sum(dm_nrow(dm_apply_filters(filtered_dm))) ``` The total number of rows in the `dm` drops from `r format(sum_nrow, big.mark = " ")` to `r format(sum_nrow_filtered, big.mark = " ")` (the only unaffected table is the disconnected `weather` table). Next example: **Get a `dm` object containing data for flights from New York to the Dulles International Airport in Washington D.C., abbreviated with `IAD`.** ```{r} dm %>% dm_filter(flights = (dest == "IAD")) %>% dm_nrow() ``` Applying multiple filters to different tables is also supported. An example: **Get all January 2013 flights from Delta Air Lines which didn't depart from John F. Kennedy International Airport.** ```{r} dm_delta_may <- dm %>% dm_filter( airlines = (name == "Delta Air Lines Inc."), airports = (name != "John F Kennedy Intl"), flights = (month == 1) ) dm_delta_may dm_delta_may %>% dm_nrow() ``` You can inspect the filtered tables by subsetting them. In the `airlines` table, Delta is the only remaining carrier: ```{r} dm_delta_may$airlines ``` Which planes were used to service these flights? ```{r} dm_delta_may$planes ``` And indeed, all included flights departed in January (`month == 1`): ```{r} dm_delta_may$flights %>% dplyr::count(month) ``` For comparison, let's review the equivalent manual query for `flights` in `dplyr` syntax: ```{r} airlines_filtered <- filter(airlines, name == "Delta Air Lines Inc.") airports_filtered <- filter(airports, name != "John F Kennedy Intl") flights %>% semi_join(airlines_filtered, by = "carrier") %>% semi_join(airports_filtered, by = c("origin" = "faa")) %>% filter(month == 5) ``` The {dm} code is leaner because the foreign key information is encoded in the object. ## SQL statements behind filtering a `dm` object on a database {dm} is meant to work with relational data models, locally as well as on databases. In your project, the data is probably not stored locally but in a remote [relational database](https://dm.cynkra.com/articles/howto-dm-theory.html) that can be queried with SQL statements. You can check the queries by using `sql_render()` from the [{dbplyr}](https://dbplyr.tidyverse.org/) package. Example: **Print the SQL statements for getting all January 2013 flights from Delta Air Lines, which did not depart from John F. Kennedy International Airport, with the data stored in a sqlite database.** To show the SQL query behind a `dm_filter()`, we copy the `flights`, `airlines` and `airports` tables from the `nyflights13` dataset to a temporary in-memory database using the built-in function `copy_dm_to()` and `dbplyr::src_memdb`. Then we filter the data, and print the corresponding SQL statement with `dbplyr::sql_render()`. ```{r, warning=FALSE} dm %>% dm_select_tbl(flights, airlines, airports) %>% copy_dm_to(dbplyr::src_memdb(), .) %>% dm_filter( airlines = (name == "Delta Air Lines Inc."), airports = (name != "John F Kennedy Intl"), flights = (month == 1) ) %>% dm_get_tables() %>% purrr::map(dbplyr::sql_render) ``` Further reading: {dm}'s function for copying data [from and to databases](https://dm.cynkra.com/articles/dm.html).