Unidad: Redes y Grafos para Estadística

Conectar estadística con modelos de redes en problemas reales: negocios en ciudad, relaciones de similitud, rutas, comunidades y recomendaciones; más un cierre con árboles de decisión y simulación por eventos.


🔧 Requisitos prácticos (Google Colab)

  • Instalar librerías con %pip (no !pip).

  • Separar celdas: instalar → importar → usar.

  • Guardar datasets en CSV para reproducibilidad.

  • Si no hay API Key, usar dataset simulado (incluido).

Teoría de redes (grafos) para estadística

Conceptos fundamentales

Un grafo se denota como \(G = (V, E)\), donde:

  • \(V\) es el conjunto de vértices o nodos.
  • \(E\) es el conjunto de aristas o enlaces que conectan pares de nodos.

Tipos de grafos

  • No dirigido: las aristas no tienen orientación, es decir, la conexión entre \(i\) y \(j\) es simétrica (\(i \leftrightarrow j\)).
  • Dirigido: las aristas tienen dirección, representando relaciones asimétricas (\(i \rightarrow j\)).

Peso de una arista

A cada conexión puede asociarse un peso \(w_{ij}\) que representa la intensidad, costo o similitud de la relación entre los nodos \(i\) y \(j\).

Matriz de adyacencia

El grafo puede representarse mediante una matriz de adyacencia \(A = [A_{ij}]\), donde:

\[ A_{ij} = \begin{cases} 1, & \text{si existe una arista entre los nodos } i \text{ y } j, \\ 0, & \text{en caso contrario.} \end{cases} \]

En grafos ponderados, \(A_{ij} = w_{ij}\).

Grado de un nodo

El grado de un nodo \(i\) en un grafo no dirigido se define como:

\[ k_i = \sum_j A_{ij} \]

e indica el número de conexiones (o la suma de pesos, si el grafo es ponderado) asociadas al nodo \(i\).

Ejemplo

Considera el siguiente grafo no dirigido con tres nodos:

library(ggplot2)

# Coordenadas de los nodos
nodos <- data.frame(
  x = c(0, 2, 1),
  y = c(1.7, 1.7, 0),
  label = c("1", "2", "3")
)

# Aristas (pares de nodos)
aristas <- data.frame(
  x = c(0, 2, 1, 0, 2, 1),
  y = c(1.7, 1.7, 0, 1.7, 1.7, 0),
  xend = c(2, 1, 0, 1, 0, 2),
  yend = c(1.7, 0, 1.7, 0, 1.7, 1.7)
)

ggplot() +
  geom_segment(data = aristas, aes(x = x, y = y, xend = xend, yend = yend),
               color = "gray30", linewidth = 1) +
  geom_point(data = nodos, aes(x = x, y = y), size = 6, shape = 21, fill = "white") +
  geom_text(data = nodos, aes(x = x, y = y, label = label), size = 5) +
  coord_fixed() +
  theme_void() +
  theme(plot.margin = margin(10, 10, 10, 10))

La matriz de adyacencia correspondiente es:

\[ A = \begin{pmatrix} 0 & 1 & 1 \\ 1 & 0 & 1 \\ 1 & 1 & 0 \end{pmatrix} \]

y los grados de los nodos son:

\[ k_1 = 2, \quad k_2 = 2, \quad k_3 = 2 \]

Este es un grafo completo con tres vértices (\(K_3\)).

Variables y redes

Una red es un modelo estructural; las variables (rating, reseñas, precio, barrio) son atributos de nodos.

La estadística describe y relaciona:

  • Distribuciones de atributos (p. ej., rating).

  • Distribuciones estructurales (p. ej., grados).

  • Correlaciones entre atributos y métricas de red.

Métricas clave

Las métricas permiten caracterizar la estructura y relevancia de los nodos dentro de un grafo. A continuación se presentan las más utilizadas en análisis de redes.

Densidad

La densidad mide la proporción de conexiones existentes frente a las posibles:

\[ \delta = \frac{2|E|}{|V|(|V|-1)} \]

En un grafo no dirigido, \(\delta = 1\) indica un grafo completamente conectado (\(K_n\)).

Coeficiente de clustering

