Pandas: Manipular y Analizar Datos en Python
Guía completa de Pandas para cargar, limpiar, filtrar, agregar y exportar datos tabulares en Python: Series, DataFrame, groupby, merge y fechas.
InSkillCoach
Pandas: Manipular y Analizar Datos en Python
Pandas es la biblioteca de referencia para la manipulación de datos tabulares en Python. Construida sobre NumPy, ofrece dos estructuras de datos esenciales, la Series y el DataFrame, así como un completo arsenal para cargar archivos, limpiar valores faltantes, filtrar, agregar, unir y exportar datos. Esta guía cubre el flujo de trabajo completo de un análisis: desde la lectura de un CSV hasta la exportación de los resultados, pasando por las trampas clásicas que conviene evitar.
Series y DataFrame: las dos estructuras fundamentales
La Series: una columna etiquetada
Una Series es un array unidimensional en el que cada valor está asociado a una etiqueta de índice.
import pandas as pd
s = pd.Series([15, 12, 17, 9], index=["Alice", "Bob", "Chloé", "David"])
print(s)
# Alice 15
# Bob 12
# Chloé 17
# David 9
# dtype: int64
# Acceso por etiqueta o por posición
print(s["Chloé"]) # 17
print(s.iloc[0]) # 15
# Las operaciones están vectorizadas, como con NumPy
print(s.mean()) # 13.25
print(s + 1) # cada nota aumentada en 1
El DataFrame: una tabla de dos dimensiones
Un DataFrame es un conjunto de columnas (Series) que comparten el mismo índice de filas.
df = pd.DataFrame({
"nom": ["Alice", "Bob", "Chloé", "David"],
"age": [25, 32, 28, 41],
"ville": ["Paris", "Lyon", "Paris", "Nantes"],
"salaire": [42000, 38000, 51000, 46000],
})
print(df)
# nom age ville salaire
# 0 Alice 25 Paris 42000
# 1 Bob 32 Lyon 38000
# 2 Chloé 28 Paris 51000
# 3 David 41 Nantes 46000
# Una columna es una Series
print(df["age"].dtype) # int64
print(df.shape) # (4, 4)
print(df.columns.tolist()) # ['nom', 'age', 'ville', 'salaire']
Lectura de archivos con read_csv
read_csv es la función más utilizada de Pandas. Sus opciones cubren prácticamente todos los formatos CSV que se encuentran en la práctica.
# Lectura simple
df = pd.read_csv("ventes.csv")
# Opciones habituales
df = pd.read_csv(
"ventes.csv",
sep=";", # separador (el punto y coma es frecuente en Francia)
decimal=",", # coma decimal
encoding="utf-8", # o "latin-1" para archivos antiguos
usecols=["date", "produit", "montant"], # cargar solo algunas columnas
dtype={"produit": "string"}, # forzar un tipo
parse_dates=["date"], # convertir a datetime desde la lectura
na_values=["N/A", "inconnu"], # valores que se tratan como faltantes
nrows=1000, # limitar el número de filas (útil para explorar)
)
Pandas también lee muchos otros formatos: read_excel, read_json, read_parquet, read_sql. Para grandes volúmenes, el formato Parquet es notablemente más rápido y compacto que el CSV.
Inspección: head, info, describe
Los tres primeros comandos que conviene ejecutar sobre cualquier conjunto de datos nuevo:
# Vista previa de las primeras filas
print(df.head()) # 5 primeras filas (df.tail() para las últimas)
# Estructura: tipos, valores no nulos, memoria
df.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 4 entries, 0 to 3
# Data columns (total 4 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 nom 4 non-null object
# 1 age 4 non-null int64
# 2 ville 4 non-null object
# 3 salaire 4 non-null int64
# Estadísticas descriptivas de las columnas numéricas
print(df.describe())
# age salaire
# count 4.000000 4.000000
# mean 31.500000 44250.000000
# std 6.952218 5560.275773
# min 25.000000 38000.000000
# max 41.000000 51000.000000
# Recuento de los valores distintos de una columna
print(df["ville"].value_counts())
# Paris 2
# Lyon 1
# Nantes 1
Selección con loc e iloc
Dos accesores complementarios: loc selecciona por etiquetas, iloc por posiciones enteras.
# loc[filas, columnas] por etiquetas
print(df.loc[0, "nom"]) # Alice
print(df.loc[0:2, ["nom", "ville"]]) # filas 0 a 2 INCLUIDAS, dos columnas
# iloc[filas, columnas] por posiciones
print(df.iloc[0, 0]) # Alice
print(df.iloc[0:2, 0:2]) # filas 0 y 1 (límite excluido), columnas 0 y 1
# Todas las filas, una columna
print(df.loc[:, "salaire"])
# Última fila
print(df.iloc[-1])
Atención a la diferencia de convención: loc[0:2] incluye el límite 2 (lógica de etiquetas), mientras que iloc[0:2] lo excluye (lógica de posiciones, como las listas de Python).
Filtrado booleano
# Una condición
seniors = df[df["age"] > 30]
print(seniors)
# nom age ville salaire
# 1 Bob 32 Lyon 38000
# 3 David 41 Nantes 46000
# Condiciones combinadas: & (y), | (o), ~ (no), paréntesis obligatorios
cible = df[(df["ville"] == "Paris") & (df["salaire"] >= 45000)]
print(cible)
# nom age ville salaire
# 2 Chloé 28 Paris 51000
# Pertenencia a una lista
grandes_villes = df[df["ville"].isin(["Paris", "Lyon"])]
# Filtrado sobre texto
noms_a = df[df["nom"].str.startswith("A")]
# query: sintaxis alternativa legible
resultat = df.query("age > 30 and salaire < 47000")
Columnas calculadas
# Nueva columna a partir de las existentes
df["salaire_mensuel"] = df["salaire"] / 12
# Columna condicional con numpy.where
import numpy as np
df["categorie"] = np.where(df["age"] < 30, "junior", "senior")
# Transformación mediante función (reservar para casos no vectorizables)
df["initiale"] = df["nom"].apply(lambda n: n[0])
# assign: crear columnas encadenando las operaciones
df2 = df.assign(
salaire_k=df["salaire"] / 1000,
age_en_2030=df["age"] + 4,
)
print(df2[["nom", "salaire_k", "age_en_2030"]].head(2))
# nom salaire_k age_en_2030
# 0 Alice 42.0 29
# 1 Bob 38.0 36
Priorice siempre las operaciones vectorizadas (df["a"] + df["b"], np.where, .str, .dt): apply con una lambda suele ser entre 10 y 100 veces más lento.
Valores faltantes: isna, fillna, dropna
df_nan = pd.DataFrame({
"produit": ["A", "B", "C", "D"],
"prix": [10.0, None, 25.0, None],
"stock": [5, 3, None, 8],
})
# Detectar los valores faltantes
print(df_nan.isna())
print(df_nan.isna().sum()) # número de NaN por columna
# produit 0
# prix 2
# stock 1
# Reemplazar por un valor fijo
df_rempli = df_nan.fillna({"prix": 0.0, "stock": 0})
# Reemplazar por una estadística
df_nan["prix"] = df_nan["prix"].fillna(df_nan["prix"].mean())
print(df_nan["prix"])
# 0 10.0
# 1 17.5
# 2 25.0
# 3 17.5
# Eliminar las filas que contengan al menos un NaN
df_propre = df_nan.dropna()
# Eliminar solo si falta una columna concreta
df_propre = df_nan.dropna(subset=["stock"])
Elija la estrategia según el contexto: eliminar filas supone perder información, e imputar una media puede sesgar un análisis. Documente siempre su decisión.
groupby y agregaciones
El patrón «split-apply-combine»: dividir los datos por grupo, aplicar un cálculo y recombinar los resultados.
ventes = pd.DataFrame({
"region": ["Nord", "Sud", "Nord", "Sud", "Nord"],
"produit": ["A", "A", "B", "B", "A"],
"montant": [100, 150, 200, 120, 80],
})
# Agregación simple
print(ventes.groupby("region")["montant"].sum())
# region
# Nord 380
# Sud 270
# Varias estadísticas a la vez
print(ventes.groupby("region")["montant"].agg(["sum", "mean", "count"]))
# sum mean count
# region
# Nord 380 126.666667 3
# Sud 270 135.000000 2
# Agrupación por varias columnas
print(ventes.groupby(["region", "produit"])["montant"].sum())
# region produit
# Nord A 180
# B 200
# Sud A 150
# B 120
# Agregaciones con nombre: columnas de salida explícitas
resume = ventes.groupby("region").agg(
total=("montant", "sum"),
panier_moyen=("montant", "mean"),
)
print(resume)
# total panier_moyen
# region
# Nord 380 126.666667
# Sud 270 135.000000
Para transformar un resultado agrupado en una tabla cruzada, pivot_table suele ser más directo:
tableau = ventes.pivot_table(index="region", columns="produit",
values="montant", aggfunc="sum", fill_value=0)
print(tableau)
# produit A B
# region
# Nord 180 200
# Sud 150 120
Combinar tablas: merge y concat
merge: las uniones al estilo SQL
clients = pd.DataFrame({
"client_id": [1, 2, 3],
"nom": ["Alice", "Bob", "Chloé"],
})
commandes = pd.DataFrame({
"client_id": [1, 1, 2, 4],
"montant": [50, 30, 70, 20],
})
# Unión interna: solo las coincidencias
interne = pd.merge(clients, commandes, on="client_id", how="inner")
print(interne)
# client_id nom montant
# 0 1 Alice 50
# 1 1 Alice 30
# 2 2 Bob 70
# Unión izquierda: todos los clientes, incluso sin pedido
gauche = pd.merge(clients, commandes, on="client_id", how="left")
# Chloé aparece con montant NaN
# how también acepta "right" y "outer"
concat: apilar tablas
janvier = pd.DataFrame({"produit": ["A", "B"], "ventes": [10, 20]})
fevrier = pd.DataFrame({"produit": ["A", "B"], "ventes": [15, 25]})
# Apilado vertical con reindexación
annee = pd.concat([janvier, fevrier], ignore_index=True)
print(annee)
# produit ventes
# 0 A 10
# 1 B 20
# 2 A 15
# 3 B 25
Ordenación
# Ordenar por una columna
print(df.sort_values("age"))
# Orden descendente por varias columnas
print(df.sort_values(["ville", "salaire"], ascending=[True, False]))
# Ordenar por el índice
print(df.sort_index())
# Los n valores más grandes (más rápido que una ordenación completa)
print(df.nlargest(2, "salaire")[["nom", "salaire"]])
# nom salaire
# 2 Chloé 51000
# 3 David 46000
Fechas: to_datetime y resample
journal = pd.DataFrame({
"date": ["2026-01-05", "2026-01-12", "2026-02-03", "2026-02-20", "2026-03-01"],
"ca": [120, 80, 200, 150, 90],
})
# Conversión a datetime
journal["date"] = pd.to_datetime(journal["date"])
print(journal["date"].dtype) # datetime64[ns]
# El accesor .dt expone los componentes
journal["mois"] = journal["date"].dt.month
journal["jour_semaine"] = journal["date"].dt.day_name()
# resample: agregación temporal (requiere un índice datetime)
serie = journal.set_index("date")["ca"]
mensuel = serie.resample("ME").sum() # "ME" = fin de mes (pandas >= 2.2)
print(mensuel)
# date
# 2026-01-31 200
# 2026-02-28 350
# 2026-03-31 90
# Filtrar un rango de fechas
recentes = journal[journal["date"] >= "2026-02-01"]
Si el formato no es estándar, indíquelo: pd.to_datetime(s, format="%d/%m/%Y") evita las ambigüedades entre día y mes.
Exportación de los resultados
# CSV (index=False para no escribir la columna de índice)
df.to_csv("resultats.csv", index=False, sep=";", encoding="utf-8")
# Excel (requiere openpyxl)
df.to_excel("resultats.xlsx", sheet_name="Analyse", index=False)
# Parquet: rápido, compacto, conserva los tipos (requiere pyarrow)
df.to_parquet("resultats.parquet")
# JSON
df.to_json("resultats.json", orient="records", force_ascii=False)
Trampas habituales: el SettingWithCopyWarning
Esta advertencia aparece cuando modifica el resultado de una selección de la que Pandas no sabe si es una vista o una copia.
# MAL: indexación encadenada, la modificación puede perderse
sous_ensemble = df[df["ville"] == "Paris"]
sous_ensemble["salaire"] = 0 # SettingWithCopyWarning
# BIEN: modificar el DataFrame original en una sola operación loc
df.loc[df["ville"] == "Paris", "salaire"] = 0
# BIEN: trabajar sobre una copia explícita y asumida
sous_ensemble = df[df["ville"] == "Paris"].copy()
sous_ensemble["salaire"] = 0 # sin warning, df original intacto
Otras trampas frecuentes:
- Comparar con
NaN:df["col"] == np.nansiempre devuelveFalse; utilicedf["col"].isna(). - Tipos object silenciosos: una columna de números que contiene una cadena pasa a ser
object; compruébelo condf.dtypesy convierta conpd.to_numeric(s, errors="coerce"). inplace=True: desaconsejado por el equipo de Pandas; prefiera la reasignacióndf = df.dropna().
Buenas prácticas
- Inspeccione sistemáticamente cada nuevo conjunto de datos con
head,infoydescribeantes de cualquier transformación. - Utilice
loc/ilocde forma explícita para cualquier escritura y prohíba la indexación encadenada (df[...][...] = ...). - Vectorice: evite
applyy los bucles cuando exista una operación sobre columna completa. - Parsee las fechas desde la lectura con
parse_dates, en lugar de convertirlas después. - Nombre sus agregaciones con
agg(total=("montant", "sum"))para obtener columnas de salida claras. - Prefiera Parquet al CSV para los archivos intermedios: más rápido, más compacto y sin pérdida de tipos.
- Encadene las transformaciones de forma legible con
assign,queryypipeen lugar de multiplicar las variables intermedias.
Conclusión
Pandas cubre todo el ciclo de vida de un análisis de datos: carga robusta con read_csv, exploración con info y describe, limpieza de valores faltantes, filtrado booleano, columnas calculadas, agregaciones con groupby, uniones con merge, gestión precisa de fechas y exportación a múltiples formatos. Al comprender la diferencia entre loc e iloc y evitar la indexación encadenada, eliminará la gran mayoría de los errores sutiles. Los siguientes pasos naturales son la visualización de sus DataFrames con Matplotlib y el análisis estadístico con SciPy, dos bibliotecas que se integran de forma nativa con Pandas.
Acerca de InSkillCoach
Experto en formación y tecnologías
Coach especializado en tecnologías avanzadas e IA, impulsado por GNeurone Inc.
Certificaciones:
- AWS Certified Solutions Architect – Professional
- Certificaciones de Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comentarios
Los comentarios funcionan con GitHub Discussions
Inicia sesión con GitHub para participar en la discusión