🎬 SISTEMA DI RACCOMANDAZIONE AVANZATO

Progetto Finale che Unisce Database, ML e Reti Neurali

INTEGRAZIONE COMPLETA AI: Database + ML + Deep Learning

🎯 Progetto Finale: Netflix-Style Recommendation Engine

📺 Scenario Reale: Netflix Tech Lead

Sei stato assunto come AI Engineer in una startup di streaming video (MovieFlix). Il tuo compito è costruire un sistema di raccomandazione professionale che combina:

  • Database SQL per gestire milioni di utenti e film
  • Machine Learning Classico per raccomandazioni collaborative
  • Reti Neurali per raccomandazioni basate su contenuto
  • Ensemble Learning per combinare i migliori modelli
  • API REST per servire raccomandazioni in tempo reale

🏗️ Architettura del Sistema

1️⃣ Database Layer

PostgreSQL con 1M+ utenti, 50k film, 10M+ rating

2️⃣ ML Layer

Collaborative Filtering + Content-Based Filtering

3️⃣ Deep Learning Layer

Reti Neurali per embeddings e similarità

4️⃣ API Layer

FastAPI per servire raccomandazioni in real-time

🚀 Obiettivi del Progetto

  • Costruire database relazionale scalabile per dati di streaming
  • Implementare 3+ algoritmi di raccomandazione diversi
  • Creare sistema ibrido che combina ML classico e reti neurali
  • Sviluppare API REST per servire raccomandazioni
  • Valutare performance con metriche professionali (RMSE, Precision@K)
  • Deploy su cloud (simulato) con Docker

Parte 1: Database Professionale per Streaming Video

1
Setup Ambiente e Creazione Database Complesso
PYTHON
# ================================================
# 🎬 PROGETTO: SISTEMA DI RACCOMANDAZIONE AVANZATO
# ================================================

print("🚀 INIZIO PROGETTO: SISTEMA RACCOMANDAZIONE NETFLIX-STYLE")
print("=" * 70)

# 1.1 IMPORTAZIONE LIBRERIE COMPLETE
print("\n📦 IMPORT TUTTE LE LIBRERIE NECESSARIE...")

# Database e Data Manipulation
import sqlite3
import pandas as pd
import numpy as np
from sqlalchemy import create_engine, text
import json
from datetime import datetime, timedelta
import random

# Machine Learning
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import mean_squared_error, mean_absolute_error
from sklearn.preprocessing import StandardScaler, LabelEncoder, MinMaxScaler
from sklearn.decomposition import TruncatedSVD
from sklearn.neighbors import NearestNeighbors
import scipy.sparse as sp

# Deep Learning
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models, optimizers, losses, metrics
from tensorflow.keras.layers import Embedding, Flatten, Dot, Add, Concatenate, Dense, Dropout, Input
from tensorflow.keras.models import Model
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau

# NLP e Text Processing (per descrizioni film)
import re
from collections import Counter
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
nltk.download('punkt', quiet=True)
nltk.download('stopwords', quiet=True)

# Visualizzazione
import matplotlib.pyplot as plt
import seaborn as sns
from wordcloud import WordCloud
import plotly.graph_objects as go
import plotly.express as px

# API e Deployment
from fastapi import FastAPI, HTTPException
import uvicorn
import pickle
import joblib

# Configurazione
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 1000)
np.random.seed(42)
tf.random.set_seed(42)
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")

print("✅ Tutte le librerie importate correttamente!")
print(f"📊 TensorFlow Version: {tf.__version__}")
print(f"📊 Scikit-learn Version: {pd.__version__}")

# 1.2 CREAZIONE DATABASE COMPLESSO PER STREAMING
print("\n" + "=" * 70)
print("🗄️ CREAZIONE DATABASE MOVIEFLIX (SIMULAZIONE REALISTICA)")
print("=" * 70)

# Crea database SQLite in memoria (in produzione sarebbe PostgreSQL)
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# 1.3 SCHEMA DATABASE PROFESSIONALE
print("\n🏗️ CREAZIONE SCHEMA DATABASE AVANZATO...")

tables_sql = [
    # Tabella Utenti (con features demografiche)
    '''CREATE TABLE IF NOT EXISTS users (
        user_id INTEGER PRIMARY KEY,
        age INTEGER CHECK(age >= 13 AND age <= 100),
        gender TEXT CHECK(gender IN ('M', 'F', 'O')),
        country TEXT,
        subscription_type TEXT CHECK(subscription_type IN ('basic', 'standard', 'premium')),
        join_date DATE,
        total_watch_time_hours DECIMAL(10,2) DEFAULT 0,
        avg_rating DECIMAL(3,2) DEFAULT 0
    )''',
    
    # Tabella Film (con metadata dettagliato)
    '''CREATE TABLE IF NOT EXISTS movies (
        movie_id INTEGER PRIMARY KEY,
        title TEXT NOT NULL,
        release_year INTEGER,
        genre TEXT,  # Separato da pipe: "Action|Adventure|Sci-Fi"
        director TEXT,
        duration_minutes INTEGER,
        imdb_rating DECIMAL(2,1),
        description TEXT,
        main_actors TEXT,  # Separato da pipe
        language TEXT,
        production_country TEXT,
        budget_millions DECIMAL(10,2),
        revenue_millions DECIMAL(10,2)
    )''',
    
    # Tabella Rating (interazioni utente-film)
    '''CREATE TABLE IF NOT EXISTS ratings (
        rating_id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        movie_id INTEGER,
        rating DECIMAL(2,1) CHECK(rating >= 0.5 AND rating <= 5.0),
        timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
        watch_duration_minutes INTEGER,
        device_type TEXT CHECK(device_type IN ('mobile', 'tablet', 'tv', 'desktop')),
        FOREIGN KEY (user_id) REFERENCES users(user_id),
        FOREIGN KEY (movie_id) REFERENCES movies(movie_id),
        UNIQUE(user_id, movie_id)  # Un utente può rateare un film una volta sola
    )''',
    
    # Tabella Watch History (per sessioni di visione)
    '''CREATE TABLE IF NOT EXISTS watch_history (
        session_id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        movie_id INTEGER,
        start_time DATETIME,
        end_time DATETIME,
        completed BOOLEAN DEFAULT 0,
        pause_count INTEGER DEFAULT 0,
        FOREIGN KEY (user_id) REFERENCES users(user_id),
        FOREIGN KEY (movie_id) REFERENCES movies(movie_id)
    )''',
    
    # Tabella Simili (film simili, pre-calcolata)
    '''CREATE TABLE IF NOT EXISTS similar_movies (
        movie_id INTEGER,
        similar_movie_id INTEGER,
        similarity_score DECIMAL(4,3),
        similarity_type TEXT CHECK(similarity_type IN ('content', 'collaborative', 'hybrid')),
        PRIMARY KEY (movie_id, similar_movie_id),
        FOREIGN KEY (movie_id) REFERENCES movies(movie_id),
        FOREIGN KEY (similar_movie_id) REFERENCES movies(movie_id)
    )'''
]

# Esegui creazione tabelle
for i, table_sql in enumerate(tables_sql, 1):
    cursor.execute(table_sql)
    print(f"✅ Tabella {i}/5 creata: {table_sql.split('(')[0].split()[-1]}")

# 1.4 GENERAZIONE DATI SINTETICI REALISTICI
print("\n📊 GENERAZIONE DATI SINTETICI PER 10,000 UTENTI E 5,000 FILM...")

# Genera utenti
countries = ['US', 'UK', 'CA', 'AU', 'DE', 'FR', 'IT', 'ES', 'JP', 'BR', 'IN', 'CN']
subscriptions = ['basic', 'standard', 'premium']

users_data = []
for user_id in range(1, 10001):
    age = random.randint(13, 80)
    gender = random.choice(['M', 'F', 'O'])
    country = random.choice(countries)
    subscription = random.choices(subscriptions, weights=[0.4, 0.4, 0.2])[0]
    join_date = datetime(2020, 1, 1) + timedelta(days=random.randint(0, 1460))
    watch_time = random.expovariate(1/100)  # Media 100 ore
    
    users_data.append((
        user_id, age, gender, country, subscription,
        join_date.strftime('%Y-%m-%d'), round(watch_time, 2)
    ))

cursor.executemany('''
    INSERT INTO users (user_id, age, gender, country, subscription_type, join_date, total_watch_time_hours)
    VALUES (?, ?, ?, ?, ?, ?, ?)
''', users_data)
print(f"✅ {len(users_data)} utenti generati")

# Genera film
print("🎬 Generazione catalogo film...")

# Dataset di film reali (sintetizzato)
movie_templates = [
    # Titolo, Anno, Generi, Regista, Durata, Rating IMDb
    ("The Matrix", 1999, "Action|Sci-Fi", "Lana Wachowski", 136, 8.7),
    ("Inception", 2010, "Action|Sci-Fi|Thriller", "Christopher Nolan", 148, 8.8),
    ("The Dark Knight", 2008, "Action|Crime|Drama", "Christopher Nolan", 152, 9.0),
    ("Pulp Fiction", 1994, "Crime|Drama", "Quentin Tarantino", 154, 8.9),
    ("Forrest Gump", 1994, "Drama|Romance", "Robert Zemeckis", 142, 8.8),
    ("The Godfather", 1972, "Crime|Drama", "Francis Ford Coppola", 175, 9.2),
    ("Fight Club", 1999, "Drama", "David Fincher", 139, 8.8),
    ("The Shawshank Redemption", 1994, "Drama", "Frank Darabont", 142, 9.3),
    ("Interstellar", 2014, "Adventure|Drama|Sci-Fi", "Christopher Nolan", 169, 8.6),
    ("Parasite", 2019, "Comedy|Drama|Thriller", "Bong Joon Ho", 132, 8.6),
]

# Espandi con variazioni
movies_data = []
movie_id = 1
genres_list = ["Action", "Adventure", "Comedy", "Drama", "Horror", "Romance", 
               "Sci-Fi", "Thriller", "Documentary", "Animation", "Fantasy"]

directors = ["Christopher Nolan", "Steven Spielberg", "Martin Scorsese", 
             "Quentin Tarantino", "James Cameron", "David Fincher", 
             "Peter Jackson", "Tim Burton", "Ridley Scott", "Alfred Hitchcock"]

actors_pool = ["Leonardo DiCaprio", "Meryl Streep", "Tom Hanks", "Scarlett Johansson",
               "Robert De Niro", "Jennifer Lawrence", "Denzel Washington",
               "Emma Stone", "Brad Pitt", "Angelina Jolie", "Morgan Freeman"]

for _ in range(5000):
    if movie_id <= len(movie_templates):
        title, year, genre, director, duration, imdb = movie_templates[movie_id-1]
    else:
        # Genera film casuali
        title = f"Movie {movie_id}"
        year = random.randint(1980, 2023)
        num_genres = random.randint(1, 3)
        genre = "|".join(random.sample(genres_list, num_genres))
        director = random.choice(directors)
        duration = random.randint(80, 180)
        imdb = round(random.uniform(5.0, 9.5), 1)
    
    # Genera descrizione
    description_words = ["epic", "thrilling", "emotional", "mind-bending", 
                        "heartwarming", "suspenseful", "funny", "dramatic"]
    description = f"A {random.choice(description_words)} story about {random.choice(['love', 'war', 'friendship', 'betrayal', 'redemption'])}."
    
    # Attori principali
    num_actors = random.randint(2, 4)
    main_actors = "|".join(random.sample(actors_pool, num_actors))
    
    # Altri metadati
    language = random.choice(["English", "Spanish", "French", "German", "Japanese"])
    country = random.choice(["USA", "UK", "France", "Japan", "South Korea", "India"])
    budget = round(random.uniform(10, 300), 2)
    revenue = round(budget * random.uniform(1, 10), 2)
    
    movies_data.append((
        movie_id, title, year, genre, director, duration, imdb,
        description, main_actors, language, country, budget, revenue
    ))
    
    movie_id += 1

cursor.executemany('''
    INSERT INTO movies VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', movies_data)
print(f"✅ {len(movies_data)} film generati")

# 1.5 GENERAZIONE RATING REALISTICI (10% SPARSITÀ)
print("\n⭐ GENERAZIONE RATING UTENTE-FILM (MATRICE SPARSA)...")

ratings_data = []
num_ratings = 0

# Simula che ogni utente abbia visto/rateato alcuni film
for user_id in range(1, 10001):
    # Numero di film rateati da questo utente (power law distribution)
    num_movies_rated = int(np.random.power(0.7) * 200) + 1
    
    # Seleziona film casuali (non tutti gli utenti vedono tutti i film)
    rated_movies = random.sample(range(1, 5001), min(num_movies_rated, 100))
    
    for movie_id in rated_movies:
        # Bias rating basato su età, genere, etc.
        base_rating = random.uniform(2.5, 4.5)
        
        # Aggiungi rumore
        rating = max(0.5, min(5.0, round(base_rating + random.uniform(-1, 1), 1)))
        
        # Altri dati
        watch_duration = random.randint(10, duration if 'duration' in locals() else 120)
        device = random.choice(['mobile', 'tablet', 'tv', 'desktop'])
        timestamp = datetime(2023, 1, 1) + timedelta(
            days=random.randint(0, 365),
            hours=random.randint(0, 23),
            minutes=random.randint(0, 59)
        )
        
        ratings_data.append((
            user_id, movie_id, rating, timestamp.strftime('%Y-%m-%d %H:%M:%S'),
            watch_duration, device
        ))
        num_ratings += 1
        
        if len(ratings_data) % 100000 == 0:
            print(f"   Generati {len(ratings_data):,} rating...")

# Inserimento in batch
batch_size = 50000
for i in range(0, len(ratings_data), batch_size):
    batch = ratings_data[i:i+batch_size]
    cursor.executemany('''
        INSERT INTO ratings (user_id, movie_id, rating, timestamp, watch_duration_minutes, device_type)
        VALUES (?, ?, ?, ?, ?, ?)
    ''', batch)

print(f"✅ {len(ratings_data):,} rating generati")
print(f"   • Sparsità: {(1 - len(ratings_data)/(10000*5000))*100:.1f}%")
print(f"   • Rating per utente medio: {len(ratings_data)/10000:.1f}")

# 1.6 AGGIORNA STATISTICHE UTENTI
print("\n📈 CALCOLO STATISTICHE UTENTI...")

# Aggiorna avg_rating per ogni utente
cursor.execute('''
    UPDATE users 
    SET avg_rating = (
        SELECT AVG(rating) 
        FROM ratings 
        WHERE ratings.user_id = users.user_id
    )
    WHERE EXISTS (
        SELECT 1 FROM ratings WHERE ratings.user_id = users.user_id
    )
''')

# 1.7 VERIFICA DATABASE
print("\n🔍 VERIFICA DATABASE COMPLETATO...")

tables_info = cursor.execute('''
    SELECT name FROM sqlite_master WHERE type='table'
''').fetchall()

print("📊 TABELLE NEL DATABASE:")
for table in tables_info:
    count = cursor.execute(f'SELECT COUNT(*) FROM {table[0]}').fetchone()[0]
    print(f"   • {table[0]}: {count:,} righe")

# Statistiche rating
rating_stats = cursor.execute('''
    SELECT 
        COUNT(*) as total_ratings,
        AVG(rating) as avg_rating,
        MIN(rating) as min_rating,
        MAX(rating) as max_rating
    FROM ratings
''').fetchone()

print(f"\n⭐ STATISTICHE RATING:")
print(f"   • Totale rating: {rating_stats[0]:,}")
print(f"   • Rating medio: {rating_stats[1]:.2f}/5.0")
print(f"   • Range: {rating_stats[2]:.1f} - {rating_stats[3]:.1f}")

# Distribuzione rating
distribuzione = cursor.execute('''
    SELECT rating, COUNT(*) as count
    FROM ratings
    GROUP BY rating
    ORDER BY rating
''').fetchall()

print(f"\n📈 DISTRIBUZIONE RATING:")
for rating, count in distribuzione:
    percent = (count / rating_stats[0]) * 100
    print(f"   • {rating:.1f}: {count:6,} ({percent:5.1f}%) {'█' * int(percent/2)}")

# 1.8 SALVA DATABASE SU FILE (PER PERSISTENZA)
conn.commit()
print("\n💾 DATABASE SALVATO CON SUCCESSO!")
print("   • Utenti: 10,000")
print("   • Film: 5,000")
print("   • Rating: ~1,000,000")
print("   • Pronto per analisi ML e Deep Learning!")

print("\n" + "=" * 70)
print("✅ PARTE 1 COMPLETATA: DATABASE PROFESSIONALE CREATO")
print("=" * 70)

Parte 2: Analisi Dati e Feature Engineering Avanzato

2
EDA Completa e Preparazione Dati per ML
PYTHON
# ================================================
# 📊 PARTE 2: ANALISI DATI E FEATURE ENGINEERING
# ================================================

print("\n" + "=" * 70)
print("📈 ANALISI ESPLORATIVA DATI (EDA) E FEATURE ENGINEERING")
print("=" * 70)

# 2.1 ESTRAZIONE DATI DA SQL A PANDAS
print("\n🔍 ESTRAZIONE DATI PER ANALISI...")

# Carica dati principali in DataFrame
users_df = pd.read_sql_query("SELECT * FROM users", conn)
movies_df = pd.read_sql_query("SELECT * FROM movies", conn)
ratings_df = pd.read_sql_query("SELECT * FROM ratings", conn)

print(f"✅ Dati estratti:")
print(f"   • Utenti: {users_df.shape[0]:,} x {users_df.shape[1]} colonne")
print(f"   • Film: {movies_df.shape[0]:,} x {movies_df.shape[1]} colonne")
print(f"   • Rating: {ratings_df.shape[0]:,} x {ratings_df.shape[1]} colonne")

# 2.2 ANALISI UTENTI
print("\n👥 ANALISI DEMOGRAFICA UTENTI...")

fig, axes = plt.subplots(2, 3, figsize=(18, 12))
fig.suptitle('Analisi Demografica Utenti', fontsize=16, fontweight='bold')

# Distribuzione età
axes[0, 0].hist(users_df['age'], bins=30, edgecolor='black', alpha=0.7, color='skyblue')
axes[0, 0].set_xlabel('Età')
axes[0, 0].set_ylabel('Frequenza')
axes[0, 0].set_title('Distribuzione Età Utenti')
axes[0, 0].grid(True, alpha=0.3)

# Distribuzione genere
gender_counts = users_df['gender'].value_counts()
axes[0, 1].pie(gender_counts.values, labels=gender_counts.index, autopct='%1.1f%%',
               colors=['lightblue', 'lightpink', 'lightgreen'])
axes[0, 1].set_title('Distribuzione Genere')

# Distribuzione paese
top_countries = users_df['country'].value_counts().head(10)
axes[0, 2].barh(top_countries.index, top_countries.values, color='lightcoral')
axes[0, 2].set_xlabel('Numero Utenti')
axes[0, 2].set_title('Top 10 Paesi Utenti')
axes[0, 2].invert_yaxis()

# Tipo abbonamento
subscription_counts = users_df['subscription_type'].value_counts()
axes[1, 0].bar(subscription_counts.index, subscription_counts.values, 
               color=['lightgray', 'gold', 'lightblue'])
axes[1, 0].set_xlabel('Tipo Abbonamento')
axes[1, 0].set_ylabel('Numero Utenti')
axes[1, 0].set_title('Distribuzione Abbonamenti')
for i, v in enumerate(subscription_counts.values):
    axes[1, 0].text(i, v + 50, str(v), ha='center')

# Tempo visione totale
axes[1, 1].hist(users_df['total_watch_time_hours'], bins=50, 
                edgecolor='black', alpha=0.7, color='mediumseagreen')
axes[1, 1].set_xlabel('Ore Totali Visione')
axes[1, 1].set_ylabel('Frequenza')
axes[1, 1].set_title('Distribuzione Ore Visione')
axes[1, 1].grid(True, alpha=0.3)

# Rating medio utenti
axes[1, 2].hist(users_df['avg_rating'].dropna(), bins=30, 
                edgecolor='black', alpha=0.7, color='orange')
axes[1, 2].set_xlabel('Rating Medio')
axes[1, 2].set_ylabel('Frequenza')
axes[1, 2].set_title('Distribuzione Rating Medio Utenti')
axes[1, 2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 2.3 ANALISI FILM
print("\n🎬 ANALISI CATALOGO FILM...")

fig, axes = plt.subplots(2, 3, figsize=(18, 12))
fig.suptitle('Analisi Catalogo Film', fontsize=16, fontweight='bold')

# Distribuzione anni di uscita
axes[0, 0].hist(movies_df['release_year'], bins=30, edgecolor='black', alpha=0.7, color='coral')
axes[0, 0].set_xlabel('Anno di Uscita')
axes[0, 0].set_ylabel('Numero Film')
axes[0, 0].set_title('Distribuzione Anni di Uscita')
axes[0, 0].grid(True, alpha=0.3)

# Distribuzione durata
axes[0, 1].hist(movies_df['duration_minutes'], bins=30, edgecolor='black', alpha=0.7, color='lightseagreen')
axes[0, 1].set_xlabel('Durata (minuti)')
axes[0, 1].set_ylabel('Numero Film')
axes[0, 1].set_title('Distribuzione Durata Film')
axes[0, 1].grid(True, alpha=0.3)

# Distribuzione rating IMDb
axes[0, 2].hist(movies_df['imdb_rating'], bins=20, edgecolor='black', alpha=0.7, color='gold')
axes[0, 2].set_xlabel('Rating IMDb')
axes[0, 2].set_ylabel('Numero Film')
axes[0, 2].set_title('Distribuzione Rating IMDb')
axes[0, 2].grid(True, alpha=0.3)

# Analisi generi (più frequenti)
# Estrai tutti i generi
all_genres = []
for genres in movies_df['genre'].dropna():
    all_genres.extend(genres.split('|'))

genre_counts = pd.Series(all_genres).value_counts().head(15)
axes[1, 0].barh(genre_counts.index, genre_counts.values, color='mediumpurple')
axes[1, 0].set_xlabel('Numero Film')
axes[1, 0].set_title('Top 15 Generi')
axes[1, 0].invert_yaxis()

# Budget vs Revenue
axes[1, 1].scatter(movies_df['budget_millions'], movies_df['revenue_millions'],
                   alpha=0.5, color='dodgerblue', s=20)
axes[1, 1].set_xlabel('Budget (milioni $)')
axes[1, 1].set_ylabel('Revenue (milioni $)')
axes[1, 1].set_title('Budget vs Revenue')
axes[1, 1].grid(True, alpha=0.3)

# Cloud parole descrizioni
# Combina tutte le descrizioni
all_descriptions = ' '.join(movies_df['description'].dropna().astype(str))
wordcloud = WordCloud(width=800, height=400, background_color='white',
                      max_words=100, contour_width=3, contour_color='steelblue').generate(all_descriptions)
axes[1, 2].imshow(wordcloud, interpolation='bilinear')
axes[1, 2].axis('off')
axes[1, 2].set_title('Word Cloud Descrizioni Film')

plt.tight_layout()
plt.show()

# 2.4 ANALISI RATING E INTERAZIONI
print("\n⭐ ANALISI RATING E COMPORTAMENTO UTENTI...")

fig, axes = plt.subplots(2, 3, figsize=(18, 12))
fig.suptitle('Analisi Rating e Interazioni', fontsize=16, fontweight='bold')

# Heatmap rating nel tempo (sample)
rating_sample = ratings_df.sample(10000, random_state=42)
rating_sample['hour'] = pd.to_datetime(rating_sample['timestamp']).dt.hour
rating_sample['day_of_week'] = pd.to_datetime(rating_sample['timestamp']).dt.dayofweek

pivot_data = rating_sample.pivot_table(
    index='day_of_week', 
    columns='hour', 
    values='rating',
    aggfunc='mean'
)

im = axes[0, 0].imshow(pivot_data, cmap='YlOrRd', aspect='auto')
axes[0, 0].set_xlabel('Ora del Giorno')
axes[0, 0].set_ylabel('Giorno della Settimana')
axes[0, 0].set_title('Heatmap Rating per Ora/Giorno')
axes[0, 0].set_xticks(range(0, 24, 4))
axes[0, 0].set_yticks(range(7))
axes[0, 0].set_yticklabels(['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'])
plt.colorbar(im, ax=axes[0, 0])

# Distribuzione device
device_counts = ratings_df['device_type'].value_counts()
axes[0, 1].pie(device_counts.values, labels=device_counts.index, autopct='%1.1f%%',
               colors=['lightblue', 'lightgreen', 'lightcoral', 'gold'])
axes[0, 1].set_title('Distribuzione Device')

# Rating per genere film (top 10)
# Unisci rating con generi film
rating_with_genre = pd.merge(
    ratings_df[['movie_id', 'rating']],
    movies_df[['movie_id', 'genre']],
    on='movie_id'
)

# Espandi generi (un film può avere multiple generi)
expanded_ratings = []
for idx, row in rating_with_genre.iterrows():
    if pd.notna(row['genre']):
        for genre in row['genre'].split('|'):
            expanded_ratings.append({'genre': genre, 'rating': row['rating']})

genre_ratings_df = pd.DataFrame(expanded_ratings)
genre_avg_rating = genre_ratings_df.groupby('genre')['rating'].agg(['mean', 'count']).sort_values('count', ascending=False).head(10)

axes[0, 2].barh(genre_avg_rating.index, genre_avg_rating['mean'], color='lightseagreen')
axes[0, 2].set_xlabel('Rating Medio')
axes[0, 2].set_title('Top 10 Generi per Rating Medio')
axes[0, 2].invert_yaxis()

# Distribuzione numero rating per utente
user_rating_counts = ratings_df['user_id'].value_counts()
axes[1, 0].hist(user_rating_counts.values, bins=50, edgecolor='black', alpha=0.7, color='mediumorchid', log=True)
axes[1, 0].set_xlabel('Numero Rating per Utente')
axes[1, 0].set_ylabel('Frequenza (log)')
axes[1, 0].set_title('Distribuzione Rating per Utente (log scale)')
axes[1, 0].grid(True, alpha=0.3)

# Distribuzione numero rating per film
movie_rating_counts = ratings_df['movie_id'].value_counts()
axes[1, 1].hist(movie_rating_counts.values, bins=50, edgecolor='black', alpha=0.7, color='darkorange', log=True)
axes[1, 1].set_xlabel('Numero Rating per Film')
axes[1, 1].set_ylabel('Frequenza (log)')
axes[1, 1].set_title('Distribuzione Rating per Film (log scale)')
axes[1, 1].grid(True, alpha=0.3)

# Correlazione età utente con rating medio
user_age_rating = pd.merge(
    users_df[['user_id', 'age', 'avg_rating']],
    ratings_df[['user_id', 'rating']],
    on='user_id'
).groupby('age')['rating'].mean().reset_index()

axes[1, 2].scatter(user_age_rating['age'], user_age_rating['rating'], alpha=0.5, color='crimson', s=30)
axes[1, 2].set_xlabel('Età Utente')
axes[1, 2].set_ylabel('Rating Medio')
axes[1, 2].set_title('Rating Medio per Età Utente')
axes[1, 2].grid(True, alpha=0.3)

# Regression line
z = np.polyfit(user_age_rating['age'], user_age_rating['rating'], 1)
p = np.poly1d(z)
axes[1, 2].plot(user_age_rating['age'], p(user_age_rating['age']), "r--", alpha=0.8)

plt.tight_layout()
plt.show()

# 2.5 FEATURE ENGINEERING AVANZATO
print("\n🔧 FEATURE ENGINEERING AVANZATO PER ML...")

# A. Features per Utenti
print("\n👤 CREAZIONE FEATURES UTENTI:")

user_features = users_df.copy()

# 1. Activity level basato su numero rating
user_activity = ratings_df['user_id'].value_counts().reset_index()
user_activity.columns = ['user_id', 'num_ratings']
user_features = pd.merge(user_features, user_activity, on='user_id', how='left')
user_features['num_ratings'] = user_features['num_ratings'].fillna(0)

# 2. Rating std dev (quanto è severo/indulgente)
user_rating_stats = ratings_df.groupby('user_id')['rating'].agg(['std', 'min', 'max']).reset_index()
user_rating_stats.columns = ['user_id', 'rating_std', 'rating_min', 'rating_max']
user_features = pd.merge(user_features, user_rating_stats, on='user_id', how='left')

# 3. Genre preference (top 3 generi preferiti)
user_genre_pref = pd.merge(
    ratings_df[['user_id', 'movie_id', 'rating']],
    movies_df[['movie_id', 'genre']],
    on='movie_id'
)

# Espandi generi e calcola preferenze
user_genre_scores = []
for user_id in user_features['user_id']:
    user_ratings = user_genre_pref[user_genre_pref['user_id'] == user_id]
    if len(user_ratings) > 0:
        genre_scores = {}
        for idx, row in user_ratings.iterrows():
            if pd.notna(row['genre']):
                genres = row['genre'].split('|')
                for genre in genres:
                    if genre not in genre_scores:
                        genre_scores[genre] = []
                    genre_scores[genre].append(row['rating'])
        
        # Calcola rating medio per ogni genere
        genre_avg = {genre: np.mean(scores) for genre, scores in genre_scores.items() if len(scores) > 2}
        if genre_avg:
            top_genres = sorted(genre_avg.items(), key=lambda x: x[1], reverse=True)[:3]
            user_genre_scores.append({
                'user_id': user_id,
                'top_genre_1': top_genres[0][0] if len(top_genres) > 0 else None,
                'top_genre_1_score': top_genres[0][1] if len(top_genres) > 0 else 0,
                'top_genre_2': top_genres[1][0] if len(top_genres) > 1 else None,
                'top_genre_3': top_genres[2][0] if len(top_genres) > 2 else None
            })

user_genre_df = pd.DataFrame(user_genre_scores)
user_features = pd.merge(user_features, user_genre_df, on='user_id', how='left')

print(f"   • Features utenti create: {user_features.shape[1]} colonne")

# B. Features per Film
print("\n🎬 CREAZIONE FEATURES FILM:")

movie_features = movies_df.copy()

# 1. Popularity score (basato su numero rating)
movie_popularity = ratings_df['movie_id'].value_counts().reset_index()
movie_popularity.columns = ['movie_id', 'num_ratings']
movie_features = pd.merge(movie_features, movie_popularity, on='movie_id', how='left')
movie_features['num_ratings'] = movie_features['num_ratings'].fillna(0)

# 2. Average rating e variabilità
movie_rating_stats = ratings_df.groupby('movie_id')['rating'].agg(['mean', 'std', 'count']).reset_index()
movie_rating_stats.columns = ['movie_id', 'avg_rating', 'rating_std', 'rating_count']
movie_features = pd.merge(movie_features, movie_rating_stats, on='movie_id', how='left')

# 3. Genre encoding (one-hot per top 15 generi)
top_genres = pd.Series(all_genres).value_counts().head(15).index.tolist()
for genre in top_genres:
    movie_features[f'genre_{genre.lower().replace(" ", "_")}'] = movie_features['genre'].apply(
        lambda x: 1 if pd.notna(x) and genre in x.split('|') else 0
    )

# 4. Text features dalle descrizioni (lunghezza, complessità)
movie_features['desc_length'] = movie_features['description'].apply(
    lambda x: len(str(x).split()) if pd.notna(x) else 0
)
movie_features['desc_sentiment'] = movie_features['description'].apply(
    lambda x: len([w for w in str(x).lower().split() if w in ['great', 'awesome', 'amazing', 'love', 'best']]) -
              len([w for w in str(x).lower().split() if w in ['bad', 'terrible', 'worst', 'boring', 'slow']])
)

# 5. Success score (revenue/budget)
movie_features['success_ratio'] = movie_features.apply(
    lambda row: row['revenue_millions'] / row['budget_millions'] 
    if row['budget_millions'] > 0 else 0, axis=1
)

print(f"   • Features film create: {movie_features.shape[1]} colonne")

# C. Matrice Interazioni Utente-Film
print("\n🔗 CREAZIONE MATRICE INTERAZIONI PER COLLABORATIVE FILTERING...")

# Crea user-item matrix
user_item_matrix = ratings_df.pivot_table(
    index='user_id',
    columns='movie_id',
    values='rating'
).fillna(0)

print(f"   • User-Item Matrix: {user_item_matrix.shape[0]} utenti x {user_item_matrix.shape[1]} film")
print(f"   • Sparsità: {(user_item_matrix == 0).sum().sum() / (user_item_matrix.shape[0] * user_item_matrix.shape[1]) * 100:.2f}%")

# D. Preparazione Dataset per ML
print("\n📊 PREPARAZIONE DATASET FINALE PER MODELLI ML...")

# Crea dataset combinato per supervised learning
ml_dataset = ratings_df[['user_id', 'movie_id', 'rating']].copy()

# Aggiungi features utenti
ml_dataset = pd.merge(ml_dataset, user_features, on='user_id', how='left')

# Aggiungi features film
ml_dataset = pd.merge(ml_dataset, movie_features, on='movie_id', how='left')

# Rimuovi colonne non necessarie
columns_to_drop = ['title', 'genre', 'director', 'description', 'main_actors', 
                   'language', 'production_country', 'join_date', 'top_genre_1',
                   'top_genre_2', 'top_genre_3', 'timestamp', 'device_type']
ml_dataset = ml_dataset.drop(columns=[col for col in columns_to_drop if col in ml_dataset.columns])

# Gestione valori mancanti
ml_dataset = ml_dataset.fillna(0)

print(f"✅ Dataset ML creato:")
print(f"   • Dimensioni: {ml_dataset.shape[0]:,} samples x {ml_dataset.shape[1]} features")
print(f"   • Target: 'rating' (valore da 0.5 a 5.0)")
print(f"   • Features numeriche: {len(ml_dataset.select_dtypes(include=[np.number]).columns)}")
print(f"   • Features categoriche: {len(ml_dataset.select_dtypes(include=['object']).columns)}")

# E. Salvataggio dataset per uso futuro
ml_dataset.to_csv('movieflix_ml_dataset.csv', index=False)
user_features.to_csv('user_features.csv', index=False)
movie_features.to_csv('movie_features.csv', index=False)

print("\n💾 DATASET SALVATI:")
print("   • movieflix_ml_dataset.csv - Dataset completo per ML")
print("   • user_features.csv - Features utenti")
print("   • movie_features.csv - Features film")

print("\n" + "=" * 70)
print("✅ PARTE 2 COMPLETATA: ANALISI DATI E FEATURE ENGINEERING")
print("=" * 70)

Parte 3: Modelli Machine Learning Classici

3
Implementazione Algoritmi Raccomandazione Classici
PYTHON
# ================================================
# 🤖 PARTE 3: MODELLI MACHINE LEARNING CLASSICI
# ================================================

print("\n" + "=" * 70)
print("🧠 IMPLEMENTAZIONE ALGORITMI RACCOMANDAZIONE CLASSICI")
print("=" * 70)

# Carica dataset preparato
print("\n📂 CARICAMENTO DATASET PREPARATO...")
ml_dataset = pd.read_csv('movieflix_ml_dataset.csv')
user_features = pd.read_csv('user_features.csv')
movie_features = pd.read_csv('movie_features.csv')

print(f"📊 Dataset caricati:")
print(f"   • ML Dataset: {ml_dataset.shape}")
print(f"   • User Features: {user_features.shape}")
print(f"   • Movie Features: {movie_features.shape}")

# 3.1 PREPARAZIONE DATI PER MODELLI DIVERSI
print("\n🔧 PREPARAZIONE DATI PER MODELLI ML...")

# Separazione features e target
X = ml_dataset.drop(columns=['rating', 'user_id', 'movie_id'])
y = ml_dataset['rating']

# Identifica colonne numeriche e categoriche
numeric_cols = X.select_dtypes(include=[np.number]).columns.tolist()
categorical_cols = X.select_dtypes(include=['object']).columns.tolist()

print(f"   • Features numeriche: {len(numeric_cols)}")
print(f"   • Features categoriche: {len(categorical_cols)}")

# Encoding features categoriche
label_encoders = {}
for col in categorical_cols:
    le = LabelEncoder()
    X[col] = le.fit_transform(X[col].astype(str))
    label_encoders[col] = le

# Split train/test
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=pd.cut(y, bins=10)
)

# Scaling features numeriche
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print(f"\n🎯 SPLIT DATASET:")
print(f"   • Training set: {X_train.shape[0]:,} samples")
print(f"   • Test set: {X_test.shape[0]:,} samples")
print(f"   • Features: {X_train.shape[1]}")

# 3.2 MODEL 1: RANDOM FOREST REGRESSOR (Baseline)
print("\n" + "=" * 50)
print("🌲 MODELLO 1: RANDOM FOREST REGRESSOR")
print("=" * 50)

from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

print("\n🚀 Addestramento Random Forest...")

rf_model = RandomForestRegressor(
    n_estimators=100,
    max_depth=15,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42,
    n_jobs=-1
)

rf_model.fit(X_train_scaled, y_train)

# Predizioni
y_pred_rf = rf_model.predict(X_test_scaled)

# Metriche
rf_rmse = np.sqrt(mean_squared_error(y_test, y_pred_rf))
rf_mae = mean_absolute_error(y_test, y_pred_rf)
rf_r2 = r2_score(y_test, y_pred_rf)

print(f"\n📊 PERFORMANCE RANDOM FOREST:")
print(f"   • RMSE: {rf_rmse:.4f}")
print(f"   • MAE: {rf_mae:.4f}")
print(f"   • R² Score: {rf_r2:.4f}")

# Feature importance
rf_feature_importance = pd.DataFrame({
    'feature': X.columns,
    'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)

print(f"\n🏆 TOP 10 FEATURE IMPORTANCE:")
print(rf_feature_importance.head(10).to_string(index=False))

# 3.3 MODEL 2: MATRIX FACTORIZATION (SVD)
print("\n" + "=" * 50)
print("🔢 MODELLO 2: MATRIX FACTORIZATION (SVD)")
print("=" * 50)

print("\n🎯 Preparazione User-Item Matrix per Collaborative Filtering...")

# Crea user-item matrix sparsa
user_item_matrix = ratings_df.pivot_table(
    index='user_id',
    columns='movie_id',
    values='rating'
).fillna(0)

# Converti in formato sparse per efficienza
sparse_matrix = sp.csr_matrix(user_item_matrix.values)

print(f"   • User-Item Matrix: {sparse_matrix.shape}")
print(f"   • Sparsity: {(sparse_matrix == 0).sum() / (sparse_matrix.shape[0] * sparse_matrix.shape[1]) * 100:.2f}%")

# Applica SVD (Truncated SVD per matrici sparse)
n_components = 50
print(f"\n🔧 Applicando Truncated SVD con {n_components} componenti...")

svd = TruncatedSVD(n_components=n_components, random_state=42)
user_factors = svd.fit_transform(sparse_matrix)
item_factors = svd.components_.T

print(f"   • Explained variance: {svd.explained_variance_ratio_.sum():.3f}")
print(f"   • User factors shape: {user_factors.shape}")
print(f"   • Item factors shape: {item_factors.shape}")

# Funzione per predizioni SVD
def predict_svd_rating(user_idx, movie_idx):
    """Predici rating usando fattori SVD"""
    if user_idx >= user_factors.shape[0] or movie_idx >= item_factors.shape[0]:
        return 2.5  # Valore di default
    return np.dot(user_factors[user_idx], item_factors[movie_idx])

# Test su subset
test_sample = ml_dataset.sample(10000, random_state=42)
svd_predictions = []
for _, row in test_sample.iterrows():
    user_id = row['user_id']
    movie_id = row['movie_id']
    
    # Converti ID a indici
    user_idx = user_id - 1 if user_id <= user_item_matrix.shape[0] else None
    movie_idx = movie_id - 1 if movie_id <= user_item_matrix.shape[1] else None
    
    if user_idx is not None and movie_idx is not None:
        pred = predict_svd_rating(user_idx, movie_idx)
        # Clip tra 0.5 e 5.0
        pred = max(0.5, min(5.0, pred))
        svd_predictions.append(pred)
    else:
        svd_predictions.append(2.5)

# Valuta solo predizioni valide
valid_idx = [i for i, pred in enumerate(svd_predictions) if pred != 2.5]
if valid_idx:
    svd_rmse = np.sqrt(mean_squared_error(
        test_sample.iloc[valid_idx]['rating'],
        np.array(svd_predictions)[valid_idx]
    ))
    svd_mae = mean_absolute_error(
        test_sample.iloc[valid_idx]['rating'],
        np.array(svd_predictions)[valid_idx]
    )
else:
    svd_rmse = svd_mae = float('inf')

print(f"\n📊 PERFORMANCE SVD:")
print(f"   • RMSE: {svd_rmse:.4f}")
print(f"   • MAE: {svd_mae:.4f}")
print(f"   • Predizioni valide: {len(valid_idx)}/{len(test_sample)}")

# 3.4 MODEL 3: K-NEAREST NEIGHBORS (Item-Based CF)
print("\n" + "=" * 50)
print("👥 MODELLO 3: K-NEAREST NEIGHBORS (Item-Based)")
print("=" * 50)

print("\n🔍 Costruzione modello KNN per similarità film...")

# Usa item factors da SVD per similarità
knn_model = NearestNeighbors(
    n_neighbors=20,
    metric='cosine',
    algorithm='brute'
)

knn_model.fit(item_factors)

# Funzione per raccomandazioni KNN
def knn_recommendations(movie_id, n_recommendations=10):
    """Trova film simili usando KNN"""
    movie_idx = movie_id - 1
    if movie_idx >= item_factors.shape[0]:
        return []
    
    distances, indices = knn_model.kneighbors(
        item_factors[movie_idx].reshape(1, -1),
        n_neighbors=n_recommendations + 1
    )
    
    # Escludi il film stesso (primo risultato)
    similar_movies = []
    for i in range(1, len(indices[0])):
        similar_movie_id = indices[0][i] + 1
        similarity = 1 - distances[0][i]  # Converti distanza in similarità
        similar_movies.append((similar_movie_id, similarity))
    
    return similar_movies

# Test KNN su alcuni film
test_movies = [1, 100, 500, 1000, 2500]  # Matrix, Inception, etc.
print("\n🎯 TEST RACCOMANDAZIONI KNN:")
for movie_id in test_movies:
    movie_title = movies_df[movies_df['movie_id'] == movie_id]['title'].values
    title = movie_title[0] if len(movie_title) > 0 else f"Movie {movie_id}"
    
    recommendations = knn_recommendations(movie_id, 5)
    
    print(f"\n🎬 Per '{title}' (ID: {movie_id}):")
    for rec_id, similarity in recommendations:
        rec_title = movies_df[movies_df['movie_id'] == rec_id]['title'].values
        rec_title = rec_title[0] if len(rec_title) > 0 else f"Movie {rec_id}"
        print(f"   • {rec_title} (similarity: {similarity:.3f})")

# 3.5 MODEL 4: CONTENT-BASED FILTERING
print("\n" + "=" * 50)
print("📝 MODELLO 4: CONTENT-BASED FILTERING")
print("=" * 50)

print("\n🔠 Estrazione features dai testi delle descrizioni...")

# Preprocessing descrizioni
def preprocess_text(text):
    """Pulizia e tokenizzazione testo"""
    if pd.isna(text):
        return ""
    
    # Converti in lowercase
    text = str(text).lower()
    
    # Rimuovi caratteri speciali
    text = re.sub(r'[^a-z\s]', '', text)
    
    # Tokenizza
    tokens = word_tokenize(text)
    
    # Rimuovi stopwords
    stop_words = set(stopwords.words('english'))
    tokens = [word for word in tokens if word not in stop_words]
    
    return ' '.join(tokens)

# Applica preprocessing
movies_df['processed_description'] = movies_df['description'].apply(preprocess_text)

# TF-IDF Vectorization
from sklearn.feature_extraction.text import TfidfVectorizer

print("📊 Creazione TF-IDF vectors per descrizioni film...")

tfidf = TfidfVectorizer(
    max_features=1000,
    stop_words='english',
    ngram_range=(1, 2)
)

tfidf_matrix = tfidf.fit_transform(movies_df['processed_description'].fillna(''))

print(f"   • TF-IDF Matrix: {tfidf_matrix.shape}")
print(f"   • Vocabolario: {len(tfidf.get_feature_names_out())} parole")

# Cosine similarity tra film basata su TF-IDF
from sklearn.metrics.pairwise import cosine_similarity

print("\n🔍 Calcolo similarità cosine tra film...")
content_similarity = cosine_similarity(tfidf_matrix, tfidf_matrix)

# Funzione per raccomandazioni content-based
def content_based_recommendations(movie_id, n_recommendations=10):
    """Raccomandazioni basate su similarità di contenuto"""
    movie_idx = movie_id - 1
    if movie_idx >= content_similarity.shape[0]:
        return []
    
    # Prendi similarity scores per questo film
    sim_scores = list(enumerate(content_similarity[movie_idx]))
    
    # Ordina per similarità (escludendo il film stesso)
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)[1:n_recommendations+1]
    
    # Estrai ID film e similarity scores
    recommendations = [(score[0] + 1, score[1]) for score in sim_scores]
    
    return recommendations

# Test content-based filtering
print("\n🎯 TEST RACCOMANDAZIONI CONTENT-BASED:")
for movie_id in test_movies[:3]:  # Solo primi 3 per brevità
    movie_title = movies_df[movies_df['movie_id'] == movie_id]['title'].values
    title = movie_title[0] if len(movie_title) > 0 else f"Movie {movie_id}"
    
    recommendations = content_based_recommendations(movie_id, 5)
    
    print(f"\n🎬 Per '{title}' (ID: {movie_id}):")
    for rec_id, similarity in recommendations:
        rec_title = movies_df[movies_df['movie_id'] == rec_id]['title'].values
        rec_title = rec_title[0] if len(rec_title) > 0 else f"Movie {rec_id}"
        print(f"   • {rec_title} (similarity: {similarity:.3f})")

# 3.6 MODEL 5: HYBRID APPROACH
print("\n" + "=" * 50)
print("🤝 MODELLO 5: APPROCCIO IBRIDO (Collaborative + Content)")
print("=" * 50)

print("\n🔗 Combinazione Collaborative Filtering e Content-Based Filtering...")

def hybrid_recommendations(user_id, movie_id=None, n_recommendations=10, alpha=0.7):
    """
    Raccomandazioni ibride che combinano:
    - Collaborative filtering (peso: alpha)
    - Content-based filtering (peso: 1 - alpha)
    """
    
    recommendations = {}
    
    # Se movie_id è fornito, usa come punto di partenza
    if movie_id:
        # 1. Content-based recommendations
        content_recs = content_based_recommendations(movie_id, n_recommendations * 2)
        
        # 2. Per ogni film raccomandato, trova utenti simili che l'hanno visto
        for rec_id, content_score in content_recs:
            # Trova utenti che hanno visto questo film
            movie_ratings = ratings_df[ratings_df['movie_id'] == rec_id]
            
            if len(movie_ratings) > 0:
                # Calcola rating medio per questo film
                avg_rating = movie_ratings['rating'].mean()
                
                # Trova similarità con utente corrente (se presente)
                user_rating = ratings_df[
                    (ratings_df['user_id'] == user_id) & 
                    (ratings_df['movie_id'] == rec_id)
                ]
                
                if len(user_rating) == 0:  # Utente non ha ancora visto
                    # Punteggio ibrido
                    hybrid_score = (alpha * avg_rating/5.0) + ((1 - alpha) * content_score)
                    recommendations[rec_id] = hybrid_score
            else:
                # Solo content-based se nessun rating
                recommendations[rec_id] = (1 - alpha) * content_score
    
    # Se abbiamo abbastanza raccomandazioni, restituisci le migliori
    if recommendations:
        sorted_recs = sorted(recommendations.items(), key=lambda x: x[1], reverse=True)
        return [(rec_id, score) for rec_id, score in sorted_recs[:n_recommendations]]
    else:
        # Fallback: top film popolari
        popular_movies = movie_features.sort_values('num_ratings', ascending=False).head(n_recommendations)
        return [(row['movie_id'], row['num_ratings']/1000) for _, row in popular_movies.iterrows()]

# Test approccio ibrido
print("\n🎯 TEST RACCOMANDAZIONI IBRIDE:")
test_user = 42
test_movie = 1  # The Matrix

movie_title = movies_df[movies_df['movie_id'] == test_movie]['title'].values[0]
print(f"\n👤 Utente {test_user} ha visto '{movie_title}' (ID: {test_movie})")
print("🎬 Raccomandazioni ibride:")

hybrid_recs = hybrid_recommendations(test_user, test_movie, n_recommendations=8, alpha=0.6)

for i, (rec_id, score) in enumerate(hybrid_recs, 1):
    rec_title = movies_df[movies_df['movie_id'] == rec_id]['title'].values
    rec_title = rec_title[0] if len(rec_title) > 0 else f"Movie {rec_id}"
    print(f"   {i}. {rec_title} (score: {score:.3f})")

# 3.7 VALUTAZIONE COMPARATIVA MODELLI
print("\n" + "=" * 50)
print("📈 VALUTAZIONE COMPARATIVA MODELLI")
print("=" * 50)

# Prepara risultati comparativi
models_comparison = {
    'Model': ['Random Forest', 'SVD', 'Hybrid (Sample)'],
    'RMSE': [rf_rmse, svd_rmse, 'N/A'],
    'MAE': [rf_mae, svd_mae, 'N/A'],
    'R²': [rf_r2, 'N/A', 'N/A'],
    'Type': ['Supervised ML', 'Matrix Factorization', 'Hybrid CF + CB'],
    'Strengths': ['Utilizza tutte le features', 'Scopre pattern nascosti', 'Combina multiple approcci']
}

comparison_df = pd.DataFrame(models_comparison)
print("\n📊 COMPARAZIONE MODELLI:")
print(comparison_df.to_string(index=False))

# Visualizzazione comparativa
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Confronto errori
models = ['Random Forest', 'SVD']
rmse_values = [rf_rmse, svd_rmse]
mae_values = [rf_mae, svd_mae]

x = np.arange(len(models))
width = 0.35

axes[0].bar(x - width/2, rmse_values, width, label='RMSE', color='lightcoral', alpha=0.8)
axes[0].bar(x + width/2, mae_values, width, label='MAE', color='skyblue', alpha=0.8)
axes[0].set_xlabel('Modello')
axes[0].set_ylabel('Error Value')
axes[0].set_title('Confronto Errori: RMSE vs MAE')
axes[0].set_xticks(x)
axes[0].set_xticklabels(models)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Feature importance top 10
top_features = rf_feature_importance.head(10)
axes[1].barh(top_features['feature'], top_features['importance'], color='mediumseagreen')
axes[1].set_xlabel('Importance')
axes[1].set_title('Top 10 Feature Importance (Random Forest)')
axes[1].invert_yaxis()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 3.8 SALVATAGGIO MODELLI
print("\n💾 SALVATAGGIO MODELLI E COMPONENTI...")

# Salva Random Forest
joblib.dump(rf_model, 'models/random_forest_model.pkl')
print("✅ Random Forest salvato")

# Salva SVD components
svd_components = {
    'user_factors': user_factors,
    'item_factors': item_factors,
    'explained_variance': svd.explained_variance_ratio_
}
joblib.dump(svd_components, 'models/svd_components.pkl')
print("✅ Componenti SVD salvati")

# Salva KNN model
joblib.dump(knn_model, 'models/knn_model.pkl')
print("✅ Modello KNN salvato")

# Salva TF-IDF vectorizer
joblib.dump(tfidf, 'models/tfidf_vectorizer.pkl')
print("✅ TF-IDF vectorizer salvato")

# Salva content similarity matrix
joblib.dump(content_similarity, 'models/content_similarity.pkl')
print("✅ Matrice similarità contenuto salvata")

# Salva preprocessing objects
preprocessing_objects = {
    'scaler': scaler,
    'label_encoders': label_encoders
}
joblib.dump(preprocessing_objects, 'models/preprocessing_objects.pkl')
print("✅ Oggetti preprocessing salvati")

print("\n" + "=" * 70)
print("✅ PARTE 3 COMPLETATA: MODELLI ML CLASSICI IMPLEMENTATI")
print("=" * 70)

Parte 4: Reti Neurali per Raccomandazioni

4
Neural Collaborative Filtering e Reti Avanzate
PYTHON
# ================================================
# 🧠 PARTE 4: DEEP LEARNING PER RACCOMANDAZIONI
# ================================================

print("\n" + "=" * 70)
print("⚡ NEURAL COLLABORATIVE FILTERING E RETI AVANZATE")
print("=" * 70)

print("\n🔮 Implementazione state-of-the-art per raccomandazioni con Deep Learning")

# 4.1 PREPARAZIONE DATI PER NEURAL NETWORKS
print("\n🔧 PREPARAZIONE DATI PER RETI NEURALI...")

# Carica dati rating
ratings_sample = ratings_df[['user_id', 'movie_id', 'rating']].copy()

# Normalizza rating tra 0 e 1 (per attivazione sigmoid)
ratings_sample['rating_norm'] = (ratings_sample['rating'] - 0.5) / 4.5

# Encoding user e movie ID per embedding layers
n_users = ratings_sample['user_id'].nunique()
n_movies = ratings_sample['movie_id'].nunique()

print(f"   • Utenti unici: {n_users:,}")
print(f"   • Film unici: {n_movies:,}")
print(f"   • Rating totali: {len(ratings_sample):,}")

# Mapping user e movie ID a indici sequenziali
user_to_index = {user_id: idx for idx, user_id in enumerate(ratings_sample['user_id'].unique())}
movie_to_index = {movie_id: idx for idx, movie_id in enumerate(ratings_sample['movie_id'].unique())}

ratings_sample['user_idx'] = ratings_sample['user_id'].map(user_to_index)
ratings_sample['movie_idx'] = ratings_sample['movie_id'].map(movie_to_index)

# Split train/test
train_data, test_data = train_test_split(
    ratings_sample[['user_idx', 'movie_idx', 'rating_norm']].values,
    test_size=0.2,
    random_state=42
)

print(f"\n🎯 SPLIT DATASET NEURAL NETWORK:")
print(f"   • Training samples: {len(train_data):,}")
print(f"   • Test samples: {len(test_data):,}")

# 4.2 MODEL 1: NEURAL COLLABORATIVE FILTERING (NCF)
print("\n" + "=" * 50)
print("🧠 MODELLO 1: NEURAL COLLABORATIVE FILTERING")
print("=" * 50)

print("\n🏗️ Costruzione architettura NCF...")

def create_ncf_model(n_users, n_movies, embedding_dim=50):
    """
    Neural Collaborative Filtering Model
    Combina user e movie embeddings con MLP
    """
    # Input layers
    user_input = Input(shape=(1,), name='user_input')
    movie_input = Input(shape=(1,), name='movie_input')
    
    # Embedding layers
    user_embedding = Embedding(
        input_dim=n_users,
        output_dim=embedding_dim,
        name='user_embedding'
    )(user_input)
    
    movie_embedding = Embedding(
        input_dim=n_movies,
        output_dim=embedding_dim,
        name='movie_embedding'
    )(movie_input)
    
    # Flatten embeddings
    user_vec = Flatten()(user_embedding)
    movie_vec = Flatten()(movie_embedding)
    
    # Concatenate embeddings
    concatenated = Concatenate()([user_vec, movie_vec])
    
    # Deep layers
    dense1 = Dense(256, activation='relu')(concatenated)
    dropout1 = Dropout(0.3)(dense1)
    
    dense2 = Dense(128, activation='relu')(dropout1)
    dropout2 = Dropout(0.2)(dense2)
    
    dense3 = Dense(64, activation='relu')(dropout2)
    
    # Output layer (rating prediction 0-1)
    output = Dense(1, activation='sigmoid', name='rating_output')(dense3)
    
    # Create model
    model = Model(inputs=[user_input, movie_input], outputs=output)
    
    return model

# Crea e compila modello
ncf_model = create_ncf_model(n_users, n_movies, embedding_dim=64)

ncf_model.compile(
    optimizer=optimizers.Adam(learning_rate=0.001),
    loss=losses.MeanSquaredError(),
    metrics=[metrics.RootMeanSquaredError(), metrics.MeanAbsoluteError()]
)

print("✅ Modello NCF creato!")
ncf_model.summary()

# Callbacks
ncf_callbacks = [
    EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True),
    ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6)
]

# Addestramento
print("\n🚀 Addestramento Neural Collaborative Filtering...")

ncf_history = ncf_model.fit(
    x=[train_data[:, 0], train_data[:, 1]],
    y=train_data[:, 2],
    batch_size=512,
    epochs=30,
    validation_split=0.1,
    callbacks=ncf_callbacks,
    verbose=1
)

# Valutazione
print("\n📊 Valutazione modello NCF...")
ncf_test_loss = ncf_model.evaluate(
    x=[test_data[:, 0], test_data[:, 1]],
    y=test_data[:, 2],
    verbose=0
)

# Converti RMSE back to original scale (0.5-5.0)
ncf_rmse = ncf_test_loss[1] * 4.5  # Denormalize
ncf_mae = ncf_test_loss[2] * 4.5

print(f"✅ Performance NCF:")
print(f"   • RMSE (denormalized): {ncf_rmse:.4f}")
print(f"   • MAE (denormalized): {ncf_mae:.4f}")

# 4.3 MODEL 2: DEEP AUTORECODER PER DIMENSIONALITY REDUCTION
print("\n" + "=" * 50)
print("🌀 MODELLO 2: DEEP AUTOENCODER PER USER EMBEDDINGS")
print("=" * 50)

print("\n🔍 Costruzione autoencoder per apprendere rappresentazioni utente dense...")

# Prepara user-item matrix per autoencoder
user_item_dense = user_item_matrix.values  # Dense matrix

# Normalizza
user_item_scaled = StandardScaler().fit_transform(user_item_dense)

def create_autoencoder(input_dim, encoding_dim=128):
    """Autoencoder per compressione features utente"""
    
    # Encoder
    input_layer = Input(shape=(input_dim,))
    encoded = Dense(512, activation='relu')(input_layer)
    encoded = Dropout(0.3)(encoded)
    encoded = Dense(256, activation='relu')(encoded)
    encoded = Dropout(0.2)(encoded)
    encoded = Dense(encoding_dim, activation='relu')(encoded)
    
    # Decoder
    decoded = Dense(256, activation='relu')(encoded)
    decoded = Dropout(0.2)(decoded)
    decoded = Dense(512, activation='relu')(decoded)
    decoded = Dropout(0.3)(decoded)
    decoded = Dense(input_dim, activation='linear')(decoded)
    
    # Autoencoder model
    autoencoder = Model(input_layer, decoded)
    
    # Encoder model (per estrazione features)
    encoder = Model(input_layer, encoded)
    
    return autoencoder, encoder

# Crea autoencoder
input_dim = user_item_scaled.shape[1]
autoencoder, encoder = create_autoencoder(input_dim, encoding_dim=128)

autoencoder.compile(
    optimizer='adam',
    loss='mse'
)

print(f"✅ Autoencoder creato per {input_dim} features → 128 encoding dim")

# Addestramento su subset (per performance)
train_size = min(5000, user_item_scaled.shape[0])
train_data_ae = user_item_scaled[:train_size]
val_data_ae = user_item_scaled[train_size:min(train_size+1000, user_item_scaled.shape[0])]

print(f"\n🚀 Addestramento autoencoder su {train_size} utenti...")

ae_history = autoencoder.fit(
    train_data_ae, train_data_ae,
    epochs=20,
    batch_size=256,
    validation_data=(val_data_ae, val_data_ae),
    verbose=1
)

# Estrai user embeddings
print("\n🔎 Estrazione user embeddings dall'encoder...")
user_embeddings = encoder.predict(user_item_scaled, verbose=0)

print(f"✅ User embeddings estratti: {user_embeddings.shape}")
print(f"   • Utenti: {user_embeddings.shape[0]}")
print(f"   • Dimensioni embedding: {user_embeddings.shape[1]}")

# 4.4 MODEL 3: TWO-TOWER NEURAL NETWORK
print("\n" + "=" * 50)
print("🏗️ MODELLO 3: TWO-TOWER NEURAL NETWORK")
print("=" * 50)

print("\n🧱 Costruzione two-tower architecture per matching utente-film...")

def create_two_tower_model(n_users, n_movies, user_features_dim, movie_features_dim):
    """
    Two-tower model: una torre per utenti, una per film
    Output: similarity score
    """
    
    # User Tower
    user_id_input = Input(shape=(1,), name='user_id_input')
    user_features_input = Input(shape=(user_features_dim,), name='user_features_input')
    
    user_embedding = Embedding(n_users, 64)(user_id_input)
    user_embedding = Flatten()(user_embedding)
    
    # Combina embedding con features
    user_combined = Concatenate()([user_embedding, user_features_input])
    
    user_dense1 = Dense(128, activation='relu')(user_combined)
    user_dense1 = Dropout(0.3)(user_dense1)
    user_dense2 = Dense(64, activation='relu')(user_dense1)
    user_tower_output = Dense(32, activation='relu', name='user_tower_output')(user_dense2)
    
    # Movie Tower
    movie_id_input = Input(shape=(1,), name='movie_id_input')
    movie_features_input = Input(shape=(movie_features_dim,), name='movie_features_input')
    
    movie_embedding = Embedding(n_movies, 64)(movie_id_input)
    movie_embedding = Flatten()(movie_embedding)
    
    # Combina embedding con features
    movie_combined = Concatenate()([movie_embedding, movie_features_input])
    
    movie_dense1 = Dense(128, activation='relu')(movie_combined)
    movie_dense1 = Dropout(0.3)(movie_dense1)
    movie_dense2 = Dense(64, activation='relu')(movie_dense1)
    movie_tower_output = Dense(32, activation='relu', name='movie_tower_output')(movie_dense2)
    
    # Dot product similarity
    dot_product = Dot(axes=1, normalize=True)([user_tower_output, movie_tower_output])
    
    # Output (similarity score 0-1)
    output = Dense(1, activation='sigmoid', name='similarity_output')(dot_product)
    
    # Create model
    model = Model(
        inputs=[user_id_input, user_features_input, movie_id_input, movie_features_input],
        outputs=output
    )
    
    return model

# Prepara features per two-tower
print("\n📊 Preparazione features per two-tower model...")

# Seleziona features numeriche rilevanti
user_numeric_features = user_features.select_dtypes(include=[np.number]).drop(
    columns=['user_id', 'avg_rating'], errors='ignore'
).columns.tolist()

movie_numeric_features = movie_features.select_dtypes(include=[np.number]).drop(
    columns=['movie_id', 'avg_rating', 'imdb_rating'], errors='ignore'
).columns.tolist()

print(f"   • User features: {len(user_numeric_features)}")
print(f"   • Movie features: {len(movie_numeric_features)}")

# Crea dataset per two-tower
two_tower_data = ratings_sample[['user_idx', 'movie_idx', 'rating_norm']].copy()

# Aggiungi features
two_tower_data = pd.merge(
    two_tower_data,
    user_features[['user_id'] + user_numeric_features],
    left_on='user_id',
    right_on='user_id',
    how='left'
)

two_tower_data = pd.merge(
    two_tower_data,
    movie_features[['movie_id'] + movie_numeric_features],
    left_on='movie_id',
    right_on='movie_id',
    how='left'
)

# Fill NaN
two_tower_data = two_tower_data.fillna(0)

# Split features
X_user_id = two_tower_data['user_idx'].values
X_user_features = two_tower_data[user_numeric_features].values
X_movie_id = two_tower_data['movie_idx'].values
X_movie_features = two_tower_data[movie_numeric_features].values
y = two_tower_data['rating_norm'].values

# Split train/test
X_train_user_id, X_test_user_id, X_train_user_feat, X_test_user_feat, \
X_train_movie_id, X_test_movie_id, X_train_movie_feat, X_test_movie_feat, \
y_train_tt, y_test_tt = train_test_split(
    X_user_id, X_user_features, X_movie_id, X_movie_features, y,
    test_size=0.2, random_state=42
)

# Crea e addestra two-tower model
two_tower_model = create_two_tower_model(
    n_users=n_users,
    n_movies=n_movies,
    user_features_dim=len(user_numeric_features),
    movie_features_dim=len(movie_numeric_features)
)

two_tower_model.compile(
    optimizer='adam',
    loss='mse',
    metrics=['mae']
)

print(f"\n🚀 Addestramento two-tower model...")

tt_history = two_tower_model.fit(
    x=[X_train_user_id, X_train_user_feat, X_train_movie_id, X_train_movie_feat],
    y=y_train_tt,
    batch_size=512,
    epochs=20,
    validation_split=0.1,
    verbose=1
)

# Valutazione
tt_test_loss = two_tower_model.evaluate(
    x=[X_test_user_id, X_test_user_feat, X_test_movie_id, X_test_movie_feat],
    y=y_test_tt,
    verbose=0
)

tt_rmse = np.sqrt(tt_test_loss[0]) * 4.5
tt_mae = tt_test_loss[1] * 4.5

print(f"\n✅ Performance Two-Tower Model:")
print(f"   • RMSE (denormalized): {tt_rmse:.4f}")
print(f"   • MAE (denormalized): {tt_mae:.4f}")

# 4.5 MODEL 4: ATTENTION-BASED RECOMMENDER
print("\n" + "=" * 50)
print("🎯 MODELLO 4: ATTENTION-BASED RECOMMENDER")
print("=" * 50)

print("\n🔍 Implementazione meccanismo di attention per ponderare features importanti...")

class AttentionLayer(layers.Layer):
    """Custom attention layer per ponderare features"""
    def __init__(self, **kwargs):
        super(AttentionLayer, self).__init__(**kwargs)
    
    def build(self, input_shape):
        self.W = self.add_weight(name="attention_weight",
                                shape=(input_shape[-1], 1),
                                initializer="random_normal",
                                trainable=True)
        self.b = self.add_weight(name="attention_bias",
                                shape=(1,),
                                initializer="zeros",
                                trainable=True)
        super(AttentionLayer, self).build(input_shape)
    
    def call(self, x):
        # Calcola attention scores
        e = tf.keras.activations.tanh(tf.tensordot(x, self.W, axes=1) + self.b)
        # Softmax per pesi
        alpha = tf.keras.activations.softmax(e, axis=1)
        # Applica attention
        context = x * alpha
        return context

def create_attention_model(n_users, n_movies, feature_dim):
    """Modello con attention mechanism"""
    
    # Inputs
    user_input = Input(shape=(1,))
    movie_input = Input(shape=(1,))
    features_input = Input(shape=(feature_dim,))
    
    # Embeddings
    user_embedding = Embedding(n_users, 32)(user_input)
    movie_embedding = Embedding(n_movies, 32)(movie_input)
    
    user_vec = Flatten()(user_embedding)
    movie_vec = Flatten()(movie_embedding)
    
    # Combina tutti gli input
    combined = Concatenate()([user_vec, movie_vec, features_input])
    
    # Attention layer
    attention = AttentionLayer()(combined)
    
    # Deep layers
    dense1 = Dense(128, activation='relu')(attention)
    dropout1 = Dropout(0.3)(dense1)
    
    dense2 = Dense(64, activation='relu')(dropout1)
    dropout2 = Dropout(0.2)(dense2)
    
    dense3 = Dense(32, activation='relu')(dropout2)
    
    # Output
    output = Dense(1, activation='sigmoid')(dense3)
    
    return Model(inputs=[user_input, movie_input, features_input], outputs=output)

# Prepara features combinate per attention model
print("\n📊 Preparazione features per attention model...")

# Combina user e movie features
attention_features = pd.concat([
    two_tower_data[user_numeric_features].reset_index(drop=True),
    two_tower_data[movie_numeric_features].reset_index(drop=True)
], axis=1)

feature_dim = attention_features.shape[1]

# Crea e addestra attention model
attention_model = create_attention_model(n_users, n_movies, feature_dim)

attention_model.compile(
    optimizer='adam',
    loss='mse',
    metrics=['mae']
)

# Split per attention model
X_train_att, X_test_att, y_train_att, y_test_att = train_test_split(
    attention_features.values, y, test_size=0.2, random_state=42
)

# Aggiungi user e movie indices
X_train_user_idx = two_tower_data.iloc[y_train_att.index]['user_idx'].values
X_test_user_idx = two_tower_data.iloc[y_test_att.index]['user_idx'].values
X_train_movie_idx = two_tower_data.iloc[y_train_att.index]['movie_idx'].values
X_test_movie_idx = two_tower_data.iloc[y_test_att.index]['movie_idx'].values

print(f"\n🚀 Addestramento attention model...")

att_history = attention_model.fit(
    x=[X_train_user_idx, X_train_movie_idx, X_train_att],
    y=y_train_att,
    batch_size=512,
    epochs=15,
    validation_split=0.1,
    verbose=1
)

# Valutazione
att_test_loss = attention_model.evaluate(
    x=[X_test_user_idx, X_test_movie_idx, X_test_att],
    y=y_test_att,
    verbose=0
)

att_rmse = np.sqrt(att_test_loss[0]) * 4.5
att_mae = att_test_loss[1] * 4.5

print(f"\n✅ Performance Attention Model:")
print(f"   • RMSE (denormalized): {att_rmse:.4f}")
print(f"   • MAE (denormalized): {att_mae:.4f}")

# 4.6 ENSEMBLE DI TUTTI I MODELLI NEURALI
print("\n" + "=" * 50)
print("🤝 ENSEMBLE NEURAL NETWORKS")
print("=" * 50)

print("\n🔗 Combinazione predizioni di tutti i modelli neurali...")

def neural_ensemble_predict(user_id, movie_id):
    """
    Ensemble di predizioni da tutti i modelli neurali
    """
    predictions = []
    
    # 1. NCF prediction
    user_idx = user_to_index.get(user_id)
    movie_idx = movie_to_index.get(movie_id)
    
    if user_idx is not None and movie_idx is not None:
        # NCF
        ncf_pred = ncf_model.predict(
            [np.array([user_idx]), np.array([movie_idx])],
            verbose=0
        )[0][0] * 4.5 + 0.5  # Denormalize
        
        predictions.append(('NCF', ncf_pred))
        
        # Two-Tower (se features disponibili)
        try:
            user_feat = user_features[user_features['user_id'] == user_id][user_numeric_features].values[0]
            movie_feat = movie_features[movie_features['movie_id'] == movie_id][movie_numeric_features].values[0]
            
            tt_pred = two_tower_model.predict(
                [
                    np.array([user_idx]),
                    user_feat.reshape(1, -1),
                    np.array([movie_idx]),
                    movie_feat.reshape(1, -1)
                ],
                verbose=0
            )[0][0] * 4.5 + 0.5
            
            predictions.append(('Two-Tower', tt_pred))
        except:
            pass
        
        # Attention (se features disponibili)
        try:
            combined_feat = np.concatenate([user_feat, movie_feat]).reshape(1, -1)
            
            att_pred = attention_model.predict(
                [
                    np.array([user_idx]),
                    np.array([movie_idx]),
                    combined_feat
                ],
                verbose=0
            )[0][0] * 4.5 + 0.5
            
            predictions.append(('Attention', att_pred))
        except:
            pass
    
    # Se abbiamo predizioni, calcola media pesata
    if predictions:
        # Pesiamo più i modelli migliori
        weights = {'NCF': 0.4, 'Two-Tower': 0.35, 'Attention': 0.25}
        weighted_sum = 0
        total_weight = 0
        
        for model_name, pred in predictions:
            weight = weights.get(model_name, 0.3)
            weighted_sum += pred * weight
            total_weight += weight
        
        ensemble_pred = weighted_sum / total_weight
        return ensemble_pred, predictions
    else:
        return 2.5, []  # Default rating

# Test ensemble
print("\n🎯 TEST ENSEMBLE NEURAL NETWORKS:")

test_cases = [
    (42, 1),   # User 42, The Matrix
    (42, 100), # User 42, altro film
    (100, 1),  # User 100, The Matrix
]

for user_id, movie_id in test_cases:
    movie_title = movies_df[movies_df['movie_id'] == movie_id]['title'].values
    title = movie_title[0] if len(movie_title) > 0 else f"Movie {movie_id}"
    
    ensemble_pred, individual_preds = neural_ensemble_predict(user_id, movie_id)
    
    print(f"\n👤 Utente {user_id}, 🎬 '{title}' (ID: {movie_id}):")
    print(f"   • Ensemble prediction: {ensemble_pred:.2f}/5.0")
    
    for model_name, pred in individual_preds:
        print(f"   • {model_name}: {pred:.2f}/5.0")

# 4.7 VISUALIZZAZIONE EMBEDDINGS E PERFORMANCE
print("\n" + "=" * 50)
print("📊 VISUALIZZAZIONE RISULTATI NEURAL NETWORKS")
print("=" * 50)

fig, axes = plt.subplots(2, 2, figsize=(15, 12))

# Training history NCF
axes[0, 0].plot(ncf_history.history['loss'], label='Training Loss', color='blue')
axes[0, 0].plot(ncf_history.history['val_loss'], label='Validation Loss', color='orange')
axes[0, 0].set_xlabel('Epoch')
axes[0, 0].set_ylabel('Loss')
axes[0, 0].set_title('Neural Collaborative Filtering - Training History')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Confronto performance modelli neurali
neural_models = ['NCF', 'Two-Tower', 'Attention']
neural_rmse = [ncf_rmse, tt_rmse, att_rmse]
neural_mae = [ncf_mae, tt_mae, att_mae]

x = np.arange(len(neural_models))
width = 0.35

axes[0, 1].bar(x - width/2, neural_rmse, width, label='RMSE', color='lightcoral', alpha=0.8)
axes[0, 1].bar(x + width/2, neural_mae, width, label='MAE', color='skyblue', alpha=0.8)
axes[0, 1].set_xlabel('Modello')
axes[0, 1].set_ylabel('Error Value')
axes[0, 1].set_title('Confronto Performance Modelli Neurali')
axes[0, 1].set_xticks(x)
axes[0, 1].set_xticklabels(neural_models)
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Autoencoder reconstruction error
axes[1, 0].plot(ae_history.history['loss'], label='Training Loss', color='green')
axes[1, 0].plot(ae_history.history['val_loss'], label='Validation Loss', color='red')
axes[1, 0].set_xlabel('Epoch')
axes[1, 0].set_ylabel('Reconstruction Error')
axes[1, 0].set_title('Autoencoder - Training History')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# User embeddings visualization (PCA)
from sklearn.decomposition import PCA

# PCA su user embeddings
pca = PCA(n_components=2)
user_embeddings_2d = pca.fit_transform(user_embeddings[:1000])  # Primi 1000 utenti

scatter = axes[1, 1].scatter(user_embeddings_2d[:, 0], user_embeddings_2d[:, 1],
                           alpha=0.6, s=10, c=user_item_dense[:1000].mean(axis=1),
                           cmap='viridis')
axes[1, 1].set_xlabel('PCA Component 1')
axes[1, 1].set_ylabel('PCA Component 2')
axes[1, 1].set_title('User Embeddings Visualization (PCA)')
plt.colorbar(scatter, ax=axes[1, 1], label='Average Rating')
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 4.8 SALVATAGGIO MODELLI NEURALI
print("\n💾 SALVATAGGIO MODELLI NEURALI...")

# Crea directory se non esiste
import os
os.makedirs('models/neural', exist_ok=True)

# Salva NCF
ncf_model.save('models/neural/ncf_model.h5')
print("✅ NCF model salvato")

# Salva Autoencoder
encoder.save('models/neural/user_encoder.h5')
print("✅ User encoder salvato")

# Salva Two-Tower
two_tower_model.save('models/neural/two_tower_model.h5')
print("✅ Two-tower model salvato")

# Salva Attention model
attention_model.save('models/neural/attention_model.h5')
print("✅ Attention model salvato")

# Salva mapping dictionaries
mappings = {
    'user_to_index': user_to_index,
    'movie_to_index': movie_to_index,
    'user_numeric_features': user_numeric_features,
    'movie_numeric_features': movie_numeric_features
}

joblib.dump(mappings, 'models/neural/mappings.pkl')
print("✅ Mappings salvati")

print("\n" + "=" * 70)
print("✅ PARTE 4 COMPLETATA: RETI NEURALI IMPLEMENTATE")
print("=" * 70)

Parte 5: API REST e Sistema di Produzione

5
API REST e Deployment per Sistema di Raccomandazione
PYTHON
# ================================================
# 🌐 PARTE 5: API REST E SISTEMA DI PRODUZIONE
# ================================================

print("\n" + "=" * 70)
print("🚀 IMPLEMENTAZIONE API REST E DEPLOYMENT")
print("=" * 70)

print("\n🔧 Creazione API REST professionale per servire raccomandazioni in tempo reale")

# 5.1 SETUP API CON FASTAPI
print("\n🏗️ Configurazione FastAPI...")

# Import per API
from fastapi import FastAPI, HTTPException, Query, Depends
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import uvicorn
from datetime import datetime

# Modelli Pydantic per request/response
class RecommendationRequest(BaseModel):
    """Schema per richiesta raccomandazioni"""
    user_id: int = Field(..., description="ID dell'utente")
    movie_id: Optional[int] = Field(None, description="ID film di riferimento (opzionale)")
    n_recommendations: int = Field(10, ge=1, le=50, description="Numero di raccomandazioni")
    algorithm: str = Field("hybrid", description="Algoritmo da usare: hybrid, neural, collaborative, content")

class RecommendationItem(BaseModel):
    """Schema per singola raccomandazione"""
    movie_id: int
    title: str
    predicted_rating: float
    confidence: float
    genres: List[str]
    reason: str

class RecommendationResponse(BaseModel):
    """Schema per risposta API"""
    user_id: int
    recommendations: List[RecommendationItem]
    algorithm_used: str
    processing_time_ms: float
    timestamp: datetime

class HealthResponse(BaseModel):
    """Schema per health check"""
    status: str
    models_loaded: List[str]
    database_connected: bool
    timestamp: datetime

# 5.2 SISTEMA DI RACCOMANDAZIONE PROFESSIONALE
print("\n🔌 Implementazione Recommendation System Engine...")

class MovieFlixRecommender:
    """Sistema di raccomandazione professionale che unisce tutti i modelli"""
    
    def __init__(self, conn):
        self.conn = conn
        self.models_loaded = False
        self.load_all_models()
    
    def load_all_models(self):
        """Carica tutti i modelli pre-addestrati"""
        print("📦 Caricamento modelli pre-addestrati...")
        
        try:
            # Carica modelli ML classici
            self.rf_model = joblib.load('models/random_forest_model.pkl')
            self.svd_components = joblib.load('models/svd_components.pkl')
            self.knn_model = joblib.load('models/knn_model.pkl')
            self.tfidf = joblib.load('models/tfidf_vectorizer.pkl')
            self.content_similarity = joblib.load('models/content_similarity.pkl')
            self.preprocessing = joblib.load('models/preprocessing_objects.pkl')
            
            # Carica modelli neurali
            self.ncf_model = tf.keras.models.load_model('models/neural/ncf_model.h5')
            self.two_tower_model = tf.keras.models.load_model('models/neural/two_tower_model.h5')
            self.attention_model = tf.keras.models.load_model('models/neural/attention_model.h5')
            self.user_encoder = tf.keras.models.load_model('models/neural/user_encoder.h5')
            self.mappings = joblib.load('models/neural/mappings.pkl')
            
            self.models_loaded = True
            print("✅ Tutti i modelli caricati con successo!")
            
        except Exception as e:
            print(f"❌ Errore caricamento modelli: {e}")
            self.models_loaded = False
    
    def get_user_history(self, user_id: int):
        """Recupera storico visioni utente"""
        query = """
        SELECT m.movie_id, m.title, r.rating, m.genre
        FROM ratings r
        JOIN movies m ON r.movie_id = m.movie_id
        WHERE r.user_id = ?
        ORDER BY r.timestamp DESC
        LIMIT 50
        """
        df = pd.read_sql_query(query, self.conn, params=(user_id,))
        return df
    
    def get_movie_details(self, movie_ids: List[int]):
        """Recupera dettagli per multiple film"""
        if not movie_ids:
            return pd.DataFrame()
        
        placeholders = ','.join(['?'] * len(movie_ids))
        query = f"""
        SELECT movie_id, title, genre, imdb_rating, release_year
        FROM movies
        WHERE movie_id IN ({placeholders})
        """
        df = pd.read_sql_query(query, self.conn, params=tuple(movie_ids))
        return df
    
    def collaborative_filtering_recommendations(self, user_id: int, n: int = 10):
        """Raccomandazioni basate su collaborative filtering"""
        try:
            # Trova utenti simili
            user_idx = self.mappings['user_to_index'].get(user_id)
            if user_idx is None:
                return []
            
            # Usa user embeddings per trovare utenti simili
            user_embedding = self.user_encoder.predict(
                np.array([user_idx]).reshape(1, -1), verbose=0
            )
            
            # Calcola similarità con tutti gli utenti
            all_user_embeddings = self.user_encoder.predict(
                np.arange(len(self.mappings['user_to_index'])).reshape(-1, 1),
                verbose=0
            )
            
            similarities = cosine_similarity(user_embedding, all_user_embeddings)[0]
            
            # Trova utenti più simili
            similar_users_idx = np.argsort(similarities)[-20:-1]  # Top 20 (escludendo se stesso)
            
            # Trova film che utenti simili hanno apprezzato
            movie_scores = {}
            for sim_user_idx in similar_users_idx:
                sim_user_id = list(self.mappings['user_to_index'].keys())[
                    list(self.mappings['user_to_index'].values()).index(sim_user_idx)
                ]
                
                # Rating dell'utente simile
                user_ratings_query = """
                SELECT movie_id, rating
                FROM ratings
                WHERE user_id = ?
                AND rating >= 4.0
                """
                sim_ratings = pd.read_sql_query(
                    user_ratings_query, self.conn, params=(sim_user_id,)
                )
                
                for _, row in sim_ratings.iterrows():
                    movie_id = row['movie_id']
                    rating = row['rating']
                    similarity = similarities[sim_user_idx]
                    
                    if movie_id not in movie_scores:
                        movie_scores[movie_id] = []
                    movie_scores[movie_id].append(rating * similarity)
            
            # Calcola score medio per ogni film
            avg_scores = {
                movie_id: np.mean(scores)
                for movie_id, scores in movie_scores.items()
                if len(scores) >= 2  # Almeno 2 utenti simili l'hanno votato
            }
            
            # Ordina e restituisci top N
            sorted_movies = sorted(avg_scores.items(), key=lambda x: x[1], reverse=True)
            return [movie_id for movie_id, _ in sorted_movies[:n]]
            
        except Exception as e:
            print(f"Errore collaborative filtering: {e}")
            return []
    
    def content_based_recommendations(self, movie_id: int, n: int = 10):
        """Raccomandazioni basate su contenuto"""
        try:
            movie_idx = movie_id - 1
            if movie_idx >= self.content_similarity.shape[0]:
                return []
            
            # Trova film simili
            sim_scores = list(enumerate(self.content_similarity[movie_idx]))
            sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)[1:n+1]
            
            return [score[0] + 1 for score in sim_scores]
            
        except:
            return []
    
    def neural_recommendations(self, user_id: int, n: int = 10):
        """Raccomandazioni usando modelli neurali"""
        try:
            user_idx = self.mappings['user_to_index'].get(user_id)
            if user_idx is None:
                return []
            
            # Predici rating per tutti i film usando NCF
            all_movie_indices = np.arange(len(self.mappings['movie_to_index']))
            user_indices = np.full_like(all_movie_indices, user_idx)
            
            predictions = self.ncf_model.predict(
                [user_indices, all_movie_indices],
                batch_size=1024,
                verbose=0
            ).flatten()
            
            # Denormalizza predictions
            predictions = predictions * 4.5 + 0.5
            
            # Ordina per rating predetto
            top_indices = np.argsort(predictions)[::-1][:n*2]  # Prendi più del necessario
            
            # Converti indici a movie_id
            movie_id_to_idx = {v: k for k, v in self.mappings['movie_to_index'].items()}
            top_movies = [movie_id_to_idx[idx] for idx in top_indices if idx in movie_id_to_idx]
            
            # Filtra film già visti
            user_history = self.get_user_history(user_id)
            watched_movies = set(user_history['movie_id'].values)
            top_movies = [m for m in top_movies if m not in watched_movies]
            
            return top_movies[:n]
            
        except Exception as e:
            print(f"Errore neural recommendations: {e}")
            return []
    
    def hybrid_recommendations(self, user_id: int, reference_movie_id: Optional[int] = None, n: int = 10):
        """Raccomandazioni ibride che combinano multiple algoritmi"""
        all_recommendations = []
        
        # 1. Raccomandazioni collaborative
        collab_recs = self.collaborative_filtering_recommendations(user_id, n)
        all_recommendations.extend([(movie_id, 'collaborative', 0.4) for movie_id in collab_recs])
        
        # 2. Raccomandazioni neurali
        neural_recs = self.neural_recommendations(user_id, n)
        all_recommendations.extend([(movie_id, 'neural', 0.4) for movie_id in neural_recs])
        
        # 3. Se c'è un film di riferimento, aggiungi content-based
        if reference_movie_id:
            content_recs = self.content_based_recommendations(reference_movie_id, n)
            all_recommendations.extend([(movie_id, 'content', 0.2) for movie_id in content_recs])
        
        # Calcola score aggregato per ogni film
        movie_scores = {}
        for movie_id, algo, weight in all_recommendations:
            if movie_id not in movie_scores:
                movie_scores[movie_id] = 0
            movie_scores[movie_id] += weight
        
        # Ordina per score e restituisci top N
        sorted_movies = sorted(movie_scores.items(), key=lambda x: x[1], reverse=True)
        return [movie_id for movie_id, _ in sorted_movies[:n]]
    
    def generate_recommendations(self, user_id: int, reference_movie_id: Optional[int] = None,
                                n: int = 10, algorithm: str = "hybrid") -> List[Dict]:
        """Genera raccomandazioni complete con dettagli"""
        
        start_time = time.time()
        
        # Seleziona algoritmo
        if algorithm == "collaborative":
            movie_ids = self.collaborative_filtering_recommendations(user_id, n)
        elif algorithm == "neural":
            movie_ids = self.neural_recommendations(user_id, n)
        elif algorithm == "content" and reference_movie_id:
            movie_ids = self.content_based_recommendations(reference_movie_id, n)
        else:  # hybrid default
            movie_ids = self.hybrid_recommendations(user_id, reference_movie_id, n)
        
        # Recupera dettagli film
        if not movie_ids:
            # Fallback: film popolari
            popular_query = """
            SELECT movie_id FROM movies 
            ORDER BY imdb_rating DESC 
            LIMIT ?
            """
            movie_ids = pd.read_sql_query(popular_query, self.conn, params=(n,))['movie_id'].tolist()
        
        movie_details = self.get_movie_details(movie_ids)
        
        # Crea risposta strutturata
        recommendations = []
        for _, movie in movie_details.iterrows():
            # Calcola confidence score basato su algoritmo
            confidence = 0.7  # Default
            if algorithm == "hybrid":
                confidence = 0.85
            elif algorithm == "neural":
                confidence = 0.8
            
            recommendation = {
                'movie_id': int(movie['movie_id']),
                'title': movie['title'],
                'predicted_rating': round(movie['imdb_rating'], 1) if pd.notna(movie['imdb_rating']) else 3.5,
                'confidence': round(confidence, 2),
                'genres': movie['genre'].split('|') if pd.notna(movie['genre']) else [],
                'reason': f"Basato sul tuo profilo e {algorithm} filtering"
            }
            recommendations.append(recommendation)
        
        processing_time = (time.time() - start_time) * 1000  # Converti a ms
        
        return recommendations, processing_time

# 5.3 IMPLEMENTAZIONE API
print("\n🌐 Creazione API FastAPI...")

# Inizializza FastAPI app
app = FastAPI(
    title="MovieFlix Recommendation API",
    description="API professionale per sistema di raccomandazione film",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc"
)

# Inizializza recommendation engine
recommender = None

@app.on_event("startup")
async def startup_event():
    """Inizializza recommendation engine all'avvio"""
    global recommender
    print("🚀 Inizializzazione Recommendation Engine...")
    recommender = MovieFlixRecommender(conn)
    print("✅ Recommendation Engine pronto!")

@app.get("/", tags=["Root"])
async def root():
    """Endpoint root"""
    return {
        "message": "MovieFlix Recommendation API",
        "version": "1.0.0",
        "endpoints": {
            "/docs": "API Documentation",
            "/health": "Health Check",
            "/recommend": "Get Recommendations",
            "/user/{user_id}/history": "Get User History"
        }
    }

@app.get("/health", response_model=HealthResponse, tags=["Monitoring"])
async def health_check():
    """Health check endpoint"""
    models_loaded = [
        "Random Forest", "SVD", "KNN", "NCF", 
        "Two-Tower", "Attention", "Autoencoder"
    ] if recommender and recommender.models_loaded else []
    
    return HealthResponse(
        status="healthy" if recommender and recommender.models_loaded else "degraded",
        models_loaded=models_loaded,
        database_connected=conn is not None,
        timestamp=datetime.now()
    )

@app.get("/user/{user_id}/history", tags=["User"])
async def get_user_history(user_id: int):
    """Recupera storico visioni utente"""
    if not recommender:
        raise HTTPException(status_code=503, detail="Service unavailable")
    
    history = recommender.get_user_history(user_id)
    if history.empty:
        raise HTTPException(status_code=404, detail="User not found")
    
    return {
        "user_id": user_id,
        "total_watched": len(history),
        "average_rating": round(history['rating'].mean(), 2),
        "history": history.to_dict(orient='records')
    }

@app.post("/recommend", response_model=RecommendationResponse, tags=["Recommendations"])
async def get_recommendations(request: RecommendationRequest):
    """Endpoint principale per raccomandazioni"""
    if not recommender:
        raise HTTPException(status_code=503, detail="Service unavailable")
    
    start_time = time.time()
    
    # Genera raccomandazioni
    recommendations, processing_time = recommender.generate_recommendations(
        user_id=request.user_id,
        reference_movie_id=request.movie_id,
        n=request.n_recommendations,
        algorithm=request.algorithm
    )
    
    # Converti in formato Pydantic
    recommendation_items = []
    for rec in recommendations:
        item = RecommendationItem(
            movie_id=rec['movie_id'],
            title=rec['title'],
            predicted_rating=rec['predicted_rating'],
            confidence=rec['confidence'],
            genres=rec['genres'],
            reason=rec['reason']
        )
        recommendation_items.append(item)
    
    total_time = (time.time() - start_time) * 1000
    
    return RecommendationResponse(
        user_id=request.user_id,
        recommendations=recommendation_items,
        algorithm_used=request.algorithm,
        processing_time_ms=round(total_time, 2),
        timestamp=datetime.now()
    )

@app.get("/movie/{movie_id}/similar", tags=["Movies"])
async def get_similar_movies(
    movie_id: int,
    n: int = Query(10, ge=1, le=50)
):
    """Trova film simili a un film specifico"""
    if not recommender:
        raise HTTPException(status_code=503, detail="Service unavailable")
    
    similar_movie_ids = recommender.content_based_recommendations(movie_id, n)
    similar_movies = recommender.get_movie_details(similar_movie_ids)
    
    if similar_movies.empty:
        raise HTTPException(status_code=404, detail="Movie not found")
    
    return {
        "movie_id": movie_id,
        "similar_movies": similar_movies.to_dict(orient='records')
    }

# 5.4 DOCKERFILE E DEPLOYMENT
print("\n🐳 Creazione Dockerfile per deployment...")

dockerfile_content = """
# Dockerfile per MovieFlix Recommendation API
FROM python:3.9-slim

WORKDIR /app

# Installa dipendenze sistema
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    && rm -rf /var/lib/apt/lists/*

# Copia requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copia codice applicazione
COPY . .

# Copia modelli pre-addestrati
COPY models/ ./models/

# Crea directory per logs
RUN mkdir -p /app/logs

# Esponi porta
EXPOSE 8000

# Comando di avvio
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
"""

print("📝 Dockerfile creato:")
print(dockerfile_content)

# 5.5 REQUIREMENTS.TXT
print("\n📋 Creazione requirements.txt...")

requirements_content = """
# Core
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0

# Data Science
numpy==1.24.3
pandas==2.1.3
scikit-learn==1.3.2
scipy==1.11.4

# Deep Learning
tensorflow==2.15.0
keras==2.15.0

# NLP
nltk==3.8.1

# Database
sqlalchemy==2.0.23

# Utilità
python-multipart==0.0.6
joblib==1.3.2
matplotlib==3.8.2
seaborn==0.13.0
plotly==5.18.0
wordcloud==1.9.3
"""

print(requirements_content)

# 5.6 TEST API LOCALE
print("\n🧪 Test API locale...")

# Funzione per testare API localmente
def test_api_locally():
    """Test delle funzionalità API"""
    print("🔧 Test delle funzionalità recommendation engine...")
    
    # Crea engine
    test_engine = MovieFlixRecommender(conn)
    
    if not test_engine.models_loaded:
        print("❌ Modelli non caricati, skipping test")
        return
    
    # Test 1: User history
    print("\n📚 Test 1: User History")
    user_history = test_engine.get_user_history(42)
    print(f"   • Utente 42 ha visto {len(user_history)} film")
    if len(user_history) > 0:
        print(f"   • Ultimo film: {user_history.iloc[0]['title']} ({user_history.iloc[0]['rating']}/5)")
    
    # Test 2: Collaborative Filtering
    print("\n👥 Test 2: Collaborative Filtering")
    collab_recs = test_engine.collaborative_filtering_recommendations(42, 5)
    print(f"   • Raccomandazioni collaborative: {len(collab_recs)} film")
    if collab_recs:
        movie_details = test_engine.get_movie_details(collab_recs[:3])
        for _, movie in movie_details.iterrows():
            print(f"     - {movie['title']}")
    
    # Test 3: Neural Recommendations
    print("\n🧠 Test 3: Neural Recommendations")
    neural_recs = test_engine.neural_recommendations(42, 5)
    print(f"   • Raccomandazioni neurali: {len(neural_recs)} film")
    
    # Test 4: Hybrid Recommendations
    print("\n🤝 Test 4: Hybrid Recommendations")
    recs, proc_time = test_engine.generate_recommendations(
        user_id=42,
        n=5,
        algorithm="hybrid"
    )
    print(f"   • Raccomandazioni ibride ({len(recs)} film, {proc_time:.0f}ms):")
    for rec in recs[:3]:
        print(f"     - {rec['title']} (confidence: {rec['confidence']})")
    
    print("\n✅ Test completati con successo!")

# Esegui test
test_api_locally()

# 5.7 DEPLOYMENT SCRIPT
print("\n🚀 Creazione script di deployment...")

deployment_script = """
#!/bin/bash
# Deployment script per MovieFlix Recommendation System

set -e

echo "🚀 Starting MovieFlix Recommendation System Deployment"

# 1. Build Docker image
echo "📦 Building Docker image..."
docker build -t movieflix-recommender:latest .

# 2. Run container
echo "🐳 Starting container..."
docker run -d \
  --name movieflix-recommender \
  -p 8000:8000 \
  -v ./logs:/app/logs \
  -e "ENVIRONMENT=production" \
  movieflix-recommender:latest

# 3. Health check
echo "🏥 Performing health check..."
sleep 10  # Wait for service to start
curl -f http://localhost:8000/health || {
  echo "❌ Health check failed!"
  docker logs movieflix-recommender
  exit 1
}

echo "✅ Deployment completed successfully!"
echo "📊 API available at: http://localhost:8000"
echo "📚 Documentation at: http://localhost:8000/docs"
"""

print(deployment_script)

# 5.8 MONITORING E LOGGING
print("\n📊 Configurazione monitoring e logging...")

monitoring_config = """
# monitoring_config.py
import logging
from datetime import datetime
import json

def setup_logging():
    """Configura logging professionale"""
    
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler(f'logs/movieflix_{datetime.now().strftime("%Y%m")}.log'),
            logging.StreamHandler()
        ]
    )
    
    return logging.getLogger(__name__)

class RecommendationMonitor:
    """Monitor per tracciare performance raccomandazioni"""
    
    def __init__(self):
        self.metrics = {
            'requests_total': 0,
            'requests_by_algorithm': {},
            'avg_processing_time': 0,
            'errors_total': 0
        }
        self.logger = setup_logging()
    
    def log_request(self, user_id: int, algorithm: str, processing_time: float):
        """Logga una richiesta di raccomandazione"""
        self.metrics['requests_total'] += 1
        
        if algorithm not in self.metrics['requests_by_algorithm']:
            self.metrics['requests_by_algorithm'][algorithm] = 0
        self.metrics['requests_by_algorithm'][algorithm] += 1
        
        # Aggiorna tempo medio di processing
        total_time = self.metrics['avg_processing_time'] * (self.metrics['requests_total'] - 1)
        self.metrics['avg_processing_time'] = (total_time + processing_time) / self.metrics['requests_total']
        
        self.logger.info(
            f"Recommendation request - User: {user_id}, "
            f"Algorithm: {algorithm}, "
            f"Time: {processing_time:.2f}ms"
        )
    
    def log_error(self, error_type: str, details: str):
        """Logga un errore"""
        self.metrics['errors_total'] += 1
        self.logger.error(f"{error_type}: {details}")
    
    def get_metrics(self):
        """Restituisce metriche corrente"""
        return {
            **self.metrics,
            'timestamp': datetime.now().isoformat()
        }
    
    def save_metrics(self):
        """Salva metriche su file"""
        filename = f"logs/metrics_{datetime.now().strftime('%Y%m%d')}.json"
        with open(filename, 'w') as f:
            json.dump(self.get_metrics(), f, indent=2)

# Utilizzo nel FastAPI app
monitor = RecommendationMonitor()

# In ogni endpoint, chiama monitor.log_request()
"""

print(monitoring_config)

# 5.9 CONCLUSIONE E PROSSIMI PASSI
print("\n" + "=" * 70)
print("🎉 PROGETTO COMPLETATO: SISTEMA DI RACCOMANDAZIONE AVANZATO")
print("=" * 70)

print("\n🏆 ARCHITETTURA IMPLEMENTATA CON SUCCESSO:")
print("""
    ┌─────────────────────────────────────────────────────────┐
    │                    CLIENT APPLICATIONS                   │
    │                 (Web, Mobile, TV Apps)                   │
    └───────────────────────────┬─────────────────────────────┘
                                │
    ┌───────────────────────────▼─────────────────────────────┐
    │              FASTAPI REST API (Port: 8000)               │
    │  • /recommend - Get personalized recommendations        │
    │  • /user/{id}/history - Get user viewing history       │
    │  • /health - System health check                       │
    └───────────────────────────┬─────────────────────────────┘
                                │
    ┌───────────────────────────▼─────────────────────────────┐
    │            RECOMMENDATION ENGINE (Python)                │
    │  • Neural Collaborative Filtering (NCF)                 │
    │  • Matrix Factorization (SVD)                           │
    │  • Content-Based Filtering (TF-IDF)                     │
    │  • K-Nearest Neighbors                                   │
    │  • Two-Tower Neural Network                             │
    │  • Attention-Based Model                                 │
    │  • Hybrid Ensemble Approach                             │
    └───────────────────────────┬─────────────────────────────┘
                                │
    ┌───────────────────────────▼─────────────────────────────┐
    │                DATABASE LAYER (PostgreSQL)               │
    │  • Users: 10,000+ profiles                             │
    │  • Movies: 5,000+ with metadata                        │
    │  • Ratings: 1,000,000+ interactions                    │
    │  • Watch History: User viewing sessions                │
    └─────────────────────────────────────────────────────────┘
""")

print("\n📊 PERFORMANCE RIEPILOGO:")
print("""
    Algorithm               RMSE      MAE      Type
    ----------              -----     -----    -----
    Random Forest           {rf_rmse:.4f}   {rf_mae:.4f}   Supervised ML
    SVD Matrix Factorization {svd_rmse:.4f}   {svd_mae:.4f}   Collaborative
    Neural Collaborative Filtering {ncf_rmse:.4f}   {ncf_mae:.4f}   Deep Learning
    Two-Tower Network       {tt_rmse:.4f}   {tt_mae:.4f}   Neural Matching
    Attention Model         {att_rmse:.4f}   {att_mae:.4f}   Attention-Based
""".format(
    rf_rmse=rf_rmse, rf_mae=rf_mae,
    svd_rmse=svd_rmse, svd_mae=svd_mae,
    ncf_rmse=ncf_rmse, ncf_mae=ncf_mae,
    tt_rmse=tt_rmse, tt_mae=tt_mae,
    att_rmse=att_rmse, att_mae=att_mae
))

print("\n🚀 PROSSIMI PASSI PER PRODUZIONE:")
print("""
    1. 🐳 Dockerize: Containerizzare l'applicazione
    2. ☁️  Cloud Deployment: Deploy su AWS/Azure/GCP
    3. 📈 Monitoring: Implementare Prometheus + Grafana
    4. 🔄 CI/CD: Pipeline automatizzata con GitHub Actions
    5. 🚀 Scaling: Load balancing e auto-scaling
    6. 🎯 A/B Testing: Testare diversi algoritmi
    7. 📊 Analytics: Dashboard per analisi performance
    8. 🔍 Explainability: Aggiungere spiegazioni raccomandazioni
""")

print("\n💡 COMPETENZE ACQUISITE IN QUESTO PROGETTO:")
print("""
    ✅ Database Design: Schema relazionale complesso per streaming
    ✅ SQL Avanzato: Query complesse per feature engineering
    ✅ Machine Learning: 5+ algoritmi di raccomandazione
    ✅ Deep Learning: Reti neurali per collaborative filtering
    ✅ NLP: Text processing per content-based filtering
    ✅ API Development: REST API professionale con FastAPI
    ✅ System Design: Architettura scalabile end-to-end
    ✅ MLOps: Deployment e monitoring modelli ML
    ✅ Ensemble Methods: Combinazione di multiple modelli
    ✅ Production Readiness: Docker, logging, monitoring
""")

print("\n" + "=" * 70)
print("🎓 COMPLIMENTI! HAI COMPLETATO IL PROGETTO FINALE")
print("🎬 SISTEMA DI RACCOMANDAZIONE NETFLIX-STYLE")
print("=" * 70)

print("\n🌟 SEI ORA PRONTO PER:")
print("   • Lavorare come ML Engineer in aziende tech")
print("   • Costruire sistemi di raccomandazione reali")
print("   • Gestire progetti AI end-to-end")
print("   • Lavorare con database, ML e Deep Learning")

print("\n🚀 IL TUO VIAGGIO NELL'AI È INIZIATO!")
print("   Continua ad apprendere, costruire e innovare!")

🎓 PERCORSO COMPLETATO CON SUCCESSO!

📚 Concetti Appresi

  • Database Design per AI
  • Feature Engineering SQL
  • Collaborative Filtering
  • Content-Based Filtering
  • Neural Networks per ML
  • API REST Development
  • System Architecture

🛠️ Tecnologie Utilizzate

  • PostgreSQL/SQLite
  • Pandas & NumPy
  • Scikit-learn
  • TensorFlow & Keras
  • FastAPI
  • Docker
  • NLTK & NLP

🎯 Competenze Professionali

  • ML Engineer
  • Data Engineer
  • AI Developer
  • Backend Developer
  • System Architect
  • Data Scientist

🚀 Prossime Tappe nel Tuo Percorso AI

Computer Vision
NLP Avanzato
Reinforcement Learning
MLOps
Big Data AI

"Il sistema che hai costruito è a livello industriale. Sei pronto per il mondo reale dell'AI!"

🎉 COMPLIMENTI PER IL COMPLETAMENTO!

Hai completato il Percorso Completo Python AI per la Classe Quinta
Ora possiedi le competenze per costruire sistemi AI reali!

Torna in alto