added cosine similarity score + articles categories

This commit is contained in:
Grégoire Mureau 2025-09-11 08:35:56 +02:00
parent 25639153aa
commit 664c8893a6
21 changed files with 311 additions and 2251 deletions

203
README.MD Normal file
View file

@ -0,0 +1,203 @@
# Système de Recommandation de Contenu - My Content
Projet de développement d'un système de recommandation d'articles basé sur une architecture serverless Azure Functions avec interface utilisateur Streamlit.
## Vue d'ensemble
Ce projet implémente un système de recommandation de contenu utilisant des algorithmes de filtrage collaboratif et basé sur le contenu. L'architecture serverless permet une mise à l'échelle automatique et un coût optimisé.
### Fonctionnalités principales
- **API de recommandation** : Azure Function retournant le top 5 des articles recommandés pour un utilisateur
- **Interface utilisateur** : Application Streamlit pour tester et démontrer le système
- **Algorithmes implémentés** : Content-Based Filtering avec embeddings pré-calculés
- **Architecture serverless** : Déploiement sur Azure Functions avec plan Consumption
## Architecture
### MVP Actuel
```
[Interface Streamlit] → [Azure Functions API] → [Algorithme de recommandation]
↓
[Données embeddings + historique utilisateur]
```
### Composants
- **Frontend** : Streamlit (interface de test)
- **Backend** : Azure Functions (Python 3.12, Linux)
- **Algorithme** : Content-Based Filtering avec similarité cosinus
- **Données** : Embeddings articles (50D via PCA vs 250D embeddings originaux) + historique des clics
## Structure du Projet
```
Project_10/
├── azure-app/ # Azure Function
│ ├── function_app.py # Point d'entrée Azure Function
│ ├── requirements.txt # Dépendances Python
│ ├── host.json # Configuration Azure Functions
│ ├── local.settings.json # Configuration locale (non versionnée)
│ ├── data/
│ │ ├── embeddings_azure_50d.pkl # Embeddings articles (PCA 50D)
│ │ └── clicks_sample.csv # Historique des clics utilisateurs
│ └── utils/
│ ├── data_loader.py # Chargement et cache des données
│ └── content_based.py # Algorithme de recommandation
├── streamlit_app.py # Interface utilisateur
├── streamlit_requirements.txt # Dépendances Streamlit
└── README.md
```
## Installation et Déploiement
### Prérequis
- Python 3.12+
- Azure CLI
- Azure Functions Core Tools
- Compte Azure
### Déploiement Azure Functions
1. **Connexion à Azure**
```bash
az login
```
2. **Création de l'infrastructure**
```bash
# Groupe de ressources
az group create --name rg-content-recommender --location francecentral
# Compte de stockage
az storage account create --name [votre-nom-stockage-unique] \
--resource-group rg-content-recommender \
--location francecentral \
--sku Standard_LRS
# Function App Linux avec Python 3.12
az functionapp create \
--resource-group rg-content-recommender \
--consumption-plan-location francecentral \
--runtime python \
--runtime-version 3.12 \
--functions-version 4 \
--name contentrecommender-linux \
--storage-account stcontentrecommend2025 \
--os-type Linux
```
3. **Déploiement du code**
```bash
# Depuis le dossier azure-app
func azure functionapp publish contentrecommender-linux
```
### Interface Streamlit
1. **Installation des dépendances**
```bash
pip install -r streamlit_requirements.txt
```
2. **Lancement de l'interface**
```bash
streamlit run streamlit_app.py
```
## Utilisation
### API Azure Functions
**Endpoint** : `https://contentrecommender-linux.azurewebsites.net/api/recommend`
**Paramètres** :
- `user_id` (int) : Identifiant de l'utilisateur (0-706)
**Exemple d'appel** :
```bash
curl "https://contentrecommender-linux.azurewebsites.net/api/recommend?user_id=0"
```
**Réponse** :
```json
{
"user_id": 0,
"recommendations": [157519, 157944, 159495, 156690, 162857]
}
```
### Interface Streamlit
1. Configurer l'URL de l'API (local ou Azure)
2. Sélectionner un ID utilisateur
3. Obtenir les recommandations
4. Tester les fonctionnalités avancées
## Algorithme de Recommandation
### Content-Based Filtering
1. **Chargement des données** : Embeddings pré-calculés et historique utilisateur
2. **Calcul du profil utilisateur** : Moyenne des embeddings des articles consultés
3. **Similarité cosinus** : Calcul avec tous les articles du catalogue
4. **Filtrage** : Exclusion des articles déjà vus
5. **Sélection** : Top 5 des articles les plus similaires
### Optimisations
- **Réduction dimensionnelle** : PCA 250D → 50D pour optimiser les performances Azure
- **Cache en mémoire** : Embeddings et données utilisateur chargés une seule fois
- **Architecture serverless** : Mise à l'échelle automatique selon la demande
## Tests et Validation
### Tests locaux
```bash
# Démarrage Azure Functions local
func start
# Test de l'algorithme
python test_local.py
```
### Métriques de performance
- **Temps de réponse** : ~200ms par recommandation
- **Utilisateurs supportés** : 707 dans le dataset actuel
- **Articles disponibles** : 364,047 avec embeddings
## Problèmes Connus et Solutions
### Déploiement Azure
- **Python sur Windows** : Non supporté, utiliser Linux obligatoirement
- **Authentification** : Configurer `http_auth_level=func.AuthLevel.ANONYMOUS`
- **Erreurs "Service unavailable"** : Problèmes temporaires Azure, réessayer plus tard
### Limitations actuelles
- **Cold start** : Première requête lente après période d'inactivité
- **Nouveaux utilisateurs** : Pas de gestion du problème de démarrage à froid
- **Mise à jour des données** : Processus manuel
## Architecture Cible (Future)
### Améliorations prévues
- **Gestion cold start** : Recommandations populaires pour nouveaux utilisateurs
- **Pipeline de données** : Mise à jour automatique des embeddings
- **Base de données** : Stockage persistant des profils utilisateur
- **API hybride** : Combinaison Content-Based + Collaborative Filtering
- **Cache distribué** : Précalcul des recommandations populaires
## Technologies Utilisées
- **Backend** : Azure Functions, Python 3.12
- **Frontend** : Streamlit
- **ML/Data** : scikit-learn, pandas, numpy
- **Infrastructure** : Azure CLI, Azure Storage
- **Déploiement** : Azure Functions Core Tools
## Auteur
Projet réalisé dans le cadre d'une formation OpenClassrooms - Ingénieur IA.
## Licence
Ce projet est à des fins éducatives uniquement.

