Standard Market Basket
Executive Summary

Executive Summary

What sells together across 800 orders.

Orders
800
Distinct Items
15
Avg Basket Size
3.26
Qualifying Pairs
101
Top Pairing
Coffee + Milk
Top Pairing Lift
6.67
Across 800 orders with an average basket of 3.3 items, the strongest product pairing is Coffee and Milk: shoppers who buy Coffee also buy Milk 100% of the time, a lift of 6.67 — they land in the same basket 6.67 times more often than if the two were bought independently. The most-bought Product overall is Reusable Bag, in 100% of orders. These are co-purchase patterns to act on for cross-sell, bundling, and layout; they show what sells together, not proof that one item drives the other.
Interpretation

The strongest product pairing is Coffee + Milk: shoppers who buy Coffee also buy Milk 100% of the time, with a lift of 6.67—they appear together 6.67 times more often than chance predicts. This pair shows up in 15% of the 800 orders. The single most-bought item is Reusable Bag, present in 100% of orders, followed by Bread at 30%. These co-purchase patterns are actionable for cross-sell prompts, bundling offers, and shelf placement; they reveal what sells together, not proof that one item drives demand for the other.

Overview

Analysis Overview

Market basket analysis of 800 orders and 15 distinct items.

N Orders800
N Items15
N Pairs101
Interpretation

Market basket analysis identifies which products land in the same order, measured by three metrics. Support is the share of all orders containing both items—how common the pairing is. Confidence is directional: of orders with the first item, the percentage that also contain the second. Lift compares observed co-occurrence to chance: a lift of 1 means independence; above 1 means they sell together more than random; below 1 means less. Coffee + Milk, for example, has a lift of 6.67, meaning they co-occur 6.67 times more often than if purchases were independent. To act: bundle high-lift pairs, use high-confidence rules for "frequently bought together" prompts, or place associated items near each other in layout. These patterns show co-purchase behavior, not causation.

Data Preparation

Data Quality

Basket reconstruction, deduplication, and item lumping.

Initial Rows2611
Final Rows2611
Rows Removed0
N Orders800
Avg Basket Size3.26
Interpretation

The dataset loaded 2,611 item rows that grouped into 800 orders with 15 distinct products. No rows were removed; all 2,611 were retained. The average basket contained 3.26 items, calculated by counting each product once per order even if bought multiple times. This clean reconstruction ensures pairing analysis reflects genuine co-purchase patterns without duplication or missing data, establishing a reliable foundation for lift and confidence calculations across the 101 qualifying product pairs.

Visualization

Most-Bought Items

Share of orders containing each Product (top 15).

Interpretation

Reusable Bag dominates, appearing in 100% of orders, followed by Bread at 30%, Butter at 25%, and Yogurt at 16.1%. The remaining items—Coffee, Milk, Sugar, Jam, Apple, Cereal, Cheese, Tea, Eggs, and Juice—each appear in 13.2% to 15% of orders. This frequency distribution sets the baseline for lift: a pairing of two already-common items must show high raw co-occurrence to beat chance, while two moderately common items that consistently land together signal a strong association. Bread's 30% prevalence makes it a natural candidate for bundling with lower-frequency items like Apple (13.9%) or Tea (13.6%).

Data Table

Top Product Pairs by Lift

Product pairs ranked by lift, with support and confidence.

Item AItem BSupport PCTConfidence PCTLift
CoffeeMilk151006.67
ButterBread18.75752.5
CheeseJam3.3824.81.75
BananaYogurt3.75251.55
JamCoffee2.8820.41.36
JamMilk2.8820.41.36
EggsSugar2.6219.81.34
AppleBread5.539.61.32
JuiceButter4.38331.32
EggsTea2.3817.91.32
TeaBread5.3839.41.31
CheeseYogurt2.8821.11.31
CheeseBanana2.6219.31.28
CerealCoffee2.6219.11.27
CerealMilk2.6219.11.27
Interpretation

