📊 Taller de Análisis de Datos · Python

Explorando datos
con pandas,
matplotlib & seaborn

Una clase práctica y pedagógica para comprender el ciclo completo del análisis de datos: carga, limpieza, transformación, estadística y visualización.

Dataset
Amazon Sales Dataset

Registros
50,000 órdenes

Lenguaje
Python 3.x

Librerías
pandas · matplotlib · seaborn

🗺️

¿Qué vamos a hacer hoy?

En este taller analizamos un conjunto de datos de ventas de Amazon con 50,000 registros. Seguiremos el ciclo completo de análisis de datos, que se puede resumir en este flujo de trabajo:

1

📥 Cargar los datos

Importar el CSV y convertirlo en un DataFrame de pandas.

2

🔍 Explorar los datos

Seleccionar columnas, filtrar registros, entender la estructura.

3

🧹 Limpiar los datos

Eliminar columnas innecesarias y filas con valores faltantes.

4

⚙️ Transformar los datos

Crear nuevas columnas derivadas y calcular métricas útiles.

5

📐 Analizar estadísticamente

Calcular medidas de tendencia central y de dispersión.

6

📈 Visualizar

Crear gráficas que comuniquen los hallazgos de forma clara.

Las tres librerías clave

pandas

La librería principal para manipulación de datos tabulares. Provee el objeto DataFrame, que funciona como una hoja de cálculo en memoria pero con el poder de Python. Permite cargar, filtrar, transformar y agrupar datos con facilidad.

matplotlib

La librería base de visualización en Python. Con ella creamos gráficos de barras, líneas, pastel e histogramas. Es muy personalizable aunque requiere más código que seaborn para lograr resultados refinados.

seaborn

Construida sobre matplotlib, seaborn facilita la creación de gráficos estadísticos más elaborados con menos código. En este proyecto la usamos para crear el mapa de calor de correlaciones.

📥

Cargando el dataset

El primer paso de cualquier análisis es cargar los datos y hacer una primera inspección para entender con qué estamos trabajando.

mision.py
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv("amazon_sales_dataset.csv")
df["order_date"] = pd.to_datetime(df["order_date"])

print(df.head())   # primeros 5 registros
print(df.info())   # estructura y tipos de datos

¿Qué hace cada línea?

📌
pd.read_csv()

Lee el archivo CSV y lo convierte en un DataFrame: una tabla de filas y columnas que vive en memoria RAM. Es equivalente a abrir un Excel dentro de Python.

📅
pd.to_datetime()

Los CSV guardan las fechas como texto ("2022-04-13"). Esta función las convierte a tipo datetime64, lo que nos permite hacer cálculos con fechas: comparar, agrupar por mes, etc.

🔎
df.head() y df.info()

head() muestra los primeros 5 registros para ver cómo lucen los datos. info() lista todas las columnas con sus tipos de datos y cuántos valores no nulos tiene cada una — esencial para detectar datos faltantes.

Resultado obtenido

Consola → df.info()
RangeIndex: 50000 entries, 0 to 49999
Data columns (total 13 columns):
 order_id          50000 non-null  int64
 order_date        50000 non-null  datetime64[ns]
 product_id        50000 non-null  int64
 product_category  50000 non-null  object
 price             50000 non-null  float64
 discount_percent  50000 non-null  int64
 quantity_sold     50000 non-null  int64
 customer_region   50000 non-null  object
 payment_method    50000 non-null  object
 rating            50000 non-null  float64
 review_count      50000 non-null  int64
 discounted_price  50000 non-null  float64
 total_revenue     50000 non-null  float64
💡
Interpretación

El dataset tiene 50,000 filas y 13 columnas. Hay columnas numéricas (float64, int64) y de texto (object). Ninguna columna tiene valores nulos en esta etapa, lo que es un buen indicio de calidad del dataset.

50,000
Registros totales
13
Columnas originales
6
Categorías de producto
2022–23
Período temporal
🎯

Seleccionar solo lo que necesitamos

No siempre necesitamos trabajar con todas las columnas. Podemos crear una vista reducida del DataFrame eligiendo solo las columnas relevantes para una tarea específica.

mision.py
selected_columns = df[['order_id', 'product_category', 'price']]
print(selected_columns)
📋
Indexación con listas de columnas

Cuando usamos df[['col1', 'col2']] (doble corchete), le pasamos una lista de nombres de columnas al DataFrame. El resultado es un nuevo DataFrame con solo esas columnas. No modifica el original.

Consola → selected_columns
       order_id product_category   price
0             1            Books  128.75
1             2          Fashion  302.60
2             3           Sports  495.80
...
49999     50000   Home & Kitchen  253.44
[50000 rows x 3 columns]
🔍

Filtrar registros con condiciones

El filtrado nos permite quedarnos solo con las filas que cumplen una determinada condición. Es equivalente al filtro de Excel, pero expresado como código.

mision.py
filtered_data = df[df['product_category'] == 'Books']
print(filtered_data)
⚙️
Indexación booleana (Boolean Indexing)

La expresión df['product_category'] == 'Books' genera una serie de True/False para cada fila. Al pasarla como índice al DataFrame, obtenemos únicamente las filas donde la condición es True. Esto se llama indexación booleana y es el mecanismo de filtrado estándar en pandas.

Consola → filtered_data (resumen)
[8327 rows x 13 columns]
→ De 50,000 registros, 8,327 son de categoría Books (≈ 16.6%)
🧹

Limpiar: eliminar columnas y filas problemáticas

La limpieza de datos es una de las etapas más críticas del análisis. Datos sucios producen resultados erróneos. Los dos pasos que realizamos son: eliminar columnas irrelevantes y eliminar filas con valores faltantes.

Eliminar columna

df = df.drop(columns=["review_count"])

Removemos review_count porque no la usaremos en nuestro análisis. El método drop() recibe la lista de columnas a eliminar y devuelve un nuevo DataFrame sin ellas. Reasignamos a df para guardar el cambio.

Eliminar filas nulas

df = df.dropna()

dropna() elimina toda fila que tenga al menos un valor faltante (NaN o nulo). En este dataset todos los registros eran completos, por lo que el conteo se mantiene en 50,000 filas tras la operación.

⚠️
¿Por qué es importante la limpieza?

Los valores nulos pueden distorsionar promedios, sumas y correlaciones. Una columna irrelevante puede confundir el análisis y ocupar memoria innecesariamente. Limpiar siempre antes de analizar es una buena práctica profesional.

⚙️

Crear nuevas columnas derivadas

A partir de las columnas existentes podemos calcular métricas nuevas que son más útiles para el análisis. Esto se llama feature engineering o ingeniería de variables.

mision.py
# Ingreso por unidad = ingresos totales / unidades vendidas
df["ingreso_por_unidad"] = df["total_revenue"] / df["quantity_sold"].replace(0, 1)
📐
¿Qué mide el "ingreso por unidad"?

El ingreso total dividido entre las unidades vendidas nos da el precio efectivo promedio por unidad. Es más informativo que el precio de lista porque ya refleja los descuentos aplicados.

🛡️
.replace(0, 1) — Prevención de división por cero

Si quantity_sold fuera 0 en algún registro, dividiríamos por cero y obtendríamos inf. Al reemplazar los ceros con 1 antes de dividir, evitamos ese error. Es una técnica defensiva de programación.

Consola → primeras filas con ingreso_por_unidad
   total_revenue  quantity_sold  ingreso_por_unidad
0         463.52              4              115.88
1        1210.40              5              242.08
2         793.28              2              396.64
3        1264.64              4              316.16
4         806.72              4              201.68

Ordenar los datos

df_ordenado = df.sort_values(
    by=["product_category", "ingreso_por_unidad"],
    ascending=[True, False]
)

Ordenamos por categoría (ascendente A→Z) y dentro de cada categoría por ingreso por unidad (descendente, de mayor a menor). Esto nos permite ver fácilmente los productos más rentables de cada categoría.

📊

groupby() — El corazón del análisis agregado

Una de las operaciones más poderosas de pandas es groupby(), que nos permite calcular métricas resumidas por grupos. Es equivalente a una tabla dinámica (pivot table) de Excel.

