Skip to contents
library(review)
library(dplyr)
#> Warning: package 'dplyr' was built under R version 4.5.3
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

The review package offers a dual extension of the tidyverse data model for iterative semantic review of tabular data. In the introductory vignette, The Review Algebra, we introduce the four verbs that support semantic review. The second vignette, From Review Algebra to Provenance Modelling, explained how review rounds can be interpreted as lightweight provenance activities.

This vignette shows how the review algebra integrates naturally into ordinary tidyverse workflows. The package extends tidy workflows with review states and provenance while preserving compatibility with familiar data-frame operations. The tidyverse provides an elegant relational algebra for transforming data frames. Following the principles of tidy data, observations, dimensions and attributes are represented explicitly, while many analytical assumptions, quality judgements and review decisions remain implicit in the analyst’s workflow. During interactive data analysis this is often exactly the right design.

However, once data leave the analyst’s workspace, those implicit decisions become difficult for others to inspect or reproduce. This is particularly important when review involves external collaborators, domain experts, statistical production pipelines, or released datasets that should satisfy the FAIR principles of reusability and reproducibility.

Several packages attempt to bridge this gap by serialising data together with metadata. In practice, however, separating data from their metadata often creates another maintenance problem: unless both are updated together, they gradually drift apart.

The review package takes a different approach. Rather than introducing a new data representation, it extends ordinary tidyverse data frames with a lightweight review layer that remains attached to the data throughout the workflow. Review rounds, reviewer annotations and provenance therefore travel with the data, while the object continues to behave like a familiar tidy data frame.

This design aims to bridge the gap between interactive data analysis and the exchange of reviewable, reproducible and trustworthy data without requiring users to abandon existing tidyverse workflows.

Review as a pause in a tidy workflow

Tidyverse pipelines usually move directly from one transformation to the next. In many real workflows, however, this is not enough. A value may need to be checked by a human reviewer, reconciled in another tool, or returned from an external process before the workflow can continue.

The review package introduces this missing review state.

<text, observation>
      ↓
     data
      ↓
  revisions()
      ↓
   review()
      ↓
< external review >
      ↓
  document()
      ↓
   approve()

review() does not perform the review itself. It allocates a review column and allows the workflow to pause.

claims <- revisions(
  Orange,
  scope_var = "age",
  subject_var = "Tree"
)

reviewed <- claims |>
  review(
    "circumference",
    review_id = "remeasure",
    label = "Check the circumference values."
  )

names(reviewed)
#> [1] "claim_id"                "age"                    
#> [3] "Tree"                    "circumference_candidate"
#> [5] "circumference_remeasure"

The reviewer may now edit the allocated review column. This can happen in many ways: in R (either via an interactive data entry on the console, or via a mutation of the assigned review column), in a spreadsheet, in OpenRefine, in Shiny, or in another external process.

Review with mutate

A review can be expressed as an ordinary mutate() step when the review decision is reproducible in code.

reviewed <- reviewed |>
  dplyr::mutate(
    circumference_remeasure =
      dplyr::if_else(
        claim_id == 1,
        31,
        circumference_remeasure
      )
  )

The review round can then be documented.

reviewed <- reviewed |>
  document(
    revision = "circumference_remeasure",
    activity = "remeasured",
    agent = utils::person("Jane", "Doe", role = "dtc"),
    comment = "First circumference value corrected in code."
  )

Review with an external file

A review may also leave R entirely. For example, the allocated review column can be written to a temporary CSV file, edited elsewhere, and read back into the same review object.

review_file <- tempfile(fileext = ".csv")

write.csv(
  reviewed[c("claim_id", "circumference_remeasure")],
  review_file,
  row.names = FALSE
)

In a real workflow, the CSV file could be reviewed in a spreadsheet or another curation tool. Here we simulate the edited file.

external_review <- read.csv(review_file)

# Modify the circumference
external_review$circumference_remeasure[2] <- 34
write.csv(external_review, review_file, row.names = FALSE)

# Re-read
external_review <- read.csv(review_file)

The reviewed values are then joined back by the claim identifier.

reviewed <- reviewed |>
  select(-circumference_remeasure) |>
  left_join(external_review, by = "claim_id") |>
  relocate(
    circumference_remeasure,
    .after = circumference_candidate
  )

The review can then be documented in exactly the same way.

reviewed <- reviewed |>
  document(
    revision = "circumference_remeasure",
    agent = person("Jane", "Doe", role = "rev"),
    used = review_file,
    comment = "Reviewed values were read back from a CSV file."
  )

