--- name: project_clusters_2d kind: function lang: py domain: datascience version: "1.0.0" purity: pure signature: "def project_clusters_2d(columns: dict, k_min: int = 2, k_max: int = 8, max_points: int = 2000) -> dict" description: "PCA a 2D + KMeans sobre el MISMO subset numerico estandarizado, devolviendo proyeccion 2D y labels de cluster ALINEADOS por fila para pintar un scatter PCA coloreado por cluster. Estandariza una sola vez, elige k por silhouette y proyecta centroides al espacio PCA. Determinista." tags: [eda, models, clustering, pca, kmeans, scatter, dimensionality-reduction, datascience, sklearn] params: - name: columns desc: "Mapa {nombre_columna: [valores numericos]}. Listas alineadas por fila (misma longitud). Columnas no numericas o con <2 valores distintos se descartan; None/NaN descartan la fila completa (listwise)." - name: k_min desc: "Numero minimo de clusters a probar por silhouette (default 2). El minimo de filas validas requerido es max(3, k_min*2)." - name: k_max desc: "Numero maximo de clusters a probar (default 8). Se acota a min(k_max, n_filas_validas-1)." - name: max_points desc: "Tope de puntos devueltos en points/labels (default 2000). Si n_used lo supera, points y labels se submuestrean CONJUNTAMENTE con paso determinista para seguir alineados; el fit usa siempre todas las filas." output: "dict con points (proyeccion 2D, posiblemente submuestreada a max_points), labels (cluster de cada point, alineado con points), centers_2d (centroides en espacio PCA, len==best_k), best_k, silhouette, explained_2d ([var PC1, var PC2]), cluster_sizes (sobre n_used total), cluster_profiles (lista de {cluster, size, pct, centroid_original, distinctive top-3 por |z|, centroid_z}), feature_names, n_used (filas del fit antes de muestreo) y note (\"\" si ok). Con <2 columnas numericas o max_points` (paso determinista `[::ceil(n_used/max_points)]`); `n_used`, `centers_2d`, `cluster_sizes` y `cluster_profiles` se calculan SIEMPRE sobre todas las filas. Cuando hay submuestreo, `note` lo indica. - `centroid_z` y `distinctive` estan en z-score (espacio escalado); `centroid_original` esta en las unidades originales (via `scaler.inverse_transform`). No mezcles ambos al interpretar. - `centers_2d` esta en el espacio PCA (coordenadas del scatter), no en unidades originales: pintalo sobre el mismo eje que `points`. - Silhouette baja con best_k alto sugiere que no hay estructura de cluster real; el scatter puede no mostrar grupos separados. ## Notas Pieza de composicion que `pca_explained` + `kmeans_segments` no cubren: ambas estandarizan internamente por separado (cada una su propio `StandardScaler`) y `kmeans_segments` no expone los labels por fila, por lo que no se pueden cruzar con la `projection` de `pca_explained`. Esta funcion usa `sklearn` directo (StandardScaler una sola vez compartido por PCA y KMeans) para garantizar la alineacion `points[i] <-> labels[i]` y proyectar los centroides KMeans al espacio PCA. Coercion y listwise deletion siguen el estilo de `pca_explained` (None/NaN -> fila descartada, columnas no parseables o constantes descartadas). Degrada con gracia: con <2 columnas numericas o