proc-compare-parity
proc-compare-parity.RmdMotivation
SAS’s PROC COMPARE is the reference point for
dfdiffs. Both tools answer the same questions about two
versions of a dataset: which rows are new, which rows are gone, and
which values changed? This vignette maps PROC COMPARE’s
options and output to the matching dfdiffs functions. It
also shows that each function produces readable output at the console on
its own, independent of the Shiny app. The app
(launch_app()) is a thin layer on top of these functions,
and it doesn’t compute anything they don’t.
The only package we need is dfdiffs:
Mapping PROC COMPARE to dfdiffs
The table below pairs each PROC COMPARE concept with the
dfdiffs function or argument that handles it.
PROC COMPARE concept |
dfdiffs function |
|---|---|
BASE=/COMPARE= datasets |
base/compare arguments, every
comparison function |
ID/BY statement (row matching
key) |
by argument |
| Variable Summary (vars only in one dataset) | compare_columns() |
METHOD=ABSOLUTE/CRITERION=
(numeric tolerance) |
tolerance/scale arguments to
compare_values()
|
case-sensitive text compare
(PROC COMPARE’s default) |
ignore_case/trim_ws (a
dfdiffs addition, off by default) |
duplicate ID note |
a warning() from
diff_values_by_key()
|
| Observation Summary / Values Comparison Summary | compare_summary() |
| the full printed comparison listing |
create_comparison_report()’s xlsx
output |
Roster data
We’ll compare two snapshots of the synthetic clinical site roster, taken a year apart:
Roster2021 <- dfdiffs::Roster2021
Roster2022 <- dfdiffs::Roster2022
nrow(Roster2021)
#> [1] 200
nrow(Roster2022)
#> [1] 237Variable Summary: compare_columns()
Before comparing any values, PROC COMPARE lists the
variables that exist in only one dataset. compare_columns()
answers the same question and prints readable output at the console:
compare_columns(base = Roster2021, compare = Roster2022)
#> <dfdiffs column diff>
#> common to both: 11
#> subject_id, first_name, last_name, site_id, enroll_year, enroll_month, enroll_day, height_cm, full_name, first_visit_date, status
#> base only: 0
#> compare only: 0Observation Summary / Values Comparison Summary:
compare_summary()
PROC COMPARE prints a headline summary of row, column,
and value differences before its detailed diff tables.
compare_summary() gives the same headline by building on
compare_data() and compare_columns(), and it
adds no comparison logic of its own:
compare_summary(compare = Roster2022, base = Roster2021, by = "subject_id")
#> <dfdiffs comparison summary>
#> Observations: base = 200, compare = 237
#> common: 197 | new: 40 | deleted: 3
#> Variables: base = 11, compare = 11 (11 common, 0 base only, 0 compare only)
#> No unequal values found.A small example makes the value-level counts easier to follow. In the
two data frames below, note differs in two of the three
rows:
base <- data.frame(
id = 1:3, note = c("ok", "ok", "ok"), val = c(1, 2, 3), stringsAsFactors = FALSE
)
compare <- data.frame(
id = 1:3, note = c("OK", "ok", "changed"), val = c(1, 2, 3), stringsAsFactors = FALSE
)
compare_summary(compare = compare, base = base, by = "id")
#> <dfdiffs comparison summary>
#> Observations: base = 3, compare = 3
#> common: 3 | new: 0 | deleted: 0
#> Variables: base = 3, compare = 3 (3 common, 0 base only, 0 compare only)
#> Values: 2 differing value(s) across 1 variable(s); 0 class mismatch(es)PROC COMPARE compares character values case-sensitively
with no option to change that. Setting ignore_case = TRUE
treats "ok" and "OK" as equal, an option
dfdiffs offers beyond PROC COMPARE parity:
compare_summary(compare = compare, base = base, by = "id", ignore_case = TRUE)
#> <dfdiffs comparison summary>
#> Observations: base = 3, compare = 3
#> common: 3 | new: 0 | deleted: 0
#> Variables: base = 3, compare = 3 (3 common, 0 base only, 0 compare only)
#> Values: 1 differing value(s) across 1 variable(s); 0 class mismatch(es)Duplicate keys
When the BY/ID variables don’t uniquely
identify rows, PROC COMPARE prints a note instead of
stopping with an error. diff_values_by_key() does the same
by issuing a warning:
base_dupe <- data.frame(id = c(1, 1, 2), val = c("a", "z", "b"), stringsAsFactors = FALSE)
compare_dupe <- data.frame(id = c(1, 2), val = c("A", "b"), stringsAsFactors = FALSE)
diff_values_by_key(base = base_dupe, compare = compare_dupe, by = "id")
#> Warning: 'by' does not uniquely identify rows in base (1 duplicate key(s));
#> only the first match per key is used.
#> $diffs
#> # A tibble: 1 × 4
#> `Variable name` id `Current Value` `Previous Value`
#> <chr> <dbl> <chr> <chr>
#> 1 val 1 A a
#>
#> $diffs_byvar
#> # A tibble: 1 × 2
#> `Variable name` `Modified Values`
#> <chr> <int>
#> 1 val 1
#>
#> $class_diffs
#> # A tibble: 0 × 3
#> # ℹ 3 variables: variable <chr>, class_base <chr>, class_compare <chr>Numeric tolerance
compare_values() covers PROC COMPARE’s
CRITERION= with its tolerance argument. The
first call below relies on the default tolerance
(sqrt(.Machine$double.eps)), and the second sets a wider
one:
compare_values(5.0, 5.00000001)
#> [1] FALSE
compare_values(5.0, 5.1, tolerance = 0.2)
#> [1] FALSEThe full report: create_comparison_report()
PROC COMPARE ends with a full printed listing of the
comparison. create_comparison_report() plays that role by
writing every table shown above, plus the row-level and value-level diff
tables, to a single xlsx file. The workbook has nine sheets: New Data,
Deleted Data, Changed Data, Review Changes, Base Data, Compare Data,
Column Diffs, Class Diffs, and Summary.
create_comparison_report(
compare = Roster2022,
base = Roster2021,
by = "subject_id",
file = "roster-comparison.xlsx"
)The download button in the Shiny app calls this same function, so the app supplies no additional comparison logic of its own.