mision.py
resumen = df.groupby("product_category").agg(
    ingreso_total=("total_revenue", "sum"),
    cantidad_total=("quantity_sold", "sum"),
    ingreso_promedio_unidad=("ingreso_por_unidad", "mean"),
    numero_ordenes=("order_id", "count")
)
🗂️
Patrón Split → Apply → Combine

groupby() divide el DataFrame en grupos (Split), aplica una función a cada grupo (Apply), y combina los resultados en un nuevo DataFrame (Combine). Le estamos diciendo: "para cada categoría de producto, calcula la suma del ingreso, la suma de unidades, el promedio del ingreso por unidad y el conteo de órdenes".

El método .agg() — Agregaciones múltiples en una sola instrucción

.agg() es la abreviatura de aggregate (agregar/resumir). Su función es aplicar una o varias operaciones de resumen a una o varias columnas del DataFrame simultáneamente. Sin .agg(), tendríamos que hacer cada cálculo en una línea separada.

Anatomía de .agg()
# Sintaxis general:
df.agg(
    nombre_resultado = ("columna_origen", "función"),
    ...
)

# En nuestro taller:
df.groupby("product_category").agg(
    ingreso_total         = ("total_revenue",     "sum"  ),  # suma
    cantidad_total        = ("quantity_sold",     "sum"  ),  # suma
    ingreso_promedio_unidad = ("ingreso_por_unidad", "mean" ),  # promedio
    numero_ordenes        = ("order_id",          "count")   # conteo
)
Parte de .agg() ¿Qué es? Ejemplo
nombre_resultado El nombre que tendrá la columna en el DataFrame de salida. Lo elegimos nosotros libremente. ingreso_total
"columna_origen" La columna del DataFrame original sobre la que se aplica la operación. "total_revenue"
"función" La operación matemática a aplicar: "sum", "mean", "count", "min", "max", "std", "median", etc. "sum"
💡
¿Por qué usar .agg() en lugar de calcular uno por uno?

Sin .agg() necesitaríamos 4 líneas separadas y luego unir los resultados manualmente. Con .agg() obtenemos un DataFrame completo y bien estructurado en una sola instrucción, con los nombres de columna que nosotros queramos. Es más legible, más eficiente y produce resultados listos para analizar o exportar.

🔁
.agg() también funciona sin groupby()

En el Bloque 7 (estadística descriptiva) usamos .agg(["mean", "median"]) directamente sobre el DataFrame, sin agrupar. En ese caso, aplica las funciones a todas las filas de las columnas seleccionadas, calculando la media y mediana globales del dataset completo.

Resultados obtenidos

Categoría Ingreso Total Cantidad Total Ing. Prom./Unidad N° Órdenes
Beauty$5,550,624.9725,422$218.688,465
Books$5,484,863.0325,065$218.748,327
Electronics$5,470,594.0324,898$218.848,320
Fashion$5,480,123.3425,089$218.648,365
Home & Kitchen$5,473,132.5524,743$220.478,258
Sports$5,407,235.8224,753$217.968,265
🔍
Hallazgo importante: distribución muy uniforme

Los ingresos por categoría son sorprendentemente parejos: todos oscilan entre $5.4M y $5.5M. Esto sugiere que o bien el dataset fue generado sintéticamente, o que Amazon ha logrado un equilibrio comercial notable entre categorías. El ingreso promedio por unidad también es casi idéntico en todas las categorías (~$218–$220).

📐

Medidas de tendencia central y dispersión

La estadística descriptiva nos permite resumir y caracterizar los datos con números clave. Se divide en dos grandes familias de métricas:

Tendencia central

estadisticas = df[[
    "ingreso_por_unidad",
    "total_revenue",
    "quantity_sold"
]].agg(["mean", "median"])
Resultado
        ing/unidad  revenue  qty
mean      218.89    657.33   2.99
median    215.81    505.41   3.00

Dispersión

dispersion = df[[
    "ingreso_por_unidad",
    "total_revenue",
    "quantity_sold"
]].agg(["var", "std"])
Resultado
      ing/unidad   revenue   qty
