D
Nucleo tecnico M3 intermedio 28 min

Capitulo 03

NumPy y Pandas: el motor de la manipulacion de datos

Pensar vectorialmente, dejar de escribir loops

NumPy y Pandas son las dos librerias que hacen todo el resto posible. Aca aprendes el modelo mental vectorial, broadcasting, indexacion, groupby y los 3 errores que rompen tu codigo en produccion.

3.1 Por que NumPy cambia tu forma de pensar

NumPy no es “Python con numeros”. Es un cambio de paradigma: de operar elemento por elemento (imperativo) a operar sobre el array completo (declarativo/vectorial). La diferencia no es solo velocidad (10-100x). Es que la operacion vectorizada expresa la intencion de forma mas clara.

python Vectorizacion: el mismo problema, 3 maneras
import numpy as np
import time

# Dataset grande
np.random.seed(42)
N = 1_000_000
alturas = np.random.normal(170, 10, N)  # cm
pesos   = np.random.normal(70, 15, N)    # kg

# MANERA 1: loop ingenuo (Python puro, ~10x mas lento)
start = time.perf_counter()
imcs = []
for i in range(N):
    h_m = alturas[i] / 100
    imcs.append(pesos[i] / (h_m ** 2))
print(f"Loop Python:     {time.perf_counter() - start:.3f}s")

# MANERA 2: list comprehension (mejor, pero todavia lenta)
start = time.perf_counter()
imcs = [pesos[i] / ((alturas[i] / 100) ** 2) for i in range(N)]
print(f"List comp:       {time.perf_counter() - start:.3f}s")

# MANERA 3: vectorizacion NumPy (30-100x mas rapido)
start = time.perf_counter()
imcs_np = pesos / ((alturas / 100) ** 2)
print(f"NumPy vectorial: {time.perf_counter() - start:.3f}s")

# Las 3 dan el MISMO resultado. NumPy es ~50x mas rapido.
¿Por que NumPy es mas rapido?

NumPy ejecuta operaciones en C continuo (no en Python interpretado). Los arrays son bloques de memoria contiguos de tipos homogeneos (todos float64 o todos int32), lo que permite usar instrucciones SIMD del procesador (Single Instruction Multiple Data). Python, en cambio, tiene cada numero como un objeto PyObject con punteros, type checking, garbage collection: mucha sobrecarga.

3.2 Broadcasting: la operacion mas importante de NumPy

El broadcasting es la regla que permite operar arrays de formas distintas sin copiar datos. Por ejemplo, restar la media (vector de 784,) a una imagen de (28, 28) sin que falle.

Broadcasting (reglas)

M3

Cuando dos arrays tienen formas distintas, NumPy 'estira' el array de rango menor para que coincida con el mayor, SI: (1) cada dimension es igual, o (2) una de ellas es 1. NO estira si las dimensiones son distintas y ninguna es 1: eso es un error.

Ej: (3, 4) + (4,) → (3, 4) porque el segundo se 'estira' a (1, 4) y luego a (3, 4). (3, 4) + (3,) → ERROR porque no se puede hacer 3 = 4.

#numpy #broadcasting #core
python Broadcasting en accion
import numpy as np

# Normalizacion Z-score columna por columna (operacion clave de ML)
X = np.random.randn(100, 5)  # 100 muestras, 5 features
mean = X.mean(axis=0)        # shape (5,)
std  = X.std(axis=0)         # shape (5,)
X_norm = (X - mean) / std    # broadcasting: (100, 5) - (5,) → (100, 5)

print("X.shape:    ", X.shape)
print("mean.shape: ", mean.shape)
print("X_norm.mean(axis=0):", X_norm.mean(axis=0).round(10))  # ~[0, 0, 0, 0, 0]
print("X_norm.std(axis=0): ", X_norm.std(axis=0).round(10))   # ~[1, 1, 1, 1, 1]

# Ejemplo que FALLA: shapes incompatibles
A = np.ones((3, 4))
B = np.ones((3,))  # solo 1 dimension
try:
    C = A + B
except ValueError as e:
    print(f"\nError esperado: {e}")
# Solucion: reshape B a (1, 3) y luego broadcast a (?, 3) — pero
# eso no matchearia las 4 columnas. Mejor: usar (3, 1) si queremos
# restar por fila.
Regla mnemotecnica

“Right-align las shapes y compara columna por columna”. Ej: (100, 5) vs (5,) → alineamos a la derecha, queda (100, 5) vs (_, 5). Las dimensiones son iguales (5 = 5), entonces OK. (100, 5) vs (100,) → (100, 5) vs (100, _). La segunda columna es 5 vs nada → ERROR.

3.3 Pandas: Series, DataFrame e indexacion

Pandas es la abstraccion de datos tabulares sobre NumPy. Tres conceptos: Series (1D), DataFrame (2D), Index (etiquetas de fila/columna). El Index es lo que hace a Pandas diferente de NumPy: permite joins, time series, alineacion automatica.

