k-means a mano
Asignar, recalcular, repetir: k-means son dos funciones y un bucle. Agrupa 90 clientes por ticket y frecuencia y mira cómo el codo elige k.
POINTS son 90 clientes como [ticket medio en EUR, visitas al mes]. Escribe cinco funciones:
standardize(points): cada columna a (x - media) / desviación (desviación entre n); si la desviación es 0, ceros.
assign(points, centroids): para cada punto, el índice del centroide más cercano (distancia al cuadrado, dist2); en empate, el índice menor.
update(points, labels, k, prev): k centroides nuevos, cada uno la media de sus puntos; un grupo sin puntos conserva prev[c].
kmeans(points, k, seed, maxIters): centroides iniciales = k puntos distintos elegidos con mulberry32(seed) (Math.floor(rng() * n), repitiendo la tirada si el índice ya salió). Repite asignar y actualizar hasta maxIters veces o hasta que las etiquetas no cambien. Devuelve { centroids, labels, iterations, inertia }, con inertia la suma de dist2 de cada punto a su centroide.
elbow(points, ks, seed): la inercia de kmeans para cada k de ks (con maxIters 50). La vista previa pinta los grupos, los centroides y la curva del codo.
Retos 0/5
- Estandarizar por columnas, con ceros si no hay dispersión
- Asignar al centroide más cercano, con el índice menor en empate
- Recalcular centroides, conservando el de un grupo vacío
- k-means con k = 3 sobre los datos estandarizados separa los tres grupos
- La curva del codo baja y se aplana en k = 3
function standardize(points) {
// por columna: (x - media) / sd con sd entre n; ceros si sd es 0
return points;
}
function assign(points, centroids) {
// índice del centroide más cercano por dist2; el índice menor en empate
return points.map(() => 0);
}
function update(points, labels, k, prev) {
// media de los puntos de cada grupo; prev[c] si el grupo está vacío
return prev;
}
function kmeans(points, k, seed, maxIters) {
// inicio: k puntos distintos con mulberry32(seed); repite assign + update hasta que las etiquetas no cambien o maxIters
// devuelve { centroids, labels, iterations, inertia }
return { centroids: [], labels: points.map(() => 0), iterations: 0, inertia: 0 };
}
function elbow(points, ks, seed) {
// kmeans(points, k, seed, 50).inertia para cada k
return ks.map(() => 0);
}
console.log(kmeans(standardize(POINTS), 3, 7, 50));La idea
k-means no sabe nada de clientes. Parte de k centros al azar, asigna cada punto al centro más cercano, mueve cada centro a la media de sus puntos y repite hasta que nada cambia. Son dos funciones y un bucle, y con 90 puntos converge en menos de diez vueltas.
La trampa está en las unidades. El ticket medio va de 20 a 400 EUR y las visitas de 0,2 a 6: sin estandarizar, la distancia la decide el ticket y las visitas no cuentan. Prueba la vista previa con y sin estandarizar y verás que los grupos cambian. Esa decisión la tomas tú, no el algoritmo.
k tampoco lo decide el algoritmo. La curva del codo pinta la inercia (suma de distancias al cuadrado) para cada k: baja siempre, pero deja de bajar deprisa en el k «natural». Aquí el codo está en 3, que es como construimos los datos; con datos reales el codo suele ser menos claro y conviene mirar los grupos, no solo la curva.
La semilla fija el resultado. Con otra semilla, otro arranque y a veces otro mínimo local; scikit-learn lo resuelve arrancando diez veces y quedándose con la mejor inercia. Aquí lo haces una vez para ver qué hay debajo.
Hazlo en tu máquina
Con Python y scikit-learn, sobre el CSV de este ejercicio (CC BY 4.0):
curl -O https://vorluxai.com/data/labs/clientes-kmeans.csv
python3 -c "
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
X = StandardScaler().fit_transform(pd.read_csv('clientes-kmeans.csv')[['gasto_medio_eur', 'visitas_mes']])
for k in range(1, 7):
print(k, round(KMeans(n_clusters=k, n_init=10, random_state=0).fit(X).inertia_, 1))
" Para profundizar: la referencia de scikit-learn (en inglés) →
¿Esto en producción, con tus datos? Hablemos 15 minutos →