6  Representación vectorial de datos

estadistica
algebra-lineal
vectores
Autor/a

Wilder Ramírez Delgado

Fecha de publicación

25 de septiembre de 2026

Cada fila es un punto

Un modelo de machine learning no recibe resúmenes sueltos como la media de una columna: recibe la fila completa. Esa fila es un vector con una coordenada por variable, y el dataset entero es una matriz.

En este capítulo pasas de la tabla a ese lenguaje: el dataset como matriz, la vectorización, el producto punto, la norma, la distancia euclidiana, la similitud coseno y su interpretación geométrica.

Los datos son seis perfiles ficticios de hábitos de estudio, escritos a mano para que la geometría se lea a ojo: dos perfiles con la misma dirección y distinta magnitud, dos con magnitudes iguales y direcciones distintas, y una consulta cuyo vecino más cercano cambia según la medida que uses. No hay ningún paso aleatorio en el capítulo.

  • Tiempo estimado de lectura: 45 minutos (unas 2 horas si ejecutas el código y resuelves los ejercicios)
  • Nivel: Básico
  • Conocimientos previos recomendados: los capítulos 3 (desviación estándar), 4 (z-score y estandarización) y 5 (covarianza y correlación).
  • Tecnologías mencionadas: Python 3.13, pandas 2.3, NumPy 2.3 y Matplotlib.

Objetivos

Al terminar este capítulo vas a ser capaz de:

  • escribir un dataset como una matriz \(X\) con \(N\) filas y \(D\) columnas, separada del vector objetivo \(y\);
  • leer cada fila como un vector en \(\mathbb{R}^D\);
  • calcular e interpretar el producto punto, la norma (\(L_2\) y \(L_1\)) y el ángulo entre dos vectores;
  • distinguir la distancia euclidiana, que mide separación, de la similitud coseno, que mide orientación;
  • comprobar la desigualdad de Cauchy–Schwarz, construir el vector unitario y calcular la proyección ortogonal de un perfil sobre otro;
  • explicar por qué la escala de las variables cambia quién es cercano, y relacionar esta geometría con k vecinos más cercanos, los embeddings y el PCA.

Ejecuta las celdas en orden: las funciones de gráficos y la matriz de la etapa 1 se usan hasta el final.

6.1 De la tabla al espacio

Una tabla tiene filas (observaciones) y columnas (variables). En álgebra, esa misma tabla es una matriz \(X \in \mathbb{R}^{N \times D}\), con \(N\) observaciones y \(D\) variables de entrada (features). La fila \(i\) es el vector \(\mathbf{x}_i = (x_{i1},\ldots,x_{iD})\). Vectorizar no agrega información: cambia la forma de leerla, de celdas sueltas a un punto en el espacio.

Producto punto

\[ \mathbf{a}\cdot\mathbf{b} = \sum_{j=1}^{D} a_j b_j = \|\mathbf{a}\|\,\|\mathbf{b}\|\cos\theta \]

Suma los productos coordenada a coordenada. Geométricamente combina magnitudes y alineación: con normas fijas, cuanto menor es el ángulo \(\theta\), mayor es el producto punto.

Norma euclidiana

\[ \|\mathbf{a}\|_2 = \sqrt{\sum_{j=1}^{D} a_j^2} \]

Es la longitud del vector: qué tan intenso es el perfil, sin importar hacia dónde apunta. Quien dedica el doble de horas en las mismas proporciones tiene la misma dirección y el doble de norma.

Distancia euclidiana

\[ d(\mathbf{a},\mathbf{b}) = \|\mathbf{a}-\mathbf{b}\|_2 \]

Mide la separación entre dos puntos. Es la medida por defecto de muchos métodos basados en vecindarios, como k vecinos más cercanos y k-means. Crece si cambia la magnitud o la dirección, y la domina la variable de mayor rango.

Similitud coseno

\[ \cos\theta = \frac{\mathbf{a}\cdot\mathbf{b}}{\|\mathbf{a}\|\,\|\mathbf{b}\|} \]

Vale entre \(-1\) y \(1\) e ignora el tamaño de los vectores: dos perfiles proporcionales tienen \(\cos\theta = 1\) aunque uno sea más intenso. Es la medida habitual para comparar textos y embeddings.

Desigualdad de Cauchy–Schwarz

\[ |\mathbf{a}\cdot\mathbf{b}| \le \|\mathbf{a}\|\,\|\mathbf{b}\| \]

La igualdad se cumple si y solo si los vectores son linealmente dependientes, es decir, si uno es múltiplo del otro. Por esta desigualdad el coseno nunca sale del intervalo \([-1,1]\).

Vector unitario

\[ \hat{\mathbf{a}} = \frac{\mathbf{a}}{\|\mathbf{a}\|} \qquad (\mathbf{a}\neq\mathbf{0}) \]

Tiene norma \(1\) y conserva la dirección. Dividir por la norma quita la intensidad y deja solo la orientación; es el paso algebraico que hay detrás de la similitud coseno.

Ortogonalidad

\[ \mathbf{a}\perp\mathbf{b} \iff \mathbf{a}\cdot\mathbf{b}=0 \]

Los vectores forman un ángulo de \(\pi/2\). Conocer la componente de un vector en la dirección del otro no te dice nada: esa componente es cero.

Proyección ortogonal

\[ \mathrm{proj}_{\mathbf{b}}(\mathbf{a}) = \frac{\mathbf{a}\cdot\mathbf{b}}{\|\mathbf{b}\|^2}\,\mathbf{b} \]

Es la sombra de \(\mathbf{a}\) sobre la recta de \(\mathbf{b}\): la parte de \(\mathbf{a}\) que se explica por esa dirección. El residual \(\mathbf{a}-\mathrm{proj}_{\mathbf{b}}(\mathbf{a})\) es ortogonal a \(\mathbf{b}\). Esta descomposición sostiene la regresión lineal y el PCA.

Pregunta Herramienta
¿Cómo guardo el dataset para un modelo? Matriz \(X\) y vector \(y\)
¿Qué tan largo es un perfil? Norma
¿Cuánto se alinean dos perfiles? Producto punto o coseno
¿Qué tan lejos están dos observaciones? Distancia euclidiana
¿Se parecen en proporción, aunque no en intensidad? Similitud coseno

6.2 Por qué importa en IA y machine learning

Para un modelo, cada columna es solo una coordenada:

  • En regresión y en las redes neuronales, \(X\) es la entrada y cada capa lineal calcula productos punto \(\mathbf{w}\cdot\mathbf{x} + b\) entre unos pesos y cada fila.
  • En k vecinos más cercanos y clustering, «parecido» significa «cerca» según una distancia. Si no escalas, una variable medida en minutos aplasta a otra medida en horas (etapa 10).
  • En búsqueda de textos, recomendaciones y embeddings, dos documentos pueden tener longitudes distintas y el mismo tema; ahí suele importar más el ángulo que la distancia, y por eso los buscadores semánticos comparan embeddings con el coseno.
  • En PCA se buscan direcciones nuevas que capturen la mayor varianza en este mismo espacio.

Distancia y ángulo pueden ordenar distinto a las mismas observaciones. Dos perfiles pueden coincidir en dirección y estar lejos en magnitud, y el algoritmo decide según la medida que le des (etapa 11).

6.3 Flujo recomendado de trabajo

  1. Escribe el dataset como \(X\): separa las variables de entrada del objetivo y anota \(N\) y \(D\).
  2. Lee cada fila como un vector: qué significa cada coordenada y en qué unidad está.
  3. Decide si importa la magnitud o la dirección: intensidad del perfil (norma, distancia euclidiana) o proporción entre sus partes (coseno).
  4. Calcula las dos familias de medidas: primero producto punto y normas; con ellas obtienes la distancia y el coseno.
  5. Revisa la escala: si las variables no comparten unidad ni rango, estandariza antes de medir distancias.
  6. Mira el gráfico: un coeficiente sin flechas ni nube de puntos es fácil de malinterpretar.
  7. Redacta el reporte con las variables, la medida y el escalamiento:

«En el espacio [variables], [caso A] y [caso B] tienen distancia ___ y coseno ___. Se reporta [medida] porque [pregunta]. Implicación para el modelo: [decisión].»

Por ejemplo, con los datos de este capítulo: «En el espacio (horas de lectura, horas de práctica), Ana y Bruno tienen distancia 5.66 y coseno 1.00. Se reporta el coseno porque la pregunta es quién estudia con la misma proporción de lectura y práctica, no quién dedica las mismas horas. Implicación para el modelo: un recomendador de rutinas debería usar el coseno; un modelo de k vecinos que prediga la puntuación debería usar la distancia, porque la cantidad de horas sí cambia el resultado».

6.4 Preparar el entorno

La celda carga NumPy, pandas y Matplotlib y fija un estilo de gráficos común. Los datos se escriben a mano en la etapa 1, así que el capítulo no usa números aleatorios.

%matplotlib inline
%config InlineBackend.figure_format = "retina"
import sys
print("Python:", sys.version.split()[0])
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

SEMILLA = 42  # semilla central; este capítulo no genera datos aleatorios
rng = np.random.default_rng(SEMILLA)

plt.rcParams["figure.figsize"] = (8, 5)
plt.rcParams["axes.grid"] = True
plt.rcParams["grid.alpha"] = 0.25

print("pandas:", pd.__version__, "| numpy:", np.__version__)
Python: 3.13.14
pandas: 3.0.5 | numpy: 2.5.1

El entorno queda con Python 3.13.9, pandas 2.3.3 y NumPy 2.3.5.

La siguiente celda define cuatro funciones de gráficos: flechas desde el origen, nube de puntos con una consulta, barras que comparan dos medidas y proyección de un vector sobre otro. Cada etapa las llama en una o dos líneas.

def graficar_vectores_2d(nombres, X2, titulo, query=None, query_nombre="Consulta"):
    """Flechas desde el origen: cada fila es un perfil en (lectura, práctica)."""
    fig, ax = plt.subplots(figsize=(7.5, 7.5))
    colores = plt.cm.tab10(np.linspace(0, 0.7, len(nombres)))
    desplazamiento = {"Ana": (0.25, -0.45), "Elena": (-0.65, 0.25)}   # evita que choquen las etiquetas
    # Se dibuja de la flecha más larga a la más corta: así Bruno no tapa a Ana (misma recta)
    for rango, i in enumerate(np.argsort(-np.linalg.norm(X2, axis=1))):
        nombre, color = nombres[i], colores[i]
        x, y = X2[i]
        ax.arrow(0, 0, x, y, head_width=0.18, head_length=0.18,
                 length_includes_head=True, color=color, linewidth=2, zorder=2 + rango)
        dx, dy = desplazamiento.get(nombre, (0.15, 0.15))
        ax.text(x + dx, y + dy, nombre, fontsize=10, color=color)
    if query is not None:
        ax.arrow(0, 0, query[0], query[1], head_width=0.18, head_length=0.18,
                 length_includes_head=True, color="black", linewidth=2, linestyle="--")
        ax.text(query[0] + 0.15, query[1] - 0.35, query_nombre, fontsize=10)
    ax.axhline(0, color="0.6", linewidth=0.8)
    ax.axvline(0, color="0.6", linewidth=0.8)
    ax.set_aspect("equal", adjustable="box")
    ax.set_xlabel("Horas de lectura / semana")
    ax.set_ylabel("Horas de práctica / semana")
    ax.set_title(titulo)
    plt.tight_layout()
    plt.show()


def graficar_puntos_y_consulta(nombres, X2, query, vecinos, titulo):
    """Nube 2D: consulta + segmentos hacia los vecinos elegidos por una métrica."""
    fig, ax = plt.subplots(figsize=(7.2, 6.2))
    ax.scatter(X2[:, 0], X2[:, 1], s=70)
    for i, nombre in enumerate(nombres):
        ax.annotate(nombre, (X2[i, 0], X2[i, 1]), textcoords="offset points",
                    xytext=(-40, -4) if nombre == "Elena" else (6, 6), fontsize=9)
    ax.scatter(query[0], query[1], s=110, marker="*", zorder=3, label="Consulta")
    ax.annotate("Consulta", (query[0], query[1]), textcoords="offset points",
                xytext=(6, -12), fontsize=9)
    for idx, estilo, etiqueta in vecinos:
        ax.plot([query[0], X2[idx, 0]], [query[1], X2[idx, 1]],
                linestyle=estilo, linewidth=1.8, label=etiqueta)
    ax.set_xlabel("Horas de lectura / semana")
    ax.set_ylabel("Horas de práctica / semana")
    ax.set_title(titulo)
    ax.legend(fontsize=9)
    plt.tight_layout()
    plt.show()


def graficar_barras_metricas(nombres, euclidiana, coseno, titulo):
    """Comparar distancia (menor = más cerca) y coseno (mayor = más parecido)."""
    fig, axes = plt.subplots(1, 2, figsize=(11, 4.2), constrained_layout=True)
    euclidiana, coseno = np.asarray(euclidiana), np.asarray(coseno)
    gana_d = np.isclose(euclidiana, euclidiana.min())
    gana_c = np.isclose(coseno, coseno.max())
    barras_d = axes[0].bar(nombres, euclidiana, color=np.where(gana_d, "C1", "C0"))
    axes[0].bar_label(barras_d, fmt="%.2f", fontsize=8)
    axes[0].set_ylabel("Distancia euclidiana")
    axes[0].set_title("Separación respecto a la consulta")
    axes[0].tick_params(axis="x", rotation=20)
    barras_c = axes[1].bar(nombres, coseno, color=np.where(gana_c, "C1", "C0"))
    axes[1].bar_label(barras_c, fmt="%.4f", fontsize=8)
    axes[1].set_ylim(0, 1.12)
    axes[1].set_ylabel("Similitud coseno")
    axes[1].set_title("Alineación respecto a la consulta")
    axes[1].tick_params(axis="x", rotation=20)
    fig.suptitle(titulo + "  (naranja: el más cercano según cada medida)")
    plt.show()


def graficar_proyeccion(a, b, nombre_a, nombre_b, titulo):
    """Vector a, dirección b, proyección de a sobre b y residual."""
    a = np.asarray(a, dtype=float)
    b = np.asarray(b, dtype=float)
    proj = (np.dot(a, b) / np.dot(b, b)) * b
    residual = a - proj
    fig, ax = plt.subplots(figsize=(7.2, 7.2))
    # La proyección va sobre la misma recta que b: se dibuja primero, ancha y translúcida
    ax.plot([0, proj[0]], [0, proj[1]], color="C1", linewidth=8, alpha=0.35,
            solid_capstyle="round", label=f"proj de {nombre_a} sobre {nombre_b}")
    ax.scatter(proj[0], proj[1], s=60, color="C1", zorder=4)
    ax.arrow(0, 0, b[0], b[1], head_width=0.18, head_length=0.18,
             length_includes_head=True, linewidth=2, color="C0", label=nombre_b, zorder=3)
    ax.arrow(0, 0, a[0], a[1], head_width=0.18, head_length=0.18,
             length_includes_head=True, linewidth=2, color="C2", label=nombre_a, zorder=3)
    ax.plot([a[0], proj[0]], [a[1], proj[1]], linestyle=":", linewidth=2, color="C3",
            label="residual")
    ax.axhline(0, color="0.6", linewidth=0.8)
    ax.axvline(0, color="0.6", linewidth=0.8)
    ax.set_aspect("equal", adjustable="box")
    ax.set_xlabel("Horas de lectura / semana")
    ax.set_ylabel("Horas de práctica / semana")
    ax.set_title(titulo)
    ax.legend(fontsize=8, loc="upper left")
    plt.tight_layout()
    plt.show()
    return proj, residual