ConceptoDescripcion
Series Array 1D con etiquetas. Piensa en una columna de Excel con nombre. dtype homogeneo.
DataFrame Tabla 2D: columnas de Series que comparten el mismo Index. El objeto central de Pandas.
Index Etiquetas de fila. Por defecto 0, 1, 2, ... pero puede ser fechas, strings, categorias. Permite joins.

Indexacion: .loc vs .iloc vs .at vs .iat

ConceptoDescripcion
df.loc[fila, col] Por ETIQUETA. Inclusivo en ambos extremos. El que mas usas.
df.iloc[0:5, 1:3] Por POSICION ENTERA. Exclusivo en el extremo derecho (como Python slices).
df.at[idx, "col"] Acceso ESCALAR por etiqueta. Mas rapido que .loc para un solo valor.
df.iat[i, j] Acceso ESCALAR por posicion. Mas rapido que .iloc para un solo valor.
df.query("col > 5") Filtros con sintaxis SQL-like. Util para filtros complejos legibles.
df[mask & mask2] Componer condiciones con & (AND), | (OR), ~ (NOT). Parentesis obligatorios.
python Indexacion eficiente
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'region':   ['Norte', 'Sur', 'Centro', 'Norte', 'Sur'],
    'producto': ['A', 'B', 'A', 'B', 'A'],
    'monto':    [100, 200, 150, 300, 250],
    'fecha':    pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05'])
}, index=['t1', 't2', 't3', 't4', 't5'])

# Por etiqueta
print(df.loc['t2', 'monto'])   # 200

# Por posicion
print(df.iloc[1, 2])          # 200

# Query: legible para filtros complejos
print(df.query("monto > 150 and region == 'Sur'"))

# Asignacion: USAR .loc, NUNCA encadenar []
df.loc[df['monto'] > 150, 'monto_alto'] = True
print(df)

3.4 Tidy Data y melt/pivot

El formato Tidy Data (Hadley Wickham) es la convencion canonica: cada variable es una columna, cada observacion es una fila, cada tipo de unidad forma una tabla. Casi todas las herramientas de DS asumen tidy data.

python Wide vs Long: pd.melt()
import pandas as pd

# Wide format: 1 fila por anio, 1 columna por trimestre
wide = pd.DataFrame({
    'anio':    [2023, 2024],
    'Q1':      [100, 120],
    'Q2':      [110, 130],
    'Q3':      [105, 125],
    'Q4':      [115, 135],
})
print("Wide:")
print(wide)

# Long format: 1 fila por (anio, trimestre, ventas)
long = pd.melt(wide, id_vars=['anio'], var_name='trimestre', value_name='ventas')
print("\nLong (tidy):")
print(long)

# Para volver a wide: pivot
back_to_wide = long.pivot(index='anio', columns='trimestre', values='ventas')
print("\nDe vuelta a wide:")
print(back_to_wide)
Regla practica

La mayoria de las visualizaciones (Seaborn, ggplot) y modelos asumen long format. Si tus datos estan en wide, pd.melt() primero. Si los necesitas wide para reporting, pivot() despues.

3.5 Groupby: split-apply-combine

GroupBy es el patron mas importante de Pandas. Implementa el paradigma split-apply-combine: dividir el DataFrame en grupos segun una o mas claves, aplicar una funcion a cada grupo, y combinar los resultados.

python GroupBy: agg vs transform vs apply
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'region':  ['Norte', 'Norte', 'Sur', 'Sur', 'Centro', 'Centro'],
    'producto':['A', 'B', 'A', 'B', 'A', 'B'],
    'monto':   [100, 200, 150, 300, 120, 180],
    'unidades':[1, 2, 1, 3, 1, 2]
})

# 1) AGG: reduce a 1 fila por grupo. La salida es mas chica que la entrada.
print("=== agg ===")
print(df.groupby('region').agg(
    ventas_totales=('monto', 'sum'),
    venta_promedio=('monto', 'mean'),
    unidades_totales=('unidades', 'sum'),
))

# 2) TRANSFORM: conserva la forma original. Util para features.
print("\n=== transform ===")
df['pct_del_total'] = df.groupby('region')['monto'].transform(
    lambda x: x / x.sum()
)
print(df[['region', 'monto', 'pct_del_total']])

# 3) APPLY: maxima flexibilidad, minima performance.
# Solo usar cuando agg/transform no alcanzan.
print("\n=== apply ===")
print(df.groupby('region').apply(lambda g: g.nlargest(1, 'monto')))
agg vs transform: cuando cada uno
  • agg: cuando quieres UN numero por grupo (reporte, KPI). Salida: 1 fila por grupo.
  • transform: cuando quieres agregar una feature al DataFrame original (ej: “ventas/ventas_de_su_region”). Salida: misma forma que la entrada.
  • apply: cuando necesitas logica custom que no es aggregacion ni transformacion simple. Mas lento. Evitar si es posible.

