feat(eda): project_clusters_2d + describe_clusters_llm para el capitulo MODELOS
project_clusters_2d (pura): PCA(2)+KMeans sobre el MISMO subset estandarizado, devolviendo proyeccion 2D y labels alineados por fila + centroides en espacio PCA + perfiles de cluster desestandarizados. Es la pieza que garantiza la alineacion points<->labels que pca_explained y kmeans_segments no cubren (estandarizan por separado y kmeans descarta los labels). Habilita el scatter PCA coloreado por cluster (MUST-8.1). describe_clusters_llm (impura): micro-analisis LLM de los clusters en una sola llamada a ask_llm (grupo claude-direct), devuelve titulo + descripcion por cluster con degradacion dict-no-throw a titulos genericos si el LLM no responde (MUST-8.2). Ambas re-exportadas en datascience/__init__.py. Tests: 6/6 y 9/9 (sin red). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: describe_clusters_llm
|
||||
kind: function
|
||||
lang: py
|
||||
domain: datascience
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def describe_clusters_llm(cluster_profiles: list, feature_names: list, model: str = \"claude-haiku-4-5-20251001\") -> dict"
|
||||
description: "Micro-analisis LLM de clusters de KMeans (grupo eda). Toma los perfiles AGREGADOS de cada cluster (los que produce project_clusters_2d: tamano, centroide en escala original, features distintivas y centroide en z-score) y, con UNA sola llamada al LLM, pide por cada cluster un TITULO corto + una descripcion de 1-2 frases en espanol. Clave de coste/privacidad: NO envia filas crudas, solo el resumen agregado de cada grupo (tamano, % del total y la media de las features distintivas con su signo respecto a la media global). Reusa ask_llm del grupo claude-direct (API directa con token OAuth de Claude). Impura, dict-no-throw: nunca lanza, degrada a titulos genericos 'Cluster N' si el LLM no responde o el parseo falla."
|
||||
tags: [eda, clustering, llm, claude-direct, datascience, kmeans]
|
||||
params:
|
||||
- name: cluster_profiles
|
||||
desc: "Lista de perfiles de cluster con la forma que produce project_clusters_2d: cada uno {cluster:int, size:int, pct:float, centroid_original:{feature: media en escala original}, distinctive:[features distintivas], centroid_z:{feature: z-score}}. Solo se le envia al LLM un resumen agregado; nunca filas crudas. Lista vacia o no-lista -> clusters=[] sin llamar al LLM."
|
||||
- name: feature_names
|
||||
desc: "Nombres de las features del dataset. Se incluyen como contexto en el prompt para que el LLM pueda nombrar los clusters; no es obligatorio que coincida con las features distintivas de cada perfil."
|
||||
- name: model
|
||||
desc: "id del modelo Anthropic a usar. Default 'claude-haiku-4-5-20251001' (haiku, coste bajo, ~2-3s). Para titulos/descripciones mas finas, pasar p.ej. 'claude-opus-4-8'."
|
||||
output: "dict dict-no-throw: {clusters:[{cluster:int, title:str, description:str}], model:str, note:str}. note=='' si todo fue bien. Si el LLM no respondio (note='LLM no disponible') o el parseo fallo (note='parse fallido'), clusters trae titulos genericos 'Cluster N' con description vacia. Si cluster_profiles esta vacio o no es lista: {clusters:[], model, note:'sin clusters'}. NUNCA lanza."
|
||||
uses_functions: [ask_llm_py_core]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["test_parse_clusters_json_valid_array", "test_parse_clusters_json_wrapped_in_junk_text", "test_parse_clusters_json_non_json_returns_none", "test_parse_clusters_json_fills_missing_cluster_by_index", "test_describe_clusters_llm_ok_with_monkeypatched_llm", "test_describe_clusters_llm_degrades_on_empty_response", "test_describe_clusters_llm_degrades_on_unparseable_response", "test_describe_clusters_llm_empty_list_skips_llm", "test_describe_clusters_llm_non_list_input_skips_llm"]
|
||||
test_file_path: "python/functions/datascience/describe_clusters_llm_test.py"
|
||||
file_path: "python/functions/datascience/describe_clusters_llm.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
|
||||
from datascience.describe_clusters_llm import describe_clusters_llm
|
||||
|
||||
# Perfiles agregados producidos por project_clusters_2d (no hay filas crudas).
|
||||
cluster_profiles = [
|
||||
{
|
||||
"cluster": 0, "size": 60, "pct": 60.0,
|
||||
"centroid_original": {"acidez": 8.5, "alcohol": 9.2},
|
||||
"distinctive": ["acidez", "alcohol"],
|
||||
"centroid_z": {"acidez": 1.4, "alcohol": -0.9},
|
||||
},
|
||||
{
|
||||
"cluster": 1, "size": 40, "pct": 40.0,
|
||||
"centroid_original": {"acidez": 5.1, "alcohol": 13.0},
|
||||
"distinctive": ["alcohol"],
|
||||
"centroid_z": {"acidez": -0.7, "alcohol": 1.6},
|
||||
},
|
||||
]
|
||||
feature_names = ["acidez", "alcohol", "azucar"]
|
||||
|
||||
out = describe_clusters_llm(cluster_profiles, feature_names) # haiku por defecto
|
||||
# out = describe_clusters_llm(cluster_profiles, feature_names, model="claude-opus-4-8")
|
||||
|
||||
if not out["note"]:
|
||||
for c in out["clusters"]:
|
||||
print(f"Cluster {c['cluster']}: {c['title']}")
|
||||
print(" ", c["description"])
|
||||
else:
|
||||
# Degradacion: titulos genericos "Cluster N".
|
||||
print("LLM no usado:", out["note"])
|
||||
for c in out["clusters"]:
|
||||
print(c["cluster"], c["title"])
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando ya has clusterizado un dataset (KMeans + `project_clusters_2d`) y quieres
|
||||
poner NOMBRE y descripcion legible a cada grupo en vez de dejar "Cluster 0/1/2".
|
||||
Es el paso interpretativo que sigue al perfilado de clusters: `project_clusters_2d`
|
||||
calcula tamano, centroides y features distintivas, y `describe_clusters_llm` los
|
||||
traduce a un titulo corto + 1-2 frases por cluster. Usala al cerrar un EDA con
|
||||
segmentacion para el resumen final o el report. Una sola llamada al LLM describe
|
||||
todos los clusters a la vez (barato).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura: hace 1 llamada de red al LLM.** No es determinista ni gratis. Latencia
|
||||
tipica ~2-3s con haiku.
|
||||
- **Requiere token OAuth de Claude** en `~/.claude/.credentials.json` (via `ask_llm`
|
||||
/ grupo `claude-direct`). Sin token / sin red, NO lanza: degrada a titulos
|
||||
genericos `Cluster N` con `note="LLM no disponible"`.
|
||||
- **NO envia filas crudas al LLM**, solo el resumen AGREGADO de cada cluster
|
||||
(tamano, % del total y la media de las features distintivas con su signo respecto
|
||||
a la media global). Privacidad y coste minimos por diseno — pero requiere que los
|
||||
perfiles vengan ya calculados por `project_clusters_2d`.
|
||||
- **Modelo `haiku` por defecto** para coste bajo; sube a `claude-opus-4-8` si
|
||||
necesitas titulos/descripciones mas finas (mas caro y lento).
|
||||
- **dict-no-throw**: si el modelo no devuelve un JSON array parseable, retorna
|
||||
titulos genericos con `note="parse fallido"`. Comprueba siempre `out["note"]`
|
||||
antes de fiarte de los titulos.
|
||||
- El LLM puede sobre-interpretar: el system prompt le pide ser sobrio y no inventar
|
||||
causas, pero revisa los titulos antes de publicarlos en un report.
|
||||
Reference in New Issue
Block a user