A dashboard shows a number. The viewer thinks: “Why is this
number smaller than I expected?” whence gives the
answer with one click. It shows how many rows each step of the pipeline
removed, and the code that makes the number again.
Install
whence is not on CRAN yet. Install it from GitHub:
# install.packages("pak")
pak::pak("tenmeh/whence")In a Shiny app
Replace textOutput() and renderText() with
whenceOutput() and renderWhence(). Give the
render a pipeline that ends in one value. You do not need to change the
pipeline.
library(shiny)
library(dplyr)
library(whence)
ui <- fluidPage(
selectInput("cyl", "Cylinders", c(4, 6, 8)),
whenceOutput("mean_mpg", label = "Mean mpg")
)
server <- function(input, output, session) {
output$mean_mpg <- renderWhence(
mtcars |>
filter(cyl == input$cyl) |>
filter(hp < 150) |>
summarise(mpg = round(mean(mpg), 1))
)
}
shinyApp(ui, server)The value has a dotted underline. A click on it, or Enter or Space when it has the focus, opens a panel next to it. The panel holds:
- Where the rows went. The source, then each step with the rows after it and the rows that it removed.
-
Reproduce. The pipeline as code, with each
input$namereplaced by its current value. The viewer can copy it and run it. - View the rows. The rows of the source that make the value. This is the only part that asks the server for something.
The live demo shows all of this in your browser.
Show a value as you want
format in renderWhence() is a function that
turns the value into the text to show. A formatter of ‘scales’
works:
output$revenue <- renderWhence(
sales |> filter(region == input$region) |> summarise(total = sum(revenue)),
format = scales::label_dollar()
)So does a function of your own, such as
function(x) paste0(round(x), " kg"). It runs on the server,
so it can read inputs, for example to choose a currency. With no
format, a number gets a comma for each thousand.
With no Shiny
The engine works in the console, which makes it easy to test.
whence_trace() takes the same pipeline:
library(dplyr, warn.conflicts = FALSE)
library(whence)
trace <- whence_trace(
mtcars |>
filter(cyl == 4) |>
filter(hp < 90) |>
summarise(mpg = round(mean(mpg), 1))
)
trace
#> <whence_trace> 29.7
#>
#> Where the rows went
#> 0 mtcars 32
#> 1 -> cyl == 4 11 -21 rows (66%)
#> 2 -> hp < 90 5 -6 rows (55%)
#> 3 -> aggregated to 1 row 1
#>
#> Reproduce
#> mtcars |>
#> filter(cyl == 4) |>
#> filter(hp < 90) |>
#> summarise(mpg = round(mean(mpg), 1))One row for each step:
as.data.frame(trace)
#> step label n delta percent kind verb groups shown
#> 1 0 mtcars 32 NA NA source <NA> NA TRUE
#> 2 1 cyl == 4 11 -21 -65.62500 filter filter NA TRUE
#> 3 2 hp < 90 5 -6 -54.54545 filter filter NA TRUE
#> 4 3 aggregated to 1 row 1 NA NA summarise summarise NA TRUEThe value, the code, and the rows that make the value:
whence_value(trace)
#> [1] 29.7
cat(whence_code(trace))
#> mtcars |>
#> filter(cyl == 4) |>
#> filter(hp < 90) |>
#> summarise(mpg = round(mean(mpg), 1))
whence_rows(trace)
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
#> Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
#> Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
#> Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
#> Fiat X1-9 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1Which rows did a step remove?
Each count of removed rows in the waterfall has its percentage, and
in the panel it is a button that shows those rows. In the console, give
the step to whence_rows(). print() shows the
number of each step:
whence_rows(trace, step = 2)
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
#> Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
#> Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1
#> Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.70 0 1 5 2
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2
#> Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2Tables
A pipeline that ends in a summary gives a table.
whenceTableOutput() and renderWhenceTable()
show it, and the first cell of each row opens the panel of that row: the
rows of its group at each step, the code of the row, and its rows.
ui <- fluidPage(whenceTableOutput("by_cyl", label = "Cars by cylinders"))
server <- function(input, output, session) {
output$by_cyl <- renderWhenceTable(
mtcars |>
filter(hp > 90) |>
group_by(cyl) |>
summarise(cars = n(), mpg = mean(mpg)),
format = list(mpg = function(x) format(round(x, 1)))
)
}In the console, whence_table() traces the table, and
whence_group() explains one row:
by_cyl <- whence_table(
mtcars |>
filter(hp > 90) |>
group_by(cyl) |>
summarise(cars = n(), mpg = mean(mpg)) |>
mutate(share = cars / sum(cars))
)
six <- whence_group(by_cyl, cyl = 6)
six
#> <whence_group> cyl = 6
#>
#> Where the rows went
#> 0 mtcars 7
#> 1 -> hp > 90 7
#> 2 -> grouped by cyl 7
#> 3 -> summarised into this row 1
#> 4 -> mutate(share = cars/sum(cars)) 1
#>
#> Reproduce
#> mtcars |>
#> filter(hp > 90) |>
#> group_by(cyl) |>
#> summarise(cars = n(), mpg = mean(mpg)) |>
#> mutate(share = cars/sum(cars)) |>
#> filter(cyl == 6)
whence_rows(six, step = 1)
#> [1] mpg cyl disp hp drat wt qsec vs am gear carb
#> <0 rows> (or 0-length row.names)A row can be found by its number, as in
whence_group(by_cyl, 2), or by its group values, as
above.
The code of the row keeps share right: it is the code of
the table, and a last filter() that keeps the row.
Plots
whencePlotOutput() and renderWhencePlot()
draw a ‘ggplot2’ plot of a summary. A click on a bar or a point opens
the panel of its row. The x aesthetic can be computed from
the columns of the summary, as in
aes(reorder(cyl, mpg), mpg). Under the plot, “Show the
data” gives the table, so a keyboard user can open each panel too.
Follow the viewer
whence_opened() gives a reactive with the panel that the
viewer has open. While a row of a table is open, it gives the number of
the row and the row itself, so the app can show more about it:
server <- function(input, output, session) {
output$by_cyl <- renderWhenceTable(
mtcars |> group_by(cyl) |> summarise(cars = n())
)
opened <- whence_opened("by_cyl")
output$detail <- renderText({
req(opened())
paste("Looking at the cars with", opened()$data$cyl, "cylinders.")
})
}Summaries are not attrition
When summarise() turns 1,204 rows into one, the panel
says “aggregated to 1 row”. It does not say “-1,203 rows”, because no
rows were lost. After a group_by() it says “grouped into 14
rows”. A group_by() is shown only when it changes the
groups.
whence_trace(
mtcars |>
group_by(cyl) |>
summarise(mpg = mean(mpg)) |>
filter(cyl == 6) |>
pull(mpg)
)
#> <whence_trace> 19.74286
#>
#> Where the rows went
#> 0 mtcars 32
#> 1 -> grouped by cyl 32
#> 2 -> grouped into 3 rows 3
#> 3 -> cyl == 6 1 -2 rows (67%)
#>
#> Reproduce
#> mtcars |>
#> group_by(cyl) |>
#> summarise(mpg = mean(mpg)) |>
#> filter(cyl == 6) |>
#> pull(mpg)When a pipeline is not traced
whence knows the verbs of ‘dplyr’ and two pivots of ‘tidyr’ (the list
is on the help page of whence_trace()). Any other call ends
the chain, and the trace says where:
cylinders <- data.frame(cyl = c(4, 6, 8), engine = c("small", "mid", "big"))
whence_trace(
mtcars |>
filter(hp > 100) |>
inner_join(cylinders, by = "cyl") |>
count()
)
#> <whence_trace> 23
#>
#> Where the rows went
#> 0 inner_join(filter(mtcars, hp > 100), cylinders, by = "cyl") 23
#> 1 -> aggregated to 1 row 1
#> Steps before inner_join() are not shown, because whence does not know inner_join().
#>
#> Reproduce
#> inner_join(filter(mtcars, hp > 100), cylinders, by = "cyl") |>
#> count()When the rows cannot be found
The waterfall needs only counts. To find which rows make the
value, whence_rows() runs the steps again with a hidden id
column. Some steps break the link between a row and the source. After
distinct(), a pivot of ‘tidyr’, or a step that is not a
known verb, whence_rows() gives NULL with a
warning that names the step. The waterfall still works.
trace <- whence_trace(
mtcars |>
filter(hp > 100) |>
distinct(cyl) |>
count()
)
rows <- whence_rows(trace)
#> Warning: Row attribution unavailable past distinct().Limits
-
Database-backed pipelines are not supported. With
‘dbplyr’ the steps run as SQL only at
collect(), so there are no counts for each step.whencestops with an error, and shows no waterfall that would mislead. - Joins are not traced. A join ends the chain: the join and everything upstream of it become the source.
- Window functions are counted correctly, but their row attribution has no clear meaning.
- The verbs of ‘data.table’, ‘arrow’ and ‘duckplyr’ are not known.
- A table or a plot is explained row by row when it is the result of a summary step that keeps its group columns. A plot must be a ‘ggplot2’ plot of that table.
