Capítulo 28 de 37 · intermedio
Análisis de componentes principales
Qué cubre este capítulo
k-means preguntaba dónde están los grupos. Este capítulo hace una pregunta no supervisada distinta: qué direcciones de los datos realmente cargan información, y cuáles son puro ruido que puedes tirar a la basura. Eso es reducción de dimensionalidad, y el análisis de componentes principales es el caballo de batalla que todos aprenden primero.
La idea cabe en una línea. Encuentra los ejes a lo largo de los cuales los datos varían más, quédate con los primeros y descarta el resto. Un dataset de cuatro columnas se convierte en dos columnas que puedes graficar, y si elegiste bien, casi no pierdes nada que valga la pena conservar. Lo construimos a mano en NumPy — centrar los datos, formar la matriz de covarianza, tomar su eigendescomposición, ordenar por eigenvalor, proyectar sobre las direcciones principales — y luego le pasamos el mismo trabajo a scikit-learn y comprobamos que los dos coinciden hasta el signo. Y como todo el asunto es una rotación en el plano, podemos verlo en acción: una nube de puntos inclinada, los dos ejes principales acomodándose en su lugar, la nube girando hasta que el eje largo queda horizontal, y cada punto cayendo sobre esa única línea.
El concepto corre sobre una nube 2-D de juguete para que veas moverse cada punto. El resultado real corre sobre iris — 150 flores, cuatro mediciones cada una — aplastadas de cuatro dimensiones a dos y coloreadas por especie, para mostrar que la estructura sobrevive al apretón.
Un poco de historia
El PCA se ha inventado al menos dos veces. Karl Pearson llegó primero, en 1901, en un artículo con el título maravillosamente literal "On lines and planes of closest fit to systems of points in space". Su planteamiento era geométrico: dada una nube de puntos, encontrar la línea, y luego el plano, que queda más cerca de todos ellos en el sentido de mínimos cuadrados. Esa línea de mejor ajuste es el primer componente principal. Lo hacía con lápiz y datos biométricos, sin computadora a la vista, y todo el argumento gira en torno a distancias perpendiculares a una línea.
Harold Hotelling llegó por el otro lado en 1933, en el Journal of Educational Psychology, y de ahí vienen el nombre moderno y la maquinaria. Hotelling no estaba ajustando una línea; miraba un montón de variables correlacionadas — puntajes de exámenes, sobre todo — y buscaba un conjunto más pequeño de variables no correlacionadas que capturara la misma información. Las derivó como las direcciones de máxima varianza, demostró que eran los eigenvectores de la matriz de covarianza, y las llamó los componentes principales. Dos descripciones, un algoritmo: la línea más cercana a los puntos es la misma que la dirección a lo largo de la cual los puntos más se dispersan. Esa equivalencia es el corazón silencioso del método, y por eso la misma herramienta se vende a veces como compresión y a veces como decorrelación.
La intuición
Mira una nube de puntos estirada e inclinada, como un puro acostado en ángulo. Dos números describen cada punto, su x y su y, pero son redundantes: dime dónde está un punto a lo largo de la dirección larga del puro y puedo adivinar el resto, porque la nube es delgada en la otra dirección. Los dos ejes originales son una mala descripción de estos datos. No están mal, simplemente no están alineados con nada que le importe a los datos.
El PCA encuentra un mejor par de ejes. El primero, PC1, apunta a lo largo de la dirección en la que la nube está más dispersa — el largo del puro. El segundo, PC2, está obligado a ser perpendicular a él y recoge la varianza que queda, el ancho delgado. Estos no son dos de tus features originales; son direcciones nuevas, mezclas de las viejas, elegidas para que la primera capture tanta dispersión como sea posible y cada una de las siguientes recoja las sobras. Una vez que estás sobre esos ejes, los datos te dicen cuánto les importa cada uno: casi toda la varianza está en PC1, casi nada en PC2. Así que te quedas con PC1, descartas PC2, y pasaste de dos números por punto a uno sin apenas alterar la imagen.
Aquí está la nube con la que vamos a trabajar — 150 puntos, un blob gaussiano correlacionado, inclinado fuera de los ejes a propósito:
Tu ojo ya encontró la dirección larga. El PCA es la manera en que una computadora encuentra esa misma línea, y lo hace con una matriz de covarianza y una eigendescomposición.
La matemática
Empieza con puntos , cada uno un vector en dimensiones. El PCA mide varianza, y la varianza se mide alrededor de la media, así que el primer paso es centrar. Sea la media, y réstala de cada punto:
Ahora la nube está centrada en el origen. Apila los puntos centrados como los renglones de una matriz (forma ) y forma la matriz de covarianza:
es y simétrica. Su entrada diagonal es la varianza del feature ; la entrada fuera de la diagonal es la covarianza entre los features y , cuánto se mueven juntos. Todo lo que el PCA sabe sobre la forma de los datos está en esta matriz. Las direcciones principales son sus eigenvectores — los vectores que solamente estira sin rotar:
Cada eigenvector es una dirección principal, y su eigenvalor es exactamente la varianza de los datos a lo largo de esa dirección. Como es simétrica, los eigenvectores son ortogonales, así que los forman un conjunto limpio de nuevos ejes perpendiculares. Ordénalos de modo que y es la dirección de mayor varianza, la siguiente, y así sucesivamente.
Para reducir a dimensiones, quédate con los eigenvectores principales como las columnas de una matriz y proyecta cada punto centrado sobre ellas:
El vector contiene las coordenadas del punto sobre los nuevos ejes — sus scores. Esa es la reducción: entran números, salen . Para juzgar lo que conservaste, la proporción de varianza explicada del componente es su eigenvalor sobre el total:
Estas suman 1 a lo largo de los componentes. La suma acumulada sobre los principales es la fracción de la varianza total de los datos que tu versión reducida todavía carga — el único número que usas para decidir cuántos componentes son suficientes.
En qué es bueno, en qué no
La razón por la que el PCA está en todas partes es que hace varios trabajos que no parecen relacionados con un solo cómputo. Comprime: cambia features correlacionados por que cargan casi toda la varianza. Visualiza: fuerza datos de alta dimensión a bajar a dos o tres ejes que sí puedes graficar. Decorrelaciona: los nuevos ejes son ortogonales por construcción, así que los componentes no están correlacionados aunque los features originales estuvieran enredados entre sí, lo cual es un regalo para cualquier modelo que se atraganta con la colinealidad. Y quita ruido: las direcciones de eigenvalor pequeño suelen ser donde vive el ruido, así que descartarlas puede dejar una señal más limpia que la original. Una eigendescomposición, cuatro usos.
El pero es una sola palabra: lineal. El PCA solo sabe rotar y proyectar. Sus componentes son combinaciones en línea recta de tus features, y la estructura que encuentra es la que una matriz de covarianza puede ver — varianza y correlación por pares, nada más. Dale datos que se curvan, una espiral o una forma que vive sobre una hoja doblada, y el PCA los aplana mal, porque ninguna rotación de los ejes desenreda una curva. También equipara varianza con importancia, lo cual no siempre es cierto; una dirección de baja varianza puede cargar la señal que de verdad te importa mientras una de alta varianza es solamente el feature más ruidoso e inútil ahogando todo lo demás. Y los componentes son combinaciones de todos los features originales, así que a menudo son difíciles de nombrar — PC1 es una mezcla ponderada de largo de sépalo, largo de pétalo y el resto, lo cual resume bien y explica mal.
Los datos
Dos datasets, para dos trabajos. La animación del concepto corre sobre la nube 2-D inclinada de arriba: 150 puntos de una gaussiana estirada a lo largo de un eje y rotada unos 33 grados fuera de la horizontal, con semilla fija para que la imagen sea estable. Dos dimensiones para que toda la rotación quepa en la página y puedas seguir puntos individuales.
El resultado real corre sobre iris — las mediciones de Fisher de 1936 sobre 150 flores, 50 de cada una de tres especies, cuatro features cada una: largo de sépalo, ancho de sépalo, largo de pétalo, ancho de pétalo. Cuatro dimensiones, que no puedes dibujar. Toda la promesa del PCA es arreglar eso: colapsar las cuatro mediciones a dos componentes principales y graficar cada flor sobre un plano. Las etiquetas de especie vienen de pasajeras, pero el PCA nunca las ve — solo recibe los cuatro números por flor. Usamos las etiquetas hasta el final, para colorear la gráfica y comprobar si la reducción mantuvo separadas a las tres especies.
Constrúyelo, una función a la vez
Siete funciones cortas, y se alinean exactamente con la matemática. Centrar, covarianza, eigendescomponer, y luego las tres cosas que haces con el resultado: medir varianza, proyectar, reconstruir. El primer paso es la resta de la media, y devolvemos la media porque la vamos a necesitar después para deshacer el desplazamiento:
def center(X):
"""Subtract the column means so the cloud sits at the origin.
PCA is about variance — directions of spread — and spread is measured
around the mean. Returns the centered data and the mean vector (kept so we
can undo the shift when we reconstruct).
"""
mean = X.mean(axis=0)
return X - mean, mean
Con la nube centrada en el origen, la matriz de covarianza es un producto de matrices. Divide entre , el estimador insesgado, para coincidir con lo que reportan numpy y sklearn:
def covariance(Xc):
"""The (D, D) covariance matrix of already-centered data.
Entry (a, b) is the covariance between feature a and feature b. The
diagonal is each feature's variance; off-diagonals say how features move
together. We divide by N-1 (the unbiased estimator), which is also what
numpy and sklearn use.
"""
n = len(Xc)
return (Xc.T @ Xc) / (n - 1)
Esa matriz contiene la forma completa de los datos. Ahora la
línea que la convierte en direcciones. La matriz de covarianza es simétrica, así
que eigh es la rutina correcta — devuelve eigenvalores reales y eigenvectores
ortonormales, y volteamos su orden ascendente a descendente para que el
componente 0 sea la dirección de mayor varianza:
def eigendecompose(C):
"""Eigenvalues and eigenvectors of the covariance matrix, sorted.
C is symmetric, so `eigh` is the right tool — real eigenvalues, orthonormal
eigenvectors. `eigh` returns them in ascending order; we flip to descending
so component 0 is the direction of greatest variance. Eigenvectors are the
COLUMNS of the returned matrix, one principal direction each.
"""
eigvals, eigvecs = np.linalg.eigh(C)
order = np.argsort(eigvals)[::-1]
return eigvals[order], eigvecs[:, order]
Los eigenvectores regresan como columnas, una dirección principal cada una, ya ordenadas. Los eigenvalores son las varianzas a lo largo de ellas, y convertir eso en la fracción que explica cada componente es una simple división:
def explained_variance_ratio(eigvals):
"""Fraction of total variance each component accounts for.
Each eigenvalue IS the variance along its eigenvector, so the ratio is just
the eigenvalue over their sum. These add to 1 across all components; the
cumulative sum tells you how much you keep by stopping at k.
"""
return eigvals / eigvals.sum()
Ese es el número que decide con cuántos componentes quedarte. Ahora la reducción en sí — proyecta los puntos centrados sobre las k direcciones principales para obtener los scores:
def project(Xc, components, k):
"""Project centered data onto the top-k principal directions.
`components` holds eigenvectors as columns (from eigendecompose). Taking the
first k columns and multiplying gives the scores: each point's coordinates
in the new k-dimensional basis. This is the dimensionality reduction —
(N, D) in, (N, k) out.
"""
return Xc @ components[:, :k]
Entra (N, D), sale (N, k). Ese es todo el punto del método en una
multiplicación de matrices. El camino de regreso es la reconstrucción: sube los
scores de baja dimensión de vuelta al espacio original y vuelve a sumar la
media. Con k < D no puede ser exacta — lo que regresa es la aproximación de
rango k más cercana, y la brecha es la varianza que descartaste:
def reconstruct(scores, components, mean, k):
"""Map the k-dim scores back into the original feature space.
Undo the projection (multiply by the components' transpose) and add the
mean back. With k < D this is lossy: what comes back is the closest
rank-k approximation of the original point. The gap between the two is the
reconstruction error, and it equals the variance in the components we
dropped.
"""
return scores @ components[:, :k].T + mean
Al final, el wrapper que corre el pipeline de principio a fin y devuelve todo lo que el capítulo necesita — media, componentes, eigenvalores, proporción de varianza explicada y los scores — en un solo dict:
def pca(X, k):
"""Full PCA: center, covariance, eigendecompose, project onto top-k.
Returns everything the chapter needs in one dict — the mean, the sorted
principal directions (columns), the eigenvalues (per-component variance),
the explained-variance ratio, and the k-dim scores. This is the whole
algorithm; every field comes from the small functions above.
"""
Xc, mean = center(X)
C = covariance(Xc)
eigvals, components = eigendecompose(C)
scores = project(Xc, components, k)
return {
"mean": mean,
"components": components, # (D, D), eigenvectors as columns
"eigenvalues": eigvals, # variance along each component
"explained_variance_ratio": explained_variance_ratio(eigvals),
"scores": scores, # (N, k) reduced coordinates
}
Sin ciclos, sin iteración, sin convergencia que esperar. A diferencia de k-means, el PCA es un cómputo de forma cerrada: una eigendescomposición y listo. Lo cual lo hace raro de animar — no hay pasos que reproducir. Así que en vez de eso animamos el significado.
Míralo trabajar
El PCA no itera, así que no hay nada que recorrer paso a paso como k-means
marcha sus centroides. Lo que sí hay que ver es la geometría: qué le hace
realmente a la nube eso de "proyectar sobre los componentes principales". La
animación de abajo corre la función pca real sobre los datos 2-D inclinados y
reproduce la transformación en tres actos. Cada coordenada está calculada, no
montada — los ejes son los eigenvectores verdaderos, la rotación es el cambio de
base real, los residuales son el componente descartado de verdad.
Primero aparecen los ejes. La flecha naranja larga es PC1, tendida a lo largo de la dirección en la que la nube está más dispersa; ella sola da cuenta del 92.7% de la varianza. La flecha cian más corta es PC2, perpendicular a ella, cargando el 7.3% restante. Luego la nube entera rota, flechas incluidas, hasta que PC1 queda horizontal — ese es el cambio de base, el momento en que los ejes naturales de los datos se vuelven los ejes de la gráfica. Al final, cada punto cae en línea recta sobre la línea de PC1, y el muñón gris que queda atrás es su residual, la coordenada de PC2 que estamos eligiendo tirar. Los puntos están sombreados según su posición a lo largo de PC1 para que puedas rastrear dónde aterriza cada uno.
Fíjate en el último acto. Casi todos los puntos apenas se mueven al caer — los muñones residuales son diminutos — porque para empezar casi no había nada en la dirección de PC2. Ese es el 92.7% hecho visible: después de la proyección cada punto es un solo número, su lugar sobre la línea, y la imagen queda casi intacta. Reinicia y córrelo otra vez; es determinista, la misma rotación cada vez, porque aquí no hay semilla ni arranque aleatorio, solo álgebra lineal.
Ese es el PCA en dos dimensiones, donde bajar a una es un truco de fiesta. El valor aparece cuando la nube vive en cuatro dimensiones, o en cuatrocientas, y la misma jugada te compra una gráfica que de otro modo no podrías dibujar.
La implementación completa
El archivo entero, sin librería, de arriba abajo. Esto es exactamente lo que corrió la animación y de donde salen los números de iris de abajo:
"""Principal component analysis, built from scratch.
Unsupervised again — no labels, just coordinates. PCA finds the axes along
which the data varies most, so we can rotate onto those axes and keep only the
first few. Pure NumPy: center the data, form the covariance matrix, take its
eigendecomposition, sort by eigenvalue, project onto the top components, and
measure how much variance we kept.
Every function below appears in the chapter one step at a time (the
`# region:` markers are what the book's include directives pull in).
"""
import numpy as np
import pandas as pd
# region: center
def center(X):
"""Subtract the column means so the cloud sits at the origin.
PCA is about variance — directions of spread — and spread is measured
around the mean. Returns the centered data and the mean vector (kept so we
can undo the shift when we reconstruct).
"""
mean = X.mean(axis=0)
return X - mean, mean
# endregion
# region: covariance
def covariance(Xc):
"""The (D, D) covariance matrix of already-centered data.
Entry (a, b) is the covariance between feature a and feature b. The
diagonal is each feature's variance; off-diagonals say how features move
together. We divide by N-1 (the unbiased estimator), which is also what
numpy and sklearn use.
"""
n = len(Xc)
return (Xc.T @ Xc) / (n - 1)
# endregion
# region: eig
def eigendecompose(C):
"""Eigenvalues and eigenvectors of the covariance matrix, sorted.
C is symmetric, so `eigh` is the right tool — real eigenvalues, orthonormal
eigenvectors. `eigh` returns them in ascending order; we flip to descending
so component 0 is the direction of greatest variance. Eigenvectors are the
COLUMNS of the returned matrix, one principal direction each.
"""
eigvals, eigvecs = np.linalg.eigh(C)
order = np.argsort(eigvals)[::-1]
return eigvals[order], eigvecs[:, order]
# endregion
# region: explained_variance
def explained_variance_ratio(eigvals):
"""Fraction of total variance each component accounts for.
Each eigenvalue IS the variance along its eigenvector, so the ratio is just
the eigenvalue over their sum. These add to 1 across all components; the
cumulative sum tells you how much you keep by stopping at k.
"""
return eigvals / eigvals.sum()
# endregion
# region: project
def project(Xc, components, k):
"""Project centered data onto the top-k principal directions.
`components` holds eigenvectors as columns (from eigendecompose). Taking the
first k columns and multiplying gives the scores: each point's coordinates
in the new k-dimensional basis. This is the dimensionality reduction —
(N, D) in, (N, k) out.
"""
return Xc @ components[:, :k]
# endregion
# region: reconstruct
def reconstruct(scores, components, mean, k):
"""Map the k-dim scores back into the original feature space.
Undo the projection (multiply by the components' transpose) and add the
mean back. With k < D this is lossy: what comes back is the closest
rank-k approximation of the original point. The gap between the two is the
reconstruction error, and it equals the variance in the components we
dropped.
"""
return scores @ components[:, :k].T + mean
# endregion
# region: pca
def pca(X, k):
"""Full PCA: center, covariance, eigendecompose, project onto top-k.
Returns everything the chapter needs in one dict — the mean, the sorted
principal directions (columns), the eigenvalues (per-component variance),
the explained-variance ratio, and the k-dim scores. This is the whole
algorithm; every field comes from the small functions above.
"""
Xc, mean = center(X)
C = covariance(Xc)
eigvals, components = eigendecompose(C)
scores = project(Xc, components, k)
return {
"mean": mean,
"components": components, # (D, D), eigenvectors as columns
"eigenvalues": eigvals, # variance along each component
"explained_variance_ratio": explained_variance_ratio(eigvals),
"scores": scores, # (N, k) reduced coordinates
}
# endregion
def load_cloud(path="../data/cloud.csv"):
"""The 2-D toy cloud: a tilted, correlated Gaussian blob (x, y)."""
return pd.read_csv(path)
def load_iris_csv(path="../data/iris.csv"):
"""The real dataset: 150 iris flowers, 4 measurements + species label.
The species column is only used to color the 2-D projection at the end —
PCA itself never sees it. This is unsupervised.
"""
return pd.read_csv(path)
La versión de librería
Nadie implementa PCA a mano en producción, y una vez que lo construiste, ya no
te hace falta. El PCA de scikit-learn calcula lo mismo de una manera más
inteligente — toma la SVD de los datos centrados directamente en vez de formar
la matriz de covarianza y eigendescomponerla, lo cual es más estable
numéricamente y aterriza en la respuesta idéntica:
def sklearn_pca(X, k):
"""Fit PCA and return the pieces our scratch version returns.
`components_` are the principal directions as ROWS (note: sklearn stores
them transposed relative to our column layout). `explained_variance_ratio_`
is the per-component variance fraction, and `transform` gives the scores.
"""
model = PCA(n_components=k)
scores = model.fit_transform(X)
return {
"components": model.components_, # (k, D), as rows
"explained_variance_ratio": model.explained_variance_ratio_,
"scores": scores, # (N, k)
}
Dos diferencias de contabilidad que conviene saber antes de comparar. sklearn guarda los componentes como renglones, mientras que nuestra versión los mantiene como columnas, así que uno es la transpuesta del otro. Y la importante: un eigenvector solo está definido hasta el signo. Voltea a y sigue siendo un vector unitario apuntando a lo largo del mismo eje con el mismo eigenvalor — a la línea no le importa hacia dónde apunta la flecha. Así que el PC1 de sklearn puede salir apuntando en la dirección opuesta al nuestro, lo cual voltea el signo de cada score sobre ese componente, mientras que el eje, la varianza y la proporción de varianza explicada son todos idénticos. Cuando compares dos implementaciones de PCA y los componentes se vean negados, eso no es un bug, es la ambigüedad de signo, y la alineas antes de verificar.
Desde cero contra librería
Los mismos datos de iris, ambos reducidos a dos componentes — nuestra eigendescomposición de la covarianza contra la SVD de sklearn. Las barras son la proporción de varianza explicada de cada componente:
Son iguales hasta el dígito: PC1 en 0.9246, PC2 en 0.0531, de ambos lados. Eso no es redondeo escondiendo una brecha. Después de alinear el signo, el mayor desacuerdo entre nuestros componentes y los de sklearn es del orden de 1e-14, la varianza explicada coincide a precisión de máquina, y las coordenadas proyectadas se alinean a mejor que 1e-4 — las dos implementaciones están calculando lo mismo, una vía la eigendescomposición de la covarianza y otra vía SVD. La única diferencia visible fue el signo: nuestro PC1 salió volteado respecto al de sklearn, exactamente la ambigüedad de la sección pasada, que corregimos antes de que los números coincidan.
Ahora la recompensa. Aquí está iris proyectado sobre esos dos componentes, las cuatro mediciones colapsadas a un plano, cada flor coloreada por su especie verdadera:
El PCA nunca vio los colores. Recibió cuatro números por flor y devolvió dos, y aun así las tres especies caen en su lugar — setosa limpiamente aparte, por su cuenta; versicolor y virginica adyacentes pero en su mayoría separables. Los primeros dos componentes cargan el 97.77% de la varianza total (92.46% en PC1, 5.31% en PC2), así que esos dos ejes son casi todo el dataset; los dos que descartamos apenas guardan un 2%. Esa es la promesa cumplida: cuatro dimensiones a dos, una imagen que sí puedes mirar, y la estructura que importaba sobrevivió la reducción. El punto promedio se reconstruye a partir de sus dos scores con un error cuadrático de alrededor de 0.10 sobre cuatro features — ese 2% de varianza sobrante, y nada más.
El scree plot es la herramienta para elegir sin adivinar. Grafica la varianza de cada componente y el acumulado corriente, y busca dónde se aplana la curva:
Las barras se desploman después de PC1 y la línea acumulada ya está en 0.98 para PC2, y luego se arrastra hasta 1.0 a lo largo de PC3 y PC4. Ese aplanamiento es la señal: dos componentes bastan y sobran para iris, y el tercero y el cuarto son casi puros rendimientos decrecientes. En la práctica esta curva es tu presupuesto — elige la donde el acumulado cruce la varianza que estés dispuesto a conservar, 90%, 95%, 99%, y detente.
Conclusiones
El PCA es la primera herramienta que agarro cuando un dataset es ancho y necesito verlo, o cuando un modelo se está ahogando en features correlacionados. Es barato, es una sola eigendescomposición sin hiperparámetros que cuidar y sin semilla con la que tener mala suerte, y hace cuatro trabajos a la vez: comprime, dibuja datos de alta dimensión sobre un plano, le entrega a un modelo subsecuente entradas no correlacionadas, y puede recortar ruido descartando las direcciones de baja varianza. Para visualización y para decorrelación antes de un modelo lineal, es casi una victoria gratis, y el scree plot te dice cuántos componentes conservar en vez de dejarlo al gusto.
La palabra que hay que tener en mente es lineal. El PCA rota y proyecta, y eso es todo lo que hace. Lee el mundo a través de una matriz de covarianza, así que ve varianza y correlación por pares y nada más — ni curvas, ni manifolds, ni noción de que tus datos podrían vivir sobre una hoja doblada en vez de una plana. Cuando la estructura es no lineal, el PCA la aplana y reporta alta varianza explicada mientras destruye calladamente lo que te importaba. También trata la varianza como importancia, lo cual es un default útil y una trampa ocasional: la dirección más ruidosa no siempre es la informativa. Esa es exactamente la frontera que los siguientes métodos empujan. t-SNE y UMAP conservan la meta — pocas dimensiones que puedas graficar — pero doblan el mapa para preservar vecindarios locales en vez de varianza global, así que una espiral sigue siendo una espiral. Kernel PCA hace el truco de la covarianza en un espacio de features no lineal. Los autoencoders aprenden el pipeline completo de reducir-y-reconstruir como una red neuronal, sin nada de la restricción de línea recta. Cada uno de ellos es un PCA relajado, que es la mejor razón posible para haber construido primero el lineal a mano.