El Problema Central

Los modelos ML consumen features, variables derivadas computadas desde datos raw. Un modelo simple de recomendación podría necesitar:

user_avg_session_duration_7d, de clickstream
user_purchase_count_30d, de transacciones
item_view_count_24h, de stream de eventos
user_item_category_affinity, de historial de compras

Cada una tiene que computarse en al menos dos contextos:

  1. Tiempo de entrenamiento: desde datos históricos, con semántica point-in-time correcta
  2. Tiempo de serving: desde datos vivos, a baja latencia (usualmente <50ms)

Si la lógica de computación difiere entre estos dos contextos, aunque sea un toque, teners training-serving skew, y tu modelo performa peor en producción que en evaluación. Esto no es una preocupación teórica. Una caída de 15% en accuracy por un mismatch de timezone de fecha entre un job batch de Spark y una función de serving en Python es un patrón documentado.

Feast: La Base Open-Source

Feast (Feature Store) es la opción open-source estándar. Maneja registro de features, almacenamiento offline/online, y materialización (copiar features del store offline al online).

Definiendo Features

# feature_repo/features.py
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
from datetime import timedelta

user = Entity(name="user_id", join_keys=["user_id"])

user_transaction_stats = FeatureView(
 name="user_transaction_stats",
 entities=[user],
 schema=[
 Field(name="avg_transaction_amount_30d", dtype=Float32),
 Field(name="transaction_count_30d", dtype=Int64),
 Field(name="max_transaction_amount_30d", dtype=Float32),
 Field(name="days_since_last_transaction", dtype=Int64),
 ],
 source=FileSource(
 path="s3://ml-features/user_transaction_stats.parquet",
 timestamp_field="event_timestamp",
 ),
 ttl=timedelta(days=1),
 online=True,
)

Point-in-Time Joins

Acá es donde los feature stores se ganan su sustento:

from feast import FeatureStore
import pandas as pd

store = FeatureStore(repo_path="feature_repo/")

entity_df = pd.DataFrame({
 "user_id": [1001, 1002, 1001, 1003],
 "event_timestamp": [
 "2025-11-15 10:30:00",
 "2025-11-15 14:00:00",
 "2025-12-01 09:00:00",
 "2025-12-02 16:45:00",
 ],
})

training_df = store.get_historical_features(
 entity_df=entity_df,
 features=[
 "user_transaction_stats:avg_transaction_amount_30d",
 "user_transaction_stats:transaction_count_30d",
 "user_session_stats:avg_session_duration_7d",
 ],
).to_df()

La columna event_timestamp es la clave. Para user 1001 el 15 de noviembre, Feast devuelve los valores de features computados con datos disponibles hasta el 15 de noviembre. Para el mismo usuario el 1 de diciembre, devuelve valores diferentes, reflejando las dos semanas adicionales de datos. Sin point-in-time joins, usarías los valores más recientes y sin querer filtrarías información futura al entrenamiento.

Serving Online

features = store.get_online_features(
 features=[
 "user_transaction_stats:avg_transaction_amount_30d",
 "user_transaction_stats:transaction_count_30d",
 ],
 entity_rows=[{"user_id": 1001}],
).to_dict()

Con Redis como online store, esto devuelve en menos de 5ms a p99. Los benchmarks muestran 2.000 requests/segundo en una sola instancia de servidor Feast con 50 features por request.

Tecton: La Plataforma Gestionada

Tecton es la plataforma comercial de features construida por ex-ingenieros de Uber Michelangelo. La funcionalidad principal son las definiciones de transformación unificadas:

@batch_feature_view(
 sources=[transactions_batch],
 entities=[user_entity],
 mode="pandas",
 batch_schedule=timedelta(days=1),
)
def user_transaction_stats(transactions):
 return transactions.groupby("user_id").agg(
 avg_amount_30d=("amount", "mean"),
 count_30d=("amount", "count"),
 max_amount_30d=("amount", "max"),
 )

@stream_feature_view(
 source=transactions_stream,
 entities=[user_entity],
 mode="pandas",
 aggregation_interval=timedelta(minutes=5),
 aggregations=[
 Aggregation(column="amount", function="mean", time_window=timedelta(days=30)),
 Aggregation(column="amount", function="count", time_window=timedelta(days=30)),
 ],
)
def user_transaction_stats_realtime(transactions):
 return transactions

La misma lógica de features, aplicada tanto como computación batch (para entrenamiento) como computación streaming (para serving). Tecton garantiza consistencia entre ambas.

Features en Tiempo Real: La Parte Difícil

Un approach probado para features en tiempo real con Feast:

from feast import FeatureStore
from kafka import KafkaConsumer
import json

store = FeatureStore(repo_path="feature_repo/")
consumer = KafkaConsumer("user-events", bootstrap_servers="kafka:9092")

from collections import defaultdict
import time

windows = defaultdict(list)
WINDOW_SIZE = 300 # 5 minutos

for message in consumer:
 event = json.loads(message.value)
 user_id = event["user_id"]
 timestamp = event["timestamp"]

 windows[user_id].append({"ts": timestamp, "value": event["amount"]})
 cutoff = time.time() - WINDOW_SIZE
 windows[user_id] = [e for e in windows[user_id] if e["ts"] > cutoff]

 values = [e["value"] for e in windows[user_id]]
 features = {
 "user_id": user_id,
 "event_count_5m": len(values),
 "avg_amount_5m": sum(values) / len(values) if values else 0,
 }

 store.push("user_realtime_stats", pd.DataFrame([features]))

Esto es viable en producción pero operacionalmente pesado. Estás manejando un consumer de Kafka, estado en memoria, y el push a Feast.

Detección de Feature Drift

Las features driftean. El comportamiento de los usuarios cambia, las fuentes de datos se modifican, los schemas upstream evolucionan.

from evidently.metric_preset import DataDriftPreset
from evidently.report import Report

reference_data = pd.read_parquet("training_features_snapshot.parquet")
current_data = fetch_current_features(sample_size=10000)

report = Report(metrics=[DataDriftPreset()])
report.run(reference_data=reference_data, current_data=current_data)
result = report.as_dict()

for feature_name, drift_info in result["metrics"][0]["result"]["drift_by_columns"].items():
 if drift_info["drift_detected"]:
 alert(
 f"Feature drift detectado: {feature_name}",
 f"Drift score: {drift_info['drift_score']:.4f}, "
 f"Método: {drift_info['stattest_name']}"
 )

Esto típicamente corre diariamente como un job programado. Cuando se detecta drift, re-entrenar automáticamente no es aconsejable, investigar primero. Drift en user_avg_session_duration podría significar un cambio de UI, no un problema de calidad de datos. El contexto importa.

Arquitectura de Producción

La arquitectura completa para un sistema de detección de fraude con ~200 features:

Fuentes de Datos Raw
├── PostgreSQL (transacciones, cuentas)
├── Kafka (eventos tiempo real)
└── S3 (logs clickstream)
 │
 ▼
Computación de Features
├── Spark (features batch, diario)
├── Flink (features streaming, tiempo real)
└── Transforms Python (features on-demand)
 │
 ▼
Feast Feature Store
├── Offline: S3 Parquet (entrenamiento)
├── Online: Redis Cluster (serving)
└── Registry: PostgreSQL (metadata)
 │
 ├──► Pipeline de Entrenamiento
 │ └── get_historical_features() → Entrenamiento del Modelo
 │
 └──► Pipeline de Serving
 └── get_online_features() → Inferencia del Modelo (p99 < 10ms)

Presupuesto total de latencia para el path de detección de fraude: 50ms. Recuperación de features toma 4-8ms (Redis), inferencia del modelo toma 2-5ms (ONNX Runtime), y el resto es overhead de red y serialización.

Para Cerrar

Los feature stores son infraestructura que no te hace quedar como un héroe. Nadie asciende por deployar Feast. Pero la alternativa, cada data scientist manteniendo sus propios pipelines de computación de features, con código diferente para entrenamiento vs serving, sin monitoreo, y con semántica point-in-time implementada ad hoc (o para nada), es como terminás con sistemas ML que funcionan en notebooks y fallan en producción.

Arrancar con Feast. Usar Redis para online, el warehouse existente para offline. Definir features una vez, usarlas en todos lados. Monitorear distribuciones. Graduar a Tecton cuando la carga operacional de manejar features streaming de forma independiente supere el costo de la plataforma.

El modelo es la parte fácil. Las features son la parte difícil. Y el feature store es lo que evita que la parte difícil se vuelva imposible.