Medidas de heterogeneidad en Big Mart Sales

Introducción

Después de estudiar la localización, la variabilidad y la forma de las distribuciones, el análisis exploratorio de datos debe avanzar hacia una pregunta más estructural: ¿el comportamiento observado es homogéneo en toda la población o cambia de manera importante entre grupos?. Esta cuestión introduce el concepto de heterogeneidad, una dimensión central del EDA cuando el dataset contiene segmentaciones naturales, como categorías de producto, tipos de tienda, tamaños de outlet o ubicaciones geográficas.

En términos estadísticos, la heterogeneidad alude a la existencia de diferencias sistemáticas entre subpoblaciones. Tales diferencias pueden expresarse en el centro de la distribución, en su dispersión, en su forma o incluso en la presencia diferencial de valores extremos. En un problema de negocio como BigMart, detectar heterogeneidad es indispensable, pues permite identificar si ciertos tipos de tiendas venden consistentemente más, si algunas categorías presentan patrones de ventas más variables, o si la estructura comercial cambia según el entorno del outlet.

En este capítulo se trabaja la heterogeneidad como una extensión natural del análisis univariado. La idea principal es que una variable numérica no debe interpretarse de manera aislada, sino también en relación con variables categóricas relevantes.

Preparación del entorno

Trabajaremos con el siguiente dataset BigMart, para esta sección usaremos Python y las bibliotecas pandas, numpy, plotly y matplotlib.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go

plt.rcParams["figure.figsize"] = (8, 5)
plt.rcParams["axes.grid"] = True

DATA_PATH = "../../data/bigmart_sales.csv"
df = pd.read_csv(DATA_PATH)
df.head()
Item_Identifier Item_Weight Item_Fat_Content Item_Visibility Item_Type Item_MRP Outlet_Identifier Outlet_Establishment_Year Outlet_Size Outlet_Location_Type Outlet_Type Item_Outlet_Sales
0 FDA15 9.30 Low Fat 0.016047 Dairy 249.8092 OUT049 1999 Medium Tier 1 Supermarket Type1 3735.1380
1 DRC01 5.92 Regular 0.019278 Soft Drinks 48.2692 OUT018 2009 Medium Tier 3 Supermarket Type2 443.4228
2 FDN15 17.50 Low Fat 0.016760 Meat 141.6180 OUT049 1999 Medium Tier 1 Supermarket Type1 2097.2700
3 FDX07 19.20 Regular 0.000000 Fruits and Vegetables 182.0950 OUT010 1998 NaN Tier 3 Grocery Store 732.3800
4 NCD19 8.93 Low Fat 0.000000 Household 53.8614 OUT013 1987 High Tier 3 Supermarket Type1 994.7052

Conviene verificar las variables categóricas y numéricas del conjunto de datos.

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8523 entries, 0 to 8522
Data columns (total 12 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   Item_Identifier            8523 non-null   object 
 1   Item_Weight                7060 non-null   float64
 2   Item_Fat_Content           8523 non-null   object 
 3   Item_Visibility            8523 non-null   float64
 4   Item_Type                  8523 non-null   object 
 5   Item_MRP                   8523 non-null   float64
 6   Outlet_Identifier          8523 non-null   object 
 7   Outlet_Establishment_Year  8523 non-null   int64  
 8   Outlet_Size                6113 non-null   object 
 9   Outlet_Location_Type       8523 non-null   object 
 10  Outlet_Type                8523 non-null   object 
 11  Item_Outlet_Sales          8523 non-null   float64
dtypes: float64(4), int64(1), object(7)
memory usage: 799.2+ KB
num_cols = df.select_dtypes(include=np.number).columns.tolist()
cat_cols = df.select_dtypes(exclude=np.number).columns.tolist()

num_cols, cat_cols
(['Item_Weight',
  'Item_Visibility',
  'Item_MRP',
  'Outlet_Establishment_Year',
  'Item_Outlet_Sales'],
 ['Item_Identifier',
  'Item_Fat_Content',
  'Item_Type',
  'Outlet_Identifier',
  'Outlet_Size',
  'Outlet_Location_Type',
  'Outlet_Type'])

Fundamentos teóricos

La heterogeneidad puede analizarse desde varias perspectivas. Si \(Y\) es una variable numérica de interés y \(G\) una variable categórica que define grupos, pueden estudiarse diferencias en:

Centro por grupo

Se comparan medidas como:

\[ \bar{y}_g = \frac{1}{n_g}\sum_{i \in g} y_i, \]

así como la mediana y otros cuantiles dentro de cada grupo \(g\).

Dispersión por grupo

La variabilidad interna de cada subpoblación puede resumirse mediante la desviación estándar, el rango intercuartílico o el coeficiente de variación. Dos grupos pueden tener medias semejantes y, sin embargo, diferir de forma sustancial en estabilidad o consistencia.

Forma por grupo

La heterogeneidad también puede aparecer en la asimetría o en la presencia de colas pesadas. Algunos grupos pueden concentrarse en rangos estrechos y otros exhibir ventas con distribución muy extendida.

Participación relativa de cada grupo

Finalmente, también es útil estudiar cuánto contribuye cada grupo al total de observaciones o al total de ventas. Esta dimensión conecta la heterogeneidad con el estudio posterior de la concentración.

Variable objetivo y grupos de interés

En este capítulo tomaremos nuevamente como variable principal a \(\texttt{Item\_Outlet\_Sales}\). Como ejes de segmentación se consideran, al menos, las siguientes variables categóricas:

  • \(\texttt{Outlet\_Type}\),
  • \(\texttt{Outlet\_Size}\),
  • \(\texttt{Outlet\_Location\_Type}\),
  • \(\texttt{Item\_Type}\).

Antes de comparar grupos, es útil revisar su tamaño.

for col in ["Outlet_Type", "Outlet_Size", "Outlet_Location_Type", "Item_Type"]:
    print(f"\n{col}")
    print(df[col].value_counts(dropna=False))

Outlet_Type
Outlet_Type
Supermarket Type1    5577
Grocery Store        1083
Supermarket Type3     935
Supermarket Type2     928
Name: count, dtype: int64

Outlet_Size
Outlet_Size
Medium    2793
NaN       2410
Small     2388
High       932
Name: count, dtype: int64

Outlet_Location_Type
Outlet_Location_Type
Tier 3    3350
Tier 2    2785
Tier 1    2388
Name: count, dtype: int64

Item_Type
Item_Type
Fruits and Vegetables    1232
Snack Foods              1200
Household                 910
Frozen Foods              856
Dairy                     682
Canned                    649
Baking Goods              648
Health and Hygiene        520
Soft Drinks               445
Meat                      425
Breads                    251
Hard Drinks               214
Others                    169
Starchy Foods             148
Breakfast                 110
Seafood                    64
Name: count, dtype: int64

Este paso es importante porque la interpretación de diferencias entre grupos debe considerar el tamaño muestral. Un grupo muy pequeño puede mostrar estadísticas inestables o aparentes extremos debidos a pocos casos.

Heterogeneidad por tipo de outlet

Comenzamos con una variable de alto interés de negocio: \(\texttt{Outlet\_Type}\).

outlet_type_summary = (
    df.groupby("Outlet_Type")["Item_Outlet_Sales"]
      .agg(["count", "mean", "median", "std", "min", "max"])
      .sort_values("mean", ascending=False)
)
outlet_type_summary
count mean median std min max
Outlet_Type
Supermarket Type3 935 3694.038558 3364.9532 2127.760054 241.6854 13086.9648
Supermarket Type1 5577 2316.181148 1990.7420 1515.965558 73.2380 10256.6490
Supermarket Type2 928 1995.498739 1655.1788 1375.932889 69.2432 6768.5228
Grocery Store 1083 339.828500 256.9988 260.851582 33.2900 1775.6886
fig = px.box(
    df,
    x="Outlet_Type",
    y="Item_Outlet_Sales",
    color="Outlet_Type",
    title="Distribución de ventas por tipo de outlet"
)
fig.update_layout(showlegend=False)
fig.show()
fig = px.bar(
    outlet_type_summary.reset_index(),
    x="Outlet_Type",
    y="mean",
    error_y="std",
    title="Media y desviación estándar de ventas por tipo de outlet",
    text_auto=True
)
fig.show()

A partir de este análisis se observa una heterogeneidad clara por tipo de outlet, lo que indica que Outlet_Type sí está asociado con diferencias relevantes en el nivel de ventas.

En primer lugar, Supermarket Type3 destaca como el segmento con mayor nivel de ventas, tanto en la media (3694.039) como en la mediana (3364.953). Además, su boxplot se ubica claramente por encima del resto, lo que sugiere un desempeño consistentemente superior.

En segundo lugar, Supermarket Type1 y Supermarket Type2 presentan niveles de ventas intermedios, aunque Supermarket Type1 muestra valores centrales algo mayores (media = 2316.181, mediana = 1990.742) que Supermarket Type2 (media = 1995.499, mediana = 1655.179). Esto sugiere que, aunque ambos formatos son comparables, no tienen exactamente el mismo comportamiento comercial.

Por otro lado, Grocery Store aparece como el grupo con menor nivel de ventas, con una media de 339.829 y una mediana de 256.999, muy por debajo de los supermercados. Esto revela una diferencia estructural importante entre este tipo de outlet y los demás.

En términos de variabilidad, Supermarket Type3 también presenta la mayor dispersión (std = 2127.760), lo que indica que no solo vende más, sino que sus ventas son más heterogéneas. Supermarket Type1 y Supermarket Type2 también muestran dispersión considerable, mientras que Grocery Store tiene una variabilidad mucho menor en términos absolutos (std = 260.852), coherente con su escala de ventas más baja.

Los boxplots también muestran la presencia de valores atípicos en todos los grupos, especialmente en los supermercados. Esto sugiere que dentro de cada tipo de outlet existen productos o combinaciones producto-tienda con desempeños excepcionalmente altos.

En conjunto, este resultado tiene varias implicaciones para el EDA:

  • Outlet_Type debe considerarse una variable clave de segmentación;
  • no conviene analizar Item_Outlet_Sales como si proviniera de una sola población homogénea;
  • es recomendable profundizar con comparaciones entre grupos, distribuciones segmentadas y medidas robustas;
  • en etapas posteriores, Outlet_Type probablemente será una variable explicativa importante en cualquier análisis predictivo o inferencial.

En síntesis, el análisis muestra que el tipo de outlet introduce diferencias sustanciales tanto en el nivel como en la dispersión de las ventas, por lo que esta variable merece un lugar central dentro del EDA.

Heterogeneidad por tamaño del outlet

El tamaño del outlet puede influir tanto en el volumen típico de ventas como en la dispersión observada.

outlet_size_summary = (
    df.groupby("Outlet_Size")["Item_Outlet_Sales"]
      .agg(["count", "mean", "median", "std"])
      .sort_values("mean", ascending=False)
)
outlet_size_summary
count mean median std
Outlet_Size
Medium 2793 2681.603542 2251.0698 1855.210528
High 932 2298.995256 2050.6640 1533.531664
Small 2388 1912.149161 1544.6560 1582.370364
fig = px.violin(
    df,
    x="Outlet_Size",
    y="Item_Outlet_Sales",
    color="Outlet_Size",
    box=True,
    points=False,
    title="Ventas por tamaño de outlet"
)
fig.update_layout(showlegend=False)
fig.show()

Los violin plots son especialmente útiles aquí porque combinan información sobre dispersión y forma dentro de cada grupo. Si dos grupos tienen medianas parecidas pero densidades internas distintas, la heterogeneidad va más allá del simple promedio.

Este análisis también muestra heterogeneidad por tamaño del outlet, aunque con diferencias menos extremas que las observadas por Outlet_Type. Aun así, Outlet_Size aporta información relevante para el EDA porque parece asociarse tanto con el nivel típico de ventas como con su dispersión.

En primer lugar, los outlets de tamaño Medium presentan el mayor nivel de ventas, con una media de 2681.604 y una mediana de 2251.070. Les siguen los outlets High, con media = 2298.995 y mediana = 2050.664, mientras que los outletsSmall` muestran los valores más bajos (media = 1912.149, mediana = 1544.656**). Esto sugiere que el tamaño del outlet sí introduce diferencias sistemáticas en el comportamiento comercial.

En términos de dispersión, Medium también presenta la mayor variabilidad (std = 1855.211), seguido por Small (std = 1582.370) y High (std = 1533.532). Los violin plots refuerzan esta idea al mostrar que no solo cambian los niveles centrales, sino también la forma interna de la distribución en cada grupo.

Para el EDA, esto tiene varias consecuencias:

  • Outlet_Size debe considerarse una variable relevante de segmentación, ya que las ventas no parecen distribuirse igual entre tamaños de tienda;
  • no basta con comparar solo promedios: la forma de los violines sugiere diferencias en concentración, dispersión y densidad interna entre grupos;
  • conviene cruzar Outlet_Size con otras variables categóricas, especialmente Outlet_Type, para ver si parte de esta heterogeneidad se explica por la combinación entre ambas;
  • en análisis posteriores, Outlet_Size puede funcionar como una variable explicativa útil, aunque probablemente con menor poder discriminante que Outlet_Type;
  • dado que esta variable tenía valores faltantes en etapas previas, su papel analítico resulta todavía más importante, porque ayuda a decidir si conviene imputarla y preservarla en lugar de descartarla.

En síntesis, el tamaño del outlet no solo afecta el nivel típico de ventas, sino también la estructura de su distribución. Por ello, en el EDA conviene tratarlo como un factor de heterogeneidad relevante y no como una simple característica secundaria del establecimiento.

Heterogeneidad por ubicación del outlet

Otra dimensión natural del análisis es \(\texttt{Outlet\_Location\_Type}\), que puede reflejar diferencias territoriales o de mercado.

location_summary = (
    df.groupby("Outlet_Location_Type")["Item_Outlet_Sales"]
      .agg(["count", "mean", "median", "std"])
      .sort_values("mean", ascending=False)
)
location_summary
count mean median std
Outlet_Location_Type
Tier 2 2785 2323.990559 2004.0580 1520.543543
Tier 3 3350 2279.627651 1812.3076 1912.451333
Tier 1 2388 1876.909159 1487.3972 1561.649293
fig = px.box(
    df,
    x="Outlet_Location_Type",
    y="Item_Outlet_Sales",
    color="Outlet_Location_Type",
    title="Ventas por tipo de localización del outlet"
)
fig.update_layout(showlegend=False)
fig.show()

El análisis por Outlet_Location_Type revela también heterogeneidad territorial en las ventas, aunque con diferencias más moderadas que las observadas por tipo de outlet.

En términos de nivel central, Tier 2 presenta el mayor promedio (mean = 2323.991, median = 2004.058), seguido muy de cerca por Tier 3 (mean = 2279.628, median = 1812.308). En contraste, Tier 1 muestra los valores más bajos (mean = 1876.909, median = 1487.397), lo que sugiere que las ventas tienden a ser menores en este tipo de localización.

En cuanto a la dispersión, Tier 3 presenta la mayor variabilidad (std = 1912.451), lo que indica una mayor heterogeneidad en el desempeño de ventas dentro de este grupo. Los boxplots también muestran la presencia de valores extremos en todas las ubicaciones, especialmente en Tier 3.

Para el EDA, esto sugiere que:

  • Outlet_Location_Type introduce diferencias territoriales en el nivel de ventas;
  • Tier 2 y Tier 3 tienen comportamientos relativamente similares en el centro de la distribución, aunque Tier 3 muestra mayor dispersión;
  • Tier 1 parece corresponder a ubicaciones con menor desempeño promedio;
  • conviene explorar interacciones entre ubicación, tipo y tamaño del outlet, ya que parte de la heterogeneidad observada podría explicarse por la combinación de estas variables.

En síntesis, la localización del outlet añade otra dimensión de segmentación en el EDA y refuerza la idea de que las ventas no provienen de una población homogénea, sino de subgrupos con comportamientos distintos.

Heterogeneidad por tipo de producto

La variable \(\texttt{Item\_Type}\) suele contener más niveles que las anteriores, por lo que conviene resumir y ordenar cuidadosamente la información.

item_type_summary = (
    df.groupby("Item_Type")["Item_Outlet_Sales"]
      .agg(["count", "mean", "median", "std"])
      .sort_values("mean", ascending=False)
)
item_type_summary.head(10)
count mean median std
Item_Type
Starchy Foods 148 2374.332773 1968.1048 1773.945328
Seafood 64 2326.065928 2055.3246 1842.988719
Fruits and Vegetables 1232 2289.009592 1830.9500 1799.503459
Snack Foods 1200 2277.321739 1944.1360 1705.121755
Household 910 2258.784300 1981.4208 1692.245757
Dairy 682 2232.542597 1650.8511 1884.404698
Canned 649 2225.194904 1860.2452 1645.235638
Breads 251 2204.132226 1860.2452 1644.235914
Meat 425 2158.977911 1829.6184 1695.231081
Hard Drinks 214 2139.221622 1816.6353 1606.191587
fig = px.bar(
    item_type_summary.reset_index(),
    x="mean",
    y=item_type_summary.reset_index()["Item_Type"],
    orientation="h",
    title="Media de ventas por tipo de producto",
    labels={"mean": "Media de ventas", "y": "Tipo de producto"}
)
fig.show()
top_item_types = item_type_summary.head(8).index.tolist()
fig = px.box(
    df[df["Item_Type"].isin(top_item_types)],
    x="Item_Type",
    y="Item_Outlet_Sales",
    color="Item_Type",
    title="Distribución de ventas en los tipos de producto con mayor media"
)
fig.update_layout(showlegend=False)
fig.show()

El análisis por Item_Type muestra que también existe heterogeneidad por categoría de producto, aunque menos marcada que la observada por Outlet_Type. Aun así, las diferencias son suficientes para justificar una exploración segmentada.

En términos de promedio, las categorías con mayores ventas son Starchy Foods (mean = 2374.333), Seafood (mean = 2326.066) y Fruits and Vegetables (mean = 2289.010). Sin embargo, estas dos primeras tienen tamaños muestrales mucho menores (148 y 64 observaciones, respectivamente), por lo que sus medias deben interpretarse con más cautela.

Entre las categorías con mayor presencia en el dataset, Fruits and Vegetables (1232 casos), Snack Foods (1200) y Household (910) combinan niveles altos de ventas con una base de observaciones más sólida, lo que las vuelve especialmente relevantes para el EDA.

Los boxplots sugieren además que estas categorías no solo difieren en el nivel central, sino también en su dispersión y en la presencia de valores extremos. Por ejemplo, Household, Fruits and Vegetables y Snack Foods muestran colas superiores amplias y varios valores altos, lo que sugiere comportamientos heterogéneos dentro de cada tipo de producto.

Para el EDA, esto tiene varias implicaciones:

  • Item_Type debe considerarse una variable de segmentación importante;
  • conviene distinguir entre categorías con muchas observaciones y categorías con poca representación, para no sobredimensionar diferencias basadas en muestras pequeñas;
  • resulta útil cruzar Item_Type con variables como Outlet_Type u Outlet_Size, ya que parte de la heterogeneidad podría depender de la interacción entre producto y tienda;
  • en etapas posteriores, Item_Type puede aportar valor explicativo, pero probablemente con efecto desigual según la categoría.

En síntesis, el tipo de producto introduce diferencias reales en las ventas, pero su interpretación debe combinar nivel de ventas, dispersión y tamaño muestral para evitar conclusiones apresuradas.

Tablas comparativas con múltiples indicadores

Una forma útil de sistematizar la heterogeneidad es construir tablas resumen que integren varias medidas por grupo.

def group_profile(df, group_col, target="Item_Outlet_Sales"):
    out = (
        df.groupby(group_col)[target]
          .agg(
              n="count",
              media="mean",
              mediana="median",
              desviacion="std",
              q1=lambda x: x.quantile(0.25),
              q3=lambda x: x.quantile(0.75)
          )
          .reset_index()
    )
    out["iqr"] = out["q3"] - out["q1"]
    out["cv"] = out["desviacion"] / out["media"]
    return out.sort_values("media", ascending=False)

profile_outlet_type = group_profile(df, "Outlet_Type")
profile_outlet_type
Outlet_Type n media mediana desviacion q1 q3 iqr cv
3 Supermarket Type3 935 3694.038558 3364.9532 2127.760054 2044.33890 4975.52340 2931.1845 0.575998
1 Supermarket Type1 5577 2316.181148 1990.7420 1515.965558 1151.16820 3135.91800 1984.7498 0.654511
2 Supermarket Type2 928 1995.498739 1655.1788 1375.932889 981.55565 2702.64865 1721.0930 0.689518
0 Grocery Store 1083 339.828500 256.9988 260.851582 153.79980 458.73620 304.9364 0.767598
profile_location = group_profile(df, "Outlet_Location_Type")
profile_location
Outlet_Location_Type n media mediana desviacion q1 q3 iqr cv
1 Tier 2 2785 2323.990559 2004.0580 1520.543543 1171.80800 3110.6176 1938.80960 0.654281
2 Tier 3 3350 2279.627651 1812.3076 1912.451333 731.38130 3307.6944 2576.31310 0.838931
0 Tier 1 2388 1876.909159 1487.3972 1561.649293 593.72715 2803.0180 2209.29085 0.832032

Estas tablas permiten pasar de una observación visual a una descripción más precisa: no solo se identifican grupos “más altos” o “más bajos”, sino también grupos más o menos variables y con distinta estabilidad relativa.

Heterogeneidad cruzada

En muchos problemas reales, la segmentación relevante no depende de una sola variable categórica. Una comparación cruzada, por ejemplo entre tipo de outlet y localización, ayuda a identificar patrones más finos.

pivot_mean = pd.pivot_table(
    df,
    values="Item_Outlet_Sales",
    index="Outlet_Type",
    columns="Outlet_Location_Type",
    aggfunc="mean"
)
pivot_mean
Outlet_Location_Type Tier 1 Tier 2 Tier 3
Outlet_Type
Grocery Store 340.329723 NaN 339.351662
Supermarket Type1 2313.099451 2323.990559 2298.995256
Supermarket Type2 NaN NaN 1995.498739
Supermarket Type3 NaN NaN 3694.038558
fig = px.imshow(
    pivot_mean,
    text_auto=True,
    aspect="auto",
    title="Media de ventas: tipo de outlet vs localización"
)
fig.show()

La heterogeneidad cruzada muestra que una sola variable categórica no siempre basta para describir el comportamiento de las ventas. En este caso, la combinación entre Outlet_Type y Outlet_Location_Type permite detectar patrones más específicos que no se observan con la misma claridad al analizar cada variable por separado.

El resultado más visible es que Supermarket Type3 en Tier 3 presenta la media de ventas más alta (3694.039), mientras que Grocery Store mantiene niveles muy bajos tanto en Tier 1 (340.330) como en Tier 3 (339.352). Por su parte, Supermarket Type1 muestra un comportamiento bastante estable entre ubicaciones, con medias de 2313.099 en Tier 1, 2323.991 en Tier 2 y 2298.995 en Tier 3.

Para el EDA, esto tiene varias implicaciones:

  • la variación en ventas no depende solo del tipo de outlet ni solo de la ubicación, sino de su combinación;
  • Supermarket Type1 parece ser un formato relativamente estable entre territorios, mientras que otros tipos están mucho más concentrados en ciertas ubicaciones;
  • existen combinaciones ausentes (NaN), lo que indica que no todos los formatos de outlet están presentes en todas las localizaciones, y eso también forma parte de la estructura real del negocio;
  • conviene seguir explorando interacciones entre variables categóricas, porque parte de la heterogeneidad puede estar oculta cuando se estudian los factores por separado;
  • en etapas posteriores, estas combinaciones pueden ser útiles para construir variables explicativas más informativas que cada factor individual por sí solo.

En síntesis, este análisis refuerza una idea central del EDA: las ventas no provienen de grupos simples, sino de segmentos combinados donde el contexto del outlet importa tanto como su categoría individual.

Entropía de Shannon como medida de heterogeneidad

Hasta ahora, la heterogeneidad se ha estudiado comparando promedios, medianas, dispersiones y distribuciones entre grupos. Sin embargo, también es útil contar con una medida global que resuma qué tan diversificada o concentrada está una variable categórica. Para ello puede utilizarse la entropía de Shannon.

Si una categoría concentra casi toda la masa de observaciones o de ventas, la entropía será baja. En cambio, si la distribución está más repartida entre varios grupos, la entropía será mayor. En este sentido, la entropía ofrece una medida sintética de diversidad.

En términos generales, para una variable categórica con proporciones \(p_1, p_2, \dots, p_k\), la entropía se define como

\[ H = - \sum_{i=1}^{k} p_i \log(p_i) \]

donde valores más altos indican mayor heterogeneidad. Como el valor máximo depende del número de categorías, también conviene trabajar con una entropía normalizada:

\[ H_{\text{norm}} = \frac{H}{\log(k)} \]

De esta forma, el resultado queda entre 0 y 1:

  • valores cercanos a 0 indican alta concentración;
  • valores cercanos a 1 indican alta diversidad.

Entropía de ventas por variable categórica

En esta sección calcularemos la entropía sobre la distribución de ventas agregadas por categoría, para responder preguntas como: ¿las ventas están repartidas entre muchos grupos o dependen de unos pocos?

import numpy as np
import pandas as pd

vars_cat_heterogeneidad = [
    "Outlet_Type",
    "Outlet_Size",
    "Outlet_Location_Type",
    "Item_Type"
]

def shannon_entropy_from_sales(df, cat_var, sales_var="Item_Outlet_Sales"):
    sales_by_group = df.groupby(cat_var)[sales_var].sum().dropna()
    p = sales_by_group / sales_by_group.sum()
    h = -(p * np.log(p)).sum()
    h_norm = h / np.log(len(p)) if len(p) > 1 else np.nan
    
    return pd.Series({
        "n_categorias": len(p),
        "entropia": h,
        "entropia_normalizada": h_norm
    })

entropy_table = pd.DataFrame({
    var: shannon_entropy_from_sales(df, var)
    for var in vars_cat_heterogeneidad
}).T.reset_index(names="variable")

entropy_table.sort_values("entropia_normalizada", ascending=False).round(4)
variable n_categorias entropia entropia_normalizada
2 Outlet_Location_Type 3.0 1.0758 0.9792
3 Item_Type 16.0 2.5168 0.9078
1 Outlet_Size 3.0 0.9876 0.8990
0 Outlet_Type 4.0 0.8731 0.6298

La interpretación debe hacerse con cuidado. Una entropía alta no significa necesariamente “mejor”, sino simplemente que las ventas están más repartidas entre las categorías de esa variable. Una entropía baja, en cambio, sugiere que el negocio está más concentrado en unos pocos segmentos.

import plotly.express as px

entropy_plot = entropy_table.sort_values("entropia_normalizada", ascending=True)

fig = px.bar(
    entropy_plot,
    x="entropia_normalizada",
    y="variable",
    orientation="h",
    text="entropia_normalizada",
    title="Entropía normalizada de Shannon por variable categórica",
    labels={
        "entropia_normalizada": "Entropía normalizada",
        "variable": "Variable categórica"
    },
    range_x=[0, 1]
)

fig.update_traces(texttemplate="%{text:.3f}", textposition="outside")
fig.update_layout(height=450, width=900)

fig.show()

Si se desea profundizar en una variable concreta, también puede visualizarse directamente la distribución de las ventas agregadas por categoría. Por ejemplo, para Item_Type:

sales_item_type = (
    df.groupby("Item_Type")["Item_Outlet_Sales"]
      .sum()
      .sort_values(ascending=False)
      .reset_index()
)

sales_item_type["proporcion"] = (
    sales_item_type["Item_Outlet_Sales"] /
    sales_item_type["Item_Outlet_Sales"].sum()
)

fig = px.bar(
    sales_item_type,
    x="Item_Type",
    y="proporcion",
    text="proporcion",
    title="Proporción de ventas por tipo de producto",
    labels={
        "Item_Type": "Tipo de producto",
        "proporcion": "Proporción de ventas"
    }
)

fig.update_traces(texttemplate="%{text:.3f}", textposition="outside")
fig.update_layout(xaxis_tickangle=45, height=500, width=950)

fig.show()

Los resultados de entropía sugieren que no todas las variables categóricas aportan el mismo tipo de segmentación para el EDA. En particular, Outlet_Location_Type presenta la mayor entropía normalizada (0.9792), seguido por Item_Type (0.9078) y Outlet_Size (0.8990). Esto indica que, en estas variables, las ventas están bastante repartidas entre sus categorías y no dependen fuertemente de un solo grupo.

En cambio, Outlet_Type muestra una entropía normalizada claramente menor (0.6298), lo que sugiere una mayor concentración de las ventas en unos pocos tipos de outlet. Esto es coherente con los análisis previos, donde se observó que ciertos formatos, especialmente Supermarket Type3 y Supermarket Type1, concentran gran parte del volumen de ventas.

Para el EDA, esto tiene varias implicaciones:

  • Outlet_Type aparece como una variable especialmente útil para detectar concentración comercial, ya que distingue grupos con pesos muy desiguales en el negocio;
  • Outlet_Location_Type, Outlet_Size e Item_Type reflejan una estructura más diversificada, por lo que su utilidad analítica está menos en la concentración y más en comparar patrones internos entre categorías;
  • una entropía alta no elimina la heterogeneidad, sino que indica que el negocio está más repartido entre grupos, por lo que conviene profundizar con medidas de nivel, dispersión y forma dentro de cada categoría;
  • el gráfico de proporciones por Item_Type muestra que, aunque la distribución es bastante diversa, algunas categorías como Fruits and Vegetables (0.152) y Snack Foods (0.147) tienen un peso algo mayor que otras, por lo que pueden merecer atención especial en análisis posteriores.

En síntesis, la entropía complementa el EDA porque ayuda a distinguir entre variables que describen un negocio más concentrado y variables que describen un negocio más distribuido. En este caso, Outlet_Type parece ser la variable más útil para estudiar concentración, mientras que Item_Type, Outlet_Size y Outlet_Location_Type son más útiles para explorar diversidad y segmentación interna.

Índice de Gini como medida de concentración

Además de la entropía de Shannon, otra forma de cuantificar la heterogeneidad o concentración en una distribución es mediante el índice de Gini. Mientras la entropía mide cuán repartida está la masa de una distribución, el índice de Gini mide qué tan desigual es esa distribución.

En términos generales, el índice de Gini toma valores entre 0 y 1:

  • 0 indica distribución perfectamente uniforme (todas las categorías tienen el mismo peso).
  • 1 indica concentración extrema (una sola categoría concentra toda la masa).

En el contexto de este dataset, el índice de Gini permite responder preguntas como:

  • ¿las ventas están relativamente equilibradas entre categorías?
  • ¿o unas pocas categorías concentran gran parte del volumen de ventas?

Matemáticamente, el índice de Gini puede calcularse a partir de las proporciones ordenadas de la distribución.

Cálculo del índice de Gini por variable categórica

En este caso se calcula el índice sobre la distribución de ventas agregadas por categoría.

import numpy as np
import pandas as pd

vars_cat_heterogeneidad = [
    "Outlet_Type",
    "Outlet_Size",
    "Outlet_Location_Type",
    "Item_Type"
]

def gini_from_sales(df, cat_var, sales_var="Item_Outlet_Sales"):
    sales = df.groupby(cat_var)[sales_var].sum().dropna().values
    p = sales / sales.sum()
    
    p_sorted = np.sort(p)
    n = len(p_sorted)
    
    gini = 1 - 2 * np.sum((np.arange(1, n+1) * p_sorted)) / n
    
    return pd.Series({
        "n_categorias": n,
        "gini": gini
    })

gini_table = pd.DataFrame({
    var: gini_from_sales(df, var)
    for var in vars_cat_heterogeneidad
}).T.reset_index(names="variable")

gini_table.sort_values("gini", ascending=False).round(4)
variable n_categorias gini
2 Outlet_Location_Type 3.0 -0.4465
3 Item_Type 16.0 -0.4593
1 Outlet_Size 3.0 -0.5844
0 Outlet_Type 4.0 -0.7778

Visualización del índice de Gini

import plotly.express as px

gini_plot = gini_table.sort_values("gini", ascending=True)

fig = px.bar(
    gini_plot,
    x="gini",
    y="variable",
    orientation="h",
    text="gini",
    title="Índice de Gini de ventas por variable categórica",
    labels={
        "gini": "Índice de Gini",
        "variable": "Variable categórica"
    },
    range_x=[0,1]
)

fig.update_traces(texttemplate="%{text:.3f}", textposition="outside")
fig.update_layout(height=450, width=900)

fig.show()

Mini-dashboard de heterogeneidad

A continuación se construye un dashboard compacto que integra varias vistas segmentadas.

from plotly.subplots import make_subplots

fig = make_subplots(
    rows=2,
    cols=2,
    subplot_titles=(
        "Media por tipo de outlet",
        "Media por tamaño de outlet",
        "Media por localización",
        "Top 8 tipos de producto"
    )
)

summary1 = group_profile(df, "Outlet_Type")
summary2 = group_profile(df.dropna(subset=["Outlet_Size"]), "Outlet_Size")
summary3 = group_profile(df, "Outlet_Location_Type")
summary4 = group_profile(df, "Item_Type").head(8)

fig.add_trace(
    go.Bar(x=summary1["Outlet_Type"], y=summary1["media"], name="Outlet Type"),
    row=1, col=1
)
fig.add_trace(
    go.Bar(x=summary2["Outlet_Size"], y=summary2["media"], name="Outlet Size"),
    row=1, col=2
)
fig.add_trace(
    go.Bar(x=summary3["Outlet_Location_Type"], y=summary3["media"], name="Location"),
    row=2, col=1
)
fig.add_trace(
    go.Bar(x=summary4["Item_Type"], y=summary4["media"], name="Item Type"),
    row=2, col=2
)

fig.update_layout(
    title="Dashboard básico de heterogeneidad en BigMart",
    height=750,
    showlegend=False
)
fig.show()

Conclusiones del capítulo

En este capítulo se mostró que la heterogeneidad constituye una dimensión esencial del análisis exploratorio de datos. Comparar grupos permite ampliar la descripción estadística de una variable y pasar de una lectura global a una interpretación segmentada y contextual.

En el dataset de BigMart, la heterogeneidad se manifiesta en diferencias entre tipos de outlet, tamaños, localizaciones y categorías de producto. Estas comparaciones enriquecen el EDA porque permiten distinguir subestructuras internas del fenómeno, identificar patrones consistentes y detectar segmentos con comportamientos contrastantes.

Este capítulo prepara de manera natural el siguiente paso del proyecto: el estudio de la concentración, donde se analizará si una fracción reducida de grupos, productos o outlets explica una proporción sustancial del total de ventas.