from matplotlib.ticker import MaxNLocator
def graficar_centro(x, titulo, xlabel, bins=20, ax=None):
"""Histograma con líneas verticales en la media y la mediana."""
standalone = ax is None
if standalone:
fig, ax = plt.subplots(figsize=(9, 4))
x = np.asarray(x, dtype=float)
ax.hist(x, bins=bins, alpha=0.75, edgecolor="white")
ax.axvline(np.mean(x), color="C3", linestyle="--", linewidth=2, label=f"Media = {np.mean(x):,.0f}")
ax.axvline(np.median(x), color="C2", linewidth=2, label=f"Mediana = {np.median(x):,.0f}")
ax.set_xlabel(xlabel)
ax.set_ylabel("Frecuencia")
ax.set_title(titulo)
ax.legend(fontsize=9)
if standalone:
plt.tight_layout()
plt.show()
def graficar_perdidas(x):
"""MSE(c) y MAE(c) al resumir los datos con una constante c: sus mínimos son la media y la mediana."""
x = np.asarray(x, dtype=float)
c = np.unique(np.concatenate([np.linspace(np.min(x), np.percentile(x, 95), 400),
x, [np.mean(x), np.median(x)]]))
c = c[c <= np.percentile(x, 95)]
mse = ((x[None, :] - c[:, None]) ** 2).mean(axis=1)
mae = np.abs(x[None, :] - c[:, None]).mean(axis=1)
fig, axes = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)
for ax, curva, nombre, centro, color, etiqueta in [
(axes[0], mse, "MSE(c)", np.mean(x), "C3", "Media"),
(axes[1], mae, "MAE(c)", np.median(x), "C2", "Mediana"),
]:
ax.plot(c, curva)
en_minimo = c[curva <= curva.min() * (1 + 1e-12)]
if en_minimo.max() > en_minimo.min():
ax.axvspan(en_minimo.min(), en_minimo.max(), color="0.6", alpha=0.35,
label=f"Tramo mínimo = [{en_minimo.min():,.0f}, {en_minimo.max():,.0f}]")
else:
ax.plot(en_minimo, [curva.min()], "ko", label=f"Mínimo de la curva = {en_minimo[0]:,.0f}")
ax.axvline(centro, color=color, linestyle="--", label=f"{etiqueta} = {centro:,.0f}")
ax.set_xlabel("Constante c usada como resumen")
ax.set_ylabel(nombre)
ax.set_title(f"{nombre}: el mínimo está en la {etiqueta.lower()}")
ax.legend(fontsize=8)
plt.show()
def graficar_frecuencias(series, ordenes=None):
"""Barras de frecuencia; la moda se resalta. Una variable ordinal se dibuja en su orden natural."""
ordenes = ordenes or [None] * len(series)
fig, axes = plt.subplots(1, len(series), figsize=(5.5 * len(series), 3.8), constrained_layout=True)
for ax, s, orden in zip(np.atleast_1d(axes), series, ordenes):
conteo = s.value_counts() if orden is None else s.value_counts().reindex(orden)
colores = ["C1" if v == conteo.max() else "C0" for v in conteo.values]
barras = ax.bar(conteo.index.astype(str), conteo.values, color=colores)
for barra, v in zip(barras, conteo.values):
if v == conteo.max():
barra.set_hatch("//")
tipo = "nominal, ordenada por frecuencia" if orden is None else "ordinal, en su orden natural"
ax.set_title(f"{s.name} ({tipo})\nla moda es la barra resaltada", fontsize=10)
ax.set_ylabel("Frecuencia")
plt.show()
def graficar_moda_discreta_continua(discreta, continua, ancho_intervalo):
"""Moda en una variable discreta (barras) frente a intervalo modal en una continua (histograma)."""
fig, axes = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)
conteo = discreta.value_counts().sort_index()
colores = ["C1" if v == conteo.max() else "C0" for v in conteo.values]
barras = axes[0].bar(conteo.index, conteo.values, color=colores)
for barra, v in zip(barras, conteo.values):
if v == conteo.max():
barra.set_hatch("//")
axes[0].set_title(f"{discreta.name} (discreta): moda(s) resaltada(s)")
axes[0].set_xlabel(discreta.name)
axes[0].set_ylabel("Frecuencia")
bordes = np.arange(0, continua.max() + ancho_intervalo, ancho_intervalo)
frec, bordes, parches = axes[1].hist(continua, bins=bordes, edgecolor="white")
parches[int(np.argmax(frec))].set_facecolor("C1")
parches[int(np.argmax(frec))].set_hatch("//")
axes[1].set_title(f"{continua.name} (continua): intervalo modal resaltado")
axes[1].set_xlabel(f"{continua.name} (intervalos de {ancho_intervalo:,})")
axes[1].set_ylabel("Frecuencia")
for ax in axes:
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
plt.show()
def graficar_ruptura(porcentajes, medias, medianas, recortadas, referencia):
"""Cómo cambian media, mediana y media recortada al contaminar una fracción creciente de datos."""
fig, ax = plt.subplots(figsize=(9, 4.2))
ax.plot(porcentajes, medias, marker="o", color="C3", label="Media")
ax.plot(porcentajes, recortadas, marker="^", color="C1", label="Media recortada 10%")
ax.plot(porcentajes, medianas, marker="s", color="C2", label="Mediana")
ax.axhline(referencia, color="0.3", linestyle=":", label=f"Mediana sin contaminar = {referencia:,.0f}")
ax.set_yscale("log")
ax.set_xlabel("% de datos reemplazados por valores extremos")
ax.set_ylabel("Valor de la medida (escala log)")
ax.set_title("Punto de ruptura: cuántos datos malos aguanta cada medida")
ax.legend(fontsize=8)
plt.tight_layout()
plt.show()
def graficar_centro_por_grupo(tabla, mediana_global):
"""Media y mediana por grupo, con la mediana global como referencia."""
x = np.arange(len(tabla))
w = 0.38
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(x - w / 2, tabla["media"], width=w, color="C3", alpha=0.8, label="Media")
ax.bar(x + w / 2, tabla["mediana"], width=w, color="C2", alpha=0.8, hatch="//", edgecolor="white", label="Mediana")
for xi, n_i in zip(x, tabla["n"]):
ax.text(xi, 150, f"n={n_i}", ha="center", color="white", fontsize=9)
ax.axhline(mediana_global, color="0.3", linestyle=":", label=f"Mediana global = {mediana_global:,.0f}")
ax.set_xticks(x)
ax.set_xticklabels(tabla.index)
ax.set_ylabel("Ingreso mensual")
ax.set_title("Centro por ciudad: media y mediana")
ax.legend(fontsize=8)
plt.tight_layout()
plt.show()
def graficar_imputacion(observado, imputado_media, imputado_mediana, bins):
"""Distribución observada frente a la distribución tras imputar con media o mediana."""
fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True, constrained_layout=True)
for ax, imputado, nombre, color in [
(axes[0], imputado_media, "media", "C3"),
(axes[1], imputado_mediana, "mediana", "C2"),
]:
ax.hist(imputado, bins=bins, color=color, alpha=0.55, label=f"Tras imputar con la {nombre}")
ax.hist(observado, bins=bins, histtype="step", color="black", linewidth=1.5, label="Solo valores observados")
ax.set_title(f"Imputación con la {nombre}: un pico artificial")
ax.set_xlabel("Ingreso mensual")
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
ax.legend(fontsize=8)
axes[0].set_ylabel("Frecuencia")
plt.show()
def graficar_salarios(salarios, etiquetas):
"""Salarios ordenados con media y mediana: ¿cuántas personas ganan 'el promedio'?"""
orden = np.argsort(salarios)
s = np.asarray(salarios)[orden]
fig, ax = plt.subplots(figsize=(10, 4))
barras = ax.bar(np.arange(len(s)), s / 1e6, color=["C1" if e != "Equipo" else "C0" for e in np.asarray(etiquetas)[orden]])
for barra, e in zip(barras, np.asarray(etiquetas)[orden]):
if e != "Equipo":
barra.set_hatch("//")
ax.axhline(np.mean(s) / 1e6, color="C3", linestyle="--", linewidth=2, label=f"Media = {np.mean(s)/1e6:.2f} M")
ax.axhline(np.median(s) / 1e6, color="C2", linewidth=2, label=f"Mediana = {np.median(s)/1e6:.2f} M")
ax.set_xlabel("25 empleados ordenados por salario (azul: equipo; naranja con trama: directivos y fundador)")
ax.set_ylabel("Salario mensual (millones)")
ax.set_title("¿Cuántas personas ganan el 'salario promedio'?")
ax.set_xticks([])
ax.legend(fontsize=9)
plt.tight_layout()
plt.show()