Siksha Sarovar

Siksha Sarovar (sikshasarovar.com) is a free educational web application that helps students in India learn programming and prepare for academic and competitive exams. The platform offers structured coding courses (C, C++, Python, Java, HTML, CSS, PHP, Power BI, AI, Machine Learning, Data Science), complete university curriculum notes for BCA/MCA students with previous year question papers, Class 10 and Class 12 CBSE/HBSE school notes, and dedicated preparation material for SSC, UPSC, Banking, Railway and other government exams. Browsing the site is completely free and requires no account. Users may optionally sign in with Google solely to save their learning progress, quiz scores and personal preferences across devices.

Privacy Policy | Terms of Service | Contact Siksha Sarovar | About Siksha Sarovar

v4.0.9 · PWA
Siksha Sarovar logo
Siksha Sarovar
Your Learning Universe

Siksha Sarovar is a free e-learning platform for coding courses, BCA university notes and competitive exam preparation. Optional Google sign-in saves your learning progress across devices.

Initializing knowledge base…
Compiling modules 0%

Unit 3 — Apriori Algorithm

Lesson 35 of 46 in the free Introduction to Data Analytics notes on Siksha Sarovar, written by Rohit Jangra.

The Apriori Algorithm

Apriori (Agrawal and Srikant, 1994) is the classic algorithm for mining frequent itemsets and generating association rules. It solves the combinatorial explosion problem with a single powerful observation.

The Apriori Property (Downward Closure)

   "All non-empty SUBSETS of a frequent itemset must also be frequent."

   Equivalently (the contrapositive, which is what makes it useful):

   "If an itemset is INFREQUENT, then ALL of its supersets are also infrequent."

Why this works: support is anti-monotonic — adding an item to an itemset can never increase its support, because a transaction containing {A,B,C} necessarily contains {A,B}.

The payoff: if {Milk, Beer} is infrequent, we can prune {Milk, Beer, Bread}, {Milk, Beer, Diaper}, and every other superset without ever counting them. This prunes an exponential search space into something tractable.

In the lattice above, if {B,C} (red) is found infrequent, then {A,B,C} (grey) is pruned immediately — it can never be frequent.

The Algorithm

INPUT:  transaction database D, minimum support threshold min_sup

1. Scan D once; count the support of every individual item
   ->  L₁ = set of frequent 1-itemsets (those with support ≥ min_sup)

2. FOR k = 2, 3, 4, … WHILE L(k−1) is not empty:

   a) JOIN step:      generate candidate k-itemsets Cₖ by joining L(k−1)
                      with itself (join two (k−1)-itemsets that share their
                      first k−2 items)

   b) PRUNE step:     remove any candidate that has a (k−1)-subset NOT in
                      L(k−1)   <- this is the Apriori property at work

   c) COUNT step:     scan D to count the support of the surviving candidates

   d) SELECT step:    Lₖ = candidates in Cₖ with support ≥ min_sup

3. Frequent itemsets = L₁ ∪ L₂ ∪ … ∪ Lₖ

4. Generate association rules from each frequent itemset and keep those
   with confidence ≥ min_confidence

Complete Worked Example

Transaction database (min_support = 2 transactions = 40%):

TIDItems
T1I1, I2, I5
T2I2, I4
T3I2, I3
T4I1, I2, I4
T5I1, I3
T6I2, I3
T7I1, I3
T8I1, I2, I3, I5
T9I1, I2, I3

Iteration 1 — Find L₁

C₁ (candidate 1-itemsets) with counts:

ItemsetSupport countFrequent? (≥ 2)
{I1}6
{I2}7
{I3}6
{I4}2
{I5}2

L₁ = { {I1}, {I2}, {I3}, {I4}, {I5} } — all five survive.

Iteration 2 — Find L₂

C₂ = all pairs from L₁ (C(5,2) = 10 candidates):

ItemsetSupport countFrequent?
{I1,I2}4
{I1,I3}4
{I1,I4}1
{I1,I5}2
{I2,I3}4
{I2,I4}2
{I2,I5}2
{I3,I4}0
{I3,I5}1
{I4,I5}0

L₂ = { {I1,I2}, {I1,I3}, {I1,I5}, {I2,I3}, {I2,I4}, {I2,I5} }

Iteration 3 — Find L₃

Join step — join pairs in L₂ sharing their first item:

   {I1,I2} ⋈ {I1,I3}  ->  {I1,I2,I3}
   {I1,I2} ⋈ {I1,I5}  ->  {I1,I2,I5}
   {I1,I3} ⋈ {I1,I5}  ->  {I1,I3,I5}
   {I2,I3} ⋈ {I2,I4}  ->  {I2,I3,I4}
   {I2,I3} ⋈ {I2,I5}  ->  {I2,I3,I5}
   {I2,I4} ⋈ {I2,I5}  ->  {I2,I4,I5}

Prune step — check that every 2-subset is in L₂:

Candidate2-subsetsAll in L₂?Action
{I1,I2,I3}{I1,I2} ✓, {I1,I3} ✓, {I2,I3} ✓YesKeep
{I1,I2,I5}{I1,I2} ✓, {I1,I5} ✓, {I2,I5} ✓YesKeep
{I1,I3,I5}{I1,I3} ✓, {I1,I5} ✓, {I3,I5} ✗NoPRUNED
{I2,I3,I4}{I2,I3} ✓, {I2,I4} ✓, {I3,I4} ✗NoPRUNED
{I2,I3,I5}{I2,I3} ✓, {I2,I5} ✓, {I3,I5} ✗NoPRUNED
{I2,I4,I5}{I2,I4} ✓, {I2,I5} ✓, {I4,I5} ✗NoPRUNED
This is the whole value of Apriori — four of six candidates are eliminated without a single database scan.

Count step — only two candidates need counting:

ItemsetSupport countFrequent?
{I1,I2,I3}2 (T8, T9)
{I1,I2,I5}2 (T1, T8)

L₃ = { {I1,I2,I3}, {I1,I2,I5} }

Iteration 4 — Find L₄

Join:  {I1,I2,I3} ⋈ {I1,I2,I5}  ->  {I1,I2,I3,I5}
Prune: subset {I1,I3,I5} is NOT in L₃  ->  PRUNED

C₄ is empty  ->  L₄ is empty  ->  ALGORITHM TERMINATES

Rule Generation from {I1, I2, I5} (support = 2/9 = 0.222)

For each non-empty proper subset as antecedent (2³ − 2 = 6 rules):

RuleConfidence calculationConfidenceKeep at 70%?
{I1,I2} → {I5}2/450.0%
{I1,I5} → {I2}2/2100.0%
{I2,I5} → {I1}2/2100.0%
{I1} → {I2,I5}2/633.3%
{I2} → {I1,I5}2/728.6%
{I5} → {I1,I2}2/2100.0%

Strong rules (confidence ≥ 70%): {I1,I5}→{I2}, {I2,I5}→{I1}, {I5}→{I1,I2}

Notice all three high-confidence rules involve I5 as (part of) the antecedent — I5 is rare (appears twice) but always appears alongside I1 and I2.

Python

import pandas as pd
from itertools import combinations

transactions = [
    ["I1","I2","I5"], ["I2","I4"], ["I2","I3"],
    ["I1","I2","I4"], ["I1","I3"], ["I2","I3"],
    ["I1","I3"], ["I1","I2","I3","I5"], ["I1","I2","I3"],
]
min_support_count = 2
n = len(transactions)
tsets = [set(t) for t in transactions]

def support_count(itemset):
    return sum(1 for t in tsets if itemset.issubset(t))

def apriori_manual(min_sup_count):
    items = sorted({i for t in transactions for i in t})
    # L1
    L = {frozenset([i]): support_count({i}) for i in items}
    L = {k: v for k, v in L.items() if v >= min_sup_count}
    all_frequent = dict(L)
    k = 2
    while L:
        prev = list(L.keys())
        # JOIN
        candidates = set()
        for a, b in combinations(prev, 2):
            union = a | b
            if len(union) == k:
                candidates.add(union)
        # PRUNE using the Apriori property
        pruned = {c for c in candidates
                  if all(frozenset(sub) in L for sub in combinations(c, k - 1))}
        print(f"k={k}: {len(candidates)} candidates after join, "
              f"{len(pruned)} survive pruning")
        # COUNT + SELECT
        L = {c: support_count(set(c)) for c in pruned}
        L = {c: s for c, s in L.items() if s >= min_sup_count}
        all_frequent.update(L)
        k += 1
    return all_frequent

frequent = apriori_manual(min_support_count)
print("\nFrequent itemsets:")
for itemset, count in sorted(frequent.items(), key=lambda x: (len(x[0]), -x[1])):
    print(f"  {set(itemset)}: count={count}, support={count/n:.3f}")
# Generate rules from the frequent itemsets
def generate_rules(frequent, min_conf=0.7):
    rules = []
    for itemset, sup_count in frequent.items():
        if len(itemset) < 2:
            continue
        for r in range(1, len(itemset)):
            for ante in combinations(itemset, r):
                ante = frozenset(ante)
                cons = itemset - ante
                conf = sup_count / frequent[ante]
                lift = conf / (frequent[frozenset(cons)] / n) if frozenset(cons) in frequent else None
                if conf >= min_conf:
                    rules.append({
                        "rule": f"{set(ante)} -> {set(cons)}",
                        "support": round(sup_count / n, 3),
                        "confidence": round(conf, 3),
                        "lift": round(lift, 3) if lift else None,
                    })
    return pd.DataFrame(rules).sort_values("lift", ascending=False)

print(generate_rules(frequent, min_conf=0.7).to_string(index=False))
# Using mlxtend — the production-standard implementation
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori, association_rules, fpgrowth

te = TransactionEncoder()
df = pd.DataFrame(te.fit(transactions).transform(transactions), columns=te.columns_)

frequent_itemsets = apriori(df, min_support=2/9, use_colnames=True)
frequent_itemsets["length"] = frequent_itemsets["itemsets"].apply(len)
print(frequent_itemsets.sort_values(["length", "support"], ascending=[True, False]))

rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.7)
print("\nStrong rules:")
print(rules[["antecedents", "consequents", "support", "confidence", "lift"]]
      .sort_values("lift", ascending=False).round(3).to_string(index=False))
# Apriori vs FP-Growth on a larger dataset
import numpy as np, time

np.random.seed(42)
products = [f"P{i}" for i in range(30)]
big = [list(np.random.choice(products, np.random.randint(2, 8), replace=False))
       for _ in range(3000)]

te = TransactionEncoder()
df_big = pd.DataFrame(te.fit(big).transform(big), columns=te.columns_)

t0 = time.time(); fi_ap = apriori(df_big, min_support=0.01, use_colnames=True); t_ap = time.time() - t0
t0 = time.time(); fi_fp = fpgrowth(df_big, min_support=0.01, use_colnames=True); t_fp = time.time() - t0

print(f"Apriori  : {len(fi_ap)} itemsets in {t_ap:.3f}s")
print(f"FP-Growth: {len(fi_fp)} itemsets in {t_fp:.3f}s")
print("Same results:", len(fi_ap) == len(fi_fp))
# FP-Growth finds identical itemsets, typically several times faster.

Apriori vs FP-Growth

BasisAprioriFP-Growth
ApproachCandidate generate-and-test, breadth-firstDivide and conquer on a compressed tree
Database scansMany — one per level kOnly 2
Candidate generationYes — the main bottleneckNone
Data structureHash tree / plain countingFP-Tree (compressed prefix tree)
MemoryLower per step, but many candidatesHigher — the FP-tree must fit in memory
SpeedSlower, especially with low min_supportTypically 5–10× faster
ImplementationSimple, easy to explainMore complex

Other alternatives: ECLAT (vertical data format, uses set intersection of TID-lists) and Partition/Sampling algorithms for very large databases.

Improving Apriori's Efficiency

TechniqueIdea
Hash-based itemset countingHash candidate k-itemsets into buckets; a bucket below min_sup prunes all its itemsets
Transaction reductionA transaction containing no frequent k-itemsets cannot contain any frequent (k+1)-itemset — drop it
PartitioningAny globally frequent itemset must be frequent in at least one partition; requires only 2 scans
SamplingMine a random sample, then verify candidates on the full database
Dynamic Itemset Counting (DIC)Start counting new candidates mid-scan rather than waiting for the scan to finish

Advantages and Disadvantages

AdvantagesDisadvantages
Simple, intuitive, easy to implement and explainMultiple database scans — expensive I/O on large data
The Apriori property prunes the search space massivelyCandidate generation is a serious bottleneck
Produces easily interpretable, actionable rulesPerformance degrades sharply at low min_support
Works on any transactional dataStruggles with very long frequent itemsets
Well-studied, available in every analytics toolHigh memory usage when candidate sets are large
Parallelises reasonably wellIgnores item quantities, prices, and time ordering

That completes Unit 3. You can now build predictive models (classification, regression) and descriptive models (clustering, association rules). Unit 4 turns to the tools — the Python analytics stack — and to what happens when data outgrows a single machine.