# compare()


Compare two tables row by row and report how they differ.


Usage

``` python
compare(
    source,
    target,
    keys=None,
    columns=None,
    column_map=None,
    tolerance=None,
    normalize=None,
    null_equal=True,
    schema="strict",
    dup_keys="flag",
    order_by=None,
    partitions=None,
    source_name=None,
    target_name=None,
)
```


The `compare()` function aligns the rows of a *source* table (the reference, or expected data) with those of a *target* table (the data being checked) and classifies every aligned row: rows that match, rows whose values changed, rows missing from the target, and extra rows in the target. It also reports schema differences (columns added, removed, retyped, or reordered). The returned [Comparison](Comparison.md#pointblank.Comparison) object gives you the counts, samples of the differing rows, and a tabular report.

Rows are aligned in one of two ways:

- **by key** (`keys=` given): rows are matched on the values of one or more key columns, so row order doesn't matter. This is the most useful mode for comparing extracts of the same data (e.g., the source and target of a pipeline run).
- **by position** (`keys=None`, the default): the first row of the source is compared with the first row of the target, and so on.

The comparison is backend-agnostic and runs inside the table's own engine: tables can be Polars or Pandas DataFrames, Polars LazyFrames, Ibis tables (e.g., DuckDB, PostgreSQL), or paths to CSV/Parquet files. File paths are scanned lazily, and only counts and small samples of differing rows are ever collected, so tables that don't fit in memory can still be compared.


## Parameters


`source: Any`  
The reference table. Can be any table type supported by Pointblank, a path to a CSV or Parquet file, or a callable that returns a table.

`target: Any`  
The table to check against the source. Same input types as `source=`.

`keys: str | list[str] | None = None`  
One or more columns that uniquely identify a row, used to align rows between the tables. Key names refer to the source table (use `column_map=` if the target names them differently). If `None`, rows are aligned by position, which requires the tables to have a defined row order (eager DataFrames, Polars LazyFrames, or any table with `order_by=`).

`columns: str | list[str] | None = None`  
The source columns whose values should be compared. By default, all columns present in both tables (other than the keys) are compared. Giving `columns=` also limits the schema check to the keys and these columns.

`column_map: dict[str, str] | None = None`  
A mapping of source column names to target column names, for columns that were renamed.

`tolerance: Any = None`  
Tolerance for numeric and datetime comparisons. A number is an absolute tolerance applied to all numeric columns. A dict with `"atol"` and/or `"rtol"` keys applies to all numeric columns, where values are equal if `|source - target| <= atol + rtol * |source|`. A dict mapping column names to either of the above sets per-column tolerances. Datetime tolerances are given as a `datetime.timedelta` or a string like `"1s"`, `"500ms"`, `"2m"`, or `"1d"`, e.g., `tolerance={"updated_at": "1s"}`.

`normalize: dict[str, Any] | None = None`  
Per-column transformations applied to both sides before comparing, as a dict mapping a column name to a list of operations: `"strip"`, `"lower"`, `"upper"`, `"collapse_whitespace"`, or `{"round": n}`. The string operations only apply to string columns and `round` only to numeric ones (other columns are left as they are). Reported values are always the original ones.

`null_equal: bool = ``True`  
Should a null in the source and a null in the target count as equal? By default, they do. NaN values in floating-point columns are treated as null.

`schema: Literal[``"strict", `<span class="st">`"common"``]`</span>` = ``"strict"`  
How schema differences affect the result. With `"strict"` (the default), any schema difference (other than renames given in `column_map=`) means the tables don't match, and every row counts as failing. With `"common"`, only the shared columns are compared and schema differences are reported without failing rows.

`dup_keys: Literal[``"flag", `<span class="st">`"compare"``]`</span>` = ``"flag"`  
Rows whose key appears more than once in either table can't be aligned unambiguously, so they always get the `"dup_key"` status. With `"flag"` (the default), they are only flagged and counted. With `"compare"`, the report also compares the duplicated rows pairwise (every source row against every target row with the same key) to show which columns differ.

`order_by: str | list[str] | None = None`  
Column(s) defining row order for positional alignment on backends without an inherent row order (e.g., database tables). Ignored when `keys=` is given.

`partitions: int | None = None`  
Compare the tables in this many passes, each covering a disjoint subset of the key values (by key hash). This bounds peak memory for tables too large to compare in one pass (each pass joins only about `1/partitions` of the rows), at the cost of reading the tables once per pass. Requires `keys=`, and Polars, DuckDB, or Ibis tables (including CSV/Parquet paths). The results are identical to a single-pass comparison.

`source_name: str | None = None`  
Optional labels for the two tables, used in the report.

`target_name: str | None = None`  
Optional labels for the two tables, used in the report.


## Returns


`Comparison`  
The result of the comparison.


## Test Units And Row Statuses

Every aligned row is one *test unit* and gets exactly one status:

- `"match"`: the row is in both tables and all compared values are equal (within tolerance)
- `"changed"`: the row is in both tables but at least one compared value differs
- `"missing"`: the row is in the source but not in the target
- `"extra"`: the row is in the target but not in the source
- `"dup_key"`: the row's key appears more than once in either table. A duplicated key counts as many units as the larger of its two occurrence counts.

Rows with a null in a key column can't be matched: they count as `"missing"` (source) or `"extra"` (target).


## Large Tables

The comparison runs inside the tables' own engine and only counts and small samples of rows are collected, so file paths (scanned lazily) and database tables never have to be loaded into Python. The engine's join still needs memory for the rows it compares. If that's too much, use `partitions=` to compare the tables in several smaller passes. For the lowest memory use with very large files, read them as DuckDB relations (e.g., `duckdb.connect().read_csv(path)`) and use `partitions=`: in a benchmark with two 10-million-row CSV files (730 MB each), peak memory was about 0.8 GB with DuckDB and `partitions=8`, versus about 5.7 GB to read both files with Polars and join them. With 100-million-row files (7.4 GB each), DuckDB with `partitions=8` peaked at about 2 GB.


## Examples


``` python
import pointblank as pb
import polars as pl

source = pl.DataFrame(
    {"id": [1, 2, 3, 4], "amount": [10.0, 20.0, 30.0, 40.0], "region": ["N", "S", "E", "W"]}
)
target = pl.DataFrame(
    {"id": [1, 2, 4, 5], "amount": [10.0, 20.5, 40.0, 50.0], "region": ["N", "S", "W", "N"]}
)

cmp = pb.compare(source, target, keys="id")
cmp
```


<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap');
#pb_comparison_tbl table {
          font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
        }

#pb_comparison_tbl thead, tbody, tfoot, tr, td, th { border-style: none; }
 tr { background-color: transparent; }
#pb_comparison_tbl p { margin: 0; padding: 0; }
 #pb_comparison_tbl .gt_table { display: table; border-collapse: collapse; line-height: normal; margin-left: auto; margin-right: auto; color: #333333; font-size: 16px; font-weight: normal; font-style: normal; background-color: #FFFFFF; width: 100%; border-top-style: solid; border-top-width: 2px; border-top-color: #A8A8A8; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #A8A8A8; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; }
 #pb_comparison_tbl .gt_caption { padding-top: 4px; padding-bottom: 4px; }
 #pb_comparison_tbl .gt_title { color: #333333; font-size: 125%; font-weight: initial; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; border-bottom-color: #FFFFFF; border-bottom-width: 0; }
 #pb_comparison_tbl .gt_subtitle { color: #333333; font-size: 85%; font-weight: initial; padding-top: 3px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; border-top-color: #FFFFFF; border-top-width: 0; }
 #pb_comparison_tbl .gt_heading { background-color: #FFFFFF; text-align: left; border-bottom-color: #FFFFFF; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; }
 #pb_comparison_tbl .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
 #pb_comparison_tbl .gt_col_headings { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; }
 #pb_comparison_tbl .gt_col_heading { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; }
 #pb_comparison_tbl .gt_column_spanner_outer { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; }
 #pb_comparison_tbl .gt_column_spanner_outer:first-child { padding-left: 0; }
 #pb_comparison_tbl .gt_column_spanner_outer:last-child { padding-right: 0; }
 #pb_comparison_tbl .gt_column_spanner { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 5px; overflow-x: hidden; display: inline-block; width: 100%; }
 #pb_comparison_tbl .gt_spanner_row { border-bottom-style: hidden; }
 #pb_comparison_tbl .gt_group_heading { padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; text-align: left; }
 #pb_comparison_tbl .gt_empty_group_heading { padding: 0.5px; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; vertical-align: middle; }
 #pb_comparison_tbl .gt_from_md> :first-child { margin-top: 0; }
 #pb_comparison_tbl .gt_from_md> :last-child { margin-bottom: 0; }
 #pb_comparison_tbl .gt_row { padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; }
 #pb_comparison_tbl .gt_stub { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; }
 #pb_comparison_tbl .gt_indent_1 { text-indent: 5px; }
 #pb_comparison_tbl .gt_indent_2 { text-indent: calc(5px * 2); }
 #pb_comparison_tbl .gt_indent_3 { text-indent: calc(5px * 3); }
 #pb_comparison_tbl .gt_indent_4 { text-indent: calc(5px * 4); }
 #pb_comparison_tbl .gt_indent_5 { text-indent: calc(5px * 5); }
 #pb_comparison_tbl .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; }
 #pb_comparison_tbl .gt_row_group_first td { border-top-width: 2px; }
 #pb_comparison_tbl .gt_row_group_first th { border-top-width: 2px; }
 #pb_comparison_tbl .gt_striped { color: #333333; background-color: #F4F4F4; }
 #pb_comparison_tbl .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
 #pb_comparison_tbl .gt_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
 #pb_comparison_tbl .gt_first_summary_row { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; }
 #pb_comparison_tbl .gt_last_summary_row_top { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
 #pb_comparison_tbl .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
 #pb_comparison_tbl .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
 #pb_comparison_tbl .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
 #pb_comparison_tbl .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
 #pb_comparison_tbl .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
 #pb_comparison_tbl .gt_left { text-align: left; }
 #pb_comparison_tbl .gt_center { text-align: center; }
 #pb_comparison_tbl .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
 #pb_comparison_tbl .gt_font_normal { font-weight: normal; }
 #pb_comparison_tbl .gt_font_bold { font-weight: bold; }
 #pb_comparison_tbl .gt_font_italic { font-style: italic; }
 #pb_comparison_tbl .gt_super { font-size: 65%; }
 #pb_comparison_tbl .gt_footnotes { color: font-color(#FFFFFF); background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
 #pb_comparison_tbl .gt_footnote { margin: 0px; font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; }
 #pb_comparison_tbl .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
 #pb_comparison_tbl .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
 #pb_comparison_tbl .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
 #pb_comparison_tbl .gt_asterisk { font-size: 100%; vertical-align: 0; }
 