The top 15 product pairs by lift reveal two standouts: Coffee + Milk at 6.67 and Butter + Bread at 2.5. Coffee + Milk appears in 15% of orders with 100% confidence—every Coffee buyer also takes Milk. Butter + Bread lands in 18.75% of orders with 75% confidence: three-quarters of Butter buyers also buy Bread. The next tier—Cheese + Jam, Banana + Yogurt, Jam + Coffee/Milk—all lift between 1.75 and 1.36 but on much smaller support (2.88% to 3.75%), making them less robust for bundling. Reading lift and support together prevents over-relying on fragile pairings: high lift on low support is brittle, while moderate lift on solid support is actionable.

Data Table

Cross-Sell Recommendations

Buy-this-recommend-that rules ranked by confidence.

If They BuyRecommendConfidence PCTLift
CoffeeMilk1006.67
MilkCoffee1006.67
ButterBread752.5
BreadButter62.52.5
AppleBread39.61.32
TeaBread39.41.31
JuiceBread37.71.26
CerealBread33.61.12
JuiceButter331.32
SugarBread32.21.07
EggsBread32.11.07
JamBread311.03
AppleButter30.61.23
SugarButter29.71.19
EggsButter29.21.17
Interpretation

Coffee → Milk and Milk → Coffee both show 100% confidence and 6.67 lift—the strongest directional recommendation for a "frequently bought together" prompt or bundle. Butter → Bread follows at 75% confidence and 2.5 lift, with the reverse (Bread → Butter) at 62.5% confidence and the same 2.5 lift. Apple, Tea, and Juice buyers show 39.6%, 39.4%, and 37.7% confidence respectively for Bread, all with modest lift (1.26–1.32). These rules identify which items to highlight when a customer adds the first product to their cart. The highest-confidence rules—Coffee/Milk and Butter/Bread—warrant front-and-center prompts; lower-confidence rules work better as secondary suggestions or shelf adjacencies.

Methodology

Methodology

Statistical methodology and diagnostics for Market Basket — What Sells Together

Statistical Method

Market Basket — What Sells Together

Standard-library analysis: which products are bought together? Give it a transaction log — one row per item in each order — and it mines the classic retail associations: the most-bought items, every product pair ranked by lift (how much more often they co-occur than chance), and the directional cross-sell rules (buy A, recommend B) with their confidence. Built for cross-sell, bundling, and store or menu layout. Works on any dataset: map an order/basket ID column and an item column.

Data
N = 2611 observations
Assumptions
  • Each row is one item in one order; the order ID groups items into a basket
  • A product counted more than once in the same order still counts once toward that basket
  • Co-occurrence is association, not causation — a promotion or placement can create a pairing
Limitations
  • Pairwise only — it finds pairs, not three-or-more-item combinations
  • Items beyond the top 40 by frequency are folded into Other and excluded from pair mining
  • A pairing can be an artifact of a bundle, promotion, or shelf placement rather than natural affinity
Software & Citation
MCP Analytics · mcpanalytics.ai
Code Appendix

Analysis Code

Complete R source code for this analysis

Market Basket — What Sells Together

Mines classic retail associations from a transaction log (one row per item in each order): the most-bought items, every product pair ranked by lift with support and confidence, and the directional cross-sell rules for bundling and layout.

Why This Method?

Pairwise co-occurrence is the interpretable heart of market basket analysis. Support says how common a combination is, confidence says how reliably one item follows another, and lift says whether that pairing beats chance. Base R counting recovers all three exactly — no arules dependency — and the pair is the unit retailers actually act on.

What This Analysis Covers

  • The most-bought items (share of orders containing each)
  • Product pairs ranked by lift, with support and confidence
  • Directional cross-sell rules: buy this, recommend that

Standard Library

Platform standard-library module (LAT-1441): runs on ANY transaction log via the semantic mapping {order_id, item}. 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

Step 1: Discover the two mapped columns

initial_rows <- nrow(df)
  for (k in c("order_id", "item")) {
    if (!k %in% names(df)) {
      stop(sprintf("column_mapping must map &#x27;%s' to a column in the transaction log.", k))
    }
  }
  name_order <- humanize_semantic("order_id", col_map)
  name_item  <- humanize_semantic("item", col_map)

Step 2: Clean — drop blank order/item rows, coerce to character

oid <- trimws(as.character(df[["order_id"]]))
  itm <- trimws(as.character(df[["item"]]))
  bad <- is.na(oid) | oid == "" | is.na(itm) | itm == ""
  n_bad_rows <- sum(bad)
  oid <- oid[!bad]; itm <- itm[!bad]
  final_rows <- length(oid)
  rows_removed <- initial_rows - final_rows
  if (final_rows < 1) {
    stop(sprintf("No usable rows: every row was missing an %s or an %s.",
                 name_order, name_item))
  }

Step 3: Reconstruct baskets — unique items per order, drop empties

basket_list <- split(itm, oid)
  basket_list <- lapply(basket_list, unique)
  basket_list <- basket_list[lengths(basket_list) >= 1]
  n_orders <- length(basket_list)
  if (n_orders < 10) {
    stop(sprintf("Only %s reconstructed from %s — market basket analysis needs at least 10 orders.",
                 plural(n_orders, "basket"), name_order))
  }

Step 4: Item frequencies (baskets containing each item)

all_items <- unlist(basket_list, use.names = FALSE)   # each item once per basket (deduped)
  item_freq0 <- sort(table(all_items), decreasing = TRUE)
  n_distinct_items <- length(item_freq0)                # original distinct count
  if (n_distinct_items < 2) {
    stop(sprintf("Only one distinct %s appears across the orders — pairing needs at least two distinct items.",
                 name_item))
  }

Step 5: Lump items beyond the top 40 into "Other" for pair-mining sanity

The 40 most frequent items stay as themselves; everything rarer is folded into a single "Other" bucket that is REPORTED but held out of pair mining (a mixed bucket would pair with everything and mean nothing).

MAX_ITEMS <- 40
  keep_items <- names(item_freq0)[seq_len(min(MAX_ITEMS, n_distinct_items))]
  lumped_items <- setdiff(names(item_freq0), keep_items)
  n_lumped <- length(lumped_items)
  if (n_lumped > 0) {
    basket_list <- lapply(basket_list, function(b) {
      b[b %in% lumped_items] <- "Other"
      unique(b)
    })
  }

Frequencies + average basket recomputed AFTER lumping.

all_items2 <- unlist(basket_list, use.names = FALSE)
  item_freq <- sort(table(all_items2), decreasing = TRUE)
  avg_basket <- mean(lengths(basket_list))

  item_freq_df <- data.frame(
    item = names(item_freq),
    orders_pct = round(100 * as.integer(item_freq) / n_orders, 1),
    stringsAsFactors = FALSE
  )
  top_items_df <- head(item_freq_df, 15)

Most-bought real item (skip the synthetic "Other" bucket if it leads).

real_freq_df <- item_freq_df[item_freq_df$item != "Other", , drop = FALSE]
  top_item <- real_freq_df$item[1]
  top_item_pct <- real_freq_df$orders_pct[1]

Step 6a: The pairable universe — top-40 real items, "Other" excluded

pairable <- setdiff(names(item_freq), "Other")
  pairable <- pairable[seq_len(min(MAX_ITEMS, length(pairable)))]
  k <- length(pairable)

Step 6b: Co-occurrence via a basket-by-item incidence matrix

M <- matrix(0L, nrow = n_orders, ncol = k, dimnames = list(NULL, pairable))
  for (o in seq_len(n_orders)) {
    hit <- basket_list[[o]][basket_list[[o]] %in% pairable]
    if (length(hit)) M[o, hit] <- 1L
  }
  Co <- crossprod(M)            # k x k co-occurrence; diagonal = item basket count
  cnt <- diag(Co)               # baskets containing each pairable item