Review in the R console

For small examples, review can also be interactive. A reviewer can inspect the object and overwrite one or more values directly.

reviewed$circumference_remeasure[3] <- 37

This is not less valid than a spreadsheet or a pipeline. What matters is that the revised value is stored in the allocated review column and the review is documented with document().

Scope of the review

The review algebra works best on single vectors. In this case, the review is a row-by-row revision of the validity of some claim on a variable. Technically, it would be possible to review several variables at one time, which would mean the revision of interactions.

data.frame(
  rowid = c(1, 2),
  title = c("Semantics", "Revisions"),
  author = c("Jane Doe", "Alice Cooper"),
  year = c("2026", "2021")
) %>%
  revisions(
    scope_var = "rowid",
    subject_var = "title"
  )
#>   claim_id rowid     title author_candidate year_candidate
#> 1        1     1 Semantics         Jane Doe           2026
#> 2        2     2 Revisions     Alice Cooper           2021
library(dplyr)
bibliography_df <- data.frame(
  rowid = c(1, 2),
  title = c("Semantics", "Revisions"),
  bibliography = c("Jane Doe 2026", "Alice Cooper 2021")
) |>
  revisions(scope_var = "rowid", subject_var = "title") |>
  review(
    review_var = "bibliography",
    review_id = "author_year",
    label = "Check the author and year of the articles"
  ) |>
  dplyr::mutate(
    bibliography_author_year = ifelse(
      test = bibliography_author_year == "Alice Cooper 2021",
      yes = "Alice Cooper 2022",
      no = bibliography_author_year
    )
  ) |>
  document(
    revision = "bibliography_author_year",
    agent = person("Librarian"),
    comment = "Alice Cooper seems to published only in 2022"
  )

bibliography_df
#>   claim_id rowid     title bibliography_candidate bibliography_author_year
#> 1        1     1 Semantics          Jane Doe 2026            Jane Doe 2026
#> 2        2     2 Revisions      Alice Cooper 2021        Alice Cooper 2022

Finalising the review

Once the reviewed values are approved, approve() promotes the current review values back into the candidate columns.

approved <- bibliography_df |>
  approve(agent = utils::person("Joe", "Doe", role = "rev"))

approved$bibliography_author_year
#> [1] "Jane Doe 2026"     "Alice Cooper 2022"

The review history is preserved.

names(approved)
#> [1] "claim_id"                 "rowid"                   
#> [3] "title"                    "bibliography_candidate"  
#> [5] "bibliography_author_year"
attr(approved, "prov_agent")
#> bibliography_author_year 
#>              "Librarian"
attr(approved, "prov_comment")
#>                       bibliography_author_year 
#> "Alice Cooper seems to published only in 2022"
attr(approved, "approval_agent")
#> [1] "Joe Doe [rev]"

A tidy annotation layer

The review algebra is designed to remain close to ordinary data-frame workflows. It does not replace dplyr; it adds review state, provenance, and review history around transformations that can still be inspected with ordinary R tools.

This makes review similar in spirit to dataset_df: it adds a semantic layer while remaining compatible with the tidyverse data-frame model. The larger design idea is not to replace tidy data, but to make the assumptions and review decisions around tidy data more explicit.

This works best for vector-wise transformations such as mutate(), transmute(), filter(), select(), relocate(), fill().

Operations that collapse or reshape the data, such as summarise() or some pivot_*() workflows, may change the identity of claims and therefore need more explicit handling.

A future release is expected to implement the dplyr::explain() generic for review objects, providing human-readable summaries of review workflows and their recorded provenance.

Conclusion

The tidyverse already provides an algebra that is frequently used to construct inference workflows. Data are filtered, reconciled, joined, transformed, aggregated, and enriched to derive new semantic statements. These inference steps are often deterministic, but their meaning and justification remain largely implicit in the analysis code and the analyst’s expertise.

The review package introduces a dual extension of the tidy algebra that makes one class of these semantic transformations—human review—explicit. Rather than merely producing a new data frame, it records the semantic states of reviewable variables together with the provenance of the transformations that produced them.

Human review is only the first application of this dual algebra. The same computational model naturally extends to semantic inference. Rule-based reasoning, statistical imputation, authority reconciliation, ontology alignment, and AI-assisted annotation all produce successive semantic states that can be represented using the same review algebra and provenance layer. In this broader view, review is simply one specialised inference operator, distinguished by the fact that the transformation is performed or validated by a human agent.