Skip to main content

Almost No Account Reaches the Greyson Threshold

2026
A beeswarm of 587 scored near-death experience accounts across the eight themes the archive codes for, showing that only 9% reach the score at which the Greyson NDE Scale counts an account as a near-death experience.
Author
Published

Tuesday, the 21st of July, 2026

See the code that generated this plot.
// Gribouille comes from the typst-render preamble (assets/typst/_preamble.typ),
// so this file does not import it.

// One row per account in the near-death experience archive. The Greyson score and
// the eight theme flags come from the narrative text rather than from an
// interview, which is the caveat in the caption. Every number in the chart is
// computed from this table.
// Source: data/nde_experiences.csv (TidyTuesday 2026-07-21).
#let raw = csv("data/nde_experiences.csv", row-type: dictionary)

// Scores are written as whole numbers, with "NA" where none was assigned.
#let score-of(v) = if v == none or v == "" or v == "NA" { none } else { int(v) }
#let pct = format-percent()

// The Greyson NDE Scale runs 0 to 32 and treats 7 as the threshold for a
// near-death experience. The chart is built around that number.
#let threshold = 7
#let scale-max = 32

// Every account carrying a score: the denominator behind the headline share.
#let scored = raw.map(r => score-of(r.greyson_score)).filter(s => s != none)
#let n-accounts = raw.len()
#let n-scored = scored.len()
#let n-above = scored.filter(s => s >= threshold).len()
#let pct-above = pct(n-above / n-scored)
#let overall-median = median(scored).y
#let archive-max = calc.max(..scored)

// One pass per theme, collecting the scores of the accounts that carry it. An
// account can carry several themes, so it appears in several rows. Commonest
// first: the panel is built upright and flipped, so slot 1 lands at the bottom.
//
// The eight coded themes, each with the gloss the axis carries beside its column
// name. This order is the source order. The rows sort by how many accounts carry
// each theme.
#let rows = (
  (col: "ai_clinical", gloss: "clinical crisis"),
  (col: "ai_obe", gloss: "out-of-body"),
  (col: "ai_unity", gloss: "oneness, unity"),
  (col: "ai_esp", gloss: "extrasensory perception"),
  (col: "ai_hellish", gloss: "distressing, hellish"),
  (col: "ai_world_future", gloss: "the world's future"),
  (col: "ai_past_lives", gloss: "past lives"),
  (col: "ai_aliens", gloss: "aliens"),
).map(spec => {
  let s = raw
    .filter(r => r.at(spec.col) == "TRUE")
    .map(r => score-of(r.greyson_score))
    .filter(v => v != none)
  spec + (
    n: s.len(),
    median: median(s).y,
    above: s.filter(v => v >= threshold).len(),
    scores: s,
  )
}).sorted(key: t => t.n).rev()
#let n-rows = rows.len()
#let slot-of(i) = n-rows - i

// The row whose tail clears the threshold most often, found by share, and the
// row too small to say anything about. Rows under twenty accounts are left out
// of the first search.
#let stretch = rows.filter(t => t.n >= 20).sorted(key: t => t.above / t.n).last()
#let stretch-pct = pct(stretch.above / stretch.n)
#let biggest = rows.first()
#let thinnest = rows.last()

#let bands = ("below", "at or above")

// The subtitle claims no theme shifts the middle, so a median crossing the
// threshold fails the render.
#let medians = rows.map(t => t.median)
#let median-lo = calc.min(..medians)
#let median-hi = calc.max(..medians)
#assert(
  median-hi < threshold,
  message: "a theme's median now reaches the threshold: " + repr(medians),
)

// The crimson is reserved for accounts that clear the threshold. Everything below
// is a backdrop rather than a second category, so the neutral reads as grey. The
// rule and the per-row counts repeat the split, so identity never rests on
// colour. Both clear the checks on both surfaces.
#let above-col = rgb("#c93b4c") // crimson: reaches the diagnostic threshold
#let below-col = rgb("#9a8f80") // warm neutral: the rest of the archive

// A public-record sans for the prose, and a data face for the column names,
// because the row labels are columns of the dataset. Both are vendored in
// assets/fonts, so CI renders them too.
#let body-font = "Public Sans"
#let mono-font = "IBM Plex Mono"

// No fill means "say nothing about the colour", so the text takes the page ink
// whichever way the site is toggled. A colour is passed only for text naming a
// coloured mark.
#let note(body, fill: none, size: 7pt, weight: "regular") = {
  set text(fill: fill) if fill != none
  text(font: body-font, size: size, weight: weight)[#body]
}
#let col-name(name, size) = text(font: mono-font, size: size, weight: "medium")[#name]

#let swarm-width = 0.32

#plot(
  // One point per account per theme it carries. Colour encodes one thing:
  // whether the account reaches the threshold.
  data: rows.enumerate().map(((i, t)) => t.scores.map(s => (
    slot: slot-of(i),
    score: s,
    band: if s >= threshold { bands.last() } else { bands.first() },
  ))).join(),
  mapping: aes(x: "slot", y: "score", fill: "band", alpha: "band"),
  layers: (
    // The threshold: the line almost nothing crosses. Drawn half a point below 7,
    // so it separates the integer scores.
    geom-hline(
      yintercept: threshold - 0.5,
      stroke: 0.9pt, colour: above-col, linetype: "dashed", inherit-aes: false,
    ),
    // One dot per account, swarmed so the thickness of a row shows where the
    // scores pile up. Small and translucent, since dense rows stack hundreds.
    geom-beeswarm(
      size: 1.6pt, stroke: 0pt,
      position: position-beeswarm(width: swarm-width),
    ),
    // Each row's median, as a rule across its own swarm, in a neutral that
    // reads as a mark rather than a category. Sized in data units off the swarm
    // width, so it tracks the rows.
    geom-errorbarh(
      data: rows.enumerate().map(((i, t)) => (slot: slot-of(i), median: t.median)),
      mapping: aes(x: "slot", xmin: "median", xmax: "median"),
      inherit-aes: false, height: swarm-width, stroke: 2pt, colour: rgb("#8a8f96"),
    ),
    // Both rules are labelled in the empty band above the top row, so the panel
    // needs no legend.
    annotate(
      "typst", x: n-rows + 0.5, y: threshold - 0.5,
      label: note(fill: above-col, size: 7.5pt, weight: "bold")[Threshold #threshold],
      anchor: "south-west", clip: false,
    ),
    annotate(
      "typst", x: n-rows + 0.5, y: overall-median,
      label: note(size: 7.5pt)[row medians, #median-lo to #median-hi],
      anchor: "south-east", clip: false,
    ),
    // One callout in the empty tail, where the scores the scale was built for
    // would sit.
    annotate(
      "typst", x: n-rows, y: 11,
      label: box(width: 8.4cm)[
        #note(fill: above-col, size: 9pt, weight: "bold")[
          #n-above of #n-scored scored accounts (#pct-above) reach #threshold.
        ] \
        #note(size: 7.5pt)[
          The scale runs to #scale-max, and the whole archive stops at
          #archive-max.
        ]
      ],
      anchor: "north-west", clip: false,
    ),
  ),
  scales: scales(
    x: scale-continuous(
      breaks: range(n-rows).map(slot-of),
      // Two-line tick label: the column name as it appears in the data, then the
      // gloss, the number of accounts, and how many reach the threshold. Those
      // counts do the work of a legend.
      labels: rows.map(t => box(inset: (right: 3pt))[
        #set align(right)
        #set par(leading: 2pt)
        #col-name(t.col, 7.5pt) \
        #note(size: 6pt)[#t.gloss · n #t.n, #t.above at #sym.gt.eq #threshold]
      ]),
      limits: (0.4, n-rows + 0.85),
      expand: (0%, 0%),
    ),
    y: scale-continuous(
      breaks: (0, 5, 10, 15, 20, 25, 30),
      limits: (-0.8, scale-max),
      expand: (0%, 1%),
    ),
    fill: scale-manual(values: (below-col, above-col), limits: bands),
    // The accounts that clear the threshold are the point, so they run at full
    // strength while the bulk recedes.
    alpha: scale-manual(values: (0.6, 1), limits: bands),
  ),
  coord: coord-flip(),
  guides: guides(default: none),
  labels: labels(
    title: "Almost No Account Reaches the Greyson Threshold",
    // Under coord-flip the scale keys stay pre-flip, so `x` is the row axis and
    // `y` is the score axis.
    x: none,
    y: "Greyson NDE Scale Score",
    subtitle: [
      The Greyson NDE Scale counts an account as a near-death experience at
      #text(fill: above-col, weight: "bold")[#threshold points] out of #scale-max. \
      Across #n-scored scored accounts, all
      but #text(fill: above-col, weight: "bold")[#n-above of them] fall
      #text(fill: below-col.darken(15%), weight: "bold")[below that line], and
      no theme shifts the middle: every row's median sits between
      #median-lo and #median-hi. 
      Even #col-name(stretch.col, 8.5pt), whose
      tail clears the threshold most often, does so in only #stretch-pct of the
      accounts that carry it.
    ],
    caption: typst([
      #n-accounts accounts, #n-scored of them scored; one dot per account per theme, so an account carrying several themes appears in several rows and the rows are not a partition. \
      Score and themes are both derived from the narrative text, not from an interview. Rows run from #biggest.n accounts down to #thinnest.n, and #col-name(thinnest.col, 6.5pt) supports no inference. \
      Source: near-death experience archive (TidyTuesday 2026-07-21). Author: #link("https://mickael.canouil.fr")[Mickaël CANOUIL].
    ]),
  ),
  theme: theme-minimal(
    plot-title: element-text(font: body-font, size: 17pt, weight: "bold"),
    plot-subtitle: element-text(font: body-font, size: 8.5pt),
    plot-caption: element-text(font: body-font, size: 6.5pt),
    axis-title: element-text(font: body-font, size: 8.5pt),
    axis-text-y: element-text(font: mono-font, size: 7.5pt),
    panel-grid-major-x: element-blank(),
    panel-grid-minor: element-blank(),
  ),
  width: auto,
  height: auto,
)

