--- title: "Getting started with ksCompare" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with ksCompare} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(ksCompare) ``` `ksCompare` compares two data frames in the spirit of SAS `PROC COMPARE` — column-by-column, row-by-row, with tolerances and labelled-metadata awareness — and returns a structured `ks_comparison` object you can print, query, export, or render to HTML / Excel. ## A first comparison ```{r} base <- data.frame( id = 1:5, age = c(34, 41, 28, 55, 19), arm = c("A", "A", "B", "B", "A"), stringsAsFactors = FALSE ) comp <- data.frame( id = 1:5, age = c(34, 41, 27, 55, 19), arm = c("A", "A", "b", "B", "A"), stringsAsFactors = FALSE ) cmp <- ks_compare(base, comp, by = "id") cmp ``` `summary(cmp)` returns a structured object with per-section counts; `as_tibble(cmp)` returns the long-format value-diff table. ```{r} summary(cmp) tibble::as_tibble(cmp) |> head() ``` ## What's inside `cmp`? A `ks_comparison` is a list with named slots; you can read them directly when scripting QC dashboards: ```{r} names(cmp) ``` - `cmp$schema_diff` -- one row per column pair (matched, base-only, or comp-only) with `kind_match`, `label_match`, `format_match`. - `cmp$row_diff` -- one row per matched / unmatched row with `key_id`, `base_row`, `comp_row`, `status`. - `cmp$value_diff` -- one row per differing cell with `kind`, `base`, `comp`, `diff`, `note`. Same as `as_tibble(cmp)` and `ks_tidy(cmp)`. - `cmp$pattern_summary` -- recurring shapes per column (e.g. `case_only`, `constant_offset`) when `find_patterns = TRUE` was requested; otherwise an empty tibble with the usual columns. - `cmp$unmatched_rows` -- full base-only / comp-only observations, capped by `max_unmatched_rows`. - `cmp$first_last_unequal` -- PROC COMPARE-style first / last differing observations per matched column. - `cmp$meta` -- counts, key columns, row-matching strategy, and the rendered key labels used by reports. - `cmp$manifest` -- input hashes + run timestamp for QC traceability. - `cmp$verdict` -- executive headline used by `print(cmp)` and the HTML report. ```{r} cmp$schema_diff cmp$value_diff ks_glance(cmp) ``` ## Triage helpers When a comparison is not clean, these helpers narrow the search space quickly: ```{r} ks_cause_summary(cmp) ks_row_diff_summary(cmp) ``` Use `ks_unmatched_rows(cmp)` to inspect full base-only / comp-only records, and rerun with `find_patterns = TRUE` when you want `cmp$pattern_summary` populated. ## Tolerances ```{r} ks_compare( data.frame(id = 1, x = 1.0), data.frame(id = 1, x = 1.0001), by = "id", tolerance = ks_tol(abs = 1e-3) ) ``` Per-column overrides: ```{r, eval = FALSE} ks_tol( abs = 0, per_column = list( weight = ks_tol(abs = 0.01), height = ks_tol(rel = 0.001) ) ) ``` ULP tolerance is useful when you only want to absorb floating-point round-trip noise: ```{r, eval = FALSE} ks_compare(base, comp, by = "id", tolerance = ks_tol(ulp = 4)) ``` ## Options `ks_comp_options()` controls semantics that apply to all columns: - `na_equal`: treat `NA == NA` as equal (default `TRUE`). - `sas_special_missing`: distinguish `.A`-`.Z` and `._` SAS missings (haven-imported numerics only). - `compare_labels` / `compare_formats`: schema-level checks. Format comparison is trailing-dot- and case-tolerant (`DATE9.` == `DATE9` == `date9.`). - `str_trim`, `str_case`, `str_norm`: string normalisation. Strings are always compared encoding-safely (Latin1 vs UTF-8 will not produce false diffs). - `tz`: `"preserve"` (default), `"UTC"`, or `"strip"` for `POSIXct`. ```{r, eval = FALSE} ks_compare( base, comp, by = "id", options = ks_comp_options(str_trim = TRUE, str_case = "fold") ) ``` ## Renamed columns Use `mapping =` for renamed non-key columns and a named `by =` for a renamed key: ```{r, eval = FALSE} ks_compare( base, comp, by = c(USUBJID = "SUBJID"), mapping = c(AGE = "AGE_YEARS") ) ``` ## Reading files directly `ks_compare()` accepts file paths in addition to in-memory data frames and dispatches the right reader by extension: ```{r, eval = FALSE} # SAS transport file ks_compare("prod/adsl.xpt", "qc/adsl.xpt", by = "USUBJID") # .sas7bdat ks_compare("prod/adsl.sas7bdat", "qc/adsl.sas7bdat", by = "USUBJID") # Parquet / Feather ks_compare("a.parquet", "b.parquet", by = "id") # RDS / RData / CSV / TSV are also recognised ks_compare("a.rds", "b.rds", by = "id") ``` When both paths share the same basename (`prod/adsl.xpt` vs `qc/adsl.xpt`), display names are automatically disambiguated using the parent folder, so `print(cmp)` and the HTML report show `prod/adsl` vs `qc/adsl` instead of two identical labels. ## Duplicate keys By default `dup_keys = "first"` keeps the first occurrence per side (emitting a `ksCompare_dup_keys_resolved` info message). Other strategies: - `"last"` -- keep the last per side. - `"keep_all"` -- pair duplicates positionally within each key. - `"all_pairs"` -- cartesian-pair duplicates (warns when group sizes differ). - `"error"` -- raise on any duplicate key. ```{r, eval = FALSE} ks_compare(base, comp, by = "id", dup_keys = "keep_all") ``` ## Next steps - **From SAS:** see `vignette("from-proc-compare")` for a side-by-side mapping of `PROC COMPARE` options. - **Smart features:** see `vignette("smart-features")` for auto-key inference, duplicate-key handling, pattern detection, and SAS-style first / last unequal summaries. - **Reports:** see `vignette("reports")` for HTML and Excel output. - **CI / pipelines:** see `vignette("pipeline-gates")` for using comparisons as assertions.