--- title: "Joins" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Joins} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` Metadata often lives in more than one table. tidymatrix supports all six dplyr joins on the active metadata. Joins that only *add columns* leave the matrix alone; joins that *add or remove rows* of the metadata add or remove the corresponding rows (or columns) of the matrix. ```{r load-packages} library(tidymatrix) library(dplyr, warn.conflicts = FALSE) tm <- tidymatrix(big5_responses, big5_respondents, big5_items) |> activate(rows) |> filter(completion_min > 3.5) ``` We use the `big5` personality survey (see `?big5`), without the careless respondents. Respondents come from six countries: ```{r countries-in-data} tm |> activate(rows) |> pull(country) |> table() ``` The `big5_countries` table has extra information about the countries. It is deliberately imperfect: Germany (`DE`) is missing and Norway (`NO`) has no respondents. ```{r countries} big5_countries ``` ## Mutating joins on rows ### Left join: add columns, keep every respondent `left_join()` is the usual way to add annotations. Every respondent is kept; the ones without a match (Germany) get `NA`. ```{r left-join} tm_left <- tm |> activate(rows) |> left_join(big5_countries, by = "country") tm_left tm_left |> activate(rows) |> filter(is.na(country_name)) |> select(respondent_id, country, country_name) identical(dim(tm_left$matrix), dim(tm$matrix)) ``` ### Inner join: keep only respondents with a match `inner_join()` drops the German respondents from the metadata *and* from the matrix. ```{r inner-join} tm_inner <- tm |> activate(rows) |> inner_join(big5_countries, by = "country") nrow(tm$matrix) nrow(tm_inner$matrix) ``` Now we can, for example, compare Baltic and Nordic respondents: ```{r inner-join-use} tm_inner |> activate(rows) |> count(region) ``` ### Right and full joins: new rows get `NA` in the matrix `right_join()` keeps every row of the *external* table, and `full_join()` keeps every row of both. A row that exists only in the external table — Norway here — has no answers, so its row of the matrix is filled with `NA`. ```{r full-join} tm_full <- tm |> activate(rows) |> full_join(big5_countries, by = "country") nrow(tm_full$matrix) tm_full |> activate(rows) |> filter(is.na(respondent_id)) |> select(respondent_id, country, country_name) tail(tm_full$matrix[, 1:6], 3) ``` These joins are rarely what you want for annotations, but they can be useful to line up a matrix against a fixed list of expected rows or columns. ## Filtering joins `semi_join()` and `anti_join()` filter the metadata by whether a match exists, without adding any columns. ```{r semi-anti} # respondents from countries in the table tm_semi <- tm |> activate(rows) |> semi_join(big5_countries, by = "country") nrow(tm_semi$matrix) ncol(tm_semi$row_data) # no new columns # respondents from countries missing from the table tm |> activate(rows) |> anti_join(big5_countries, by = "country") |> select(respondent_id, country, age, gender) ``` `anti_join()` is a handy check before a `left_join()`: it shows exactly which rows will end up with missing annotations. ## Joins on columns Everything above works for the column metadata too. Here is a table describing the five traits: ```{r trait-table} trait_info <- data.frame( trait = c("Extraversion", "Agreeableness", "Conscientiousness", "Neuroticism", "Openness"), abbreviation = c("E", "A", "C", "N", "O"), high_pole = c("outgoing, energetic", "friendly, compassionate", "organised, dependable", "anxious, moody", "curious, imaginative"), low_pole = c("reserved, quiet", "critical, detached", "careless, spontaneous", "calm, stable", "conventional, practical") ) tm_items <- tm |> activate(columns) |> left_join(trait_info, by = "trait") tm_items |> activate(columns) |> select(item_id, trait, abbreviation, high_pole) ``` A filtering join is a convenient way to select a predefined subset of columns. Suppose a short form of the questionnaire uses only two items per trait: ```{r short-form} short_form <- data.frame( item_id = c("E1", "E5", "A1", "A5", "C1", "C5", "N1", "N5", "O1", "O5") ) tm_short <- tm |> activate(columns) |> semi_join(short_form, by = "item_id") colnames(tm_short$matrix) ``` Note that the matrix columns keep their original order, not the order of `short_form`. Use `arrange()` afterwards if the order matters. ## Joining on differently named keys `by` accepts everything `dplyr` does, including `join_by()` and named vectors for keys with different names: ```{r different-keys} country_codes <- data.frame( iso2 = c("EE", "FI", "LV", "LT", "SE", "DE"), eu_member_since = c(2004, 1995, 2004, 2004, 1995, 1958) ) tm |> activate(rows) |> left_join(country_codes, by = join_by(country == iso2)) |> select(respondent_id, country, eu_member_since) ``` ## Duplicate keys If the external table has several rows for the same key, a mutating join duplicates the matching metadata rows — and the matrix rows with them. This is the same behaviour as in dplyr, and it is almost always a mistake when annotating a matrix. Check that the key is unique first: ```{r unique-keys} anyDuplicated(big5_countries$country) == 0 ``` ## Joins and stored analyses Stored analysis objects (PCA, clustering, ...) are removed after a join, because a join may change which rows or columns are present. The metadata columns an analysis created are kept. If you need both, join first and analyse afterwards: ```{r join-then-analyse} tm_analysed <- tm |> activate(rows) |> left_join(big5_countries, by = "country") |> activate(columns) |> compute_hclust(k = 5, method = "ward.D2") list_analyses(tm_analysed) ``` ## See also * [Working with rows and columns](dplyr-verbs.html) for the other dplyr verbs. * [Getting started](basic-usage.html) for an overview of the package.