Workflow de exploración de datos

Cómo manejamos queries de exploración vs. scripts reusables en el proyecto trivasa-bi-dev, para mantener el directorio de trabajo limpio y el proceso reproducible — más la convención de formato de salida en terminal que comparten los scripts de validación.

Empaquetado como skill (2026-09-02): trivasa-sql-exploracion, en el repo claude-skills — cubre las secciones 1-6 de este documento (puntual vs. reusable, conexión por default, heredoc, checklist) más la convención de nombres NN_slug.py para lo que sí se guarda, que esta página nunca llegó a documentar. Esta página queda como referencia histórica de origen — para explorar o decidir cómo nombrar un script nuevo, usar el skill, no este documento. (La sección 7, formato de salida, ya apuntaba a su propio skill, rich-output-checks, desde 2026-08-10 — sin cambios.)

1. Regla general: exploración puntual vs. reusable

TipoEjemploDónde vive
Puntual (curiosidad, validación, responder una pregunta de negocio)“¿Qué productos tienen IVA variable?”No se guarda como archivo — se corre vía heredoc a stdin
Reusable (se va a volver a correr, alimenta un pipeline)Carga de Reorden a Postgres, modelo dbtSí se guarda, en su carpeta correspondiente — nunca en el root de trivasa-bi-dev/

Regla de oro: si dudas si un script se va a volver a usar, probablemente no — no lo guardes.

2. Patrón de conexión

Antes de conectar, confirmar en Conexiones a TRIVASADB qué servidor corresponde — hay 3 copias con distinto rol/frescura y usar la equivocada da datos truncados o desfasados sin error visible.

Corrección 2026-08-05: una versión anterior de esta página decía que connection.py era el entorno de prueba .204 por default. Ya no es así: connection.py apunta a .200/TRIVASADB, la copia que Conexiones a TRIVASADB marca como desactualizada y “nunca usar”. Actualmente no existe un connection_204.py dedicado.

Decisión: connection_200_trivasadb3.py (.200/TRIVASADB3) es el entorno de pruebas recomendado por default para cualquier exploración nueva — vivo pero con ~1 mes de atraso respecto a producción. connection_207.py (.207, producción) solo se usa cuando se pide explícitamente revisar o validar contra el entorno de producción — no como punto de partida.

Los tres módulos exponen el mismo helper:

from connection_200_trivasadb3 import show, engine  # default — .200/TRIVASADB3, ~1 mes atrasado
# from connection_207 import show, engine  # solo si se pide validar contra producción

show(sql) ya imprime el resultado como markdown y regresa el DataFrame — no envolver en print() (causa doble output).

3. Patrón heredoc para exploraciones puntuales

En vez de crear un archivo .py, correr el bloque completo directo por stdin:

python3 << 'EOF'
from connection_200_trivasadb3 import show, engine
import pandas as pd
 
query = """
SELECT TOP 20 *
FROM Producto
WHERE Es_Cve_Estado = 'ACTI'
"""
 
df = pd.read_sql(query, engine)
print(df)
EOF

Esto corre igual que un script, pero no deja ningún archivo en el directorio.

⚠️ Importante — no pegar bloques for/if línea por línea en el REPL interactivo (>>>). Una línea en blanco dentro de un bloque indentado hace que el intérprete cierre el bloque antes de tiempo, causando IndentationError en cada línea siguiente. Los bloques con lógica (loops, condicionales) siempre van vía heredoc, nunca pegados directo en >>>.

4. Patrón rich para salida con formato (exploración puntual)

Cuando el resultado incluye texto largo (ej. Pr_Descripcion) que se trunca feo en show(), usar rich para tablas legibles en terminal:

from rich.console import Console
from rich.table import Table
 
console = Console()
table = Table(title="Título descriptivo", show_lines=False)
 
table.add_column("Columna A", style="cyan", no_wrap=True)
table.add_column("Descripción", style="white", max_width=45)
# ... más columnas
 
for _, row in df.iterrows():
    table.add_row(str(row["col_a"]), str(row["descripcion"]))
 
console.print(table)

Para resultados que abarcan varios grupos (ej. por año), separar en una tabla rich por grupo:

for valor in sorted(df["columna_grupo"].unique()):
    df_grupo = df[df["columna_grupo"] == valor]
    table = Table(title=f"Título — {valor}")
    # ...
    console.print(table)
    console.print()

Este patrón inline sirve para exploración puntual. Para scripts reusables de validación que se van a volver a correr, usar el helper ya armado en vez de repetir este boilerplate cada vez — ver sección 7.

5. Organización del directorio

Vive en el root de trivasa-bi-dev/:

  • connection.py (⚠️ apunta a la copia desactualizada .200/TRIVASADB, ver sección 2), connection_207.py, connection_200_trivasadb3.py
  • trivasadb_context.md (contexto de schema curado)
  • Carpetas de proyecto por reporte (ej. layout_gastos/) directamente en el root, no dentro de un exploracion/ intermedio
  • stack/: scaffolding de servicios BI explorados pero no adoptados en producción (trivasa_dbt/, postgres/, superset/, lightdash/, trivasa-evidence/) — ver Stack de BI explorado, no adoptado
  • logs/

Va a scratch/ (gitignored):

  • Scripts de exploración que por alguna razón sí se guardaron
  • Nunca se versionan ni se documentan individualmente

6. Checklist antes de cualquier exploración nueva

  1. Confirmar que la tabla existe en el contexto documentado. trivasadb_context.md cubre ~50 tablas grandes + dominios de negocio principales, pero hay ~1600 tablas en total en TRIVASADB (muchas vacías, módulos no usados por Trivasa). Si la tabla no aparece en el doc, no asumir su estructura — ir directo a INFORMATION_SCHEMA.COLUMNS.
  2. Validar columnas reales antes de escribir el join, incluso si la tabla sí está documentada. El .md lista columnas representativas (“Cols: …”), no necesariamente todas las columnas de la tabla. Ejemplo real: PKs compuestas con columnas repetidas en el doc (ej. Cxc_Folio, Cxc_Folio, Pc_ID, Pc_ID, Im_Cve_Impuesto) probablemente reflejan cómo se generó el documento automáticamente — no tomarlas como la PK real sin confirmar.
  3. Para tablas grandes (top 50 del doc), evitar envolver columnas de fecha en funciones (YEAR(col)) dentro del WHERE. Usar rango (col >= 'YYYY-01-01' AND col < 'YYYY+1-01-01') para permitir uso de índice. Para tablas pequeñas no es crítico.
  4. Probar el join en pequeño antes de agregar. Si el query incluye GROUP BY sobre una tabla grande, correr primero un SELECT TOP 20 del join sin agregar, para confirmar que no hay explosión de filas por una PK compuesta mal entendida.
  5. Confirmar el servidor correcto primero (sección 2). Nunca correr una exploración nueva contra .200/TRIVASADB (desactualizada). Empezar en connection_200_trivasadb3 (default) y pasar a connection_207 (producción) solo si se pide explícitamente validar contra producción.

7. Convención de formato de salida en scripts (helpers_output.py)

Estandariza cómo los scripts de validación reusables muestran resultados en terminal, para que sean legibles de forma consistente sin que cada script reinvente su propio formato de tabla. Nació en layout_gastos/ (segunda exploración, 2026-08-04) pero el patrón aplica a cualquier script de validación del repo — copia literal del helper en sources/trivasa/layout-gastos/helpers_output.py.

Empaquetado como skill (2026-08-10): rich-output-checks, en el repo claude-skills — el mismo helpers_output.py, generalizado sin dependencias de layout_gastos/, disponible en ~/.claude/skills/rich-output-checks/ en cualquier host onboardeado a ese repo (ctunlinux incluido). La copia de sources/ de este repo queda como referencia histórica del origen; el skill es la fuente viva para usarlo en un script nuevo.

Ancho de consola: auto-detectado, no fijo

rich.Console() se usa sin ancho fijo — auto-detecta el ancho real de la terminal del usuario (confirmado 138 columnas en surface-wsl). Se probaron y descartaron 3 alternativas: Console(width=160) fijo rompe en bloques verticales ilegibles con tablas de 8+ columnas si el ancho no coincide con la terminal real; pandas.to_string() plano no aporta ventaja sobre rich; una vista vertical (1 fila = 1 bloque campo/valor) solo sirve para drill-down de un folio individual, no para tablas de comparación.

from helpers_output import console, mostrar_tabla, resumen
 
resumen(folios=1425, cuadran=1425, pct="100.0%")
mostrar_tabla(df, titulo="Resumen por ORIGEN")

mostrar_tabla() trunca a 30 filas por default (max_filas=) y avisa si el DataFrame tiene más, para no saturar la pantalla con tablas de cientos de filas.

Salida hacia Claude (aparte de la convención de terminal): para pegar resultados en la conversación con Claude, preferir CSV plano (df.to_csv(index=False)) sobre las tablas rich con bordes — más compacto en tokens, sin ambigüedad de columnas truncadas. Las tablas rich son para legibilidad humana en pantalla; el CSV es lo que se pega al LLM cuando se necesita que procese el resultado.

Separación de stream: tablas humanas a stderr, CSV a stdout

Problema encontrado al usar python3 script.py | cb (pipe al portapapeles para pegar en la conversación con Claude): capturaba todo stdout, mezclando las tablas rich (pensadas para lectura humana) con los bloques CSV destinados a Claude — obligaba a copiar a mano solo la parte CSV cada vez.

Solución: las tablas rich van a stderr (console_err, mostrar_tabla_err, resumen_err); los bloques CSV_PARA_CLAUDE van a stdout vía print() normal, prefijados con print("--- CSV_PARA_CLAUDE: <nombre> ---"). Ambos streams se ven igual en pantalla (comparten la tty), pero | cb solo engancha stdout — así python3 script.py | cb deja en el portapapeles solo los CSV, sin flags ni copiar a mano.

from helpers_output import console_err, mostrar_tabla_err, resumen_err
 
mostrar_tabla_err(df, titulo="Resumen por ORIGEN")   # rich -> stderr, humano
resumen_err(folios=1425, cuadran=1425, pct="100.0%") # rich -> stderr, humano
 
print("--- CSV_PARA_CLAUDE: nombre_bloque ---")
print(df.to_csv(index=False))                        # -> stdout, para Claude

Regla: cualquier script nuevo de validación usa las variantes _err para pantalla y el prefijo CSV_PARA_CLAUDE para lo que se necesite pegar a la conversación. El helper original (console, mostrar_tabla, resumen, todo a stdout) se conserva para scripts que se corren sin intención de hacerles pipe (exploración interactiva rápida).

Véase también