3.6 Errores comunes en NumPy/Pandas

SettingWithCopyWarning
✗ df[df['x'] > 0]['y'] = 1 # Pandas no sabe si quieres modificar el df original o una copia
✓ df.loc[df['x'] > 0, 'y'] = 1 # .loc garantiza que modificas el df original
Comparar con 'is' en lugar de '=='
✗ df[df['x'] is np.nan] # SIEMPRE False: nan nunca es 'is' a si mismo
✓ df[df['x'].isna()] # .isna() es el metodo correcto para detectar NaN
Olvidar axis= en operaciones
✗ df.sum() # por defecto axis=0 → suma cada COLUMNA (devuelve Series de n_columnas valores)
✓ df.sum(axis=1) # suma cada FILA (devuelve Series de n_filas valores). Es lo opuesto de numpy.
Iterar filas con iterrows() (lento)
✗ for idx, row in df.iterrows(): ... # MUY lento para > 10k filas
✓ df.itertuples() # 10x mas rapido. O, mejor aun, vectoriza: df.apply(axis=1) solo si no queda otra.
Crear DataFrame con copias innecesarias
✗ df_new = df.copy(deep=True) # copia profunda por defecto. Lento y gasta memoria
✓ df_new = df # comparte datos hasta que modifiques. .copy(deep=False) cuando necesites copiar.

3.7 EDA en 5 minutos (el flujo profesional)

python EDA template
import pandas as pd
import numpy as np

def eda_rapido(df: pd.DataFrame, target: str = None) -> None:
    """EDA de 5 minutos: 6 chequeos que SIEMPRE hay que hacer."""
    print(f"=== EDA rapido: {df.shape[0]} filas x {df.shape[1]} columnas ===\n")

    # 1. Tipos
    print("1. Tipos de dato:")
    print(df.dtypes.value_counts())

    # 2. Nulos
    print("\n2. Valores nulos:")
    nulos = df.isna().sum()
    if nulos.sum() == 0:
        print("  Sin nulos")
    else:
        print(nulos[nulos > 0].sort_values(ascending=False))

    # 3. Duplicados
    n_dups = df.duplicated().sum()
    print(f"\n3. Duplicados: {n_dups} ({n_dups/len(df):.1%})")

    # 4. Numericas: distribucion
    print("\n4. Estadisticas de columnas numericas:")
    print(df.describe().T.round(2))

    # 5. Categoricas: cardinalidad
    cat_cols = df.select_dtypes(include=['object', 'category']).columns
    if len(cat_cols) > 0:
        print("\n5. Cardinalidad de categoricas:")
        for c in cat_cols:
            n_unique = df[c].nunique()
            print(f"  {c}: {n_unique} valores unicos {'⚠️ ALTA' if n_unique > 50 else ''}")

    # 6. Target (si existe)
    if target and target in df.columns:
        print(f"\n6. Distribucion del target '{target}':")
        vc = df[target].value_counts(normalize=True).round(3)
        print(vc)
        if len(vc) > 2 and vc.min() < 0.1:
            print("  ⚠️  CLASES DESBALANCEADAS: considera stratified split y metricas como F1/PR-AUC.")

# Uso:
# eda_rapido(df, target='churn')
Libro: "Python for Data Analysis" (3ra ed) - Wes McKinney. Cap 4-13 son el corazon de Pandas.
Libro: "Effective Pandas" - Matt Harrison. Patron de patrones utiles para DS del dia a dia.
Video: Keith Galli: "Pandas in 15 Minutes" (YouTube, intro rapida)
Video: Data School: "Pandas best practices" (YouTube, 4 videos de 30 min cada uno)
Articulo: "Modern Pandas" - Tom Augspurger (blog post, Patron method chaining)
Paper: Wickham: "Tidy Data" (Journal of Statistical Software, 2014, 23 paginas que cambiaron la disciplina)
Herramienta: pandas-profiling / ydata-profiling: genera reportes EDA automaticos con 1 linea
Mini-proyecto 3: EDA completo de un dataset real intermedio
  1. Descargá un dataset publico (Titanic, House Prices o el de tu preferencia, al menos 5 columnas y 1000+ filas).
  2. Aplica la funcion eda_rapido() de la seccion 3.7. Reporta todos los hallazgos.
  3. Para cada columna numerica con outliers (IQR), investigá si son errores o valores legitimos (un outlier de $5M en precios de casas es real, un outlier de edad=200 probablemente no).
  4. Converti a tidy data con pd.melt() si esta en wide. Hacé una visualizacion con Seaborn (que cubriremos en M5).
  5. Para columnas categoricas de alta cardinalidad (> 50), considera agrupar las menos frecuentes en “OTROS”.
  6. Identifica el % de nulos por columna. Propon una estrategia: ¿imputar, eliminar fila, eliminar columna?