MissingPatterns is a terminal-based toolkit for exploring missing data
patterns in any Tables.jl-compatible
source (DataFrame, CSV.File, NamedTuple of vectors, row tables, ...) β
zero plotting-library dependencies, pure Unicode/ANSI terminal rendering.
using Pkg
Pkg.add("MissingPatterns")using MissingPatterns
# Works with NamedTuples, DataFrames, CSV.File, etc.
tbl = (A = [1, missing, 3, 4],
B = [missing, 2, 3, 4],
C = [1, missing, missing, 4])
plotmissing(tbl)Nothing to install to try it: Google Colab runs Julia natively, and every notebook below opens there from the badge. Pick the Julia runtime under Runtime βΈ Change runtime type and run the cells. All are committed with the outputs of a real run, so they also read on GitHub without being executed.
| Notebook | What it does |
|---|---|
getting-started.ipynb |
The tour, on a table whose missingness was put there on purpose β and then found with the package rather than by knowing where it was. Every entry point, the isna sentinel form, grouping by category and by calendar period, the data API, and an imputation audited with plotmissingdiff. |
obis-missingness.ipynb |
The same diagnostics on real data: marine biodiversity records pulled live from OBIS with OBISClient.jl. Two depth fields that turn out to be one, a provenance split hiding in a negative Ο, not a single complete record in three thousand β and a check of the sample against OBIS's own counts for the whole query. |
whats-new-0.7.ipynb |
What 0.7 adds, on the getting-started table: Little's MCAR test β including the mechanism it cannot see β then missingsentinels finding the coded absences and missingpredictors finding that mechanism, pattern tables ordered by structure, grouped plots without the grouping column, row labels in HTML, and two releases of a table compared by key with plotmissingdiff(...; on). |
novidades-0.7.ipynb |
Em portuguΓͺs. As novidades da 0.7 numa base simulada no formato do SUS β escolaridade com 9 = ignorado, renda recusada no Sudeste, exames perdidos na pandemia β, com a fΓ³rmula matemΓ‘tica de cada diagnΓ³stico ao lado do cΓ³digo e um falso positivo discutido em vez de escondido. |
| Function | What it shows |
|---|---|
plotmissing |
Where/how much is missing (heatmap) |
missingpatterns |
Which columns go missing together (Γ la mice::md.pattern()) |
missingsummary |
Per-column counts, % and a distribution sparkline |
missingcooccurrence |
Pairwise Ο/Jaccard correlation of missingness masks |
plotmissingdiff |
Before/after diff (e.g. auditing an imputation step) |
missingrows |
How many values are missing per row (what listwise deletion costs) |
missingdrop |
Which column to drop to buy back complete rows |
missingmcar |
Little's test of missing completely at random |
missingsentinels |
Values that probably encode absence (9, "", "NA"β¦), ready for isna |
missingpredictors |
What a column's missingness depends on (the MAR diagnostic) |
missinghtml |
The heatmap as a standalone HTML fragment |
missingreport |
The heatmap as an object that renders itself in terminal or HTML |
Every display above has a data counterpart that returns a Tables.jl-compatible row table instead of printing β see Getting the numbers out.
Shows where and how much data is missing. Each cell represents the proportion of missing values in that block.
plotmissing(tbl)
plotmissing(tbl; layout=:compact) # half-block compact mode
plotmissing(tbl; layout=:auto, target_lines=28) # fit within N lines
plotmissing(tbl; color=:always) # force ANSI/truecolor output
plotmissing(tbl; color=:always, emphasis=:missing, missing_color="#ff6600")
plotmissing(tbl; max_rows=20, max_cols=10, cell_chars=3)
plotmissing(tbl; char_missing='X', char_present='.')
plotmissing(tbl; name_width=6)
plotmissing(tbl; show_row_range=true) # show original row ranges| Kwarg | Default | Description |
|---|---|---|
layout |
:auto |
:auto, :classic, or :compact (half-block truecolor) |
color |
:auto |
:auto (TTY detection), :always, or :never |
emphasis |
:present |
:present or :missing β which side of the data carries the ink |
missing_color |
"#f3a9a9" |
Hex color ("#rrggbb") of the ramp |
target_lines |
28 |
Max lines for the compact layout |
max_rows |
50 |
Display rows before compression (classic layout) |
max_cols |
20 |
Display columns before compression |
cell_chars |
5 |
Width of each grid cell (max 80) |
char_missing |
'β' |
Character for fully-missing cells |
char_present |
'β' |
Character for fully-present cells |
name_width |
4 |
Column-name max chars before truncating (0 = full name) |
color_cells |
false |
Apply the color ramp to classic-layout glyphs |
show_row_range |
false |
Show row-range (or period) labels on the left |
by |
nothing |
Name of a column β group rows by category or calendar period instead of position |
period |
nothing |
nothing (categorical grouping by by's exact value), or :year, :quarter, :month, :week (ISO-8601), :day for a Date/DateTime by column |
isna |
ismissing |
Predicate deciding what counts as an absent value |
order |
:table |
Column order: :table, :missing, :name or :cluster |
exclude_by |
true |
Leave the by column itself out of the grid |
:classicβ one grid row per line, with a 3-line header and a 6-line summary. Best in a full terminal with room to scroll.:compactβ fits the entire plot in at mosttarget_lineslines, so IDE/Jupyter output cells never truncate it. With color available, each output line encodes two grid rows viaβ(foreground = top row, background = bottom row), doubling vertical resolution.:auto(default) β uses:classicwhen it fits withintarget_lines,:compactotherwise.
# categorical grouping (period=nothing, the default): groups by exact value
tbl = (region = ["north", "south", "north", "east"], v = [1, missing, 3, missing])
plotmissing(tbl; by=:region)
# temporal grouping: groups by calendar period of a Date/DateTime column
using Dates
tbl2 = (date = [Date(2023,1,15), Date(2024,6,1), Date(2024,6,2)],
v = [1, missing, 3])
plotmissing(tbl2; by=:date, period=:year)
plotmissing(tbl2; by=:date, period=:quarter)
plotmissing(tbl2; by=:date, period=:month)
plotmissing(tbl2; by=:date, period=:week)
plotmissing(tbl2; by=:date, period=:day)Rows are grouped by the values of the by column (not by position), so the
vertical axis becomes honest categories or calendar time instead of arbitrary
row ranges. With period=nothing (default), groups are the column's exact
values, sorted β works for any sortable column (String, Symbol, Int,
...). With period set to a calendar unit, groups are periods of a
Date/DateTime column (e.g. 2004, 2013-Q2). Rows whose by value is
missing form a trailing β
group either way. The by column itself is
left out of the grid β it would only restate the grouping, present in every
group and missing in β
β and the summary's totals cover the columns drawn;
pass exclude_by=false to keep it.
Columns are drawn in table order by default β an accident of how the file was written, which usually scatters the columns that go missing together and hides the very structure the plot exists to show.
plotmissing(tbl; order=:cluster) # co-missing columns side by side
plotmissing(tbl; order=:missing) # emptiest columns first
plotmissing(tbl; order=:name) # alphabetical:cluster seriates the Ο matrix of the missingness masks, starting at the
column with the most missing values and repeatedly appending the unplaced
column most associated with the last one placed. Columns with no missing values
carry no pattern and go to the end, so a complete column never splits a block
in half.
Reordering is display-only: every count, percentage and total is identical whatever the order.
Real microdata rarely uses missing. DATASUS, the TSE and most public
statistical files code absence as a sentinel: 9/99 for "ignored", "" for
a blank field. isna counts those as holes without rewriting the table:
tbl = (idade = [34, 9, 51, 9], sexo = ["M", "", "F", "M"])
plotmissing(tbl) # nothing is missing
plotmissing(tbl; isna = x -> ismissing(x) || x == 9 || x == "")That form applies one predicate to every column, which is rarely what you
want: a sentinel belongs to a variable, not to a table. 9 means "ignored"
in a coded field but is a perfectly good age, and the blanket predicate above
punches a hole in idade for every 9-year-old. Pass a NamedTuple (or a
Dict) of per-column predicates instead, with ismissing assumed for any
column left out:
plotmissing(tbl; isna = (idade = x -> ismissing(x) || x == 9,
sexo = x -> ismissing(x) || x == ""))Naming a column the table does not have is an error rather than a silently ignored entry, so a typo surfaces instead of quietly showing a complete table.
In either form, test ismissing first and let || short-circuit:
missing == 9 is missing, not false, and a bare x == 9 would throw in a
boolean context.
The predicate is available on every entry point β heatmap, diagnostics,
data API, HTML export β and applies to the by column too, where a sentinel
forms the β
group just as missing does.
Shows which combinations of columns are missing together β the same
diagnostic produced by R's mice::md.pattern(). Patterns are sorted
most-frequent first.
df = DataFrame(
A = [1, missing, 3, missing, 5, 6, 7, missing],
B = [missing, 2, 3, missing, 5, 6, 7, missing],
C = [1, 2, 3, 4, missing, 6, 7, 8],
)
missingpatterns(df)βββββββββββ³ββββββββββ³ββββββββββ³ββββββββββ³ββββββββββ
β A β B β C β n β % β
β£ββββββββββββββββββββββββββββββββββββββββββββββββββ«
β βββββ β βββββ β βββββ β 3 β 37.5% β
β βββββ β βββββ β βββββ β 2 β 25.0% β
β βββββ β βββββ β βββββ β 1 β 12.5% β
β βββββ β βββββ β βββββ β 1 β 12.5% β
β βββββ β βββββ β βββββ β 1 β 12.5% β
βββββββββββ»ββββββββββ»ββββββββββ»ββββββββββ»ββββββββββ
5 unique patterns across 8 rows
missingpatterns(tbl; max_patterns=10, min_pct=5.0) # hide rare patterns
missingpatterns(tbl; color_cells=true, emphasis=:missing)
missingpatterns(tbl; show_bar=false) # hide the UpSet-style frequency bar
missingpatterns(tbl; order=:cluster) # co-missing columns side by sidemax_patterns (default 20) caps how many rows are displayed; min_pct
(default 0.0) hides patterns matching fewer than that percentage of rows.
cell_chars, char_missing, char_present, name_width, color_cells,
missing_color and emphasis behave exactly as in plotmissing.
Shows each column's type, missing count, percentage, and a sparkline of where along the rows the missing values concentrate.
missingsummary(tbl)
missingsummary(tbl; sortby=:missing) # sort by missing count, descending (default)
missingsummary(tbl; sortby=:name) # alphabetical
missingsummary(tbl; sortby=:none) # original column order
missingsummary(tbl; bins=5) # group sparkline into 5 bins instead of 20
missingsummary(tbl; color=:always)Computes the Ο (phi) coefficient or Jaccard index between every pair of
columns' missingness masks. Positive values indicate columns tend to be
missing together; this complements missingpatterns with a
correlation-style view of the same question.
missingcooccurrence(tbl)
missingcooccurrence(tbl; method=:jaccard) # Jaccard index instead of Ο (default)
missingcooccurrence(tbl; max_cols=10) # cap displayed columns (default 20)
missingcooccurrence(tbl; color=:always)Compares two versions of a dataset (e.g. before/after an imputation step)
and highlights cells where missing values were resolved (-, fewer missing)
or introduced (+, more missing).
before = (a=[missing, 2, missing, 4], b=[1, missing, 3, 4])
after = (a=[1, 2, 3, 4], b=[1, 2, missing, 4])
plotmissingdiff(before, after)
plotmissingdiff(before, after; color=:always)When the two releases do not line up row for row β re-sorted, filtered or appended to β pair them by a key column instead of by position:
plotmissingdiff(release_2023, release_2024; on=:id)Only keys present on both sides are compared; the summary says how many rows each side had that the other did not.
The transposed view: not which columns are missing, but how many values
are missing in each row. The 0 line is the complete-case count β everything
below it is what dropmissing would throw away.
missingrows(tbl)
missingrows(tbl; sortby=:rows) # most common shape first (default: :nmissing)
missingrows(tbl; bar_width=50)
missingrows(tbl; color=:always) missing/row rows % distribution
0 3 37.50% ββββββββββββββββββββββββββββββ
1 3 37.50% ββββββββββββββββββββββββββββββ
2 2 25.00% ββββββββββββββββββββ
3 complete rows (37.50%) β 5 with β₯1 missing (62.50%) β 3 distinct counts across 3 columns
missingrows prices listwise deletion for the table as it stands.
missingdrop prices the alternative β trading a variable for rows β and names
the variable worth trading. It walks the greedy path, at each step removing the
column that turns the most rows complete.
missingdrop(tbl)
missingdrop(tbl; bar_width=50)
missingdrop(tbl; color=:always) drop cols complete % distribution
β 5 626 62.60% βββββββββββββββββββ
lab 4 940 94.00% ββββββββββββββββββββββββββββ β most complete-case cells
income 3 980 98.00% βββββββββββββββββββββββββββββ
age 2 1000 100.00% ββββββββββββββββββββββββββββββ
626 of 1000 rows complete as given (62.60%) β dropping 1 column leaves 940 complete across 4 columns (94.00%)
Dropping lab alone takes complete-case analysis from 626 rows to 940, at the
price of one variable. The flag marks the step maximizing complete Γ columns left β the size of the surviving complete-case block. Whether that trade is
worth making is a modeling judgment; the package only prices it.
Every view above suggests a mechanism. missingmcar tests the one most
analyses assume without saying so: that whether a value is missing has nothing
to do with the data β missing completely at random. Little's (1988) test
compares each pattern's observed means with the maximum-likelihood means of the
whole table (estimated by EM under a multivariate normal model); under MCAR the
weighted distance is ΟΒ²-distributed.
r = missingmcar(tbl) # every real-valued column
r = missingmcar(tbl; columns=[:age, :weight]) # a subset
r = missingmcar(tbl; isna=(income = x -> ismissing(x) || x == 99,))On a table where older respondents refuse to state their income:
julia> missingmcar(tbl)
(statistic = 124.6, df = 5, pvalue = 3.3e-25, npatterns = 4, ...)
julia> missingmcar(tbl; columns=[:age, :weight]) # weight is missing at random
(statistic = 0.3, df = 1, pvalue = 0.585, ...)
A small p-value is evidence against MCAR, so complete-case analysis is likely
biased. A large one fails to reject it; it does not prove it, and it cannot tell
MAR from MNAR. The test is defined for continuous columns: by default every
column with a real element type is used (Bool excluded), and rows with nothing
observed are left out. The result also carries the EM estimates (mu, sigma)
and whether EM converged.
Administrative microdata code absence as values β 9 for "ignored", 999 for
"not stated", an empty string, "NA", "ignorado". isna counts them as
missing, but someone has to find them first. missingsentinels proposes
candidates, each with the evidence for it:
found = missingsentinels(tbl)
DataFrame(found) Row β column value n pct reason
ββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1 β schooling 99 206 20.6 above every other value (max 3)
Text is proposed when it is blank or reads as absent. A numeric code (9,
99, β¦, -1, -9, β¦) is only proposed when the data back it: it lies beyond
every other value of its column, past a gap, or it is at least five times as
frequent as the values around it β so 9 in a 0β9 scale, or 99 in an age
column that reaches 105, is left alone. Codes stored as text ("9" among
"1"β¦"5") are judged as numbers. The rules are on the
Methods page of the documentation.
These are candidates for review. Keep the ones that are codes and turn
them into the per-column isna every other function takes:
isna = sentinelisna(found) # all of them
isna = sentinelisna(filter(r -> r.column != :age, found)) # all but one column
missingsummary(tbl; isna)missingmcar says whether the missingness is related to the data;
missingpredictors says to what. For one column, every other column is
compared between the rows where it is missing and the rows where it is
present β Welch's t-test for numbers and dates, a ΟΒ² test of independence for
everything else β and sorted by p-value. A column that differs predicts the
missingness: that is the working diagnostic for MAR, and the list of variables
an imputation model for the column must include.
missingpredictors(tbl, :income; isna) column test pvalue effect detail
region chisq 2.18e-08 0.20 south: 42.1% of rows where missing vs 20.9% where present
age welch_t 1.82e-05 0.35 mean 60 where missing vs 52.47 where present
date welch_t 0.242 0.09 mean 2022-02-18 where missing vs 2021-12-21 where present
schooling welch_t 0.7 0.03 mean 1.509 where missing vs 1.472 where present
lab welch_t 0.756 -0.03 mean 3.429 where missing vs 3.456 where present
weight welch_t 0.87 0.02 mean 60.32 where missing vs 60.15 where present
Income is refused more in the south and by older respondents β which is how
this table was built. For numeric columns effect is the standardized mean
difference (0.2 small, 0.5 medium, 0.8 large by Cohen's convention); for the
others it is CramΓ©r's V. Dates are compared as dates, which catches what
missingmcar cannot: the same call on :lab puts date first, with
p = 1.7e-19 and a mean a year earlier where lab is missing.
One test is run per column, so with many columns a few unrelated ones will come out significant by chance; read the effect sizes alongside the p-values.
Renders the same heatmap and color ramp as plotmissing as a standalone,
self-contained HTML fragment (no external CSS/JS) β suitable for reports,
blog posts, or notebook exports. Every cell carries a tooltip with its row
range and exact missing percentage.
missinghtml(tbl) # returns a String
missinghtml(tbl; title="My Report", emphasis=:missing, missing_color="#ff0000")
missinghtml(tbl; by=:region) # same grouping as plotmissing
missinghtml(tbl; show_row_range=true) # row-range labels beside the grid
missinghtml("/path/to/report.html", tbl) # writes to a file, returns the pathmissingreport returns an object that renders itself as the terminal heatmap
under MIME"text/plain" and as the HTML heatmap under MIME"text/html". The
same expression therefore shows Unicode in a REPL and a colored, tooltipped
grid in Jupyter or Pluto, with no branching on the caller's side.
missingreport(tbl)
missingreport(tbl; emphasis=:missing, missing_color="#ff6600")
missingreport(tbl; by=:region) # grouped in both media
missingreport(tbl; layout=:compact, title="Cohort A")
show(stdout, MIME"text/html"(), missingreport(tbl)) # force one mediumIt accepts the keyword arguments of both plotmissing and missinghtml, and
forwards each only to the renderer that takes it β so per-medium defaults (a
200Γ60 HTML grid vs a 50Γ20 terminal grid) survive unless you override them.
An unknown keyword is an error immediately, not at display time.
plotmissing and missinghtml are unchanged and remain the direct,
single-medium entry points.
Every view has a data counterpart returning a plain
Tables.jl-compatible row table
(Vector{<:NamedTuple}) β no display compression, no max_patterns/max_cols
cap, nothing printed. They share the same kernels as the renderers, so a number
read here can never disagree with the one drawn on screen.
| Function | One row per | Key fields |
|---|---|---|
missingstats |
column | column, eltype, nmissing, npresent, nrows, pct |
missingpatternstats |
unique missingness pattern | pattern (a NamedTuple of Bool keyed by column), nmissing, n, pct |
missingpairstats |
unordered pair of columns | a, b, phi, jaccard, n11, n1, n2, nrows |
missingrowstats |
observed missing-count | nmissing, nrows, pct |
missingdropstats |
column-drop step | ndropped, dropped, ncols, complete, pct, cells |
missingsentinels |
candidate sentinel value | column, value, n, pct, reason |
missingpredictors |
other column | column, kind, test, statistic, df, pvalue, effect, n_missing, n_present, detail |
using DataFrames
df = DataFrame(age = [34, missing, 51, missing, 29],
income = [missing, 4200, 5100, missing, 3300],
city = ["SP", "RJ", "BH", "SP", missing])
DataFrame(missingstats(df)) # straight into a DataFrame
filter(r -> r.pct > 20, missingstats(df)) # columns worse than 20% missing
# most co-missing column pairs β `first` rather than `[1:5]`, which would
# throw on a table with fewer than five pairs
first(sort(missingpairstats(df); by = r -> -r.phi), 5)
ps = missingpatternstats(df)
filter(r -> r.pattern.age && !r.pattern.income, ps) # age missing, income present
filter(r -> r.nmissing == 0, ps) # the complete-case pattern
rs = missingrowstats(df)
only(r.nrows for r in rs if r.nmissing == 0) # complete-case count
sum(r.nrows for r in rs if r.nmissing > 0) # rows lost to listwise deletionmissingpairstats returns both Ο and Jaccard rather than selecting one
with a method keyword: both fall out of the same n11/n1/n2 counts, so
the schema stays fixed regardless of which you read. Undefined coefficients
are NaN β phi whenever a column is entirely missing or entirely present,
jaccard only when neither column has a single missing value.
When a table exceeds max_rows/max_cols (or the :compact layout's own
budget), multiple rows/columns are compressed into single cells. The
character gradient shows the proportion of missing values in each block:
| Proportion | Compressed glyph |
|---|---|
| 0% | β |
| 1β5% | Β· |
| 5β15% | β |
| 15β30% | β |
| 30β50% | β |
| 50%+ | β |
# 20k rows Γ 10 cols β auto-compressed to display bounds
using Random
Random.seed!(123)
nrows, ncols = 20_000, 10
data = [rand() < 0.2 ? missing : rand(1:100) for _ in 1:nrows, _ in 1:ncols]
tbl = NamedTuple{Tuple(Symbol("Col_$i") for i in 1:ncols)}(Tuple(view(data, :, j) for j in 1:ncols))
plotmissing(tbl; layout=:compact)Every function (except missinghtml, which returns/writes a String)
accepts an optional leading io::IO argument, defaulting to stdout:
# Write to a file
open("missing_report.txt", "w") do f
plotmissing(f, tbl)
end
# Capture to a string
io = IOBuffer()
plotmissing(io, tbl)
report = String(take!(io))Use color=:always when redirecting to a destination that renders ANSI but
isn't a TTY (e.g. a VS Code or Jupyter output cell), and color=:never when
writing plain text to a file.
All functions accept any Tables.jl-compatible source β DataFrames.jl is not a dependency of the package itself.
using DataFrames, CSV
# DataFrame
plotmissing(DataFrame(a=[1,missing,3], b=[4,5,missing]))
# NamedTuple of vectors
plotmissing((a=[1,missing,3], b=[4,5,missing]))
# CSV file
plotmissing(CSV.File("data.csv"))- Zero plotting dependencies β pure Unicode/ANSI terminal rendering
- Tables.jl-native β works with any compatible source, not just DataFrames
- Automatic compression for large datasets, with enhanced sensitivity to subtle patterns
- Compact half-block layout with truecolor gradients for IDE/Jupyter output cells
- Grouping by category (any sortable column) or by calendar year/quarter/month/week/day
- Sentinel-aware (
isna) β count9,99,""or any other coded absence as missing, as public microdata does - Column ordering (
order=:cluster) β put co-missing columns side by side so the block structure is visible - Pattern detection (
missingpatterns) and pairwise correlation (missingcooccurrence) of missingness - Before/after diffing (
plotmissingdiff) for auditing imputation steps - Row-completeness distribution (
missingrows) β what listwise deletion costs - Listwise-deletion trade-off (
missingdrop) β which column to drop to buy back complete rows - Little's MCAR test (
missingmcar) β the formal check of missing completely at random - Sentinel detection (
missingsentinels,sentinelisna) β find the9s,""s and"NA"s that mean "missing", and turn them intoisna - Predictors of missingness (
missingpredictors) β which variables a column's missingness depends on - Tables.jl data API (
missingstats,missingpatternstats,missingpairstats,missingrowstats,missingdropstats) β every view also available as data - HTML export (
missinghtml) for reports and notebooks - Medium-aware display (
missingreport) β terminal in the REPL, HTML in Jupyter/Pluto - IO-customizable output β render to
stdout, a file, or anIOBuffer - TTY-aware ANSI/truecolor coloring β colors enabled only where supported
The methods this package implements, and the work it follows. Each entry says where in the package it is used.
Missing-data mechanisms
- Rubin, D. B. (1976). Inference and missing data. Biometrika, 63(3), 581β592. https://doi.org/10.1093/biomet/63.3.581 β the MCAR / MAR / MNAR distinction the diagnostics are read against.
- Little, R. J. A., & Rubin, D. B. (2019). Statistical Analysis with Missing
Data (3rd ed.). Wiley. https://doi.org/10.1002/9781119482260 β the
standard reference for the field, including listwise deletion
(
missingrows,missingdrop) and maximum-likelihood estimation under missing data.
Little's MCAR test (missingmcar)
- Little, R. J. A. (1988). A test of missing completely at random for multivariate data with missing values. Journal of the American Statistical Association, 83(404), 1198β1202. https://doi.org/10.1080/01621459.1988.10478722 β the test statistic and its ΟΒ² reference distribution.
- Dempster, A. P., Laird, N. M., & Rubin, D. B. (1977). Maximum likelihood from incomplete data via the EM algorithm. Journal of the Royal Statistical Society: Series B, 39(1), 1β22. https://doi.org/10.1111/j.2517-6161.1977.tb01600.x β the EM estimates of the mean and covariance the test compares against.
- Anderson, T. W. (1957). Maximum likelihood estimates for a multivariate normal distribution when some observations are missing. Journal of the American Statistical Association, 52(278), 200β203. https://doi.org/10.1080/01621459.1957.10501379 β the closed-form estimates for a monotone pattern, which the test suite checks EM against.
- Press, W. H., Teukolsky, S. A., Vetterling, W. T., & Flannery, B. P. (2007).
Numerical Recipes: The Art of Scientific Computing (3rd ed.), Β§6.2.
Cambridge University Press. β Β§6.2, the series and continued-fraction
evaluation of the incomplete gamma function behind the ΟΒ² p-values; Β§6.4, the
incomplete beta function behind the t p-values of
missingpredictors. - Lanczos, C. (1964). A precision approximation of the gamma function. Journal of the Society for Industrial and Applied Mathematics, Series B: Numerical Analysis, 1(1), 86β96. https://doi.org/10.1137/0701008 β the log-gamma used by the incomplete beta function.
Predictors of missingness (missingpredictors)
- Welch, B. L. (1947). The generalization of 'Student's' problem when several different population variances are involved. Biometrika, 34(1β2), 28β35. https://doi.org/10.1093/biomet/34.1-2.28 β the unequal-variance t-test and its degrees of freedom.
- Pearson, K. (1900). On the criterion that a given system of deviations from the probable in the case of a correlated system of variables is such that it can be reasonably supposed to have arisen from random sampling. The London, Edinburgh, and Dublin Philosophical Magazine and Journal of Science, 50(302), 157β175. https://doi.org/10.1080/14786440009463897 β the ΟΒ² test of independence.
- CramΓ©r, H. (1946). Mathematical Methods of Statistics. Princeton University Press. https://doi.org/10.1515/9781400883868 β CramΓ©r's V.
- Cohen, J. (1988). Statistical Power Analysis for the Behavioral Sciences (2nd ed.). Lawrence Erlbaum; reprinted 2013, Routledge. https://doi.org/10.4324/9780203771587 β the standardized mean difference and its conventional 0.2 / 0.5 / 0.8 benchmarks.
Patterns and co-occurrence (missingpatterns, missingcooccurrence, order=:cluster)
- van Buuren, S., & Groothuis-Oudshoorn, K. (2011). mice: Multivariate
imputation by chained equations in R. Journal of Statistical Software,
45(3). https://doi.org/10.18637/jss.v045.i03 β
md.pattern(), the pattern tablemissingpatternsreproduces. - Lex, A., Gehlenborg, N., Strobelt, H., Vuillemot, R., & Pfister, H. (2014). UpSet: Visualization of intersecting sets. IEEE Transactions on Visualization and Computer Graphics, 20(12), 1983β1992. https://doi.org/10.1109/TVCG.2014.2346248 β the frequency bars beside each pattern.
- Yule, G. U. (1912). On the methods of measuring association between two attributes. Journal of the Royal Statistical Society, 75(6), 579β652. https://doi.org/10.2307/2340126 β the Ο coefficient of two binary attributes, here the missingness masks.
- Jaccard, P. (1912). The distribution of the flora in the alpine zone. New Phytologist, 11(2), 37β50. https://doi.org/10.1111/j.1469-8137.1912.tb05611.x β the Jaccard index of the missing-row sets.
- Hahsler, M., Hornik, K., & Buchta, C. (2008). Getting things in order: An
introduction to the R package seriation. Journal of Statistical Software,
25(3). https://doi.org/10.18637/jss.v025.i03 β seriation, of which
order=:clusteris a greedy nearest-neighbour instance.
Calendar grouping (period=:week)
- ISO 8601-1:2019. Date and time β Representations for information interchange β Part 1: Basic rules. International Organization for Standardization. β the week numbering and week-based year.
Sentinel detection (missingsentinels) has no reference: its rules are
this package's own heuristic, written out on the Methods page of the documentation.
Related software
- Tierney, N., & Cook, D. (2023). Expanding tidy data principles to facilitate
missing data exploration, visualization and assessment of imputations.
Journal of Statistical Software, 105(7).
https://doi.org/10.18637/jss.v105.i07 β
naniar, the R package for the same exploration, including a Little's test (mcar_test).
The repository ships a CITATION.cff, which GitHub reads
natively: the "Cite this repository" button in the sidebar generates ready
APA and BibTeX. A CITATION.bib is also provided:
@software{bertuzzi_missingpatterns_2026,
author = {Bertuzzi, Dante},
title = {{MissingPatterns.jl}: terminal-based exploration of missing
data patterns in {Julia}},
year = {2026},
version = {0.7.0},
doi = {10.5281/zenodo.22217099},
url = {https://github.com/dantebertuzzi/MissingPatterns.jl},
note = {Julia package}
}Cite the version you used, not "the latest". What the package reports is
part of your result, and it has changed between releases: plotmissing's
period default became nothing in 0.4.0, the data API and missingrows
arrived in 0.5.0, isna and missingdrop in 0.6.0 β and 0.6.0 also fixed
period=:week, which until then merged ISO weeks across a year boundary. In
0.7.0 grouping with by stopped drawing the by column itself, which changes
the totals of every grouped plot, and missingmcar arrived.
missingpairstats reports Ο and Jaccard side by side where
missingcooccurrence shows one at a time. Run pkg> status MissingPatterns and use the number it prints.
Cite the method too. The package implements published methods, and a result
reported from one is the method's as much as the software's: a missingmcar
p-value is Little's (1988) test, a pattern table is md.pattern() (van Buuren
& Groothuis-Oudshoorn, 2011), a Ο matrix is Yule's (1912) coefficient. The
References list says which work backs which function.
MissingPatterns.jl reads data, it does not supply any β so there is no
upstream source to cite alongside it, unlike a package that downloads a
public database. What makes a missingness figure reproducible is the input
plus the environment. So that someone else reaches your numbers, record: the
MissingPatterns.jl and Julia versions; the Project.toml and
Manifest.toml of the environment (the Manifest.toml pins the whole
dependency tree and is what makes it reconstructible with
Pkg.instantiate()); and the dataset itself β its own citation, version or
extraction date, and any filtering applied before the table reached this
package, since dropping rows changes every count reported here.
If you reproduce a figure rather than a number, note the keywords too:
layout, max_rows/max_cols and by/period determine how rows are
compressed into blocks, so two calls on the same data can render differently.
The Data API returns the uncompressed numbers and
is the more citable form.
| Standard | What it establishes |
|---|---|
| FORCE11 β Software Citation Principles | Software is a citable research product. Six principles: importance, credit, unique identification, persistence, accessibility and specificity (cite the exact version). |
| Citation File Format (CFF) 1.2.0 | Machine-readable citation metadata. What GitHub and Zenodo consume. |
| Zenodo + GitHub | Mints a persistent DOI per release, plus a concept DOI always pointing at the newest version. |
The DOIs of this project: the repository is connected to Zenodo, so every release is archived and gets a persistent identifier β the citation no longer depends on the GitHub URL surviving a rename or a transfer. Two DOIs coexist, and they are not interchangeable:
| DOI | What it identifies |
|---|---|
| 10.5281/zenodo.22217099 | Concept DOI β the project as a whole. Always resolves to the newest version; it is what the badge at the top of this README points at. |
| one per release | Each archived version gets its own β 0.5.1 is 10.5281/zenodo.22217708. All of them are listed on the Zenodo page. |
The BibTeX above carries the concept DOI, so it keeps working across releases. In a paper, swap it for the DOI of the version you used: the concept DOI says which project you used, the version DOI says which code actually ran.
