Chapter 22 · Eclat
Same 7,501 baskets, leaner question. Eclat — Equivalence Class Transformation of Lattice-orderings — drops arrows, confidence and lift entirely, and asks only: which itemsets are frequent? Its trick is to flip the data sideways, from baskets-listing-items to items-listing-baskets, so every support is one set intersection — computed by hand here, run for real at 951 itemsets, and timed against Apriori. The chapter, and Part V, close with the honest comparison of the two philosophies.
🎯 Learning Objectives
- Transpose a basket table into the vertical TID layout and compute supports by set intersection, by hand
- State precisely what Eclat drops from Apriori — and what that costs and buys
- Run the real frequent-itemset search and read the size distribution (114 singles, 578 pairs, 259 triples)
- Explain why every top pair contains mineral water — and why that is volume, not insight
- Time the vertical family (FP-growth) against Apriori and explain the speed gap
- Choose between Apriori and Eclat by question, not habit
1. The Leaner Question
Chapter 21 produced rules: directional, lift-refereed statements — fromage blanc → honey — with three metrics to read together. Eclat asks for less and gets it faster: which itemsets (unordered sets of items) clear a minimum support? No arrow, no confidence, no lift. {mineral water, spaghetti} is frequent — full stop — with no claim about which causes which or whether the pairing beats baseline.
What does the leaner question cost and buy? It costs insight machinery: from frequent itemsets you can always compute Chapter 21's rules afterwards (confidence and lift are simple ratios over supports), but Eclat itself ranks nothing and referees nothing — its raw output, sorted by support, systematically promotes popular items. It buys speed and simplicity: fewer metrics, one counting operation, and — as Section 6 measures — a substantially faster search, which is exactly what you want when the itemset length grows or the basket count explodes.
2. The Vertical Layout, by Hand
Apriori scanned baskets (rows) to count itemsets. Eclat flips the table sideways — the vertical (TID) layout: for each item, the list of transaction IDs containing it. Chapter 21's five baskets, transposed:
| Item | TID list (baskets containing it) | Count | ||||
|---|---|---|---|---|---|---|
| bread | 1 | 2 | 3 | 5 | — | 4 |
| milk | 1 | 2 | 5 | — | — | 3 |
| eggs | 2 | 3 | 4 | 5 | — | 4 |
| cheese | 5 | — | — | — | — | 1 |
And now the entire algorithm is one operation — set intersection:
supp(milk ∧ eggs) = |{1,2,5} ∩ {2,3,4,5}| / 5 = 2/5 = 0.40
supp(bread ∧ milk ∧ eggs) = |{1,2,3,5} ∩ {1,2,5} ∩ {2,3,4,5}| / 5 = |{2,5}| / 5 = 0.40
3. Eclat's Search & the Source's Confession
The search inherits Apriori's pruning (a pair whose singles are infrequent is never counted — anti-monotonicity is layout-independent) but organises work differently: depth-first through prefix groups. Start from mineral water's TID list; intersect with each co-item; each surviving pair becomes a new prefix branch for triples; recurse. Apriori instead sweeps levels breadth-first, re-reading the database; Eclat touches the database once — everything after is intersection arithmetic.
One honest confession about the source code: eclat.py actually
calls apyori again — Apriori's machinery — simply discarding the
confidence and lift columns and ranking by support alone. That reproduces
Eclat's question (support-only output) without Eclat's
engine. The modern, honest implementation of the vertical family is
mlxtend's fpgrowth (FP-growth: a compressed prefix-tree over
the transactions — a cousin of Eclat's prefix idea), which this chapter runs
for its real numbers and its timing.
4. The Real Itemsets
FP-growth, min_support = 0.004 (≈ 30 baskets ≈ 4+ per day), itemsets of up to three items, on the real 7,501 baskets:
| Itemset size | 1 (singles) | 2 (pairs) | 3 (triples) | Total |
|---|---|---|---|---|
| Count | 114 | 578 | 259 | 951 |
The top of each size class, by support (all real):
| Rank | Top pairs | Support | Top triples | Support |
|---|---|---|---|---|
| 1 | {mineral water, spaghetti} | 0.0597 | {ground beef, mineral water, spaghetti} | 0.0171 |
| 2 | {chocolate, mineral water} | 0.0527 | {chocolate, mineral water, spaghetti} | 0.0159 |
| 3 | {eggs, mineral water} | 0.0509 | {milk, mineral water, spaghetti} | 0.0157 |
| 4 | {milk, mineral water} | 0.0480 | {eggs, mineral water, spaghetti} | 0.0143 |
| 5 | {ground beef, mineral water} | 0.0409 | {chocolate, milk, mineral water} | 0.0140 |
| 6 | {ground beef, spaghetti} | 0.0392 | {chocolate, eggs, mineral water} | 0.0135 |
Three readings, each a Part V lesson in miniature:
- Every top pair contains mineral water. The #1 pair (water + spaghetti, 0.0597 ≈ 448 baskets/week) is genuinely massive volume — but it is volume, not discovery: water rides along with everything (Chapter 21's 0.238 support doing exactly what we warned). Support-only rankings are popularity contests with extra steps.
- The interesting itemsets are invisible here. Chapter 21's insight champion — fromage blanc + honey, lift 5.16 — has support 0.0033, below even this chapter's 0.004 bar. The two metrics select different champions: support finds volume, lift finds association. A store needs both reports.
- Triples tell the water story again, deeper. All six top triples are {mineral water, spaghetti, X} or {mineral water, chocolate/milk/eggs} — the staples' co-purchase core. Fine for inventory and placement of high-traffic goods; useless for the cross-sell discovery a lift-based analysis provides.
5. Implementation in Python
The modern vertical-family pipeline is mlxtend
(pip install mlxtend): encode, mine, sort:
import pandas as pd
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import fpgrowth
dataset = pd.read_csv('Market_Basket_Optimisation.csv', header=None)
transactions = [[str(v) for v in row if pd.notna(v)]
for row in dataset.values] # blanks stripped (Ch 21's bug)
# 1. One-hot encode: rows = baskets, columns = 120 items, cells = True/False
te = TransactionEncoder()
df = pd.DataFrame(te.fit(transactions).transform(transactions),
columns=te.columns_) # shape (7501, 120)
# 2. Mine frequent itemsets — FP-growth, the vertical family's industrial engine
fis = fpgrowth(df, min_support=0.004, use_colnames=True, max_len=3)
fis['size'] = fis['itemsets'].apply(len) # 951 rows: 114 / 578 / 259
print(fis[fis['size'] == 2]
.sort_values('support', ascending=False)
.head()) # top pairs, water-led
# 3. Attach lift when insight is needed — supports are all you need for the ratios
# conf(X->Y) = supp(X,Y)/supp(X); lift = supp(X,Y)/(supp(X)·supp(Y))
Listing 22.1 — The mlxtend pipeline (source's apyori variant noted in Section 3).
frequent_patterns.apriori on the same one-hot frame — one
line to A/B the two engines on identical data, which is exactly how
Section 6's timing was measured. Note both expect the boolean one-hot
matrix, not lists of lists (that was apyori's format).
6. Speed: the Real Motivation
Same baskets, same min_support = 0.003, same one-hot matrix — timed (real):
| Engine | Layout & strategy | Time (real) |
|---|---|---|
| FP-growth (vertical family) | Compressed prefix tree; database read twice, then memory arithmetic | 0.067 s |
| Apriori (mlxtend, horizontal) | Level-wise candidate generation, repeated scans | 0.238 s |
A 3.5× gap on a toy-sized dataset, and the gap widens with data: Apriori's cost scales with candidate counts per level (each level re-derives supports from scratch), while the vertical family reuses every intersection's result downward through the prefix tree — support of {water, spaghetti, ground beef} is computed from {water, spaghetti}'s list intersected with ground beef's, never from the raw database. On million-basket retail data this is the difference between an interactive tool and an overnight job.
7. Apriori vs Eclat — Settled
| Apriori (Ch 21) | Eclat / vertical family (Ch 22) | |
|---|---|---|
| Question | "What implies what?" — directional rules | "What is frequent?" — unordered itemsets |
| Metrics | Support, confidence, lift | Support only (lift computable afterwards from supports) |
| Data layout | Horizontal — baskets listing items | Vertical — items listing basket IDs; counting = intersection |
| Search | Breadth-first, level-wise, repeated scans | Depth-first through prefix groups; database touched ~once |
| Pruning | Anti-monotone subset rule | Same rule — layout changes, mathematics doesn't |
| Speed (real, this data) | 0.238 s | 0.067 s — and the gap widens with scale |
| Raw-output bias | Lift-refereed — insights by association strength | Support-ranked — a popularity contest (water everywhere) |
| Reach for it when | Cross-sell discovery, shelf pairs, recommender hints | Volume reporting, inventory, planograms, big baskets, long itemsets |
The professional's summary, one sentence each: Apriori answers "why would a customer buy Y" (with lift as the lie detector); Eclat answers "what moves together in bulk" (with speed as the enabler). Same baskets, complementary reports — and the rule mining journey of this course ends with both on the table.
8. Part V Retrospective & the Road Ahead
Two chapters, one dataset, two philosophies of co-occurrence: rules with a referee (fromage blanc → honey, lift 5.16 — discovery) and itemsets with speed (water + spaghetti, support 0.0597 — volume). Together they close the unsupervised arc that Part IV opened: clustering asked who groups, association asked what groups, and neither needed a single label.
Part VI changes the shape of time itself. So far every dataset was static — a table to be studied. Reinforcement learning makes data arrive as consequences of actions: an ad shown, a reward observed, a strategy updated — the exploration-versus-exploitation dilemma, the multi-armed bandit, and Upper Confidence Bound, where an algorithm must gamble wisely rather than analyse quietly.
🛠 Hands-On: Flip the TableIntermediate
The inventory team wants the volume report the lift-based analysis never showed them. Build the vertical layout by hand, run the real itemset search, attach lift after the fact, and deliver the two-report summary.
Steps
- By hand: build the TID lists for the five-basket table; verify supp(bread ∧ milk) = 0.60 and supp(bread ∧ milk ∧ eggs) = 0.40 by intersection.
- By hand: compute supp(cheese ∧ eggs) via {5} ∩ {2,3,4,5}; explain why cheese joins nothing.
- One-hot encode the real dataset with TransactionEncoder; report the matrix shape and spot-check row 1.
- Run fpgrowth at min_support = 0.004, max_len = 3; report the 114/578/259 distribution; list the top 3 pairs and triples.
- For {mineral water, spaghetti} and {ground beef, spaghetti}, compute lift from supports alone; compare with the hand expectation from Chapter 21.
- Time fpgrowth vs mlxtend's apriori at 0.003; write the two-report recommendation (volume report + insight report) in four sentences.
View Solution / Walkthrough
import pandas as pd, time
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import fpgrowth, apriori
dataset = pd.read_csv('Market_Basket_Optimisation.csv', header=None)
transactions = [[str(v) for v in row if pd.notna(v)] for row in dataset.values]
te = TransactionEncoder()
df = pd.DataFrame(te.fit(transactions).transform(transactions), columns=te.columns_)
fis = fpgrowth(df, min_support=0.004, use_colnames=True, max_len=3)
fis['size'] = fis['itemsets'].apply(len)
print(fis['size'].value_counts().sort_index()) # 114 / 578 / 259
print(fis[fis['size']==2].nlargest(3, 'support'))
# Step 5: lift from supports
sup = lambda items: fis[fis.itemsets == frozenset(items)].support.iloc[0]
sw, ss, sg = sup(['mineral water']), sup(['spaghetti']), sup(['ground beef'])
print('lift(water,spag) = %.2f' % (sup(['mineral water','spaghetti'])/(sw*ss)))
print('lift(beef,spag) = %.2f' % (sup(['ground beef','spaghetti'])/(sg*ss)))
t0 = time.time(); fpgrowth(df, min_support=0.003, use_colnames=True); t1 = time.time()
apriori(df, min_support=0.003, use_colnames=True); t2 = time.time()
print('fpgrowth %.3fs apriori %.3fs' % (t1-t0, t2-t1))
Expected results (real):
- Steps 1–2: tid lists as in Section 2; {5} ∩ {2,3,4,5} = {5} → supp 0.20 — cheese is a one-basket item whose intersections can only shrink; anti-monotonicity in action.
- Steps 3–4: matrix (7501, 120); 951 itemsets; top pairs water-led — {water, spaghetti} 0.0597, {chocolate, water} 0.0527, {eggs, water} 0.0509; top triple {ground beef, water, spaghetti} 0.0171.
- Step 5: lift(water, spaghetti) = 0.0597 / (0.2384 × 0.1741) ≈ 1.44 — positive but far below Chapter 21's gate; lift(ground beef, spaghetti) = 0.0392 / (0.0983 × 0.1741) ≈ 2.29 — the same pair that carried rule 8's 3.29 with herb & pepper. Volume pairs are weakly associated; insight lives elsewhere.
- Step 6: fpgrowth ≈ 0.07 s vs apriori ≈ 0.24 s; the recommendation: hand inventory the support-ranked itemset report, hand marketing the lift-gated rule report — one dataset, two questions, two tools.
🎯 Key Takeaways
Eclat's question is leaner: which itemsets are frequent — no arrows, no confidence, no lift in the raw output.
The vertical flip: items list basket IDs, and every support is one set intersection — supp(bread ∧ milk) = |{1,2,3,5} ∩ {1,2,5}|/5 = 0.60.
Anti-monotonicity survives the transposition — triple TID lists are subsets of their pairs'; Apriori's pruning applies unchanged.
951 real itemsets (114/578/259) — and every top pair holds mineral water: support rankings are popularity contests; volume ≠ insight.
Water + spaghetti: support 0.0597 (massive) but lift 1.44 (modest) — the two metrics crown different champions; run both reports.
Speed is the honest motivation: 0.067 s vs 0.238 s here, widening with scale — the prefix-tree family is what production runs.
Test Your Understanding
30 questions across the chapter — the vertical layout, intersections by hand, the real itemsets, the speed comparison and the two philosophies. Choose one answer per question, then check your answers.
Q1.Eclat's question, compared with Apriori's, drops:
Q2.The vertical (TID) layout stores, for each item:
Q3.(Hand) With tid(bread) = {1,2,3,5} and tid(milk) = {1,2,5}, supp(bread ∧ milk) over 5 baskets is:
Q4.(Hand) The triple's TID list {2,5} is a subset of the pair's {1,2,5}. This demonstrates:
Q5.A triple's support in Eclat is computed from:
Q6.(Hand) supp(cheese ∧ eggs) = |{5} ∩ {2,3,4,5}|/5 equals:
Q7."Equivalence Class Transformation of Lattice-orderings" in practice means:
Q8.The source code's honest confession (Section 3):
Q9.(Real) At min_support 0.004, max_len 3, the search returned:
Q10.(Real) The #1 pair by support was:
Q11.(Real) Every top pair and every top triple contains mineral water because:
Q12.(Real) Chapter 21's insight champion {fromage blanc, honey} — lift 5.16 — is absent here because:
Q13.(Real) lift(mineral water → spaghetti), computed from supports alone, is about:
Q14.That lift can be computed after mining from supports alone shows:
Q15.(Real) The timing run gave fpgrowth 0.067 s vs Apriori 0.238 s. The ratio and its future:
Q16.The one-hot matrix that mlxtend expects has:
Q17.FP-growth, the engine this chapter actually runs, belongs to Eclat's family because both:
Q18.The honest footnote about strict original Eclat at scale:
Q19.Anti-monotonicity (pruning) in Eclat:
Q20.The business question that picks Eclat over Apriori:
Q21.Apriori's search versus Eclat's, in one line each:
Q22.(Real) The top triples were all {mineral water, spaghetti, X} shapes. The inventory-team reading:
Q23.min_support = 0.004 on 7,501 baskets corresponds to roughly:
Q24.The TransactionEncoder's role in the pipeline:
Q25.What Eclat's raw output — support-ranked itemsets — systematically promotes:
Q26.(Real) lift(ground beef → spaghetti) ≈ 2.29 versus lift(water → spaghetti) ≈ 1.44. Together they illustrate:
Q27.The two-report recommendation this chapter lands on:
Q28.Part V's arc, in one sentence:
Q29.Part VI's fundamental change from every preceding part:
Q30.The next chapter's first algorithm, facing the exploration-exploitation dilemma, is: