---
title: "Look at the data first"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Look at the data first}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

A capsule is only worth pinning if somebody looked at the data first.
Looking is the step that gets skipped, because it is tedious and because
it is never obviously necessary until afterwards.

This vignette is the short version of looking.

## One command

`capsule_report()` runs the checks this package provides and puts the
findings in one place, worst first.

```{r}
reference <- data.frame(
  id = 1:300,
  score = stats::runif(300, 0, 10),
  grade = sample(c("a", "b", "c"), 300, TRUE),
  stringsAsFactors = FALSE
)

fresh <- reference
fresh$score <- stats::runif(300, 0, 10)

capsule_report(fresh, reference = reference,
               schema = infer_schema(reference))
```

Now a fetch that went wrong in four different ways at once — a rescaled
column, a category nobody expected, a column that arrived empty, and one
that arrived constant:

```{r}
broken <- fresh
broken$score <- broken$score * 5
broken$grade[1:100] <- "z"
broken$dead <- NA_real_
broken$flat <- 7

report <- capsule_report(broken, reference = reference,
                         schema = infer_schema(reference))
report
```

```{r}
report$verdict
summary(report)
```

Each problem is reported **once**. An entirely missing column is missing,
is trivially constant, and makes any covariance singular — reporting all
three would bury the findings that matter under a single cause.

`report_markdown()` writes the same assessment beside the capsule it
describes, so it outlives the console.

```{r}
cat(head(report_markdown(report), 12), sep = "\n")
```

## Per column

`profile_columns()` is the underlying description. It reports the
classical and robust centre side by side, which is the cheapest outlier
detector there is: where a mean and a median disagree, the mean is not
describing the column.

```{r}
messy <- data.frame(
  clean = stats::rnorm(200),
  bimodal = c(stats::rnorm(100, -3), stats::rnorm(100, 3)),
  skewed = c(stats::rexp(199), 500),
  zeros = c(rep(0, 50), stats::runif(150))
)
profile_columns(messy)[, c("column", "mean", "median", "sd", "mad")]
```

The `hist` column carries what no single number can:

```{r}
profile_columns(messy)[, c("column", "hist")]
```

`bimodal` is visibly two humps. Its mean and median agree perfectly and
tell you nothing about that.

## Where the gaps are

The *rate* of missingness is the least interesting thing about it. Two
frames with identical per-column rates can need completely different
handling:

```{r}
structural <- data.frame(
  id = 1:20,
  a = c(rep(NA, 6), 7:20),
  b = c(rep(NA, 6), 7:20)
)
scattered <- data.frame(
  id = 1:20,
  a = c(rep(NA, 6), 7:20),
  b = c(1:14, rep(NA, 6))
)

c(structural = sum(is.na(structural$a)), scattered = sum(is.na(scattered$a)))
```

```{r}
missingness_pattern(structural)
```

```{r}
missingness_pattern(scattered)
```

Two patterns against three. In the first, one structural gap took out
both columns in the same rows — those rows are a different population,
and often belong dropped or modelled separately. In the second the
failures are independent.

`missing_runs()` answers a different question again — whether a gap is
one outage or many failures:

```{r}
missing_runs(data.frame(
  outage = c(1, 2, rep(NA, 8), 11:20),
  sporadic = c(1, NA, 3, NA, 5, NA, 7:20)
), min_run = 2)
```

And `missingness_map()` shows the whole table at once, with no graphics
device, so it works over SSH and inside a plain-text summary:

```{r}
gappy <- data.frame(
  complete = 1:100,
  early = c(rep(NA, 30), 31:100),
  random = ifelse(stats::runif(100) < 0.3, NA, 1),
  late = c(1:70, rep(NA, 30))
)
missingness_map(gappy, height = 10)
```

## Is the missingness itself a problem?

Dropping incomplete rows is unbiased **only** if the data are missing
completely at random. Otherwise the complete cases are a biased sample
and every downstream estimate inherits the bias.

`mcar_test()` is Little's test for that assumption. It needs
maximum-likelihood estimates of the mean and covariance *under*
missingness, which have no closed form, so it carries an EM estimator.

```{r}
n <- 400
x <- stats::rnorm(n)
y <- x + stats::rnorm(n)

# Missing on a coin flip: nothing to find.
mcar <- data.frame(x = x, y = y)
mcar$y[sample(n, 120)] <- NA
mcar_test(mcar)
```

```{r}
# Missing whenever x is large: the complete cases are a biased sample,
# and that is detectable because the pattern's mean of x is shifted.
mar <- data.frame(x = x, y = y)
mar$y[x > 0.4] <- NA
mcar_test(mar)
```

Read the caveat the second report prints. A large p-value is a failure to
detect a departure, not evidence of MCAR, and the test has little power
on small samples. Neither this test nor any other can separate
missing-at-random from missing-**not**-at-random, because that depends on
values that were never observed — only knowing how the data were
collected settles it.

## Rows that do not belong

A row can be unremarkable on every column separately and impossible
jointly.

```{r}
people <- data.frame(height_cm = stats::rnorm(300, 170, 10))
people$weight_kg <- people$height_cm * 0.5 + stats::rnorm(300, 0, 5)

# Inside both marginal ranges, outside the cloud.
people[1, ] <- list(height_cm = 150, weight_kg = 140)

range(people$height_cm)
range(people$weight_kg)
```

```{r}
head(mahalanobis_outliers(people), 3)
```

The default is a robust centre and scale, and that default matters more
than it sounds: outliers inflate the very covariance used to judge them,
so with several of them the classical distance hides exactly the rows it
is meant to find.

```{r}
many <- people
many[1:8, ] <- list(height_cm = rep(150, 8), weight_kg = rep(140, 8))

c(robust = mahalanobis_outliers(many, robust = TRUE)$distance[1],
  classical = mahalanobis_outliers(many, robust = FALSE)$distance[1])
```

## Figures that were not measured

Quantities spanning several orders of magnitude follow Benford's law.
Figures that were rounded, capped, rescaled or invented usually do not.

```{r}
benford_test(10^stats::runif(2000, 0, 6))
```

```{r}
benford_test(as.numeric(paste0(sample(1:9, 2000, TRUE), "000")))
```

This is a **screen**, not a verdict. Postcodes, year fields, prices
ending in 99 and anything with a unit floor all violate Benford's law
perfectly legitimately. A small p-value is a reason to look.

## What none of this does

Every check here has a blind spot, and a clean report means only that
these particular checks found nothing:

- the drift tests have little power on small samples;
- `mcar_test()` assumes multivariate normality, so on markedly
  non-normal columns a rejection may be telling you about the
  distribution rather than the missingness;
- Mahalanobis distance assumes the bulk of the data is elliptical;
- Benford's law does not apply to most bounded or assigned quantities.

None of them can tell you the data means what you think it means. That
still requires reading the documentation for the source, which is the
part `load_provenance()` and the manifest exist to keep attached.
