---
title: "K-Means and K-Medoids"
description: "Study K-Means and K-Medoids clustering, the within-cluster sum-of-squares objective, and Lloyd's iterative assignment-update algorithm."
pagetitle: "K-Means and K-Medoids"
open-graph:
description: "Study K-Means and K-Medoids clustering, the within-cluster sum-of-squares objective, and Lloyd's iterative assignment-update algorithm."
twitter-card:
description: "Study K-Means and K-Medoids clustering, the within-cluster sum-of-squares objective, and Lloyd's iterative assignment-update algorithm."
author: "Diego Villalba"
date: today
lang: en
format:
html:
toc: true
toc-depth: 3
toc-title: "Contents"
number-sections: true
code-fold: true
code-tools: true
code-summary: "Show code"
fig-align: center
theme: cosmo
highlight-style: github
smooth-scroll: true
pdf:
toc: true
toc-depth: 3
number-sections: true
documentclass: scrbook
papersize: letter
fontsize: 11pt
geometry: margin=2.5cm
keep-tex: false
execute:
echo: true
warning: false
message: false
cache: false
bibliography: [referencias_clustering.bib, ../../../references.bib]
crossref:
fig-title: "Figure"
tbl-title: "Table"
eq-prefix: "Eq."
jupyter: python3
---
K-Means is the most widely deployed clustering algorithm due to its conceptual simplicity, linear computational scaling per iteration, and interpretable mean-centroid cluster representations [@macqueen1967; @lloyd1982].
## K-Means Objective Function
K-Means partitions $n$ instances into $k$ clusters $C_1,\dots,C_k$ minimizing within-cluster sum-of-squares (WCSS):
::: {.callout-note appearance="minimal"}
Definition. K-Means WCSS Objective:
$$
J(C_1,\dots,C_k) = \sum_{j=1}^{k}\sum_{\mathbf{x}\in C_j}\|\mathbf{x}-\boldsymbol{\mu}_j\|^2,
$$ {#eq-kmeans-obj}
where $\boldsymbol{\mu}_j = \frac{1}{|C_j|}\sum_{\mathbf{x}\in C_j}\mathbf{x}$ denotes the centroid vector of cluster $j$.
:::
## Lloyd's Algorithm
::: {.callout-note appearance="minimal"}
**Lloyd's K-Means Algorithm:**
1. **Initialize:** Select $k$ initial centroids $\boldsymbol{\mu}_1,\dots,\boldsymbol{\mu}_k$.
2. **Assignment Step:** Assign each instance $\mathbf{x}_i$ to nearest centroid:
$$\ell(i) = \arg\min_{j} \|\mathbf{x}_i - \boldsymbol{\mu}_j\|^2$$
3. **Update Step:** Recompute centroids as mean of assigned points:
$$\boldsymbol{\mu}_j = \frac{1}{|C_j|}\sum_{\ell(i)=j}\mathbf{x}_i$$
4. **Convergence:** Repeat steps 2-3 until cluster assignments stabilize.
:::
## K-Means++ Initialization
Standard random initialization is vulnerable to suboptimal local minima. K-Means++ [@arthur2007] selects initial centroids using distance-proportional sampling $P(\mathbf{x}_i) \propto D(\mathbf{x}_i)^2$, bounding expected WCSS within $O(\log k)$ of global optimum.
```{=html}
<div id="km-wrap" style="font-family:Georgia,serif;max-width:700px;margin:2em auto;background:#f9fafc;border:1px solid #d0d7e3;border-radius:10px;padding:20px 24px;box-shadow:0 2px 12px rgba(30,60,120,.07);">
<div style="text-align:center;margin-bottom:12px;">
<span style="font-size:1.05em;font-weight:bold;color:#1a2e45;">K-Means — Step-by-Step Animation</span><br>
<span style="font-size:0.81em;color:#666;">Observe centroid updates and Voronoi partition shifts</span>
</div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;">
<label style="font-size:0.83em;color:#444;">k = <strong id="km-kv">3</strong></label>
<input id="km-ksl" type="range" min="2" max="6" step="1" value="3" style="flex:1;min-width:120px;accent-color:#1a3a5c;">
<button onclick="window.kmReset()" style="font-size:0.8em;padding:4px 12px;border:1px solid #d0d7e3;border-radius:5px;background:#fff;cursor:pointer;">Reset</button>
<button onclick="window.kmStep()" style="font-size:0.8em;padding:4px 14px;border:1px solid #1a3a5c;border-radius:5px;background:#1a3a5c;color:#fff;cursor:pointer;">Step ▶</button>
<button onclick="window.kmRun()" style="font-size:0.8em;padding:4px 14px;border:1px solid #16a34a;border-radius:5px;background:#16a34a;color:#fff;cursor:pointer;">Run ⟳</button>
</div>
<div id="km-info" style="text-align:center;font-size:0.82em;padding:5px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;margin-bottom:10px;min-height:22px;"></div>
<canvas id="km-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
</div>
<script>
(function(){
const COLS=['#2563eb','#dc2626','#16a34a','#f59e0b','#7c3aed','#0891b2'];
const SEED=[
[1.0,1.2],[1.4,0.8],[0.8,1.5],[1.2,0.7],[1.6,1.4],[0.9,0.9],[1.3,1.7],[1.1,1.0],[1.5,0.6],[0.7,1.3],
[1.8,1.9],[0.5,1.1],[1.0,1.5],[1.3,1.2],[0.8,0.5],[1.7,1.1],[0.6,0.8],[1.4,1.6],
[4.5,4.8],[4.9,4.2],[4.2,5.0],[4.7,4.4],[5.2,4.7],[4.3,4.0],[4.8,5.1],[4.4,4.3],[5.0,4.1],[4.1,4.5],
[5.3,4.9],[4.0,4.1],[4.6,5.2],[5.1,4.6],[4.3,3.9],[4.7,4.2],
[7.8,1.8],[7.2,2.2],[8.1,1.3],[7.5,2.0],[7.0,1.7],[7.9,2.4],[7.3,1.1],[7.6,2.1]
];
const cv=document.getElementById('km-cv');
const W=680,H=380,DPR=window.devicePixelRatio||1;
cv.width=W*DPR;cv.height=H*DPR;cv.style.width=W+'px';cv.style.height=H+'px';
const ctx=cv.getContext('2d');ctx.scale(DPR,DPR);
const PAD={l:44,r:16,t:20,b:30};
const CW=W-PAD.l-PAD.r,CH=H-PAD.t-PAD.b;
const xMn=0,xMx=9,yMn=0,yMx=7;
const sx=x=>PAD.l+(x-xMn)/(xMx-xMn)*CW;
const sy=y=>PAD.t+CH-(y-yMn)/(yMx-yMn)*CH;
let k=3,cents=[],labels=[],phase='init',iter=0,converged=false;
function dist2(a,b){return(a[0]-b[0])**2+(a[1]-b[1])**2;}
function initCents(k){
const idx=[];
while(idx.length<k){const r=Math.floor(Math.random()*SEED.length);if(!idx.includes(r))idx.push(r);}
return idx.map(i=>[...SEED[i]]);
}
function assign(){
return SEED.map(p=>{let bi=0,bd=Infinity;cents.forEach((c,i)=>{const d=dist2(p,c);if(d<bd){bd=d;bi=i;}});return bi;});
}
function update(labels){
return Array.from({length:k},(_,ci)=>{
const cl=SEED.filter((_,i)=>labels[i]===ci);
if(!cl.length)return cents[ci];
return[cl.reduce((s,p)=>s+p[0],0)/cl.length,cl.reduce((s,p)=>s+p[1],0)/cl.length];
});
}
function wcss(labels){
return SEED.reduce((s,p,i)=>s+dist2(p,cents[labels[i]]),0);
}
function draw(){
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#f8f9fb';ctx.fillRect(PAD.l,PAD.t,CW,CH);
SEED.forEach((p,i)=>{
ctx.beginPath();ctx.arc(sx(p[0]),sy(p[1]),5.5,0,2*Math.PI);
ctx.fillStyle=labels.length?COLS[labels[i]]+'cc':'#94a3b8cc';ctx.fill();
ctx.strokeStyle='#fff';ctx.lineWidth=1.5;ctx.stroke();
});
cents.forEach((c,i)=>{
ctx.beginPath();ctx.arc(sx(c[0]),sy(c[1]),10,0,2*Math.PI);
ctx.fillStyle=COLS[i];ctx.fill();
ctx.strokeStyle='#fff';ctx.lineWidth=2.5;ctx.stroke();
ctx.fillStyle='#fff';ctx.font='bold 10px Georgia,serif';ctx.textAlign='center';
ctx.fillText(i+1,sx(c[0]),sy(c[1])+3);
});
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('K-Means — Iteration '+iter,PAD.l+CW/2,PAD.t-6);
}
window.kmReset=function(){
k=parseInt(document.getElementById('km-ksl').value);
cents=initCents(k);labels=[];iter=0;converged=false;phase='init';
draw();
document.getElementById('km-info').innerHTML='Centroids initialized randomly. Click <strong>Step</strong> to assign.';
};
window.kmStep=function(){
if(converged) return;
if(phase==='init'||phase==='update'){
const newL=assign();
const same=labels.length&&newL.every((l,i)=>l===labels[i]);
labels=newL;phase='assign';iter++;
draw();
const w=wcss(labels);
if(same){converged=true;document.getElementById('km-info').innerHTML=`Iteration ${iter} — <strong style="color:#16a34a">Converged</strong>. WCSS = ${w.toFixed(2)}`;}
else document.getElementById('km-info').innerHTML=`Iteration ${iter}: assignments updated. WCSS = <strong>${w.toFixed(2)}</strong>`;
} else {
cents=update(labels);phase='update';
draw();
document.getElementById('km-info').innerHTML=`Iteration ${iter}: centroids updated.`;
}
};
window.kmRun=function(){
const run=()=>{if(!converged){window.kmStep();setTimeout(run,300);}};
run();
};
document.getElementById('km-ksl').addEventListener('input',function(){
document.getElementById('km-kv').textContent=this.value;
window.kmReset();
});
window.kmReset();
})();
</script>
```
## K-Medoids (PAM)
K-Medoids represents clusters using actual exemplar data points (medoids) rather than spatial means, offering robustness against outliers [@kaufman1990].
::: {.callout-note appearance="minimal"}
Definition. Medoid Selection:
$$
\mathbf{m}_j = \arg\min_{\mathbf{x}\in C_j}\sum_{\mathbf{x}'\in C_j}d(\mathbf{x},\mathbf{x}').
$$ {#eq-medoid}
:::
```{python}
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
X, _ = make_blobs(n_samples=300, centers=4, cluster_std=0.7, random_state=7)
wcss = []
for k in range(1, 7):
km = KMeans(n_clusters=k, random_state=0, n_init=10)
km.fit(X)
wcss.append(km.inertia_)
print(f"k={k} | WCSS: {km.inertia_:.2f}")
```
## Summary
K-Means partitions vector spaces by minimizing squared Euclidean distance from centroids. K-Means++ mitigates initialization sensitivity, while K-Medoids replaces cluster means with actual medoid observations to improve outlier robustness [@macqueen1967; @arthur2007; @kaufman1990].
## References
::: {#refs}
:::