Los datos en bruto casi nunca están listos para analizarse. Las exportaciones de tiendas en línea, formularios de contacto, CRM y hojas de cálculo llegan con espacios de más, mayúsculas inconsistentes, precios guardados como texto, filas duplicadas, valores faltantes y el ocasional pedido de “prueba” que alguien olvidó borrar. Si analizas esos datos tal como están, cada total, promedio y gráfica que construyas encima estará mal sin que lo notes.

Pandas es la herramienta estándar de Python para resolver eso, y para todo lo que viene después. En esta guía vas a llevar un solo conjunto de datos realista y desordenado por un flujo de trabajo completo: cargarlo, explorarlo, limpiarlo y analizarlo. En lugar de fragmentos aislados con nombres de ejemplo, cada ejemplo usa los mismos datos y muestra el resultado real, para que veas exactamente qué hace cada paso.

Al terminar, sabrás hacer las tareas que forman la mayor parte del trabajo real con Pandas:

  • Cargar y explorar un conjunto de datos
  • Seleccionar y filtrar filas y columnas
  • Limpiar texto, números guardados como texto, fechas, duplicados, valores faltantes y valores atípicos
  • Agregar columnas calculadas
  • Ordenar, agrupar y resumir
  • Crear tablas dinámicas y tendencias mensuales
  • Combinar conjuntos de datos y cambiar su forma
  • Exportar los resultados

¿Qué Es Pandas?

Pandas es una biblioteca de Python gratuita y de código abierto para trabajar con datos tabulares: datos organizados en filas y columnas, como una hoja de cálculo o una tabla de base de datos. Su nombre viene de “panel data” (datos de panel), un término de la estadística y la economía.

Pandas tiene dos piezas principales:

  • Un DataFrame es una tabla completa: filas y columnas, cada columna con un nombre. Piensa en él como una hoja de cálculo dentro de Python.
  • Una Series es una sola columna (o fila) de un DataFrame: una lista de valores con etiquetas.
DataFrame (the whole table)                      Series (one column: "price")

   order_id     customer      price              0       $49.00
0      1001  Maria Lopez     $49.00              1    $1,200.00
1      1002  carlos ruiz  $1,200.00              2        $9.99
2      1003   Ana Torres      $9.99

¿Por qué usar Pandas en lugar de una hoja de cálculo? Porque cada paso es código: puedes repetir toda la limpieza con la exportación del próximo mes en un segundo, ver exactamente qué se cambió y manejar archivos con millones de filas que congelarían una hoja de cálculo.

Preparación

Instala Pandas en un entorno virtual (o con uv):

pip install pandas

Por convención, Pandas siempre se importa como pd:

import pandas as pd

Versiones de Pandas: los resultados de esta guía vienen de Pandas 3.1. Todos los ejemplos también funcionan sin advertencias en Pandas 2.x. La única diferencia visible está en df.info() y en los tipos de datos: Pandas 3 identifica las columnas de texto como str, mientras que Pandas 2 muestra object.

El Conjunto de Datos de Ejemplo

Nuestro conjunto de datos es una exportación de los pedidos de una pequeña tienda en línea que vende plantillas de sitios web, diseño de logotipos, hosting y servicios de SEO. Guárdalo como orders.csv en la misma carpeta que tu script:

order_id,order_date,customer,email,product,category,quantity,price,status
1001,2026-01-05,Maria Lopez,[email protected],Website Template,Templates,1,$49.00,Completed
1002,2026-01-07,  carlos ruiz ,[email protected],Logo Design,design,1,"$1,200.00",completed
1003,2026-01-07,Ana Torres,[email protected],Hosting Plan,Hosting,12,$9.99,Completed
1003,2026-01-07,Ana Torres,[email protected],Hosting Plan,Hosting,12,$9.99,Completed
1004,2026-01-12,Valley Dental,,SEO Audit,Marketing,1,$300.00,Pending
1005,2026-01-20,Lopez Landscaping,[email protected],Website Template,Templates,2,$49.00,Completed
1006,2026-02-02,Calexico Coffee,[email protected],Website Template,templates,2,$49.00,Completed
1007,2026-02-10,Bright Bakery,[email protected],Logo Design,Design,1,"$1,200.00",Refunded
1008,2026-02-14,Lopez Landscaping,[email protected],Hosting Plan,Hosting,,$9.99,Completed
1009,2026-02-20,Maria Lopez,[email protected],SEO Audit,Marketing,1,$300.00,Completed
1010,2026-03-03,Test User,[email protected],Website Template,Templates,1,"$49,000.00",Completed
1011,2026-03-08,Valley Dental,[email protected],Hosting Plan,HOSTING,6,$9.99,Completed
1012,2026-03-15,Calexico Coffee,[email protected],SEO Audit,Marketing,1,$300.00, completed
1013,2026-03-22,Bright Bakery,[email protected],Website Template,Templates,3,$49.00,Completed

Tiene los problemas que verás en las exportaciones reales: un nombre con espacios de más y en minúsculas, un correo en mayúsculas, categorías escritas de cuatro formas distintas, precios guardados como texto con $ y comas, un pedido duplicado, un correo faltante, una cantidad faltante, estados con mayúsculas inconsistentes y un pedido de “prueba” de $49,000.

Más adelante también usaremos un segundo archivo, customers.csv:

customer,city,segment
Maria Lopez,El Centro,Freelancer
Carlos Ruiz,Calexico,Small Business
Ana Torres,Brawley,Freelancer
Valley Dental,El Centro,Small Business
Lopez Landscaping,Imperial,Small Business
Calexico Coffee,Calexico,Small Business
Bright Bakery,El Centro,Small Business

Los ejemplos se construyen uno sobre otro, así que ejecútalos en orden, en un solo script o en un cuaderno de Jupyter.

Parte 1: Cargar y Explorar

Cargar los Datos

import pandas as pd

pd.set_option("display.max_columns", None)  # mostrar todas las columnas
pd.set_option("display.width", 200)

df = pd.read_csv("orders.csv")
print(df.shape)
print(df.head())
(14, 9)
   order_id  order_date        customer               email           product   category  quantity      price     status
0      1001  2026-01-05     Maria Lopez   [email protected]  Website Template  Templates       1.0     $49.00  Completed
1      1002  2026-01-07    carlos ruiz   [email protected]       Logo Design     design       1.0  $1,200.00  completed
2      1003  2026-01-07      Ana Torres     [email protected]      Hosting Plan    Hosting      12.0      $9.99  Completed
3      1003  2026-01-07      Ana Torres     [email protected]      Hosting Plan    Hosting      12.0      $9.99  Completed
4      1004  2026-01-12   Valley Dental                 NaN         SEO Audit  Marketing       1.0    $300.00    Pending

df.shape te dice el tamaño: 14 filas y 9 columnas. df.head() muestra las primeras cinco filas (usa df.tail() para las últimas cinco). Las dos líneas de set_option evitan que Pandas oculte columnas detrás de ... cuando la tabla es ancha.

Pandas lee muchos formatos además de CSV, como pd.read_excel() para archivos de Excel, pd.read_json() para JSON y pd.read_sql() para bases de datos (consulta Python para Análisis de Datos: Cómo Acceder a tus Datos y SQLAlchemy).

Revisar la Estructura con info()

df.info() es la primera mirada más útil a un conjunto de datos nuevo:

df.info()
<class 'pandas.DataFrame'>
RangeIndex: 14 entries, 0 to 13
Data columns (total 9 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   order_id    14 non-null     int64
 1   order_date  14 non-null     str
 2   customer    14 non-null     str
 3   email       13 non-null     str
 4   product     14 non-null     str
 5   category    14 non-null     str
 6   quantity    13 non-null     float64
 7   price       14 non-null     str
 8   status      14 non-null     str
dtypes: float64(1), int64(1), str(7)
memory usage: 1.1 KB

Ya se ven tres problemas:

  1. price es texto (str), no un número, por los signos $ y las comas. No se puede sumar texto.
  2. order_date es texto, así que todavía no puedes ordenarlo ni agruparlo por mes.
  3. quantity es float64 (un número decimal), aunque las cantidades son números enteros. Es una pista: Pandas usa NaN (“not a number”, no es un número) para los valores faltantes, y NaN es un flotante, así que una columna numérica con cualquier valor faltante se convierte en una columna de flotantes. Los conteos de valores no nulos lo confirman: email y quantity tienen 13 valores de 14.

Encontrar Valores Faltantes, Duplicados y Categorías Inconsistentes

print(df.isna().sum())
print("Duplicate rows:", df.duplicated().sum())
print(df["category"].value_counts())
order_id      0
order_date    0
customer      0
email         1
product       0
category      0
quantity      1
price         0
status        0
dtype: int64
Duplicate rows: 1
category
Templates    4
Hosting      3
Marketing    3
design       1
templates    1
Design       1
HOSTING      1
Name: count, dtype: int64
  • isna().sum() cuenta los valores faltantes de cada columna.
  • duplicated().sum() cuenta las filas que son copias exactas de una fila anterior.
  • value_counts() cuenta cada valor distinto de una columna. Es la forma más rápida de detectar escrituras inconsistentes: Pandas ve Design, design, Hosting y HOSTING como cuatro categorías distintas.

Seleccionar Columnas y Filas

Vas a seleccionar datos todo el tiempo, así que conviene aprender pronto las tres formas principales:

print(df[["customer", "product", "price"]].head(3))  # columnas por nombre
print(df.loc[0, "customer"])                          # un valor, por etiqueta de fila y nombre de columna
print(df.iloc[0:2, 0:3])                              # por posición: filas 0-1, columnas 0-2
         customer           product      price
0     Maria Lopez  Website Template     $49.00
1    carlos ruiz        Logo Design  $1,200.00
2      Ana Torres      Hosting Plan      $9.99
Maria Lopez
   order_id  order_date        customer
0      1001  2026-01-05     Maria Lopez
1      1002  2026-01-07    carlos ruiz
  • df["col"] selecciona una columna como Series; df[["col1", "col2"]] (con doble corchete) selecciona varias columnas como DataFrame.
  • .loc[] selecciona por etiquetas: las etiquetas de las filas (el índice de la izquierda) y los nombres de las columnas.
  • .iloc[] selecciona por posición, como las rebanadas de una lista: 0:2 significa las posiciones 0 y 1.

Parte 2: Limpiar los Datos

Limpiar las Columnas de Texto

Las columnas de texto necesitan espacios y mayúsculas consistentes antes de poder agruparlas o combinarlas. El accesor .str de Pandas aplica los métodos de cadenas a todos los valores de una columna a la vez:

df["customer"] = df["customer"].str.strip().str.title()
df["email"] = df["email"].str.strip().str.lower()
df["category"] = df["category"].str.strip().str.title()
df["status"] = df["status"].str.strip().str.title()

print(df["category"].value_counts())
print(df["status"].value_counts())
category
Templates    5
Hosting      4
Marketing    3
Design       2
Name: count, dtype: int64
status
Completed    12
Pending       1
Refunded      1
Name: count, dtype: int64

.str.strip() quita los espacios de más, .str.title() pone en mayúscula la primera letra de cada palabra y .str.lower() pone los correos en minúsculas para que la misma dirección siempre coincida. Las siete formas de escribir las categorías ahora son cuatro categorías limpias, y " carlos ruiz " ahora es "Carlos Ruiz". (Son los mismos métodos de cadenas de Fundamentos de Python: Variables y Tipos de Datos, aplicados a toda una columna a la vez.)

Convertir Precios y Fechas al Tipo Correcto

df["price"] = (
    df["price"]
    .str.replace("$", "", regex=False)
    .str.replace(",", "", regex=False)
    .astype(float)
)
df["order_date"] = pd.to_datetime(df["order_date"])

print(df[["order_date", "price"]].dtypes)
order_date    datetime64[us]
price                float64
dtype: object

Quitar tanto el $ como la coma es importante: si solo quitas el $, convertir "1,200.00" en número falla con ValueError: could not convert string to float. regex=False le indica a Pandas que trate el $ como un carácter normal y no como un patrón especial.

pd.to_datetime() convierte el texto en fechas reales, que puedes ordenar, filtrar por rango y agrupar por mes. (Pandas 2 muestra el tipo como datetime64[ns]; ambos funcionan igual.) Si algunas fechas no se pueden interpretar, pd.to_datetime(df["col"], errors="coerce") las convierte en valores faltantes en lugar de lanzar un error, para que puedas encontrarlas y corregirlas.

Quitar Duplicados

df = df.drop_duplicates()
print("Rows after removing duplicates:", len(df))
print("Duplicate order IDs left:", df["order_id"].duplicated().sum())
Rows after removing duplicates: 13
Duplicate order IDs left: 0

drop_duplicates() quita las filas que son idénticas en todas las columnas. En los datos reales, los duplicados no siempre son copias exactas (el mismo pedido podría aparecer dos veces con una hora distinta), así que revisa también la columna que debería ser única, como el ID del pedido. Para conservar solo el registro más reciente de cada cliente, ordena primero y usa subset y keep:

latest = df.sort_values("order_date").drop_duplicates(subset="customer", keep="last")

Manejar los Valores Faltantes

Primero, mira las filas con datos faltantes antes de decidir qué hacer:

print(df[df["quantity"].isna()][["order_id", "customer", "product"]])

df["quantity"] = df["quantity"].fillna(1).astype(int)
df["email"] = df["email"].fillna("unknown")

print(df.isna().sum().sum(), "missing values left")
   order_id           customer       product
8      1008  Lopez Landscaping  Hosting Plan
0 missing values left

No hay una sola forma correcta de manejar los valores faltantes; depende de lo que significan los datos:

  • Llenar con un valor predeterminado razonable. Aquí, un pedido debe tener al menos un artículo, así que llenar la cantidad faltante con 1 es razonable (en un trabajo real, confírmalo con la fuente si puedes). Después de llenarla, .astype(int) vuelve a convertir la columna en números enteros.
  • Llenar con una etiqueta como "unknown" para el texto que conviene conservar.
  • Llenar con una estadística (df["col"].fillna(df["col"].median())) para las mediciones numéricas. La mediana es más segura que el promedio cuando hay valores atípicos.
  • Quitar las filas con df.dropna(subset=["col"]) cuando un valor es esencial y no se puede adivinar.
  • Arrastrar los valores hacia adelante con df["col"].ffill() en las series de tiempo, donde el último valor conocido sigue siendo válido.

Evita inplace=True en una columna. Los tutoriales viejos escriben df["col"].fillna(0, inplace=True). En Pandas 3, eso no cambia nada en df: modifica una copia temporal, y Pandas advierte que “nunca funciona”. Siempre asigna el resultado de vuelta: df["col"] = df["col"].fillna(0). Esto funciona igual en todas las versiones de Pandas.

Encontrar Valores Atípicos

Los valores atípicos (outliers) son los que están muy lejos del resto. Una forma común de encontrarlos es la regla del IQR (rango intercuartílico): se señala todo lo que esté a más de 1.5 × IQR por encima del tercer cuartil (o por debajo del primero).

q1 = df["price"].quantile(0.25)
q3 = df["price"].quantile(0.75)
iqr = q3 - q1
upper = q3 + 1.5 * iqr
print(f"Upper limit: {upper:.2f}")
print(df[df["price"] > upper][["order_id", "customer", "product", "price"]])
Upper limit: 676.50
    order_id       customer           product    price
1       1002    Carlos Ruiz       Logo Design   1200.0
7       1007  Bright Bakery       Logo Design   1200.0
10      1010      Test User  Website Template  49000.0

Esta es una lección importante: un valor atípico no es automáticamente un error. La regla señaló tres pedidos, pero los dos pedidos de diseño de logotipo de $1,200 son reales: el diseño de logotipos simplemente es el servicio más caro. Solo la “Website Template” de $49,000 de “Test User” está claramente mal. Las reglas estadísticas encuentran candidatos; tu conocimiento de los datos decide qué hacer con ellos. Aquí quitamos solo el pedido de prueba:

df = df[df["customer"] != "Test User"].copy()
print("Rows:", len(df))
Rows: 12

El .copy() deja claro que df ahora es una tabla propia e independiente, así que agregarle columnas después nunca provoca la advertencia “SettingWithCopyWarning” de Pandas 2.

Parte 3: Analizar los Datos

Filtrar Filas

Para filtrar se usan máscaras booleanas: una condición que es True o False para cada fila.

completed = df[df["status"] == "Completed"]
print("Completed orders:", len(completed))

big = df[(df["status"] == "Completed") & (df["price"] >= 300)]
print(big[["order_id", "customer", "product", "price"]])

print(df[df["category"].isin(["Design", "Marketing"])][["order_id", "product"]])
print(df.query("quantity > 2 and status == 'Completed'")[["order_id", "customer", "quantity"]])
Completed orders: 10
    order_id         customer      product   price
1       1002      Carlos Ruiz  Logo Design  1200.0
9       1009      Maria Lopez    SEO Audit   300.0
12      1012  Calexico Coffee    SEO Audit   300.0
    order_id      product
1       1002  Logo Design
4       1004    SEO Audit
7       1007  Logo Design
9       1009    SEO Audit
12      1012    SEO Audit
    order_id       customer  quantity
2       1003     Ana Torres        12
11      1011  Valley Dental         6
13      1013  Bright Bakery         3
  • Combina condiciones con & (y), | (o) y ~ (no), y pon cada condición entre paréntesis: la precedencia de operadores de Python los hace necesarios.
  • .isin([...]) revisa si un valor está en una lista.
  • .query() te permite escribir las condiciones como una cadena fácil de leer.
  • Otros filtros útiles: df["price"].between(10, 100) y df["email"].str.contains("example.com").

Observa que los números de fila de la izquierda (el índice) conservaron sus valores originales después de filtrar. Es normal; usa .reset_index(drop=True) si quieres que se vuelvan a numerar.

Agregar Columnas Calculadas

df["revenue"] = df["quantity"] * df["price"]
df["month"] = df["order_date"].dt.to_period("M")
df["is_large_order"] = df["revenue"] >= 200

print(df[["order_id", "quantity", "price", "revenue", "month", "is_large_order"]].head())
   order_id  quantity    price  revenue    month  is_large_order
0      1001         1    49.00    49.00  2026-01           False
1      1002         1  1200.00  1200.00  2026-01            True
2      1003        12     9.99   119.88  2026-01           False
4      1004         1   300.00   300.00  2026-01            True
5      1005         2    49.00    98.00  2026-01           False

Las operaciones entre columnas funcionan con todas las filas a la vez, sin necesidad de un ciclo. Esto se llama vectorización, y es más sencillo y mucho más rápido que recorrer las filas una por una. El accesor .dt funciona como .str, pero para fechas: .dt.year, .dt.month, .dt.day_name() y .dt.to_period("M") (el mes) están disponibles.

Ordenar y Clasificar

A partir de aquí analizaremos solo las ventas completadas:

sales = df[df["status"] == "Completed"].copy()
print(sales.sort_values(["revenue", "customer"], ascending=[False, True])[["order_id", "customer", "revenue"]].head(4))
print(sales.nlargest(3, "revenue")[["customer", "revenue"]])
    order_id         customer  revenue
1       1002      Carlos Ruiz   1200.0
12      1012  Calexico Coffee    300.0
9       1009      Maria Lopez    300.0
13      1013    Bright Bakery    147.0
           customer  revenue
1       Carlos Ruiz   1200.0
9       Maria Lopez    300.0
12  Calexico Coffee    300.0

Ordenar por más de una columna rompe los empates: dos pedidos tienen $300 de ingresos, así que la segunda columna (customer, de la A a la Z) decide su orden. nlargest() y nsmallest() son atajos para las listas de “los N primeros”.

Agrupar y Resumir

groupby() es una de las herramientas más poderosas de Pandas. Funciona en tres pasos: dividir los datos en grupos, aplicar un cálculo a cada grupo y combinar los resultados:

print(sales.groupby("category")["revenue"].sum().sort_values(ascending=False))
category
Design       1200.00
Marketing     600.00
Templates     392.00
Hosting       189.81
Name: revenue, dtype: float64

Para calcular varias estadísticas a la vez, usa .agg() con agregaciones con nombre: cada columna nueva recibe un nombre, una columna de origen y una función:

summary = sales.groupby("customer").agg(
    orders=("order_id", "count"),
    total_revenue=("revenue", "sum"),
    avg_order=("revenue", "mean"),
).sort_values("total_revenue", ascending=False)
print(summary.round(2))
                   orders  total_revenue  avg_order
customer
Carlos Ruiz             1        1200.00    1200.00
Calexico Coffee         2         398.00     199.00
Maria Lopez             2         349.00     174.50
Bright Bakery           1         147.00     147.00
Ana Torres              1         119.88     119.88
Lopez Landscaping       2         107.99      54.00
Valley Dental           1          59.94      59.94

Es un reporte completo de clientes en cinco líneas. Algunas funciones de agregación comunes son "sum", "mean", "median", "count", "nunique" (cantidad de valores distintos), "min" y "max".

Crear una Tabla Dinámica

Una tabla dinámica (pivot table) resume los datos en dos dimensiones, igual que en Excel:

pivot = sales.pivot_table(
    index="month",
    columns="category",
    values="revenue",
    aggfunc="sum",
    fill_value=0,
)
print(pivot)
category  Design  Hosting  Marketing  Templates
month
2026-01   1200.0   119.88        0.0      147.0
2026-02      0.0     9.99      300.0       98.0
2026-03      0.0    59.94      300.0      147.0

index define las filas, columns define las columnas, values es lo que se va a resumir y fill_value=0 reemplaza las combinaciones vacías (no hubo ventas de diseño en febrero) con cero.

Para simplemente contar combinaciones, pd.crosstab() es un atajo. Aquí muestra los estados de los pedidos por categoría, usando todos los pedidos:

print(pd.crosstab(df["category"], df["status"]))
status     Completed  Pending  Refunded
category
Design             1        0         1
Hosting            3        0         0
Marketing          2        1         0
Templates          4        0         0

Analizar Tendencias en el Tiempo

Con una columna de fechas real, Pandas puede resumir los datos por cualquier periodo. resample() agrupa por tiempo; aquí, los totales mensuales:

monthly = sales.set_index("order_date")["revenue"].resample("MS").sum()
print(monthly)
print(monthly.pct_change().round(3))
order_date
2026-01-01    1466.88
2026-02-01     407.99
2026-03-01     506.94
Freq: MS, Name: revenue, dtype: float64
order_date
2026-01-01      NaN
2026-02-01   -0.722
2026-03-01    0.243
Freq: MS, Name: revenue, dtype: float64

"MS" significa “inicio de mes” (month start); usa "W" para semanas o "QS" para trimestres. pct_change() calcula el cambio de un periodo al siguiente: los ingresos bajaron 72.2% en febrero y subieron 24.3% en marzo. Enero es NaN porque no hay un mes anterior con el cual compararlo. Usa .rolling(3).mean() para los promedios móviles.

Combinar Conjuntos de Datos con merge()

Para agregar la ciudad y el segmento de cada cliente, combina (merge) las ventas con customers.csv. Funciona igual que un JOIN de SQL (consulta Los JOINs de SQL Explicados):

customers = pd.read_csv("customers.csv")
merged = sales.merge(customers, on="customer", how="left", validate="many_to_one")
print(merged[["customer", "city", "segment", "revenue"]].head())
print(merged.groupby("city")["revenue"].sum().sort_values(ascending=False))
            customer       city         segment  revenue
0        Maria Lopez  El Centro      Freelancer    49.00
1        Carlos Ruiz   Calexico  Small Business  1200.00
2         Ana Torres    Brawley      Freelancer   119.88
3  Lopez Landscaping   Imperial  Small Business    98.00
4    Calexico Coffee   Calexico  Small Business    98.00
city
Calexico     1598.00
El Centro     555.94
Brawley       119.88
Imperial      107.99
Name: revenue, dtype: float64
  • how="left" conserva todas las ventas, aunque un cliente no esté en el segundo archivo. ("inner" conserva solo las coincidencias, "outer" conserva todo y "right" conserva todas las filas de la segunda tabla.)
  • validate="many_to_one" hace que Pandas lance un error si un cliente aparece más de una vez en customers.csv, lo que duplicaría filas del resultado sin avisar.

Fíjate en que Carlos Ruiz encontró su ciudad. Eso solo funcionó porque antes limpiamos " carlos ruiz ": las combinaciones comparan el texto exactamente, así que la limpieza siempre va primero. Para revisar cuántas filas coincidieron, agrega indicator=True:

check = df.merge(customers, on="customer", how="left", indicator=True)
print(check["_merge"].value_counts())
_merge
both          12
left_only      0
right_only     0
Name: count, dtype: int64

Los 12 pedidos encontraron a su cliente. Cualquier fila left_only sería un pedido sin cliente que coincida, normalmente señal de una diferencia de escritura que hay que corregir.

Para apilar tablas con las mismas columnas (como agregar los pedidos del mes siguiente), usa pd.concat():

april = pd.DataFrame({
    "order_id": [1014, 1015],
    "customer": ["Maria Lopez", "Ana Torres"],
    "revenue": [49.0, 300.0],
})
all_orders = pd.concat([sales[["order_id", "customer", "revenue"]], april], ignore_index=True)
print(len(sales), "+", len(april), "=", len(all_orders))
10 + 2 = 12

Cambiar la Forma de los Datos: Ancho y Largo

Los datos vienen en dos formas. Los datos anchos tienen una columna por categoría, como la tabla dinámica de arriba. Los datos largos (u “ordenados”, tidy) tienen una fila por medición, que es lo que esperan la mayoría de las bibliotecas de gráficas y muchos análisis. melt() convierte de ancho a largo (y pivot_table() hace lo contrario):

wide = pivot.reset_index()
long = wide.melt(id_vars="month", var_name="category", value_name="revenue")
print(long.head(6))
     month category  revenue
0  2026-01   Design  1200.00
1  2026-02   Design     0.00
2  2026-03   Design     0.00
3  2026-01  Hosting   119.88
4  2026-02  Hosting     9.99
5  2026-03  Hosting    59.94

Traducir Valores y Crear Categorías

Hay dos transformaciones más que aparecen todo el tiempo. .map() traduce valores usando un diccionario (consulta Fundamentos de Python: Listas y Diccionarios), y pd.cut() agrupa números en rangos con etiquetas:

tier = {"Templates": "Product", "Hosting": "Recurring", "Design": "Service", "Marketing": "Service"}
df["revenue_type"] = df["category"].map(tier)
df["size"] = pd.cut(df["revenue"], bins=[0, 100, 500, float("inf")], labels=["Small", "Medium", "Large"])
print(df[["product", "category", "revenue_type", "revenue", "size"]].head())
            product   category revenue_type  revenue    size
0  Website Template  Templates      Product    49.00   Small
1       Logo Design     Design      Service  1200.00   Large
2      Hosting Plan    Hosting    Recurring   119.88  Medium
4         SEO Audit  Marketing      Service   300.00  Medium
5  Website Template  Templates      Product    98.00   Small

Los valores que no están en el diccionario se convierten en NaN, lo que hace de .map() una forma rápida de detectar categorías inesperadas. Para una lógica demasiado compleja para un diccionario, .apply() ejecuta tu propia función con cada valor, pero intenta primero un enfoque vectorizado, porque .apply() es mucho más lento con datos grandes.

Parte 4: Exportar los Resultados

summary.round(2).to_csv("customer_summary.csv")
sales.drop(columns="month").to_json("sales.json", orient="records", date_format="iso", indent=2)

Ahora customer_summary.csv contiene:

customer,orders,total_revenue,avg_order
Carlos Ruiz,1,1200.0,1200.0
Calexico Coffee,2,398.0,199.0
Maria Lopez,2,349.0,174.5
Bright Bakery,1,147.0,147.0
Ana Torres,1,119.88,119.88
Lopez Landscaping,2,107.99,54.0
Valley Dental,1,59.94,59.94

Usa index=False con to_csv() cuando el índice solo son números de fila que no necesitas. Para el JSON quitamos la columna auxiliar month, porque JSON no puede guardar el tipo “Period” de Pandas: un buen recordatorio de exportar columnas simples. Otros formatos son to_excel() (que necesita el paquete openpyxl) y to_sql() para bases de datos.

Para convertir estos resultados en gráficas, consulta Visualización de Datos con Python: Guía Práctica de Gráficas.

Errores Comunes con Pandas

  • Usar inplace=True en una columna. df["col"].fillna(0, inplace=True) no hace nada en Pandas 3. Asigna el resultado de vuelta: df["col"] = df["col"].fillna(0).
  • Olvidar los paréntesis al combinar condiciones. df[df["a"] > 1 & df["b"] < 5] lanza un error o da resultados incorrectos. Escribe df[(df["a"] > 1) & (df["b"] < 5)].
  • Usar and/or en lugar de &/|. El and de Python no funciona con columnas completas; Pandas necesita & y |.
  • Hacer operaciones con números guardados como texto. Revisa primero df.info() y convierte con .astype() o pd.to_numeric().
  • Comparar con NaN usando ==. NaN == NaN es False. Usa .isna() y .notna().
  • Recorrer las filas con un ciclo. for index, row in df.iterrows() es lento y casi nunca hace falta. Usa operaciones con columnas.
  • Combinar antes de limpiar. "Carlos Ruiz" y " carlos ruiz " no coinciden. Limpia primero las columnas clave y usa indicator=True para revisar el resultado.
  • Borrar automáticamente todos los valores atípicos. Revisa los valores señalados antes de quitarlos; algunos son tus datos más importantes.
  • Modificar una tabla filtrada sin .copy(). Puede provocar SettingWithCopyWarning en Pandas 2. Agrega .copy() cuando filtres un DataFrame que vayas a modificar.

Hoja de Referencia de Pandas

TareaCódigo
Cargar un CSVdf = pd.read_csv("file.csv")
Primera miradadf.head(), df.info(), df.describe(), df.shape
Valores faltantesdf.isna().sum()
Contar valoresdf["col"].value_counts()
Seleccionar columnasdf[["a", "b"]]
Seleccionar por etiqueta / posicióndf.loc[row, "col"], df.iloc[0:5, 0:3]
Filtrar filasdf[(df["a"] > 1) & (df["b"] == "x")]
Limpiar textodf["col"].str.strip().str.title()
Convertir tipos.astype(float), pd.to_datetime(), pd.to_numeric()
Llenar valores faltantesdf["col"] = df["col"].fillna(value)
Quitar filas con valores faltantesdf.dropna(subset=["col"])
Quitar duplicadosdf.drop_duplicates()
Columna nuevadf["c"] = df["a"] * df["b"]
Ordenardf.sort_values("col", ascending=False)
Agrupar y resumirdf.groupby("col")["val"].sum() o .agg(...)
Tabla dinámicadf.pivot_table(index=..., columns=..., values=..., aggfunc="sum")
Periodos de tiempodf.set_index("date")["val"].resample("MS").sum()
Combinardf.merge(other, on="key", how="left")
Apilar tablaspd.concat([df1, df2], ignore_index=True)
De ancho a largodf.melt(id_vars=..., var_name=..., value_name=...)
Exportardf.to_csv("out.csv", index=False)

Conclusión

La mayor parte del trabajo real con Pandas sigue el mismo patrón que acabas de usar: cargar los datos, explorarlos con info(), isna() y value_counts(), limpiar el texto, los tipos, los duplicados, los valores faltantes y los atípicos, y luego analizarlos con filtros, columnas calculadas, groupby(), tablas dinámicas, resúmenes por tiempo y combinaciones, antes de exportar los resultados.

Los pasos de limpieza no son solo preparación; deciden si tus respuestas son correctas. Sin ellos, este conjunto de datos habría reportado siete categorías de productos en lugar de cuatro, contado un pedido dos veces, perdido la ciudad de Carlos Ruiz en la combinación y sumado una venta falsa de $49,000 a los ingresos de marzo. Guarda tus pasos de limpieza en un script, y la exportación del próximo mes te tomará segundos en lugar de una tarde.

Para ver más a fondo las técnicas de limpieza usando SQL puro, consulta Limpiar Datos Desordenados con SQL Puro.