Autor

José Ramón Hernández Aguilar

Fecha de publicación

6 de julio de 2025

1 Introducción

El presente análisis exploratorio de datos (EDA) tiene como objetivo comprender la estructura, la calidad y las principales características de los datos históricos de ventas de Walmart. Se busca describir cómo están organizados los datos, su naturaleza jerárquica, e identificar patrones, valores atípicos y posibles inconsistencias que puedan afectar su análisis.

El conjunto de datos está compuesto por 42,840 series temporales jerárquicas, correspondientes a registros de ventas a lo largo del tiempo organizados en distintos niveles. Los datos provienen de tres estados de Estados Unidos: California (CA), Texas (TX) y Wisconsin (WI). La naturaleza jerárquica de los datos permite su agregación a diferentes niveles, como producto, departamento, categoría o estado. El período cubierto por las ventas abarca desde enero de 2011 hasta abril de 2016, e incluye también información sobre precios, promociones y días festivos. Cabe señalar que un alto porcentaje de las series presenta periodos con valores de ventas iguales a cero.

En total, el conjunto de datos incluye 3,049 productos individuales, distribuidos en 3 categorías, 7 departamentos y ubicadas en los tres estados mencionados.

Los datos se presentan en tres archivos separados:

  • sales_train.csv: Son los datos principales. Contienen una columna para cada uno de los 1913 días desde el 29/01/2011 hasta el 25/04/2016. También incluye los ID de artículo, departamento, categoría, tienda y estado.

  • calendar.csv: Contiene las fechas en las que se venden los productos junto con características relacionadas como día de la semana, mes, año y 3 indicadores binarios que indican si las tiendas en cada estado permitían compras con cupones de alimentos SNAP en esta fecha (1) o no (0).

  • sell_prices.csv: Contiene información sobre los productos vendidos (ID de tienda, artículo, fecha y precio de venta).

2 Preparación

Carga de módulos para la manipulación de datos y visualización interactiva.

Código
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px

Carga de los datos utilizando pandas. Los archivos originales en formato .csv han sido previamente convertidos a .parquet para optimizar el uso de memoria y espacio en disco, mejorando la eficiencia en el manejo de grandes volúmenes de datos.

Código
calendar = pd.read_parquet('data/calendar.parquet')
train = pd.read_parquet('data/sales_train.parquet')
prices = pd.read_parquet('data/sell_prices.parquet')
sample_submit = pd.read_parquet('data/sample_submission.parquet')

3 Estructura y contenido de los datos

Como primer paso, es recomendable echar un vistazo rápido a los conjuntos de datos.

Aquí están las primeras 10 filas de los datos de ventas:

Código
train.head(10)
id item_id dept_id cat_id store_id state_id d_1 d_2 d_3 d_4 ... d_1904 d_1905 d_1906 d_1907 d_1908 d_1909 d_1910 d_1911 d_1912 d_1913
0 HOBBIES_1_001_CA_1_validation HOBBIES_1_001 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 1 3 0 1 1 1 3 0 1 1
1 HOBBIES_1_002_CA_1_validation HOBBIES_1_002 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 0
2 HOBBIES_1_003_CA_1_validation HOBBIES_1_003 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 2 1 2 1 1 1 0 1 1 1
3 HOBBIES_1_004_CA_1_validation HOBBIES_1_004 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 1 0 5 4 1 0 1 3 7 2
4 HOBBIES_1_005_CA_1_validation HOBBIES_1_005 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 2 1 1 0 1 1 2 2 2 4
5 HOBBIES_1_006_CA_1_validation HOBBIES_1_006 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 0 1 0 1 0 0 0 2 0 0
6 HOBBIES_1_007_CA_1_validation HOBBIES_1_007 HOBBIES_1 HOBBIES CA_1 CA 0 0 0 0 ... 0 0 0 1 0 1 0 0 1 1
7 HOBBIES_1_008_CA_1_validation HOBBIES_1_008 HOBBIES_1 HOBBIES CA_1 CA 12 15 0 0 ... 0 0 1 37 3 4 6 3 2 1
8 HOBBIES_1_009_CA_1_validation HOBBIES_1_009 HOBBIES_1 HOBBIES CA_1 CA 2 0 7 3 ... 0 0 1 1 6 0 0 0 0 0
9 HOBBIES_1_010_CA_1_validation HOBBIES_1_010 HOBBIES_1 HOBBIES CA_1 CA 0 0 1 0 ... 1 0 0 0 0 0 0 2 0 2

10 rows × 1919 columns

Se infiere que:

  • Hay una columna para cada ID de artículo, departamento, categoría, tienda y estado; además de un ID general que combina los demás ID y una marca de validación.

  • Las ventas por fecha se codifican como columnas que comienzan con el prefijo d_. Estas indican el número de unidades vendidas por día (no el total de dólares).

  • Hay bastantes valores cero.

Este conjunto de datos tiene demasiadas columnas y filas para mostrarlas todas:

Código
train.shape
(30490, 1919)

Este conjunto de datos da los cambios de precio semanales por artículo:

Código
prices.head(10)
store_id item_id wm_yr_wk sell_price
0 CA_1 HOBBIES_1_001 11325 9.58
1 CA_1 HOBBIES_1_001 11326 9.58
2 CA_1 HOBBIES_1_001 11327 8.26
3 CA_1 HOBBIES_1_001 11328 8.26
4 CA_1 HOBBIES_1_001 11329 8.26
5 CA_1 HOBBIES_1_001 11330 8.26
6 CA_1 HOBBIES_1_001 11331 8.26
7 CA_1 HOBBIES_1_001 11332 8.26
8 CA_1 HOBBIES_1_001 11333 8.26
9 CA_1 HOBBIES_1_001 11334 8.26
Código
# desactivar notación científica
pd.options.display.float_format = '{:.2f}'.format

prices.describe(include='all')
store_id item_id wm_yr_wk sell_price
count 6841121 6841121 6841121.00 6841121.00
unique 10 3049 NaN NaN
top TX_2 HOUSEHOLD_2_142 NaN NaN
freq 701214 2820 NaN NaN
mean NaN NaN 11382.94 4.41
std NaN NaN 148.61 3.41
min NaN NaN 11101.00 0.01
25% NaN NaN 11247.00 2.18
50% NaN NaN 11411.00 3.47
75% NaN NaN 11517.00 5.84
max NaN NaN 11621.00 107.32

Resultados:

  • Los precios varían desde $0.10 hasta poco más de $100.

Los datos del calendario brindan características de fecha, como día de la semana, mes o año; junto con 2 características de eventos diferentes y una columna de cupones de alimentos SNAP:

Código
calendar.head(10)
date wm_yr_wk weekday wday month year d event_name_1 event_type_1 event_name_2 event_type_2 snap_CA snap_TX snap_WI
0 2011-01-29 11101 Saturday 1 1 2011 d_1 None None None None 0 0 0
1 2011-01-30 11101 Sunday 2 1 2011 d_2 None None None None 0 0 0
2 2011-01-31 11101 Monday 3 1 2011 d_3 None None None None 0 0 0
3 2011-02-01 11101 Tuesday 4 2 2011 d_4 None None None None 1 1 0
4 2011-02-02 11101 Wednesday 5 2 2011 d_5 None None None None 1 0 1
5 2011-02-03 11101 Thursday 6 2 2011 d_6 None None None None 1 1 1
6 2011-02-04 11101 Friday 7 2 2011 d_7 None None None None 1 0 0
7 2011-02-05 11102 Saturday 1 2 2011 d_8 None None None None 1 1 1
8 2011-02-06 11102 Sunday 2 2 2011 d_9 SuperBowl Sporting None None 1 1 1
9 2011-02-07 11102 Monday 3 2 2011 d_10 None None None None 1 1 0
Código
calendar.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1969 entries, 0 to 1968
Data columns (total 14 columns):
 #   Column        Non-Null Count  Dtype 
---  ------        --------------  ----- 
 0   date          1969 non-null   object
 1   wm_yr_wk      1969 non-null   int64 
 2   weekday       1969 non-null   object
 3   wday          1969 non-null   int64 
 4   month         1969 non-null   int64 
 5   year          1969 non-null   int64 
 6   d             1969 non-null   object
 7   event_name_1  162 non-null    object
 8   event_type_1  162 non-null    object
 9   event_name_2  5 non-null      object
 10  event_type_2  5 non-null      object
 11  snap_CA       1969 non-null   int64 
 12  snap_TX       1969 non-null   int64 
 13  snap_WI       1969 non-null   int64 
dtypes: int64(7), object(7)
memory usage: 215.5+ KB
Código
calendar.describe(include='all')
date wm_yr_wk weekday wday month year d event_name_1 event_type_1 event_name_2 event_type_2 snap_CA snap_TX snap_WI
count 1969 1969.00 1969 1969.00 1969.00 1969.00 1969 162 162 5 5 1969.00 1969.00 1969.00
unique 1969 NaN 7 NaN NaN NaN 1969 30 4 4 2 NaN NaN NaN
top 2011-01-29 NaN Saturday NaN NaN NaN d_1 SuperBowl Religious Father's day Cultural NaN NaN NaN
freq 1 NaN 282 NaN NaN NaN 1 6 55 2 4 NaN NaN NaN
mean NaN 11347.09 NaN 4.00 6.33 2013.29 NaN NaN NaN NaN NaN 0.33 0.33 0.33
std NaN 155.28 NaN 2.00 3.42 1.58 NaN NaN NaN NaN NaN 0.47 0.47 0.47
min NaN 11101.00 NaN 1.00 1.00 2011.00 NaN NaN NaN NaN NaN 0.00 0.00 0.00
25% NaN 11219.00 NaN 2.00 3.00 2012.00 NaN NaN NaN NaN NaN 0.00 0.00 0.00
50% NaN 11337.00 NaN 4.00 6.00 2013.00 NaN NaN NaN NaN NaN 0.00 0.00 0.00
75% NaN 11502.00 NaN 6.00 9.00 2015.00 NaN NaN NaN NaN NaN 1.00 1.00 1.00
max NaN 11621.00 NaN 7.00 12.00 2016.00 NaN NaN NaN NaN NaN 1.00 1.00 1.00

Se obtiene que:

  • El calendario contiene todas las fechas, días de la semana y meses relevantes, además de indicadores binarios.
  • Solo hay 5 filas no NA en la columna event_name_2; es decir, solo 5 (de 1969) instancias con más de un evento en un día determinado.
Código
train.isna().sum()
id          0
item_id     0
dept_id     0
cat_id      0
store_id    0
           ..
d_1909      0
d_1910      0
d_1911      0
d_1912      0
d_1913      0
Length: 1919, dtype: int64
Código
from matplotlib.ticker import PercentFormatter

df = train.loc[:, ~train.columns.str.contains('id')]
df = df.replace(0, np.nan)
df_na = df.isna().copy()
df_na['sum'] = df_na.sum(axis=1)
df_na['mean'] = df_na['sum'] / df_na.shape[1]
bar = df_na[['sum', 'mean']]

plt.figure(figsize=(8, 6))
sns.kdeplot(bar['mean'], fill=True, color='blue')
plt.gca().xaxis.set_major_formatter(PercentFormatter(1))  # eje x como %
plt.xlim(0, 1)
plt.gca().set_yticklabels([])
plt.title("Densidad del porcentaje de valores cero - todas las series temporales")
plt.xlabel("")
plt.ylabel("")
plt.tight_layout()
plt.show()

Esto significa que solo una minoría de las series temporales tienen menos del 50% de valores cero. El pico está bastante cerca del 100%.

4 Análisis de ventas: Gráficos interactivos de series temporales

Se realizará una exploración visual para analizar varios gráficos de series temporales en diferentes niveles de agregación empleando funciones auxiliares: cols_d identifica las columnas cuyo nombre comienza con “d_”, extract_ts transforma el dataframe ancho en formato largo asignando a cada valor de ventas su fecha real a partir de MIN_DATE y normaliza los identificadores eliminando sufijos, agg_wide agrupa y suma las columnas de días según las dimensiones indicadas renombrando la última agrupación como id, y set_monthly utiliza estas transformaciones para agregar ventas por año y mes, conservar solo el primer día de cada periodo y descartar el último mes incompleto, dejando así listo el dataset para crear gráficos temporales interactivos.

Código
# constante para fecha mínima
MIN_DATE = datetime(2011, 1, 29)

def cols_d(df):
    """Devuelve las columnas cuya etiqueta empieza con 'd_'"""
    return [c for c in df.columns if c.startswith('d_')]

def extract_ts(df):
    """
    Convierte un dataframe ancho (columnas d_1, d_2, …) en formato largo con columnas:
      - id: identificador de la serie (sin sufijo "_validation")
      - dates: fecha real
      - sales: valor de ventas
    """
    df = df.copy()
    # conservar id y columnas de días
    df = df[['id'] + cols_d(df)]
    # pivot largo
    ts = df.melt(id_vars=['id'], var_name='day', value_name='sales')
    # convertir índice de día en entero
    ts['day'] = ts['day'].str.removeprefix('d_').astype(int)
    # calcular fecha real
    ts['dates'] = ts['day'].apply(lambda x: MIN_DATE + timedelta(days=x - 1))
    ts.drop(columns='day', inplace=True)
    # limpiar sufijo
    ts['id'] = ts['id'].astype(str).str.replace('_validation', '')
    return ts

def agg_wide(df, group_cols):
    """
    Agrega un dataframe ancho sumando las columnas d_* según group_cols,
    renombrando la última columna de agrupación como 'id'.
    """
    agg = (
        df
        .groupby(group_cols)[cols_d(df)]
        .sum()
        .reset_index()
    )
    agg = agg.rename(columns={group_cols[-1]: 'id'})
    return agg

def set_monthly(df):
    """
    Toma un dataframe ancho con columna 'id', transforma a largo,
    agrega por año y mes, filtra sólo primer día y elimina mes incompleto.
    """
    ts = extract_ts(df)
    ts['month'] = ts['dates'].dt.month
    ts['year'] = ts['dates'].dt.year
    monthly = (
        ts
        .groupby(['year', 'month', 'id'], as_index=False)
        .agg(
            sales=('sales', 'sum'),
            dates=('dates', 'min')
        )
    )
    monthly = monthly[monthly['dates'].dt.day == 1]
    last = monthly['dates'].max()
    monthly = monthly[monthly['dates'] != last]
    return monthly

4.1 Todas las ventas agregadas

En primer lugar, se presenta la serie temporal agregada de todos los artículos, tiendas, categorías, departamentos y ventas.

Código
# ventas totales agregadas
agg = train.sum().to_frame('sales').T
agg['id'] = 'total'
ts_agg = extract_ts(agg)

fig = px.line(
    ts_agg,
    x='dates',
    y='sales',
    title='Ventas agregadas',
    labels={'dates': 'Fecha', 'sales': 'Ventas'}
)
fig.update_layout(template='plotly_white')
fig.show()

Se observa lo siguiente:

  • En general, las ventas van en aumento, lo que parece positivo para Walmart. Se nota un patrón anual claro, con una caída en Navidad, el único día en que las tiendas permanecen cerradas.

  • Las ventas más recientes de 2016 muestran un crecimiento algo más rápido que en los años anteriores.

4.2 Ventas por estado

Ahora, se analizarán las ventas por estado a nivel de agregación mensual.

Código
# ventas mensuales por estado
stt = agg_wide(train, ['state_id'])
ts_stt = set_monthly(stt)

fig = px.line(
    ts_stt,
    x='dates',
    y='sales',
    color='id',
    title='Ventas mensuales por estado',
    labels={'dates': 'Fecha', 'sales': 'Ventas', 'id': 'Estado'}
)
fig.update_layout(template='plotly_white')
fig.show()

Se observa lo siguiente:

  • California (CA) registra la mayor cantidad de artículos vendidos en general, mientras que Wisconsin (WI) fue acercándose gradualmente a Texas (TX) hasta superarlo en los últimos meses de los datos de entrenamiento.

  • CA presentó caídas marcadas en 2013 y 2015, las cuales también se perciben en los demás estados, aunque con menor intensidad. Estos descensos y picos no ocurren de manera constante (por ejemplo, no se aprecian en 2012), pero podrían reflejar principalmente el patrón anual previamente identificado.

4.3 Ventas por categoría y tienda

El conjunto de datos incluye 10 tiendas: 4 en California, 3 en Texas y 3 en Wisconsin, así como 3 categorías: FOODS (alimentos), HOBBIES (pasatiempos) y HOUSEHOLD (hogar). Se utilizarán niveles de agregación mensuales para mantener las gráficas claras.

Código
# ventas mensuales por categoría
cat = agg_wide(train, ['cat_id'])
cat_monthly = set_monthly(cat)

fig = px.line(
    cat_monthly,
    x='dates',
    y='sales',
    color='id',
    title='Ventas por categoría',
    labels={'dates': 'Fecha', 'sales': 'Ventas', 'id': 'Categoría'}
)
fig.update_layout(template='plotly_white')
fig.show()
Código
# conteo de ventas por categoría
counts = train['cat_id'].value_counts().reset_index()
counts.columns = ['id','n']

fig = px.bar(
    counts,
    x='id',
    y='n',
    color='id',
    title='Ventas por categoría',
    labels={'id': 'Categoría', 'n': 'Conteo'}
)
fig.update_layout(template='plotly_white')
fig.update_xaxes(tickfont=dict(size=7))
fig.show()
Código
sto = agg_wide(train, ['store_id'])
store_monthly = set_monthly(sto)
# extraer estado de la id de tienda
store_monthly['state_id'] = store_monthly['id'].str.slice(0, 2)

fig = px.line(
    store_monthly,
    x='dates',
    y='sales',
    color='id',
    facet_col='state_id',
    facet_col_wrap=3,
    title='Ventas por tienda',
    labels={'dates': 'Fecha', 'sales': 'Ventas', 'id': 'Tienda', 'state_id': 'Estado'}
)
fig.update_layout(
    template='plotly_white',
    legend_title_text='Tienda',
    legend_orientation='h',
    legend_y=-0.2
)
fig.update_xaxes(title=None)
fig.show()

Se observa lo siguiente:

  • La categoría FOODS es la más frecuente, seguida de HOUSEHOLD, que se encuentra claramente por encima de HOBBIES. El número de registros de HOUSEHOLD se aproxima más al de FOODS que las cifras de ventas correspondientes, lo que sugiere que se venden más unidades de FOODS que de HOUSEHOLD.

  • En cuanto a las tiendas, las ubicadas en Texas muestran ventas bastante similares entre sí; TX_3 pasa de niveles comparables a TX_1 hasta alcanzar los de TX_2 a lo largo del período analizado. Las tiendas de Wisconsin WI_1 y WI_2 presentan un notable aumento en las ventas en 2012, mientras que WI_3 muestra una caída sostenida durante varios años.

  • Las tiendas de California exhiben un volumen de ventas relativamente uniforme. Destaca CA_2, que desciende al nivel de CA_4 en 2015 y posteriormente se recupera, alcanzando las ventas de CA_1 hacia finales del año.

4.4 Ventas por departamento

Los datos incluyen 7 departamentos: 3 en la categoría FOODS y 2 en cada una de las categorías HOBBIES y HOUSEHOLD. Junto con los 3 estados, estos niveles suman un total de 21 combinaciones.

Código
# ventas mensuales por departamento y estado
dept = (
    train
    .groupby(['dept_id', 'state_id'])[cols_d(train)]
    .sum()
    .reset_index()
)

df_dept = dept[['dept_id', 'state_id'] + cols_d(dept)]
ts_dept = df_dept.melt(id_vars=['dept_id', 'state_id'], var_name='day', value_name='sales')

ts_dept['day'] = ts_dept['day'].str.removeprefix('d_').astype(int)
ts_dept['dates'] = ts_dept['day'].apply(lambda d: MIN_DATE + timedelta(days=d - 1))
# agregar mes/año
ts_dept['month'] = ts_dept['dates'].dt.month
ts_dept['year'] = ts_dept['dates'].dt.year

dept_monthly = (
    ts_dept
    .groupby(['year', 'month', 'dept_id', 'state_id'], as_index=False)
    .agg(
        sales=('sales', 'sum'),
        dates=('dates', 'min')
    )
)
# filtrar primer día y quitar mes incompleto
dept_monthly = dept_monthly[dept_monthly['dates'].dt.day == 1]
last = dept_monthly['dates'].max()
dept_monthly = dept_monthly[dept_monthly['dates'] != last]

# gráfica por depto y estado
fig = px.line(
    dept_monthly,
    x='dates',
    y='sales',
    color='dept_id',
    facet_row='state_id',
    facet_col='dept_id',
    title='Ventas por departamento y estado',
    labels={
        'dates': 'Fecha',
        'sales': 'Ventas',
        'dept_id': 'Depto',
        'state_id': 'Estado'
    }
)
fig.update_layout(template='plotly_white', showlegend=False)
fig.update_annotations(font_size=8)
fig.update_xaxes(title=None)
fig.update_yaxes(title=None)
fig.show()

Se observa lo siguiente:

  • FOODS_3 concentra claramente la mayor parte de las ventas dentro de la categoría FOODS en todos los estados. FOODS_2 muestra un ligero incremento hacia el final del período, especialmente en Wisconsin.

  • De manera similar, HOUSEHOLD_1 supera con claridad a HOUSEHOLD_2 en volumen de ventas. Por su parte, HOBBIES_1 mantiene un nivel promedio de ventas superior al de HOBBIES_2, aunque en ambos casos no se aprecia un cambio significativo a lo largo del tiempo.

5 Análisis adicional: Calendario y precio de los productos

En esta sección se analizan en las dos variables adicionales proporcionadas: los precios de los productos y los eventos del calendario.

5.1 Calendario

En la Sección 3 se aprecia que el dataframe calendar incluye características básicas como día de la semana (columna weekday en formato de texto y wday en formato numérico), mes, año y, por supuesto, fecha. Junto a la fecha aparece también la columna d, que vincula cada fecha con los nombres de columna en los datos de entrenamiento.

El resto de los atributos están relacionados con eventos y con cupones de asistencia alimentaria:

  • Al revisar la Sección 3, se observa que las columnas event_name_2 y event_type_2 solo tienen datos en cinco filas (el resto son valores ausentes). Por eso, este análisis se centrará únicamente en las columnas event_name_1 y event_type_1.

  • El acrónimo SNAP corresponde a “Supplemental Nutrition Assistance Program” (programa federal de asistencia nutricional). Según su sitio web:

    “El programa SNAP es el mayor programa federal de asistencia nutricional. Proporciona beneficios a personas y familias de bajos ingresos mediante una tarjeta de transferencia electrónica de beneficios, que puede usarse como una tarjeta de débito para adquirir alimentos autorizados en establecimientos de venta al por menor.”

Código
# días con eventos vs sin eventos
events = (
    calendar
    .assign(event=lambda df: ~df['event_type_1'].isna())
    .groupby('event')
    .size()
    .reset_index(name='count')
)
events['total'] = events['count'].sum()
events['perc'] = events['count'] / events['total']

fig = px.bar(
    events,
    x='event',
    y='perc',
    title='Días con eventos',
    labels={'event': 'Evento', 'perc': 'Porcentaje'},
    color='event'
)
fig.update_layout(template='plotly_white', showlegend=False)
fig.update_xaxes(title=None)
fig.update_yaxes(title=None, tickformat='.0%')
fig.show()
Código
# tipos de eventos
tps = (
    calendar
    .dropna(subset=['event_type_1'])
    .groupby('event_type_1')
    .size()
    .reset_index(name='count')
)
tps['total'] = tps['count'].sum()
tps['perc'] = tps['count'] / tps['total']

label_map = {
    'Religious': 'Religioso',
    'National': 'Nacional',
    'Cultural': 'Cultural',
    'Sporting': 'Deportivo'
}
tps['event_type_1'] = tps['event_type_1'].map(label_map)

fig = px.pie(
    tps,
    names='event_type_1',  
    values='perc',         
    title='Tipos de eventos',
    hole=0,
    labels={'event_type_1':'Evento','perc':'Porcentaje'}
)
fig.update_traces(
    texttemplate='%{label}: %{percent:.0%}',
    hovertemplate='%{label}: %{percent:.0%}'
)
fig.show()
Código
# días con SNAP por estado
snap_cols = [col for col in calendar.columns if col.startswith('snap_')]

snp = (
    calendar
    .melt(id_vars=['date'], value_vars=snap_cols, var_name='state', value_name='snap')
    .assign(state=lambda df: df['state'].str[-2:])  
    .assign(snap=lambda df: df['snap'].astype(bool))  
    .groupby(['state', 'snap'])
    .size()
    .reset_index(name='count')
)
snp['total'] = snp.groupby('state')['count'].transform('sum')
snp['perc'] = snp['count'] / snp['total']

