D
Nucleo tecnico M2 introductorio 22 min

Capitulo 02

Python para Data Science: mas alla de la sintaxis

Pensar como un programador, no como un usuario de Jupyter

Python como lenguaje: tipos, funciones, comprehensions, OOP, closures, decoradores y los patrones que un Data Scientist profesional usa a diario. No es un tutorial, es un mapa mental.

2.1 El modelo mental: Python es un contrato, no una caja negra

Antes de aprender cualquier sintaxis, vale la pena entender como Python ejecuta tu codigo. El modelo de datos de Python (el “Data Model” descrito por Luciano Ramalho en Fluent Python) define como todos los objetos se comportan. Cuando entiendes este modelo, todo lo demas encaja.

El modelo de datos

En Python, todo es un objeto. Cada objeto tiene: (1) una identidad (direccion en memoria, unica), (2) un tipo (define que operaciones soporta), (3) un valor. Las variables son referencias (etiquetas) a objetos, no cajas que contienen valores. Esto explica casi todos los “gotchas” de Python.

python Identidad vs igualdad
# Dos formas de comparar objetos
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True  - mismo VALOR
print(a is b)   # False - distinta IDENTIDAD (distintos objetos)
print(a is c)   # True  - misma IDENTIDAD (mismo objeto)
print(id(a), id(b), id(c))  # Direcciones de memoria

Mutabilidad vs inmutabilidad

M2

Un objeto es mutable si su valor puede cambiar sin cambiar su identidad. Listas, dicts, sets son mutables. Strings, tuplas, ints, frozensets son inmutables. Esto determina si una funcion modifica su argumento o si necesitas retornar una copia.

Ej: Listas se modifican in-place (a.append(4)). Strings NO: s.replace('a', 'b') retorna un string nuevo, no modifica s.

#python #mutabilidad #core

2.2 Funciones: el corazon de Python

En Python, las funciones son objetos de primera clase: se pueden asignar a variables, pasar como argumentos, retornar desde otras funciones y almacenar en estructuras de datos. Esto es lo que hace posible decoradores, map/filter, callbacks y patrones avanzados.

python Funciones como objetos de primera clase
from typing import Callable, Iterable

# Una funcion es un objeto. Se puede asignar, pasar, retornar.
def aplicar_a_lista(func: Callable, datos: Iterable):
    return [func(x) for x in datos]

def cuadrado(x):
    return x ** 2

# La funcion cuadrado SE PASA como argumento
resultado = aplicar_a_lista(cuadrado, [1, 2, 3, 4])
print(resultado)  # [1, 4, 9, 16]

# Tambien se puede hacer al vuelo con lambda
resultado = aplicar_a_lista(lambda x: x ** 3, [1, 2, 3])
print(resultado)  # [1, 8, 27]

Closures: funciones que recuerdan

Un closure es una funcion que “captura” variables del ambito donde fue definida, incluso despues de que ese ambito haya terminado. Es la base de los decoradores y de la programacion funcional en Python.

python Closure: fabrica de funciones
def crear_multiplicador(factor):
    """Retorna una funcion que multiplica por `factor`."""
    def multiplicar(x):
        return x * factor  # 'factor' viene del ambito exterior
    return multiplicar

duplicar = crear_multiplicador(2)
triplicar = crear_multiplicador(3)

print(duplicar(5))   # 10
print(triplicar(5))  # 15
print(duplicar(10))  # 20

# El closure mantiene su propio 'factor' aunque crear_multiplicador ya termino
print(duplicar.__closure__[0].cell_contents)  # 2

Decoradores: composicion horizontal

Un decorador es una funcion que toma otra funcion, la envuelve, y retorna una version mejorada. Es composicion sin modificar el codigo original.

python Decoradores utiles en Data Science
import time
import functools
from typing import Callable

def timing(func: Callable) -> Callable:
    """Mide el tiempo de ejecucion de una funcion."""
    @functools.wraps(func)  # preserva __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"[{func.__name__}] ejecutado en {elapsed:.4f}s")
        return result
    return wrapper

def logger(func: Callable) -> Callable:
    """Loguea argumentos de entrada y retorno."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[{func.__name__}] llamado con args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[{func.__name__}] retorno {result}")
        return result
    return wrapper

@timing
@logger
def entrenar_modelo(epochs, lr):
    # Simular entrenamiento
    time.sleep(0.1)
    return f"modelo entrenado en {epochs} epochs, lr={lr}"

entrenar_modelo(epochs=10, lr=0.001)
# [entrenar_modelo] llamado con args=(), kwargs={'epochs': 10, 'lr': 0.001}
# [entrenar_modelo] retorno modelo entrenado en 10 epochs, lr=0.001
# [entrenar_modelo] ejecutado en 0.1008s
Decorador con argumentos

Los decoradores pueden tomar argumentos. Se logra con un “decorador de tres niveles”: una funcion externa que toma los argumentos y retorna un decorador real. Patron clave en frameworks (Flask, FastAPI, Click).

2.3 Comprehensions: sintaxis declarativa

Las comprehensions existen porque en Data Science el 80% del codigo es transformar colecciones. Si escribis bucles for para eso, perdes 5-10x en performance y claridad.

ConceptoDescripcion
[x*2 for x in lst if x > 0] Genera lista. Caso general. Mas rapido que loop + append.
{x*2 for x in lst if x > 0} Genera set (sin duplicados). Util para unicidad.
{k: v*2 for k, v in d.items() if v > 0} Genera diccionario. Transformaciones de mappings.
(x*2 for x in lst if x > 0) Genera valores lazy, uno a uno. Memoria O(1) en lugar de O(n).
python Comprehensions vs loops
import time
import random

random.seed(42)
N = 10_000_000
datos = [random.randint(1, 100) for _ in range(N)]

# MANERA 1: loop + append (lenta, verbosa)
start = time.perf_counter()
result = []
for x in datos:
    if x % 2 == 0:
        result.append(x ** 2)
print(f"Loop:        {time.perf_counter() - start:.3f}s")

# MANERA 2: list comprehension (rapida, declarativa)
start = time.perf_counter()
result = [x ** 2 for x in datos if x % 2 == 0]
print(f"List comp:   {time.perf_counter() - start:.3f}s")

# MANERA 3: generator (memoria minima, mismo rendimiento)
start = time.perf_counter()
result = sum(x ** 2 for x in datos if x % 2 == 0)
print(f"Generator:   {time.perf_counter() - start:.3f}s")

# Tipicamente la list comp es 30-50% mas rapida que el loop
Regla practica

Usa comprehensions para transformaciones simples (hasta 2-3 condiciones). Si la logica es compleja (multiples pasos, excepciones, regex), divide en una funcion y usa map/filter o un loop explicito. La claridad gana sobre la brevedad.

2.4 OOP: cuando y como

La OOP en Python no es obligatoria, pero la API de scikit-learn esta basada en una jerarquia de clases (BaseEstimator, TransformerMixin, ClassifierMixin) que necesitás entender para hacer feature engineering custom o crear tus propios modelos.

python Herencia de BaseEstimator para sklearn
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np

class LogTransformer(BaseEstimator, TransformerMixin):
    """Aplica np.log1p a columnas numericas. Encaja en un Pipeline."""

    def __init__(self, columns=None):
        self.columns = columns

    def fit(self, X, y=None):
        # No aprende nada, solo retorna self
        return self

    def transform(self, X):
        X = X.copy() if hasattr(X, 'copy') else np.array(X, copy=True)
        cols = self.columns if self.columns is not None else (X.columns if hasattr(X, 'columns') else range(X.shape[1]))
        for c in cols:
            X[c] = np.log1p(X[c])
        return X

# Esto ahora se puede usar en un Pipeline de sklearn
# from sklearn.pipeline import Pipeline
# pipe = Pipeline([
#     ('log', LogTransformer(columns=['precio', 'ingreso'])),
#     ('scaler', StandardScaler()),
# ])

2.5 Generadores: laziness para datasets grandes

Un generador produce valores uno a uno, sin construir la coleccion completa en memoria. Es la unica forma sensata de procesar datasets de varios GB en una laptop.

python Procesar CSV grande con generadores
import csv
from typing import Iterator, Dict

def leer_csv_lazy(path: str) -> Iterator[Dict[str, str]]:
    """Lee un CSV linea por linea. Memoria: O(1)."""
    with open(path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:  # 'yield' implicito (es un generator function)
            yield row

# Procesar 5 GB de transacciones sin cargar todo a memoria
def calcular_monto_total(path: str) -> float:
    total = 0.0
    for row in leer_csv_lazy(path):
        if 'monto' in row:
            total += float(row['monto'])
    return total

# Memoria constante, no importa el tamaño del CSV
print(calcular_monto_total('transacciones.csv'))

# Los generators SON iterables pero NO indexables
gen = (x for x in range(10))
# gen[0]  # TypeError!
list(gen)  # OK, los consume
Error clasico: reusar un generator

Los generators se agotan. Una vez que los consumiste, estan vacios. Si necesitas recorrerlos dos veces, materializa a lista O crea una funcion generadora nueva.

2.6 Errores comunes en Python para Data Science

Argumento mutable por defecto
✗ def agregar(item, lista=[]): lista.append(item); return lista # todas las llamadas comparten la MISMA lista
✓ def agregar(item, lista=None): if lista is None: lista = []; lista.append(item); return lista
Modificar una lista mientras se itera
✗ for x in lst: if x < 0: lst.remove(x) # comportamiento indefinido, omite elementos
✓ lst = [x for x in lst if x >= 0] # O lst[:] = [x for x in lst if x >= 0] para in-place
Confundir 'is' con '=='
✗ if a is None: ... # funciona, pero 'is' compara identidad, no valor
✓ Usa 'is' solo para comparar con None, True, False. Para todo lo demas, '=='.
List comprehension para side effects
✗ [print(x) for x in datos] # crea una lista de None innecesaria
✓ for x in datos: print(x) # O solo print(*datos, sep='\n')
Olvidar que 'for' NO crea scope en Python
✗ [x for x in range(10)] print(x) # NameError: x no esta definido (esto SI funciona)
✓ En realidad las list comprehensions SI tienen su propio scope. El problema es con 'for' planos o while. Siempre definir variables antes de usarlas.

2.7 Distribuciones con scipy.stats (preview)

python scipy.stats: el arsenal del estadistico
import numpy as np
from scipy import stats

# Distribucion normal: media=0, std=1
X = stats.norm(loc=0, scale=1)

# PDF (densidad de probabilidad)
print(f"PDF en x=0: {X.pdf(0):.4f}")  # 0.3989
print(f"PDF en x=1: {X.pdf(1):.4f}")  # 0.2419

# CDF (probabilidad acumulada)
print(f"P(X <= 0): {X.cdf(0):.4f}")  # 0.5
print(f"P(X <= 1.96): {X.cdf(1.96):.4f}")  # 0.975 (el famoso 95%)

# Cuantil inverso (ppf = percent point function)
print(f"95% quantile: {X.ppf(0.95):.4f}")  # 1.6449

# Muestreo
samples = X.rvs(size=10_000, random_state=42)
print(f"Media de la muestra: {samples.mean():.3f}")  # ~0
print(f"Std de la muestra: {samples.std():.3f}")    # ~1

# Test de hipotesis: t-test de una muestra
t_stat, p_value = stats.ttest_1samp(samples, 0)
print(f"t-stat: {t_stat:.3f}, p-value: {p_value:.3f}")  # p > 0.05: no rechazamos H0

2.8 Recursos

Libro: "Fluent Python" (2da ed) - Luciano Ramalho (Capitulos 1-9: data model, funciones, comprehensions, closures)
Libro: "Python for Data Analysis" (3ra ed) - Wes McKinney (Capitulos 1-3: Python basico para DS)
Video: Corey Schafer: "Python OOP Tutorial" (YouTube, ~4h, muy didactico)
Video: mCoding: "Python's most weird language features" (YouTube, edge cases del lenguaje)
Articulo: PEP 8 - Style Guide for Python Code (la biblia del estilo)
Herramienta: pylint / ruff / black (linters y formatters: ejecutalos antes de commitear)
Curso: MIT 6.0001 - Introduction to CS and Programming using Python (gratis en OCW)
Mini-proyecto 2: tu primera utilidad reutilizable intermedio
  1. Creá un decorador @validar_positivo que verifique que todos los argumentos numericos de una funcion sean >= 0, lanzando ValueError si no lo son. Aplicalo a una funcion calcular_raiz(x).
  2. Creá un generador fibonacci() que produzca la secuencia de Fibonacci indefinidamente. Consumilo con itertools.islice(fibonacci(), 10) para obtener los primeros 10.
  3. Creá una clase DatasetAnalyzer con metodos: info() (describe shape y tipos), missing_report() (% de nulos por columna), detectar_outliers() (IQR), tendencia(serie) (creciente/decreciente/constante). Hereda de BaseEstimator para que sea compatible con sklearn.
  4. Para cada funcion, escribi 3 tests usando unittest o pytest.