Ilustración técnica para: Optimización del Preprocesamiento de Datos en IA: Implementando el Strategy Pattern con Python

Strategy Pattern para preprocesamiento de datos: cuando conviene frente a un Pipeline


El Strategy Pattern aparece en casi todo tutorial de patrones de diseño aplicado a IA como la solución obvia para intercambiar algoritmos de preprocesamiento. Lo que esos tutoriales casi nunca dicen es que, si tu preprocesamiento son transformaciones tabulares estándar (normalizar, imputar, codificar categorías), scikit-learn ya te da una abstracción equivalente con Pipeline y FunctionTransformer, con integracion nativa en GridSearchCV y serializacion incluidas. La pregunta relevante no es "como implemento el Strategy Pattern" sino "cuando me conviene escribir el mio en vez de usar el que ya existe".

El criterio antes de elegir patron

Antes de escribir una linea de código, responde tres preguntas sobre tu caso concreto:

  • ¿Tus estrategias son transformaciones puras de arrays/DataFrames? Si reciben datos y devuelven datos sin efectos secundarios, encajan directamente en la interfaz fit/transform que espera un Pipeline de scikit-learn.
  • ¿Necesitas búsqueda de hiperparámetros o validación cruzada sobre la elección de estrategia? Si sí, un Pipeline te la da gratis vía GridSearchCV tratando la estrategia como un parámetro más. Un DataPreprocessor artesanal no.
  • ¿Tus estrategias hacen algo que no es tabular (llamadas a un servicio externo, lógica de negocio condicional, side effects de logging o auditoría)? Ahí el Strategy Pattern puro sigue siendo la herramienta correcta, porque forzar eso dentro de un Transformer de scikit-learn es forzar una abstracción que no fue pensada para eso.

Con esas tres respuestas ya sabes en qué mitad de este artículo vas a aterrizar.

La implementación completa del Strategy Pattern

Esta es la versión que deberías usar si tus estrategias no son transformaciones tabulares puras: interfaz abstracta con abc.ABC, tres estrategias concretas y un contexto que las administra. A diferencia de los ejemplos habituales con listas sueltas de float, aquí se usan los alias genéricos incorporados en el lenguaje (list[float], dict[str, list[int]]) sin importar nada de typing para eso, disponibles desde Python 3.9 vía PEP 585 en la documentación oficial de typing y todavía la forma recomendada en la Python 3.14 vigente.

from abc import ABC, abstractmethod
from statistics import mean, pstdev

class PreprocessingStrategy(ABC):
    @abstractmethod
    def process(self, data: list[float]) -> list[float]:
        """Transforma una lista de valores numéricos."""

class NormalizationStrategy(PreprocessingStrategy):
    """Escala los valores al rango [0, 1]."""

    def process(self, data: list[float]) -> list[float]:
        if not data:
            return []
        low, high = min(data), max(data)
        span = high - low or 1.0
        return [(x - low) / span for x in data]

class StandardizationStrategy(PreprocessingStrategy):
    """Centra en media 0 y desviación típica 1."""

    def process(self, data: list[float]) -> list[float]:
        if not data:
            return []
        mu = mean(data)
        sigma = pstdev(data) or 1.0
        return [(x - mu) / sigma for x in data]

class OneHotEncodingStrategy(PreprocessingStrategy):
    """Codifica categorías como vectores binarios, con orden estable."""

    def process(self, data: list[str]) -> list[list[int]]:
        categories = sorted(set(data))
        index = {cat: i for i, cat in enumerate(categories)}
        return [[1 if i == index[value] else 0 for i in range(len(categories))] for value in data]

class PreprocessingContext:
    def __init__(self, strategy: PreprocessingStrategy) -> None:
        self._strategy = strategy

    def set_strategy(self, strategy: PreprocessingStrategy) -> None:
        self._strategy = strategy

    def run(self, data: list) -> list:
        return self._strategy.process(data)


if __name__ == "__main__":
    numeric = [10.0, 20.0, 30.0, 40.0, 50.0]
    categorical = ["rojo", "azul", "verde", "rojo"]

    context = PreprocessingContext(NormalizationStrategy())
    print("Normalizado:", context.run(numeric))

    context.set_strategy(StandardizationStrategy())
    print("Estandarizado:", context.run(numeric))

    context.set_strategy(OneHotEncodingStrategy())
    print("One-hot:", context.run(categorical))

