Saltar a contenido

1.5 · Pandas

Objetivos. Al terminar este capítulo podrás: crear Series y DataFrames; seleccionar, filtrar y ordenar datos; tratar valores faltantes; agrupar y agregar; combinar tablas (incluido el anti-join); leer y escribir CSV; y transformar entre formato largo y ancho.

Evidencia de logro. Cargarás un CSV de TechShop, lo limpiarás, lo explorarás con agrupaciones y lo combinarás con otra tabla, justificando cada decisión.

Contexto y motivación

NumPy trabaja con números en bloque; Pandas trabaja con tablas etiquetadas: filas y columnas con nombre, datos mixtos (texto, números, fechas) y valores faltantes. Es la herramienta que usarás a diario para preparar y explorar datos antes de modelar. En la práctica, pasarás más tiempo con Pandas que con cualquier otra librería del curso.

Frente a una hoja de cálculo (Excel), Pandas es reproducible: el código documenta cada paso, así que cualquiera puede repetir el análisis y ver qué hiciste (y corregirlo). Cada herramienta tiene su terreno: Pandas para transformar y graficar, SQL para consultar bases de datos. El patrón del curso: filtrar y agregar donde estén los datos (SQL) y transformar/explorar con Pandas (lo retomamos en el capítulo 1.6).

Vocabulario

Término Significado
Series Columna con índice (1D)
DataFrame Tabla completa (2D): índice + columnas
Índice Etiquetas de las filas
Faltante Valor ausente, representado como NaN (o None)
groupby "Agrupar por" una clave y aplicar una función a cada grupo
merge Combinar dos tablas por una clave común (como un JOIN de SQL)

Prerrequisitos

Haber trabajado 1.4 · NumPy.

Ruta de estudio

El núcleo obligatorio comprende Series y DataFrames, inspección, selección, filtrado, tipos y faltantes, agrupaciones, merge, lectura y escritura, y pivot_table. El anti-join, apply y la revisión detallada de cambios de Pandas 3.x se pueden abordar como ampliación guiada después de que la práctica básica sea correcta.

Orden de los ejemplos

Los ejemplos se apoyan en el df y en las tablas creadas en bloques anteriores. Recorre la página de arriba abajo o copia también la celda de preparación cuando quieras probar un fragmento de forma aislada.

1. Series

Una Series es una columna con índice:

import pandas as pd

serie = pd.Series([12000, 25000, 41000])
print(serie)

precios = pd.Series([10, 20, 30], index=["raton", "teclado", "monitor"])
print(precios)
print(precios["monitor"])   # acceso por etiqueta

Salida esperada:

0    12000
1    25000
2    41000
dtype: int64
raton      10
teclado    20
monitor    30
dtype: int64
30

A la izquierda de cada valor está el índice (las etiquetas de las filas). Cuando no lo defines, Pandas asigna 0, 1, 2…; cuando lo defines, puedes acceder por etiqueta (precios["monitor"]).

La clave está en la alineación por índice: al operar dos Series, Pandas empareja por etiqueta, no por posición.

s1 = pd.Series({"a": 1, "b": 2})
s2 = pd.Series({"b": 20, "c": 30})
print(s1 + s2)

Salida esperada:

a     NaN
b    22.0
c     NaN
dtype: float64

Fíjate en lo que ocurre: b existe en ambas, así que suma 2 + 20 = 22; a y c solo existen en una, así que el resultado es NaN (no hay pareja). Esto es distinto de NumPy (que suma por posición) y es una de las ideas más importantes de Pandas: los datos se emparejan por sus etiquetas. Evita errores cuando las tablas no están en el mismo orden.

2. DataFrames e inspección

import pandas as pd

df = pd.DataFrame({
    "producto": ["raton", "teclado", "monitor"],
    "precio": [10.0, 25.0, 130.0],
    "unidades": [12, 5, 2],
})

print(df)
print("shape:", df.shape)
print(df.dtypes)      # tipo de cada columna
print(df.head(2))     # primeras filas

Salida esperada:

  producto  precio  unidades
0    raton    10.0        12
1  teclado    25.0         5
2  monitor   130.0         2
shape: (3, 3)
producto        str
precio      float64
unidades      int64
dtype: object
  producto  precio  unidades
0    raton    10.0        12
1  teclado    25.0         5

Un DataFrame es una tabla de Series alineadas: cada columna es una Series y comparten el mismo índice (las filas). Los tres métodos de inspección que usarás siempre:

  • shape: (filas, columnas).
  • dtypes: el tipo de cada columna (crucial para detectar errores: un número leído como texto, por ejemplo).
  • head(n): las primeras n filas, para "ver" la tabla sin imprimirla entera.

Fíjate en el dtype de producto: str. En Pandas 3.x las columnas de texto son str por defecto (antes eran object). Y recuerda describe() (de 1.2) para un resumen numérico inmediato.

3. Selección y filtrado

print(df["precio"])                       # una columna
print(df[["producto", "unidades"]])       # varias columnas
print(df[df["precio"] > 20])              # filtrado booleano
print(df[(df["precio"] > 20) & (df["unidades"] > 3)])  # varias condiciones

Salida esperada:

0     10.0
1     25.0
2    130.0
Name: precio, dtype: float64
  producto  unidades
0    raton        12
1  teclado         5
2  monitor         2
  producto  precio  unidades
1  teclado    25.0         5
2  monitor   130.0         2
  producto  precio  unidades
1  teclado    25.0         5

El patrón es el mismo que en NumPy: df[condición] se queda con las filas donde la condición es True. Con una sola columna obtienes una Series; con varias, un DataFrame.

Paréntesis y operadores

Como en NumPy, combina condiciones con &, |, ~ y paréntesis. No uses and/or (fallan con Series).

3.1 loc e iloc

  • loc selecciona por etiqueta.
  • iloc selecciona por posición.
print(df.loc[1])                             # fila con etiqueta 1
print(df.iloc[1])                            # fila en posición 1
print(df.loc[0:1, ["producto", "precio"]])   # filas 0-1 (inclusive), dos columnas
print(df.iloc[0:2, 0:2])                     # posiciones 0-1, columnas 0-1

Salida esperada:

producto    teclado
precio         25.0
unidades          5
Name: 1, dtype: object
producto    teclado
precio         25.0
unidades          5
Name: 1, dtype: object
  producto  precio
0    raton    10.0
1  teclado    25.0
  producto  precio
0    raton    10.0
1  teclado    25.0

Aquí etiqueta y posición coinciden (el índice es 0, 1, 2), así que ambos dan lo mismo. Pero en cuanto el índice deje de ser 0, 1, 2… la diferencia importa, y mucho. Dos detalles que confunden siempre:

  • loc[0:1] incluye el final del rango (etiqueta 1).
  • iloc[0:2] excluye el final (posiciones 0 y 1, como el slicing de Python).

Cómo acordarte

iloc se comporta como el [inicio:fin] de Python (final excluido); loc se comporta como "dame las etiquetas entre estas dos, incluidas". Cuando dudes, piensa: iloc = integer location (posición), loc = label (etiqueta).

4. Eliminar filas y columnas

df2 = df.copy()
df2["categoria"] = ["periferico", "periferico", "pantalla"]

print(df2.drop(columns=["unidades"]))   # sin la columna unidades
print(df2.drop(index=1))                # sin la fila 1

Salida esperada:

  producto  precio   categoria
0    raton    10.0  periferico
1  teclado    25.0  periferico
2  monitor   130.0    pantalla
  producto  precio  unidades   categoria
0    raton    10.0        12  periferico
2  monitor   130.0         2    pantalla

Fíjate en df2 = df.copy(): hacemos una copia antes de tocar nada para no modificar el df original. Es un hábito importante (relacionado con el Copy-on-Write que veremos al final).

inplace: mejor evitarlo

drop devuelve un DataFrame nuevo y no modifica el original. El parámetro inplace=True existe, pero en Pandas 3 se desaconseja: asigna el resultado (df2 = df2.drop(...)) para que el código sea explícito y no haya sorpresas.

5. Tipos de datos, nulos y conversiones

La mayoría de los bugs de análisis de datos no están en el algoritmo, sino en el tipo de dato de cada columna: un número leído como texto, una fecha como texto o un nulo escondido. Este apartado junta tres ideas que los tutoriales suelen dar por separado y que en realidad van de la mano: qué dtype hay, qué es un nulo y cómo convertir entre tipos.

5.1 Los dtype esenciales

