<!-- Canonical: https://lucioduran.com/es/blog/mlops-feature-stores-feast-tecton-production
Language: es
Content-SHA256: 2b63fda2cc1f78d1dc07b802f3740083b4a2a1a839fd0f8f7d9284e58d2b0dd2
-->

AI/ML

# MLOps Feature Stores: Feast, Tecton, Features en Tiempo Real y Arquitectura de Pipelines ML en Producción

2025-11-305 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/mlops-feature-stores-feast-tecton-production>)

## 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](<https://feast.dev/>) (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

```python
# 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:

```python
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

```python
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](<https://www.tecton.ai/>) es la plataforma comercial de features construida por ex-ingenieros de Uber Michelangelo. La funcionalidad principal son las definiciones de transformación unificadas:

```python
@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:

```python
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.

```python
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.

## Preguntas frecuentes

### ¿Qué es un feature store y por qué lo necesito?

Un feature store es un sistema centralizado para gestionar features de ML, las variables de entrada que tus modelos consumen. Maneja almacenamiento (online para serving de baja latencia, offline para entrenamiento), transformación (computar features desde datos raw), y consistencia (asegurar que entrenamiento y serving usen lógica de features idéntica). Lo se necesita cuando la duplicación de features entre equipos, el training-serving skew, o la frescura de features se convierte en cuello de botella.

### ¿Cómo previenen los point-in-time joins el data leakage?

Los point-in-time joins recuperan valores de features como existían al momento de cada ejemplo de entrenamiento, no como existen ahora. Si se está entrenando un detector de fraude con una transacción del 15 de enero, el join busca el gasto promedio del usuario al 15 de enero, no el valor de hoy, que incluye meses de datos que no hubieran estado disponibles al momento de la predicción.

### ¿Feast está listo para producción para serving en tiempo real?

Sí, con el backend correcto de online store. Feast con Redis sirve features a menos de 5ms de latencia p99 para tamaños moderados de feature set. Para throughput muy alto (100K+ requests/segundo), se va a querer tunear la configuración del cluster Redis o considerar DynamoDB como online store. La limitación principal es que la ingesta streaming de Feast requiere que manejes el job de Spark/Flink usted mismo.

## Tips

1. **Tip:** Siempre computá features de entrenamiento con point-in-time joins, nunca con lookups del estado actual. Entrenar un modelo de fraude con 'balance actual de cuenta' en vez de 'balance al momento de la transacción' produce un modelo que se ve increíble en entrenamiento y es inútil en producción. Leakage clásico.
2. **Tip:** Para features en tiempo real, mantener la lógica de transformación en un solo lugar. Si el pipeline batch computa ` avg_transactions_7d ` diferente que tu pipeline de streaming, hay un training-serving skew que destruirá silenciosamente la performance del modelo.
3. **Tip:** Arrancar con Feast + Redis para online serving y Feast + el data warehouse existente para offline. Tecton se justifica una vez que la cantidad de features cruza 500 y hay un equipo dedicado de plataforma ML. El overhead operacional de un servicio gestionado solo rinde a escala.

- mlops
- feature-stores
- feast
- tecton
- machine-learning
- features-tiempo-real
- point-in-time-joins
- feature-drift
- ml-pipelines
- produccion-ml

[Volver al Blog](<https://lucioduran.com/es/blog>)
