Los mapas de calor o heatmaps se han convertido en una herramienta esencial para la visualización y el análisis de datos. Su utilidad radica esencialmente en la simpleza para observar patrones y concentraciones de datos de manera intuitiva. Sin embargo, tienen bastantes limitaciones. Pueden resultar imprecisos en la representación de áreas con alta densidad de puntos o en la interpretación de datos cuando estos están muy dispersos. Es en estos casos precisamente donde un diagrama de Voronoi puede ofrecernos una solución ya que nos permiten tener una visión más precisa de las áreas influidas por cada datapoint. El artículo de hoy trata precisamente de eso, de cómo programar un diagrama de Voronoi en Python.
Pero antes de empezar, y con el único afán de sumergirte en la importancia de los diagramas de Voronoi y sus muchas utilidades, me gustaría reinvindicar la figura de Clara Grima y animarte a descubrir su gran aportación al estudio de las células: la figura geométrica ‘escutoide’. Seguramente no te suene este nombre, pero en parte tiene mucho que ver con lo que te voy a contar a continuación y por el por qué me he animado (a través de una reactualización de esta historia que se ha publicado hace poco en una famosa revista científica) a recuperar la funcionalidad de los diagramas de Voronoi y buscarles una aplicación mucho más mundana dentro del universo digital. Lógicamente, a mil años luz del aporte de esta fenómena matemática, la cual nos demostró hace seis años que una gran dosis de matemáticas y mucho de intuición puede ayudar a salvar vidas.
Diagrama de Voronoi
Es una partición del plano en diferentes espacios poligonales donde cada datapoint está más cerca de su región correspondiente que de la definida por otros. Ésto proporciona una estructura para analizar la proximidad y la distribución espacial de puntos en un área específica, facilitando la toma de decisiones basadas en la proximidad. De hecho, se utilizan en una amplia gama de aplicaciones como la geografía (mapas), la biología (células) o la optimización algoritmos. La representación habitual de este tipo de diagrama se parece (retomo la referencia a la historia previa de los escutoides) y mucho a cómo se organizan las células en nuestro organismo observadas desde arriba (vista cenital).
Diagrama de Voronoi genérico.
Desarrollo Python en mapas de calor
Como he comentado al inicio, cuando los datapoints están muy cerca unos de otros, o cuando la distribución es desigual, los mapas de calor pueden ser muy confusos y por lo tanto no demasiado útiles. Al aplicar un diagrama de Voronoi a un mapa de calor se puede dividir el espacio en regiones claramente definidas alrededor de cada punto, donde cada región representa el área de influencia del punto más cercano. Esto ayuda a identificar de manera más precisa qué áreas tienen mayor proximidad a los puntos de interés y cómo se relacionan entre sí. Por ejemplo, en un mapa de calor de clics en un sitio web o app, este diagrama puede facilitar la visualización de áreas de alta y baja actividad, ayudándonos a entender patrones y tendencias con mayor claridad.
A continuación añado primero el resultado obtenido y después el código básico para implementarlo. Creo que es mejor detenerse antes en el potencial visual de lo que nos puede aportar esta solución. Por otra parte y como es lógico, he utilizado los datos recogidos en una pantalla de una app real pero seleccionando muchos menos datapoints y añadiendo una capa para ocultar la imagen con la intención de anonimizar la información.
Resultado de aplicar el diagrama de Voronoi desarrollado en Python.
Como puedes ver la solución que ofrece un diagrama de Voronoi es muy útil. Da una perspectiva ampliada y específica de lo que realmente está ocurriendo, más allá de lo que se puede llegar a percibir con un mapa de calor.
Además, en este caso concreto, se ha complementado la creación del diagrama de Voronoi con la clusterización de las diferentes agrupaciones de datapoints para potencial la utilidad del desarrollo. Para ello, he hecho uso del que seguramente es el algoritmo más utilizado para ello: K-means (si quieres saber más sobre éste y otros algoritmos de clusterización te recomiendo clicar aquí). Usar conjuntamente K-means y el diagrama de Voronoi permite, entre otras cuestiones, lo siguiente:
-
-
Identificar clústeres: Identificar patrones y estructuras dentro de los datos.
-
Visualizar áreas de influencia: El hecho de que cada región corresponda a un clúster permite una visualización nítida de la distribución de los datos.
-
Sabiendo todo esto, ¡vamos a por el código! …
import numpy as np
from scipy.spatial import Voronoi
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from PIL import Image
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from shapely.geometry import Polygon as ShapelyPolygon, box
from shapely.ops import unary_union
import warnings
warnings.filterwarnings("ignore")
# Paso 1: Cargar y escalar la imagen
image_path = '[AÑADIR AQUÍ RUTA DE LA IMAGEN]'
img = Image.open(image_path)
img_width, img_height = img.size
img = img.resize((img_width * 2, img_height * 2), Image.LANCZOS)
# Paso 2: Extraer datapoints de la herramienta de Analítica (Mapa de calor) usada y guardarlos en la variable all_points como si de un array se tratase con la siguiente estructura [[dp01_axisx, dp01_axisy],[dp02_axisx, dp02_axisy],...]
# Paso 3: Utilizar el método silhouette para encontrar el número óptimo de clústeres en que se dividirá el diagrama de Voronoi
def find_optimal_clusters_silhouette(data, max_k):
iters = range(2, max_k + 1)
silhouette_scores = []
for k in iters:
kmeans = KMeans(n_clusters=k, random_state=42)
labels = kmeans.fit_predict(data)
score = silhouette_score(data, labels)
silhouette_scores.append(score)
plt.figure(figsize=(10, 6))
plt.plot(iters, silhouette_scores, marker='o')
plt.xlabel('Número de clústeres')
plt.ylabel('Silhouette Score')
plt.show()
optimal_k = iters[np.argmax(silhouette_scores)]
highest_score = max(silhouette_scores)
return optimal_k, highest_score
optimal_k, highest_score = find_optimal_clusters_silhouette(all_points, 10)
print(f'Número óptimo de clústeres: {optimal_k}')
print(f'Silhouette score más grande: {highest_score}')
# Paso 4: Aplicar, por ejemplo, KMeans para llevar a cabo la clusterización
kmeans = KMeans(n_clusters=optimal_k, random_state=42)
labels = kmeans.fit_predict(all_points)
# Paso 5: Crear y dibujar el diagrama de Voronoi
vor = Voronoi(all_points)
bounding_box = box(0, 0, img_width * 2, img_height * 2)
fig, ax = plt.subplots(figsize=(12, 12))
ax.imshow(img)
colors = plt.cm.get_cmap('tab10', optimal_k)
for region_index, region in enumerate(vor.regions):
if not -1 in region and len(region) > 0:
polygon = [vor.vertices[i] for i in region]
poly_shape = ShapelyPolygon(polygon)
clipped_poly = poly_shape.intersection(bounding_box)
if clipped_poly.is_empty:
continue
ax.add_patch(Polygon(np.array(clipped_poly.exterior.coords),
facecolor=(*colors(labels[vor.point_region == region_index][0])[:3], 0.4),
edgecolor='none'))
ax.axis('off')
plt.show()
Si quieres probar el código anterior te dejo a continuación un pequeño script para crear unos datos sintéticos similares a los del resultado previo (ver imagen más arriba). Para ello debes añadir las siguientes líneas de código en el paso 2 anteriormente mencionado.
points = [
[28, 497],
[95, 150],
[209, 138],
[149, 60],
[275, 575]
]
points = np.array(points) * 2
def generate_points_around(center, num_points, radius=30):
angles = np.random.uniform(0, 2 * np.pi, num_points)
radii = np.random.uniform(0, radius, num_points)
x_offsets = radii * np.cos(angles)
y_offsets = radii * np.sin(angles)
new_points = np.column_stack((
center[0] + x_offsets,
center[1] + y_offsets
))
new_points[:, 0] = np.clip(new_points[:, 0], 0, img_width * 2)
new_points[:, 1] = np.clip(new_points[:, 1], 0, img_height * 2)
return new_points
all_points = np.array(points)
for point in points:
num_additional_points = np.random.randint(5, 11)
additional_points = generate_points_around(point, num_additional_points)
all_points = np.vstack([all_points, additional_points])
Con este artículo creo que he conseguido aunar desarrollos no tan conocidos como los diagramas de Voronoi con algo tan utilizado como los mapas de calor. Una forma original y al alcance de tod@s de unificar universos matemáticos y marketinianos.
Si quieres seguir aprendiendo sobre este tipo de implementaciones de Analítica de datos, te invito a que visites el resto de artículos de mi Blog. ¡Te espero en el siguiente artículo!