View file

@ -7,8 +7,8 @@ import sys
# Ajouter le dossier utils au path pour les imports # Ajouter le dossier utils au path pour les imports
sys.path.append(os.path.dirname(os.path.abspath(__file__))) sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from utils.data_loader import load_embeddings, load_user_data from utils.data_loader import load_embeddings, load_user_data, get_articles_categories
from utils.content_based import get_recommendations from utils.content_based import get_recommendations_with_scores
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@ -37,14 +37,26 @@ def recommend(req: func.HttpRequest) -> func.HttpResponse:
# Convertir en entier # Convertir en entier
user_id = int(user_id) user_id = int(user_id)
# Générer les recommandations # Générer les recommandations avec scores
recommendations = get_recommendations(user_id) recommendations = get_recommendations_with_scores(user_id)
if recommendations: if recommendations:
# Enrichir avec les catégories
article_ids = [rec['article_id'] for rec in recommendations]
categories = get_articles_categories(article_ids)
# Ajouter les catégories aux recommandations
enriched_recommendations = []
for rec in recommendations:
enriched_rec = rec.copy()
enriched_rec['category_id'] = categories.get(rec['article_id'])
enriched_rec['similarity_score_percent'] = round(rec['similarity_score'] * 100, 2)
enriched_recommendations.append(enriched_rec)
return func.HttpResponse( return func.HttpResponse(
json.dumps({ json.dumps({
"user_id": user_id, "user_id": user_id,
"recommendations": recommendations "recommendations": enriched_recommendations
}), }),
status_code=200, status_code=200,
mimetype="application/json" mimetype="application/json"

View file

@ -1,6 +1,6 @@
import numpy as np import numpy as np
from sklearn.metrics.pairwise import cosine_similarity from sklearn.metrics.pairwise import cosine_similarity
from typing import List, Optional, Tuple from typing import List, Optional, Tuple, Dict
import logging import logging
from .data_loader import load_embeddings, get_user_articles from .data_loader import load_embeddings, get_user_articles
@ -26,10 +26,16 @@ def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) ->
candidates.append((article_id, similarity)) candidates.append((article_id, similarity))
return candidates return candidates
def _get_top_recommendations(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[int]: def _get_top_recommendations_with_scores(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[Dict[str, any]]:
"""Trie les candidats et retourne le top N.""" """Trie les candidats et retourne le top N avec les scores."""
candidates.sort(key=lambda x: x[1], reverse=True) candidates.sort(key=lambda x: x[1], reverse=True)
return [article_id for article_id, _ in candidates[:n_recommendations]] recommendations = []
for article_id, score in candidates[:n_recommendations]:
recommendations.append({
'article_id': article_id,
'similarity_score': float(score)
})
return recommendations
def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray) -> List[int]: def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray) -> List[int]:
"""Valide et filtre les articles dans les limites des embeddings.""" """Valide et filtre les articles dans les limites des embeddings."""
@ -40,14 +46,24 @@ def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray)
def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[List[int]]: def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[List[int]]:
""" """
Génère des recommandations pour un utilisateur donné. Génère des recommandations pour un utilisateur donné (version compatibilité).
Retourne seulement les IDs pour maintenir la compatibilité.
"""
recommendations_with_scores = get_recommendations_with_scores(user_id, n_recommendations)
if recommendations_with_scores:
return [rec['article_id'] for rec in recommendations_with_scores]
return None
def get_recommendations_with_scores(user_id: int, n_recommendations: int = 5) -> Optional[List[Dict[str, any]]]:
"""
Génère des recommandations avec scores pour un utilisateur donné.
Args: Args:
user_id: ID de l'utilisateur user_id: ID de l'utilisateur
n_recommendations: Nombre de recommandations à retourner n_recommendations: Nombre de recommandations à retourner
Returns: Returns:
Liste des article_ids recommandés ou None si erreur Liste des recommandations avec article_id et similarity_score ou None si erreur
""" """
try: try:
# Charger les données # Charger les données
@ -69,9 +85,9 @@ def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[Li
# Filtrer et trier # Filtrer et trier
candidates = _filter_candidates(similarities, clicked_articles) candidates = _filter_candidates(similarities, clicked_articles)
recommendations = _get_top_recommendations(candidates, n_recommendations) recommendations = _get_top_recommendations_with_scores(candidates, n_recommendations)
logging.info(f"Generated {len(recommendations)} recommendations for user {user_id}") logging.info(f"Generated {len(recommendations)} recommendations with scores for user {user_id}")
return recommendations return recommendations
except Exception as e: except Exception as e:

View file

@ -9,10 +9,12 @@ import numpy as np
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data') DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
EMBEDDINGS_PATH = os.path.join(DATA_DIR, 'embeddings_azure_50d.pkl') EMBEDDINGS_PATH = os.path.join(DATA_DIR, 'embeddings_azure_50d.pkl')
CLICKS_PATH = os.path.join(DATA_DIR, 'clicks_sample.csv') CLICKS_PATH = os.path.join(DATA_DIR, 'clicks_sample.csv')
ARTICLES_METADATA_PATH = os.path.join(DATA_DIR, 'articles_metadata.csv')
# Cache global # Cache global
_embeddings_cache: Optional[np.ndarray] = None _embeddings_cache: Optional[np.ndarray] = None
_user_data_cache: Optional[Dict[int, List[int]]] = None _user_data_cache: Optional[Dict[int, List[int]]] = None
_articles_metadata_cache: Optional[pd.DataFrame] = None
def _load_pickle_file(file_path: str) -> np.ndarray: def _load_pickle_file(file_path: str) -> np.ndarray:
"""Charge un fichier pickle.""" """Charge un fichier pickle."""
@ -65,14 +67,56 @@ def load_user_data() -> Dict[int, List[int]]:
return _user_data_cache return _user_data_cache
def load_articles_metadata() -> pd.DataFrame:
"""Charge les métadonnées des articles avec cache."""
global _articles_metadata_cache
if _articles_metadata_cache is None:
try:
_articles_metadata_cache = _load_csv_file(ARTICLES_METADATA_PATH)
logging.info(f"Articles metadata loaded: {len(_articles_metadata_cache)} articles")
except Exception as e:
logging.error(f"Error loading articles metadata: {str(e)}")
raise
return _articles_metadata_cache
def get_user_articles(user_id: int) -> Optional[List[int]]: def get_user_articles(user_id: int) -> Optional[List[int]]:
"""Récupère les articles d'un utilisateur.""" """Récupère les articles d'un utilisateur."""
user_data = load_user_data() user_data = load_user_data()
return user_data.get(user_id) return user_data.get(user_id)
def get_article_category(article_id: int) -> Optional[int]:
"""Récupère la catégorie d'un article."""
try:
metadata_df = load_articles_metadata()
article_row = metadata_df[metadata_df['article_id'] == article_id]
if not article_row.empty:
return int(article_row.iloc[0]['category_id'])
return None
except Exception as e:
logging.warning(f"Error getting category for article {article_id}: {str(e)}")
return None
def get_articles_categories(article_ids: List[int]) -> Dict[int, Optional[int]]:
"""Récupère les catégories de plusieurs articles."""
try:
metadata_df = load_articles_metadata()
categories = {}
for article_id in article_ids:
article_row = metadata_df[metadata_df['article_id'] == article_id]
if not article_row.empty:
categories[article_id] = int(article_row.iloc[0]['category_id'])
else:
categories[article_id] = None
return categories
except Exception as e:
logging.warning(f"Error getting categories for articles: {str(e)}")
return {aid: None for aid in article_ids}
def check_data_files() -> bool: def check_data_files() -> bool:
"""Vérifie que tous les fichiers de données existent.""" """Vérifie que tous les fichiers de données existent."""
files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH] files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH, ARTICLES_METADATA_PATH]
for file_path in files_to_check: for file_path in files_to_check:
if not os.path.exists(file_path): if not os.path.exists(file_path):

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -1,73 +0,0 @@
import azure.functions as func
import logging
import json
import os
import sys
# Ajouter le dossier utils au path pour les imports
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from utils.data_loader import load_embeddings, load_user_data
from utils.content_based import get_recommendations
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
@app.route(route="recommend")
def recommend(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Recommendation request received.')
# Récupérer le user_id depuis les paramètres de requête
user_id = req.params.get('user_id')
if not user_id:
try:
req_body = req.get_json()
user_id = req_body.get('user_id') if req_body else None
except ValueError:
pass
if not user_id:
return func.HttpResponse(
json.dumps({"error": "user_id parameter required"}),
status_code=400,
mimetype="application/json"
)
try:
# Convertir en entier
user_id = int(user_id)
# Générer les recommandations
recommendations = get_recommendations(user_id)
if recommendations:
return func.HttpResponse(
json.dumps({
"user_id": user_id,
"recommendations": recommendations
}),
status_code=200,
mimetype="application/json"
)
else:
return func.HttpResponse(
json.dumps({
"error": f"No recommendations found for user {user_id}"
}),
status_code=404,
mimetype="application/json"
)
except ValueError:
return func.HttpResponse(
json.dumps({"error": "user_id must be a valid integer"}),
status_code=400,
mimetype="application/json"
)
except Exception as e:
logging.error(f"Error generating recommendations: {str(e)}")
return func.HttpResponse(
json.dumps({"error": "Internal server error"}),
status_code=500,
mimetype="application/json"
)

View file

@ -1,15 +0,0 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}

