0%
Pandas: Manipular y Analizar Datos en Python

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.

I

InSkillCoach

· min

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.nan siempre devuelve False; utilice df["col"].isna().
  • Tipos object silenciosos: una columna de números que contiene una cadena pasa a ser object; compruébelo con df.dtypes y convierta con pd.to_numeric(s, errors="coerce").
  • inplace=True: desaconsejado por el equipo de Pandas; prefiera la reasignación df = df.dropna().

Buenas prácticas

  • Inspeccione sistemáticamente cada nuevo conjunto de datos con head, info y describe antes de cualquier transformación.
  • Utilice loc/iloc de forma explícita para cualquier escritura y prohíba la indexación encadenada (df[...][...] = ...).
  • Vectorice: evite apply y 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, query y pipe en 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.

InSkillCoach

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+
1.2k
62

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!