Support floor: at least 2 baskets, and at least 0.5% of orders.

min_support_orders <- max(2L, as.integer(ceiling(0.005 * n_orders)))

Step 7: Score every qualifying unordered pair

a_vec <- character(0); b_vec <- character(0); co_vec <- integer(0)
  if (k >= 2) {
    for (i in seq_len(k - 1)) {
      for (j in (i + 1):k) {
        co_ij <- Co[i, j]
        if (co_ij >= min_support_orders) {
          a_vec <- c(a_vec, pairable[i])
          b_vec <- c(b_vec, pairable[j])
          co_vec <- c(co_vec, co_ij)
        }
      }
    }
  }
  n_qual <- length(co_vec)
  no_assoc <- n_qual == 0

  empty_rules <- data.frame(item_a = character(0), item_b = character(0),
                            support_pct = numeric(0), confidence_pct = numeric(0),
                            lift = numeric(0), stringsAsFactors = FALSE)
  empty_cross <- data.frame(if_they_buy = character(0), recommend = character(0),
                            confidence_pct = numeric(0), lift = numeric(0),
                            stringsAsFactors = FALSE)

  if (no_assoc) {
    pairs_all <- NULL
    rules_df <- empty_rules
    cross_sell_df <- empty_cross
    top_pair <- NULL
  } else {
    cnt_a <- as.integer(cnt[a_vec]); cnt_b <- as.integer(cnt[b_vec])
    support <- co_vec / n_orders
    conf_ab <- co_vec / cnt_a       # P(item_b | item_a)
    conf_ba <- co_vec / cnt_b       # P(item_a | item_b)
    lift    <- (co_vec * n_orders) / (cnt_a * cnt_b)

Orient each undirected pair so item_a is the stronger antecedent.

swap <- conf_ba > conf_ab
    ia <- ifelse(swap, b_vec, a_vec)
    ib <- ifelse(swap, a_vec, b_vec)
    conf_dir <- pmax(conf_ab, conf_ba)

    pairs_all <- data.frame(
      item_a = ia, item_b = ib,
      support = support,
      support_pct = round(100 * support, 2),
      confidence = conf_dir,
      confidence_pct = round(100 * conf_dir, 1),
      lift = round(lift, 2),
      co = as.integer(co_vec),
      stringsAsFactors = FALSE
    )

Rank by lift, breaking ties by support so common, reliable pairs win.

pairs_all <- pairs_all[order(-pairs_all$lift, -pairs_all$support), , drop = FALSE]
    rownames(pairs_all) <- NULL
    rules_df <- head(pairs_all[, c("item_a", "item_b", "support_pct",
                                   "confidence_pct", "lift")], 15)
    rownames(rules_df) <- NULL

Strongest actionable rule: highest lift among pairs clearing support.

top_pair <- pairs_all[1, , drop = FALSE]

Directional rules for cross-sell — both directions of each pair, kept only when lift > 1 (a genuine positive association), ranked by confidence. This drops "recommend the item that's in every basket".

dir_df <- data.frame(
      if_they_buy = c(a_vec, b_vec),
      recommend   = c(b_vec, a_vec),
      confidence_pct = round(100 * c(conf_ab, conf_ba), 1),
      lift = round(c(lift, lift), 2),
      stringsAsFactors = FALSE
    )
    dir_df <- dir_df[dir_df$lift > 1, , drop = FALSE]
    if (nrow(dir_df) > 0) {
      dir_df <- dir_df[order(-dir_df$confidence_pct, -dir_df$lift), , drop = FALSE]
      cross_sell_df <- head(dir_df, 15)
    } else {
      cross_sell_df <- empty_cross
    }
    rownames(cross_sell_df) <- NULL
  }

Step 8: Metrics + json answer