6.5 Desarrollo por etapas

Avanzas en este orden: escribes la tabla como matriz, dibujas cada fila como un vector, calculas producto punto, norma, unitario, distancia y coseno a mano y con NumPy, descompones un perfil en proyección y residual, compruebas el efecto de la escala y terminas eligiendo el vecino de una consulta con dos medidas distintas.

Etapa 1: Dataset como matriz

Construyes seis perfiles ficticios. Las variables de entrada son horas_lectura y horas_practica por semana. La puntuación del examen queda fuera de \(X\): es el objetivo \(y\), no una coordenada más del espacio de entrada.

Los valores no se sortean; se eligen para que cada par muestre algo:

  • Ana \((4, 4)\) y Bruno \((8, 8)\) tienen la misma dirección; Bruno dedica el doble de horas.
  • Camila \((8, 2)\) lee mucho y practica poco; Diego \((2, 8)\) hace lo contrario.
  • Elena \((3.6, 3.8)\) está muy cerca de Ana en el plano.
  • Fabián \((1, 1.2)\) tiene un perfil de pocas horas, con una proporción parecida a la de Ana.

Como sabes de antemano qué debería salir en distancia y en coseno, puedes comprobarlo con NumPy.

nombres = ["Ana", "Bruno", "Camila", "Diego", "Elena", "Fabián"]
X = np.array(
    [
        [4.0, 4.0],   # Ana: perfil equilibrado
        [8.0, 8.0],   # Bruno: misma dirección, doble magnitud
        [8.0, 2.0],   # Camila: lectora
        [2.0, 8.0],   # Diego: práctica
        [3.6, 3.8],   # Elena: cercana a Ana
        [1.0, 1.2],   # Fabián: intensidad baja
    ]
)
y = np.array([78.0, 91.0, 74.0, 80.0, 76.0, 58.0])

df = pd.DataFrame(X, columns=["horas_lectura", "horas_practica"], index=nombres)
df["puntuacion_examen"] = y

print("Tabla (lo que ve el analista):")
display(df)
print()
print("Matriz X (lo que ve el modelo):")
print(X)
print("Forma de X:", X.shape, "→ n observaciones × p features")
print("Vector y:", y)
print("Forma de y:", y.shape)
Tabla (lo que ve el analista):
horas_lectura horas_practica puntuacion_examen
Ana 4.0 4.0 78.0
Bruno 8.0 8.0 91.0
Camila 8.0 2.0 74.0
Diego 2.0 8.0 80.0
Elena 3.6 3.8 76.0
Fabián 1.0 1.2 58.0

Matriz X (lo que ve el modelo):
[[4.  4. ]
 [8.  8. ]
 [8.  2. ]
 [2.  8. ]
 [3.6 3.8]
 [1.  1.2]]
Forma de X: (6, 2) → n observaciones × p features
Vector y: [78. 91. 74. 80. 76. 58.]
Forma de y: (6,)

df es la vista del analista: nombres, unidades y el objetivo. X es la vista del algoritmo: solo números, con forma \((6, 2)\), es decir, \(N = 6\) filas y \(D = 2\) columnas; y tiene forma \((6,)\). Vectorizar, en la práctica, es elegir qué columnas entran a \(X\) y en qué orden. Cambiar el orden de las columnas no cambia las distancias si lo aplicas a todas las filas; meter el objetivo dentro de \(X\) sí cambia la geometría, y suele ser un error. Entrenar un modelo es casi siempre modelo.fit(X, y): esta etapa nombra esos dos objetos.

Etapa 2: Vectorización, la fila como punto

Dibujas cada fila \(\mathbf{x}_i\) como una flecha desde el origen. El origen \((0, 0)\) significa cero horas de lectura y cero de práctica; ningún perfil está ahí, pero sirve de ancla a la geometría. Al mirar la figura, fíjate qué flechas caen sobre la misma recta y cuáles apuntan a direcciones distintas aunque sumen muchas horas.

graficar_vectores_2d(
    nombres,
    X,
    "Cada estudiante es un vector en el plano lectura–práctica",
)
print("Normas (longitud de cada flecha):")
for nombre, vec in zip(nombres, X):
    print(f"  {nombre:8s}  ||x|| = {np.linalg.norm(vec):.3f}   x = {vec}")

Seis flechas desde el origen en el plano de horas de lectura (eje horizontal) y horas de práctica (eje vertical). Ana llega a (4, 4) y Bruno sigue la misma recta hasta (8, 8); Elena, a (3.6, 3.8), casi se superpone a Ana; Fabián es una flecha corta hasta (1, 1.2); Camila apunta abajo a la derecha, a (8, 2), y Diego arriba a la izquierda, a (2, 8).

Normas (longitud de cada flecha):
  Ana       ||x|| = 5.657   x = [4. 4.]
  Bruno     ||x|| = 11.314   x = [8. 8.]
  Camila    ||x|| = 8.246   x = [8. 2.]
  Diego     ||x|| = 8.246   x = [2. 8.]
  Elena     ||x|| = 5.235   x = [3.6 3.8]
  Fabián    ||x|| = 1.562   x = [1.  1.2]

Ana y Bruno están sobre la misma semirrecta, porque \((8, 8) = 2\cdot(4, 4)\), y la norma de Bruno (11.314) es el doble de la de Ana (5.657). Camila y Diego tienen exactamente la misma longitud, 8.246, con ángulos distintos. Elena (5.235) casi coincide con Ana. La flecha de Fabián es la más corta (1.562) y apunta cerca de la recta de Ana y Bruno.

La pregunta pasa a ser dónde está cada observación en el espacio y a quién se parece, más allá de la media de cada columna.

Ejercicio

Sin mirar el gráfico, decide si el vector np.array([6.0, 6.0]) queda sobre la recta de Ana y Bruno. Después compruébalo con su coseno respecto a Ana.

Sí: \((6, 6) = 1.5\cdot(4, 4)\), así que es un múltiplo positivo de Ana. Su coseno con Ana es \(\frac{6\cdot 4 + 6\cdot 4}{\|(6,6)\|\,\|(4,4)\|} = \frac{48}{8.485 \cdot 5.657} = 1\). Su norma, 8.485, queda entre la de Ana y la de Bruno.

Etapa 3: Producto punto

El producto punto resume en un solo número cuánto apuntan dos vectores hacia el mismo lado, ponderado por su tamaño. Para Ana \(\mathbf{a}=(4,4)\) y Bruno \(\mathbf{b}=(8,8)\), coordenada a coordenada:

\[ 4\cdot 8 + 4\cdot 8 = 64 \]

Y en forma geométrica:

\[ \mathbf{a}\cdot\mathbf{b} = \|\mathbf{a}\|\,\|\mathbf{b}\|\cos\theta \]

Si \(\cos\theta=1\), como en este par, el producto punto es exactamente el producto de las longitudes. La misma operación es la que hace cada neurona de una capa lineal, \(\mathbf{w}\cdot\mathbf{x} + b\); aquí la usas solo para describir.

def producto_punto(a, b):
    return float(np.dot(a, b))


