Our article "Let’s Have Some Sympathy For The Part-time R User" includes two points:

The first point feels abstract, until you find yourself wanting to re-use code on new projects. As for the second point: I feel the wrapr package is the easiest, safest, most consistent, and most legible way to achieve maintainable code re-use in R.

In this article we will show how wrapr makes code-rewriting even easier with its new let x=x automation.

Let X=X

There are very important reasons to choose a package that makes things easier. One is debugging:

Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?

Brian Kernighan, The Elements of Programming Style, 2nd edition, chapter 2

Let's take the monster example from "Let’s Have Some Sympathy For The Part-time R User".

The idea was that perhaps one had worked out a complicated (but useful and important) by-hand survey scoring method:

suppressPackageStartupMessages(library("dplyr"))
library("wrapr")

d <- data.frame(
  subjectID = c(1,                   
                1,
                2,                   
                2),
  surveyCategory = c(
    'withdrawal behavior',
    'positive re-framing',
    'withdrawal behavior',
    'positive re-framing'
  ),
  assessmentTotal = c(5,                 
                      2,
                      3,                  
                      4),
  stringsAsFactors = FALSE
)

scale <- 0.237

d %>%
  group_by(subjectID) %>%
  mutate(probability =
           exp(assessmentTotal * scale)/
           sum(exp(assessmentTotal * scale))) %>%
  arrange(probability, surveyCategory) %>%
  mutate(isDiagnosis = row_number() == n()) %>%
  filter(isDiagnosis) %>%
  ungroup() %>%
  select(subjectID, surveyCategory, probability) %>%
  rename(diagnosis = surveyCategory) %>%
  arrange(subjectID)

The presumption is that the above pipeline is considered reasonable (but long, complicated, and valuable) dplyr, and our goal is to re-use it on new data that may not have the same column names as our original data.

We are making the huge simplifying assumption that you have studied the article and the above example is now familiar.

The question is: what to do when one wants to process the same type of data with different column names? For example:

d <- data.frame(
  PID = c(1,                   
          1,
          2,                   
          2),
  DIAG = c(
    'withdrawal behavior',
    'positive re-framing',
    'withdrawal behavior',
    'positive re-framing'
  ),
  AT = c(5,                 
         2,
         3,                  
         4),
  stringsAsFactors = FALSE
)

print(d)

The new table has the following new column definitions:

subjectID       <- "PID"
surveyCategory  <- "DIAG"
assessmentTotal <- "AT"
isDiagnosis     <- "isD"
probability     <- "prob"
diagnosis       <- "label"

We could "reduce to a previously solved problem" by renaming the columns to names we know, doing the work, and then renaming back (which is actually a service that replyr::replyr_apply_f_mapped() supplies).

In "Let’s Have Some Sympathy For The Part-time R User" I advised editing the pipeline to have obvious stand-in names (perhaps in all-capitals) and then using wrapr::let() to perform symbol substitution on the pipeline.

Dr. Nina Zumel has since pointed out to me: if you truly trust the substitution method you can use the original column names and adapt the original calculation pipeline as is (without alteration). Let's try that:

let(
  c(subjectID = subjectID,
    surveyCategory = surveyCategory, 
    assessmentTotal = assessmentTotal,
    isDiagnosis = isDiagnosis,
    probability = probability,
    diagnosis = diagnosis),
  d %>%
    group_by(subjectID) %>%
    mutate(probability =
             exp(assessmentTotal * scale)/
             sum(exp(assessmentTotal * scale))) %>%
    arrange(probability, surveyCategory) %>%
    mutate(isDiagnosis = row_number() == n()) %>%
    filter(isDiagnosis) %>%
    ungroup() %>%
    select(subjectID, surveyCategory, probability) %>%
    rename(diagnosis = surveyCategory) %>%
    arrange(subjectID))

That works! All we did was: paste the original code into the block and the adapter did all of the work, with no user edits of the code.

It is a bit harder for the user to find which symbols are being replaced, but in some sense they don't really need to know (it is R's job to perform the replacements).

wrapr has a new helper function mapsyms() that automates all of the "let x = x" steps from the above example.

mapsyms() is a simple function that captures variable names and builds a mapping from them to the names they refer to in the current environment. For example we can use it to quickly build the assignment map for the let block, because the earlier assignments such as "subjectID <- "PID"" allow mapsyms() to find the intended re-mappings. This would also be true for other cases, such as re-mapping function arguments to values. Our example becomes:

print(mapsyms(subjectID,
              surveyCategory, 
              assessmentTotal,
              isDiagnosis,
              probability,
              diagnosis))

This allows the solution to be re-written and even wrapped into a function in a very legible form with very little effort:

computeRes <- function(d,
                       subjectID,
                       surveyCategory, 
                       assessmentTotal,
                       isDiagnosis,
                       probability,
                       diagnosis) {
  let(
    mapsyms(subjectID,
            surveyCategory, 
            assessmentTotal,
            isDiagnosis,
            probability,
            diagnosis),
    d %>%
      group_by(subjectID) %>%
      mutate(probability =
               exp(assessmentTotal * scale)/
               sum(exp(assessmentTotal * scale))) %>%
      arrange(probability, surveyCategory) %>%
      mutate(isDiagnosis = row_number() == n()) %>%
      filter(isDiagnosis) %>%
      ungroup() %>%
      select(subjectID, surveyCategory, probability) %>%
      rename(diagnosis = surveyCategory) %>%
      arrange(subjectID)
  )
}

computeRes(d,
           subjectID       = "PID",
           surveyCategory  = "DIAG",
           assessmentTotal = "AT",
           isDiagnosis     = "isD",
           probability     = "prob",
           diagnosis       = "label")

The idea is: instead of having to mark what instances of symbols are to be replaced (by quoting or de-quoting indicators), we instead declare what symbols are to be replaced using the mapsyms() helper.

mapsyms() is a stand-alone helper function (just as ":=" is). It works not because it is some exceptional corner-case hard-wired into other functions, but because mapsyms()'s reasonable semantics happen to synergize with let()'s reasonable semantics. mapsyms() behaves as a replacement target controller (without needing any cumbersome direct quoting or un-quoting notation!).



WinVector/wrapr documentation built on Aug. 29, 2023, 4:51 a.m.