#182
Consejos prácticos / Visualización de datos / Gestión de blogs · Python Aprox. 2400 caracteres
Insertar tablas de comparación <table> en las entradas de un blog suele generar un desplazamiento horizontal en dispositivos móviles, lo que interrumpe el flujo de lectura. Hemos desarrollado un módulo que reemplaza automáticamente todas las <table> de un artículo por gráficos neuronales SVG (nodos + líneas de sinapsis) con un degradado en tonos púrpura y rosa. El resultado: cero desplazamiento horizontal en móviles y un impacto visual mucho más potente que el de una tabla convencional. A continuación, explicamos cómo lo creamos, cómo funciona, qué resultados ofrece y cómo lo validamos.
Por qué lo creamos
Las tablas HTML <table> son excelentes para la precisión de los datos, pero presentaban dos problemas principales.
Primero, el diseño móvil. Si una tabla tiene 4 o más columnas, el desplazamiento horizontal es inevitable en pantallas de 360px. En nuestro sitio, donde más del 60% de los lectores acceden desde dispositivos móviles, la experiencia de usuario de tener que desplazar la tabla celda por celda resultaba demasiado incómoda.
Segundo, el impacto visual. Los primeros 5 segundos tras hacer clic desde los resultados de búsqueda son cruciales, y una tabla común y corriente suele transmitir la sensación de "¿otra tabla comparativa más?". Al mostrar los mismos datos mediante un gráfico, el lector puede captar las diferencias de un vistazo, permitiendo que el flujo del contenido continúe de forma natural.
Por ello, integramos un paso en nuestra cadena de ganchos de publicación (publish hook chain) para que, cuando el redactor simplemente escriba una <table>, esta se convierta automáticamente en un gráfico neuronal SVG justo antes de publicarse.
Cómo funciona
Un paso dentro de la cadena de ganchos de publish_post consiste en llamar a transform_tables_to_neural(html). Su funcionamiento es el siguiente:
- Detección de tablas — Captura todos los nodos
<table>utilizando BeautifulSoup. Las etiquetas<th>dentro de<thead>representan las categorías, y cada<tr>dentro de<tbody>representa una serie de datos. - Clasificación de la intención de la tabla — Analiza el número de columnas, filas y el tipo de valores (números, texto, calificaciones con estrellas, moneda) para recomendar un tipo de gráfico. Por ejemplo: una tabla de puntuación de 3 columnas → score_bars, tabla de puntuación de 2 columnas → donut, datos cronológicos → area_trend, tabla de comparación de precios → pricing_tier. El módulo cuenta con un catálogo de 16 tipos de gráficos (donut / score_bars / bar_grouped / area_trend / treemap / waterfall / timeline / radar / heatmap / funnel / quadrant / decision_tree / feature_breakdown / pricing_tier / pin_vertical / comparison_table).
- Normalización de datos — Extrae únicamente los números de las celdas de la tabla (por ejemplo, "₩4,900/mes" → 4900, "" → 4, "4.2" → 4.2). Esto genera un arreglo de tipo float unidimensional o bidimensional comparable.
- Renderizado SVG — Llama a
inline_charts_neural.render(kind, data, labels, title). Dibuja el gráfico con nodos de color púrpura (#2d2850), un brillo rosa (#7c5bff) y líneas de sinapsis (#46366e). Cero imágenes externas, 100% SVG en línea (inline). Todas las instancias se identifican de manera uniforme mediante la clase CSStsp-nchart. - Reemplazo de HTML — Elimina el nodo
<table>original e inserta la etiquetafiguredel SVG en la misma posición. El título o descripción hereda la etiqueta<caption>de la tabla o el texto del encabezado H2/H3 inmediatamente anterior.
El tiempo promedio de procesamiento total es de 80 ms (basado en 5 tablas por artículo). Un impacto insignificante en el tiempo total de la cadena de ganchos de publicación.
Resultados reales
- Desplazamiento horizontal en móviles en artículos publicados: promedio de 32% → 0% (tras aplicarlo a todo el sitio)
- Tiempo promedio de permanencia en el artículo (GA4): 1 min 47 s → 2 min 11 s (+13%)
- CTR de artículos comparativos (GSC): promedio de 3.4% → 4.1% (+20%, comparación de 6 semanas)
- Número acumulado de conversiones de tabla a gráfico: aprox. 1200 casos (en todo el sitio)
- Tamaño promedio del SVG: 2.8 KB en línea. Costo de alojamiento de imágenes externas: 0.
Esto no significa que un gráfico sea siempre mejor que una tabla. Si los datos son densos, como una matriz de 6 columnas × 12 filas, una tabla sigue siendo más precisa. Por ello, el módulo también incluye una opción para "mantener la tabla tal cual" (force_keep). Sin embargo, en el 90% de los casos, un gráfico resulta mucho más efectivo para realizar comparaciones.
Métodos de validación
Realizamos tres tipos de validación.
Prueba rápida A/B en móviles — Publicamos 6 artículos en versión de tabla frente a 6 en versión de gráfico para medir la tasa de aparición de desplazamiento horizontal en dispositivos móviles (iPhone de 375px). Resultado: 6/6 incidencias con tablas, 0/6 con gráficos.
Comparación de páginas vistas en GA4 (6 semanas tras el lanzamiento de sess133) — Comparamos el promedio de 8 semanas antes de implementar el módulo de conversión frente al promedio de 8 semanas posteriores, dentro de la misma categoría (artículos comparativos). El tiempo promedio de permanencia aumentó de 1 min 47 s a 2 min 11 s, lo que representa una señal clara de interacción de calidad (quality engagement) más allá del simple tráfico.
Pruebas de regresión visual — Introdujimos un conjunto de datos estándar (3 categorías × 5 elementos, puntuaciones de 0 a 100) para cada uno de los 16 tipos de gráficos y comparamos la salida SVG byte por byte. A la misma entrada, la misma salida: idempotencia de 16/16.
Cómo implementarlo paso a paso
La clave está en capturar la <table> con BeautifulSoup, extraer los datos y generar el SVG en un solo paso.
```python from bs4 import BeautifulSoup import re
def table_to_dict(table_html: str) -> dict: soup = BeautifulSoup(table_html, "html.parser") headers = [th.get_text(strip=True) for th in soup.select("thead th")] rows = [] for tr in soup.select("tbody tr"): cells = [td.get_text(strip=True) for td in tr.select("td")] rows.append(cells) return {"headers": headers, "rows": rows}
def cell_to_number(cell: str) -> float: """Extraer solo números de celdas con estrellas, moneda o porcentajes.""" if "" in cell: return cell.count("") m = re.search(r"(-?[\d,]+(?:.\d+)?)", cell) return float(m.group(1).replace(",", "")) if m else 0.0
def render_neural_bars(labels: list[str], values: list[float]) -> str:
"""SVG de gráfico de barras neuronal simple. Nodos púrpura + líneas de sinapsis."""
w, h = 600, 320
max_v = max(values) or 1
bars = []
for i, v in enumerate(values):
x = 60 + i * 100
bar_h = (v / max_v) * 220
y = h - 40 - bar_h
# Nodos (círculos)
bars.append(f'