El coeficiente de clustering (o de agrupamiento) mide la tendencia de los vecinos de un nodo a estar también conectados entre sí.

Si \(T(i)\) es el número de triángulos que involucran al nodo \(i\), y \(k_i\) su grado:

\[ C_i = \frac{2T(i)}{k_i(k_i - 1)} \]

El valor promedio \(C = \frac{1}{|V|}\sum_i C_i\) describe la cohesión local de la red.

Centralidad de grado

La centralidad de grado mide la importancia de un nodo según su número de conexiones directas:

\[ C_D(i) = \frac{k_i}{|V| - 1} \]

Un nodo con alto \(C_D(i)\) es un nodo altamente conectado (hub).

Centralidad de intermediación

La centralidad de intermediación (betweenness) cuantifica cuántas veces un nodo actúa como puente entre otros pares de nodos:

\[ C_B(i) = \sum_{s \neq i \neq t} \frac{\sigma_{st}(i)}{\sigma_{st}} \]

donde \(\sigma_{st}\) es el número total de caminos más cortos entre \(s\) y \(t\),
y \(\sigma_{st}(i)\) es el número de esos caminos que pasan por \(i\).

Centralidad de cercanía

La centralidad de cercanía mide qué tan próximo está un nodo, en promedio, al resto de la red:

\[ C_C(i) = \frac{1}{\sum_j d(i,j)} \]

donde \(d(i,j)\) es la distancia geodésica (longitud del camino más corto) entre los nodos \(i\) y \(j\).

Un nodo con alto \(C_C(i)\) puede acceder rápidamente a los demás en la red.

Ejemplo

import networkx as nx
import matplotlib.pyplot as plt
from matplotlib import cm

# 🎯 1. Construcción del grafo
G = nx.Graph()
edges = [(1, 2), (2, 3), (3, 4), (4, 5), (2, 5), (1, 3)]
G.add_edges_from(edges)
pos = nx.spring_layout(G, seed=42)

# 🌈 Función auxiliar para graficar con estilo
def dibujar_grafo(metrica, valores, titulo, cmap):
    plt.figure(figsize=(6, 5))
    nx.draw_networkx(
        G,
        pos,
        with_labels=True,
        node_color=[valores[n] for n in G.nodes()],
        cmap=cmap,
        node_size=900,
        edge_color="gray",
        font_size=10,
        font_weight="bold"
    )
    plt.title(titulo, fontsize=12, weight="bold")
    plt.axis("off")
    plt.show()

# --- Paso 1. Grafo inicial ---
dibujar_grafo(None, {n: 0.5 for n in G.nodes()}, "Paso 1 — Grafo inicial", cm.Greys)

Exploración paso a paso de métricas de red
# --- Paso 2. Densidad global ---
densidad = nx.density(G)
print(f"🔹 Densidad global: {densidad:.2f}")
🔹 Densidad global: 0.60
# --- Paso 3. Clustering local ---
clustering = nx.clustering(G)
print("🔹 Clustering local por nodo:", clustering)
🔹 Clustering local por nodo: {1: 1.0, 2: 0.3333333333333333, 3: 0.3333333333333333, 4: 0, 5: 0}
dibujar_grafo("Clustering", clustering, "Paso 2 — Clustering local (mayor = más azul)", cm.Blues)

Exploración paso a paso de métricas de red
# --- Paso 4. Centralidad de grado ---
grado = nx.degree_centrality(G)
print("🔹 Centralidad de grado:", grado)
🔹 Centralidad de grado: {1: 0.5, 2: 0.75, 3: 0.75, 4: 0.5, 5: 0.5}
dibujar_grafo("Grado", grado, "Paso 3 — Centralidad de grado (verde = más conexiones)", cm.viridis)

Exploración paso a paso de métricas de red
# --- Paso 5. Centralidad de intermediación ---
betweenness = nx.betweenness_centrality(G)
print("🔹 Centralidad de intermediación:", betweenness)
🔹 Centralidad de intermediación: {1: 0.0, 2: 0.25, 3: 0.25, 4: 0.08333333333333333, 5: 0.08333333333333333}
dibujar_grafo("Betweenness", betweenness, "Paso 4 — Intermediación (rosado = más puentes)", cm.plasma)

