Author: John Mount, Win-Vector LLC
Date: 2023-08-19

# Assignment Partitioner

#### John Mount, Win-Vector LLC

#### 2023-08-19

`rquery::extend_se()` and `rquery::extend()` each automatically partition a sequence of assignments so that no statement is using any value created in the same partition element or group. This is to eliminate potentially dangerous ambiguity in statements.

For such a partition: the evaluation result does not depend on the order of execution of the statements in each group (as they are all independent of each other’s left-hand-sides). A no-dependency small number of groups partition is very helpful when executing expressions on `SQL` based data interfaces (such as `Apache Spark`).

The method used to partition expressions is to scan the remaining expressions in order taking any that: have all their values available from earlier groups, do not use a value formed in the current group, and do not overwrite a value formed in the current group.

This partitioning method ensures safe and correct results, and can lead to far fewer groups (and much more efficient queries) than the straightforward method of breaking up the sequence of expressions at each new-value use.

Here is a non-trivial database based example (demonstrating methods that might be used in big data work such as with `Spark`). We are going to assign pairs of items to complimentary treatment (“T”) and control (“C”) groups based on already populated pseudo-random numbers (pre-populating the pseudo-random numbers is to avoid known issues in using `rand()` in `RSQlite`, and would not be necessary with other databases).

    run_vignette <- requireNamespace("DBI", quietly = TRUE) && requireNamespace("RSQLite", quietly = TRUE)

    library("rquery")

    Loading required package: wrapr

    # example data
    set.seed(3463)
    d <- data.frame(id = seq_len(4))
    for(group in c('a', 'b', 'c', 'd', 'e')) {
      d[[paste0("rand_", group)]] <- runif(nrow(d))
    }
    my_db <- DBI::dbConnect(RSQLite::SQLite(),
                            ":memory:")
    d1 <- rq_copy_to(my_db, "example_table", d)
    d1 %.>%
      to_sql(., my_db) %.>%
      DBI::dbGetQuery(my_db, .) %.>%
      knitr::kable(.)

id | rand_a | rand_b | rand_c | rand_d | rand_e
--- | --- | --- | --- | --- | ---
1 | 0.8438177 | 0.9459773 | 0.2941489 | 0.1054046 | 0.3038159
2 | 0.9045364 | 0.4839231 | 0.4654982 | 0.6617276 | 0.9056346
3 | 0.5496617 | 0.6112306 | 0.6989960 | 0.6536909 | 0.1683751
4 | 0.6545816 | 0.6593733 | 0.9678277 | 0.8316179 | 0.0597492

    # design the experiment
    plan <- d1 %.>%
      extend(.,
       choice_a = rand_a>=0.5,
        a_1 = ifelse(choice_a,
                      'T',
                      'C'),
        a_2 = ifelse(choice_a,
                      'C',
                      'T'),
       choice_b = rand_b>=0.5,
        b_1 = ifelse(choice_b,
                      'T',
                      'C'),
        b_2 = ifelse(choice_b,
                      'C',
                      'T'),
       choice_c = rand_c>=0.5,
        c_1 = ifelse(choice_c,
                      'T',
                      'C'),
        c_2 = ifelse(choice_c,
                      'C',
                      'T'),
       choice_d = rand_d>=0.5,
        d_1 = ifelse(choice_d,
                      'T',
                      'C'),
        d_2 = ifelse(choice_d,
                      'C',
                      'T'),
       choice_e = rand_e>=0.5,
        e_1 = ifelse(choice_e,
                      'T',
                      'C'),
        e_2 = ifelse(choice_e,
                      'C',
                      'T')
      ) %.>%
      select_columns(.,
                     qc(id,
                        a_1, a_2, b_1, b_2,
                        c_1, c_2, d_1, d_2,
                        e_1, e_2))

    cat(format(plan))

    mk_td("example_table", c(
      "id",
      "rand_a",
      "rand_b",
      "rand_c",
      "rand_d",
      "rand_e")) %.>%
     extend(.,
      choice_a := rand_a >= 0.5,
      choice_b := rand_b >= 0.5,
      choice_c := rand_c >= 0.5,
      choice_d := rand_d >= 0.5,
      choice_e := rand_e >= 0.5) %.>%
     extend(.,
      a_1 := ifelse(choice_a, "T", "C"),
      a_2 := ifelse(choice_a, "C", "T"),
      b_1 := ifelse(choice_b, "T", "C"),
      b_2 := ifelse(choice_b, "C", "T"),
      c_1 := ifelse(choice_c, "T", "C"),
      c_2 := ifelse(choice_c, "C", "T"),
      d_1 := ifelse(choice_d, "T", "C"),
      d_2 := ifelse(choice_d, "C", "T"),
      e_1 := ifelse(choice_e, "T", "C"),
      e_2 := ifelse(choice_e, "C", "T")) %.>%
     select_columns(.,
        c('id', 'a_1', 'a_2', 'b_1', 'b_2', 'c_1', 'c_2', 'd_1', 'd_2', 'e_1', 'e_2'))

Notice `rquery::extend_se()` split the work into 3 unambiguous groups. The statements inside each group can now be executed in any order (or even in parallel) with no ambiguity of meaning or risk of error. The goal was: split into a small number of groups such that the observable execution semantics are very close to executing the original statements in order in completely separate groups (which is likely what a user intends).

    sql <- to_sql(plan, my_db)
    cat(sql)

    SELECT
     `id`,
     `a_1`,
     `a_2`,
     `b_1`,
     `b_2`,
     `c_1`,
     `c_2`,
     `d_1`,
     `d_2`,
     `e_1`,
     `e_2`
    FROM (
     SELECT
      `id`,
      ( CASE WHEN ( `choice_a` ) THEN ( 'T' ) WHEN NOT ( `choice_a` ) THEN ( 'C' ) ELSE NULL END )  AS `a_1`,
      ( CASE WHEN ( `choice_a` ) THEN ( 'C' ) WHEN NOT ( `choice_a` ) THEN ( 'T' ) ELSE NULL END )  AS `a_2`,
      ( CASE WHEN ( `choice_b` ) THEN ( 'T' ) WHEN NOT ( `choice_b` ) THEN ( 'C' ) ELSE NULL END )  AS `b_1`,
      ( CASE WHEN ( `choice_b` ) THEN ( 'C' ) WHEN NOT ( `choice_b` ) THEN ( 'T' ) ELSE NULL END )  AS `b_2`,
      ( CASE WHEN ( `choice_c` ) THEN ( 'T' ) WHEN NOT ( `choice_c` ) THEN ( 'C' ) ELSE NULL END )  AS `c_1`,
      ( CASE WHEN ( `choice_c` ) THEN ( 'C' ) WHEN NOT ( `choice_c` ) THEN ( 'T' ) ELSE NULL END )  AS `c_2`,
      ( CASE WHEN ( `choice_d` ) THEN ( 'T' ) WHEN NOT ( `choice_d` ) THEN ( 'C' ) ELSE NULL END )  AS `d_1`,
      ( CASE WHEN ( `choice_d` ) THEN ( 'C' ) WHEN NOT ( `choice_d` ) THEN ( 'T' ) ELSE NULL END )  AS `d_2`,
      ( CASE WHEN ( `choice_e` ) THEN ( 'T' ) WHEN NOT ( `choice_e` ) THEN ( 'C' ) ELSE NULL END )  AS `e_1`,
      ( CASE WHEN ( `choice_e` ) THEN ( 'C' ) WHEN NOT ( `choice_e` ) THEN ( 'T' ) ELSE NULL END )  AS `e_2`
     FROM (
      SELECT
       `id`,
       `rand_a` >= 0.5  AS `choice_a`,
       `rand_b` >= 0.5  AS `choice_b`,
       `rand_c` >= 0.5  AS `choice_c`,
       `rand_d` >= 0.5  AS `choice_d`,
       `rand_e` >= 0.5  AS `choice_e`
      FROM (
       SELECT
        `id`,
        `rand_a`,
        `rand_b`,
        `rand_c`,
        `rand_d`,
        `rand_e`
       FROM
        `example_table`
       ) tsql_24400885758306713700_0000000000
      ) tsql_24400885758306713700_0000000001
    ) tsql_24400885758306713700_0000000002

    DBI::dbGetQuery(my_db, sql) %.>%
      knitr::kable(.)

id | a_1 | a_2 | b_1 | b_2 | c_1 | c_2 | d_1 | d_2 | e_1 | e_2
--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
1 | T | C | T | C | C | T | C | T | C | T
2 | T | C | C | T | C | T | T | C | T | C
3 | T | C | T | C | T | C | T | C | C | T
4 | T | C | T | C | T | C | T | C | C | T

A straightforward method (with no statement re-ordering) of splitting into non-dependent groups would have to split the mutate at each first use of a new value: yielding more mutate stages. For why a low number of execution stages is important please see here.

Note: re-using variable variable names does limit the planner’s ability to efficiently partition the the statement. The planner still emits safe and correct code, but unless it were to be allowed to introduce new variable names it must break sequences in more places. We show this effect below:

    plan2 <- d1 %.>%
      extend(.,
       choice = rand_a>=0.5,
        a_1 = ifelse(choice,
                      'T',
                      'C'),
        a_2 = ifelse(choice,
                      'C',
                      'T'),
       choice = rand_b>=0.5,
        b_1 = ifelse(choice,
                      'T',
                      'C'),
        b_2 = ifelse(choice,
                      'C',
                      'T'),
       choice = rand_c>=0.5,
        c_1 = ifelse(choice,
                      'T',
                      'C'),
        c_2 = ifelse(choice,
                      'C',
                      'T'),
       choice = rand_d>=0.5,
        d_1 = ifelse(choice,
                      'T',
                      'C'),
        d_2 = ifelse(choice,
                      'C',
                      'T'),
       choice = rand_e>=0.5,
        e_1 = ifelse(choice,
                      'T',
                      'C'),
        e_2 = ifelse(choice,
                      'C',
                      'T')
      ) %.>%
      select_columns(.,
                     qc(id,
                        a_1, a_2, b_1, b_2,
                        c_1, c_2, d_1, d_2,
                        e_1, e_2))

    cat(format(plan2))

    mk_td("example_table", c(
      "id",
      "rand_a",
      "rand_b",
      "rand_c",
      "rand_d",
      "rand_e")) %.>%
     extend(.,
      choice := rand_a >= 0.5) %.>%
     extend(.,
      a_1 := ifelse(choice, "T", "C"),
      a_2 := ifelse(choice, "C", "T")) %.>%
     extend(.,
      choice := rand_b >= 0.5) %.>%
     extend(.,
      b_1 := ifelse(choice, "T", "C"),
      b_2 := ifelse(choice, "C", "T")) %.>%
     extend(.,
      choice := rand_c >= 0.5) %.>%
     extend(.,
      c_1 := ifelse(choice, "T", "C"),
      c_2 := ifelse(choice, "C", "T")) %.>%
     extend(.,
      choice := rand_d >= 0.5) %.>%
     extend(.,
      d_1 := ifelse(choice, "T", "C"),
      d_2 := ifelse(choice, "C", "T")) %.>%
     extend(.,
      choice := rand_e >= 0.5) %.>%
     extend(.,
      e_1 := ifelse(choice, "T", "C"),
      e_2 := ifelse(choice, "C", "T")) %.>%
     select_columns(.,
        c('id', 'a_1', 'a_2', 'b_1', 'b_2', 'c_1', 'c_2', 'd_1', 'd_2', 'e_1', 'e_2'))

    sql2 <- to_sql(plan2, my_db)
    DBI::dbGetQuery(my_db, sql2) %.>%
      knitr::kable(.)

id | a_1 | a_2 | b_1 | b_2 | c_1 | c_2 | d_1 | d_2 | e_1 | e_2
--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
1 | T | C | T | C | C | T | C | T | C | T
2 | T | C | C | T | C | T | T | C | T | C
3 | T | C | T | C | T | C | T | C | C | T
4 | T | C | T | C | T | C | T | C | C | T

Notice the returned tables are identical (as they should be, which is not always the case for database backed `dplyr`).

    DBI::dbDisconnect(my_db)