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:
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.
Salida esperada:
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 primerasnfilas, 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¶
locselecciona por etiqueta.ilocselecciona 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:
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:
La regla que evita el 90 % de las confusiones: el nulo vive dentro del dtype.
int64no puede guardar nulos; al meter unNone, Pandas degrada la columna afloat64para poder usarNaN.float64guardaNaNde forma natural.str(3.x) es nullable: guarda el nulo sin degradarse.boolcon unNonese degrada aobject(por eso existenInt64,booleanystring, 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»:
- Dejarlos si son pocos y no afectan.
- Imputarlos con un valor razonable: la media, la mediana o un valor del contexto.
- 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:
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.
transformdevuelve un valor por fila (misma forma que el original).filterdevuelve 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:
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:
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:
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
strpor defecto. Las columnas de texto ya no sonobject, sonstr. -
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:
La asignación
d["b"] = d["a"] * 10calcula una nueva serie en ese momento; no crea un vínculo entreayb. 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 comoassign:Salida esperada:
-
GroupBy.apply. Si necesitas aplicar una función a cada grupo, ten en cuenta queapplyya excluye la columna por la que agrupas (include_groups=Falsees 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,transformofiltersuelen expresar mejor la intención y son más fáciles de validar. -
APIs retiradas. Ya no existen
applymap(usamapsobre el DataFrame) nifillna(method=...)(usaffill/bfillo un valor). Para convertir a número de forma segura, usapd.to_numeric(..., errors="coerce"); no ocultes conversiones fallidas conerrors="ignore".
Errores frecuentes¶
- Usar
and/oren el filtrado en lugar de&/|con paréntesis. - Confundir
loc(etiquetas) coniloc(posiciones). - Olvidar
how="left"y perder filas sin pareja en un merge. - Borrar faltantes sin preguntarse por qué faltan.
- Asignar con
inplace=Trueen lugar de reasignar. - Copiar ejemplos antiguos con
applymapofillna(method=...). - Olvidar
index=Falseal 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
- Lee el CSV y muestra sus primeras filas, su
shapey susdtypes. - Calcula el importe total por categoría y ordénalo de mayor a menor.
- 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. - Usa
pivot_tablepara ver el importe porcategoria(filas) yregion(columnas). - 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 convalidate="many_to_one"y comprueba conindicator=Trueque 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:
- Los ejercicios 1–5 anteriores, cada uno con su celda de código y una celda Markdown que interprete el resultado.
- 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.