Exploración paso a paso de métricas de red
# --- Paso 6. Centralidad de cercanía ---
closeness = nx.closeness_centrality(G)
print("🔹 Centralidad de cercanía:", closeness)
🔹 Centralidad de cercanía: {1: 0.6666666666666666, 2: 0.8, 3: 0.8, 4: 0.6666666666666666, 5: 0.6666666666666666}
dibujar_grafo("Cercanía", closeness, "Paso 5 — Cercanía (amarillo = más accesible)", cm.cividis)

Exploración paso a paso de métricas de red
print("✅ Análisis completado — cada paso muestra visualmente la métrica principal.")
✅ Análisis completado — cada paso muestra visualmente la métrica principal.

Interpretación estadística

El análisis de redes puede entenderse desde una perspectiva estadística.
Cada métrica del grafo puede tratarse como una variable aleatoria con propiedades propias.

Distribución del grado

En una red real, el grado de los nodos, \(k_i\), puede interpretarse como una variable aleatoria que describe cuántas conexiones tiene un nodo típico.

\[ P(k) = \Pr(K = k) \]

Esta distribución puede ser estrecha (homogénea) o de cola pesada (heterogénea), indicando la presencia de nodos altamente conectados (hubs).

Redes aleatorias vs. empíricas

Una red Erdős–Rényi (ER) es una referencia clásica: cada par de nodos se conecta con probabilidad \(p\). Su distribución de grado sigue aproximadamente una Binomial:

\[ P(k) = \binom{n-1}{k} p^k (1-p)^{n-1-k} \]

En contraste, las redes empíricas (sociales, biológicas, de transporte) muestran colas más largas,
frecuentemente aproximadas por una Ley de Potencia:

\[ P(k) \propto k^{-\gamma} \]

Sesgos de muestreo

En datos reales, pueden existir sesgos que alteran las métricas:

  • Datos faltantes: nodos o aristas no observadas.

  • Sesgo de popularidad: sobre-representación de nodos altamente conectados.

Estos sesgos afectan la estimación de \(P(k)\), de la modularidad y de las medidas de centralidad.

Comunidades y modularidad

Una comunidad es un grupo de nodos con alta densidad interna de conexiones y pocas externas.
La modularidad, denotada \(Q\), cuantifica la calidad de dicha partición:

\[ Q = \frac{1}{2m} \sum_{i,j} \left( A_{ij} - \frac{k_i k_j}{2m} \right) \delta(c_i, c_j) \]

Algunas redes famosas

Redes aleatorias (Erdős–Rényi)

Una red aleatoria de Erdős–Rényi, denotada como \(G(n, p)\), se construye a partir de un conjunto de \(n\) nodos donde cada par de nodos se conecta de manera independiente con probabilidad \(p\).

Matemáticamente:

\[ \Pr[(i,j) \in E] = p, \quad \text{para cada par } (i,j) \]

Propiedades clave:

  • El número total de aristas sigue una distribución Binomial: \[ |E| \sim \text{Binomial}\left(\binom{n}{2}, p\right) \]

  • El grado de cada nodo \(k_i\) también sigue una Binomial: \[ k_i \sim \text{Binomial}(n-1, p) \]

  • La mayoría de los nodos tienen un grado cercano al promedio: \[ \mathbb{E}[k_i] = p(n-1) \]

  • No aparecen hubs: todos los nodos tienen grados parecidos.

  • Es un modelo simple y controlado, ideal como referencia o “punto de comparación” para redes reales.

Redes preferenciales (Barabási–Albert)

La red de Barabási–Albert (BA) se basa en el principio de apego preferencial (preferential attachment). La idea es que los nodos nuevos prefieren conectarse con los nodos que ya son populares (los que tienen más conexiones).

Construcción:

  1. Se inicia con un pequeño número de nodos conectados.

  2. En cada paso, se agrega un nuevo nodo que se conecta con \(m\) nodos existentes.

  3. La probabilidad de que el nuevo nodo se conecte con el nodo \(i\) es proporcional a su grado \(k_i\):

\[ \Pr[(\text{nuevo} \rightarrow i)] = \frac{k_i}{\sum_j k_j} \]

Consecuencias:

  • Se generan hubs naturalmente.
  • La distribución de grados sigue una ley de potencia:

\[ P(k) \propto k^{-\gamma}, \quad \text{con } \gamma \approx 3 \]

  • La red es escalafree (scale-free): no hay un grado típico, y existen nodos con muchísimas conexiones.

  • Describe bien muchas redes reales: Internet, redes sociales, redes biológicas, transporte, etc.

HUBS

Un hub (plural: hubs) es un nodo muy conectado dentro de una red, es decir, un vértice que tiene un grado (k_i) mucho mayor que el promedio.

En términos simples:

  • En una red, cada nodo tiene cierto número de conexiones o enlaces.

  • Si la mayoría de los nodos tienen, por ejemplo, 2 o 3 conexiones, pero algunos tienen 20 o más, esos pocos nodos altamente conectados se llaman hubs.

Ejemplo

En una red social, los usuarios “normales” pueden tener unos cientos de amigos, pero unas pocas personas (influencers, celebridades, etc.) tienen miles o millones de conexiones. Ellos son los hubs de la red.

En las redes aleatorias (Erdős–Rényi) no suelen aparecer hubs: casi todos los nodos tienen grados parecidos.

En las redes preferenciales (Barabási–Albert) sí aparecen hubs, porque los nodos nuevos tienden a conectarse con los que ya tienen muchas conexiones — este fenómeno se conoce como preferencia de apego (preferential attachment).

Los hubs son nodos que concentran gran parte de las conexiones de una red y son responsables de su robustez, eficiencia y, a veces, vulnerabilidad.

Ejemplo práctico (Python, paso a paso)

Paso 0 — Preparación: importar librerías y crear una red “empírica”

# 0) Importamos las librerías que usaremos
import networkx as nx
import matplotlib.pyplot as plt

# 0.1) Creamos una red "empírica" sintética: Barabási–Albert
#     - n=50 nodos
#     - Cada nuevo nodo se conecta a m=2 nodos existentes (preferencia por hubs)
G = nx.barabasi_albert_graph(50, 2, seed=42)

# 0.2) Mostramos la red (solo para ubicarla visualmente)

# Creamos una figura de tamaño 6x5 pulgadas
plt.figure(figsize=(6,5))

# Dibujamos el grafo 'G' usando la disposición (layout) de tipo "spring"
# El layout spring_layout simula un sistema de resortes:
#   - Los nodos se repelen entre sí
#   - Las aristas actúan como resortes que los atraen
# Esto produce una representación equilibrada y legible de la red.
nx.draw(
    G,
    pos=nx.spring_layout(G, seed=1),  # disposición reproducible
    node_size=80,                     # tamaño de cada nodo
    node_color="royalblue",           # color de los nodos
    edge_color="gray"                 # color de las aristas
)

# Agregamos un título descriptivo al gráfico
plt.title("Red empírica sintética (Barabási–Albert)")

# Quitamos los ejes (no son útiles en este tipo de gráficos)
plt.axis("off")
(-1.0212073833045716, 1.1920604300877646, -0.9779452301625974, 0.9980519872977348)
# Mostramos el gráfico en pantalla
plt.show()

# Mensaje final para confirmar que se generó correctamente
print("Listo: tenemos una red con 50 nodos y preferencia por hubs.\n")
Listo: tenemos una red con 50 nodos y preferencia por hubs.

Paso 1 — Distribución de grados (P(k))

# 1) Obtenemos el grado (número de conexiones) de cada nodo
grados = [d for _, d in G.degree()]
print("Ejemplo de grados (primeros 10 nodos):", grados[:10])
Ejemplo de grados (primeros 10 nodos): [24, 14, 4, 3, 14, 11, 6, 2, 6, 2]
# 1.1) Graficamos el histograma de P(k)
plt.figure(figsize=(6,4))
plt.hist(grados, bins=range(1, max(grados)+2), color="skyblue", edgecolor="black")
plt.xlabel("Grado k")
plt.ylabel("Frecuencia")
plt.title("Distribución empírica del grado P(k)")
plt.show()

Histograma de grados: P(k) en la red empírica
print("Interpretación: si hay cola larga (valores altos de k con cierta frecuencia), hay hubs.\n")
Interpretación: si hay cola larga (valores altos de k con cierta frecuencia), hay hubs.

