Pandas básico: DataFrames, Series y análisis de datos en Python
- ¿Qué es Pandas y por qué domina el análisis de datos?
- DataFrame y Series: las dos estructuras fundamentales
- Leer y explorar datos desde archivos
- Selección con loc e iloc
- Filtrado booleano y limpieza de nulos
- Manipulación: ordenar, renombrar y transformar
- Agrupación con groupby
- Combinar DataFrames: merge y concat
- Preguntas frecuentes
Si NumPy es el motor matemático de Python, Pandas es el volante con el que cualquiera puede conducirlo. No necesitas saber de álgebra lineal ni de memoria contigua: le das un CSV, un Excel o una base de datos SQL, y Pandas te devuelve un DataFrame con el que puedes filtrar, agrupar, combinar y analizar igual que en SQL o Excel, pero con todo el poder de Python. Es la herramienta más usada en Data Science por una razón sencilla: hace manejable lo que sin ella sería caótico.
¿Qué es Pandas y por qué domina el análisis de datos?
Pandas nació en 2008 en el contexto de la gestión de activos financieros: Wes McKinney necesitaba una herramienta que permitiera manipular series temporales y tablas de datos con la flexibilidad de Python y la velocidad de NumPy. El resultado fue una librería que en quince años se ha convertido en el estándar de facto del análisis de datos en Python, con más de 100 millones de descargas mensuales.
Lo que hace especial a Pandas es la combinación de dos ideas: el DataFrame (una tabla con filas y columnas etiquetadas, como en SQL o en R) y la integración transparente con todo el ecosistema Python (NumPy, Matplotlib, scikit-learn, SQLAlchemy). No reemplaza a Excel; hace lo que Excel no puede: manejar millones de filas, automatizar transformaciones complejas y reproducir cada paso del análisis con código.
pip install pandas
import pandas as pd
import numpy as np # Pandas usa NumPy internamente
print(pd.__version__) # 2.x es la versión actual
df[(df['genero']=='jazz') & (df['año'] < 1970)]. Fuente: Pexels (licencia libre).DataFrame y Series: las dos estructuras fundamentales
Pandas tiene dos estructuras de datos principales. La Serie es un array unidimensional con etiquetas (un índice). El DataFrame es una colección de Series que comparten el mismo índice: una tabla con filas y columnas etiquetadas.
import pandas as pd
# ── Serie ──────────────────────────────────────────────────────
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print(s['b']) # 20
print(s.mean()) # 25.0
print(s.dtype) # int64
# Serie desde dict
s2 = pd.Series({'Madrid': 3.3, 'Barcelona': 1.6, 'Valencia': 0.8})
s2['Madrid'] # 3.3
# ── DataFrame ──────────────────────────────────────────────────
df = pd.DataFrame({
'nombre': ['Ana', 'Luis', 'María', 'Pedro'],
'edad': [28, 34, 22, 41],
'ciudad': ['Madrid', 'Barcelona', 'Sevilla', 'Madrid'],
'ventas': [1200.0, 850.0, 2100.0, 980.0],
})
print(df)
# nombre edad ciudad ventas
# 0 Ana 28 Madrid 1200.0
# 1 Luis 34 Barcelona 850.0
# 2 María 22 Sevilla 2100.0
# 3 Pedro 41 Madrid 980.0
print(df.shape) # (4, 4) → 4 filas, 4 columnas
print(df.dtypes) # object, int64, object, float64
print(df.index) # RangeIndex(start=0, stop=4, step=1)
print(df.columns) # Index(['nombre', 'edad', 'ciudad', 'ventas'])
# Acceder a columna → Serie
df['edad'] # Serie con los valores de edad
type(df['edad']) #
# Acceder a múltiples columnas → DataFrame
df[['nombre', 'ventas']]
pd.DataFrame(np.random.randn(4, 3), columns=['a', 'b', 'c']). Esto es habitual en Machine Learning, donde los datos ya están en formato matricial.
Leer y explorar datos desde archivos
La primera acción en cualquier proyecto de análisis de datos es cargar el dataset. Pandas tiene funciones de lectura para casi cualquier formato.
import pandas as pd
# ── Leer CSV ───────────────────────────────────────────────────
df = pd.read_csv('ventas.csv')
df = pd.read_csv('ventas.csv', sep=';') # separador punto y coma
df = pd.read_csv('ventas.csv', encoding='utf-8') # codificación explícita
df = pd.read_csv('ventas.csv', index_col='id') # columna como índice
df = pd.read_csv('ventas.csv', parse_dates=['fecha'])# parsear fechas
df = pd.read_csv('ventas.csv', usecols=['a','b','c'])# solo ciertas columnas
df = pd.read_csv('ventas.csv', nrows=1000) # primeras N filas
# ── Otros formatos ─────────────────────────────────────────────
df = pd.read_excel('libro.xlsx', sheet_name='Hoja1')
df = pd.read_json('datos.json')
df = pd.read_sql('SELECT * FROM ventas', connection) # SQLAlchemy o sqlite3
# ── Exploración inicial (siempre hacerlo primero) ──────────────
df.head(5) # primeras 5 filas
df.tail(3) # últimas 3 filas
df.sample(5) # 5 filas aleatorias
df.info() # tipos de dato, nulos, memoria
df.describe() # estadísticas básicas de columnas numéricas
df.shape # (filas, columnas)
df.dtypes # tipo de cada columna
df.isnull().sum() # ← SIEMPRE ejecutar: cuenta nulos por columna
# ── Exportar ───────────────────────────────────────────────────
df.to_csv('resultado.csv', index=False)
df.to_excel('resultado.xlsx', index=False)
df.to_json('resultado.json', orient='records')
df.shape → df.info() → df.describe() → df.isnull().sum() → df.head(). En cinco pasos entiendes la estructura, los tipos, las estadísticas, los valores ausentes y el aspecto real de los datos.
Selección con loc e iloc
Aquí es donde muchos principiantes se pierden: Pandas tiene dos sistemas de indexación. loc trabaja con etiquetas (nombres del índice y las columnas). iloc trabaja con posiciones enteras (0, 1, 2...). La confusión viene de que cuando el índice es el rango por defecto (0, 1, 2...), ambos dan el mismo resultado para filas, pero siguen siendo semánticamente distintos.
df = pd.DataFrame({
'nombre': ['Ana', 'Luis', 'María', 'Pedro'],
'edad': [28, 34, 22, 41],
'ciudad': ['Madrid', 'Barcelona', 'Sevilla', 'Madrid'],
}, index=[10, 20, 30, 40]) # índice no estándar para ilustrar la diferencia
# ── loc: por ETIQUETA ──────────────────────────────────────────
df.loc[20] # fila con índice 20 (Luis)
df.loc[10:30] # filas de índice 10 a 30 (¡inclusive!)
df.loc[10, 'edad'] # fila 10, columna 'edad'
df.loc[10:30, 'nombre':'ciudad'] # rango de filas y columnas
df.loc[df['edad'] > 25] # filtrado booleano
# ── iloc: por POSICION ─────────────────────────────────────────
df.iloc[0] # primera fila (sea cual sea su índice)
df.iloc[1:3] # filas en posición 1 y 2 (no incluye 3)
df.iloc[0, 1] # primera fila, segunda columna
df.iloc[:, 0:2] # todas las filas, primeras 2 columnas
df.iloc[-1] # última fila
# ── Acceso rápido a columna ────────────────────────────────────
df['nombre'] # Serie (corchetes siempre para columnas)
df[['nombre','edad']] # DataFrame (doble corchete = lista de columnas)
df.loc[10:30] incluye ambos extremos (10 y 30). df.iloc[1:3] no incluye el extremo derecho (solo posiciones 1 y 2). Este comportamiento inconsistente entre loc e iloc sorprende siempre la primera vez.
Filtrado booleano y limpieza de nulos
El filtrado booleano es el mecanismo principal para seleccionar filas según condiciones. Pandas evalúa la condición sobre cada fila y devuelve un array de True/False que se usa como máscara.
df = pd.DataFrame({
'nombre': ['Ana', 'Luis', 'María', 'Pedro', 'Sara'],
'edad': [28, 34, 22, 41, 29],
'ciudad': ['Madrid', 'Barcelona', 'Madrid', 'Madrid', 'Sevilla'],
'ventas': [1200.0, 850.0, None, 980.0, 2100.0],
})
# ── Filtrado simple ────────────────────────────────────────────
df[df['edad'] > 28] # mayores de 28
df[df['ciudad'] == 'Madrid'] # de Madrid
# ── Condiciones combinadas (& para AND, | para OR) ─────────────
df[(df['edad'] > 25) & (df['ciudad'] == 'Madrid')] # AND
df[(df['ciudad'] == 'Madrid') | (df['ciudad'] == 'Sevilla')] # OR
df[~(df['ciudad'] == 'Madrid')] # negación (NOT)
# ── Métodos útiles ─────────────────────────────────────────────
df[df['ciudad'].isin(['Madrid', 'Sevilla'])] # en lista de valores
df[df['edad'].between(25, 35)] # rango inclusivo
df[df['nombre'].str.contains('a', case=False)] # contiene texto
df.query('edad > 25 and ciudad == "Madrid"') # sintaxis SQL-like
# ── Nulos: detectar y tratar ───────────────────────────────────
df.isnull() # DataFrame booleano
df.isnull().sum() # cuenta de nulos por columna
df.isnull().sum().sum() # total de nulos en el DataFrame
df.dropna() # eliminar filas con algún NaN
df.dropna(subset=['ventas']) # eliminar solo si NaN en 'ventas'
df.dropna(axis=1) # eliminar columnas con algún NaN
df.fillna(0) # reemplazar NaN por 0
df.fillna({'ventas': df['ventas'].mean()}) # rellenar con media
df['ventas'].fillna(method='ffill') # forward fill (valor anterior)
# ── Duplicados ─────────────────────────────────────────────────
df.duplicated() # máscara de filas duplicadas
df.drop_duplicates() # eliminar filas duplicadas
df.drop_duplicates(subset=['ciudad']) # duplicados en columna específica
Manipulación: ordenar, renombrar y transformar
Una vez que tienes los datos cargados y filtrados, el siguiente paso habitual es transformarlos: cambiar nombres de columnas, crear columnas derivadas, convertir tipos, ordenar. Pandas tiene un método para cada uno de estos casos.
import pandas as pd
df = pd.DataFrame({
'nombre': ['Ana', 'Luis', 'María', 'Pedro'],
'edad': [28, 34, 22, 41],
'ventas': [1200.0, 850.0, 2100.0, 980.0],
'fecha': ['2025-01', '2025-02', '2025-01', '2025-03'],
})
# ── Renombrar columnas ─────────────────────────────────────────
df.rename(columns={'nombre': 'Nombre', 'edad': 'Edad'})
df.columns = ['nombre', 'edad', 'ventas', 'fecha'] # asignar todos
# ── Añadir columnas ────────────────────────────────────────────
df['comision'] = df['ventas'] * 0.05 # operación vectorizada
df['senior'] = df['edad'] >= 35 # columna booleana
df['categoria'] = pd.cut(df['edad'], bins=[0, 30, 40, 100],
labels=['joven', 'adulto', 'senior'])
# ── apply: función personalizada ───────────────────────────────
df['nombre_upper'] = df['nombre'].apply(lambda x: x.upper())
df['rango_ventas'] = df['ventas'].apply(
lambda v: 'alto' if v >= 1000 else 'bajo'
)
# apply por fila (axis=1)
df['resumen'] = df.apply(
lambda row: f"{row['nombre']} ({row['edad']} años)", axis=1
)
# ── Eliminar columnas o filas ──────────────────────────────────
df.drop(columns=['comision']) # eliminar columna
df.drop(index=[0, 2]) # eliminar filas por índice
# ── Ordenar ────────────────────────────────────────────────────
df.sort_values('ventas', ascending=False) # descendente
df.sort_values(['fecha', 'ventas']) # por múltiples columnas
df.nlargest(3, 'ventas') # top 3 por ventas
df.nsmallest(2, 'edad') # 2 más jóvenes
# ── Tipos de dato ──────────────────────────────────────────────
df['edad'].astype(float) # convertir a float
pd.to_datetime(df['fecha']) # convertir a datetime
df['fecha'] = pd.to_datetime(df['fecha'])
df['fecha'].dt.year # extraer año
df['fecha'].dt.month # extraer mes
# ── Strings ────────────────────────────────────────────────────
df['nombre'].str.lower()
df['nombre'].str.strip()
df['nombre'].str.replace(' ', '_')
df['nombre'].str.startswith('A')
Agrupación con groupby
El patrón split-apply-combine de groupby es uno de los más potentes y frecuentes en análisis de datos: divide el DataFrame en grupos según los valores de una columna, aplica una función a cada grupo, y combina los resultados. Es el equivalente Python del GROUP BY de SQL.
df = pd.DataFrame({
'depto': ['Ventas','Ventas','IT','IT','Marketing','Marketing'],
'nombre': ['Ana','Luis','María','Pedro','Sara','Tom'],
'ventas': [1200, 850, 2100, 980, 1500, 760],
'edad': [28, 34, 22, 41, 29, 35],
})
# ── Agrupación simple ──────────────────────────────────────────
df.groupby('depto')['ventas'].sum()
# depto
# IT 3080
# Marketing 2260
# Ventas 2050
df.groupby('depto')['ventas'].mean()
df.groupby('depto')['ventas'].count()
df.groupby('depto')['ventas'].max()
# ── Múltiples funciones de agregación ─────────────────────────
df.groupby('depto').agg({
'ventas': ['sum', 'mean', 'max'],
'edad': 'mean',
})
# ── Agrupar por múltiples columnas ─────────────────────────────
df.groupby(['depto', 'nombre'])['ventas'].sum()
# ── Obtener DataFrame limpio (reset_index) ─────────────────────
resumen = (
df.groupby('depto')['ventas']
.sum()
.reset_index() # convierte el índice en columna
.sort_values('ventas', ascending=False)
.rename(columns={'ventas': 'total_ventas'})
)
print(resumen)
# depto total_ventas
# 1 IT 3080
# 2 Marketing 2260
# 0 Ventas 2050
# ── apply con función personalizada ───────────────────────────
def rango(serie):
return serie.max() - serie.min()
df.groupby('depto')['ventas'].apply(rango)
# depto
# IT 1120
# Marketing 740
# Ventas 350
# ── size y nunique ─────────────────────────────────────────────
df.groupby('depto').size() # número de filas por grupo
df.groupby('depto')['nombre'].nunique() # valores únicos por grupo
df.groupby('depto')['ventas'].sum().reset_index().sort_values('ventas', ascending=False). Usar paréntesis al inicio y al final permite romper la cadena en varias líneas sin perder legibilidad.
Combinar DataFrames: merge y concat
En el trabajo real raramente los datos viven en una sola tabla. merge implementa los JOINs de SQL (inner, left, right, outer). concat apila DataFrames verticalmente (más filas) u horizontalmente (más columnas) sin necesidad de una columna común.
import pandas as pd
clientes = pd.DataFrame({
'id': [1, 2, 3, 4],
'nombre': ['Ana', 'Luis', 'María', 'Pedro'],
'ciudad': ['Madrid', 'Barcelona', 'Sevilla', 'Madrid'],
})
pedidos = pd.DataFrame({
'pedido_id': [101, 102, 103, 104, 105],
'cliente_id':[1, 2, 1, 3, 5], # cliente_id=5 no existe en clientes
'importe': [120.0, 85.0, 210.0, 60.0, 95.0],
})
# ── merge (JOIN) ───────────────────────────────────────────────
# INNER: solo filas con clave en AMBAS tablas
pd.merge(clientes, pedidos, left_on='id', right_on='cliente_id')
# Ana (2 pedidos), Luis (1), María (1) → cliente_id=5 excluido
# LEFT: todas las filas del izquierdo, NaN si no hay coincidencia
pd.merge(clientes, pedidos, left_on='id', right_on='cliente_id',
how='left') # Pedro aparece con NaN en pedido_id e importe
# RIGHT: todas del derecho
pd.merge(clientes, pedidos, left_on='id', right_on='cliente_id',
how='right') # pedido 105 aparece, cliente_id=5 sin datos
# OUTER: todos de ambas tablas, NaN donde no haya coincidencia
pd.merge(clientes, pedidos, left_on='id', right_on='cliente_id',
how='outer')
# Merge cuando la columna tiene el mismo nombre en ambas tablas
df_a = pd.DataFrame({'id':[1,2], 'valor_a':[10,20]})
df_b = pd.DataFrame({'id':[1,3], 'valor_b':[100,300]})
pd.merge(df_a, df_b, on='id') # solo fila con id=1
# ── concat ─────────────────────────────────────────────────────
enero = pd.DataFrame({'fecha':['2025-01-01','2025-01-15'],
'ventas':[1200, 850]})
febrero = pd.DataFrame({'fecha':['2025-02-01','2025-02-20'],
'ventas':[980, 2100]})
# Apilar filas (por defecto)
todo = pd.concat([enero, febrero], ignore_index=True)
# ignore_index=True reindexar desde 0 en lugar de mantener índices originales
# Apilar columnas
pd.concat([df_a, df_b], axis=1) # pega columnas lado a lado
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Pandas básico: DataFrames, Series y análisis de datos en Python
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Pandas básico: DataFrames, Series y análisis de datos en Python? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!