ana, bruno, camila = X[0], X[1], X[2]

print("A mano (Ana · Bruno):", 4 * 8 + 4 * 8)
print("NumPy  (Ana · Bruno):", producto_punto(ana, bruno))
print("Ana · Camila:", producto_punto(ana, camila))
print("Ana · Diego :", producto_punto(ana, X[3]))
print()
print("Verificación geométrica Ana · Bruno:")
na, nb = np.linalg.norm(ana), np.linalg.norm(bruno)
print(f"  ||Ana|| = {na:.4f}, ||Bruno|| = {nb:.4f}")
print(f"  ||Ana|| × ||Bruno|| × cos(0) = {na * nb * 1:.4f}")

print()
print("Matriz de Gram G = X Xᵀ  (producto interno entre cada par de estudiantes):")
G = X @ X.T
gram = pd.DataFrame(np.round(G, 2), index=nombres, columns=nombres)
display(gram)
print("Diagonal:", np.round(np.diag(G), 2))
A mano (Ana · Bruno): 64
NumPy  (Ana · Bruno): 64.0
Ana · Camila: 40.0
Ana · Diego : 40.0

Verificación geométrica Ana · Bruno:
  ||Ana|| = 5.6569, ||Bruno|| = 11.3137
  ||Ana|| × ||Bruno|| × cos(0) = 64.0000

Matriz de Gram G = X Xᵀ  (producto interno entre cada par de estudiantes):
Ana Bruno Camila Diego Elena Fabián
Ana 32.0 64.0 40.0 40.0 29.60 8.80
Bruno 64.0 128.0 80.0 80.0 59.20 17.60
Camila 40.0 80.0 68.0 32.0 36.40 10.40
Diego 40.0 80.0 32.0 68.0 37.60 11.60
Elena 29.6 59.2 36.4 37.6 27.40 8.16
Fabián 8.8 17.6 10.4 11.6 8.16 2.44
Diagonal: [ 32.   128.    68.    68.    27.4    2.44]

Ana·Bruno \(= 64\) es grande porque los dos vectores están alineados y Bruno es largo. Ana·Camila \(= 40\) es menor: Camila también tiene muchas horas, pero buena parte de su vector apunta hacia la lectura, un eje en el que Ana no se concentra. Ana·Diego también da 40, por simetría. El producto punto mezcla tamaño y ángulo, así que no basta para decir que dos perfiles se parecen si sus normas difieren.

La matriz de Gram \(G=XX^{\top}\) reúne todos los productos punto entre filas: \(G_{ik}=\mathbf{x}_i\cdot\mathbf{x}_k\). Su diagonal contiene las normas al cuadrado (32 para Ana, 128 para Bruno, 68 para Camila y Diego). Es el objeto que reaparece en los métodos de kernel del nivel Avanzado. Su pariente \(X^{\top}X\), de tamaño \(D\times D\), compara columnas en lugar de filas: con las columnas centradas y dividida entre \(N-1\), es la matriz de covarianzas que usa el análisis de componentes principales.

Etapa 4: Norma, el tamaño del perfil

La norma euclidiana responde qué tan largo es el vector, sin decir hacia dónde va:

\[ \|\mathbf{x}\|_2 = \sqrt{x_1^2 + x_2^2} \]

Para Ana, \(\sqrt{16+16}=\sqrt{32}\approx 5.66\); para Bruno, \(\sqrt{64+64}=\sqrt{128}\approx 11.31\), el doble. Camila y Diego tienen la misma norma, \(\sqrt{68}\), con el reparto entre lectura y práctica invertido.

Además de \(L_2\), la norma \(L_1\) suma los valores absolutos:

\[ \|\mathbf{x}\|_1 = \sum_{j=1}^{D}|x_j| \]

\(L_2\) pesa más las coordenadas grandes, por el cuadrado; \(L_1\) trata cada unidad por igual. En regularización, la penalización \(L_2\) (Ridge) encoge los coeficientes y la \(L_1\) (Lasso) tiende a dejar algunos en cero.

def norma(v):
    return float(np.sqrt(np.sum(np.asarray(v) ** 2)))


print("Norma a mano (Ana):", np.sqrt(4 ** 2 + 4 ** 2))
print("Norma NumPy  (Ana):", np.linalg.norm(ana), "| función propia:", norma(ana))
print()
for nombre, vec in zip(nombres, X):
    print(f"{nombre:8s}  norma = {norma(vec):.3f}")

print()
print("Camila y Diego (normas parecidas, direcciones distintas):")
print("  Camila", X[2], "norma", norma(X[2]))
print("  Diego ", X[3], "norma", norma(X[3]))

print()
print(f"{'nombre':8s}  L2      L1      L1/L2")
for nombre, vec in zip(nombres, X):
    l2 = np.linalg.norm(vec, ord=2)
    l1 = np.linalg.norm(vec, ord=1)
    print(f"{nombre:8s}  {l2:6.3f}  {l1:6.3f}  {l1/l2:6.3f}")
Norma a mano (Ana): 5.656854249492381
Norma NumPy  (Ana): 5.656854249492381 | función propia: 5.656854249492381

Ana       norma = 5.657
Bruno     norma = 11.314
Camila    norma = 8.246
Diego     norma = 8.246
Elena     norma = 5.235
Fabián    norma = 1.562

Camila y Diego (normas parecidas, direcciones distintas):
  Camila [8. 2.] norma 8.246211251235321
  Diego  [2. 8.] norma 8.246211251235321

nombre    L2      L1      L1/L2
Ana        5.657   8.000   1.414
Bruno     11.314  16.000   1.414
Camila     8.246  10.000   1.213
Diego      8.246  10.000   1.213
Elena      5.235   7.400   1.414
Fabián     1.562   2.200   1.408

Bruno tiene el mismo perfil que Ana con más intensidad, y su norma (11.314) duplica la de Ana (5.657). Fabián apunta casi igual que Ana, pero su norma es 1.562; con la distancia euclidiana, quedará lejos de Ana aunque su proporción sea parecida (etapa 6).

El cociente \(L_1/L_2\) resume la forma del perfil: vale 1.414 (\(\sqrt{2}\)) cuando las dos coordenadas son iguales, como en Ana y Bruno, y baja a 1.213 en Camila y Diego, cuyos perfiles están desbalanceados. Camila y Diego empatan en \(L_2\) (8.246) y en \(L_1\) (\(8+2=10\)): su diferencia está en la dirección, no en el tamaño. La regularización \(L_2\), la normalización de embeddings y el weight decay trabajan con esta misma longitud.

Etapa 5: Vector unitario y Cauchy–Schwarz

El vector unitario de \(\mathbf{a}\) es \(\hat{\mathbf{a}}=\mathbf{a}/\|\mathbf{a}\|\). Dos vectores sobre la misma semirrecta, como Ana y Bruno, deben producir el mismo unitario; el de Camila debe ser distinto.

Cauchy–Schwarz afirma que \(|\mathbf{a}\cdot\mathbf{b}|\le\|\mathbf{a}\|\,\|\mathbf{b}\|\). El cociente entre los dos lados es \(|\cos\theta|\), que no puede superar 1, y la igualdad se da cuando un vector es múltiplo del otro. La celda calcula la holgura, la diferencia entre el lado derecho y el izquierdo.

def unitario(v):
    v = np.asarray(v, dtype=float)
    n = np.linalg.norm(v)
    if n == 0:
        raise ValueError("el vector nulo no tiene dirección")
    return v / n


print("Vectores unitarios (deben tener norma 1):")
for nombre, vec in zip(nombres, X):
    u = unitario(vec)
    print(f"  {nombre:8s}  û = {np.round(u, 4)}   ||û|| = {np.linalg.norm(u):.6f}")

print()
print("¿Ana y Bruno comparten dirección?  ||û_Ana − û_Bruno|| =",
      np.linalg.norm(unitario(X[0]) - unitario(X[1])))
print("¿Ana y Camila comparten dirección? ||û_Ana − û_Camila|| =",
      np.linalg.norm(unitario(X[0]) - unitario(X[2])))

print()
print("Cauchy–Schwarz: |a·b|  ≤  ||a|| ||b||")
pares = [("Ana", "Bruno", X[0], X[1]),
         ("Ana", "Camila", X[0], X[2]),
         ("Camila", "Diego", X[2], X[3])]
for na, nb, a, b in pares:
    izq = abs(np.dot(a, b))
    der = np.linalg.norm(a) * np.linalg.norm(b)
    print(f"  {na}–{nb}:  {izq:.4f}  ≤  {der:.4f}   holgura = {der - izq:.4f}")
Vectores unitarios (deben tener norma 1):
  Ana       û = [0.7071 0.7071]   ||û|| = 1.000000
  Bruno     û = [0.7071 0.7071]   ||û|| = 1.000000
  Camila    û = [0.9701 0.2425]   ||û|| = 1.000000
  Diego     û = [0.2425 0.9701]   ||û|| = 1.000000
  Elena     û = [0.6877 0.726 ]   ||û|| = 1.000000
  Fabián    û = [0.6402 0.7682]   ||û|| = 1.000000

¿Ana y Bruno comparten dirección?  ||û_Ana − û_Bruno|| = 0.0
¿Ana y Camila comparten dirección? ||û_Ana − û_Camila|| = 0.533867163791623

Cauchy–Schwarz: |a·b|  ≤  ||a|| ||b||
  Ana–Bruno:  64.0000  ≤  64.0000   holgura = 0.0000
  Ana–Camila:  40.0000  ≤  46.6476   holgura = 6.6476
  Camila–Diego:  32.0000  ≤  68.0000   holgura = 36.0000

Todos los unitarios tienen norma 1. Los de Ana y Bruno coinciden, \((0.7071, 0.7071)\), y la distancia entre ellos es 0; el de Camila, \((0.9701, 0.2425)\), queda a 0.534 del de Ana. En Cauchy–Schwarz, Ana–Bruno tiene holgura 0 (64 frente a 64), el caso de igualdad; Ana–Camila deja 6.648 y Camila–Diego, 36, porque no son múltiplos. Dividir por la norma quita la intensidad, y el coseno resulta ser el producto punto de dos unitarios:

\[ \cos\theta = \hat{\mathbf{a}}\cdot\hat{\mathbf{b}} \]

Etapa 6: Distancia euclidiana

\[ d(\mathbf{a},\mathbf{b}) = \sqrt{\sum_{j=1}^{D}(a_j-b_j)^2} = \|\mathbf{a}-\mathbf{b}\|_2 \]

Mide el segmento que une dos puntos, no el ángulo entre flechas. Antes de ver los números, piensa quién debería quedar más cerca de Ana: Bruno, con su misma dirección y el doble de horas, o Elena, que está casi en el mismo punto.

def distancia_euclidiana(a, b):
    return float(np.linalg.norm(np.asarray(a) - np.asarray(b)))


print("Distancia a Ana")
d_ana = []
for nombre, vec in zip(nombres, X):
    d = distancia_euclidiana(ana, vec)
    d_ana.append(d)
    print(f"  {nombre:8s}  d = {d:.3f}")

orden = np.argsort(d_ana)
print()
print("Ranking (más cercana → más lejana):")
for k, idx in enumerate(orden, start=1):
    if nombres[idx] == "Ana":
        continue
    print(f"  {k-1}. {nombres[idx]:8s}  d = {d_ana[idx]:.3f}")
Distancia a Ana
  Ana       d = 0.000
  Bruno     d = 5.657
  Camila    d = 4.472
  Diego     d = 4.472
  Elena     d = 0.447
  Fabián    d = 4.104

Ranking (más cercana → más lejana):
  1. Elena     d = 0.447
  2. Fabián    d = 4.104
  3. Camila    d = 4.472
  4. Diego     d = 4.472
  5. Bruno     d = 5.657

Elena gana con amplitud: está a 0.447 de Ana. Bruno, con el mismo perfil, queda el más lejano de todos (5.657), por detrás de Fabián (4.104) y de Camila y Diego (4.472), porque el doble de horas lo desplaza por el plano. La distancia euclidiana penaliza la diferencia de intensidad.

Un modelo de un solo vecino con esta medida diría que el vecino de Ana es Elena, no Bruno. Eso es correcto si «parecido» significa dedicar las mismas horas, e incorrecto si significa repartirlas en la misma proporción.

Etapa 7: Similitud coseno frente a distancia

\[ \cos\theta = \frac{\mathbf{a}\cdot\mathbf{b}}{\|\mathbf{a}\|\,\|\mathbf{b}\|} \]

Ana y Bruno van a tener \(\cos\theta = 1\) y, al mismo tiempo, la mayor distancia euclidiana. Ese es el contraste central del capítulo. Al ver el resultado, fíjate si Bruno sube al primer lugar y dónde queda Fabián, con su flecha tan corta.

def similitud_coseno(a, b):
    a = np.asarray(a, dtype=float)
    b = np.asarray(b, dtype=float)
    denom = np.linalg.norm(a) * np.linalg.norm(b)
    if denom == 0:
        return np.nan
    return float(np.dot(a, b) / denom)


print("Coseno con Ana")
c_ana = []
for nombre, vec in zip(nombres, X):
    c = similitud_coseno(ana, vec)
    c_ana.append(c)
    print(f"  {nombre:8s}  cos = {c:.4f}")

print()
print("Verificación Ana–Bruno (deben ser 1):", similitud_coseno(ana, bruno))
print("Ángulo Ana–Bruno (grados):", np.degrees(np.arccos(np.clip(similitud_coseno(ana, bruno), -1, 1))))
print("Ángulo Ana–Camila (grados):", np.degrees(np.arccos(np.clip(similitud_coseno(ana, camila), -1, 1))))

tabla = pd.DataFrame(
    {
        "distancia_a_Ana": d_ana,
        "coseno_con_Ana": c_ana,
    },
    index=nombres,
)
print()
display(tabla.round(3))
Coseno con Ana
  Ana       cos = 1.0000
  Bruno     cos = 1.0000
  Camila    cos = 0.8575
  Diego     cos = 0.8575
  Elena     cos = 0.9996
  Fabián    cos = 0.9959

Verificación Ana–Bruno (deben ser 1): 0.9999999999999998
Ángulo Ana–Bruno (grados): 1.2074182697257333e-06
Ángulo Ana–Camila (grados): 30.963756532073536
distancia_a_Ana coseno_con_Ana
Ana 0.000 1.000
Bruno 5.657 1.000
Camila 4.472 0.857
Diego 4.472 0.857
Elena 0.447 1.000
Fabián 4.104 0.996

Bruno empata con Ana misma en el primer lugar del coseno (1.000) y es el más lejano en distancia (5.657). Elena (0.9996) y Fabián (0.9959) quedan casi en 1: el coseno reconoce que Fabián reparte sus horas como Ana, aunque la distancia lo aleje (4.104). Camila y Diego bajan a 0.8575, un ángulo de 31° con Ana, aunque tengan más horas que ella. Es el mismo par de personas con dos conclusiones, como la misma media con dispersiones distintas de los capítulos 2 a 4.

Hay además un vínculo directo con el capítulo 5: si centras dos columnas (les restas su media) y calculas el coseno entre ellas, obtienes su correlación de Pearson.

Ejercicio

Calcula a mano el coseno entre Camila \((8, 2)\) y Diego \((2, 8)\) y el ángulo que forman. Interpreta el resultado en una frase: ¿comparten intensidad, dirección, ambas o ninguna?

El producto punto es \(8\cdot 2 + 2\cdot 8 = 32\) y las dos normas valen \(\sqrt{68}\), así que \(\cos\theta = 32/68 \approx 0.471\), un ángulo de unos 61.9°. Comparten intensidad (la misma norma y las mismas 10 horas en total) y no comparten dirección: uno se inclina hacia la lectura y el otro hacia la práctica. La distancia entre ellos es 8.485, la mayor de la tabla de la etapa 9.

Etapa 8: Ortogonalidad y proyección

Dos vectores son ortogonales si su producto punto es cero; en el plano, \((1,0)\) y \((0,1)\) son el ejemplo más simple. Camila \((8,2)\) y Diego \((2,8)\) no lo son, porque \(16+16=32\neq 0\). Sí puedes construir un vector ortogonal a Ana \((4,4)\), por ejemplo \(\mathbf{v}=(4,-4)\), porque \(16-16=0\).

La proyección de Camila sobre la recta de Ana responde a esta pregunta: si el único eje permitido fuera el del equilibrio entre lectura y práctica, ¿qué parte del perfil de Camila quedaría?

\[ \mathrm{proj}_{\mathbf{a}}(\mathbf{c}) = \frac{\mathbf{c}\cdot\mathbf{a}}{\|\mathbf{a}\|^2}\,\mathbf{a} \]

El residual debe quedar perpendicular a Ana, con producto punto 0.

ana, camila = X[0], X[2]
ortogonal_a_ana = np.array([4.0, -4.0])

print("Ana · (4, −4) =", np.dot(ana, ortogonal_a_ana), "  (ortogonal si es 0)")
print("Camila · Diego =", np.dot(X[2], X[3]), "  (no son ortogonales)")

proj, residual = graficar_proyeccion(
    camila, ana, "Camila", "Ana",
    "Proyección de Camila sobre la recta de Ana",
)

print("proj_Ana(Camila) =", np.round(proj, 4))
print("residual         =", np.round(residual, 4))
print("Ana · residual   =", np.round(np.dot(ana, residual), 10), "  (debe ser ~0)")
print("Teorema de Pitágoras: ||Camila||² = ||proj||² + ||residual||²")
print("  ", np.dot(camila, camila), "=", np.dot(proj, proj) + np.dot(residual, residual))
Ana · (4, −4) = 0.0   (ortogonal si es 0)
Camila · Diego = 32.0   (no son ortogonales)

Flecha azul de Ana hasta (4, 4), flecha verde de Camila hasta (8, 2) y una banda naranja ancha que prolonga la recta de Ana hasta el punto (5, 5), que es la proyección de Camila. Una línea roja punteada une (5, 5) con la punta de Camila: es el residual (3, −3), perpendicular a la recta de Ana.

proj_Ana(Camila) = [5. 5.]
residual         = [ 3. -3.]
Ana · residual   = 0.0   (debe ser ~0)
Teorema de Pitágoras: ||Camila||² = ||proj||² + ||residual||²
   68.0 = 68.0

La proyección de Camila sobre Ana es \((5, 5)\), la banda naranja ancha que sigue la recta de Ana hasta el punto naranja: con \(\mathbf{c}\cdot\mathbf{a} = 40\) y \(\|\mathbf{a}\|^2 = 32\), es \(1.25\cdot(4,4)\). El residual, la línea roja punteada, es \((3, -3)\): el exceso de lectura sobre práctica. Como Ana·residual \(= 0\), esa parte no se ve desde la recta de Ana. Las longitudes cumplen el teorema de Pitágoras: \(68 = 50 + 18\).

En un modelo lineal, el residual es el error que la dirección elegida no explica. En el PCA, con los datos centrados (restando la media de cada columna), se elige el eje que maximiza la suma de las normas al cuadrado de las proyecciones, es decir, la varianza proyectada.

Etapa 9: Interpretación geométrica conjunta

Vuelves a dibujar las flechas y resumes en una tabla la correspondencia visual: la longitud es la norma, la apertura entre flechas es el ángulo y el segmento entre las puntas es la distancia.

graficar_vectores_2d(
    nombres,
    X,
    "Geometría: misma recta (Ana–Bruno), casi la misma (Elena, Fabián) frente a otras direcciones",
)

resumen = pd.DataFrame(
    {
        "par": ["Ana–Bruno", "Ana–Elena", "Ana–Fabián", "Ana–Camila", "Camila–Diego"],
        "euclidiana": [
            distancia_euclidiana(X[0], X[1]),
            distancia_euclidiana(X[0], X[4]),
            distancia_euclidiana(X[0], X[5]),
            distancia_euclidiana(X[0], X[2]),
            distancia_euclidiana(X[2], X[3]),
        ],
        "coseno": [
            similitud_coseno(X[0], X[1]),
            similitud_coseno(X[0], X[4]),
            similitud_coseno(X[0], X[5]),
            similitud_coseno(X[0], X[2]),
            similitud_coseno(X[2], X[3]),
        ],
        "lectura": [
            "Misma dirección; distinta intensidad",
            "Cercanos en el plano",
            "Casi la misma dirección; Fabián es corto",
            "Ángulo abierto; Camila es lectora",
            "Trade-off opuesto lectura/práctica",
        ],
    }
)
display(resumen.round(3))

Las mismas seis flechas de la etapa 2: Ana y Bruno sobre la recta diagonal, Elena y Fabián casi sobre ella, Camila hacia la lectura y Diego hacia la práctica.

par euclidiana coseno lectura
0 Ana–Bruno 5.657 1.000 Misma dirección; distinta intensidad
1 Ana–Elena 0.447 1.000 Cercanos en el plano
2 Ana–Fabián 4.104 0.996 Casi la misma dirección; Fabián es corto
3 Ana–Camila 4.472 0.857 Ángulo abierto; Camila es lectora
4 Camila–Diego 8.485 0.471 Trade-off opuesto lectura/práctica

La tabla junta los dos casos extremos. Ana–Bruno tiene coseno 1.000 y distancia 5.657; Ana–Elena, coseno 1.000 y distancia 0.447. Camila–Diego tiene la mayor distancia (8.485) y el menor coseno (0.471). Si te preguntas quién estudia como Ana, aunque sea con más o menos horas, mira el coseno; si te preguntas quién tiene una carga horaria parecida a la de Ana, mira la distancia. Ninguna medida es mejor en general: la elección depende de la pregunta.

En búsqueda de documentos, dos textos del mismo tema y distinta longitud suelen recuperarse mejor con el coseno. Con sensores industriales, dos lecturas parecidas en valor absoluto se comparan mejor con la distancia, después de escalar.

Etapa 10: La escala de las variables cambia el espacio

Agregas una columna en otra unidad: minutos de pantalla al día. Ana y Elena siguen cerca en horas de estudio, pero Fabián, que en el plano original casi no se mueve, ahora tiene 240 minutos frente a 80–95 de los demás. Sin escalar, esa columna domina la distancia. La celda calcula las distancias a Ana con los datos crudos y después de estandarizar cada columna con su media y su desviación estándar, el z-score del capítulo 4.

minutos_pantalla = np.array([90.0, 95.0, 80.0, 85.0, 88.0, 240.0])
X_crudo = np.column_stack([X, minutos_pantalla])

df_crudo = pd.DataFrame(
    X_crudo,
    columns=["horas_lectura", "horas_practica", "minutos_pantalla"],
    index=nombres,
)
print("X sin escalar (unidades mezcladas):")
display(df_crudo)

print("Distancia euclidiana a Ana en R^3 sin escalar")
d_crudo = [distancia_euclidiana(X_crudo[0], fila) for fila in X_crudo]
for nombre, d in zip(nombres, d_crudo):
    print(f"  {nombre:8s}  d = {d:.3f}")

# Estandarización: (x - media) / desv. de cada columna (estadísticos del conjunto)
medias = X_crudo.mean(axis=0)
desv = X_crudo.std(axis=0, ddof=1)
X_std = (X_crudo - medias) / desv

print()
print("Distancia euclidiana a Ana después de estandarizar columnas")
d_std = [distancia_euclidiana(X_std[0], fila) for fila in X_std]
for nombre, d in zip(nombres, d_std):
    print(f"  {nombre:8s}  d = {d:.3f}")
X sin escalar (unidades mezcladas):
horas_lectura horas_practica minutos_pantalla
Ana 4.0 4.0 90.0
Bruno 8.0 8.0 95.0
Camila 8.0 2.0 80.0
Diego 2.0 8.0 85.0
Elena 3.6 3.8 88.0
Fabián 1.0 1.2 240.0
Distancia euclidiana a Ana en R^3 sin escalar
  Ana       d = 0.000
  Bruno     d = 7.550
  Camila    d = 10.954
  Diego     d = 6.708
  Elena     d = 2.049
  Fabián    d = 150.056

Distancia euclidiana a Ana después de estandarizar columnas
  Ana       d = 0.000
  Bruno     d = 1.926
  Camila    d = 1.521
  Diego     d = 1.532
  Elena     d = 0.155
  Fabián    d = 2.779

Sin escalar, Fabián queda a 150.056 de Ana: sus 150 minutos de diferencia pesan mucho más que las horas de estudio. Los minutos también reordenan al resto: Camila (10.954) queda más lejos que Diego (6.708) y Bruno (7.550) solo porque tiene 10 minutos menos que Ana. Tras estandarizar, cada variable aporta en unidades de desviación estándar: Elena sigue primera (0.155), Camila y Diego empatan casi (1.521 y 1.532), y Fabián sigue último pero a 2.779, en la misma escala que los demás.

El coseno no resuelve este problema por sí solo: no cambia si multiplicas el vector entero por una constante, pero sí cuando una sola coordenada está en otra escala. Escalar es decidir que una hora de lectura y un minuto de pantalla no deben pesar lo mismo en la distancia.

Etapa 11: El vecino de una consulta

Llega un perfil nuevo, la consulta \(\mathbf{q}=(6.5,\,6.5)\): misma dirección que Ana y Bruno, con una magnitud más cercana a la de Bruno. Eliges su vecino más cercano entre las seis filas de \(X\), que no incluyen la consulta, una vez con la distancia euclidiana y otra con el coseno. Antes de ejecutar, anticipa quién gana con cada medida.

consulta = np.array([6.5, 6.5])

d_q = np.array([distancia_euclidiana(consulta, fila) for fila in X])
c_q = np.array([similitud_coseno(consulta, fila) for fila in X])

idx_euc = int(np.argmin(d_q))
idx_cos = int(np.argmax(c_q))

print("Consulta q =", consulta)
print()
print("Ranking euclidiano (menor distancia):")
for idx in np.argsort(d_q):
    print(f"  {nombres[idx]:8s}  d = {d_q[idx]:.3f}")
print("Vecino 1-NN euclidiano:", nombres[idx_euc])

print()
print("Ranking coseno (mayor similitud):")
for idx in np.argsort(-c_q):
    print(f"  {nombres[idx]:8s}  cos = {c_q[idx]:.4f}")
print("Vecino 1-NN coseno:", nombres[idx_cos])

graficar_barras_metricas(
    nombres, d_q, c_q,
    "La misma consulta, dos órdenes distintos",
)
graficar_puntos_y_consulta(
    nombres, X, consulta,
    [(idx_euc, "-", f"1-NN euclidiano: {nombres[idx_euc]}"),
     (idx_cos, "--", f"1-NN coseno: {nombres[idx_cos]}")],
    "Segmento continuo: 1-NN euclidiano · Segmento punteado: 1-NN coseno",
)
graficar_vectores_2d(
    nombres, X,
    "La consulta (flecha negra) está en la recta de Ana–Bruno",
    query=consulta,
)
Consulta q = [6.5 6.5]

Ranking euclidiano (menor distancia):
  Bruno     d = 2.121
  Ana       d = 3.536
  Elena     d = 3.962
  Camila    d = 4.743
  Diego     d = 4.743
  Fabián    d = 7.638
Vecino 1-NN euclidiano: Bruno

Ranking coseno (mayor similitud):
  Ana       cos = 1.0000
  Bruno     cos = 1.0000
  Elena     cos = 0.9996
  Fabián    cos = 0.9959
  Diego     cos = 0.8575
  Camila    cos = 0.8575
Vecino 1-NN coseno: Ana

Tres figuras. Primera: dos paneles de barras con la distancia y el coseno de cada perfil a la consulta (6.5, 6.5); en distancia destaca en naranja Bruno (2.12) y en coseno Ana y Bruno (1.0000). Segunda: los seis perfiles como puntos, la consulta como estrella, un segmento continuo hasta Bruno (vecino euclidiano) y uno punteado hasta Ana (vecino por coseno). Tercera: las flechas de la etapa 2 con la flecha de la consulta, casi oculta bajo la de Bruno en la misma recta.

Tres figuras. Primera: dos paneles de barras con la distancia y el coseno de cada perfil a la consulta (6.5, 6.5); en distancia destaca en naranja Bruno (2.12) y en coseno Ana y Bruno (1.0000). Segunda: los seis perfiles como puntos, la consulta como estrella, un segmento continuo hasta Bruno (vecino euclidiano) y uno punteado hasta Ana (vecino por coseno). Tercera: las flechas de la etapa 2 con la flecha de la consulta, casi oculta bajo la de Bruno en la misma recta.

Tres figuras. Primera: dos paneles de barras con la distancia y el coseno de cada perfil a la consulta (6.5, 6.5); en distancia destaca en naranja Bruno (2.12) y en coseno Ana y Bruno (1.0000). Segunda: los seis perfiles como puntos, la consulta como estrella, un segmento continuo hasta Bruno (vecino euclidiano) y uno punteado hasta Ana (vecino por coseno). Tercera: las flechas de la etapa 2 con la flecha de la consulta, casi oculta bajo la de Bruno en la misma recta.

Por distancia, el vecino es Bruno (2.121), seguido de Ana (3.536). Por coseno, Ana y Bruno empatan en 1.0000 porque la consulta está sobre su recta, y argmax devuelve el primero de la lista: Ana. En las barras, Bruno es el único naranja a la izquierda y Ana y Bruno comparten el naranja a la derecha. En la última figura, la flecha negra de la consulta queda casi tapada por la de Bruno, porque las dos están sobre la misma recta. Las dos medidas eligen personas distintas: en un sistema de recomendación o en un modelo de k vecinos, cambiar la medida cambia el vecino y, con él, la predicción.

Ejercicio

Cambia la consulta a \((7.5,\,2.2)\), un perfil lector, y vuelve a calcular los dos rankings. ¿Coinciden el vecino euclidiano y el vecino por coseno? Escribe en dos líneas qué idea de «parecido» usas en cada caso.

