Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

recommendation.py

22_recommendation_systems/recommendation.py  ·  110 lines  ·  view on GitHub

"""
Recommendation Systems
Simple implementations
"""
import numpy as np

def matrix_factorization(R: np.ndarray, k: int, 
                        learning_rate: float = 0.01,
                        iterations: int = 100,
                        lambda_reg: float = 0.01) -> tuple:
    """
    Matrix Factorization: R ≈ P @ Q^T
    
    R: User-item rating matrix (n_users, n_items)
    P: User factors (n_users, k)
    Q: Item factors (n_items, k)
    k: Number of latent factors
    """
    n_users, n_items = R.shape
    
    # Initialize factors
    P = np.random.randn(n_users, k) * 0.1
    Q = np.random.randn(n_items, k) * 0.1
    
    # Train on observed ratings
    for iteration in range(iterations):
        for i in range(n_users):
            for j in range(n_items):
                if R[i, j] > 0:  # Observed rating
                    # Prediction
                    pred = P[i] @ Q[j]
                    error = R[i, j] - pred
                    
                    # Update with regularization
                    P[i] += learning_rate * (error * Q[j] - lambda_reg * P[i])
                    Q[j] += learning_rate * (error * P[i] - lambda_reg * Q[j])
    
    return P, Q

def predict_rating(user_id: int, item_id: int, P: np.ndarray, Q: np.ndarray) -> float:
    """Predict rating for user-item pair"""
    return P[user_id] @ Q[item_id]

def precision_at_k(recommended: list, relevant: list, k: int) -> float:
    """Precision@K: Of top K, how many are relevant?"""
    top_k = recommended[:k]
    relevant_in_top_k = len(set(top_k) & set(relevant))
    return relevant_in_top_k / k if k > 0 else 0.0

def recall_at_k(recommended: list, relevant: list, k: int) -> float:
    """Recall@K: Of all relevant, how many in top K?"""
    top_k = recommended[:k]
    relevant_in_top_k = len(set(top_k) & set(relevant))
    return relevant_in_top_k / len(relevant) if len(relevant) > 0 else 0.0

def ndcg_at_k(recommended: list, relevant: list, k: int) -> float:
    """NDCG@K: Normalized Discounted Cumulative Gain"""
    top_k = recommended[:k]
    dcg = 0.0
    for i, item in enumerate(top_k):
        if item in relevant:
            dcg += 1.0 / np.log2(i + 2)
    
    ideal_dcg = sum(1.0 / np.log2(i + 2) 
                   for i in range(min(len(relevant), k)))
    
    return dcg / ideal_dcg if ideal_dcg > 0 else 0.0


# Usage Example
if __name__ == "__main__":
    print("Recommendation Systems")
    print("=" * 60)
    
    # Sample rating matrix (0 = not rated)
    R = np.array([
        [5, 3, 0, 1],
        [4, 0, 0, 1],
        [1, 1, 0, 5],
        [1, 0, 0, 4],
        [0, 1, 5, 4],
    ])
    
    print("Rating Matrix R:")
    print(R)
    print()
    
    # Matrix factorization
    k = 2
    P, Q = matrix_factorization(R, k, iterations=100)
    
    print(f"User factors P shape: {P.shape}")
    print(f"Item factors Q shape: {Q.shape}")
    print()
    
    # Predict ratings
    print("Predicted Ratings:")
    R_pred = P @ Q.T
    print(R_pred)
    print()
    
    # Evaluation
    recommended = [0, 1, 2, 3]
    relevant = [0, 2]
    
    print("Evaluation Metrics:")
    print(f"Precision@2: {precision_at_k(recommended, relevant, k=2):.4f}")
    print(f"Recall@2: {recall_at_k(recommended, relevant, k=2):.4f}")
    print(f"NDCG@2: {ndcg_at_k(recommended, relevant, k=2):.4f}")