Standard Cohort Retention
Executive Summary

Executive Summary

Retention across 13 monthly cohorts of 260 customers

Customers
260
Cohorts
13
Month-1 Retention %
29.8
Best Cohort (M1)
2011-11 (46.7%)
Retention Trend
declining
Month-3 Retention %
27.4
Month-6 Retention %
23.8
Across 260 customers in 13 monthly cohorts (2010-12 to 2011-12), month-1 retention is 29.8% — of every 100 new customers, about 30 are still active a month after they arrive. By month 3 it is 27.4%. Retention is DECLINING: the most recent cohorts hold 26.6% at month 1 vs 29.1% for the earliest cohorts. The strongest cohort is 2011-11 (46.7% at month 1); the weakest is 2011-07 (10%).
What this means

The short answer

About 30 of every 100 new customers return in the month after their first purchase (29.8% month-1 retention), and by month 3 that drops to 27.4%. Newer cohorts are retaining worse than older ones: your most recent cohorts hold 26.6% at month 1 versus 29.1% for your earliest cohorts. The strongest performer is the November 2011 cohort (46.7% month-1 retention); the weakest is July 2011 (10%).

The detail

Across 260 customers in 13 monthly cohorts (2010-12 to 2011-12), month-1 retention averages 29.8%, month-3 reaches 27.4%, and month-6 settles at 23.8%. The trend is declining: comparing the first six cohorts (2010-12 to 2011-05) at month 1 yields 29.1%, while the most recent observable cohorts (2011-07 to 2011-11) average 26.6%. Cohort 2011-11 is the outlier at 46.7%; cohort 2011-07 is the lowest at 10%.

What this can't tell you

The November 2011 cohort's strong retention may reflect seasonal holiday purchasing or a small-sample artifact (15 customers); its persistence should be monitored across future months. July 2011's weakness (10 customers) is similarly thin and may not generalize.

Overview

Analysis Overview

Monthly cohort retention across 13 cohorts and 260 customers.

N Customers260
N Cohorts13
Retention Horizon Months12
What this means

The short answer

This analysis tracks 260 customers across 13 acquisition cohorts, measuring what fraction return in each subsequent month. By definition, every cohort starts at 100% in month 0 (first purchase). The method groups customers by their first-activity month and observes how many remain active at fixed intervals—a standard cohort retention analysis. Recent cohorts are excluded from later months because those months haven't occurred yet, not because they churned.

The detail

The dataset spans 13 cohorts with a 12-month observation window. Each cohort's retention is expressed as a percentage of its initial size. Month 0 is always 100%; the analysis measures the share of each cohort still active N months after first InvoiceDate. Right-censoring is handled by omitting unobserved future months from both the heatmap and the average retention curve, not by counting them as zero. The trend verdict compares month 1 retention between the earliest half and most recent half of cohorts: retention is declining.

What this can't tell you

The method does not distinguish between customers who never returned and those whose data is simply not yet recorded. Cohorts acquired near the end of the observation window have fewer observable months by design, limiting the precision of long-term retention estimates for recent acquisitions.

Data Preparation

Data Quality

Rows, customers, date coverage, and cohort formation.

Initial Rows21608
Final Rows21608
Rows Removed0
N Customers260
N Cohorts13
What this means

The short answer

No data was lost in cleaning. All 21,608 rows had valid invoice dates and customer IDs; zero rows were removed. Your dataset spans the full 13-month period from December 2010 to December 2011 and captures 260 unique customers forming 13 monthly acquisition cohorts.

The detail

Initial rows: 21,608; final rows: 21,608; rows removed: 0. Date coverage: 2010-12-01 to 2011-12-09. All activity rows were retained because every row carried a readable InvoiceDate and a non-blank CustomerID. This clean handoff means no systematic bias was introduced by missing or malformed data.

What this can't tell you

The export does not indicate whether rows represent individual orders or aggregated customer activity. A transaction-level export (one row per order, not per customer) would clarify whether the retention metric is based on order frequency or customer presence, which can move differently.

Visualization

Retention by Cohort

Percentage of each cohort still active N months after first activity (observable cells only).

What this means

The short answer

Month-to-month retention drops sharply after first purchase: on average, 70.2% of customers do not return in month 1, leaving about 30% of each cohort active. By month 6, the average cohort has stabilized around a loyal core of 23.8%. The 2010-12 cohort shows the most resilience early on, while 2011-11 leads at month 1 with 46.7%—though cohort size varies and affects interpretation.

The detail

The retention matrix shows each row as an acquisition cohort and each column as months elapsed since first activity. M0 is 100% by definition. The steepest loss occurs between month 0 and month 1: on average 70.2% do not return. By month 6, the average cohort retains 23.8% of customers. Comparing rows at the same column (e.g., month 1) ranks cohorts fairly: 2011-11 leads at 46.7%, while 2011-07 trails at 10%. The lower-right triangle is blank because recent cohorts have not yet reached those months—this is right-censoring, not zero retention. After month 1, decay patterns vary: some cohorts stabilize, others continue declining.

What this can't tell you

Small cohort sizes introduce noise into individual retention percentages. For example, 2011-07 has only 10 customers, making its 10% month 1 retention (1 customer) subject to high variance. A transaction-level export would help separate true churn from seasonal or sporadic purchase patterns.

Visualization

Average Retention Curve

Weighted average retention by months since first activity, across cohorts old enough to observe each month.

What this means

The short answer

The typical customer lifecycle is front-loaded: a steep drop from 100% to 29.8% in month 1, then a gentle slide to a stable core around 23–28% by month 6. The curve does not flatten dramatically, suggesting continuous low-level churn rather than a durable long-term base. Month 5 and months 9–11 show upticks (35–36%), but the overall pattern is downward.

The detail

Each bar averages only the cohorts old enough to observe that month, so recent cohorts never artificially depress the tail. M0 is 100% by definition; M1 drops to 29.8% (the largest single-month loss). M2–M4 drift slightly downward to 26.5% by M4. M5 rebounds to 35.9%, then M6 falls to 23.8% (the lowest observed). Months 7–12 oscillate between 23.8% and 36.4%, with M9 (36.4%), M10 (33.9%), and M11 (35.1%) slightly elevated and M12 returning to 28.1%.

What this can't tell you

The oscillations in months 5–12 lack confidence intervals in the supplied data, so it is unclear whether they reflect true seasonal variation or sampling noise. A month-by-month statistical test would be needed to confirm whether these mid-to-late-year bumps are real or artifact.

Visualization

New Customers per Cohort

Acquisition volume: unique new customers by first-activity month.

What this means

The short answer

Your acquisition volume is shrinking: the oldest cohort (December 2010) brought 57 new customers, while the most recent (December 2011) brought only 2. The average is 20 new customers per month. This shrinking feed matters because retention percentages elsewhere are relative to these sizes—a small cohort with high retention contributes less volume than a large one with average retention.

The detail

Cohort sizes range from 57 (2010-12) to 2 (2011-12). The first half averages 25 customers per cohort (2010-12 through 2011-06: 57, 37, 21, 25, 17, 12, 16); the second half averages 14 (2011-07 through 2011-12: 10, 9, 22, 17, 15, 2). December 2011 is too recent to have observable month-1 retention yet; its size of 2 means any future retention rate will be highly volatile.

What this can't tell you

The sharp drop in recent months (December 2011: 2 customers) could indicate the data export was truncated mid-month, a true decline in acquisition, or a seasonal dip. A note on the export date relative to December 2011 would clarify whether this is a data artifact or a real business signal.

Data Table

Cohort Comparison

Every cohort's size and month-1/3/6 retention side by side; blank cells are months the cohort has not reached yet.

CohortSizeM1 Retention PCTM3 Retention PCTM6 Retention PCT
2010-125742.133.333.3
2011-01372724.313.5
2011-022114.323.833.3
2011-0325201624
2011-041729.423.511.8
2011-051241.733.333.3
2011-061631.2256.2
2011-07101020
2011-08922.244.4
2011-092231.831.8
2011-101717.6
2011-111546.7
2011-122
What this means

The short answer

Month 1 retention varies widely across cohorts: 2011-11 retains 46.7% of customers, while 2011-07 retains only 10%. When comparing early cohorts (2010-12 through 2011-05) against recent ones (2011-06 through 2011-10), the recent cohorts average 26.6% month 1 retention versus 29.1% for early cohorts—a modest downward trend. By month 6, only 7 cohorts have data, and retention ranges from 6.2% to 33.3%.

The detail

Of 13 cohorts, 12 have reached month 1, 10 have reached month 3, and 7 have reached month 6; blank cells indicate months not yet observed. At month 1, 2011-11 leads at 46.7% and 2011-07 trails at 10%. The month 1 column shows early cohorts averaging 29.1% retention and recent cohorts averaging 26.6% retention. At month 3, performance clusters between 16% and 44.4%, with 2011-08 at 44.4% (9 customers). At month 6, observable cohorts range from 6.2% to 33.3%; 2011-02 and 2011-05 both reach 33.3%. Cohort size ranges from 2 to 57 customers.

What this can't tell you

Small cohorts (e.g., 2011-07 with 10 customers, 2011-12 with 2) produce volatile retention percentages that may not generalize. The trend toward lower month 1 retention in recent cohorts is real but modest; a longer observation window would clarify whether this reflects a genuine shift in customer quality or natural sampling variation.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

Cohort Retention Analysis — How Well Do You Keep Your Customers?

Builds monthly acquisition cohorts from raw activity/order data (one row per order/event), computes each cohort's month-by-month retention, and determines whether newer cohorts retain better or worse than older ones.

Why This Method?

Cohort retention separates growth from stickiness: total actives can rise while every cohort leaks. Grouping customers by first-activity month and tracking each group's survival is the standard, right-censoring-aware way to see whether the product actually keeps the customers it acquires.

What This Analysis Covers

  • Retention heatmap: every cohort x months-since-acquisition cell
  • Average retention curve (only cohorts old enough to observe each month)
  • Cohort sizes (acquisition volume per month)
  • Cohort comparison table with month-1/3/6 retention and a trend verdict

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {customer_id, activity_date}. All narrative is derived from the user's own column names and computed values.

suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))

Core Analysis Pipeline

