Un recomendador en 40 líneas
Parecido entre productos por coseno, una media ponderada y una lista ordenada: un recomendador por ítems con 14 clientes y 10 productos de un taller.
R es una matriz de 14 clientes × 10 productos con valoraciones de 1 a 5 y 0 donde no hay valoración. Escribe cuatro funciones:
cosine(a, b): producto escalar entre el producto de las normas, tratando el 0 como un cero más. Si una norma es 0, devuelve 0.
itemSimilarity(R): la matriz S de 10 × 10 con el coseno entre las **columnas** (productos) i y j.
predict(R, S, u, i, k): la nota que el cliente u daría al producto i: entre los productos que u sí valoró (distintos de i) y con S[i][j] > 0, toma los k más parecidos y haz la media ponderada Σ S[i][j] · R[u][j] / Σ S[i][j]. Sin vecinos, 0.
recommend(R, S, u, n, k): para cada producto sin valorar por u, su predicción; devuelve los n mejores como { item, score } (índice del producto y nota redondeada a dos decimales), de mayor a menor y, en empate, por índice. La vista previa deja elegir un cliente y ver sus recomendaciones.
Retos 0/4
- Coseno: vectores iguales, perpendiculares y uno vacío
- La matriz de parecidos es simétrica, con unos en la diagonal
- Una predicción con tres vecinos, y 0 sin vecinos
- Las recomendaciones de un cliente de mecánica y de uno de carrocería
function cosine(a, b) {
// producto escalar / (|a| · |b|); 0 si una norma es 0
return 0;
}
function itemSimilarity(R) {
// S[i][j] = coseno de las columnas i y j
return R[0].map(() => R[0].map(() => 0));
}
function predict(R, S, u, i, k) {
// vecinos: productos j != i valorados por u con S[i][j] > 0; los k más parecidos; media ponderada; 0 sin vecinos
return 0;
}
function recommend(R, S, u, n, k) {
// productos sin valorar con su predicción, los n mejores como { item, score } (dos decimales), empates por índice
return [];
}
const S = itemSimilarity(R);
console.log(recommend(R, S, 2, 3, 3).map((r) => PRODUCTS[r.item] + ' ' + r.score));La idea
Un recomendador por ítems hace una apuesta sencilla: si quien compró filtros compró también aceite, al próximo que valore bien los filtros le enseñas aceite. El «parecido» entre dos productos es el coseno entre sus columnas de valoraciones, y la nota prevista es una media de lo que el cliente ya valoró, pesada por ese parecido.
Es lo que usan muchas tiendas pequeñas con buenos resultados, y cabe en 40 líneas sin librerías. En la vista previa, los clientes del 1 al 8 compran mecánica y los demás carrocería y limpieza: verás que las recomendaciones respetan esa separación sin que nadie la haya programado. Los datos son sintéticos y lo dicen.
Dos límites honestos. Con pocos datos, un producto que nadie valoró no se recomienda nunca (el arranque en frío), y un cliente nuevo no recibe nada hasta que valora algo. Y el coseno sobre ceros premia a los productos populares: si eso no te gusta, centra cada fila restando la media del cliente antes de calcular.
Todo esto corre con las valoraciones en tu máquina. No hace falta enviar el historial de compra de tus clientes a nadie para recomendar bien en un catálogo de cien productos.
Hazlo en tu máquina
Con Python, pandas y NumPy, sobre el CSV de este ejercicio (CC BY 4.0). Aquí con todos los vecinos valorados, sin el corte de k, así que las notas difieren un poco:
curl -O https://vorluxai.com/data/labs/valoraciones-taller.csv
python3 -c "
import pandas as pd, numpy as np
df = pd.read_csv('valoraciones-taller.csv')
M = df.pivot(index='cliente', columns='producto', values='valoracion').fillna(0)
X = M.values; norms = np.linalg.norm(X, axis=0)
S = (X.T @ X) / np.outer(norms, norms)
u = M.index.get_loc('C03'); rated = X[u] > 0
pred = {M.columns[i]: round(float((S[i, rated] * X[u, rated]).sum() / S[i, rated].sum()), 2) for i in range(X.shape[1]) if not rated[i]}
print('C03 ->', sorted(pred.items(), key=lambda kv: -kv[1])[:3])
" Para profundizar: la referencia de Google Machine Learning Crash Course (en inglés) →
¿Esto en producción, con tus datos? Hablemos 15 minutos →