Pandas: Análisis de Datos con Python
Pandas es la biblioteca estándar para análisis y manipulación de datos en Python. Construida sobre NumPy, ofrece las estructuras Series y DataFrame con índices etiquetados, manejo nativo de datos faltantes y una API expresiva para transformar datos de todo tipo.
Instalación
pip install pandas
pip install openpyxl # para leer/escribir .xlsx
import pandas as pd
import numpy as np
Series
Una Series es un array 1D etiquetado — como una columna de hoja de cálculo.
# Crear Series
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print(s['b']) # 20
print(s[1]) # 20 — también funciona por posición
# Operaciones vectorizadas
print(s * 2)
print(s[s > 15]) # filtrado booleano: b=20, c=30, d=40
# Desde diccionario
paises = pd.Series({'España': 47_000_000, 'México': 130_000_000, 'Argentina': 45_000_000})
print(paises.sort_values(ascending=False))
DataFrame: creación
# Desde diccionario de listas
df = pd.DataFrame({
'nombre': ['Ana', 'Carlos', 'Elena', 'David'],
'edad': [28, 35, 22, 41],
'ciudad': ['Madrid', 'Barcelona', 'Sevilla', 'Madrid'],
'salario': [45000, 62000, 38000, 75000],
})
# Desde lista de dicts
registros = [
{'id': 1, 'valor': 3.14, 'etiqueta': 'pi'},
{'id': 2, 'valor': 2.71, 'etiqueta': 'e'},
]
df2 = pd.DataFrame(registros)
# Desde CSV / Excel
df_csv = pd.read_csv('datos.csv', encoding='utf-8', parse_dates=['fecha'])
df_excel = pd.read_excel('informe.xlsx', sheet_name='Ventas')
# Desde URL directa
url = 'https://raw.githubusercontent.com/datasets/gdp/main/data/gdp.csv'
# df_web = pd.read_csv(url)
Inspección inicial del DataFrame
df.head(3) # primeras 3 filas (5 por defecto)
df.tail(3) # últimas 3 filas
df.info() # tipos, valores no nulos, memoria
df.describe() # estadísticas descriptivas (solo numéricas)
df.describe(include='all') # incluye categóricas
df.shape # (filas, columnas)
df.dtypes # tipo de cada columna
df.columns.tolist()
df.index
df.nunique() # valores únicos por columna
df.isnull().sum() # conteo de NaN por columna
df.value_counts('ciudad') # frecuencia de cada ciudad
Selección e indexación
# Seleccionar columnas
df['nombre'] # Serie
df[['nombre', 'salario']] # DataFrame con 2 columnas
# .loc — indexación por etiqueta
df.loc[0] # fila con índice 0
df.loc[1:3, 'nombre':'ciudad'] # filas 1-3, columnas nombre→ciudad
df.loc[df['edad'] > 30] # filtro booleano
# .iloc — indexación por posición entera
df.iloc[0] # primera fila
df.iloc[-1] # última fila
df.iloc[1:3, 0:2] # filas 1-2, columnas 0-1
# .at y .iat — acceso a celda individual (más rápido)
df.at[0, 'nombre'] # 'Ana'
df.iat[0, 0] # 'Ana'
# Filtros compuestos
mascara = (df['edad'] > 25) & (df['ciudad'] == 'Madrid')
df_filtrado = df.loc[mascara]
# isin — equivalente a IN de SQL
df[df['ciudad'].isin(['Madrid', 'Barcelona'])]
# query — sintaxis más legible
df.query("edad > 25 and ciudad == 'Madrid'")
Crear y transformar columnas
# Añadir columna calculada
df['salario_mensual'] = df['salario'] / 12
# Aplicar función — apply es flexible pero lento
df['nombre_upper'] = df['nombre'].apply(str.upper)
# Mejor: operaciones vectorizadas (más rápidas)
df['nombre_upper'] = df['nombre'].str.upper()
df['ciudad_lower'] = df['ciudad'].str.lower()
# pd.cut — categorizar variable continua
df['grupo_edad'] = pd.cut(
df['edad'],
bins=[0, 25, 35, 100],
labels=['junior', 'mid', 'senior']
)
# np.select — múltiples condiciones
condiciones = [
df['salario'] < 40000,
df['salario'].between(40000, 65000),
df['salario'] > 65000,
]
categorias = ['bajo', 'medio', 'alto']
df['nivel_salarial'] = np.select(condiciones, categorias, default='desconocido')
# assign — encadenado (devuelve copia, no modifica in-place)
df = (df
.assign(bono=lambda d: d['salario'] * 0.1)
.assign(total=lambda d: d['salario'] + d['bono']))
Agrupación — groupby
# GroupBy básico
por_ciudad = df.groupby('ciudad')['salario'].mean()
print(por_ciudad)
# Múltiples agregaciones
resumen = df.groupby('ciudad').agg(
salario_medio=('salario', 'mean'),
salario_max=('salario', 'max'),
num_empleados=('nombre', 'count'),
)
# agg con múltiples funciones sobre una columna
df.groupby('ciudad')['salario'].agg(['mean', 'std', 'count'])
# Transformación — mantiene el índice original
df['salario_vs_ciudad'] = df.groupby('ciudad')['salario'].transform('mean')
# Filtrado de grupos
ciudades_grandes = df.groupby('ciudad').filter(lambda g: len(g) > 1)
# Named aggregations con multiple columnas
df.groupby('grupo_edad').agg(
edad_media=('edad', 'mean'),
salario_total=('salario', 'sum'),
num_personas=('nombre', 'count'),
).reset_index()
Merge y Join
empleados = pd.DataFrame({
'id': [1, 2, 3, 4],
'nombre': ['Ana', 'Carlos', 'Elena', 'David'],
'dept_id': [10, 20, 10, 30],
})
departamentos = pd.DataFrame({
'dept_id': [10, 20, 30],
'departamento': ['IT', 'Marketing', 'Finanzas'],
'presupuesto': [500000, 300000, 400000],
})
# Inner join (solo coincidencias)
inner = pd.merge(empleados, departamentos, on='dept_id', how='inner')
# Left join (todos los empleados, NaN si no hay depto)
left = pd.merge(empleados, departamentos, on='dept_id', how='left')
# Diferentes nombres de columna
# pd.merge(df1, df2, left_on='emp_dept', right_on='dept_id')
# merge_asof — join por proximidad (útil para series temporales)
# pd.merge_asof(orders, quotes, on='timestamp', by='ticker')
# join — merge por índice
df_a = empleados.set_index('id')
df_b = pd.DataFrame({'bonus': [1000, 2000, 1500, 3000]}, index=[1,2,3,4])
resultado = df_a.join(df_b)
Series temporales
# Crear índice de fechas
fechas = pd.date_range('2024-01-01', periods=365, freq='D')
ventas = pd.Series(np.random.randint(100, 500, 365), index=fechas)
# Slicing por fecha
enero = ventas['2024-01']
q1 = ventas['2024-01':'2024-03']
ventas_2024 = ventas.loc['2024']
# Resampling — agregar por periodo
ventas_semana = ventas.resample('W').sum() # semanal
ventas_mes = ventas.resample('ME').mean() # mensual (month end)
ventas_trim = ventas.resample('QE').sum() # trimestral
# Rolling — media móvil
media_7d = ventas.rolling(window=7).mean()
media_30d = ventas.rolling(window=30, min_periods=1).mean()
# Shift y diferencias
ventas_ayer = ventas.shift(1) # desplazar 1 periodo
crecimiento_dia = ventas.diff() # diferencia día a día
crecimiento_pct = ventas.pct_change() # % de cambio
# Timestamps
ts = pd.Timestamp('2024-06-15 14:30:00')
ts.year, ts.month, ts.day
ts.day_name() # 'Saturday'
ts + pd.Timedelta(days=7)
# Parsear fechas desde columna string
df['fecha_dt'] = pd.to_datetime(df['fecha_str'], format='%d/%m/%Y')
df['año'] = df['fecha_dt'].dt.year
df['mes'] = df['fecha_dt'].dt.month
df['dia_semana'] = df['fecha_dt'].dt.day_name()
Pivot tables y crosstab
# pivot_table — resumen multidimensional
tabla = pd.pivot_table(
df,
values='salario',
index='ciudad',
columns='grupo_edad',
aggfunc='mean',
fill_value=0,
margins=True, # añade totales (fila/columna 'All')
)
# melt — formato ancho → largo (opuesto a pivot)
df_ancho = pd.DataFrame({
'nombre': ['Ana', 'Carlos'],
'enero': [1000, 2000],
'febrero': [1100, 1900],
'marzo': [1200, 2100],
})
df_largo = df_ancho.melt(
id_vars=['nombre'],
value_vars=['enero', 'febrero', 'marzo'],
var_name='mes',
value_name='ventas'
)
# crosstab — tabla de contingencia
ct = pd.crosstab(df['ciudad'], df['grupo_edad'], normalize='index') # % por fila
Datos faltantes
df_con_nulos = pd.DataFrame({
'a': [1, np.nan, 3, np.nan],
'b': [np.nan, 2, 3, 4],
'c': ['x', 'y', None, 'z'],
})
# Detectar y contar
df_con_nulos.isnull()
df_con_nulos.isnull().sum()
# Eliminar filas/columnas con NaN
df_sin_nulos = df_con_nulos.dropna() # elimina filas con cualquier NaN
df_sin_nulos = df_con_nulos.dropna(subset=['a']) # solo si 'a' es NaN
df_sin_nulos = df_con_nulos.dropna(thresh=2) # mantiene si ≥2 no-NaN
# Rellenar NaN
df_relleno = df_con_nulos.fillna(0)
df_relleno = df_con_nulos.fillna({'a': df_con_nulos['a'].mean(), 'c': 'desconocido'})
# Interpolación para series temporales
df_con_nulos['a'] = df_con_nulos['a'].interpolate(method='linear')
# Forward/backward fill
df_con_nulos['b'] = df_con_nulos['b'].ffill() # forward fill
df_con_nulos['b'] = df_con_nulos['b'].bfill() # backward fill
Exportar resultados
# CSV
df.to_csv('resultado.csv', index=False, encoding='utf-8-sig') # BOM para Excel
# Excel (requiere openpyxl)
with pd.ExcelWriter('informe.xlsx', engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Empleados', index=False)
resumen.to_excel(writer, sheet_name='Resumen')
# JSON
df.to_json('datos.json', orient='records', force_ascii=False, indent=2)
# SQL (requiere sqlalchemy)
# from sqlalchemy import create_engine
# engine = create_engine('sqlite:///database.db')
# df.to_sql('empleados', engine, if_exists='replace', index=False)
# df_leido = pd.read_sql('SELECT * FROM empleados WHERE ciudad="Madrid"', engine)
# Parquet — formato columnar eficiente (requiere pyarrow)
# df.to_parquet('datos.parquet', compression='snappy')
# df_parquet = pd.read_parquet('datos.parquet')
Buenas prácticas
- Preferir operaciones vectorizadas sobre
applyy bucles:df['col'].str.upper()es 10-100x más rápido quedf['col'].apply(str.upper). - Usar
inplace=False(default) y reasignar la variable — las operaciones in-place dificultan el debugging y no son más rápidas. - Copiar con
df.copy()cuando necesites modificar un subconjunto sin afectar al original (evita elSettingWithCopyWarning). query()yassign()hacen el código más legible y encadenable; úsalos para pipelines de transformación.- Especificar
dtypeal leer CSV en datasets grandes:pd.read_csv(..., dtype={'id': int, 'precio': float})ahorra memoria. - Parquet en lugar de CSV para datasets > 100 MB que lees frecuentemente: más rápido de leer y mejor compresión.
Conversiones relacionadas
Conversiones frecuentes del catálogo: