Regresión Lineal y Logística
Derivación matemática completa desde función de costo hasta implementación distribuida en Spark ML.
01. Regresión Lineal: Derivación Completa
Función de Costo (Mean Squared Error)
Modelo:
$$
\hat{y}_i = \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \cdots + \beta_p x_{ip} = \beta^T \mathbf{x}_i
$$
En notación matricial: \(\hat{\mathbf{y}} = X\boldsymbol{\beta}\), donde \(X \in \mathbb{R}^{n \times p}\), \(\boldsymbol{\beta} \in \mathbb{R}^p\)
Función de costo (minimizar):
$$
\begin{aligned}
J(\boldsymbol{\beta}) &= \frac{1}{2n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 \\[0.5em]
&= \frac{1}{2n} \sum_{i=1}^{n} (y_i - \boldsymbol{\beta}^T \mathbf{x}_i)^2 \\[0.5em]
&= \frac{1}{2n} ||X\boldsymbol{\beta} - \mathbf{y}||^2
\end{aligned}
$$
El factor \(\frac{1}{2}\) simplifica la derivada. El \(\frac{1}{n}\) normaliza por cantidad de datos.
Gradiente (derivar respecto a β):
$$
\begin{aligned}
\nabla J(\boldsymbol{\beta}) &= \frac{\partial J}{\partial \boldsymbol{\beta}} \\[0.5em]
&= \frac{1}{n} X^T (X\boldsymbol{\beta} - \mathbf{y}) \\[0.5em]
&= \frac{1}{n} \sum_{i=1}^{n} ((\boldsymbol{\beta}^T \mathbf{x}_i) - y_i) \mathbf{x}_i
\end{aligned}
$$
Este gradiente se puede calcular distribuido: cada partición computa su suma parcial, luego se agregan.
Solución analítica (Normal Equations):
$$
\begin{aligned}
\nabla J(\boldsymbol{\beta}) &= 0 \\[0.5em]
X^T X \boldsymbol{\beta} &= X^T \mathbf{y} \\[0.5em]
\hat{\boldsymbol{\beta}} &= (X^T X)^{-1} X^T \mathbf{y}
\end{aligned}
$$
Problema: Requiere invertir \(X^T X\) (matriz \(p \times p\)).
Si \(p\) es grande o \(X^T X\) es singular, falla numéricamente.
Solver: Normal Equations
from pyspark.ml.regression import LinearRegression
lr = LinearRegression(
featuresCol="features",
labelCol="label",
solver="normal" # Usa (X^T X)^{-1} X^T y
)
model = lr.fit(train_df)
# Coeficientes aprendidos
print(f"β: {model.coefficients}")
print(f"β₀: {model.intercept}")
✅ Mejor si: p < 4096, datos caben en memoria, X^T X invertible
Solver: L-BFGS (Iterativo)
from pyspark.ml.regression import LinearRegression
lr = LinearRegression(
featuresCol="features",
labelCol="label",
solver="l-bfgs", # Quasi-Newton (default)
maxIter=100,
tol=1e-6
)
model = lr.fit(train_df)
# Converge iterativamente usando ∇J
✅ Mejor si: p > 4096, Big Data, necesitas regularización
02. Interpretación Geométrica
Proyección Ortogonal
La regresión lineal encuentra la proyección de \(\mathbf{y}\) sobre el espacio columna de \(X\).
$$
\begin{aligned}
\text{Col}(X) &= \{\mathbf{v} : \mathbf{v} = X\boldsymbol{\beta} \text{ para algún } \boldsymbol{\beta}\} \\[0.5em]
\hat{\mathbf{y}} &= \text{Proj}_{\text{Col}(X)}(\mathbf{y}) \\[0.5em]
&= X\hat{\boldsymbol{\beta}}
\end{aligned}
$$
Residuales:
$$
\mathbf{e} = \mathbf{y} - \hat{\mathbf{y}}
$$
Son perpendiculares al espacio Col(X):
$$
X^T \mathbf{e} = X^T (\mathbf{y} - X\hat{\boldsymbol{\beta}}) = \mathbf{0}
$$
y (vector real)
↑
|\\
| \\ e (residual)
| \\
| \\
| ŷ (proyección)
| /
| /
| /
|/________________→ Col(X)
Intuición: ŷ es el punto en Col(X) más cercano a y.
El error e es mínimo cuando e ⊥ Col(X).
03. Regresión Logística: De Probabilidades a Log-Odds
Derivación Paso a Paso
Paso 1: Modelo de Probabilidad
$$
\begin{aligned}
P(y=1|\mathbf{x}) &= \sigma(\boldsymbol{\beta}^T \mathbf{x}) \\[0.5em]
\text{donde } \sigma(z) &= \frac{1}{1 + e^{-z}} \quad \text{(función sigmoide)}
\end{aligned}
$$
La sigmoide mapea \((-\infty, +\infty) \to (0, 1)\), ideal para probabilidades.
Paso 2: Log-Likelihood
$$
\begin{aligned}
\mathcal{L}(\boldsymbol{\beta}) &= \prod_{i=1}^{n} P(y_i|\mathbf{x}_i) \\[0.5em]
&= \prod_{i=1}^{n} [\sigma(\boldsymbol{\beta}^T \mathbf{x}_i)]^{y_i} [1-\sigma(\boldsymbol{\beta}^T \mathbf{x}_i)]^{1-y_i} \\[0.5em]
\ell(\boldsymbol{\beta}) &= \log \mathcal{L}(\boldsymbol{\beta}) \\[0.5em]
&= \sum_{i=1}^{n} [y_i \log p_i + (1-y_i) \log (1-p_i)]
\end{aligned}
$$
Donde \(p_i = \sigma(\boldsymbol{\beta}^T \mathbf{x}_i)\)
Paso 3: Función de Costo (Cross-Entropy)
$$
\begin{aligned}
J(\boldsymbol{\beta}) &= -\frac{1}{n} \ell(\boldsymbol{\beta}) \\[0.5em]
&= -\frac{1}{n} \sum_{i=1}^{n} [y_i \log(\sigma(\boldsymbol{\beta}^T \mathbf{x}_i)) + (1-y_i) \log(1-\sigma(\boldsymbol{\beta}^T \mathbf{x}_i))]
\end{aligned}
$$
Minimizar J(β) ≡ Maximizar likelihood
Paso 4: Gradiente
$$
\begin{aligned}
\nabla J(\boldsymbol{\beta}) &= \frac{1}{n} X^T (\boldsymbol{\sigma} - \mathbf{y}) \\[0.5em]
\text{donde } \boldsymbol{\sigma} &= [\sigma(\boldsymbol{\beta}^T \mathbf{x}_1), \ldots, \sigma(\boldsymbol{\beta}^T \mathbf{x}_n)]^T
\end{aligned}
$$
Sorprendentemente similar al gradiente de regresión lineal! Solo cambia \(X\boldsymbol{\beta}\) por \(\boldsymbol{\sigma}\).
Código: Regresión Logística en PySpark
from pyspark.ml.classification import LogisticRegression
# Clasificación binaria
lr_log = LogisticRegression(
featuresCol="features",
labelCol="label", # 0 o 1
maxIter=100,
regParam=0.01, # Regularización L2 (Ridge)
elasticNetParam=0.0 # 0=Ridge, 1=Lasso
)
model = lr_log.fit(train_df)
# Predecir probabilidades
predictions = model.transform(test_df)
predictions.select("features", "label", "probability", "prediction").show(5)
# +--------+-----+--------------------+----------+
# |features|label|probability |prediction|
# +--------+-----+--------------------+----------+
# |[2.0,..]|1 |[0.15, 0.85] |1.0 | # P(y=1) = 0.85
# |[1.5,..]|0 |[0.92, 0.08] |0.0 | # P(y=1) = 0.08
# +--------+-----+--------------------+----------+
04. Métricas de Evaluación
Regresión Lineal
RMSE (Root Mean Squared Error):
$$
\text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2}
$$
Unidades: mismas que y. Penaliza errores grandes.
R² (Coeficiente de Determinación):
$$
R^2 = 1 - \frac{SS_{\text{res}}}{SS_{\text{tot}}} = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2}
$$
Rango: [0, 1]. R²=1 → ajuste perfecto. R²=0 → modelo no mejor que media.
MAE (Mean Absolute Error):
$$
\text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i|
$$
Más robusto a outliers que RMSE.
from pyspark.ml.evaluation import RegressionEvaluator
evaluator = RegressionEvaluator(
labelCol="label",
predictionCol="prediction"
)
rmse = evaluator.evaluate(
predictions,
{evaluator.metricName: "rmse"}
)
r2 = evaluator.evaluate(
predictions,
{evaluator.metricName: "r2"}
)
print(f"RMSE: {rmse:.2f}")
print(f"R²: {r2:.4f}")
Regresión Logística
Matriz de Confusión
| Predicción | ||
|---|---|---|
| Neg (0) | Pos (1) | |
| Real Neg (0) | TN | FP |
| Real Pos (1) | FN | TP |
$$
\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}
$$
$$
\text{Precision} = \frac{TP}{TP + FP}
$$
$$
\text{Recall (Sensitivity)} = \frac{TP}{TP + FN}
$$
$$
\text{F1-Score} = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}
$$
from pyspark.ml.evaluation import BinaryClassificationEvaluator
evaluator = BinaryClassificationEvaluator(
labelCol="label",
rawPredictionCol="rawPrediction"
)
auc = evaluator.evaluate(
predictions,
{evaluator.metricName: "areaUnderROC"}
)
print(f"AUC-ROC: {auc:.4f}")
05. Solvers en Spark: Cuándo Usar Cada Uno
| Solver | Método | Complejidad | Mejor para | Limitaciones |
|---|---|---|---|---|
| normal | (X^T X)^{-1} X^T y | O(np² + p³) | p < 4096, datos pequeños | Falla si X^T X singular, no regularización |
| l-bfgs | Quasi-Newton | O(np × iter) | p > 4096, Big Data, regularización | Requiere tuning de maxIter, tol |
| auto | Elige automático | Depende | No sabes qué usar | Menos control |
💡 Recomendación: Usa
solver='auto' por defecto.
Spark elige 'normal' si p < 4096 y sin regularización, caso contrario 'l-bfgs'.
06. Ejemplo Completo: Predicción de Salarios
Pipeline End-to-End
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler, StandardScaler
from pyspark.ml.regression import LinearRegression
from pyspark.ml.evaluation import RegressionEvaluator
# Dataset: salario ~ edad + educacion_años + experiencia
df = spark.read.csv("salarios.csv", header=True, inferSchema=True)
# Split
train_df, test_df = df.randomSplit([0.8, 0.2], seed=42)
# Pipeline
assembler = VectorAssembler(
inputCols=["edad", "educacion_años", "experiencia"],
outputCol="features_raw"
)
scaler = StandardScaler(
inputCol="features_raw",
outputCol="features",
withMean=True,
withStd=True
)
lr = LinearRegression(
featuresCol="features",
labelCol="salario",
maxIter=100
)
pipeline = Pipeline(stages=[assembler, scaler, lr])
# Entrenar
model = pipeline.fit(train_df)
# Predecir
predictions = model.transform(test_df)
# Evaluar
evaluator = RegressionEvaluator(labelCol="salario", predictionCol="prediction")
rmse = evaluator.evaluate(predictions, {evaluator.metricName: "rmse"})
r2 = evaluator.evaluate(predictions, {evaluator.metricName: "r2"})
print(f"RMSE: ${rmse:.2f}")
print(f"R²: {r2:.4f}")
# Coeficientes
lr_model = model.stages[2] # LinearRegression es la 3ra etapa
print(f"β (edad, educacion, experiencia): {lr_model.coefficients}")
print(f"β₀ (intercept): {lr_model.intercept:.2f}")
# Interpretación: Por cada año adicional de educación, el salario aumenta β₁ (todo lo demás constante)
Resultado ejemplo:
RMSE: $5,234.12
R²: 0.7856
β: [850.23, 3200.45, 1100.67] → [edad, educacion, experiencia]
Interpretación: Cada año adicional de educación incrementa el salario en ~$3,200 (manteniendo edad y experiencia constantes).
RMSE: $5,234.12
R²: 0.7856
β: [850.23, 3200.45, 1100.67] → [edad, educacion, experiencia]
Interpretación: Cada año adicional de educación incrementa el salario en ~$3,200 (manteniendo edad y experiencia constantes).