Cada columna tiene un único dtype; no puede mezclar enteros y texto (si lo haces, Pandas degrada la columna a object). Los que usarás casi siempre:

dtype Qué guarda Ejemplo
int64 Enteros, sin nulos 12
float64 Decimales y NaN 12.5, nan
str Texto (en 3.x; antes era object) "raton"
bool True / False True
datetime64 Fechas y horas 2026-01-10
category Valores repetidos (poca variedad) "bajo", "alto"
Int64, string, boolean Versiones nullable (mayúscula) que admiten nulos 1, <NA>
df_tipos = pd.DataFrame({
    "producto": ["raton", "teclado", "monitor"],
    "precio": [10.0, 25.0, 130.0],
    "unidades": [12, 5, 2],
    "en_stock": [True, False, True],
})

print(df_tipos.dtypes)

Salida esperada:

producto        str
precio      float64
unidades      int64
en_stock       bool
dtype: object

Dos detalles importantes. Primero, producto es str: en Pandas 3.x las columnas de texto ya no son object. Segundo, ese dtype: object del final no es el tipo de ninguna columna: es el tipo de la Series que contiene los nombres de los dtypes (la línea que más confunde al empezar).

object sigue existiendo, pero solo para columnas heterogéneas (por ejemplo, una columna que mezcla números y texto). Es el «cajón de sastre»: si lo ves, normalmente significa que hay valores mezclados o algo que Pandas no pudo tipar.

5.2 Inspeccionar los tipos

print(df_tipos.dtypes)                # tipo por columna
print(df_tipos.dtypes.value_counts()) # cuántas columnas hay de cada tipo
df_tipos.info()                       # resumen con nulos y memoria

Salida esperada:

producto        str
precio      float64
unidades      int64
en_stock       bool
dtype: object
str        1
float64    1
int64      1
bool       1
Name: count, dtype: int64
<class 'pandas.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype
---  ------    --------------  -----
 0   producto  3 non-null      str
 1   precio    3 non-null      float64
 2   unidades  3 non-null      int64
 3   en_stock  3 non-null      bool
dtypes: bool(1), float64(1), int64(1), str(1)
memory usage: 226.0 bytes

info() es el más útil al empezar: la columna Non-Null Count dice cuántos valores no nulos tiene cada columna. Si no coincide con el número de filas, hay nulos escondidos, y esa misma línea te dice en qué columna mirar.

5.3 Los nulos y su dtype

Un nulo es «aquí no hay dato». Pandas reconoce varios sinónimos: np.nan (numérico), None (Python) y pd.NA (el nulo moderno); en fechas, el nulo es NaT. isna() los detecta todos:

import numpy as np

print(pd.Series([np.nan, None, pd.NA]).isna().tolist())  # [True, True, True]
print(pd.Series([1, 2, None]).dtype)                     # float64
print(pd.Series(["raton", None]).dtype)                  # str
print(pd.Series([True, None]).dtype)                     # object

Salida esperada:

[True, True, True]
float64
str
object

La regla que evita el 90 % de las confusiones: el nulo vive dentro del dtype.

  • int64 no puede guardar nulos; al meter un None, Pandas degrada la columna a float64 para poder usar NaN.
  • float64 guarda NaN de forma natural.
  • str (3.x) es nullable: guarda el nulo sin degradarse.
  • bool con un None se degrada a object (por eso existen Int64, boolean y string, que conservan el tipo aunque haya nulos).

5.4 Detectar y tratar nulos

df3 = pd.DataFrame({
    "producto": ["raton", "teclado", None],
    "precio": [10.0, np.nan, 130.0],
})

print(df3.isna())                  # máscara: True = nulo
print(df3.isna().sum())            # cuántos nulos por columna
print(df3.dropna())                # eliminar filas con nulos
print(df3.fillna({"precio": df3["precio"].mean()}))  # imputar la media

Salida esperada:

   producto  precio
0     False   False
1     False    True
2      True   False
producto    1
precio      1
dtype: int64
  producto  precio
0    raton    10.0
  producto  precio
0    raton    10.0
1  teclado    70.0
2      NaN   130.0

isna() devuelve una máscara booleana (True = nulo), y isna().sum() la suma por columna. dropna() se queda solo con las filas completas, y fillna() rellena: aquí la media de precio es 70.0, así que el NaN de teclado pasa a 70.0. Observa que la fila 2 conserva su producto nulo: el diccionario {"precio": ...} solo rellena esa columna.

Cuando más adelante prepares datos para un modelo, esa media se calculará solo con los datos de entrenamiento; calcularla con toda la tabla filtra información de los datos de evaluación (fuga de información, Unidad 3).

Los datos reales siempre tienen valores faltantes (un formulario sin rellenar, un sensor que falló, un campo que no aplica). Por eso el primer paso es detectarlos y luego decidir. Tres estrategias, de menos a más «agresiva»:

  1. Dejarlos si son pocos y no afectan.
  2. Imputarlos con un valor razonable: la media, la mediana o un valor del contexto.
  3. Eliminar la fila (dropna) — pero pierdes el resto de columnas de esa fila.

No borres a la ligera

dropna() elimina filas enteras: pierdes las demás columnas de esa fila. Antes de imputar o borrar, pregúntate por qué falta el dato (¿error de captura?, ¿no aplica?, ¿se perdió al exportar?) y documéntalo. En Pandas 3 ya no existe fillna(method=...); usa ffill()/bfill() o pasa un valor concreto.

5.5 Conversiones de tipos

print(pd.to_numeric(pd.Series(["10", "25", "no aplica"]), errors="coerce"))
print()
print(pd.Series([1.5, 2.7]).astype(int))            # ¡trunca, no redondea!
print()
print(pd.Series([1.5, 2.7]).round().astype(int))    # redondea y convierte
print()
print(pd.Series([1.0, np.nan]).astype("Int64"))     # entero nullable
print()
print(pd.to_datetime(pd.Series(["2026-01-10", "2026-02-15", "??"]), errors="coerce"))

Salida esperada:

0    10.0
1    25.0
2     NaN
dtype: float64

0    1
1    2
dtype: int64

0    2
1    3
dtype: int64

0       1
1    <NA>
dtype: Int64

0   2026-01-10
1   2026-02-15
2          NaT
dtype: datetime64[us]

El error más típico es astype(int) sobre decimales: trunca (1.5 → 1), no redondea. Si quieres redondear, haz .round() antes. Y si la columna tiene nulos, astype(int) lanza IntCastingNaNError: usa astype("Int64"), que los conserva como <NA>.

Para que Pandas infiera los tipos más adecuados de todo el DataFrame (los nullable incluidos), existe convert_dtypes():

d = pd.DataFrame({"a": [1, 2, None], "b": ["x", "y", None], "c": [True, False, None]})
print(d.dtypes)
print(d.convert_dtypes().dtypes)

Salida esperada:

a    float64
b        str
c     object
dtype: object
a      Int64
b     string
c    boolean
dtype: object

Referencia rápida:

Si necesitas… Usa
Texto → número (inválidos a NaN) pd.to_numeric(col, errors="coerce")
Texto → fecha (inválidas a NaT) pd.to_datetime(col, errors="coerce")
Decimal → entero redondeando col.round().astype(int)
Decimal → entero con nulos col.astype("Int64")
Texto con pocos valores repetidos col.astype("category")
Que Pandas infiera los tipos df.convert_dtypes()

6. Agrupar con groupby

groupby responde a "¿cuánto hay por categoría?", "¿cuál es la media por región?"… Es la operación de resumen más usada del análisis de datos.

ventas = pd.DataFrame({
    "categoria": ["periferico", "pantalla", "periferico", "pantalla", "audio"],
    "importe": [120.0, 300.0, 80.0, 250.0, 60.0],
})

print(ventas.groupby("categoria")["importe"].sum().sort_index())
print(ventas.groupby("categoria")["importe"].agg(["mean", "max"]).sort_index())

Salida esperada:

categoria
audio          60.0
pantalla      550.0
periferico    200.0
Name: importe, dtype: float64
             mean    max
categoria
audio        60.0   60.0
pantalla    275.0  300.0
periferico  100.0  120.0

El patrón es dividir-aplicar-combinar: Pandas divide la tabla en grupos (por categoria), aplica una función a cada grupo (sum, mean…) y combina los resultados en una tabla. agg(["mean", "max"]) aplica varias funciones a la vez.

  • transform devuelve un valor por fila (misma forma que el original).
  • filter devuelve los grupos completos que cumplen una condición.
ventas["media_grupo"] = ventas.groupby("categoria")["importe"].transform("mean")
print(ventas)

# lambda g: ... es una función breve que recibe cada grupo g (un DataFrame)
grandes = ventas.groupby("categoria").filter(lambda g: g["importe"].sum() > 150)
print(grandes)

Salida esperada:

    categoria  importe  media_grupo
0  periferico    120.0        100.0
1    pantalla    300.0        275.0
2  periferico     80.0        100.0
3    pantalla    250.0        275.0
4       audio     60.0         60.0
    categoria  importe  media_grupo
0  periferico    120.0        100.0
1    pantalla    300.0        275.0
2  periferico     80.0        100.0
3    pantalla    250.0        275.0

¿Cuál es la diferencia? sum() te deja una fila por grupo; transform() te deja una fila por fila original, con el valor de su grupo repetido (útil para, p. ej., comparar cada venta con la media de su categoría). filter() selecciona grupos enteros: aquí descarta "audio" porque su suma (60) no supera 150.

observed=True con categóricas

Si agrupas por una columna category, usa groupby(..., observed=True) para no arrastrar categorías vacías. Es el comportamiento correcto y explícito en Pandas 3.

7. Combinar tablas

En la vida real los datos están repartidos en varias tablas (clientes por un lado, pedidos por otro) y hay que unirlas para analizar.

productos = pd.DataFrame({
    "id_producto": [101, 102, 103],
    "nombre": ["raton", "teclado", "monitor"],
})
pedidos = pd.DataFrame({
    "id_producto": [101, 103, 104],
    "unidades": [2, 1, 5],
})

print(productos.merge(
    pedidos, on="id_producto", how="inner", validate="one_to_many"))
print(productos.merge(
    pedidos, on="id_producto", how="left", validate="one_to_many"))

Salida esperada:

   id_producto   nombre  unidades
0          101    raton         2
1          103  monitor         1
   id_producto   nombre  unidades
0          101    raton       2.0
1          102  teclado       NaN
2          103  monitor       1.0
  • inner: solo las claves presentes en ambas tablas (el producto 102 no tiene pedido y el pedido 104 no tiene producto: ambos desaparecen).
  • left: todas las filas de la izquierda; las que no tienen pareja quedan con NaN (el teclado se mantiene aunque nadie lo haya pedido).

La elección entre inner y left es una decisión de negocio: ¿te importa conservar los productos sin ventas? Si sí, left; si solo te interesan las combinaciones existentes, inner.

Fíjate también en unidades: en el left pasa de entero a decimal (2.0) porque tiene que guardar un NaN para el teclado, igual que viste en 5.3.

Cardinalidad: comprueba cuántas veces aparece cada clave

validate declara la cardinalidad que esperas y hace que pandas lance un MergeError si los datos no la cumplen. Aquí, "one_to_many" significa «cada producto aparece una vez en la tabla izquierda y puede aparecer muchas en la derecha» (un producto tiene muchos pedidos). Si por error productos tuviera un id repetido, cada pedido de ese producto se duplicaría en silencio al unir, e inflaría cualquier suma posterior. Las opciones son "one_to_one", "one_to_many", "many_to_one" y "many_to_many": el orden se refiere a la tabla izquierda y a la derecha, en ese orden.

7.1 Anti-join: lo que está en una tabla y no en la otra

combinado = productos.merge(pedidos, on="id_producto", how="left", indicator=True)
sin_pedido = combinado.loc[combinado["_merge"] == "left_only", ["id_producto", "nombre"]]
print(sin_pedido)

Salida esperada:

   id_producto   nombre
1          102  teclado

indicator=True añade la columna _merge que dice de dónde salió cada fila (both, left_only, right_only); filtrar left_only es el anti-join (productos que nadie ha pedido). Es una de las consultas más útiles para auditar datos: encontrar clientes sin pedidos, productos sin stock, registros huérfanos…

En Pandas 3 también puedes expresar directamente el anti-join, sin crear la columna auxiliar:

sin_pedido_nativo = productos.merge(
    pedidos, on="id_producto", how="left_anti")[["id_producto", "nombre"]]
sin_pedido_nativo = sin_pedido_nativo.reset_index(drop=True)
print(sin_pedido_nativo)

Salida esperada:

   id_producto   nombre
0          102  teclado

Usa indicator=True cuando necesites auditar el origen de cada fila; usa how="left_anti" cuando solo necesites las filas sin correspondencia.

Para apilar verticalmente (mismas columnas) se usa concat:

a = pd.DataFrame({"valor": [1, 2]})
b = pd.DataFrame({"valor": [3, 4]})
print(pd.concat([a, b], ignore_index=True))

Salida esperada:

   valor
0      1
1      2
2      3
3      4

8. Leer y escribir datos

ventas.to_csv("ventas.csv", index=False)   # escribir
leido = pd.read_csv("ventas.csv")          # leer
print(leido)

Salida esperada:

    categoria  importe  media_grupo
0  periferico    120.0        100.0
1    pantalla    300.0        275.0
2  periferico     80.0        100.0
3    pantalla    250.0        275.0
4       audio     60.0         60.0

Este bloque crea el archivo ventas.csv en tu carpeta (esa es la comprobación visible). El index=False evita que se escriba el índice como una columna extra (que al releer causaría una columna "Unnamed: 0"). Con el mismo patrón: read_excel / to_excel (requiere el paquete opcional openpyxl) y read_json / to_json.

9. Formato largo y ancho

largo = pd.DataFrame({
    "dia": ["lun", "lun", "mar", "mar"],
    "categoria": ["audio", "pantalla", "audio", "pantalla"],
    "importe": [10, 20, 15, 25],
})

ancho = largo.pivot_table(index="dia", columns="categoria",
                          values="importe", aggfunc="sum")
print(ancho)

otra_vez_largo = ancho.reset_index().melt(
    id_vars="dia", var_name="categoria", value_name="importe")
print(otra_vez_largo)

Salida esperada:

categoria  audio  pantalla
dia
lun           10        20
mar           15        25
   dia categoria  importe
0  lun     audio       10
1  mar     audio       15
2  lun  pantalla       20
3  mar  pantalla       25
  • Formato largo: una fila por cada combinación (día, categoría) con su valor. Es como suelen venir los datos "crudos" y como los prefieren las librerías de gráficos.
  • Formato ancho: cada categoría es una columna. Es más cómodo de leer para las personas.
  • pivot_table: de largo a ancho (resume). melt: de ancho a largo (desapila). Saber pasar de uno a otro te desatasca muchísimos análisis.

10. Ejemplo integral: resumen de ventas

ventas_full = pd.DataFrame({
    "categoria": ["periferico", "pantalla", "audio", "periferico", "pantalla", "audio"],
    "region": ["norte", "norte", "sur", "sur", "norte", "sur"],
    "importe": [120, 300, 60, 80, 250, 45],
})

resumen = (ventas_full
           .groupby(["region", "categoria"])["importe"]
           .sum()
           .reset_index()
           .sort_values("importe", ascending=False))
print(resumen)
print(ventas_full.groupby("region")["importe"].sum())

Salida esperada:

  region   categoria  importe
0  norte    pantalla      550
1  norte  periferico      120
2    sur       audio      105
3    sur  periferico       80
region
norte    670
sur      185
Name: importe, dtype: int64

Fíjate en el encadenado de métodos: groupby(...).sum().reset_index() .sort_values(...). Cada paso devuelve un DataFrame y el siguiente opera sobre él, como una tubería. Es el estilo idiomático de Pandas: una cadena de pasos que se lee casi como una frase ("agrupa, suma, aplana y ordena").

11. Pandas 3.x: lo que cambia

Trabajamos con Pandas 3.0. Ten presentes estos cambios al leer tutoriales antiguos (la mayoría de ejemplos de Internet aún usan la 2.x):

  • Texto como str por defecto. Las columnas de texto ya no son object, son str.
  • Copy-on-Write (CoW). Una selección que podría compartir memoria se separa antes de modificarla, de modo que cambiar la selección no cambia el DataFrame original:

    d = pd.DataFrame({"a": [1, 2, 3]})
    seleccion = d["a"]
    seleccion.iloc[0] = 99
    print("selección:", seleccion.tolist())
    print("original:", d["a"].tolist())
    

    Salida esperada:

    selección: [99, 2, 3]
    original: [1, 2, 3]
    

    La asignación d["b"] = d["a"] * 10 calcula una nueva serie en ese momento; no crea un vínculo entre a y b. El ejemplo anterior sí demuestra CoW: una modificación hecha sobre la selección queda aislada y el original conserva sus datos.

  • pd.col("nombre") para referirse a una columna dentro de métodos como assign:

    print(pd.DataFrame({"a": [1, 2]}).assign(doble=pd.col("a") * 2))
    

    Salida esperada:

       a  doble
    0  1      2
    1  2      4
    
  • GroupBy.apply. Si necesitas aplicar una función a cada grupo, ten en cuenta que apply ya excluye la columna por la que agrupas (include_groups=False es su valor por defecto y la otra opción se eliminó en Pandas 3). Comprueba la forma del resultado, porque depende de qué devuelva la función. Para resúmenes habituales, agg, transform o filter suelen expresar mejor la intención y son más fáciles de validar.

  • APIs retiradas. Ya no existen applymap (usa map sobre el DataFrame) ni fillna(method=...) (usa ffill/bfill o un valor). Para convertir a número de forma segura, usa pd.to_numeric(..., errors="coerce"); no ocultes conversiones fallidas con errors="ignore".

Errores frecuentes

  • Usar and/or en el filtrado en lugar de &/| con paréntesis.
  • Confundir loc (etiquetas) con iloc (posiciones).
  • Olvidar how="left" y perder filas sin pareja en un merge.
  • Borrar faltantes sin preguntarse por qué faltan.
  • Asignar con inplace=True en lugar de reasignar.
  • Copiar ejemplos antiguos con applymap o fillna(method=...).
  • Olvidar index=False al guardar un CSV y acabar con una columna basura.

Práctica de transferencia

Crea el CSV techshop_ventas.csv con estas filas (cópialas a mano o genéralas con Python):

fecha,categoria,region,unidades,importe
2026-09-01,audio,norte,2,60
2026-09-01,pantalla,norte,1,300
2026-09-02,audio,sur,3,45
2026-09-02,periferico,sur,4,80
2026-09-03,pantalla,norte,1,250
2026-09-03,audio,sur,1,30
2026-09-04,periferico,norte,2,120
2026-09-05,audio,sur,0,30
  1. Lee el CSV y muestra sus primeras filas, su shape y sus dtypes.
  2. Calcula el importe total por categoría y ordénalo de mayor a menor.
  3. Comprueba primero si hay unidades iguales a cero. Decide cómo tratar esa fila y después crea precio_medio = importe / unidades; explica por qué el resultado original no tiene sentido.
  4. Usa pivot_table para ver el importe por categoria (filas) y region (columnas).
  5. Combina (merge) este DataFrame con otro que traiga la categoría "madre" de cada categoría (audio → sonido, pantalla → imagen, periferico → accesorios), valida la cardinalidad con validate="many_to_one" y comprueba con indicator=True que no quedan filas huérfanas.

Qué debes comprobar: la tabla tiene shape (8, 5); el importe total es 915 € (pantalla 550, periférico 200 y audio 165); y el número de filas no cambia tras el merge del punto 5. Si cambia, alguna categoría está repetida en la tabla de categorías madre.

Producto evaluable

Cuaderno 03_pandas.py con:

  1. Los ejercicios 1–5 anteriores, cada uno con su celda de código y una celda Markdown que interprete el resultado.
  2. Una celda final que resuma: total de ventas, mejor categoría y peor región, explicando cómo llegaste a cada número.

Formato de entrega: cuaderno marimo ejecutado, con conclusiones al final.

Resumen y referencia rápida

Tarea Función/patrón
Crear Series/DataFrame pd.Series, pd.DataFrame
Inspeccionar head, shape, dtypes, describe
Seleccionar df["col"], df[["a","b"]]
Filtrar df[cond] con &, |, ~
Etiqueta/posición loc / iloc
Eliminar drop(columns=...), drop(index=...)
Faltantes isna, dropna, fillna
Convertir tipos to_numeric(errors="coerce"), to_datetime(errors="coerce"), astype (con Int64 para nulos), round().astype(int), convert_dtypes, astype("category")
Agrupar groupby(...).sum()/agg()/transform()/filter()
Combinar merge(validate=..., how=...), concat, anti-join con how="left_anti" o indicator=True
Leer/escribir read_csv/to_csv (+ read_excel, read_json)
Reshape pivot_table, melt

Siguiente: 1.6 · SQL y bases de datos.