compute_shared <- function(df, params, col_map = list()) {
  # === SHARED EXPORTS ===
  #   initial_rows/final_rows/rows_removed  $ row accounting
  #   cust_h / date_h        $ humanized user column names
  #   n_customers            $ unique customers analysed
  #   cohort_labels          $ character — "YYYY-MM" per cohort (chronological)
  #   cohort_sizes_vec       $ integer — new customers per cohort
  #   n_excluded_cohorts     $ cohorts older than the 24-cohort cap
  #   date_min / date_max    $ Date — observed activity range
  #   matrix_df              $ data.frame(cohort, month_number, retention_pct) — observable cells only
  #   curve_df               $ data.frame(month_label, avg_retention_pct) — right-censoring-aware
  #   sizes_df               $ data.frame(cohort, new_customers)
  #   comparison_df          $ data.frame(cohort, size, m1/m3/m6_retention_pct) — NA when unobservable
  #   m1_overall/m3_overall/m6_overall $ weighted avg retention (NA if unobservable)
  #   m1_by_cohort           $ numeric — per-cohort M1 retention (NA if unobservable)
  #   best_cohort/worst_cohort + _m1   $ best/worst cohort at month 1
  #   trend_word/trend_first/trend_second/trend_n $ first-half vs second-half M1 verdict
  #   metrics / json_output
  # === /SHARED EXPORTS ===

  MAX_MONTHS  <- 12L   # retention horizon: M0..M12
  MAX_COHORTS <- 24L   # keep the most recent 24 cohorts

  initial_rows <- nrow(df)
  cust_h <- humanize_semantic("customer_id", col_map)
  date_h <- humanize_semantic("activity_date", col_map)

Step 1: Validate mapping and parse dates

if (!("customer_id" %in% names(df)) || !("activity_date" %in% names(df))) {
    stop(sprintf("Cohort retention needs both a customer column(%s) and an activity date column(%s) mapped.",
                 cust_h, date_h))
  }
  cust <- trimws(as.character(df$customer_id))
  raw_dates <- df$activity_date
  dts <- parse_activity_dates(raw_dates)
  n_nonblank <- sum(!is.na(raw_dates) & trimws(as.character(raw_dates)) != "")
  if (n_nonblank == 0 || sum(!is.na(dts)) < 0.95 * n_nonblank) {
    stop(sprintf("The %s column could not be read as dates — expected values like 2024-01-31 or 1/31/2024.",
                 date_h))
  }

  keep <- !is.na(dts) & !is.na(cust) & cust != ""
  cust <- cust[keep]
  dts  <- dts[keep]
  rows_bad <- initial_rows - sum(keep)
  if (length(dts) < 2) {
    stop(sprintf("Too few usable rows after cleaning — check %s and %s for blanks.", cust_h, date_h))
  }

Step 2: Assign cohorts (first-activity month per customer)

act_idx <- month_index(dts)
  cohort_by_cust <- tapply(act_idx, cust, min)   # named: customer -> cohort month index
  all_cohorts <- sort(unique(as.integer(cohort_by_cust)))

Cap at the MAX_COHORTS most recent cohorts

n_excluded_cohorts <- 0L
  if (length(all_cohorts) > MAX_COHORTS) {
    n_excluded_cohorts <- length(all_cohorts) - MAX_COHORTS
    all_cohorts <- tail(all_cohorts, MAX_COHORTS)
  }
  cust_cohort <- as.integer(cohort_by_cust[cust])  # per-row cohort of the row's customer
  in_scope <- cust_cohort %in% all_cohorts
  cust <- cust[in_scope]; dts <- dts[in_scope]
  act_idx <- act_idx[in_scope]; cust_cohort <- cust_cohort[in_scope]

  final_rows <- length(cust)
  rows_removed <- initial_rows - final_rows
  max_idx <- max(act_idx)
  date_min <- min(dts); date_max <- max(dts)

Step 3: Guards — need at least 2 cohorts and 2 observable months

if (length(all_cohorts) < 2) {
    stop(sprintf("Cohort retention needs customers arriving in at least two different calendar months — every %s in this data first appears in %s. A longer %s range is required.",
                 cust_h, month_label(all_cohorts[1]), date_h))
  }
  if (max_idx - min(all_cohorts) < 1) {
    stop(sprintf("The %s column spans a single calendar month — at least two months of activity are needed to measure retention.",
                 date_h))
  }

  cohort_labels <- month_label(all_cohorts)

Cohort sizes: distinct customers per cohort (recomputed on in-scope rows — a customer belongs to exactly one cohort, so filtering is customer-complete)

cust_first <- tapply(act_idx, cust, min)
  cohort_sizes_vec <- as.integer(sapply(all_cohorts, function(cc)
    sum(as.integer(cust_first) == cc)))
  n_customers <- length(cust_first)

Step 4: Retention matrix — distinct customers active per cohort x month

month_number <- act_idx - cust_cohort
  um <- unique(data.frame(cohort = cust_cohort, m = month_number, id = cust,
                          stringsAsFactors = FALSE))
  k <- length(all_cohorts)
  active_mat <- matrix(0L, nrow = k, ncol = MAX_MONTHS + 1L)  # counts, cols = M0..M12
  obs_mat    <- matrix(FALSE, nrow = k, ncol = MAX_MONTHS + 1L)
  for (i in seq_len(k)) {
    cc <- all_cohorts[i]
    max_obs_m <- min(MAX_MONTHS, max_idx - cc)   # right-censoring boundary
    for (m in 0:max_obs_m) {
      obs_mat[i, m + 1L] <- TRUE
      active_mat[i, m + 1L] <- sum(um$cohort == cc & um$m == m)
    }
  }
  ret_mat <- 100 * sweep(active_mat, 1, cohort_sizes_vec, "/")   # row-wise: cell / cohort size

Long format, observable cells only (no future NAs)

cells <- which(obs_mat, arr.ind = TRUE)
  cells <- cells[order(cells[, 1], cells[, 2]), , drop = FALSE]
  matrix_df <- data.frame(
    cohort = cohort_labels[cells[, 1]],
    month_number = paste0("M", cells[, 2] - 1L),
    retention_pct = round(ret_mat[cells], 1),
    stringsAsFactors = FALSE
  )
  rownames(matrix_df) <- NULL

Step 5: Aggregate curve — weighted avg over cohorts old enough (right-censoring aware)

max_m <- min(MAX_MONTHS, max_idx - min(all_cohorts))
  curve_vals <- sapply(0:max_m, function(m) {
    elig <- which(obs_mat[, m + 1L])             # cohorts that have lived m months
    100 * sum(active_mat[elig, m + 1L]) / sum(cohort_sizes_vec[elig])
  })
  curve_df <- data.frame(
    month_label = paste0("M", 0:max_m),
    avg_retention_pct = round(curve_vals, 1),
    stringsAsFactors = FALSE
  )

  sizes_df <- data.frame(cohort = cohort_labels,
                         new_customers = cohort_sizes_vec,
                         stringsAsFactors = FALSE)

Step 6: Headline metrics — M1/M3/M6, best/worst cohort, trend

ret_at <- function(m) if (max_m >= m) round(curve_vals[m + 1L], 1) else NA_real_
  m1_overall <- ret_at(1); m3_overall <- ret_at(3); m6_overall <- ret_at(6)

  cohort_ret_at <- function(m) {
    v <- rep(NA_real_, k)
    obs <- obs_mat[, m + 1L]
    v[obs] <- round(ret_mat[obs, m + 1L], 1)
    v
  }
  m1_by_cohort <- cohort_ret_at(1)
  m3_by_cohort <- cohort_ret_at(3)
  m6_by_cohort <- cohort_ret_at(6)

NEVER which.max over possibly-all-NA vectors — filter NA indices first

obs1 <- which(!is.na(m1_by_cohort))
  best_cohort <- worst_cohort <- NA_character_
  best_cohort_m1 <- worst_cohort_m1 <- NA_real_
  if (length(obs1) > 0) {
    bi <- obs1[which.max(m1_by_cohort[obs1])]
    wi <- obs1[which.min(m1_by_cohort[obs1])]
    best_cohort <- cohort_labels[bi];  best_cohort_m1 <- m1_by_cohort[bi]
    worst_cohort <- cohort_labels[wi]; worst_cohort_m1 <- m1_by_cohort[wi]
  }

Trend: first-half vs second-half cohorts' month-1 retention

trend_word <- "not assessable"
  trend_first <- trend_second <- NA_real_
  trend_n <- length(obs1)
  if (trend_n >= 4) {
    m1s <- m1_by_cohort[obs1]                     # chronological (cohorts sorted)
    half <- floor(trend_n / 2)
    trend_first  <- round(mean(head(m1s, half)), 1)
    trend_second <- round(mean(tail(m1s, half)), 1)
    d <- trend_second - trend_first
    trend_word <- if (d >= 2) "improving" else if (d <= -2) "declining" else "stable"
  }

  comparison_df <- data.frame(
    cohort = cohort_labels,
    size = cohort_sizes_vec,
    m1_retention_pct = m1_by_cohort,
    m3_retention_pct = m3_by_cohort,
    m6_retention_pct = m6_by_cohort,
    stringsAsFactors = FALSE
  )

  metrics <- list(
    `Customers`            = n_customers,
    `Cohorts`              = k,
    `Month-1 Retention %`  = m1_overall,
    `Best Cohort(M1)`     = if (!is.na(best_cohort)) sprintf("%s(%.1f%%)", best_cohort, best_cohort_m1) else "n/a",
    `Retention Trend`      = trend_word
  )
  if (!is.na(m3_overall)) metrics$`Month-3 Retention %` <- m3_overall
  if (!is.na(m6_overall)) metrics$`Month-6 Retention %` <- m6_overall

  trend_phrase <- switch(trend_word,
    improving = sprintf("newer cohorts are retaining BETTER(month-1: %.1f%% recent vs %.1f%% early)", trend_second, trend_first),
    declining = sprintf("newer cohorts are retaining WORSE(month-1: %.1f%% recent vs %.1f%% early)", trend_second, trend_first),
    stable    = sprintf("retention is stable across cohorts(month-1: %.1f%% recent vs %.1f%% early)", trend_second, trend_first),
    "too few cohorts to assess a trend")

  json_output <- list(
    answer = paste0(
      "Monthly cohort retention on ", format(n_customers, big.mark = ","),
      " customers across ", k, " cohorts(", cohort_labels[1], " to ",
      cohort_labels[k], "): month-1 retention is ",
      if (!is.na(m1_overall)) paste0(m1_overall, "%") else "unobservable",
      if (!is.na(m3_overall)) paste0(", month-3 is ", m3_overall, "%") else "",
      "; ", trend_phrase, ". Best cohort at month 1: ",
      if (!is.na(best_cohort)) paste0(best_cohort, " (", best_cohort_m1, "%)") else "n/a", "."
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "retention_heatmap",
        "retention_curve", "cohort_sizes", "cohort_comparison"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows,
    rows_removed = rows_removed, rows_bad = rows_bad,
    cust_h = cust_h, date_h = date_h,
    n_customers = n_customers,
    cohort_labels = cohort_labels, cohort_sizes_vec = cohort_sizes_vec,
    n_excluded_cohorts = n_excluded_cohorts,
    date_min = date_min, date_max = date_max, max_m = max_m,
    matrix_df = matrix_df, curve_df = curve_df, sizes_df = sizes_df,
    comparison_df = comparison_df,
    m1_overall = m1_overall, m3_overall = m3_overall, m6_overall = m6_overall,
    m1_by_cohort = m1_by_cohort,
    best_cohort = best_cohort, best_cohort_m1 = best_cohort_m1,
    worst_cohort = worst_cohort, worst_cohort_m1 = worst_cohort_m1,
    trend_word = trend_word, trend_first = trend_first,
    trend_second = trend_second, trend_n = trend_n,
    metrics = metrics, json_output = json_output
  )
}

Only cohorts with NO observable month-1 cell may be called "too recent" — never a cohort that has reached month 1.

no_m1 <- shared$cohort_labels[is.na(shared$m1_by_cohort)]
  censor_note <- if (length(no_m1) > 0) {
    paste0(" ", paste(no_m1, collapse = ", "),
           if (length(no_m1) == 1) " is" else " are",
           " too recent to have observable month-1 retention yet.")
  } else ""
  list(
    title = "New Customers per Cohort",
    description = "Acquisition volume: unique new customers by first-activity month.",
    text = paste0(
      "Cohort intake ranges from ", format(min(sz), big.mark = ","), " (",
      shared$cohort_labels[si], ") to ", format(max(sz), big.mark = ","), " (",
      shared$cohort_labels[bi], "), averaging ", format(round(mean(sz)), big.mark = ","),
      " new customers per month. Comparing the first and last cohorts(",
      format(first_v, big.mark = ","), " vs ", format(last_v, big.mark = ","),
      "), acquisition is ", acq_word, ". Retention percentages elsewhere in this report are relative ",
      "to these sizes — a small cohort with high retention can still matter less than a large one ",
      "with average retention.", censor_note
    ),
    chart_labels = list(
      cohort = "Acquisition cohort(first-activity month)",
      new_customers = "New customers"
    ),
    data = list(cohort_sizes = shared$sizes_df)
  )
}