var    16209.79   276911.6   2.00
std      127.32     526.22   1.42
Métrica¿Qué mide?¿Cuándo usarla?
Media (mean)El promedio aritméticoCuando los datos no tienen valores extremos
Mediana (median)El valor del centro cuando ordenamos los datosCuando hay valores extremos (outliers)
Moda (mode)El valor más frecuenteEn variables categóricas
Varianza (var)Dispersión promedio al cuadradoPara cálculos matemáticos
Desviación estándar (std)Dispersión en las mismas unidades que los datosPara interpretar qué tan "esparcidos" están los datos
📊
Interpretación: Media vs Mediana del ingreso total

La media del ingreso total ($657) es mayor que la mediana ($505). Esto indica una distribución sesgada a la derecha: hay órdenes de alto valor que "jalan" la media hacia arriba. La mediana es un mejor representante del ingreso "típico". La moda de categoría es Beauty, la más frecuente.

📈

Las 5 gráficas del análisis

Las visualizaciones transforman números en historias. Cada tipo de gráfica tiene un propósito específico. Analizamos cada una en detalle.

Gráfica 1 — Barras: Ingreso total por categoría

ingresos_categoria = df.groupby("product_category")["total_revenue"].sum()

plt.figure()
ax = ingresos_categoria.plot(kind="bar")

plt.title("Ingreso total por categoria")
plt.xlabel("Categoria")
plt.ylabel("Ingreso total")
plt.xticks(rotation=45)

for i, valor in enumerate(ingresos_categoria):
    ax.text(i, valor, f"{valor/1_000_000:.2f}M", ha="center", va="bottom")
descripción

Figura 1. Ingreso total acumulado por categoría de producto.

📌
¿Por qué un gráfico de barras?

Es ideal para comparar valores entre categorías discretas. Cada barra representa una categoría y su altura es proporcional al valor que queremos comparar. El loop for agrega las etiquetas de valor en millones encima de cada barra, facilitando la lectura exacta.

🔍
Análisis del resultado

Todas las categorías generan ingresos muy similares (entre ~$5.4M y ~$5.5M). Beauty lidera levemente con $5.55M, mientras que Sports queda último con $5.41M. La diferencia máxima entre la categoría más y menos rentable es de apenas ~$143K — una variación de solo el 2.6%.

Gráfica 2 — Líneas: Ingresos a lo largo del tiempo

df["anio_mes"] = df["order_date"].dt.to_period("M")
ingresos_fecha = df.groupby("anio_mes")["total_revenue"].sum()

ingresos_fecha.plot(kind="line")
plt.title("Ingresos a lo largo del tiempo")
descripción

Figura 2. Evolución mensual del ingreso total a lo largo del tiempo.

📅
to_period("M") — Agrupación por mes

El acceso .dt.to_period("M") convierte una fecha como 2022-04-13 al período mensual 2022-04. Esto nos permite sumar todos los ingresos de un mismo mes en una sola fila, creando la serie temporal que luego graficamos.

🔍
¿Por qué un gráfico de líneas?

Las líneas son perfectas para mostrar tendencias a lo largo del tiempo (series temporales). El eje X representa el tiempo (meses) y el eje Y los ingresos. La línea conecta los puntos y nos permite ver si los ingresos suben, bajan o fluctúan con el tiempo.

Gráfica 3 — Pastel: Distribución de ingresos por categoría

ingresos_categoria.plot(kind="pie", autopct="%1.1f%%")
plt.title("Distribucion de ingresos por categoria")
descripción

Figura 3. Proporción del ingreso total de cada categoría.

🥧
autopct="%1.1f%%" — Formato de porcentaje

El parámetro autopct agrega el porcentaje dentro de cada porción del pastel. El formato %1.1f%% significa: mostrar el número con 1 decimal y agregar el símbolo %.

🔍
Análisis: porciones casi iguales

El gráfico de pastel confirma lo que vimos en barras: cada categoría representa aproximadamente 1/6 (~16.7%) del ingreso total. Esta uniformidad es inusual en datos de ventas reales, donde suele haber una categoría dominante (ley de Pareto). Refuerza la hipótesis de que es un dataset sintético.

Gráfica 4 — Histograma: Distribución del ingreso por unidad

ax = df["ingreso_por_unidad"].plot(kind="hist", bins=30)

media = df["ingreso_por_unidad"].mean()
mediana = df["ingreso_por_unidad"].median()

plt.axvline(media, linestyle="--", label=f"Media: {media:.2f}")
plt.axvline(mediana, linestyle="-", label=f"Mediana: {mediana:.2f}")
Distribución del ingreso por unidad

Figura 4. Distribución de frecuencias del ingreso por unidad con líneas de media y mediana.

📊
¿Qué es un histograma?

Un histograma divide el rango de valores en intervalos (bins) y muestra cuántos datos caen en cada uno. No es un gráfico de barras: aquí el eje X es continuo y no hay espacios entre las barras. Los 30 bins dividen el rango de ingresos en 30 intervalos iguales.

↕️
axvline() — Líneas verticales de referencia

plt.axvline() dibuja una línea vertical en el valor especificado. Al agregar las líneas de media y mediana, podemos ver visualmente si la distribución es simétrica o sesgada, y qué tan cerca están ambas medidas.

🔍
Análisis: distribución y sesgo

La distribución del ingreso por unidad oscila entre ~$3 y ~$500. La media ($218.89) está ligeramente por encima de la mediana ($215.81), lo que indica un leve sesgo positivo — hay algunos productos de precio muy alto que empujan la media. La distribución es bastante uniforme, lo que de nuevo apunta a datos sintéticos generados con distribución uniforme.

Gráfica 5 — Mapa de calor: Correlaciones

correlacion = df[[
    "price", "discount_percent", "quantity_sold",
    "total_revenue", "ingreso_por_unidad"
]].corr()

sns.heatmap(correlacion, annot=True)
plt.title("Mapa de calor de correlacion")
Mapa de calor de correlaciones

Figura 5. Matriz de correlación de Pearson entre las variables numéricas del dataset.

🌡️
¿Qué es la correlación de Pearson?

La correlación mide la relación lineal entre dos variables. Su valor va de -1 (perfectamente opuestas) a +1 (perfectamente relacionadas). Un valor de 0 indica que no hay relación lineal. El mapa de calor (heatmap) colorea cada celda según su valor: colores cálidos = correlación positiva, fríos = negativa.

🔍
¿Qué buscar en el heatmap?

La diagonal principal siempre vale 1.0 (cada variable se correlaciona perfectamente consigo misma). Lo interesante es observar si price y ingreso_por_unidad están altamente correlacionados (esperamos que sí, dado que el ingreso por unidad depende del precio), y si el discount_percent afecta negativamente al ingreso por unidad.

🎓

Conclusiones del análisis

Tras completar el ciclo completo de análisis, estos son los hallazgos y lecciones principales del taller:

💰 Ingresos balanceados

Las 6 categorías de producto generan ingresos casi idénticos (~$5.4–5.5M cada una). No existe una categoría dominante: Beauty lidera y Sports queda último, pero con una brecha mínima del 2.6%.

📦 Volumen uniforme

El número de órdenes por categoría es muy similar (entre 8,258 y 8,465), y el ingreso promedio por unidad oscila alrededor de $218–$220 en todas las categorías sin variación significativa.

📅 Serie temporal

Los datos cubren los años 2022–2023. La gráfica de líneas permite identificar meses de mayor y menor actividad comercial, útil para planear inventario y campañas de marketing.

📐 Distribución sesgada

La diferencia entre media ($657) y mediana ($505) en el ingreso total revela que hay órdenes de alto valor que elevan el promedio. La mediana es el mejor indicador del ingreso "típico".

🧹 Limpieza exitosa

El dataset no presentó valores nulos, lo que facilitó el proceso. La eliminación de review_count simplificó el análisis sin perder información relevante para los objetivos del taller.

🔧 Herramientas utilizadas

Este análisis completo fue posible con solo tres librerías: pandas para manipulación, matplotlib para visualización base, y seaborn para el heatmap estadístico. Una combinación estándar en ciencia de datos profesional.

Habilidades adquiridas en este taller

Al completar este taller, ahora sabes cómo:

✅  Cargar y explorar un CSV con pandas

✅  Convertir tipos de datos (fechas)

✅  Seleccionar, filtrar y ordenar datos

✅  Eliminar columnas y filas problemáticas

✅  Crear columnas derivadas (feature engineering)

✅  Agrupar datos con groupby() y agg()

✅  Calcular estadísticas descriptivas

✅  Crear 5 tipos de visualizaciones