- Added consumer/Dockerfile with all dependencies - Updated consumer pipeline.py to use environment variables - Updated features.py and decision.py to support env var configuration - Updated docker-compose.yml to include consumer service - Consumer now runs automatically with 'docker compose up' - Proper Kafka and Redis networking for Docker - Environment variables for ML_MODELS_DIR, KAFKA_BROKER, REDIS_HOST/PORT Now entire PayGuard system runs with: docker compose up -d Co-authored-by: Copilot <[email protected]>
142 lines
4.6 KiB
Python
142 lines
4.6 KiB
Python
"""
|
|
Feature Extraction Module
|
|
|
|
Responsibilities:
|
|
- Extract raw transaction data into ML-friendly features
|
|
- Query Redis for user behavioral history
|
|
- Construct feature vectors for ML models
|
|
|
|
Academic Point:
|
|
Feature engineering is the bridge between raw business data and ML models.
|
|
This module demonstrates how to enrich simple transaction data with historical
|
|
context to improve model decision quality.
|
|
"""
|
|
|
|
import redis
|
|
import numpy as np
|
|
import logging
|
|
import os
|
|
from typing import Dict, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class FeatureExtractor:
|
|
"""Extracts and engineers features from transactions"""
|
|
|
|
def __init__(self, redis_host: str = None, redis_port: int = None):
|
|
# Support environment variables for Docker deployments
|
|
if redis_host is None:
|
|
redis_host = os.getenv("REDIS_HOST", "localhost")
|
|
if redis_port is None:
|
|
redis_port = int(os.getenv("REDIS_PORT", "6379"))
|
|
|
|
self.redis_client = redis.Redis(
|
|
host=redis_host,
|
|
port=redis_port,
|
|
decode_responses=True
|
|
)
|
|
logger.info(f"[FEATURE EXTRACTOR] Initialized, redis={redis_host}:{redis_port}")
|
|
|
|
|
|
def get_user_profile(self, user_id: str) -> Dict[str, float]:
|
|
"""
|
|
Retrieve user's behavioral profile from Redis.
|
|
|
|
Returns:
|
|
{
|
|
'avg_amount': float,
|
|
'tx_count': int,
|
|
'last_timestamp': float,
|
|
'days_since_last_tx': float
|
|
}
|
|
"""
|
|
key = f"user:{user_id}:profile"
|
|
profile = self.redis_client.hgetall(key)
|
|
|
|
if not profile:
|
|
logger.debug(f"[REDIS LOOKUP] No profile for user={user_id}, using defaults")
|
|
return {
|
|
'avg_amount': 0.0,
|
|
'tx_count': 0,
|
|
'last_timestamp': 0.0,
|
|
'days_since_last_tx': 999.0
|
|
}
|
|
|
|
logger.debug(
|
|
f"[REDIS LOOKUP] Found profile for user={user_id}: "
|
|
f"avg=${profile.get('avg_amount', 0)}, count={profile.get('tx_count', 0)}"
|
|
)
|
|
|
|
return {
|
|
'avg_amount': float(profile.get('avg_amount', 0.0)),
|
|
'tx_count': int(profile.get('tx_count', 0)),
|
|
'last_timestamp': float(profile.get('last_timestamp', 0.0)),
|
|
'days_since_last_tx': float(profile.get('days_since_last_tx', 999.0))
|
|
}
|
|
|
|
def extract_features(self, transaction: Dict) -> np.ndarray:
|
|
"""
|
|
Extract feature vector from transaction + user profile.
|
|
|
|
Feature vector: [amount, avg_amount, tx_count, days_since_last_tx]
|
|
"""
|
|
user_id = transaction['user_id']
|
|
amount = transaction['amount']
|
|
|
|
# Get user history
|
|
profile = self.get_user_profile(user_id)
|
|
|
|
# Construct feature vector
|
|
features = np.array([[
|
|
amount,
|
|
profile['avg_amount'],
|
|
profile['tx_count'],
|
|
profile['days_since_last_tx']
|
|
]])
|
|
|
|
logger.info(
|
|
f"[FEATURE EXTRACTION] tx_id={transaction['transaction_id'][:8]}... "
|
|
f"user={user_id} features=[${amount:.0f}, "
|
|
f"avg=${profile['avg_amount']:.0f}, "
|
|
f"count={profile['tx_count']}, "
|
|
f"days_since={profile['days_since_last_tx']:.1f}]"
|
|
)
|
|
|
|
return features
|
|
|
|
def update_user_profile(self, transaction: Dict) -> None:
|
|
"""Update user's behavioral profile with new transaction"""
|
|
user_id = transaction['user_id']
|
|
amount = transaction['amount']
|
|
timestamp = transaction['timestamp']
|
|
|
|
key = f"user:{user_id}:profile"
|
|
profile = self.redis_client.hgetall(key)
|
|
|
|
# Calculate new statistics
|
|
old_count = int(profile.get('tx_count', 0))
|
|
new_count = old_count + 1
|
|
|
|
old_avg = float(profile.get('avg_amount', 0.0))
|
|
new_avg = (old_avg * old_count + amount) / new_count
|
|
|
|
old_timestamp = float(profile.get('last_timestamp', 0.0))
|
|
days_since = 0.0 # Just made a transaction
|
|
|
|
# Update profile in Redis
|
|
self.redis_client.hset(
|
|
key,
|
|
mapping={
|
|
'tx_count': new_count,
|
|
'avg_amount': round(new_avg, 2),
|
|
'last_timestamp': timestamp,
|
|
'days_since_last_tx': days_since
|
|
}
|
|
)
|
|
|
|
logger.info(
|
|
f"[PROFILE UPDATE] user={user_id} "
|
|
f"count={old_count}→{new_count}, "
|
|
f"avg=${old_avg:.2f}→${new_avg:.2f}"
|
|
)
|