Initial commit of scripts

This commit is contained in:
Grégoire Mureau 2025-11-10 16:40:12 +01:00
parent 250af3ebf3
commit 799015be44
9 changed files with 838 additions and 838 deletions

450
README.MD
View file

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

View file

@ -1,9 +1,9 @@
.git* .git*
.vscode .vscode
__azurite_db*__.json __azurite_db*__.json
__blobstorage__ __blobstorage__
__queuestorage__ __queuestorage__
local.settings.json local.settings.json
test test
.venv .venv
test_local.py test_local.py

View file

@ -131,5 +131,5 @@ local.settings.json
# Azurite artifacts # Azurite artifacts
__blobstorage__ __blobstorage__
__queuestorage__ __queuestorage__
__azurite_db*__.json __azurite_db*__.json
.python_packages .python_packages

View file

@ -1,85 +1,85 @@
import azure.functions as func import azure.functions as func
import logging import logging
import json import json
import os import os
import sys 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, get_articles_categories from utils.data_loader import load_embeddings, load_user_data, get_articles_categories
from utils.content_based import get_recommendations_with_scores 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)
@app.route(route="recommend") @app.route(route="recommend")
def recommend(req: func.HttpRequest) -> func.HttpResponse: def recommend(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Recommendation request received.') logging.info('Recommendation request received.')
# Récupérer le user_id depuis les paramètres de requête # Récupérer le user_id depuis les paramètres de requête
user_id = req.params.get('user_id') user_id = req.params.get('user_id')
if not user_id: if not user_id:
try: try:
req_body = req.get_json() req_body = req.get_json()
user_id = req_body.get('user_id') if req_body else None user_id = req_body.get('user_id') if req_body else None
except ValueError: except ValueError:
pass pass
if not user_id: if not user_id:
return func.HttpResponse( return func.HttpResponse(
json.dumps({"error": "user_id parameter required"}), json.dumps({"error": "user_id parameter required"}),
status_code=400, status_code=400,
mimetype="application/json" mimetype="application/json"
) )
try: try:
# Convertir en entier # Convertir en entier
user_id = int(user_id) user_id = int(user_id)
# Générer les recommandations avec scores # Générer les recommandations avec scores
recommendations = get_recommendations_with_scores(user_id) recommendations = get_recommendations_with_scores(user_id)
if recommendations: if recommendations:
# Enrichir avec les catégories # Enrichir avec les catégories
article_ids = [rec['article_id'] for rec in recommendations] article_ids = [rec['article_id'] for rec in recommendations]
categories = get_articles_categories(article_ids) categories = get_articles_categories(article_ids)
# Ajouter les catégories aux recommandations # Ajouter les catégories aux recommandations
enriched_recommendations = [] enriched_recommendations = []
for rec in recommendations: for rec in recommendations:
enriched_rec = rec.copy() enriched_rec = rec.copy()
enriched_rec['category_id'] = categories.get(rec['article_id']) enriched_rec['category_id'] = categories.get(rec['article_id'])
enriched_rec['similarity_score_percent'] = round(rec['similarity_score'] * 100, 2) enriched_rec['similarity_score_percent'] = round(rec['similarity_score'] * 100, 2)
enriched_recommendations.append(enriched_rec) enriched_recommendations.append(enriched_rec)
return func.HttpResponse( return func.HttpResponse(
json.dumps({ json.dumps({
"user_id": user_id, "user_id": user_id,
"recommendations": enriched_recommendations "recommendations": enriched_recommendations
}), }),
status_code=200, status_code=200,
mimetype="application/json" mimetype="application/json"
) )
else: else:
return func.HttpResponse( return func.HttpResponse(
json.dumps({ json.dumps({
"error": f"No recommendations found for user {user_id}" "error": f"No recommendations found for user {user_id}"
}), }),
status_code=404, status_code=404,
mimetype="application/json" mimetype="application/json"
) )
except ValueError: except ValueError:
return func.HttpResponse( return func.HttpResponse(
json.dumps({"error": "user_id must be a valid integer"}), json.dumps({"error": "user_id must be a valid integer"}),
status_code=400, status_code=400,
mimetype="application/json" mimetype="application/json"
) )
except Exception as e: except Exception as e:
logging.error(f"Error generating recommendations: {str(e)}") logging.error(f"Error generating recommendations: {str(e)}")
return func.HttpResponse( return func.HttpResponse(
json.dumps({"error": "Internal server error"}), json.dumps({"error": "Internal server error"}),
status_code=500, status_code=500,
mimetype="application/json" mimetype="application/json"
) )

View file

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

View file

@ -1,95 +1,95 @@
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, Dict 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
def _calculate_mean_embedding(embeddings: np.ndarray, article_ids: List[int]) -> np.ndarray: def _calculate_mean_embedding(embeddings: np.ndarray, article_ids: List[int]) -> np.ndarray:
"""Calcule la moyenne des embeddings pour une liste d'articles.""" """Calcule la moyenne des embeddings pour une liste d'articles."""
valid_ids = [aid for aid in article_ids if aid < len(embeddings)] valid_ids = [aid for aid in article_ids if aid < len(embeddings)]
if not valid_ids: if not valid_ids:
raise ValueError("No valid article IDs found") raise ValueError("No valid article IDs found")
articles_embeddings = embeddings[valid_ids] articles_embeddings = embeddings[valid_ids]
return np.mean(articles_embeddings, axis=0).reshape(1, -1) return np.mean(articles_embeddings, axis=0).reshape(1, -1)
def _compute_similarities(reference_embedding: np.ndarray, embeddings: np.ndarray) -> np.ndarray: 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.""" """Calcule la similarité cosinus entre l'embedding de référence et tous les embeddings."""
return cosine_similarity(reference_embedding, embeddings)[0] return cosine_similarity(reference_embedding, embeddings)[0]
def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) -> List[Tuple[int, float]]: def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) -> List[Tuple[int, float]]:
"""Filtre les candidats en excluant les articles déjà vus.""" """Filtre les candidats en excluant les articles déjà vus."""
candidates = [] candidates = []
for article_id, similarity in enumerate(similarities): for article_id, similarity in enumerate(similarities):
if article_id not in clicked_articles: if article_id not in clicked_articles:
candidates.append((article_id, similarity)) candidates.append((article_id, similarity))
return candidates return candidates
def _get_top_recommendations_with_scores(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[Dict[str, any]]: 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 avec les scores.""" """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)
recommendations = [] recommendations = []
for article_id, score in candidates[:n_recommendations]: for article_id, score in candidates[:n_recommendations]:
recommendations.append({ recommendations.append({
'article_id': article_id, 'article_id': article_id,
'similarity_score': float(score) 'similarity_score': float(score)
}) })
return recommendations 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."""
valid_articles = [art for art in clicked_articles if 0 <= art < len(embeddings)] valid_articles = [art for art in clicked_articles if 0 <= art < len(embeddings)]
if not valid_articles: if not valid_articles:
raise ValueError("No valid articles found for user") raise ValueError("No valid articles found for user")
return valid_articles return valid_articles
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é (version compatibilité). Génère des recommandations pour un utilisateur donné (version compatibilité).
Retourne seulement les IDs pour maintenir la compatibilité. Retourne seulement les IDs pour maintenir la compatibilité.
""" """
recommendations_with_scores = get_recommendations_with_scores(user_id, n_recommendations) recommendations_with_scores = get_recommendations_with_scores(user_id, n_recommendations)
if recommendations_with_scores: if recommendations_with_scores:
return [rec['article_id'] for rec in recommendations_with_scores] return [rec['article_id'] for rec in recommendations_with_scores]
return None return None
def get_recommendations_with_scores(user_id: int, n_recommendations: int = 5) -> Optional[List[Dict[str, any]]]: 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é. 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 recommandations avec article_id et similarity_score 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
embeddings = load_embeddings() embeddings = load_embeddings()
clicked_articles = get_user_articles(user_id) clicked_articles = get_user_articles(user_id)
if not clicked_articles: if not clicked_articles:
logging.warning(f"No articles found for user {user_id}") logging.warning(f"No articles found for user {user_id}")
return None return None
# Valider les articles # Valider les articles
valid_articles = _validate_user_articles(clicked_articles, embeddings) valid_articles = _validate_user_articles(clicked_articles, embeddings)
# Calculer l'embedding de référence (stratégie mean) # Calculer l'embedding de référence (stratégie mean)
reference_embedding = _calculate_mean_embedding(embeddings, valid_articles) reference_embedding = _calculate_mean_embedding(embeddings, valid_articles)
# Calculer similarités # Calculer similarités
similarities = _compute_similarities(reference_embedding, embeddings) similarities = _compute_similarities(reference_embedding, embeddings)
# Filtrer et trier # Filtrer et trier
candidates = _filter_candidates(similarities, clicked_articles) candidates = _filter_candidates(similarities, clicked_articles)
recommendations = _get_top_recommendations_with_scores(candidates, n_recommendations) recommendations = _get_top_recommendations_with_scores(candidates, n_recommendations)
logging.info(f"Generated {len(recommendations)} recommendations with scores 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:
logging.error(f"Error generating recommendations for user {user_id}: {str(e)}") logging.error(f"Error generating recommendations for user {user_id}: {str(e)}")
return None return None

View file

@ -1,131 +1,131 @@
import pickle import pickle
import pandas as pd import pandas as pd
import os import os
import logging import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
import numpy as np import numpy as np
# Chemins des fichiers # Chemins des fichiers
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') 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 _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."""
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
return pickle.load(f) return pickle.load(f)
def _load_csv_file(file_path: str) -> pd.DataFrame: def _load_csv_file(file_path: str) -> pd.DataFrame:
"""Charge un fichier CSV.""" """Charge un fichier CSV."""
return pd.read_csv(file_path) return pd.read_csv(file_path)
def _process_clicks_data(clicks_df: pd.DataFrame) -> Dict[int, List[int]]: def _process_clicks_data(clicks_df: pd.DataFrame) -> Dict[int, List[int]]:
"""Convertit le DataFrame en dictionnaire user->articles.""" """Convertit le DataFrame en dictionnaire user->articles."""
user_data = {} user_data = {}
for _, row in clicks_df.iterrows(): for _, row in clicks_df.iterrows():
user_id = int(row['user_id']) user_id = int(row['user_id'])
article_id = int(row['click_article_id']) article_id = int(row['click_article_id'])
if user_id not in user_data: if user_id not in user_data:
user_data[user_id] = [] user_data[user_id] = []
user_data[user_id].append(article_id) user_data[user_id].append(article_id)
return user_data return user_data
def load_embeddings() -> np.ndarray: def load_embeddings() -> np.ndarray:
"""Charge les embeddings avec cache.""" """Charge les embeddings avec cache."""
global _embeddings_cache global _embeddings_cache
if _embeddings_cache is None: if _embeddings_cache is None:
try: try:
_embeddings_cache = _load_pickle_file(EMBEDDINGS_PATH) _embeddings_cache = _load_pickle_file(EMBEDDINGS_PATH)
logging.info(f"Embeddings loaded: {_embeddings_cache.shape}") logging.info(f"Embeddings loaded: {_embeddings_cache.shape}")
except Exception as e: except Exception as e:
logging.error(f"Error loading embeddings: {str(e)}") logging.error(f"Error loading embeddings: {str(e)}")
raise raise
return _embeddings_cache return _embeddings_cache
def load_user_data() -> Dict[int, List[int]]: def load_user_data() -> Dict[int, List[int]]:
"""Charge les données utilisateur avec cache.""" """Charge les données utilisateur avec cache."""
global _user_data_cache global _user_data_cache
if _user_data_cache is None: if _user_data_cache is None:
try: try:
clicks_df = _load_csv_file(CLICKS_PATH) clicks_df = _load_csv_file(CLICKS_PATH)
_user_data_cache = _process_clicks_data(clicks_df) _user_data_cache = _process_clicks_data(clicks_df)
logging.info(f"User data loaded: {len(_user_data_cache)} users") logging.info(f"User data loaded: {len(_user_data_cache)} users")
except Exception as e: except Exception as e:
logging.error(f"Error loading user data: {str(e)}") logging.error(f"Error loading user data: {str(e)}")
raise raise
return _user_data_cache return _user_data_cache
def load_articles_metadata() -> pd.DataFrame: def load_articles_metadata() -> pd.DataFrame:
"""Charge les métadonnées des articles avec cache.""" """Charge les métadonnées des articles avec cache."""
global _articles_metadata_cache global _articles_metadata_cache
if _articles_metadata_cache is None: if _articles_metadata_cache is None:
try: try:
_articles_metadata_cache = _load_csv_file(ARTICLES_METADATA_PATH) _articles_metadata_cache = _load_csv_file(ARTICLES_METADATA_PATH)
logging.info(f"Articles metadata loaded: {len(_articles_metadata_cache)} articles") logging.info(f"Articles metadata loaded: {len(_articles_metadata_cache)} articles")
except Exception as e: except Exception as e:
logging.error(f"Error loading articles metadata: {str(e)}") logging.error(f"Error loading articles metadata: {str(e)}")
raise raise
return _articles_metadata_cache 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]: def get_article_category(article_id: int) -> Optional[int]:
"""Récupère la catégorie d'un article.""" """Récupère la catégorie d'un article."""
try: try:
metadata_df = load_articles_metadata() metadata_df = load_articles_metadata()
article_row = metadata_df[metadata_df['article_id'] == article_id] article_row = metadata_df[metadata_df['article_id'] == article_id]
if not article_row.empty: if not article_row.empty:
return int(article_row.iloc[0]['category_id']) return int(article_row.iloc[0]['category_id'])
return None return None
except Exception as e: except Exception as e:
logging.warning(f"Error getting category for article {article_id}: {str(e)}") logging.warning(f"Error getting category for article {article_id}: {str(e)}")
return None return None
def get_articles_categories(article_ids: List[int]) -> Dict[int, Optional[int]]: def get_articles_categories(article_ids: List[int]) -> Dict[int, Optional[int]]:
"""Récupère les catégories de plusieurs articles.""" """Récupère les catégories de plusieurs articles."""
try: try:
metadata_df = load_articles_metadata() metadata_df = load_articles_metadata()
categories = {} categories = {}
for article_id in article_ids: for article_id in article_ids:
article_row = metadata_df[metadata_df['article_id'] == article_id] article_row = metadata_df[metadata_df['article_id'] == article_id]
if not article_row.empty: if not article_row.empty:
categories[article_id] = int(article_row.iloc[0]['category_id']) categories[article_id] = int(article_row.iloc[0]['category_id'])
else: else:
categories[article_id] = None categories[article_id] = None
return categories return categories
except Exception as e: except Exception as e:
logging.warning(f"Error getting categories for articles: {str(e)}") logging.warning(f"Error getting categories for articles: {str(e)}")
return {aid: None for aid in article_ids} 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, ARTICLES_METADATA_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):
logging.error(f"Missing data file: {file_path}") logging.error(f"Missing data file: {file_path}")
return False return False
return True return True
def get_available_users() -> List[int]: def get_available_users() -> List[int]:
"""Retourne la liste de tous les user_ids disponibles.""" """Retourne la liste de tous les user_ids disponibles."""
user_data = load_user_data() user_data = load_user_data()
return list(user_data.keys()) return list(user_data.keys())