fig = px.bar(
    snp,
    x='snap',
    y='perc',
    facet_col='state',
    facet_col_wrap=3,
    color='snap',
    title='Días con compras SNAP por estado',
    labels={'snap': 'SNAP', 'perc': 'Porcentaje', 'state': 'Estado'}
)
fig.update_yaxes(title=None, tickformat='.0%')
fig.update_xaxes(title=None)
fig.update_traces(
    hovertemplate='SNAP: %{x}<br>' + 'Porcentaje: %{y:.0%}<extra></extra>'
)
fig.update_layout(template='plotly_white', showlegend=False)
fig.show()

Se encuentra lo siguiente:

  • En el calendario, alrededor del 8% de los días registra un evento especial. De esos días, aproximadamente un tercio corresponde a celebraciones religiosas y otro tercio a festividades nacionales; el tercio restante se divide en dos tercios de eventos culturales y un tercio de eventos deportivos.
  • El porcentaje de días en que las tiendas Walmart aceptan cupones SNAP es idéntico para California, Texas y Wisconsin: 650 días, lo que equivale al 33%.

5.2 Precio de los productos

Se dispone de información detallada sobre los precios de los productos, incluyendo sus ID de categoría, departamento y tienda (que a su vez incluye el ID de estado). Los precios se presentan como promedios semanales, y la variable wm_yr_wk permite vincular cada semana con su fecha correspondiente a través de la columna de calendario del mismo nombre.

Para analizar los precios promedio de los productos por categoría y departamento entre los años 2011 y 2016, se empleó el siguiente proceso:

Código
item_info = train[['item_id', 'cat_id', 'dept_id']].drop_duplicates()
df = prices.merge(calendar[['wm_yr_wk', 'year']], on='wm_yr_wk', how='left')
df = df.merge(item_info, on='item_id', how='left')

df_group = (
    df.groupby(['year', 'cat_id', 'dept_id'])['sell_price']
    .mean()
    .reset_index()
)
Código
foods = df_group[df_group['cat_id'] == 'FOODS']

fig = px.line(
    foods,
    x='year',
    y='sell_price',
    color='dept_id',
    markers=True,
    title='Evolución del precio promedio',
    labels={'sell_price': 'Precio promedio', 'year': 'Año', 'dept_id': 'Departamento'},
    template='plotly_white'
)

fig.show()
Código
hobbies = df_group[df_group['cat_id'] == 'HOBBIES']

fig = px.line(
    hobbies,
    x='year',
    y='sell_price',
    color='dept_id',
    markers=True,
    title='Evolución del precio promedio',
    labels={'sell_price': 'Precio promedio', 'year': 'Año', 'dept_id': 'Departamento'},
    template='plotly_white'
)

fig.show()
Código
household = df_group[df_group['cat_id'] == 'HOUSEHOLD']

fig = px.line(
    household,
    x='year',
    y='sell_price',
    color='dept_id',
    markers=True,
    title='Evolución del precio promedio',
    labels={'sell_price': 'Precio promedio', 'year': 'Año', 'dept_id': 'Departamento'},
    template='plotly_white'
)

fig.show()

Se observa lo siguiente:

En términos generales, los precios promedio se mantienen relativamente estables a lo largo de los años, con incrementos graduales que podrían atribuirse a la inflación.

  • En la categoría FOODS, se identifican las siguientes tendencias:
    • En FOODS_1, el precio promedio fluctúa entre 3.3 y 3.4, mostrando estabilidad con leves variaciones a lo largo del período.
    • En FOODS_2, se observa un aumento constante desde 3.8 en 2011 hasta 4.2 en 2016, reflejando una tendencia de alza clara.
    • En FOODS_3, el precio permanece estable en 2.8 hasta 2013, con un ligero incremento a 2.9 hacia 2016.
  • En la categoría HOBBIES, se observan las siguientes dinámicas:
    • En HOBBIES_1, el precio crece de manera sostenida desde 5.2 en 2011 hasta 6.6 en 2016, mostrando un incremento notable.
    • En HOBBIES_2, el precio desciende de 2.8 hasta 2.5 a lo largo de todo el período, con una tendencia descendiente.
  • En la categoría HOUSEHOLD, se destacan los siguientes patrones:
    • En HOUSEHOLD_1, el precio se mantiene en torno a 4.9 hasta 2013, con un aumento gradual a 5.1 en 2016.
    • En HOUSEHOLD_2, el precio disminuye de 6.1 en 2011 a 5.7 en 2016, indicando una tendencia descendente leve pero constante.