metrics <- list(
    `Orders`            = as.integer(n_orders),
    `Distinct Items`    = as.integer(n_distinct_items),
    `Avg Basket Size`   = round(avg_basket, 2),
    `Qualifying Pairs`  = as.integer(n_qual),
    `Top Pairing`       = if (no_assoc) "none" else paste0(top_pair$item_a, " + ", top_pair$item_b),
    `Top Pairing Lift`  = if (no_assoc) 0 else top_pair$lift
  )

  json_output <- list(
    answer = if (no_assoc) {
      paste0(
        "Across ", plural(n_orders, "order"), " (average basket ",
        round(avg_basket, 1), " items), no product pair cleared the support floor of ",
        plural(min_support_orders, "basket"),
        ", so no strong associations were found. The most-bought ", name_item,
        " is ", top_item, ", in ", top_item_pct, "% of orders."
      )
    } else {
      paste0(
        "Across ", plural(n_orders, "order"), ", the strongest product pairing is ",
        top_pair$item_a, " and ", top_pair$item_b, ": shoppers who buy ",
        top_pair$item_a, " also buy ", top_pair$item_b, " ", top_pair$confidence_pct,
        "% of the time, a lift of ", top_pair$lift, " (bought together ", top_pair$lift,
        " times more often than chance). ", plural(n_qual, "pair"),
        " cleared the support floor. The most-bought ", name_item, " is ",
        top_item, ", in ", top_item_pct, "% of orders."
      )
    },
    cards = lapply(
      c("tldr", "overview", "preprocessing", "top_items",
        "association_rules", "cross_sell"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows, rows_removed = rows_removed,
    name_order = name_order, name_item = name_item,
    n_orders = n_orders, n_distinct_items = n_distinct_items,
    avg_basket = avg_basket, n_bad_rows = n_bad_rows, n_lumped = n_lumped,
    item_freq_df = item_freq_df, top_items_df = top_items_df,
    top_item = top_item, top_item_pct = top_item_pct,
    pairs_all = pairs_all, rules_df = rules_df, cross_sell_df = cross_sell_df,
    top_pair = top_pair, no_assoc = no_assoc,
    min_support_orders = min_support_orders,
    metrics = metrics, json_output = json_output
  )
}

# Card: tldr (tldr)
card_tldr <- function(shared, df, params) {
  if (shared$no_assoc) {
    text <- paste0(
      "Across ", plural(shared$n_orders, "order"), " (average basket ",
      round(shared$avg_basket, 1), " items), no product pair appeared together in at least ",
      plural(shared$min_support_orders, "basket"),
      ", so no strong co-purchase associations were found. The single most-bought ",
      shared$name_item, " is ", shared$top_item, ", appearing in ",
      shared$top_item_pct, "% of orders. With more transactions or a more ",
      "concentrated catalog, clearer pairings tend to emerge."
    )
  } else {
    tp <- shared$top_pair
    text <- paste0(
      "Across ", plural(shared$n_orders, "order"), " with an average basket of ",
      round(shared$avg_basket, 1), " items, the strongest product pairing is ",
      tp$item_a, " and ", tp$item_b, ": shoppers who buy ", tp$item_a,
      " also buy ", tp$item_b, " ", tp$confidence_pct, "% of the time, a lift of ",
      tp$lift, " — they land in the same basket ", tp$lift,
      " times more often than if the two were bought independently. The most-bought ",
      shared$name_item, " overall is ", shared$top_item, ", in ",
      shared$top_item_pct, "% of orders. These are co-purchase patterns to act on for ",
      "cross-sell, bundling, and layout; they show what sells together, not proof that one item drives the other."
    )
  }
  list(
    title = "Executive Summary",
    description = paste0("What sells together across ", plural(shared$n_orders, "order"), "."),
    metrics = shared$metrics,
    text = text
  )
}
Your data has more stories to tell. Run any analysis on your own data — 60+ validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No Signup Sign Up Free

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