View file

@ -1,4 +0,0 @@
azure-functions
numpy
scikit-learn
pandas

View file

@ -1,92 +0,0 @@
import logging
import sys
import os
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Ajouter le dossier utils au path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from utils.content_based import get_recommendations
from utils.data_loader import get_available_users, check_data_files, load_embeddings
def test_data_loading():
"""Test du chargement des données."""
print("🔄 Test du chargement des données...")
if not check_data_files():
print("❌ Fichiers de données manquants")
return False
try:
# Test chargement embeddings
embeddings = load_embeddings()
print(f"✅ Embeddings chargés: {embeddings.shape}")
# Test chargement utilisateurs
users = get_available_users()
print(f"✅ Utilisateurs chargés: {len(users)} utilisateurs")
print(f" Premiers utilisateurs: {users[:5]}")
return True
except Exception as e:
print(f"❌ Erreur lors du chargement: {e}")
return False
def test_recommendations():
"""Test des recommandations."""
print("\n🔄 Test des recommandations...")
users = get_available_users()
test_users = users[:3] if len(users) >= 3 else users
for user_id in test_users:
print(f"\n👤 Test pour utilisateur {user_id}:")
try:
recommendations = get_recommendations(user_id, n_recommendations=5)
if recommendations:
print(f"✅ {len(recommendations)} recommandations générées")
print(f" Articles recommandés: {recommendations}")
else:
print("⚠️ Aucune recommandation générée")
except Exception as e:
print(f"❌ Erreur pour utilisateur {user_id}: {e}")
def test_edge_cases():
"""Test des cas limites."""
print("\n🔄 Test des cas limites...")
# Test utilisateur inexistant
print("Test utilisateur inexistant (ID: 99999):")
result = get_recommendations(99999)
if result is None:
print("✅ Utilisateur inexistant géré correctement")
else:
print(f"⚠️ Résultat inattendu: {result}")
def main():
"""Fonction principale de test."""
print("🚀 Démarrage des tests locaux\n")
# Test 1: Chargement des données
if not test_data_loading():
print("\n❌ Tests interrompus - problème de chargement")
return
# Test 2: Recommandations
test_recommendations()
# Test 3: Cas limites
test_edge_cases()
print("\n✨ Tests terminés")
if __name__ == "__main__":
main()

View file

@ -1,79 +0,0 @@
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from typing import List, Optional, Tuple
import logging
from .data_loader import load_embeddings, get_user_articles
def _calculate_mean_embedding(embeddings: np.ndarray, article_ids: List[int]) -> np.ndarray:
"""Calcule la moyenne des embeddings pour une liste d'articles."""
valid_ids = [aid for aid in article_ids if aid < len(embeddings)]
if not valid_ids:
raise ValueError("No valid article IDs found")
articles_embeddings = embeddings[valid_ids]
return np.mean(articles_embeddings, axis=0).reshape(1, -1)
def _compute_similarities(reference_embedding: np.ndarray, embeddings: np.ndarray) -> np.ndarray:
"""Calcule la similarité cosinus entre l'embedding de référence et tous les embeddings."""
return cosine_similarity(reference_embedding, embeddings)[0]
def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) -> List[Tuple[int, float]]:
"""Filtre les candidats en excluant les articles déjà vus."""
candidates = []
for article_id, similarity in enumerate(similarities):
if article_id not in clicked_articles:
candidates.append((article_id, similarity))
return candidates
def _get_top_recommendations(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[int]:
"""Trie les candidats et retourne le top N."""
candidates.sort(key=lambda x: x[1], reverse=True)
return [article_id for article_id, _ in candidates[:n_recommendations]]
def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray) -> List[int]:
"""Valide et filtre les articles dans les limites des embeddings."""
valid_articles = [art for art in clicked_articles if 0 <= art < len(embeddings)]
if not valid_articles:
raise ValueError("No valid articles found for user")
return valid_articles
def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[List[int]]:
"""
Génère des recommandations pour un utilisateur donné.
Args:
user_id: ID de l'utilisateur
n_recommendations: Nombre de recommandations à retourner
Returns:
Liste des article_ids recommandés ou None si erreur
"""
try:
# Charger les données
embeddings = load_embeddings()
clicked_articles = get_user_articles(user_id)
if not clicked_articles:
logging.warning(f"No articles found for user {user_id}")
return None
# Valider les articles
valid_articles = _validate_user_articles(clicked_articles, embeddings)
# Calculer l'embedding de référence (stratégie mean)
reference_embedding = _calculate_mean_embedding(embeddings, valid_articles)
# Calculer similarités
similarities = _compute_similarities(reference_embedding, embeddings)
# Filtrer et trier
candidates = _filter_candidates(similarities, clicked_articles)
recommendations = _get_top_recommendations(candidates, n_recommendations)
logging.info(f"Generated {len(recommendations)} recommendations for user {user_id}")
return recommendations
except Exception as e:
logging.error(f"Error generating recommendations for user {user_id}: {str(e)}")
return None