# Card: cohort_comparison (table)
card_cohort_comparison <- function(shared, df, params) {
  n_m3 <- sum(!is.na(shared$comparison_df$m3_retention_pct))
  n_m6 <- sum(!is.na(shared$comparison_df$m6_retention_pct))
  list(
    title = "Cohort Comparison",
    description = "Every cohort&#x27;s size and month-1/3/6 retention side by side; blank cells are months the cohort has not reached yet.",
    text = paste0(
      "Of ", length(shared$cohort_labels), " cohorts, ", sum(!is.na(shared$m1_by_cohort)),
      " have reached month 1, ", n_m3, " month 3, and ", n_m6, " month 6 — blank cells mean the cohort ",
      "is too recent to observe that month, not that retention is zero. ",
      if (!is.na(shared$best_cohort)) paste0("At month 1, ", shared$best_cohort, " leads(",
        shared$best_cohort_m1, "%) and ", shared$worst_cohort, " trails(", shared$worst_cohort_m1,
        "%). ") else "",
      switch(shared$trend_word,
        improving = paste0("Reading down the month-1 column, newer cohorts are clearly retaining better(",
                           shared$trend_second, "% recent vs ", shared$trend_first, "% early)."),
        declining = paste0("Reading down the month-1 column, newer cohorts are retaining worse(",
                           shared$trend_second, "% recent vs ", shared$trend_first, "% early)."),
        stable    = "Month-1 retention is broadly stable from the earliest to the latest cohorts.",
        "Too few cohorts have observable month-1 retention to compare halves.")
    ),
    data = list(cohort_comparison = shared$comparison_df)
  )
}
Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing