# \[ANN\] MissingPatterns.jl — visualize missing data in the terminal

**URL:** <https://discourse.julialang.org/t/ann-missingpatterns-jl-visualize-missing-data-in-the-terminal/139114>\
**Category:** Package Announcements\
**Tags:** missing-values, unicodeplots\
**Created:** [August 31, 2026, 1:52am UTC](https://discourse.julialang.org/t/ann-missingpatterns-jl-visualize-missing-data-in-the-terminal/139114 "2026-08-31T01:52:54Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![dantebertuzzi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dantebertuzzi/32/215393_2.png) [@dantebertuzzi](https://discourse.julialang.org/u/dantebertuzzi)\
**Post date:** [August 31, 2026, 1:52am UTC](https://discourse.julialang.org/t/ann-missingpatterns-jl-visualize-missing-data-in-the-terminal/139114/1 "2026-08-31T01:52:55Z")

</div>

Hi everyone,

I’d like to announce [MissingPatterns.jl](https://github.com/dantebertuzzi/MissingPatterns.jl), a small toolkit for exploring missing data patterns directly in the terminal.

The package started from a gap I kept running into: in Python I’d reach for [missingno](https://github.com/ResidentMario/missingno) for a quick look at missingness before anything else, and in R for `mice::md.pattern()` or `naniar`. Coming to Julia I looked for an equivalent and didn’t find one, the usual answer was to compute the masks by hand and plot them yourself. So I wrote the tool I wanted to have, with one difference from `missingno`: since these are diagnostics you run early and often, I wanted them to render in the REPL without pulling in a plotting stack.

It works with any [Tables.jl](https://github.com/JuliaData/Tables.jl)-compatible source (`DataFrame`, `CSV.File`, `NamedTuple` of vectors, …) and has **zero plotting dependencies** , everything is rendered with Unicode/ANSI, so it also works over SSH and inside IDE/Jupyter output cells.

```julia
using Pkg; Pkg.add("MissingPatterns")

using MissingPatterns, DataFrames

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)

```

```plaintext
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ A ┃ B ┃ C ┃ n ┃ % ┃ freq ┃
┣━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━┫
┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ 3 ┃ 37.5% ┃ ███████ ┃
┃ █████ ┃ █████ ┃ ░░░░░ ┃ 2 ┃ 25.0% ┃ █████ ┃
┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ 1 ┃ 12.5% ┃ ██ ┃
┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ 1 ┃ 12.5% ┃ ██ ┃
┃ ░░░░░ ┃ ░░░░░ ┃ █████ ┃ 1 ┃ 12.5% ┃ ██ ┃
┗━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┛

 5 unique patterns across 8 rows

```

## A worked example

Toy tables don’t show much, so here is a full pass over something closer to real data: one year of daily readings from a weather station, with the kind of missingness these files actually have, a probe that went offline for three weeks, a sensor that was down for a whole month, and background dropouts everywhere.

```julia
using MissingPatterns, DataFrames, Dates, Random, Statistics
Random.seed!(42)

dates = Date(2024,1,1):Day(1):Date(2024,12,31)
n = length(dates)

F = Vector{Union{Missing,Float64}}

df = DataFrame(
    date = collect(dates),
    temp = F(round.(15 .+ 10 .* sin.(2pi .* (1:n) ./ 365) .+ randn(n), digits=1)),
    humid = F(round.(60 .+ 15 .* randn(n), digits=1)),
    wind = F(round.(abs.(3 .+ 2 .* randn(n)), digits=1)),
    precip = F(round.(abs.(randn(n)) .* 5, digits=1)),
    pm25 = F(round.(20 .+ 8 .* randn(n), digits=1)),
)

# the temperature/humidity probe was offline for three weeks in March
df[Date(2024,3,4) .<= df.date .<= Date(2024,3,22), [:temp, :humid]] .= missing

# the PM2.5 sensor was down for the whole of August
df[month.(df.date) .== 8, :pm25] .= missing

# plus sporadic dropouts
for (col, k) in ((:pm25, 25), (:humid, 18), (:temp, 10), (:wind, 6))
    df[randperm(n)[1:k], col] .= missing
end

```

### missingsummary — how much, and where along the rows

The first question is always “how bad is it, and is it spread out or clumped?”. Each column gets a count, a percentage, and a sparkline over the row axis (20 bins by default), so a block outage and a uniform trickle stop looking like the same number.

```julia
missingsummary(df)

```

```plaintext
 column type missing % distribution
 pm25 Float64 54 14.75% ▁▂▁▁ ▁▁▁▇█▁▁▁▂▁▁▂
 humid Float64 36 9.84% ▁▁▆▃▁ ▁▁▁▁▁ ▁▁▁▁
 temp Float64 28 7.65% ▁ ▆▃ ▁ ▁▁ ▁▁ ▁
 wind Float64 6 1.64% ▁ ▁ ▁ ▁
 date Date 0 0.00%
 precip Float64 0 0.00%
 124 missing of 2196 cells (5.65%) across 6 columns ┊ bins of 19 rows

```

`pm25` has one tall spike (the August outage) sitting on a low background; `temp` and `humid` share a bump in the same place, early in the year. A bin with even a single missing value always renders at least `▁`, so isolated holes never vanish into a blank block.

### plotmissing — the heatmap, optionally on a calendar axis

`plotmissing(df)` gives the classic `missingno`-style matrix, compressed with a block gradient when the table is taller than the terminal. What I use most, though, is `by=:date, period=:month`: the vertical axis stops being arbitrary row ranges and becomes honest calendar time.

```julia
plotmissing(df; by=:date, period=:month, name_width=6)

```

```plaintext
┏━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ ┃ 0% ┃ 8% ┃ 10% ┃ 2% ┃ 0% ┃ 15% ┃
┣━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━┫
┃ row ┃ date ┃ temp ┃ humid ┃ wind ┃ precip ┃ pm25 ┃
┣━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━┫
┃2024-01┃ ░░░░░ ┃ ····· ┃ ····· ┃ ░░░░░ ┃ ░░░░░ ┃ ····· ┃
┃2024-02┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ▒▒▒▒▒ ┃
┃2024-03┃ ░░░░░ ┃ █████ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ ····· ┃
┃2024-04┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-05┃ ░░░░░ ┃ ····· ┃ ░░░░░ ┃ ····· ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-06┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-07┃ ░░░░░ ┃ ····· ┃ ····· ┃ ····· ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-08┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ····· ┃ ░░░░░ ┃ █████ ┃
┃2024-09┃ ░░░░░ ┃ ░░░░░ ┃ ····· ┃ ····· ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-10┃ ░░░░░ ┃ ░░░░░ ┃ ····· ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-11┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃
┃2024-12┃ ░░░░░ ┃ ░░░░░ ┃ ····· ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃
┗━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┛

MissingPatterns.Analysis: 366 × 6 DataFrame
 Grouping: by date (month) → 12×6 cells
 Missing (count): 124 ┊ Missing (%): 5.65%
 Present (count): 2072 ┊ Present (%): 94.35%
 Progress Bar: [██████████████████████████████████████████████████████████]

```

Each cell is shaded by the missing fraction of its block, from fully present (`░`) through `·`, `▒` and `▓` up to fully missing (`█`), so the March probe outage and the August sensor failure are readable at a glance and dated. `period` also takes `:year`, `:quarter`, `:week` and `:day`, and `by` alone (no `period`) groups by the raw values of any sortable column — `by=:station`, `by=:region` — which turns the same plot into “is this missingness concentrated in one group?”.

In a color terminal, `layout=:compact` packs two grid rows per output line with half-blocks (use `color=:always` in VS Code or Jupyter, where `stdout` isn’t a TTY but ANSI still renders).

### missingpatterns — which columns go missing together

The equivalent of R’s `mice::md.pattern()`: unique missingness signatures, sorted by frequency.

```julia
missingpatterns(df; name_width=6, max_patterns=8)

```

```plaintext
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ date ┃ temp ┃ humid ┃ wind ┃ precip ┃ pm25 ┃ n ┃ % ┃ freq ┃
┣━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━┫
┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ 268 ┃ 73.2% ┃ ███████ ┃
┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ █████ ┃ 49 ┃ 13.4% ┃ █ ┃
┃ ░░░░░ ┃ █████ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ 19 ┃ 5.2% ┃ ┃
┃ ░░░░░ ┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ 14 ┃ 3.8% ┃ ┃
┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ 6 ┃ 1.6% ┃ ┃
┃ ░░░░░ ┃ ░░░░░ ┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ 4 ┃ 1.1% ┃ ┃
┃ ░░░░░ ┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ █████ ┃ 2 ┃ 0.5% ┃ ┃
┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ █████ ┃ ░░░░░ ┃ ░░░░░ ┃ 1 ┃ 0.3% ┃ ┃
┗━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┛

 11 unique patterns across 366 rows ┊ showing top 8 of 11

```

73.2% of the days are complete; the third row is the probe outage, the one signature where `temp` and `humid` disappear _jointly_ — that’s the one that matters for a complete-case analysis, and the one an independent per-column imputation would get wrong. `min_pct=1.0` collapses the long tail of one-off signatures instead of capping by count.

### missingcooccurrence — pairwise phi / Jaccard between masks

The patterns table says which combinations exist; this one summarises the pairwise dependence between missingness masks, which is what you actually cite when arguing against MCAR.

```julia
missingcooccurrence(df; name_width=6) # method=:jaccard for the overlap index

```

```plaintext
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ ϕ ┃ date ┃ temp ┃ humid ┃ wind ┃ precip ┃ pm25 ┃
┣━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━┫
┃ date ┃ — ┃ · ┃ · ┃ · ┃ · ┃ · ┃
┃ temp ┃ · ┃ — ┃ 0.60 ┃ 0.04 ┃ · ┃ -0.06 ┃
┃ humid ┃ · ┃ 0.60 ┃ — ┃ -0.04 ┃ · ┃ -0.06 ┃
┃ wind ┃ · ┃ 0.04 ┃ -0.04 ┃ — ┃ · ┃ 0.01 ┃
┃ precip ┃ · ┃ · ┃ · ┃ · ┃ — ┃ · ┃
┃ pm25 ┃ · ┃ -0.06 ┃ -0.06 ┃ 0.01 ┃ · ┃ — ┃
┗━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┛
 pairwise ϕ of missingness masks ┊ n = 366 rows

```

ϕ(temp, humid) = 0.60 against ~0 everywhere else: the two probes fail as one device, everything else fails independently. Columns with no missing values are `·` (degenerate, no coefficient defined) and the diagonal is `—`. With color on, cell intensity scales with the absolute value.

### plotmissingdiff — auditing an imputation step

Same table before and after a transformation, cell by cell. Here I fill the three meteorological columns with their column means and deliberately leave `pm25` alone:

```julia
imputed = copy(df)
for c in (:temp, :humid, :wind)
    imputed[!, c] = coalesce.(imputed[!, c], round(mean(skipmissing(imputed[!, c])), digits=1))
end

plotmissingdiff(df, imputed; name_width=6, target_lines=14)

```

```plaintext
┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ da… +0% ┃ te… -8% ┃ h… -10% ┃ wi… -2% ┃ pr… +0% ┃ pm… +0% ┃
┣━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━┫
┃ ····· ┃ ----- ┃ ----- ┃ ----- ┃ ····· ┃ ····· ┃
┃ ····· ┃ ----- ┃ ----- ┃ ····· ┃ ····· ┃ ····· ┃
┃ ····· ┃ ····· ┃ ----- ┃ ····· ┃ ····· ┃ ····· ┃
┃ ····· ┃ ----- ┃ ----- ┃ ----- ┃ ····· ┃ ····· ┃
┃ ····· ┃ ----- ┃ ----- ┃ ····· ┃ ····· ┃ ····· ┃
┃ ····· ┃ ----- ┃ ----- ┃ ----- ┃ ····· ┃ ····· ┃
┃ ····· ┃ ----- ┃ ----- ┃ ----- ┃ ····· ┃ ····· ┃
┃ ····· ┃ ····· ┃ ----- ┃ ····· ┃ ····· ┃ ····· ┃
┃ ····· ┃ ····· ┃ ----- ┃ ····· ┃ ····· ┃ ····· ┃
┗━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━┛
 Δ missing: 5.65% → 2.46% ┊ resolved 70 ┊ introduced 0

```

`-` marks blocks with fewer missing values than before, `+` blocks with more, `·` unchanged; the per-column header carries the signed delta and the footer the exact cell counts, computed by a row-aligned pass rather than from the block averages. In color, resolved blocks are green and introduced ones red. The point of the plot is `pm25 +0%` and `introduced 0`: the step did what it claimed, and nothing else, which is exactly the assertion that is easy to make and hard to check after a pipeline touches a dataset.

### missinghtml — the same heatmap, portable

For reports and notebook exports, the same compression engine and color ramp render to a self-contained `<div>`, no external CSS or JS, every cell carrying a tooltip with its row range and exact percentage. It affords a much bigger grid than a terminal (defaults: 200x60 blocks).

```julia
html = missinghtml(df; title = "Station 08A - 2024") # returns a String
missinghtml("station08a.html", df; title = "Station 08A - 2024", max_rows = 180)

```

## Wrapping up

Full docs: [Home · MissingPatterns.jl](https://dantebertuzzi.github.io/MissingPatterns.jl/stable)

Feedback, bug reports and ideas are very welcome — especially on the compressed rendering for large tables, and on which diagnostics would be worth adding next.

---

<div class="post-metadata">

**Author:** ![parn4q](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/parn4q/32/222376_2.png) [@parn4q](https://discourse.julialang.org/u/parn4q)\
**Post date:** [August 31, 2026, 3:06am UTC](https://discourse.julialang.org/t/ann-missingpatterns-jl-visualize-missing-data-in-the-terminal/139114/2 "2026-08-31T03:06:47Z")

</div>

Nicely Done! This is cool!