Esta vez coinciden: Camila gana en las dos medidas, a 0.539 de distancia y con coseno 0.9992. Los órdenes siguientes difieren: por distancia siguen Ana (3.936), Elena (4.215) y Bruno (5.822); por coseno, Ana y Bruno empatan en 0.8775, delante de Elena (0.8643). La distancia busca a quien dedica horas parecidas; el coseno, a quien las reparte en la misma proporción. Con una consulta cercana a un perfil en posición y en dirección, las dos ideas apuntan a la misma persona.

Errores que conviene nombrar

  1. Meter el objetivo \(y\) dentro de \(X\). Cambia la geometría y filtra la respuesta hacia las variables de entrada.
  2. Leer el producto punto como similitud. Mezcla tamaño y ángulo: Bruno tiene el mayor producto punto con todos porque es el vector más largo.
  3. Usar la distancia euclidiana cuando la pregunta es la dirección, o el coseno cuando importa la magnitud.
  4. Medir distancias con columnas en unidades distintas. En la etapa 10, los minutos de pantalla pusieron a Fabián a 150.056 de Ana.
  5. Creer que el coseno corrige la escala. No cambia al escalar el vector entero, pero sí cuando cambia una sola coordenada.
  6. Calcular el coseno con un vector nulo. No tiene dirección y el cociente no está definido.
  7. Olvidar los empates. Con dos vecinos igual de similares, argmax elige el primero, y el resultado depende del orden de las filas.

6.6 Guía rápida: ¿qué medida usar?

Situación Medida principal Justificación
Guardar el dataset para un modelo Matriz \(X\), vector \(y\) Es la entrada de casi cualquier fit.
Qué tan intenso es un perfil Norma Mide longitud; no informa dirección.
Si dos perfiles apuntan igual Coseno No cambia al escalar el vector completo.
Si dos observaciones están cerca en valor Distancia euclidiana Penaliza magnitud y dirección.
Variables en unidades distintas Estandarizar y después medir distancia Si no, gana la columna de mayor rango.
Textos, embeddings, tema compartido Coseno La longitud del documento no debería decidir.
Capa lineal, proyección, alineación Producto punto Mezcla norma y ángulo; hay que leerlo con cuidado.
Si un vector es múltiplo de otro Cauchy–Schwarz (holgura \(0\)) La igualdad equivale a dependencia lineal.
Quitar la intensidad y dejar la dirección Vector unitario \(\hat{\mathbf{x}}=\mathbf{x}/\|\mathbf{x}\|\); el coseno es \(\hat{\mathbf{a}}\cdot\hat{\mathbf{b}}\).
Qué parte de A está en la recta de B Proyección ortogonal El residual es perpendicular a la dirección de referencia.

Al reportar, indica qué variables entran a \(X\), qué medida usas y si escalaste.

6.7 Preguntas de comprensión

  1. Un recomendador de rutinas de estudio debe sugerir a Fabián compañeros con hábitos parecidos. ¿Usarías la distancia euclidiana o el coseno? ¿Qué compañero obtendría con cada una?
  2. ¿Por qué el producto punto entre Ana y Bruno (64) es mayor que entre Ana y Elena (29.6), si Elena está mucho más cerca de Ana?
  3. Agregas a un dataset una columna de ingresos en pesos junto a variables medidas en horas. ¿Qué le pasa a un modelo de k vecinos si no estandarizas?
  4. ¿Qué te dice que la holgura de Cauchy–Schwarz entre dos perfiles sea exactamente 0?
  5. En la etapa 8, ¿qué representa el residual \((3, -3)\) de Camila respecto a la recta de Ana?
  1. El coseno, si «hábitos parecidos» significa la misma proporción entre lectura y práctica. Con coseno, el más parecido es Elena (0.998), seguida de Ana y Bruno empatados (0.9959). Con distancia también gana Elena (3.677) y sigue Ana (4.104), pero Bruno cae al último lugar (9.759) por su cantidad de horas. Las dos medidas coinciden en el primero y discrepan sobre Bruno: la decisión depende de si la intensidad importa para la recomendación.
  2. Porque el producto punto crece con las normas: Bruno mide 11.314 y Elena 5.235, y los dos están casi alineados con Ana. Para comparar parecido sin el efecto del tamaño, divide por las normas (coseno) o mide la distancia.
  3. La columna de ingresos, con un rango de miles o millones, domina la distancia y decide sola quién es vecino, como los minutos de pantalla con Fabián en la etapa 10 (150.056 sin escalar frente a 2.779 tras estandarizar).
  4. Que uno es múltiplo del otro: están sobre la misma recta y su coseno es 1 o −1, como Ana y Bruno.
  5. La parte del perfil de Camila que la dirección de Ana no explica: 3 horas más de lectura y 3 menos de práctica que su proyección \((5, 5)\). Es ortogonal a Ana, con producto punto 0, y es la misma idea del residual de una regresión lineal (capítulo 13).

6.8 Referencias para profundizar

  • Strang, G. (2023). Introduction to Linear Algebra (6.ª ed.). Wellesley-Cambridge Press. Capítulo 1: vectores, producto punto, longitud y ángulo.
  • Deisenroth, M. P., Faisal, A. A. y Ong, C. S. (2020). Mathematics for Machine Learning. Cambridge University Press. Capítulo 3 (Analytic Geometry): normas, producto interno, ángulos, ortogonalidad y proyecciones. Versión en PDF gratuita en https://mml-book.github.io/.
  • Manning, C. D., Raghavan, P. y Schütze, H. (2008). Introduction to Information Retrieval. Cambridge University Press. Capítulo 6: el modelo de espacio vectorial y la similitud coseno entre documentos.
  • scikit-learn. Nearest Neighbors (guía de usuario, sección 1.6). https://scikit-learn.org/stable/modules/neighbors.html. Vecinos más cercanos y las métricas de distancia disponibles.

6.9 Lo que debes llevarte de este capítulo

Al vectorizar eliges un espacio, y en ese espacio «cerca» depende de la medida que uses: la distancia mide separación e intensidad, y el coseno mide proporción y dirección.

  • Un dataset es una matriz \(X\) con \(N\) filas y \(D\) columnas, más un objetivo \(y\) que no entra al espacio de entrada.
  • La norma mide el tamaño del perfil: Bruno (11.314) duplica a Ana (5.657) con la misma dirección.
  • El producto punto mezcla tamaño y ángulo; el coseno lo divide por las normas y queda entre −1 y 1 por la desigualdad de Cauchy–Schwarz.
  • Ana y Bruno tienen coseno 1 y distancia 5.657, la mayor desde Ana; Elena está a 0.447.
  • La proyección parte un vector en una componente explicada y un residual ortogonal: Camila \((8, 2)\) = \((5, 5)\) + \((3, -3)\).
  • Sin escalar, una columna en minutos puso a Fabián a 150.056 de Ana; estandarizada, a 2.779.
  • Con la consulta \((6.5, 6.5)\), el vecino por distancia es Bruno y el vecino por coseno es Ana: cambiar la medida cambia la predicción.

6.10 Puente hacia los siguientes capítulos

Con este capítulo termina la parte descriptiva del libro: centro, dispersión, forma, relación entre variables y geometría, todo sobre los datos que ya tienes. El capítulo 7 cambia la pregunta e introduce la probabilidad, el lenguaje para hablar de lo que todavía no has observado, y el capítulo 8 la lleva a variables aleatorias y sus distribuciones.

La geometría de este capítulo vuelve en la regresión lineal del capítulo 13, donde el ajuste es una proyección y el residual es ortogonal a las variables, y en el nivel Avanzado: el análisis de componentes principales busca en este mismo espacio ejes nuevos que conserven la mayor varianza posible, y los métodos de kernel trabajan con productos punto como los de la matriz de Gram.