--- 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.