Chapter 120 · Practice Challenges, Solved¶
Five market-basket exercises, worked in mlxtend on the Chapter 120 grocery transactions.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import seaborn as sns # seaborn = high-level statistical plots (heatmaps, pairplots, count/bar plots)
from matplotlib.colors import ListedColormap
EM="#4338ca"; DEEP="#3730a3"; LIGHT="#c7d2fe"; INK="#1a2138"; GRID="#e6e9f2"; RED="#ef4444"; AMBER="#d97706"; GREEN="#059669"; BLUE="#2563eb"; PUR="#9333ea"; GREY="#94a3b8"; SLATE="#475569"; ORG="#4338ca"; CYAN="#0891b2"
plt.rcParams.update({"figure.facecolor":"white","axes.facecolor":"white","figure.dpi":110,"font.size":11,
"axes.edgecolor":GRID,"axes.grid":True,"grid.color":GRID,"axes.axisbelow":True,"axes.spines.top":False,
"axes.spines.right":False,"axes.titlesize":12,"axes.titleweight":"bold","legend.frameon":False})
sns.set_style("whitegrid")
BASE="https://raw.githubusercontent.com/johnfisher-ai/Statistics-Data-Science-AI-Visual-Book/main/data/"
from mlxtend.frequent_patterns import apriori, fpgrowth, association_rules
try: df = pd.read_excel('../../data/association-rule-mining--baskets.xlsx', sheet_name='Data')
except FileNotFoundError: df = pd.read_excel(BASE + 'association-rule-mining--baskets.xlsx', sheet_name='Data')
basket = df.assign(v=1).pivot_table(index='transaction_id', columns='item', values='v', fill_value=0).astype(bool)
print('basket matrix:', basket.shape)
basket matrix: (900, 25)
support = basket.mean().sort_values(ascending=False)
print(support.head(5).round(3).to_string())
print('most frequent item:', support.idxmax(), '(support %.2f)' % support.max())
item bread 0.334 jam 0.187 cereal 0.172 butter 0.170 bacon 0.167 most frequent item: bread (support 0.33)
Solution. Bread has the highest support (in about a third of baskets), the popular staple every rule must be compared against.
freq = apriori(basket, min_support=0.05, use_colnames=True)
print('frequent itemsets at 5% support:', len(freq))
print(freq['itemsets'].apply(len).value_counts().sort_index().to_string())
frequent itemsets at 5% support: 54 itemsets 1 25 2 20 3 8 4 1
Solution. A higher support threshold keeps fewer, more reliable itemsets, tune it to trade coverage against trustworthiness.
freq = apriori(basket, min_support=0.03, use_colnames=True)
rules = association_rules(freq, metric='lift', min_threshold=1.0)
rules['A'] = rules['antecedents'].apply(lambda s: ', '.join(sorted(s)))
rules['B'] = rules['consequents'].apply(lambda s: ', '.join(sorted(s)))
best = rules.sort_values(['lift', 'A', 'B'], ascending=[False, True, True]).iloc[0] # break the A -> B / B -> A tie by name
print('top rule: %s -> %s | support %.3f, confidence %.3f, lift %.2f' % (best.A, best.B, best.support, best.confidence, best.lift))
top rule: chips, soda -> salsa | support 0.092, confidence 0.856, lift 5.70
Solution. The strongest rules link the cookout items (chips, salsa, soda) with lift above 5, so baskets holding two of them contain the third far more often than average. Lift is symmetric, so a rule and its reverse tie, and chips, soda -> salsa and salsa -> chips, soda share the 5.70, though their confidences differ (0.856 here, and 0.615 for the reverse in the main notebook).
r = rules[(rules.A=='bananas') & (rules.B=='bread')][['support','confidence','lift']]
print(r.round(3).to_string(index=False))
print('bread baseline support = %.2f' % basket['bread'].mean())
print('confidence 0.37 barely beats the 0.33 baseline -> lift ~1.1 -> essentially independent')
support confidence lift 0.047 0.368 1.102 bread baseline support = 0.33 confidence 0.37 barely beats the 0.33 baseline -> lift ~1.1 -> essentially independent
Solution. Bananas do not really drive bread; bread is simply in a third of all baskets, so 37% confidence is almost the baseline. Lift near 1 exposes the coincidence, always judge a rule by lift, not confidence.
a = apriori(basket, min_support=0.03, use_colnames=True)
f = fpgrowth(basket, min_support=0.03, use_colnames=True)
sa = set(frozenset(x) for x in a['itemsets']); sf = set(frozenset(x) for x in f['itemsets'])
print('Apriori itemsets:', len(sa), '| FP-Growth itemsets:', len(sf))
print('identical set of itemsets:', sa == sf)
Apriori itemsets: 61 | FP-Growth itemsets: 61 identical set of itemsets: True
Solution. FP-Growth finds exactly the same frequent itemsets as Apriori but with a compact tree instead of repeated scans, so on large catalogs it is much faster while giving identical rules.