Paso 2 — Red aleatoria equivalente (Erdős–Rényi) y comparación de (P(k))

# 2) Construimos una red ER(n, p) con:
n = G.number_of_nodes()
m = G.number_of_edges()
p = 2*m / (n*(n-1))   # probabilidad media de conexión que iguala el número de aristas
ER = nx.erdos_renyi_graph(n, p, seed=42)

# 2.1) Histogramas lado a lado
grados_ER = [d for _, d in ER.degree()]

import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(10,4), sharey=True)

ax[0].hist(grados,    bins=range(1, max(grados)+2),    color="skyblue",  edgecolor="black")
ax[0].set_title("P(k) — Red empírica (BA)")
ax[0].set_xlabel("Grado k"); ax[0].set_ylabel("Frecuencia")

ax[1].hist(grados_ER, bins=range(1, max(grados_ER)+2), color="lightgray", edgecolor="black")
ax[1].set_title("P(k) — Red aleatoria (ER)")
ax[1].set_xlabel("Grado k")

plt.show()

Comparación de P(k) entre red empírica y red aleatoria ER
print("Comparación: ER tiende a P(k) más compacta (tipo binomial), BA muestra más heterogeneidad.\n")
Comparación: ER tiende a P(k) más compacta (tipo binomial), BA muestra más heterogeneidad.

Paso 3 — Visualización estructural: empírica vs. aleatoria

fig, ax = plt.subplots(1, 2, figsize=(10,4))

nx.draw(G,  pos=nx.spring_layout(G,  seed=1), node_size=80, node_color="royalblue", edge_color="gray", ax=ax[0])
ax[0].set_title("Red empírica (BA) — hubs visibles"); ax[0].axis("off")
Text(0.5, 1.0, 'Red empírica (BA) — hubs visibles')
(-1.0212073833045716, 1.1920604300877646, -0.9779452301625974, 0.9980519872977348)
nx.draw(ER, pos=nx.spring_layout(ER, seed=1), node_size=80, node_color="gray",      edge_color="gray", ax=ax[1])
ax[1].set_title("Red aleatoria (ER)"); ax[1].axis("off")
Text(0.5, 1.0, 'Red aleatoria (ER)')
(-1.1845102074607121, 0.9417502785151122, -0.6057284986938077, 0.7775513433033138)
plt.show()

Estructura: Red empírica con hubs vs Red aleatoria
print("Visualmente, BA suele mostrar nodos muy conectados (hubs). ER luce más homogénea.\n")
Visualmente, BA suele mostrar nodos muy conectados (hubs). ER luce más homogénea.

Paso 4 — Comunidades y modularidad (Q)

# === Paso 4) Comunidades y modularidad ===
from networkx.algorithms import community

# 4.1) Detectamos comunidades con el método greedy (basado en modularidad)
comunidades = community.greedy_modularity_communities(G)

# 4.2) Calculamos la modularidad Q
modularidad = community.modularity(G, comunidades)
print(f"🔹 Moduralidad Q = {modularidad:.3f}")
🔹 Moduralidad Q = 0.408
print("\nInterpretación:")

Interpretación:
print("• La modularidad mide qué tan bien la red puede dividirse en comunidades.")
• La modularidad mide qué tan bien la red puede dividirse en comunidades.
print("• Evalúa si hay más conexiones dentro de los grupos que entre ellos.")
• Evalúa si hay más conexiones dentro de los grupos que entre ellos.
print("• Su valor va típicamente entre -0.5 y 1.0:")
• Su valor va típicamente entre -0.5 y 1.0:
print("     → Q ≈ 0: conexiones aleatorias, sin estructura modular clara.")
     → Q ≈ 0: conexiones aleatorias, sin estructura modular clara.
print("     → Q > 0.3: comunidades bien definidas (estructura modular fuerte).")
     → Q > 0.3: comunidades bien definidas (estructura modular fuerte).
print("     → Q muy alto (> 0.6): grupos muy densos y separados entre sí.\n")
     → Q muy alto (> 0.6): grupos muy densos y separados entre sí.
# 4.3) Asignamos color por comunidad y graficamos
color_map = {}
for i, com in enumerate(comunidades):
    for nodo in com:
        color_map[nodo] = i