Nota una diferencia deliberada frente a la implementación con min/max manuales de las versiones anteriores de este artículo: usar statistics.mean y statistics.pstdev de la librería estándar evita reimplementar aritmética que ya está en Python y que maneja mejor los casos borde (listas de un solo elemento, por ejemplo).

Qué te da gratis un Pipeline de scikit-learn

Si tus estrategias sí son transformaciones tabulares puras, esta es la alternativa que resuelve el mismo problema de intercambiabilidad sin código propio que mantener. La documentación oficial de Pipeline lo define como una secuencia de transformadores encadenados donde cada paso debe implementar fit y transform, exactamente el mismo contrato que PreprocessingStrategy.process pero ya integrado con el resto del ecosistema.

import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer, MinMaxScaler, StandardScaler
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression

def log1p_scale(X: np.ndarray) -> np.ndarray:
    return np.log1p(np.abs(X))

# Cada "estrategia" es un paso intercambiable, igual que en el Strategy Pattern,
# pero GridSearchCV puede probar varias sin que tú escribas el bucle.
pipeline = Pipeline([
    ("strategy", MinMaxScaler()),
    ("model", LogisticRegression(max_iter=1000)),
])

param_grid = {
    "strategy": [MinMaxScaler(), StandardScaler(), FunctionTransformer(log1p_scale)],
}

search = GridSearchCV(pipeline, param_grid, cv=5)
# search.fit(X_train, y_train)  # descomenta con tus datos reales

El propio FunctionTransformer es literalmente el puente entre ambos mundos: convierte cualquier función arbitraria (tu estrategia hecha a mano) en un paso compatible con Pipeline, con la advertencia documentada de que si usas una lambda el resultado no será serializable con pickle, así que conviene usar funciones nombradas como en el ejemplo. La versión vigente de scikit-learn en la que se verificó este código es la 1.9.0, que exige Python 3.11 o superior según su ficha en PyPI; si tu proyecto sigue en una versión de Python más antigua, necesitas fijar una versión anterior de la librería.

Tabla comparativa para decidir

CriterioStrategy Pattern propioPipeline / FunctionTransformerTemplate Method
Control granular de la lógicaTotal, sin restricciones de interfazLimitado al contrato fit/transformSolo sobre los pasos variables del algoritmo
Integración con GridSearchCV / validación cruzadaNinguna, hay que programarlaNativaNinguna
Curva de aprendizaje para el equipoBaja si ya conocen el patrónBaja si ya usan scikit-learnMedia, requiere entender herencia de plantilla
Encaja con datos no tabulares o con efectos secundariosNo, fuerza un contrato pensado para arrays
Testeo unitario aislado por estrategiaDirectoDirecto (cada transformer es independiente)Más difícil, la lógica común está en la clase base

Veredicto condicionado

Usa el Strategy Pattern escrito a mano cuando tus "estrategias" no son transformaciones tabulares puras: si alguna llama a una API externa, escribe a un log de auditoría, o su lógica depende de reglas de negocio que cambian con frecuencia y no tienen representación natural como fit/transform. Usa Pipeline con FunctionTransformer cuando el preprocesamiento sea estrictamente numérico o categórico y en algún punto vayas a necesitar búsqueda de hiperparámetros, validación cruzada o serialización del pipeline completo con joblib: reimplementar eso a mano en un DataPreprocessor casero es exactamente el tipo de trabajo que ya está resuelto y probado en producción por miles de proyectos.

Un patrón intermedio, y el que suele terminar ganando en proyectos reales, es escribir las estrategias como funciones puras siguiendo la interfaz de PreprocessingStrategy para mantener la semántica explícita del patrón en el código y los tests unitarios, y envolver cada una en un FunctionTransformer justo antes de meterla en un Pipeline de producción. Así conservas la intención de diseño del Strategy Pattern sin renunciar a lo que scikit-learn ya resolvió mejor que cualquier implementación artesanal.

Compartir X LinkedIn