International College of Digital Innovation, CMU
October 3, 2025
items = [
{key: "bread", label: "Bread", emoji: "🥖"},
{key: "rice", label: "Rice", emoji: "🍚"},
{key: "milk", label: "Milk", emoji: "🥛"},
{key: "beer", label: "Beer", emoji: "🍺"},
{key: "eggs", label: "Eggs", emoji: "🥚"},
{key: "cheese", label: "Cheese", emoji: "🧀"},
{key: "coffee", label: "Coffee", emoji: "☕"}
]
itemMap = new Map(items.map(d => [d.key, d]))// Effect: 1 click = 1 action (no re-declare)
{
// Add basket
if (addCartBtn > lastAddClick) {
mutable carts = [...carts, []];
mutable lastAddClick = addCartBtn;
}
// Clear all
if (typeof clearAllBtn !== "undefined" && clearAllBtn > lastClearClick) {
mutable carts = [[], [], []];
mutable lastClearClick = clearAllBtn;
}
// Add sample baskets
if (typeof sampleBtn !== "undefined" && sampleBtn > lastSampleClick) {
mutable carts = [
["bread","milk"],
["bread","beer","milk"],
["rice","milk"],
["bread","rice"],
["beer","milk"],
["bread","beer"],
["bread","rice","milk"],
["rice","beer"],
["bread"],
["milk"]
];
mutable lastSampleClick = sampleBtn;
}
}palette = {
const wrap = document.createElement("div");
wrap.style.display = "flex";
wrap.style.gap = "12px";
wrap.style.flexWrap = "wrap";
for (const it of items) {
const b = document.createElement("div");
b.textContent = it.emoji;
b.draggable = true;
b.title = it.label;
b.style.fontSize = "36px";
b.style.cursor = "grab";
b.addEventListener("dragstart", e => {
e.dataTransfer.setData("text/plain", it.key);
});
wrap.appendChild(b);
}
return wrap;
}viewof baskets = {
const container = document.createElement("div");
container.style.display = "grid";
container.style.gridTemplateColumns = "repeat(auto-fit, minmax(220px, 1fr))";
container.style.gap = "14px";
function render() {
container.innerHTML = "";
carts.forEach((basket, idx) => {
const card = document.createElement("div");
card.style.border = "1px solid #ccc";
card.style.borderRadius = "14px";
card.style.padding = "10px";
card.style.background = "#fff";
const title = document.createElement("div");
title.innerHTML = `<strong>🧺 Basket #${idx+1}</strong>`;
title.style.marginBottom = "8px";
card.appendChild(title);
const zone = document.createElement("div");
zone.style.border = "2px dashed #bbb";
zone.style.minHeight = "80px";
zone.style.padding = "8px";
zone.style.borderRadius = "10px";
zone.style.transition = "0.15s ease";
// DnD highlight
zone.addEventListener("dragenter", () => {
zone.style.borderColor = "#4f46e5";
zone.style.background = "#eef2ff";
});
zone.addEventListener("dragleave", () => {
zone.style.borderColor = "#bbb";
zone.style.background = "";
});
zone.addEventListener("dragover", e => e.preventDefault());
zone.addEventListener("drop", e => {
e.preventDefault();
const key = e.dataTransfer.getData("text/plain");
if (!itemMap.has(key)) return;
const copy = carts.map(d => d.slice());
if (!copy[idx].includes(key)) copy[idx].push(key); // prevent duplicates in the same basket
mutable carts = copy;
zone.style.borderColor = "#bbb";
zone.style.background = "";
render();
});
// items display
for (const key of basket) {
const chip = document.createElement("span");
chip.textContent = itemMap.get(key).emoji;
chip.style.fontSize = "28px";
chip.style.padding = "2px 6px";
chip.style.cursor = "pointer";
chip.title = `Click to remove ${itemMap.get(key).label}`;
chip.addEventListener("click", () => {
const copy = carts.map(d => d.slice());
const id = copy[idx].indexOf(key);
if (id>=0) copy[idx].splice(id,1);
mutable carts = copy;
render();
});
zone.appendChild(chip);
}
card.appendChild(zone);
container.appendChild(card);
});
}
render();
return container;
}function unique(arr){ return Array.from(new Set(arr)); }
function combos(arr, k){
const res=[]; const n=arr.length;
function rec(start, path){
if (path.length===k){ res.push(path.slice()); return; }
for (let i=start;i<n;i++) rec(i+1, path.concat(arr[i]));
}
rec(0,[]); return res;
}
function apriori(transactions, minsup=0.2, minconf=0.5){
const N = transactions.length;
if (!N) return [];
const supCount = new Map();
const allItems = unique(transactions.flat());
function support(itemset){
const key = itemset.slice().sort().join("|");
if (supCount.has(key)) return supCount.get(key)/N;
let c=0; outer: for (const t of transactions){
for (const it of itemset) if (!t.includes(it)) continue outer;
c++;
}
supCount.set(key, c);
return c/N;
}
// frequent 1-itemsets
let L = allItems.map(i=>[i]).filter(s=>support(s) >= minsup);
const Lall = [...L];
// frequent k-itemsets
for (let k=2; L.length>0; k++){
const cand = combos(unique(L.flat()), k)
.filter(s => combos(s, k-1).every(sub => support(sub) >= minsup));
L = cand.filter(s => support(s) >= minsup);
Lall.push(...L);
}
// association rules from all non-empty proper subsets
const rules = [];
for (const S of Lall){
if (S.length < 2) continue;
const supS = support(S);
for (let i=1; i<S.length; i++){
for (const A of combos(S, i)){
const B = S.filter(x => !A.includes(x));
const supA = support(A);
const supB = support(B);
const conf = supS / (supA || 1e-12);
const lift = conf / (supB || 1e-12);
if (conf >= minconf){
rules.push({
A: A.slice().sort(),
B: B.slice().sort(),
sup: supS,
conf: conf,
lift: lift
});
}
}
}
}
return rules;
}// Format results and sort by lift
rulesResFormatted = rulesRes
.map(r => ({
rule: `${r.A.map(k => itemMap.get(k).emoji).join(" ")} → ${r.B.map(k => itemMap.get(k).emoji).join(" ")}`,
A: r.A.join(", "),
B: r.B.join(", "),
support: r.sup.toFixed(2),
confidence: r.conf.toFixed(2),
lift: r.lift.toFixed(2)
}))
.sort((a, b) => +b.lift - +a.lift)// Download transactions as CSV
downloadTx = {
const csv = d3.csvFormat(txRows);
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = html`<a href=${url} download="transactions.csv">Download transactions.csv</a>`;
invalidation.then(() => URL.revokeObjectURL(url));
return a;
}A widely known case is “Market Basket Analysis”,
especially the famous “Beer and Diaper” example, often used to explain the principle of Association Rule:
1. Beer🍺 and Diaper👶
Finding: From supermarket purchase data, it was observed that:
“Customers who buy diapers are also likely to buy beer.”
Reasoning: These customers are often fathers who purchase diapers and may also grab beer to relax at home.
Application:
Place beer🍺 and diapers👶 close together to encourage cross-sales
Offer bundle promotions or discounts when purchased together
2. Bread🍞 and Butter🧈
Finding: Customers who buy bread🧈 are also likely to buy butter🍞.
Reasoning: These items are commonly consumed together, especially for breakfast or snacks.
Application:
Offer special promotions for bread🍞-and-butter🧈 bundles
Place the products together to increase sales
3. Milk🥛 and Cereal🌾
Finding: Customers who buy milk🌾 are also likely to buy cereal🌾.
Reasoning: These products are typically consumed together as a breakfast meal.
Application:
4. Coffee☕ and Sugar🍬
Finding: Customers who buy coffee☕ are also likely to buy sugar🍬 or creamer🧋.
Reasoning: These items are typically used together to prepare coffee☕.
Application:
Create bundle promotions including coffee☕, sugar🍬, and creamer🧋
Place these products close together for customer convenience
5. Chips🍟 and Soda🥤
Finding: Customers who buy chips🍟 are also likely to buy soda🥤.
Reasoning: These items are often consumed together during movies or parties.
Application:
“Buy 2 bags of chips🍟, get 1 can of soda🥤 free”
6. Toothbrush🪥 and Toothpaste🧴
Finding: Customers who buy a toothbrush🪥 are also likely to buy toothpaste🧴.
Reasoning: These are essential daily-use items that go hand in hand.
Application:
“Buy a toothbrush🪥, get toothpaste🧴 free”
Analyze customer purchasing behavior
Identify “which products are often bought together” to create promotions or place items strategically
Examples:
Customers who buy coffee often buy cookies → Promotion: “Coffee + Cookies at a special price”
Customers who buy printers often buy ink → Place products together to boost sales
Recommend related products to increase sales per transaction
Examples:
E-commerce websites suggest products with “Customers who bought this also bought…”
Recommend add-ons such as buying a phone📱 → suggest charger🔌 and head phone🎧
Recommend relevant products, services, or content
Used in e-commerce, streaming platforms, and news websites
Examples:
Netflix and YouTube recommend videos based on other users’ viewing behavior
Amazon suggests products based on other customers’ purchase history
Detect unusual transaction behavior to prevent fraud
Examples:
Identify abnormal credit card️💳 use, e.g., purchases in distant locations within a short time
Analyze transaction patterns to detect fraud or money laundering
Analyze the relationships between symptoms and diseases to support diagnosis
Examples:
Patients with symptoms X and Y often have disease Z → improve diagnostic accuracy
Analyze co-prescribed medications to detect side effects
Analyze purchasing patterns to plan stock effectively
Examples:
Suggest content on news sites, blogs, or media platforms
Examples:
Analyze learning behavior to recommend suitable content
Examples:
\[ X \Rightarrow Y \] Where:
Meaning: If X is purchased, there is a tendency that Y will also be purchased.
Examples: If a customer buys bread, they are also likely to buy butter.
Formula:
\[ \begin{aligned} Support(X) &= \frac{\text{# transactions containing } X}{\text{Total number of transactions}} \\ Support(X \Rightarrow Y) &= \frac{\text{# transactions containing both } X \text{ and } Y}{\text{Total number of transactions}} \end{aligned} \]
Note: The symbol “#” indicates “Number of …”
Formula:
\[ Confidence(X \Rightarrow Y) = \frac{\text{# transactions containing both } X \text{ and } Y}{\text{# transactions containing } X} \]
\[ Confidence(Y \Rightarrow X) = \frac{\text{# transactions containing both } X \text{ and } Y}{\text{# transactions containing } Y} \]
Note: The symbol “#” indicates “Number of …”
Formula:
\[ \begin{aligned} Lift(X \Rightarrow Y) &= \frac{Support(X \Rightarrow Y)}{Support(X) \times Support(Y)} \\ &= \frac{Confidence(X \Rightarrow Y)}{Support(Y)} \end{aligned} \]
If the association rule is
{Bagel} \(\Rightarrow\) {Cream Cheese}
and the Lift = 2,
this means if a customer buys bagels, the chance of buying cream cheese is twice as high as normal.
From 100 transactions:
Support (S):
\[
Support(Bread \Rightarrow Butter) = \frac{20}{100} = 0.2
\]
Confidence (C):
\[
Confidence(Bread \Rightarrow Butter) = \frac{20}{40} = 0.5
\]
Lift (L):
\[\begin{aligned}
&Lift(Bread \Rightarrow Butter) =\\
&\dfrac{Support(Bread \Rightarrow Butter)}{Support(Bread)\times Support(Butter)}\\
&= \dfrac{0.20}{0.4\times 0.5}=\frac{0.5}{0.5} = 1
\end{aligned}\]
Here is a sample of 10 transactions to be used for creating a table in Excel and for calculating Support, Confidence, and Lift using SUMIF() in Excel.
| Transaction ID | Item 1 | Item 2 | Item 3 |
|---|---|---|---|
| 1 | Bread | Butter | Milk |
| 2 | Bread | Jam | |
| 3 | Bread | Butter | |
| 4 | Bread | Milk | |
| 5 | Butter | Jam | |
| 6 | Bread | Butter | Jam |
| 7 | Milk | Butter | |
| 8 | Bread | Milk | Butter |
| 9 | Bread | Jam | Butter |
| 10 | Milk |
| Transaction ID | Bread | Butter | Milk | Jam |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 0 |
| 2 | 1 | 0 | 0 | 1 |
| 3 | 1 | 1 | 0 | 0 |
| 4 | 1 | 0 | 1 | 0 |
| 5 | 0 | 1 | 0 | 1 |
| 6 | 1 | 1 | 0 | 1 |
| 7 | 0 | 1 | 1 | 0 |
| 8 | 1 | 1 | 1 | 0 |
| 9 | 1 | 1 | 0 | 1 |
| 10 | 0 | 0 | 1 | 0 |
SUMIF in Excel to Calculate Support, Confidence, and Lift2. Calculate Confidence
Confidence(A \(\Rightarrow\) B) = (Number of times A and B appear together) ÷ (Number of times A appears).
Example Confidence(Bread \(\Rightarrow\) Butter)
(Counts how many times both Bread and Butter are 1, then divides by the number of times Bread is 1)
Use this dataset to answer the following:
Googledrive
Milk and Bread may appear together often simply because both are purchased frequently, not because buying one leads to the other.| Lift Value | Meaning | Decision Strategy |
|---|---|---|
| > 1 | Positive relationship | Use for cross-selling, bundling, and placement |
| ≈ 1 | No relationship | Not very useful for decision-making |
| < 1 | Negative relationship | Avoid cross-selling; adjust marketing strategy |
(async () => {
// ===== Create an isolated Shadow DOM (no CSS bleed) =====
const host = html`<div style="display:block;"></div>`;
const root = host.attachShadow({mode: "open"});
// ============= Root markup inside Shadow =============
const box = document.createElement("div");
box.innerHTML = `
<style>
/* All styles are scoped to this shadow root only */
:host{all:initial}
.wrap{max-width:1220px;font:14px system-ui, -apple-system, Segoe UI, Roboto, sans-serif;color:#0b5aa2}
.grid-ctrl{display:grid;grid-template-columns:repeat(3,minmax(260px,1fr));gap:12px;margin-bottom:12px}
.grid-editor{display:grid;grid-template-columns:1fr 2fr;gap:14px}
.panel{padding:10px;border:1px solid #bcd0e5;border-radius:10px;background:#fff}
.panel.soft{background:#f7fbff}
.headerline{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
.title{font-weight:700}
.subtle{font-size:12px;color:#2a6ebb}
.muted{color:#666}
/* Cerulean-ish buttons (scoped) */
.btn{padding:6px 12px;border:1px solid #bcd0e5;background:#e9f2fb;color:#0b5aa2;border-radius:8px;cursor:pointer;transition:all .12s ease}
.btn:hover{background:#dcecff}
.btn-primary{background:#2fa4e7;color:#fff;border-color:#1992d4}
.btn-primary:hover{background:#1992d4}
.btn-warning{background:#fff3cd;color:#8a5a00;border-color:#ffe19a}
.btn-warning:hover{background:#ffe8a6}
.btn-danger{background:#fde2e1;color:#8c1e1e;border-color:#f5b5b3}
.btn-danger:hover{background:#ffc9c7}
.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border:1px solid #cfe2ff;border-radius:9999px;background:#fff;user-select:none;box-shadow:0 1px 0 rgba(0,0,0,.03)}
.chip .x{cursor:pointer;color:#999}
.basket{min-height:44px;padding:8px;border:2px dashed #cbe1f7;border-radius:10px;background:#fbfdff}
.basket.dragover{background:#eef6ff}
.row{display:flex;gap:8px;align-items:flex-start}
.rowHead{width:120px;text-align:right;margin-top:8px;color:#2a6ebb}
.badge{display:inline-block;padding:4px 10px;border-radius:9999px;background:#e9f2fb;color:#0b5aa2;border:1px solid #cfe2ff;font-size:12px}
table.tbl{width:100%;border-collapse:collapse;font-size:13px}
table.tbl th, table.tbl td{border-bottom:1px solid #eee;padding:6px 8px}
table.tbl th{border-bottom:1px solid #bcd0e5;background:#f7fbff;cursor:pointer;user-select:none}
table.tbl th.nosort{cursor:default}
.tdc{text-align:center}
.tdr{text-align:right}
.toast{position:fixed;right:18px;bottom:18px;background:#2fa4e7;color:#fff;padding:8px 12px;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12);font-size:13px;z-index:999999}
.th-ind::after{content:""; margin-left:6px; border:4px solid transparent; display:inline-block; transform:translateY(-1px)}
.th-ind.asc::after{border-bottom-color:#2a6ebb}
.th-ind.desc::after{border-top-color:#2a6ebb}
.math{
font:13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
background:#f7fbff; border:1px solid #cfe2ff; color:#0b5aa2;
border-radius:6px; padding:2px 6px; display:inline-block;
margin:2px 6px 0 0; white-space:nowrap;
}
</style>
<div class="wrap">
<div id="ctrl" class="grid-ctrl"></div>
<div class="grid-editor">
<div class="panel soft">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<b>Product Palette</b>
<span class="subtle">Drag an emoji into a basket (≤4 items per basket)</span>
</div>
<div id="palette" style="display:flex;flex-wrap:wrap;gap:8px;"></div>
</div>
<div class="panel">
<div class="headerline">
<b class="title">Baskets</b>
<button id="btnAdd" class="btn">Add Random Basket</button>
<button id="btnClearAll" class="btn btn-danger">🧹 Clear All</button>
<button id="btnRecomp" class="btn">Recompute</button>
<button id="btnCopy" class="btn btn-primary">Copy One-Hot CSV</button>
</div>
<div id="baskets"></div>
</div>
</div>
<div id="out" style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px;">
<div id="allk" class="panel"></div>
<div id="rules" class="panel"></div>
</div>
<div id="tables" style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px;">
<div id="txTable" class="panel"></div>
<div id="onehotTbl" class="panel" style="overflow:auto;"></div>
</div>
</div>
`;
root.appendChild(box);
// ====== Grab refs from Shadow ======
const ctrl = root.getElementById("ctrl");
const paletteDiv = root.getElementById("palette");
const basketsDiv = root.getElementById("baskets");
const btnAdd = root.getElementById("btnAdd");
const btnClearAll= root.getElementById("btnClearAll");
const btnRecomp = root.getElementById("btnRecomp");
const btnCopy = root.getElementById("btnCopy");
const allkEl = root.getElementById("allk");
const rulesEl = root.getElementById("rules");
const txTableEl = root.getElementById("txTable");
const onehotEl = root.getElementById("onehotTbl");
// ========= Controls (mount Observable Inputs inside Shadow) =========
const nInp = Inputs.range([2, 20], {label:"Number of products (n)", value:10, step:1});
const minSupInp = Inputs.range([0.00, 0.60], {label:"Min Support", value:0.10, step:0.01});
const minConfInp= Inputs.range([0.00, 1.00], {label:"Min Confidence", value:0.50, step:0.05});
const kMaxInp = Inputs.range([2, 4], {label:"Max itemset size (k_max)", value:4, step:1});
const titleBox = html`<div class="panel" style="padding:8px 10px;">
<div><b>How to:</b> Drag an emoji into a basket or click “Add Random Basket”. Each basket can contain at most 4 distinct items. Click × on a chip to remove it.</div>
</div>`;
ctrl.append(nInp, minSupInp, minConfInp, kMaxInp, titleBox);
// ================= Helpers =================
const EMO_ALL = [
"🥛","🍞","🍎","🧀","🥚","🥤","🍫","🧻","🧴","🍪",
"🍺","🍜","🍗","🧂","☕️","🍌","🥦","🍊","🥔","🍚",
"🍇","🍖","🧈","🍯","🧃","🥨","🍦","🍟","🍔","🍣",
"🍤","🥫","🍷","🥟","🍍","🥕","🧅","🧄","🍅","🥬"
];
const pct = x => `${(x*100).toFixed(1)}%`;
function rngSeed(s){ return function(){ let t=s+=0x6D2B79F5; t=Math.imul(t^t>>>15,t|1); t^=t+Math.imul(t^t>>>7,t|61); return ((t^t>>>14)>>>0)/4294967296 } }
function pickK(arr, k, rng){
const idx = arr.map((_,i)=>i);
for (let i=idx.length-1;i>0;i--){ const j=Math.floor((rng?rng():Math.random())*(i+1)); [idx[i],idx[j]]=[idx[j],idx[i]]; }
return idx.slice(0,Math.min(k,idx.length)).map(i=>arr[i]);
}
function showToast(msg){
const t = document.createElement("div");
t.className = "toast";
t.textContent = msg;
root.appendChild(t);
setTimeout(()=>{ t.remove(); }, 1600);
}
function makeSortable(table){
if (!table) return;
const thead = table.querySelector("thead");
if (!thead) return;
const ths = [...thead.querySelectorAll("th")].filter(th=>!th.classList.contains("nosort"));
ths.forEach((th, idx)=>{
th.classList.add("th-ind");
th.dataset.order = "desc";
th.addEventListener("click", ()=>{
const type = th.dataset.type || "text";
const tbody = table.querySelector("tbody");
const rows = [...tbody.querySelectorAll("tr")];
const current = th.dataset.order === "asc" ? "desc" : "asc";
[...thead.querySelectorAll("th")].forEach(x=>x.classList.remove("asc","desc"));
th.classList.add(current);
th.dataset.order = current;
const dir = current === "asc" ? 1 : -1;
rows.sort((a,b)=>{
const A = a.children[idx].innerText.trim();
const B = b.children[idx].innerText.trim();
if ((th.dataset.type||"") === "num"){
const aNum = parseFloat(A.replace(/[%]/g,""));
const bNum = parseFloat(B.replace(/[%]/g,""));
return (aNum - bNum) * dir;
} else {
return A.localeCompare(B) * dir;
}
});
rows.forEach(r=>tbody.appendChild(r));
});
});
}
function combos(arr, k){
const res = [];
function dfs(start, path){
if (path.length === k){ res.push(path.slice()); return; }
for (let i=start;i<arr.length;i++) dfs(i+1, path.concat(arr[i]));
}
dfs(0, []);
return res;
}
// ================= State =================
let products = EMO_ALL.slice(0, nInp.value);
let txns = []; // [["🥛","🍞"], ["🍎"], ...]
let countsAll = {}; // k -> Map("a,b"->count)
const MAX_PER_BASKET = 4;
// ================= UI: Palette =================
function renderPalette(){
products = EMO_ALL.slice(0, nInp.value);
paletteDiv.innerHTML = "";
for (const it of products){
const chip = document.createElement("div");
chip.className = "chip";
chip.draggable = true;
chip.textContent = it;
chip.title = "Drag into a Basket";
chip.dataset.item = it;
chip.addEventListener("dragstart", e=>{
e.dataTransfer.setData("text/plain", it);
});
paletteDiv.appendChild(chip);
}
}
// ================= UI: Baskets =================
function renderBaskets(){
basketsDiv.innerHTML = "";
if (txns.length === 0) addRandomBasket();
txns.forEach((items, idx)=>{
const row = document.createElement("div");
row.className = "row";
const head = document.createElement("div");
head.className = "rowHead";
head.innerHTML = `🧺 <span class="badge">Basket ${idx+1}</span><br><span class="muted" style="font-size:12px;">(${items.length} item${items.length!==1?"s":""})</span>`;
const drop = document.createElement("div");
drop.className = "basket";
drop.dataset.index = idx;
// DnD handlers
drop.addEventListener("dragover", e=>{ e.preventDefault(); drop.classList.add("dragover"); });
drop.addEventListener("dragleave", ()=> drop.classList.remove("dragover"));
drop.addEventListener("drop", e=>{
e.preventDefault();
drop.classList.remove("dragover");
const it = e.dataTransfer.getData("text/plain");
if (!products.includes(it)) return;
if (txns[idx].includes(it)) return; // unique per basket
if (txns[idx].length >= MAX_PER_BASKET){ showToast("This shop allows up to 4 items per basket."); return; }
txns[idx].push(it);
renderBaskets();
});
// chips
const chipWrap = document.createElement("div");
chipWrap.style.display = "flex";
chipWrap.style.gap = "8px";
chipWrap.style.flexWrap = "wrap";
chipWrap.style.direction = "ltr";
for (const it of items){
const c = document.createElement("div");
c.className = "chip"; c.draggable = false;
c.innerHTML = `${it} <span class="x" title="remove">×</span>`;
c.querySelector(".x").addEventListener("click", ()=>{
txns[idx] = txns[idx].filter(z=>z!==it);
renderBaskets();
});
chipWrap.appendChild(c);
}
// actions
const actions = document.createElement("div");
actions.style.display = "flex"; actions.style.gap = "8px"; actions.style.alignItems = "center"; actions.style.marginLeft = "8px";
const btnDel = document.createElement("button"); btnDel.className = "btn btn-danger"; btnDel.textContent = "🗑️ Delete";
btnDel.title = "Remove this basket";
btnDel.addEventListener("click", ()=>{ txns.splice(idx,1); renderBaskets(); recompute(); });
const btnClearRow = document.createElement("button"); btnClearRow.className = "btn btn-warning"; btnClearRow.textContent = "🧹 Clear";
btnClearRow.title = "Clear items in this basket";
btnClearRow.addEventListener("click", ()=>{ txns[idx] = []; renderBaskets(); recompute(); });
actions.appendChild(btnDel); actions.appendChild(btnClearRow);
drop.appendChild(chipWrap);
row.appendChild(head); row.appendChild(drop); row.appendChild(actions);
basketsDiv.appendChild(row);
});
}
function addRandomBasket(){
const maxPick = Math.min(MAX_PER_BASKET, products.length);
const size = Math.max(1, Math.floor(Math.random()*maxPick) + 1); // 1..maxPick
const picked = pickK(products, size, null).sort((a,b)=> products.indexOf(a)-products.indexOf(b));
txns.push(picked);
}
// ====== Init with 5 random baskets (≤4 items each) ======
function seedFiveRandom(){
txns = [];
const rng = rngSeed(2025);
for (let b=0;b<5;b++){
const maxPick = Math.min(MAX_PER_BASKET, products.length);
const size = Math.max(1, Math.floor(rng()*maxPick) + 1); // 1..maxPick
const picked = pickK(products, size, rng).sort((a,b)=> products.indexOf(a)-products.indexOf(b));
txns.push(picked);
}
}
// ================= Counting / Frequent / Rules =================
function countAllItemsets(kmax){
countsAll = {};
for (let k=1;k<=kmax;k++) countsAll[k] = new Map();
const N = txns.length;
const MAX_UPDATES = 200000;
let updates = 0;
for (const t of txns){
const set = Array.from(new Set(t)).sort((a,b)=> products.indexOf(a)-products.indexOf(b));
for (let k=1;k<=kmax;k++){
if (set.length < k) break;
const cs = combos(set, k);
const m = countsAll[k];
for (const c of cs){
const key = c.join(",");
m.set(key, (m.get(key)||0)+1);
if (++updates > MAX_UPDATES){
showToast("Counting truncated (too many combinations). Raise min support or lower k_max.");
return {N, countsAll};
}
}
}
}
return {N, countsAll};
}
function buildFrequentAll(minSup, kmax){
const N = txns.length;
const all = [];
for (let k=1;k<=kmax;k++){
const m = countsAll[k];
const arr = [...m.entries()].map(([kstr,c])=>({
k, items: kstr.split(","), count:c, support: c / N
}));
const filtered = arr.filter(r=> r.support >= minSup).sort((a,b)=> b.support - a.support);
all.push({k, rows: filtered});
}
return {N, all};
}
function allAssociationRules(minSup, minConf, kmax){
const N = txns.length;
const rules = [];
const supportOf = (key)=>{
const len = key ? key.split(",").length : 0;
if (len===0) return 0;
const m = countsAll[len];
return (m && m.get(key) ? m.get(key)/N : 0);
};
for (let k=2;k<=kmax;k++){
const m = countsAll[k];
if (!m) continue;
for (const [key, cnt] of m.entries()){
const S = key.split(",");
const suppS = cnt / N;
if (suppS < minSup) continue;
for (let aSize = 1; aSize <= S.length-1; aSize++){
const As = combos(S, aSize);
for (const A of As){
const B = S.filter(x => !A.includes(x));
const Akey = A.join(","), Bkey = B.join(",");
const suppA = supportOf(Akey);
const suppB = supportOf(Bkey);
if (suppA <= 0 || suppB <= 0) continue;
const conf = suppS / suppA;
if (conf < minConf) continue;
const lift = conf / suppB;
rules.push({ rule: `${A.join(" ")} → ${B.join(" ")}`, support: suppS, confidence: conf, lift });
}
}
}
}
rules.sort((x,y)=> (y.lift - x.lift) || (y.confidence - x.confidence) || (y.support - x.support));
return rules.slice(0, 200);
}
// ================= One-Hot & CSV =================
function oneHotMatrix(){
const cols = products.slice();
const rows = txns.map(t => {
const set = new Set(t);
return cols.map(c => set.has(c) ? 1 : 0);
});
return {cols, rows};
}
function copyOneHotCSV(){
const {cols, rows} = oneHotMatrix();
const header = ["Basket", ...cols];
const lines = [header.join(",")];
for (let i=0;i<rows.length;i++){
const line = [ `#${i+1}`, ...rows[i] ].join(",");
lines.push(line);
}
const csv = lines.join("\n");
// Clipboard in Shadow still uses global navigator
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(csv).then(()=> showToast("Copied One-Hot CSV to clipboard"));
} else {
const ta = document.createElement("textarea"); ta.value = csv; root.appendChild(ta);
ta.select(); document.execCommand("copy"); ta.remove();
showToast("Copied One-Hot CSV to clipboard");
}
}
// ================= Rendering =================
function renderTxTable(){
const rows = txns.map((t,i)=>({id:i+1, size:t.length, items:t.join(" ")}));
txTableEl.innerHTML = `
<b>Transaction Table</b>
<div class="muted" style="font-size:12px;margin-top:2px;">Each row is a basket; “Size” = number of items in that basket (max 4).</div>
<table class="tbl" style="margin-top:6px;">
<thead><tr>
<th class="tdr nosort">Basket</th>
<th class="tdr nosort">Size</th>
<th class="nosort">Items</th>
</tr></thead>
<tbody>
${rows.map(r=>`
<tr><td class="tdr">#${r.id}</td><td class="tdr">${r.size}</td><td>${r.items || '<span class="muted">—</span>'}</td></tr>
`).join("")}
</tbody>
</table>
<div class="muted" style="margin-top:8px;font-size:12px;">
<b>Formula:</b> <span class="math">basket_size = count(distinct items in basket)</span>
</div>
`;
}
function renderOneHotTable(){
const {cols, rows} = oneHotMatrix();
const header = cols.map(c=>`<th class="tdc nosort" title="product">${c}</th>`).join("");
const body = rows.map((r,i)=>`<tr><td class="tdr">#${i+1}</td>${r.map(x=>`<td class="tdc">${x}</td>`).join("")}</tr>`).join("");
onehotEl.innerHTML = `
<b>One-Hot Encoding (0/1)</b>
<div class="muted" style="font-size:12px;margin-top:2px;">Rows = baskets (max 4 items), Columns = products (emoji)</div>
<div style="overflow:auto;margin-top:6px;">
<table class="tbl" style="min-width:${Math.max(400, 60*(cols.length+1))}px">
<thead><tr><th class="tdr nosort">Basket</th>${header}</tr></thead>
<tbody>${body}</tbody>
</table>
</div>
<div class="muted" style="margin-top:8px;font-size:12px;">
<b>Formula:</b>
<span class="math">x<sub>ij</sub> = 1</span> if product <span class="math">j</span> is in basket <span class="math">i</span>;
otherwise <span class="math">x<sub>ij</sub> = 0</span>.
</div>
`;
}
function renderAllKPanel(N, allGroups){
let sections = "";
for (const group of allGroups){
const k = group.k;
const rows = group.rows;
const tableId = `tbl-k-${k}`;
sections += `
<div style="margin-top:${k===1? '6px':'12px'};font-weight:600;">${k}-itemsets</div>
<table class="tbl" id="${tableId}" style="margin-top:4px;">
<thead><tr>
<th data-type="text">${k===1?'Item':'Items'}</th>
<th class="tdr" data-type="num">Support</th>
<th class="tdr" data-type="num">Count</th>
</tr></thead>
<tbody>
${
rows.length
? rows.map(r=>`
<tr>
<td>${r.items.join(" ")}</td>
<td class="tdr">${pct(r.support)}</td>
<td class="tdr">${r.count}</td>
</tr>`).join("")
: `<tr><td colspan="3"><span class="muted">No ${k}-itemsets meet the threshold.</span></td></tr>`
}
</tbody>
</table>
`;
}
allkEl.innerHTML = `
<b>All k-itemsets</b>
<div class="muted" style="font-size:12px;">
Total Transactions (N) = <b>${N}</b> • Min support = <b>${pct(minSupInp.value)}</b> • k from 1 to <b>${Math.min(kMaxInp.value, 4)}</b>
</div>
${sections}
<div class="muted" style="margin-top:8px;font-size:12px;">
<b>Formula:</b>
<span class="math">Support = count(itemset) / N</span>
<span class="math">Count = # supporting transactions</span>
</div>
`;
// <<— ทำหลังจาก set innerHTML แล้วค่อยผูกการ sort แต่ละ table
for (const group of allGroups){
const tbl = allkEl.querySelector(`#tbl-k-${group.k}`);
makeSortable(tbl);
}
}
function renderRules(N, rules){
rulesEl.innerHTML = `
<b>Association Rules (all sizes)</b>
<div class="muted" style="font-size:12px;">
Filters: min support = <b>${pct(minSupInp.value)}</b>,
min confidence = <b>${pct(minConfInp.value)}</b>;
Total Transactions (N) = <b>${N}</b>
</div>
<table class="tbl" id="tbl-rules" style="margin-top:6px;">
<thead><tr>
<th data-type="text">Rule</th>
<th class="tdr" data-type="num">Support</th>
<th class="tdr" data-type="num">Confidence</th>
<th class="tdr" data-type="num">Lift</th>
</tr></thead>
<tbody>
${rules.map(r=>`
<tr>
<td>${r.rule}</td>
<td class="tdr">${pct(r.support)}</td>
<td class="tdr">${pct(r.confidence)}</td>
<td class="tdr">${r.lift.toFixed(2)}</td>
</tr>
`).join("") || `<tr><td colspan="4"><span class="muted">No rules pass the thresholds.</span></td></tr>`}
</tbody>
</table>
<div class="muted" style="margin-top:8px;font-size:12px;">
<b>Formulas:</b>
<span class="math">support(A→B) = support(A∪B)</span>
<span class="math">confidence(A→B) = support(A∪B) / support(A)</span>
<span class="math">lift(A→B) = confidence(A→B) / support(B)</span>
</div>
`;
makeSortable(rulesEl.querySelector("#tbl-rules"));
}
// ================= Orchestration =================
function recompute(){
const kmax = Math.min(kMaxInp.value, 4, products.length);
countAllItemsets(kmax);
const {N, all} = buildFrequentAll(minSupInp.value, kmax);
renderAllKPanel(N, all);
const rules = allAssociationRules(minSupInp.value, minConfInp.value, kmax);
renderRules(N, rules);
renderTxTable();
renderOneHotTable();
}
// ================= Events =================
function resetAll(){
products = EMO_ALL.slice(0, nInp.value);
seedFiveRandom();
renderPalette();
renderBaskets();
recompute();
}
btnAdd.addEventListener("click", ()=>{ addRandomBasket(); renderBaskets(); recompute(); });
btnClearAll.addEventListener("click", ()=>{ txns = []; renderBaskets(); recompute(); });
btnRecomp.addEventListener("click", recompute);
btnCopy.addEventListener("click", copyOneHotCSV);
nInp.addEventListener("input", resetAll);
minSupInp.addEventListener("input", recompute);
minConfInp.addEventListener("input", recompute);
kMaxInp.addEventListener("input", recompute);
// ===== init =====
renderPalette();
seedFiveRandom();
renderBaskets();
recompute();
// Mount the isolated app
return host;
})();1. Coverage
Formula:
\[
coverage(X \Rightarrow Y) = \frac{support(X)}{|D|}
\]
📌 Meaning: Coverage shows how often the antecedent (\(X\)) appears in the dataset.
✅ Example:
- Dataset with 1,000 transactions
- \(X =\) {Bread}
- Found in 200 transactions
- \(coverage(X \Rightarrow Y) = \frac{200}{1000} = 0.2\) or 20%
🔹 Use Case:
- High coverage = \(X\) is common → rule may be more significant.
- Low coverage = \(X\) is rare → rule may not represent overall data well.
2. Strength
Formula:
\[
strength(X \Rightarrow Y) = \frac{support(Y)}{support(X)}
\]
📌 Meaning: Strength compares how frequent \(Y\) is relative to \(X\).
✅ Example:
- \(support(X) = 200\) (transactions with bread)
- \(support(Y) = 500\) (transactions with milk)
- \(strength(X \Rightarrow Y) = \frac{500}{200} = 2.5\)
🔹 Use Case:
- Strength > 1 → \(Y\) occurs more often than \(X\) → \(Y\) is widespread.
- Strength < 1 → \(Y\) occurs less often than \(X\) → \(X\) may be more specific or unrelated to \(Y\).
3. Leverage
Formula:
\[
leverage(X \Rightarrow Y) = \frac{support(X \Rightarrow Y) \times |D| - support(X) \times support(Y)}{|D|^2}
\]
📌 Meaning: Leverage shows whether \(X\) and \(Y\) occur together more often than expected by chance.
✅ Example:
- \(|D| = 1000\) (total transactions)
- \(support(X \Rightarrow Y) = 100\) (Bread and Milk together)
- \(support(X) = 200\), \(support(Y) = 500\)
\[ \frac{(100 \times 1000) - (200 \times 500)}{1000^2} = \frac{100000 - 100000}{1000000} = 0 \]
🔹 Use Case:
- If leverage > 0 → \(X\) and \(Y\) co-occur more often than expected → correlated.
- If leverage < 0 → \(X\) and \(Y\) co-occur less than expected → possibly unrelated.
- If leverage = 0 → \(X\) and \(Y\) occur randomly.
| Metric | Meaning | Use Case |
|---|---|---|
| Coverage | Frequency of \(X\) in the dataset | Influence of antecedent (\(X\)) |
| Strength | Frequency of \(Y\) relative to \(X\) | Robustness of the outcome (\(Y\)) |
| Leverage | Co-occurrence beyond expectation | Detecting genuine relationships |
We often see the terms Antecedent and Consequent,
which are the two main components of a rule:
\[ \text{Antecedent} \Rightarrow \text{Consequent} \]
1️⃣ Antecedent (Condition / LHS)
Refers to the item or set of items that occur first.
Can be thought of as the cause or condition in the rule.
✅ Example:
- Customer buys Bread, Bread = Antecedent
2️⃣ Consequent (Outcome / RHS)
✅ Example:
- Customer buys Bread and also buys Milk, Milk = Consequent
Easy Way to Remember
| Rule | Antecedent (LHS) | Consequent (RHS) |
|---|---|---|
| {Bread} ⟶ {Milk} | Bread | Milk |
| {Diaper} ⟶ {Beer} | Diaper | Beer |
| {Laptop} ⟶ {Mouse} | Laptop | Mouse |
You need to install the Associate Add-on to use this feature.
(async () => {
// ----- Plot (fallback) -----
let Plot;
try { Plot = await require("@observablehq/plot@0.6.17"); }
catch { const m = await import("https://esm.sh/@observablehq/plot@0.6?bundle"); Plot = m.default || m; }
// ----- Shell -----
const box = html`<div style="max-width:1100px;margin:0 auto;font:14px system-ui;">
<style>
.layout{display:grid;grid-template-columns:380px 1fr;gap:16px;align-items:start}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:12px}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.label{font-weight:700}
.hint{font-size:12px;color:#6b7280}
select,input[type=number],input[type=range]{padding:6px 8px;border:1px solid #d1d5db;border-radius:8px}
.btn{border:1px solid #d1d5db;background:#f9fafb;border-radius:8px;padding:6px 10px;cursor:pointer}
.btn:active{transform:translateY(1px)}
.mono{font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}
table.tbl{width:100%;border-collapse:collapse}
table.tbl th,table.tbl td{border-bottom:1px solid #eee;padding:6px 8px;text-align:left}
table.tbl th{border-bottom:1px solid #ddd}
.emopt{font-size:16px}
@media(max-width:980px){.layout{grid-template-columns:1fr}}
.math { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
</style>
<div class="layout">
<!-- LEFT: Controls -->
<div class="card">
<div class="label">Choose items (Emoji)</div>
<div class="row">
<div><div class="hint">Item A</div><select id="selA" class="emopt"></select></div>
<div><div class="hint">Item B</div><select id="selB" class="emopt"></select></div>
<div><div class="hint">Item C</div><select id="selC" class="emopt"></select></div>
</div>
<div class="row" style="margin-top:6px">
<div>
<div class="hint">Preset (realistic association)</div>
<select id="preset" class="emopt">
<option value="cafe">☕️ ⇒ 🥐 (Cafe set)</option>
<option value="burger">🍔 ⇒ 🍟 (Burger set)</option>
<option value="pizza">🍕 ⇒ 🥤 (Pizza set)</option>
<option value="breakfast">🥛 ⇒ 🥣 (Breakfast set)</option>
<option value="sushi">🍣 ⇒ 🧂 (Sushi + soy sauce)</option>
<option value="none" selected>— Manual —</option>
</select>
</div>
<button id="applyPreset" class="btn">Apply preset</button>
</div>
<div class="label" style="margin-top:10px">Transactions (N)</div>
<div class="row">
<input id="Nnum" type="number" min="50" max="20000" step="50" value="1000"/>
<input id="Nrng" type="range" min="50" max="20000" step="50" value="1000" style="width:55%"/>
</div>
<div class="label" style="margin-top:10px">Base probabilities</div>
<div class="row">
<span class="hint" style="width:74px">P(A)</span><input id="pA" type="number" min="0" max="1" step="0.01" value="0.40"/>
<span class="hint" style="width:74px">P(B)</span><input id="pB" type="number" min="0" max="1" step="0.01" value="0.35"/>
<span class="hint" style="width:74px">P(C)</span><input id="pC" type="number" min="0" max="1" step="0.01" value="0.25"/>
</div>
<div class="label" style="margin-top:10px">Pattern injection (A ⇒ B)</div>
<div class="row">
<span class="hint" style="width:120px">P(B | A)</span>
<input id="pBgivenA" type="number" min="0" max="1" step="0.01" value="0.70"/>
</div>
<label class="row" style="gap:8px;margin-top:6px">
<input id="keepMarg" type="checkbox" checked/>
<span class="hint">Keep marginal P(B) ≈ base (solve P(B|¬A))</span>
</label>
<div class="row" style="margin-top:10px">
<button id="build" class="btn">Generate</button>
<button id="copyTX" class="btn">Copy CSV (transactions)</button>
<button id="copyOH" class="btn">Copy CSV (one-hot)</button>
<span id="status" class="hint"></span>
</div>
</div>
<!-- RIGHT: Results -->
<div class="card">
<div class="label">Metrics for rule <span id="ruleName" class="mono"></span></div>
<div id="metrics" class="mono" style="margin-top:6px; white-space:pre;"></div>
<div id="explain" class="hint" style="margin-top:6px"></div>
<div class="row" style="margin-top:10px"><span class="label">Supports</span></div>
<div id="plot"></div>
<div class="row" style="margin-top:10px"><span class="label">Sample transactions</span></div>
<div id="table"></div>
<div class="row" style="margin-top:12px"><span class="label">Definitions</span></div>
<div class="math" style="white-space:pre; font-size:13px; line-height:1.5;">
Supp(A) = P(A) = #{ t : A ∈ t } / N
Supp(A ⇒ B) = Supp(A ∩ B) = P(A ∩ B)
Con(A ⇒ B) = Confidence(A ⇒ B) = P(B | A) = Supp(A ∩ B) / Supp(A)
Lift(A ⇒ B) = P(B | A) / P(B) = Supp(A ∩ B) / (Supp(A) · Supp(B))
</div>
</div>
</div>
</div>`;
// ----- Emoji options -----
const EMOJI = [
"☕️ coffee","🥐 croissant","🧁 cupcake","🍰 cake","🍩 donut","🍪 cookie",
"🍔 burger","🍟 fries","🌭 hotdog","🍕 pizza","🥤 soda","🍗 chicken",
"🍣 sushi","🧂 soy-sauce","🍚 rice","🥗 salad","🌮 taco","🌯 burrito",
"🥛 milk","🥣 cereal","🍞 bread","🧀 cheese","🍎 apple","🍌 banana","🍇 grapes",
"🍺 beer","🍷 wine","🥃 whisky","🧻 tissue","🧴 shampoo"
];
function fillEmojiSelect(sel){
sel.innerHTML = "";
EMOJI.forEach(s=>{
const o = document.createElement("option");
o.textContent = s.split(" ")[0] + " " + s.split(" ").slice(1).join(" ");
o.value = s.split(" ")[0]; // pure emoji
sel.append(o);
});
}
// ----- Refs -----
const selA = box.querySelector("#selA");
const selB = box.querySelector("#selB");
const selC = box.querySelector("#selC");
const preset = box.querySelector("#preset");
const applyPreset = box.querySelector("#applyPreset");
const Nnum = box.querySelector("#Nnum");
const Nrng = box.querySelector("#Nrng");
const pA = box.querySelector("#pA");
const pB = box.querySelector("#pB");
const pC = box.querySelector("#pC");
const pBgivenA = box.querySelector("#pBgivenA");
const keepMarg = box.querySelector("#keepMarg");
const buildBtn = box.querySelector("#build");
const copyTX = box.querySelector("#copyTX");
const copyOH = box.querySelector("#copyOH");
const status = box.querySelector("#status");
const ruleName = box.querySelector("#ruleName");
const metrics = box.querySelector("#metrics");
const explain = box.querySelector("#explain");
const plotWrap = box.querySelector("#plot");
const tableWrap= box.querySelector("#table");
fillEmojiSelect(selA); fillEmojiSelect(selB); fillEmojiSelect(selC);
selA.value = "☕️"; selB.value = "🥐"; selC.value = "🧁";
// Preset mapping
const PRESETS = {
cafe: {A:"☕️", B:"🥐", C:"🧁", pA:.45, pB:.35, pC:.20, pBgivenA:.75},
burger: {A:"🍔", B:"🍟", C:"🥤", pA:.35, pB:.30, pC:.40, pBgivenA:.70},
pizza: {A:"🍕", B:"🥤", C:"🍺", pA:.28, pB:.50, pC:.22, pBgivenA:.68},
breakfast: {A:"🥛", B:"🥣", C:"🍌", pA:.30, pB:.25, pC:.22, pBgivenA:.65},
sushi: {A:"🍣", B:"🧂", C:"🍺", pA:.22, pB:.18, pC:.30, pBgivenA:.80}
};
function applyPresetValues(){
const key = preset.value;
if (key==="none") return;
const s = PRESETS[key];
selA.value = s.A; selB.value = s.B; selC.value = s.C;
pA.value = s.pA; pB.value = s.pB; pC.value = s.pC;
pBgivenA.value = s.pBgivenA;
rebuild();
}
applyPreset.addEventListener("click", applyPresetValues);
// sync N slider
Nnum.addEventListener("input", ()=>{ Nrng.value = Nnum.value; });
Nrng.addEventListener("input", ()=>{ Nnum.value = Nrng.value; });
// show rule label
function updateRuleLabel(){ ruleName.textContent = `${selA.value} ⇒ ${selB.value}`; }
[selA, selB].forEach(el=> el.addEventListener("change", updateRuleLabel));
updateRuleLabel();
// RNG + helpers
const bern = (p)=> Math.random() < p;
const clamp01 = (x)=> Math.max(0, Math.min(1, x));
// fallback picker to avoid empty transactions
function pickOneFallback(pA0, pB0, pC0){
const s = pA0 + pB0 + pC0 || 1;
const r = Math.random();
const a = pA0 / s, b = pB0 / s;
if (r < a) return [true,false,false];
if (r < a + b) return [false,true,false];
return [false,false,true];
}
// Generate N transactions with A⇒B, NO empty rows
function generate(N, A, B, C, pA0, pB0, pC0, qBA, keepMargB){
// solve P(B|¬A) to preserve marginal P(B): pB0 = qBA*pA0 + qBnotA*(1-pA0)
let qBnotA = pB0;
if (keepMargB && pA0 < 1 - 1e-12){
qBnotA = clamp01((pB0 - qBA*pA0)/(1 - pA0));
}
const rows = [];
let nA=0, nB=0, nAB=0;
for (let i=0;i<N;i++){
let hasA = bern(pA0);
let hasB = hasA ? bern(qBA) : bern(keepMargB ? qBnotA : pB0);
let hasC = bern(pC0);
// enforce at least one item
if (!hasA && !hasB && !hasC){
const [fa, fb, fc] = pickOneFallback(pA0, pB0, pC0);
hasA = fa; hasB = fb; hasC = fc;
}
const items = [];
if (hasA) items.push(A);
if (hasB) items.push(B);
if (hasC) items.push(C);
rows.push(items);
if (hasA) nA++;
if (hasB) nB++;
if (hasA && hasB) nAB++;
}
return {rows, counts:{nA,nB,nAB,N}, qBnotA};
}
function calcMetrics({nA,nB,nAB,N}){
const sA = nA/N, sB = nB/N, sAB = nAB/N;
const conf = nA? sAB/sA : 0; // P(B|A)
const lift = sB? conf/sB : 0; // P(B|A)/P(B)
const indep = sA*sB; // expected if independent
return {sA,sB,sAB,conf,lift,indep};
}
function fmt(x,d=3){ return (isFinite(x)? x.toFixed(d) : "—"); }
function renderPlot(m, A, B){
plotWrap.innerHTML = "";
const data = [
{k:`support(${A})`, v:m.sA},
{k:`support(${B})`, v:m.sB},
{k:`support(${A} ∩ ${B})`, v:m.sAB},
{k:`expected under independence`, v:m.indep}
];
const fig = Plot.plot({
width: plotWrap.clientWidth || 650,
height: 240,
marginLeft: 200,
x: {label:"value", domain:[0,1]},
y: {domain:data.map(d=>d.k)},
marks: [
Plot.barX(data, {x:"v", y:"k", fill:"#60a5fa"}),
Plot.text(data, {x:"v", y:"k", text:d=>fmt(d.v,3), dx:6, textAnchor:"start"})
]
});
plotWrap.append(fig);
}
function renderTable(rows){
tableWrap.innerHTML = "";
const tbl = html`<table class="tbl">
<thead><tr><th>#</th><th>items (emoji)</th></tr></thead>
<tbody></tbody>
</table>`;
const tb = tbl.querySelector("tbody");
const show = Math.min(20, rows.length);
for (let i=0;i<show;i++){
tb.insertAdjacentHTML("beforeend",
`<tr><td class="mono">${i+1}</td><td class="mono">${rows[i].join(" ")}</td></tr>`);
}
if (rows.length>show){
tb.insertAdjacentHTML("beforeend", `<tr><td colspan="2" class="hint">… ${rows.length-show} more transactions</td></tr>`);
}
tableWrap.append(tbl);
}
// CSV makers
function toCSVTransactions(rows){
// comma-separated emoji items per transaction, no empties by construction
return rows.map(r => r.join(",")).join("\n");
}
function toCSVOneHot(rows, items){ // items: [A,B,C]
const header = items.join(",");
const body = rows.map(r => items.map(it => r.includes(it) ? 1 : 0).join(",")).join("\n");
return `${header}\n${body}`;
}
// main
let lastRows = [];
function rebuild(){
const A = selA.value, B = selB.value, C = selC.value;
const N = +Nnum.value || 1000;
const pa = clamp01(+pA.value||0), pb = clamp01(+pB.value||0), pc = clamp01(+pC.value||0);
const q = clamp01(+pBgivenA.value||0);
const keep = !!keepMarg.checked;
const sim = generate(N, A,B,C, pa,pb,pc, q, keep);
lastRows = sim.rows;
const m = calcMetrics(sim.counts);
ruleName.textContent = `${A} ⇒ ${B}`;
metrics.textContent =
`N = ${sim.counts.N}
support(${A}) = ${fmt(m.sA)}
support(${B}) = ${fmt(m.sB)}
support(${A}∩${B}) = ${fmt(m.sAB)}
confidence(${A}⇒${B}) = ${fmt(m.conf)}
lift(${A}⇒${B}) = ${fmt(m.lift)}
P(${B}|¬${A}) used = ${fmt(sim.qBnotA,3)} (keep marginal: ${keep ? "yes" : "no"})`;
explain.innerHTML = `
<b>Confidence</b> = P(${B}|${A}) = support(${A}∩${B}) / support(${A}) → “Given a customer buys ${A}, how likely they also buy ${B}.”<br>
<b>Lift</b> = P(${B}|${A}) / P(${B}) → >1 positive association, ≈1 independent, <1 negative.<br>
Use high Lift to justify cross-promotions of ${B} with ${A}.
`;
renderPlot(m, A, B);
renderTable(lastRows);
status.textContent = "Generated";
setTimeout(()=> status.textContent="", 800);
}
// events
buildBtn.addEventListener("click", rebuild);
[selA,selB,selC,Nnum,Nrng,pA,pB,pC,pBgivenA,keepMarg].forEach(el=>{
el.addEventListener("input", ()=>{ if (el===Nrng) Nnum.value = Nrng.value; });
});
// Copy actions (no download button)
copyTX.addEventListener("click", async ()=>{
if (!lastRows.length) return;
const csv = toCSVTransactions(lastRows);
try { await navigator.clipboard.writeText(csv); copyTX.textContent = "Copied!"; setTimeout(()=>copyTX.textContent="Copy CSV (transactions)",900); }
catch { alert(csv); }
});
copyOH.addEventListener("click", async ()=>{
if (!lastRows.length) return;
const A = selA.value, B = selB.value, C = selC.value;
const csv = toCSVOneHot(lastRows, [A,B,C]);
try { await navigator.clipboard.writeText(csv); copyOH.textContent = "Copied!"; setTimeout(()=>copyOH.textContent="Copy CSV (one-hot)",900); }
catch { alert(csv); }
});
// first build (preset cafe)
const presetSelect = box.querySelector("#preset");
presetSelect.value = "cafe";
applyPresetValues(); // this will call rebuild()
return box;
})()Agrawal, R., & Srikant, R. (1994). Fast algorithms for mining association rules. Proceedings of the 20th International Conference on Very Large Data Bases (VLDB), 487–499.
Han, J., Kamber, M., & Pei, J. (2011). Data mining: Concepts and techniques (3rd ed.). Morgan Kaufmann.
Tan, P.-N., Steinbach, M., & Kumar, V. (2018). Introduction to data mining (2nd ed.). Pearson.
8. Social Media Analysis📱: Influencer Marketing🤳
Analyze content relationships to identify influencers⭐
Examples: