---
title: "Scalability: 54 thousand to 10 million rows"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Scalability: 54 thousand to 10 million rows}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

## What was measured

The anchor is `ggplot2::diamonds` -- 53,940 rows across 10 columns, shipped
under the MIT licence with ggplot2 itself. Larger sizes are that same real data
replicated with 1% multiplicative jitter on the numeric columns, so the column
count, the type mix and the correlation structure stay realistic while the row
count grows. Rows beyond 53,940 are therefore replications of real data, not
real data; the timing behaviour is what is being demonstrated, not a claim about
a genuinely distinct 10-million-row corpus.

Reproduce with `Rscript inst/bench/benchmark.R`.

## Results

R 4.5.2, macOS arm64, single core.

| Rows | Input | Full | No duplicate scan | Figures | Report object |
|---|---|---|---|---|---|
| 53,940 | 3.5 MB | 0.12 s | 0.07 s | 0.11 s | 82 KB |
| 100,000 | 6.8 MB | 0.26 s | 0.16 s | 0.10 s | 83 KB |
| 500,000 | 34 MB | 1.40 s | 0.57 s | 0.10 s | 84 KB |
| 1,000,000 | 68 MB | 2.99 s | 0.98 s | 0.10 s | 83 KB |
| 5,000,000 | 340 MB | 17.08 s | 4.86 s | 0.11 s | 83 KB |
| 10,000,000 | 680 MB | 38.04 s | 9.44 s | 0.12 s | 83 KB |

Three things to read off that table.

**Cost is linear.** Per-row cost is 1.37 µs at 54 thousand rows and 0.94 µs at
10 million -- it goes *down*, because fixed startup cost amortises. There is no
size at which the curve bends.

**The report does not grow.** The input grows 185-fold, from 3.5 MB to 680 MB.
The returned object grows by about 1%, from 82 KB to 83 KB. It holds summary
statistics, never the data.

**Figures are constant-time.** Rendering costs about 0.11 s at every size,
because the plots are built from precomputed bin counts and five-number
summaries rather than from the observations.

Scores are identical at every size (overall 7.59--7.60), which is the check that
the subsampling used in the expensive steps is not biasing the result.

## How

**Nothing plots raw data.** `histogram_bins()` computes bin counts with
`tabulate(findInterval(...))` and `boxplot_stats()` computes a five-number
summary per column. The ggplot objects receive 30 rows per histogram and one row
per boxplot. A `geom_histogram()` on 10 million observations would carry all 10
million into the plot object.

**VIF does not fit regressions.** The variance inflation factors come from
`diag(solve(cor(X)))`, which is $O(p^3)$ in the *column* count and completely
independent of the row count. Fitting $p$ auxiliary regressions instead is
$O(n p^2)$.

**Expensive steps subsample deterministically.** Shapiro-Wilk is undefined above
5,000 observations and is given a fixed subsample; the random-forest anomaly
model is fitted on at most `sample_size` rows and then applied to all of them.
`subsample()` saves and restores `.Random.seed`, so it neither depends on nor
disturbs the caller's random state.

**Duplicate detection was rewritten.** This was the one genuinely superlinear
step, and it dominated everything else:

| Rows | `duplicated.data.frame` | Integer codes + hash |
|---|---|---|
| 1,000,000 | 3.8 s | 3.0 s |
| 5,000,000 | 58.7 s | 17.1 s |
| 10,000,000 | **494.2 s** | **38.0 s** |

`duplicated()` on a data frame builds one list per row before comparing them, so
at 10 million rows it spent over eight minutes allocating. Encoding each column
to integer codes with `match(col, unique(col))` and hashing one short key per row
returns a bit-identical answer in linear time. It is the single largest
performance change in the package.

If you do not need the duplicate count at all, `check_duplicates = FALSE` takes
10 million rows to 9.4 s.

## Column count, not row count

The one thing that does grow non-linearly is the **column** count, through the
$O(p^3)$ matrix inversion in the VIF calculation and the $O(p^2)$ correlation
matrix. That is fine for the hundreds of columns a tabular data set typically
has and would not be fine for tens of thousands. If you have a very wide matrix,
assess a column subset.

## Memory

Peak memory is roughly the size of the input plus one numeric copy of the
numeric columns (created by `numeric_matrix()` for the correlation and VIF
steps). The 680 MB input peaked around 1.4 GB. Imputation copies only the
columns it actually changes.