View file

@ -1,87 +0,0 @@
import pickle
import pandas as pd
import os
import logging
from typing import Dict, List, Optional
import numpy as np
# Chemins des fichiers
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
EMBEDDINGS_PATH = os.path.join(DATA_DIR, 'embeddings_azure_50d.pkl')
CLICKS_PATH = os.path.join(DATA_DIR, 'clicks_sample.csv')
# Cache global
_embeddings_cache: Optional[np.ndarray] = None
_user_data_cache: Optional[Dict[int, List[int]]] = None
def _load_pickle_file(file_path: str) -> np.ndarray:
"""Charge un fichier pickle."""
with open(file_path, 'rb') as f:
return pickle.load(f)
def _load_csv_file(file_path: str) -> pd.DataFrame:
"""Charge un fichier CSV."""
return pd.read_csv(file_path)
def _process_clicks_data(clicks_df: pd.DataFrame) -> Dict[int, List[int]]:
"""Convertit le DataFrame en dictionnaire user->articles."""
user_data = {}
for _, row in clicks_df.iterrows():
user_id = int(row['user_id'])
article_id = int(row['click_article_id'])
if user_id not in user_data:
user_data[user_id] = []
user_data[user_id].append(article_id)
return user_data
def load_embeddings() -> np.ndarray:
"""Charge les embeddings avec cache."""
global _embeddings_cache
if _embeddings_cache is None:
try:
_embeddings_cache = _load_pickle_file(EMBEDDINGS_PATH)
logging.info(f"Embeddings loaded: {_embeddings_cache.shape}")
except Exception as e:
logging.error(f"Error loading embeddings: {str(e)}")
raise
return _embeddings_cache
def load_user_data() -> Dict[int, List[int]]:
"""Charge les données utilisateur avec cache."""
global _user_data_cache
if _user_data_cache is None:
try:
clicks_df = _load_csv_file(CLICKS_PATH)
_user_data_cache = _process_clicks_data(clicks_df)
logging.info(f"User data loaded: {len(_user_data_cache)} users")
except Exception as e:
logging.error(f"Error loading user data: {str(e)}")
raise
return _user_data_cache
def get_user_articles(user_id: int) -> Optional[List[int]]:
"""Récupère les articles d'un utilisateur."""
user_data = load_user_data()
return user_data.get(user_id)
def check_data_files() -> bool:
"""Vérifie que tous les fichiers de données existent."""
files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH]
for file_path in files_to_check:
if not os.path.exists(file_path):
logging.error(f"Missing data file: {file_path}")
return False
return True
def get_available_users() -> List[int]:
"""Retourne la liste de tous les user_ids disponibles."""
user_data = load_user_data()
return list(user_data.keys())

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,008 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -122,9 +122,28 @@ def main():
st.subheader("🎯 Articles recommandés") st.subheader("🎯 Articles recommandés")
recommendations = result['recommendations'] recommendations = result['recommendations']
# Affichage sous forme de liste numérotée # Vérifier si on a le format enrichi (avec scores et catégories)
for i, article_id in enumerate(recommendations, 1): if recommendations and isinstance(recommendations[0], dict) and 'similarity_score_percent' in recommendations[0]:
st.write(f"**{i}.** Article ID: `{article_id}`") # Format enrichi - affichage en tableau
import pandas as pd
df_data = []
for i, rec in enumerate(recommendations, 1):
df_data.append({
'Rang': i,
'Article ID': rec['article_id'],
'Similarité (%)': f"{rec['similarity_score_percent']:.1f}%",
'Catégorie': rec.get('category_id', 'N/A')
})
df = pd.DataFrame(df_data)
st.dataframe(df, hide_index=True, use_container_width=True)
else:
# Format simple - affichage basique pour compatibilité
for i, article_id in enumerate(recommendations, 1):
if isinstance(article_id, dict):
article_id = article_id.get('article_id', article_id)
st.write(f"**{i}.** Article ID: `{article_id}`")
# JSON brut (collapsible) # JSON brut (collapsible)
with st.expander("📝 Réponse JSON complète"): with st.expander("📝 Réponse JSON complète"):