</style>

<table class="gt_table" style="table-layout: fixed;; width: 100%" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<colgroup>
<col style="width: 30%" />
<col style="width: 70%" />
</colgroup>
<thead>
<tr class="gt_heading">
<th colspan="2" class="gt_heading gt_title gt_font_normal">Table Comparison</th>
</tr>
<tr class="gt_heading">
<th colspan="2" class="gt_heading gt_subtitle gt_font_normal gt_bottom_border"><div style="font-size:13px; line-height:1.9;">
<strong>source</strong> (4 rows) → <strong>target</strong> (4 rows), aligned by key <code>id</code><br />
<span style="background:#CF142B; color:white; padding:1px 6px; border-radius:3px; font-size:11px; font-weight:bold; letter-spacing:0.3px;">DIFFERENT</span> 3 of 5 rows fail (60.0%)<br />


<br />
<span style="white-space:nowrap; margin-right:12px;"><span style="display:inline-block; width:9px; height:9px; border-radius:2px; background:#4CA64C; margin-right:4px; margin-left:4px;"></span>match <strong>2</strong></span><span style="white-space:nowrap; margin-right:12px;"><span style="display:inline-block; width:9px; height:9px; border-radius:2px; background:#E8A33D; margin-right:4px; margin-left:4px;"></span>changed <strong>1</strong></span><span style="white-space:nowrap; margin-right:12px;"><span style="display:inline-block; width:9px; height:9px; border-radius:2px; background:#CF142B; margin-right:4px; margin-left:4px;"></span>missing <strong>1</strong></span><span style="white-space:nowrap; margin-right:12px;"><span style="display:inline-block; width:9px; height:9px; border-radius:2px; background:#3D7CC9; margin-right:4px; margin-left:4px;"></span>extra <strong>1</strong></span>
</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="gt_group_heading_row">
<th colspan="2" class="gt_group_heading" style="font-size: 12px; font-weight: bold; background-color: #F6F7F8">Column mismatches (1 of 2 columns)</th>
</tr>

<tr>
<td class="gt_row gt_left" style="font-family: IBM Plex Mono; font-size: 12px">amount</td>
<td class="gt_row gt_left" style="font-size: 12px"><div style="display:inline-block; vertical-align:middle; width:120px; height:7px; background:#EEE; border-radius:2px; margin-right:8px; margin-left:8px; overflow:hidden;">


1 of 3 matched rows differ (33.3%)</td>
</tr>
<tr class="gt_group_heading_row">
<td colspan="2" class="gt_group_heading" style="font-size: 12px; font-weight: bold; background-color: #F6F7F8">Changed rows (1 row)</td>
</tr>
<tr>
<td class="gt_row gt_left" style="font-family: IBM Plex Mono; font-size: 12px">2</td>
<td class="gt_row gt_left" style="font-size: 12px"><span style="white-space:nowrap; margin-right:14px;"><span style="color:#666;">amount:</span> <span style="text-decoration:line-through; color:#999;">20</span> → <span style="background:#FDF1DC; padding:0 3px; border-radius:2px;">20.5</span> <span style="color:#888; font-size:11px;">(+0.5)</span></span></td>
</tr>
<tr class="gt_group_heading_row">
<td colspan="2" class="gt_group_heading" style="font-size: 12px; font-weight: bold; background-color: #F6F7F8">Missing from target (1 row)</td>
</tr>
<tr>
<td class="gt_row gt_left" style="font-family: IBM Plex Mono; font-size: 12px">3</td>
<td class="gt_row gt_left" style="font-size: 12px"><span style="white-space:nowrap; margin-right:10px;"><span style="color:#666;">amount:</span> 30</span> <span style="white-space:nowrap; margin-right:10px;"><span style="color:#666;">region:</span> E</span></td>
</tr>
<tr class="gt_group_heading_row">
<td colspan="2" class="gt_group_heading" style="font-size: 12px; font-weight: bold; background-color: #F6F7F8">Extra in target (1 row)</td>
</tr>
<tr>
<td class="gt_row gt_left" style="font-family: IBM Plex Mono; font-size: 12px">5</td>
<td class="gt_row gt_left" style="font-size: 12px"><span style="white-space:nowrap; margin-right:10px;"><span style="color:#666;">amount:</span> 50</span> <span style="white-space:nowrap; margin-right:10px;"><span style="color:#666;">region:</span> N</span></td>
</tr>
</tbody>
</table>


The counts are available as attributes, and the differing rows can be retrieved by status:


``` python
cmp.n_changed, cmp.n_missing, cmp.n_extra
```


    (1, 1, 1)


``` python
cmp.rows("changed")
```


shape: (1, 5)

| id  | amount_source | amount_target | region_source | region_target |
|-----|---------------|---------------|---------------|---------------|
| i64 | f64           | f64           | str           | str           |
| 2   | 20.0          | 20.5          | "S"           | "S"           |


A tolerance can absorb small numeric differences:


``` python
pb.compare(source, target, keys="id", tolerance={"amount": 1}).n_changed
```


    0