A horizontal beeswarm chart with eight rows, one for each theme the near-death experience archive codes for, ordered from the commonest at the top to the rarest at the bottom: ai_clinical with 526 accounts, ai_obe with 336, ai_unity with 96, ai_esp with 90, ai_hellish with 46, ai_world_future with 27, ai_past_lives with 20, and ai_aliens with 5. Each row label gives the column name in a monospaced face, then a plain-English gloss, the number of accounts and how many reach 7. The horizontal axis is the Greyson NDE Scale score, running the full length of the scale from 0 to 32. One dot marks each account, swarmed vertically within its row. In every row the dots pile into narrow columns between 0 and 4 and then thin out fast, so the right two-thirds of the panel is almost empty. A short dark rule marks each row’s median; all eight sit between 2 and 3. A dashed crimson line just below 7 marks the threshold at which the scale counts an account as a near-death experience, and the few dots beyond it are drawn in crimson at full strength while the rest are held back in warm grey. A callout in the empty right-hand space states that 53 of 587 scored accounts, 9 percent, reach 7, that the scale runs to 32, and that the whole archive stops at 27.

A horizontal beeswarm chart with eight rows, one for each theme the near-death experience archive codes for, ordered from the commonest at the top to the rarest at the bottom: ai_clinical with 526 accounts, ai_obe with 336, ai_unity with 96, ai_esp with 90, ai_hellish with 46, ai_world_future with 27, ai_past_lives with 20, and ai_aliens with 5. Each row label gives the column name in a monospaced face, then a plain-English gloss, the number of accounts and how many reach 7. The horizontal axis is the Greyson NDE Scale score, running the full length of the scale from 0 to 32. One dot marks each account, swarmed vertically within its row. In every row the dots pile into narrow columns between 0 and 4 and then thin out fast, so the right two-thirds of the panel is almost empty. A short dark rule marks each row’s median; all eight sit between 2 and 3. A dashed crimson line just below 7 marks the threshold at which the scale counts an account as a near-death experience, and the few dots beyond it are drawn in crimson at full strength while the rest are held back in warm grey. A callout in the empty right-hand space states that 53 of 587 scored accounts, 9 percent, reach 7, that the scale runs to 32, and that the whole archive stops at 27.

Figure 1: Almost No Account Reaches the Greyson Threshold

Data: TidyTuesday 2026-07-21.

The Greyson NDE Scale scores an account out of 32 and treats 7 as the point at which it counts as a near-death experience, so plotting the archive against that threshold asks a plain question of it. The answer is that 53 of the 587 scored accounts reach 7, about one in eleven, and the whole archive stops at 27 without ever approaching the top of the scale. Splitting the accounts by the eight themes coded for them changes almost nothing: every row’s median falls between 2 and 3, and even ai_unity, whose tail clears the threshold most often, does so in only 17% of the accounts that carry it. Both the score and the theme flags here are derived from the narrative text rather than from an interview, so the figure says more about how the archive was coded than about what the experiences were.

Back to top