colores = [color_map[n] for n in G.nodes()]

plt.figure(figsize=(6,5))
nx.draw(G, pos=nx.spring_layout(G, seed=2),
        node_color=colores, cmap=plt.cm.tab10,
        node_size=120, edge_color="gray", with_labels=False)
plt.title("Comunidades detectadas (color por grupo)")
plt.axis("off")
(-1.178502081827515, 1.0155401877356922, -1.0443698687118539, 1.194261390239588)
plt.show()

Comunidades detectadas en la red empírica (color por grupo)
# 4.4) Conclusión breve
print("💡 Si Q es alto (≈ 0.3 o más), la red presenta una estructura modular relevante.")
💡 Si Q es alto (≈ 0.3 o más), la red presenta una estructura modular relevante.
print("   Esto significa que existen grupos de nodos más conectados entre sí que con el resto.\n")
   Esto significa que existen grupos de nodos más conectados entre sí que con el resto.

La modularidad ( Q ) es una medida que compara la densidad de enlaces dentro de las comunidades con la densidad esperada en una red aleatoria con el mismo grado de conectividad. En términos intuitivos:

  • Si los nodos se agrupan de manera natural —por afinidad, cercanía o interacción frecuente—, la modularidad será alta.

  • Si las conexiones están distribuidas al azar, la modularidad será baja o cercana a cero.

Por eso, un valor ( Q ) se interpreta como presencia de comunidades reales: hay estructuras sociales, funcionales o temáticas que segmentan la red en subgrupos internamente cohesivos. Por ejemplo:

import networkx as nx
import matplotlib.pyplot as plt

def red_modularidad(tipo):
    """Crea redes pequeñas con distinta estructura modular."""
    if tipo == "baja":
        # Red aleatoria, sin grupos
        G = nx.erdos_renyi_graph(8, 0.4, seed=1)
        colores = ["#66c2a5"] * 8

    elif tipo == "media":
        # Dos grupos con algunos enlaces entre ellos
        G1 = nx.cycle_graph(5)
        G2 = nx.cycle_graph(5)
        G = nx.disjoint_union(G1, G2)
        G.add_edges_from([(1, 6), (2, 7)])  # pocas conexiones cruzadas
        colores = ["#fc8d62"] * 5 + ["#8da0cb"] * 5

    else:  # Alta modularidad
        # Tres comunidades bien separadas
        G1 = nx.cycle_graph(4)
        G2 = nx.cycle_graph(4)
        G3 = nx.cycle_graph(4)
        G = nx.disjoint_union_all([G1, G2, G3])
        G.add_edge(1, 5)  # casi desconectadas
        colores = ["#e78ac3"] * 4 + ["#a6d854"] * 4 + ["#ffd92f"] * 4

    return G, colores


# --- Dibujamos tres ejemplos ---
fig, axs = plt.subplots(1, 3, figsize=(9, 3))
casos = ["baja", "media", "alta"]
titulos = [
    "Q ≈ 0.05 — sin estructura",
    "Q ≈ 0.30 — grupos débiles",
    "Q ≈ 0.65 — grupos definidos"
]

for ax, tipo, titulo in zip(axs, casos, titulos):
    G, colores = red_modularidad(tipo)
    pos = nx.spring_layout(G, seed=2)
    nx.draw(
        G, pos,
        node_color=colores,
        edge_color="gray",
        node_size=200,
        with_labels=False,
        ax=ax
    )
    ax.set_title(titulo, fontsize=9)
    ax.axis("off")
Text(0.5, 1.0, 'Q ≈ 0.05 — sin estructura')
(-0.6257998510030132, 1.0279898936926688, -1.1969435467098746, 1.0725963725182064)
Text(0.5, 1.0, 'Q ≈ 0.30 — grupos débiles')
(-1.2090056954963142, 1.2099055185765728, -0.5337812877530883, 0.532446920132434)
Text(0.5, 1.0, 'Q ≈ 0.65 — grupos definidos')
(-0.8221004126388598, 1.1731407631919277, -0.7576197097694347, 0.8161672302346876)
plt.tight_layout()
plt.show()

Redes pequeñas con modularidad baja, media y alta