feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
@@ -0,0 +1,121 @@
"""PCA rapido sobre columnas numericas para revelar estructura latente.
Estandariza las columnas (z-score), descarta filas con valores faltantes y
ajusta un PCA determinista para ver cuanta varianza concentran pocos
componentes. Pensado para exploracion de datos (EDA) barata.
"""
import math
def pca_explained(columns: dict, n_components: int = 2) -> dict:
"""Ejecuta PCA sobre columnas numericas y resume la varianza explicada.
Args:
columns: mapa {nombre_columna: [valores numericos]}. Las listas estan
alineadas por fila (misma longitud). Las columnas no numericas o
con menos de dos valores distintos se descartan.
n_components: numero maximo de componentes principales a calcular.
Se acota a min(n_features, n_filas_validas).
Returns:
dict con:
n_components: numero de componentes realmente calculados.
n_rows_used: filas validas usadas (sin None/NaN).
n_features: columnas numericas usadas.
explained_variance_ratio: varianza explicada por componente.
cumulative: varianza acumulada componente a componente.
top_loadings: cargas mas grandes (en valor absoluto) por componente.
projection: proyeccion de las filas (cap a 1000 filas).
Si hay menos de 2 columnas numericas o menos de 3 filas validas,
devuelve {n_components: 0, explained_variance_ratio: [],
note: "datos insuficientes"} sin lanzar excepcion.
"""
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
insufficient = {
"n_components": 0,
"explained_variance_ratio": [],
"note": "datos insuficientes",
}
if not isinstance(columns, dict) or not columns:
return insufficient
# Quedarnos solo con columnas que se puedan interpretar como numericas.
numeric_cols: dict[str, list] = {}
for name, values in columns.items():
if not isinstance(values, (list, tuple)):
continue
coerced = []
usable = True
for v in values:
if v is None:
coerced.append(math.nan)
continue
try:
f = float(v)
except (TypeError, ValueError):
usable = False
break
coerced.append(f)
if usable:
numeric_cols[name] = coerced
if len(numeric_cols) < 2:
return insufficient
feature_names = list(numeric_cols.keys())
matrix = np.array([numeric_cols[n] for n in feature_names], dtype=float).T
# Descartar filas con cualquier NaN (incluye los None convertidos).
valid_mask = ~np.isnan(matrix).any(axis=1)
data = matrix[valid_mask]
if data.shape[0] < 3:
return insufficient
n_rows_used = int(data.shape[0])
n_features = int(data.shape[1])
k = min(n_components, n_features, n_rows_used)
if k < 1:
return insufficient
scaled = StandardScaler().fit_transform(data)
pca = PCA(n_components=k, random_state=0)
proj = pca.fit_transform(scaled)
evr = [float(x) for x in pca.explained_variance_ratio_]
cumulative = []
running = 0.0
for x in evr:
running += x
cumulative.append(float(running))
# Cargas: una fila por componente, una columna por feature.
top_loadings = []
for comp_idx, comp in enumerate(pca.components_):
order = np.argsort(np.abs(comp))[::-1]
for feat_idx in order:
top_loadings.append(
{
"component": int(comp_idx),
"feature": feature_names[int(feat_idx)],
"loading": float(comp[int(feat_idx)]),
}
)
projection = [[float(v) for v in row] for row in proj[:1000]]
return {
"n_components": int(k),
"n_rows_used": n_rows_used,
"n_features": n_features,
"explained_variance_ratio": evr,
"cumulative": cumulative,
"top_loadings": top_loadings,
"projection": projection,
}