Skip to content

About

πŸ•³οΈ Explore missing-data patterns straight in the terminal β€” heatmaps, md.pattern()-style summaries and co-occurrence, with zero plotting dependencies.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

60 Commits

Folders and files

Repository files navigation

MissingPatterns

Logo do MissingPatterns.jl

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.

Stable Docs Dev Docs Build Status Coverage JuliaHub DOI Open in Colab

Installation

using Pkg
Pkg.add("MissingPatterns")

Quick Start

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)

Notebooks

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 Open in Colab 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 Open in Colab 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 Open in Colab 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 Open in Colab 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.

Functions

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.

plotmissing β€” Missing-value heatmap

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

Layouts

  • :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 most target_lines lines, 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 :classic when it fits within target_lines, :compact otherwise.

Grouping by category or by time

# 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.

Ordering the columns

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.

Sentinel values with isna

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.

missingpatterns β€” Unique missingness patterns

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 side

max_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.

missingsummary β€” Per-column missing summary

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)

missingcooccurrence β€” Pairwise correlation of missingness

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)

plotmissingdiff β€” Before/after comparison

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.

missingrows β€” Per-row completeness

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

missingdrop β€” What dropping a column buys

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.

missingmcar β€” Little's MCAR test

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.

missingsentinels β€” Absence that does not say missing

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)

missingpredictors β€” What the missingness depends on

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.

missinghtml β€” HTML heatmap export

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 path

missingreport β€” One object, two media

missingreport 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 medium

It 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.

Getting the numbers out

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 deletion

missingpairstats 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.

Large Datasets

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)

Output Redirection

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.

Tables.jl Compatibility

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"))

Features

  • 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) β€” count 9, 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 the 9s, ""s and "NA"s that mean "missing", and turn them into isna
  • 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 an IOBuffer
  • TTY-aware ANSI/truecolor coloring β€” colors enabled only where supported

References

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 table missingpatterns reproduces.
  • 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=:cluster is 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).

How to cite

1. The software

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.

2. Reproducibility

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.

The standards behind this

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.

About

πŸ•³οΈ Explore missing-data patterns straight in the terminal β€” heatmaps, md.pattern()-style summaries and co-occurrence, with zero plotting dependencies.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages