Inteligencia de Retail: Análisis de Ventas en Myanmar

Explorando patrones de consumo y rendimiento comercial mediante Python

Python
EDA
Plotly
Retail
Un análisis profundo sobre el comportamiento del consumidor en tres sucursales de Myanmar, enfocado en la optimización de inventarios y marketing.
Author

LC Pallares

Published

April 23, 2025

Modified

January 6, 2026

NoteObjetivo del Análisis

Identificar los motores de ingresos y el comportamiento de compra para proponer estrategias basadas en datos que mejoren la rentabilidad de la cadena de supermercados.

📝 Resumen del Proyecto

Este análisis profundiza en las operaciones de una cadena de supermercados en Myanmar, utilizando un conjunto de datos que detalla transacciones históricas en tres ciudades principales. El objetivo es transformar datos crudos en inteligencia de negocios para identificar motores de ingresos y entender las dinámicas del consumidor local.

NoteEl Dataset

El conjunto de datos abarca variables clave como categorías de productos, demografía del cliente, métodos de pago y métricas financieras (margen bruto e impuestos), permitiendo una visión 360° de la dinámica del retail.

🎯 Objetivos Principales

  • Optimización Comercial: Identificar las líneas de productos con mayor rendimiento.
  • Inteligencia del Cliente: Analizar patrones de compra según demografía y tipo de membresía.
  • Análisis Temporal: Detectar picos de demanda para mejorar la planificación operativa.

🛠️ Metodología

Para este análisis, implementamos un pipeline de datos moderno que prioriza la velocidad y el rigor estadístico, estructurado en tres niveles:

1. Ingesta y Limpieza (Pandas)

  • Normalización: Renombramos el esquema a snake_case y ajustamos tipos de datos (fechas y categorías) para asegurar la integridad de los cálculos.
  • Curación: Tratamiento de nulos y validación de rangos en precios y cantidades.

2. Motor de Consultas (DuckDB)

  • Eficiencia: Utilizamos DuckDB como motor analítico in-process. Esto nos permite ejecutar consultas SQL complejas (como el Market Basket Analysis) directamente sobre los DataFrames de Pandas, logrando una ejecución mucho más rápida que los métodos tradicionales.

3. Validación y Visualización (Scipy & Plotly)

  • Rigor: Aplicamos pruebas de hipótesis (T-Tests) para confirmar que los hallazgos no son producto del azar.
  • Interactividad: Traducimos los resultados en gráficos dinámicos que permiten explorar dimensiones de tiempo, ciudad y producto.
TipEl Diferenciador Técnico

La combinación de Pandas + DuckDB permite que este flujo de trabajo sea escalable: la flexibilidad de Python con la potencia de un motor SQL diseñado para analítica masiva.

Ver código
import pandas as pd

# Carga y normalización de columnas a snake_case
df = pd.read_csv("../data/supermarket_sales.csv")
# df.columns = [c.lower().replace(' ', '_') for c in df.columns]

# Conversión de tipos
# df['date'] = pd.to_datetime(df['date'])
df.head(3)
Invoice ID Branch City Customer type Gender Product line Unit price Quantity Tax 5% Total Date Time Payment Cost of goods sold Gross margin percentage Gross income Customer stratification rating
0 750-67-8428 A Yangon Member Female Health and beauty 74.69 7 26.1415 548.9715 1/5/2019 13:08 Ewallet 522.83 4.761905 26.1415 9.1
1 226-31-3081 C Naypyitaw Normal Female Electronic accessories 15.28 5 3.8200 80.2200 3/8/2019 10:29 Cash 76.40 4.761905 3.8200 9.6
2 631-41-3108 A Yangon Normal Male Home and lifestyle 46.33 7 16.2155 340.5255 3/3/2019 13:23 Credit card 324.31 4.761905 16.2155 7.4
Ver código
import duckdb

# Conexión in-memory y registro del DataFrame existente
con = duckdb.connect()
con.register('sales', df)

# Verificación de la tabla mediante SQL
con.execute("SELECT * FROM sales LIMIT 3").fetchdf()
Invoice ID Branch City Customer type Gender Product line Unit price Quantity Tax 5% Total Date Time Payment Cost of goods sold Gross margin percentage Gross income Customer stratification rating
0 750-67-8428 A Yangon Member Female Health and beauty 74.69 7 26.1415 548.9715 1/5/2019 13:08 Ewallet 522.83 4.761905 26.1415 9.1
1 226-31-3081 C Naypyitaw Normal Female Electronic accessories 15.28 5 3.8200 80.2200 3/8/2019 10:29 Cash 76.40 4.761905 3.8200 9.6
2 631-41-3108 A Yangon Normal Male Home and lifestyle 46.33 7 16.2155 340.5255 3/3/2019 13:23 Credit card 324.31 4.761905 16.2155 7.4

🛠️ Parte 1: Pipeline de Limpieza y Estandarización

La base de datos original (supermarket_sales.csv) presentaba inconsistencias típicas de registros transaccionales crudos. Para transformar este archivo en un activo analítico confiable, ejecutamos un proceso de curación estructurado:

  1. Validación Temporal: Conversión de fechas a objetos datetime para habilitar análisis de series de tiempo.
  2. Tratamiento de Integridad: Eliminación de registros con valores nulos en columnas críticas (unit_price, quantity) y filtrado de errores operativos (cantidades menores o iguales a cero).
  3. Enriquecimiento (Feature Engineering): Extracción de dimensiones temporales como el nombre del día y el mes para identificar patrones estacionales.

Implementación Técnica

A continuación, comparamos cómo abordar este flujo de limpieza utilizando la manipulación procedimental de Pandas frente a la potencia declarativa de DuckDB:

Ver código
import pandas as pd
import numpy as np

# Cargar y estandarizar formatos
# df = pd.read_csv("../data/supermarket_sales.csv")
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
df['Product line'] = df['Product line'].str.title()

# Manejo de valores faltantes y corrección de errores
df = df.dropna(subset=['Unit price', 'Quantity'])
df = df[df['Quantity'] > 0]

# Enriquecimiento de columnas
df['Day of Week'] = df['Date'].dt.day_name()
df['Month'] = df['Date'].dt.month_name()

df[['Invoice ID', 'Date', 'Day of Week', 'Month']].head(3)
Invoice ID Date Day of Week Month
0 750-67-8428 2019-01-05 Saturday January
1 226-31-3081 2019-03-08 Friday March
2 631-41-3108 2019-03-03 Sunday March
Ver código
import duckdb

# Conectamos DuckDB al DataFrame cargado
con = duckdb.connect()
con.register('raw_data', df)

# Replicamos la lógica de limpieza mediante SQL
query = """
SELECT 
    *,
    dayname(Date) AS Day_of_Week,
    monthname(Date) AS Month
FROM raw_data
WHERE "Unit price" IS NOT NULL 
  AND Quantity > 0
LIMIT 3
"""
con.execute(query).fetchdf()
Invoice ID Branch City Customer type Gender Product line Unit price Quantity Tax 5% Total ... Time Payment Cost of goods sold Gross margin percentage Gross income Customer stratification rating Day of Week Month Day_of_Week Month_1
0 750-67-8428 A Yangon Member Female Health And Beauty 74.69 7 26.1415 548.9715 ... 13:08 Ewallet 522.83 4.761905 26.1415 9.1 Saturday January Saturday January
1 226-31-3081 C Naypyitaw Normal Female Electronic Accessories 15.28 5 3.8200 80.2200 ... 10:29 Cash 76.40 4.761905 3.8200 9.6 Friday March Friday March
2 631-41-3108 A Yangon Normal Male Home And Lifestyle 46.33 7 16.2155 340.5255 ... 13:23 Credit card 324.31 4.761905 16.2155 7.4 Sunday March Sunday March

3 rows × 21 columns

NoteResultado del Proceso

Este pipeline garantiza un conjunto de datos consistente, almacenado internamente para las fases de visualización. La estandarización de las categorías (ej. Title Case) asegura que los reportes finales mantengan una estética profesional y uniforme.

xxxxxxxxxxxxxxxxxxxxxxxxxx

Parte 2: Análisis Exploratorio y Visualizaciones

🔢 Parte 2.1: Estadísticas Descriptivas

Antes de profundizar en patrones visuales, es fundamental auditar las métricas de tendencia central y dispersión. Este paso nos permite identificar el “comportamiento promedio” de las transacciones y detectar posibles valores atípicos que puedan sesgar el análisis.

Perfil Estadístico del Inventario y Ventas