View file

@ -1,198 +1,198 @@
import streamlit as st import streamlit as st
import requests import requests
import json import json
import time import time
# Configuration de la page # Configuration de la page
st.set_page_config( st.set_page_config(
page_title="Système de Recommandation", page_title="Système de Recommandation",
page_icon="🎯", page_icon="🎯",
layout="wide" layout="wide"
) )
# Configuration de l'API # Configuration de l'API
API_BASE_URL = "http://localhost:7071/api" API_BASE_URL = "http://localhost:7071/api"
def call_recommendation_api(user_id, base_url=API_BASE_URL): def call_recommendation_api(user_id, base_url=API_BASE_URL):
""" """
Appelle l'API Azure Function pour obtenir des recommandations. Appelle l'API Azure Function pour obtenir des recommandations.
""" """
try: try:
url = f"{base_url}/recommend" url = f"{base_url}/recommend"
params = {"user_id": user_id} params = {"user_id": user_id}
start_time = time.time() start_time = time.time()
response = requests.get(url, params=params, timeout=30) response = requests.get(url, params=params, timeout=30)
response_time = time.time() - start_time response_time = time.time() - start_time
if response.status_code == 200: if response.status_code == 200:
return response.json(), response_time, None return response.json(), response_time, None
else: else:
error_msg = f"Erreur {response.status_code}: {response.text}" error_msg = f"Erreur {response.status_code}: {response.text}"
return None, response_time, error_msg return None, response_time, error_msg
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
return None, None, "Timeout: L'API a mis trop de temps à répondre" return None, None, "Timeout: L'API a mis trop de temps à répondre"
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
return None, None, "Erreur de connexion: Vérifiez que l'Azure Function est démarrée" return None, None, "Erreur de connexion: Vérifiez que l'Azure Function est démarrée"
except Exception as e: except Exception as e:
return None, None, f"Erreur inattendue: {str(e)}" return None, None, f"Erreur inattendue: {str(e)}"
def main(): def main():
# Titre principal # Titre principal
st.title("🎯 Système de Recommandation de Contenu") st.title("🎯 Système de Recommandation de Contenu")
st.markdown("---") st.markdown("---")
# Configuration dans la sidebar # Configuration dans la sidebar
with st.sidebar: with st.sidebar:
st.header("⚙️ Configuration") st.header("⚙️ Configuration")
# URL de l'API # URL de l'API
api_url = st.text_input( api_url = st.text_input(
"URL de l'API", "URL de l'API",
value=API_BASE_URL, value=API_BASE_URL,
help="URL de base de votre Azure Function" help="URL de base de votre Azure Function"
) )
# Test de connexion # Test de connexion
if st.button("🔍 Tester la connexion"): if st.button("🔍 Tester la connexion"):
with st.spinner("Test en cours..."): with st.spinner("Test en cours..."):
try: try:
test_response = requests.get(f"{api_url}/recommend?user_id=0", timeout=5) test_response = requests.get(f"{api_url}/recommend?user_id=0", timeout=5)
if test_response.status_code in [200, 400, 404]: # API répond if test_response.status_code in [200, 400, 404]: # API répond
st.success("✅ API accessible") st.success("✅ API accessible")
else: else:
st.error(f"❌ API répond avec le code {test_response.status_code}") st.error(f"❌ API répond avec le code {test_response.status_code}")
except Exception as e: except Exception as e:
st.error(f"❌ Impossible de joindre l'API: {str(e)}") st.error(f"❌ Impossible de joindre l'API: {str(e)}")
st.markdown("---") st.markdown("---")
st.markdown("**Instructions:**") st.markdown("**Instructions:**")
st.markdown("1. Assurez-vous que `func start` est lancé") st.markdown("1. Assurez-vous que `func start` est lancé")
st.markdown("2. Entrez un ID utilisateur") st.markdown("2. Entrez un ID utilisateur")
st.markdown("3. Cliquez sur 'Obtenir des recommandations'") st.markdown("3. Cliquez sur 'Obtenir des recommandations'")
# Interface principale # Interface principale
col1, col2 = st.columns([1, 2]) col1, col2 = st.columns([1, 2])
with col1: with col1:
st.header("👤 Sélection utilisateur") st.header("👤 Sélection utilisateur")
# Input pour l'ID utilisateur # Input pour l'ID utilisateur
user_id = st.number_input( user_id = st.number_input(
"ID de l'utilisateur", "ID de l'utilisateur",
min_value=0, min_value=0,
max_value=10000, max_value=10000,
value=0, value=0,
step=1, step=1,
help="Entrez l'ID de l'utilisateur pour lequel obtenir des recommandations" help="Entrez l'ID de l'utilisateur pour lequel obtenir des recommandations"
) )
# Bouton pour lancer la recommandation # Bouton pour lancer la recommandation
if st.button("🚀 Obtenir des recommandations", type="primary"): if st.button("🚀 Obtenir des recommandations", type="primary"):
with st.spinner(f"Génération des recommandations pour l'utilisateur {user_id}..."): with st.spinner(f"Génération des recommandations pour l'utilisateur {user_id}..."):
result, response_time, error = call_recommendation_api(user_id, api_url) result, response_time, error = call_recommendation_api(user_id, api_url)
# Stocker les résultats dans la session # Stocker les résultats dans la session
st.session_state['last_result'] = result st.session_state['last_result'] = result
st.session_state['last_response_time'] = response_time st.session_state['last_response_time'] = response_time
st.session_state['last_error'] = error st.session_state['last_error'] = error
st.session_state['last_user_id'] = user_id st.session_state['last_user_id'] = user_id
with col2: with col2:
st.header("📊 Résultats") st.header("📊 Résultats")
# Affichage des résultats # Affichage des résultats
if 'last_result' in st.session_state: if 'last_result' in st.session_state:
if st.session_state['last_error']: if st.session_state['last_error']:
st.error(f"❌ {st.session_state['last_error']}") st.error(f"❌ {st.session_state['last_error']}")
elif st.session_state['last_result']: elif st.session_state['last_result']:
result = st.session_state['last_result'] result = st.session_state['last_result']
# Métriques # Métriques
col_metrics1, col_metrics2 = st.columns(2) col_metrics1, col_metrics2 = st.columns(2)
with col_metrics1: with col_metrics1:
st.metric("👤 Utilisateur", st.session_state['last_user_id']) st.metric("👤 Utilisateur", st.session_state['last_user_id'])
with col_metrics2: with col_metrics2:
if st.session_state['last_response_time']: if st.session_state['last_response_time']:
st.metric("⏱️ Temps de réponse", f"{st.session_state['last_response_time']:.2f}s") st.metric("⏱️ Temps de réponse", f"{st.session_state['last_response_time']:.2f}s")
# Recommandations # Recommandations
if 'recommendations' in result: if 'recommendations' in result:
st.subheader("🎯 Articles recommandés") st.subheader("🎯 Articles recommandés")
recommendations = result['recommendations'] recommendations = result['recommendations']
# Vérifier si on a le format enrichi (avec scores et catégories) # Vérifier si on a le format enrichi (avec scores et catégories)
if recommendations and isinstance(recommendations[0], dict) and 'similarity_score_percent' in recommendations[0]: if recommendations and isinstance(recommendations[0], dict) and 'similarity_score_percent' in recommendations[0]:
# Format enrichi - affichage en tableau # Format enrichi - affichage en tableau
import pandas as pd import pandas as pd
df_data = [] df_data = []
for i, rec in enumerate(recommendations, 1): for i, rec in enumerate(recommendations, 1):
df_data.append({ df_data.append({
'Rang': i, 'Rang': i,
'Article ID': rec['article_id'], 'Article ID': rec['article_id'],
'Similarité (%)': f"{rec['similarity_score_percent']:.1f}%", 'Similarité (%)': f"{rec['similarity_score_percent']:.1f}%",
'Catégorie': rec.get('category_id', 'N/A') 'Catégorie': rec.get('category_id', 'N/A')
}) })
df = pd.DataFrame(df_data) df = pd.DataFrame(df_data)
st.dataframe(df, hide_index=True, use_container_width=True) st.dataframe(df, hide_index=True, use_container_width=True)
else: else:
# Format simple - affichage basique pour compatibilité # Format simple - affichage basique pour compatibilité
for i, article_id in enumerate(recommendations, 1): for i, article_id in enumerate(recommendations, 1):
if isinstance(article_id, dict): if isinstance(article_id, dict):
article_id = article_id.get('article_id', article_id) article_id = article_id.get('article_id', article_id)
st.write(f"**{i}.** 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"):
st.json(result) st.json(result)
else: else:
st.warning("⚠️ Aucune recommandation trouvée dans la réponse") st.warning("⚠️ Aucune recommandation trouvée dans la réponse")
else: else:
st.info("👆 Sélectionnez un utilisateur et cliquez sur 'Obtenir des recommandations' pour commencer") st.info("👆 Sélectionnez un utilisateur et cliquez sur 'Obtenir des recommandations' pour commencer")
# Section de test avancé # Section de test avancé
st.markdown("---") st.markdown("---")
st.header("🧪 Tests avancés") st.header("🧪 Tests avancés")
col_test1, col_test2 = st.columns(2) col_test1, col_test2 = st.columns(2)
with col_test1: with col_test1:
st.subheader("📈 Test de performance") st.subheader("📈 Test de performance")
if st.button("Tester avec plusieurs utilisateurs"): if st.button("Tester avec plusieurs utilisateurs"):
test_users = [0, 1, 2, 5, 10] test_users = [0, 1, 2, 5, 10]
progress_bar = st.progress(0) progress_bar = st.progress(0)
results = [] results = []
for i, uid in enumerate(test_users): for i, uid in enumerate(test_users):
result, resp_time, error = call_recommendation_api(uid, api_url) result, resp_time, error = call_recommendation_api(uid, api_url)
results.append({ results.append({
'user_id': uid, 'user_id': uid,
'success': error is None, 'success': error is None,
'response_time': resp_time, 'response_time': resp_time,
'error': error 'error': error
}) })
progress_bar.progress((i + 1) / len(test_users)) progress_bar.progress((i + 1) / len(test_users))
# Affichage des résultats # Affichage des résultats
success_count = sum(1 for r in results if r['success']) success_count = sum(1 for r in results if r['success'])
avg_time = sum(r['response_time'] for r in results if r['response_time']) / len([r for r in results if r['response_time']]) avg_time = sum(r['response_time'] for r in results if r['response_time']) / len([r for r in results if r['response_time']])
st.success(f"✅ {success_count}/{len(test_users)} appels réussis") st.success(f"✅ {success_count}/{len(test_users)} appels réussis")
st.info(f"⏱️ Temps moyen: {avg_time:.2f}s") st.info(f"⏱️ Temps moyen: {avg_time:.2f}s")
with col_test2: with col_test2:
st.subheader("🔍 Test d'erreur") st.subheader("🔍 Test d'erreur")
if st.button("Tester utilisateur inexistant"): if st.button("Tester utilisateur inexistant"):
with st.spinner("Test en cours..."): with st.spinner("Test en cours..."):
result, resp_time, error = call_recommendation_api(99999, api_url) result, resp_time, error = call_recommendation_api(99999, api_url)
if error: if error:
st.warning(f"Erreur attendue: {error}") st.warning(f"Erreur attendue: {error}")
elif result: elif result:
st.info("Réponse reçue (comportement inattendu)") st.info("Réponse reçue (comportement inattendu)")
st.json(result) st.json(result)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -1,9 +1,9 @@
# Azure Functions # Azure Functions
azure-functions azure-functions
numpy numpy
pandas pandas
scikit-learn scikit-learn
# Interface Streamlit # Interface Streamlit
streamlit>=1.28.0 streamlit>=1.28.0
requests>=2.25.0 requests>=2.25.0