Analizamos variables críticas como el precio unitario, la cantidad de artículos por ticket y el margen bruto.

La función .describe() de Pandas es la forma más rápida de obtener un panorama completo, incluyendo cuartiles y desviación estándar.

Ver código
# Seleccionamos las columnas numéricas clave
metrics = ['Unit price', 'Quantity', 'Tax 5%', 'Total', 'Gross income']

# Generamos el resumen y aplicamos estilo para mejorar la lectura
stats_summary = df[metrics].describe().T

# Formateo estético para el reporte
stats_summary.style.background_gradient(cmap='Blues').format("{:.2f}")
  count mean std min 25% 50% 75% max
Unit price 1000.00 55.67 26.49 10.08 32.88 55.23 77.94 99.96
Quantity 1000.00 5.51 2.92 1.00 3.00 5.00 8.00 10.00
Tax 5% 1000.00 15.38 11.71 0.51 5.92 12.09 22.45 49.65
Total 1000.00 322.97 245.89 10.68 124.42 253.85 471.35 1042.65
Gross income 1000.00 15.38 11.71 0.51 5.92 12.09 22.45 49.65

Con SQL, tenemos un control total sobre qué métricas calcular, permitiendo una visión personalizada de la eficiencia operativa.

Ver código
query_stats = """
SELECT 
    AVG("Unit price") AS avg_price,
    MIN("Unit price") AS min_price,
    MAX("Unit price") AS max_price,
    AVG(Quantity) AS avg_quantity,
    SUM(Total) AS total_revenue,
    AVG("gross income") AS avg_income
FROM df
"""
con.execute(query_stats).fetchdf()
avg_price min_price max_price avg_quantity total_revenue avg_income
0 55.67213 10.08 99.96 5.51 322966.749 15.379369

Hallazgos de la Auditoría Numérica

Al observar las tablas anteriores, podemos extraer conclusiones inmediatas sobre la operación:

  • Ticket Promedio: La media de las ventas se sitúa cerca de los 322.97 MMK, con una desviación estándar considerable, lo que indica una alta heterogeneidad en el tamaño de las compras.
  • Volumen de Artículos: En promedio, los clientes adquieren 5.5 unidades por transacción, con un rango que va desde 1 hasta 10 artículos.
  • Margen de Beneficio: El ingreso bruto promedio por ticket es de aproximadamente 15.38 MMK, manteniendo una relación constante con el impuesto aplicado.
NoteObservación sobre el Margen

El gross margin percentage se mantiene constante en el 4.76% para todos los registros. Esto indica una política de precios centralizada donde el margen no varía por categoría de producto, sino que depende estrictamente del volumen de venta.

📊 Parte 2.2: Visualización y Tendencias de Venta

En esta etapa, transformamos los datos en activos visuales interactivos. El objetivo es identificar patrones de consumo que las tablas estáticas podrían ocultar. Utilizaremos Plotly para la capa de presentación por su alta interactividad.

1. Ventas por Linea de Producto

Este análisis identifica qué líneas de producto son los motores de ingresos, permitiendo priorizar esfuerzos de inventario y marketing.

Ver código
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go

# Generamos la variable con Pandas para la gráfica
sales_by_category = df.groupby('Product line')['Total'].sum().sort_values(ascending=False).reset_index()

fig = px.bar(sales_by_category, x='Total', y='Product line', 
             orientation='h',
             title='Ventas Totales por Categoría de Producto',
             labels={'Total': 'Ventas Totales (MMK)', 'Product line': 'Categoría'},
             color='Total', color_continuous_scale='Viridis')

fig.update_layout(showlegend=False, yaxis={'categoryorder':'total ascending'})
fig.show()
Figure 1
Ver código
# Así se obtendría la misma variable usando lógica SQL
query_line = """
SELECT 
    "Product line", 
    SUM(Total) as total_sales
FROM df
GROUP BY 1
ORDER BY total_sales DESC
"""
res_line = con.execute(query_cat).fetchdf()

Figura: Ventas Totales por Linea de Producto. Las barras representan el monto total de ventas (en MMK).

2. Tendencias por Día de la Semana

Analizamos las ventas promedio según el día de la semana para detectar picos de actividad y optimizar la gestión del personal operativo.

Ver código
# Generamos la variable con Pandas
order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
sales_by_day = df.groupby('Day of Week')['Total'].mean().reindex(order).reset_index()

fig = px.line(sales_by_day, x='Day of Week', y='Total', 
              title='Ventas Promedio por Día de la Semana',
              labels={'Total': 'Ventas Promedio (MMK)', 'Day of Week': 'Día'},
              markers=True)

fig.update_traces(line_color='teal', line_width=3)
fig.show()
Figure 2
Ver código
# Lógica SQL para promedios con ordenamiento cronológico manual
query_day = """
SELECT 
    "Day of Week", 
    AVG(Total) as avg_sales
FROM df
GROUP BY 1
ORDER BY CASE 
    WHEN "Day of Week" = 'Monday' THEN 1
    WHEN "Day of Week" = 'Tuesday' THEN 2
    WHEN "Day of Week" = 'Wednesday' THEN 3
    WHEN "Day of Week" = 'Thursday' THEN 4
    WHEN "Day of Week" = 'Friday' THEN 5
    WHEN "Day of Week" = 'Saturday' THEN 6
    WHEN "Day of Week" = 'Sunday' THEN 7
END
"""
res_day = con.execute(query_day).fetchdf()
NoteHallazgo Estratégico

El incremento en las ventas promedio durante el sábado y domingo valida una oportunidad para implementar campañas de fin de semana y ajustar los turnos de reposición de stock para evitar quiebres de inventario.

Figura: Ventas Promedio por Día de la Semana. La línea muestra el promedio de ventas (en MMK) para cada día, con picos en fines de semana.

3. Evolución de Ventas Diarias

El monitoreo de la serie de tiempo es vital para detectar anomalías, tendencias o picos de demanda estacionales. La interactividad de Plotly nos permite navegar por periodos específicos y analizar la volatilidad del flujo de caja diario.

Ver código
# Agregación con Pandas para la serie temporal
ventas_diarias = df.groupby('Date')['Total'].sum().reset_index()

# Crear el gráfico de línea interactivo
fig_ventas_diarias = px.line(
    ventas_diarias, 
    x='Date', 
    y='Total', 
    title='Evolución de Ventas Diarias',
    labels={'Total': 'Ventas Totales (MMK)', 'Date': 'Fecha'},
    template='plotly_white'
)

# Optimizamos el eje X para asegurar el orden y añadir navegación
fig_ventas_diarias.update_xaxes(type='category', rangeslider_visible=True)
fig_ventas_diarias.show()
Figure 3
Ver código
# Así se obtendría la misma serie temporal usando lógica SQL
query_series = """
SELECT 
    Date, 
    SUM(Total) as Total
FROM df
GROUP BY Date
ORDER BY Date
"""
res_series = con.execute(query_series).fetchdf()
NoteAnálisis de Continuidad

La serie de tiempo revela un comportamiento cíclico sin una tendencia de crecimiento lineal evidente en el corto plazo. Esto sugiere que el supermercado opera en un mercado maduro donde las ventas dependen más de factores estacionales (días de la semana o quincenas) que de una expansión orgánica acelerada durante este periodo.


4. Desempeño por Ciudad y Sucursal

Este análisis identifica la eficiencia de los nodos geográficos. Comparamos las tres ciudades principales para entender si el volumen de ventas está centralizado o distribuido equitativamente.

Ver código
# Agrupación con Pandas para comparar sucursales por ciudad
city_sales = df.groupby(['City', 'Branch'])['Total'].sum().reset_index()

fig_city = px.bar(
    city_sales, 
    x='City', 
    y='Total', 
    color='Branch',
    barmode='group',
    title='Ventas Totales por Ciudad y Sucursal',
    labels={'Total': 'Ventas Totales (MMK)', 'City': 'Ciudad'},
    template='plotly_white',
    color_discrete_sequence=px.colors.qualitative.Prism
)

fig_city.show()
Figure 4
Ver código
# Agregación multi-nivel en SQL
query_city = """
SELECT 
    City, 
    Branch, 
    SUM(Total) as total_sales
FROM df
GROUP BY City, Branch
ORDER BY total_sales DESC
"""
res_city = con.execute(query_city).fetchdf()
TipInsight Geográfico

A pesar de las diferencias demográficas entre las ciudades, el volumen de ingresos se mantiene notablemente similar entre las sucursales, lo que sugiere una estandarización exitosa del modelo de negocio en las diferentes regiones.


5. Análisis de Métodos de Pago y Preferencias

El último eslabón de nuestro análisis descriptivo es entender cómo interactúan los clientes con el punto de venta. Identificar el método de pago preferido es crucial para negociar comisiones bancarias y optimizar la experiencia de usuario en caja.

Analizamos si existe una brecha digital o de comportamiento entre géneros respecto al uso de billeteras electrónicas, efectivo o tarjetas de crédito.

Ver código
# Generamos la variable con Pandas
payment_data = df.groupby(['Payment', 'Gender'])['Total'].sum().reset_index()

# Crear gráfico de barras agrupadas
fig_pay = px.bar(
    payment_data, 
    x='Payment', 
    y='Total', 
    color='Gender',
    barmode='group',
    title='Uso de Métodos de Pago según Género',
    labels={'Total': 'Ingresos Totales (MMK)', 'Payment': 'Método de Pago'},
    template='plotly_white',
    color_discrete_map={'Female': '#EF553B', 'Male': '#636EFA'}
)

fig_pay.show()
Figure 5
Ver código
# Consulta para cruzar métodos de pago y género
query_pay = """
SELECT 
    Payment, 
    Gender, 
    SUM(Total) as total_revenue
FROM df
GROUP BY Payment, Gender
ORDER BY Payment, total_revenue DESC
"""
res_pay = con.execute(query_pay).fetchdf()
NoteConclusión de la Fase Descriptiva

A diferencia de otros mercados donde predomina el efectivo, aquí observamos un uso equilibrado de E-wallet. Esto abre la puerta a integrar programas de fidelización digitales que capturen datos de comportamiento en tiempo real.


Hallazgos Preliminares del Análisis Exploratorio

Tras auditar las dimensiones clave del negocio, los datos revelan patrones críticos que servirán de base para las pruebas de hipótesis posteriores. Estos hallazgos permiten pasar de una descripción de “qué pasó” a una estrategia de “qué optimizar”.

  • Dominancia de Categorías: Las líneas de Alimentos (Food and Beverages) y Accesorios de Moda (Fashion Accessories) se posicionan como los motores de ingresos. Este comportamiento sugiere una alta rotación en productos de consumo básico, los cuales podrían utilizarse como “productos gancho” para categorías de menor rotación.
  • Ciclo de Ventas Semanal: Existe un incremento estadístico visual en el ticket promedio durante el sábado y domingo. Este patrón justifica una planificación de personal más robusta para el fin de semana y la ejecución de campañas de marketing tipo “Weekend Sale”.
  • Eficiencia Geográfica: La paridad de ventas entre ciudades indica que la marca tiene una penetración de mercado madura y uniforme. No obstante, las sucursales en Naypyitaw muestran una consistencia operativa que podría servir de modelo para los procesos en Yangon.
  • Optimización de Inventario: Se han identificado segmentos con bajo rendimiento en volumen (como la línea de Health and Beauty en ciertas horas del día). Esto representa una oportunidad directa para ajustar los niveles de stock y reducir los costos de mantenimiento de inventario.
TipSiguiente Paso: Validación Científica

Los hallazgos anteriores son observaciones basadas en datos. En la siguiente sección, utilizaremos Inferencia Estadística para determinar si estas diferencias (por ejemplo, el aumento de ventas en fines de semana) son estadísticamente significativas o si son simplemente fruto de la variabilidad natural de los datos.


Parte 3: Próximos Pasos (En Desarrollo)

En las siguientes fases, planeo: - Implementar un dashboard interactivo en Streamlit para explorar las ventas por categoría, ciudad o tipo de cliente. - Realizar un análisis de correlación entre variables como género, tipo de cliente y monto de compra. - Explorar modelos predictivos para pronosticar ventas futuras.

Conclusiones Preliminares

Este análisis inicial revela patrones claros en las ventas del supermercado, destacando categorías y días clave. Las visualizaciones proporcionan una base sólida para decisiones estratégicas, como optimizar inventarios o planificar promociones. La carpeta data/ asegura que los datos sean accesibles y reutilizables para futuros análisis.

Recursos

  • Datos: supermarket_sales.csv en data/ (basado en datasets típicos de ventas, como este ejemplo).
  • Código: Disponible en el repositorio del proyecto [enlace si lo subes].
Back to top