Initial commit of scripts
This commit is contained in:
parent
250af3ebf3
commit
799015be44
9 changed files with 838 additions and 838 deletions
450
README.MD
450
README.MD
|
|
@ -1,226 +1,226 @@
|
|||
# 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.
|
||||
|
||||
|
||||
## Livrables du Projet
|
||||
1. **Application fonctionnelle** : Streamlit + Azure Functions déployée
|
||||
2. **Code source versionné** : Repository GitHub complet
|
||||
3. **Présentation technique** : Support PDF (docs/presentation.pdf)
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
## Analyse et Développement
|
||||
|
||||
### Notebook Jupyter
|
||||
- **Exploration des données** : Analyse du dataset Kaggle Globo.com
|
||||
- **Implémentation des algorithmes** : Content-Based et Collaborative Filtering
|
||||
- **Comparaison des approches** : Évaluation et choix technique
|
||||
- **Optimisation ACP** : Réduction dimensionnelle 250D → 50D
|
||||
|
||||
Localisation : `notebook/p10_notebook.ipynb`
|
||||
|
||||
## 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 synthétique 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
|
||||
├── notebook/
|
||||
│ └── p10_notebook.ipynb # Analyse données + développement modèles
|
||||
├── docs/
|
||||
│ ├── presentation.pdf # Support de présentation (15-25 slides)
|
||||
│ └── illustrations/ # Schémas d'architecture et captures
|
||||
├── 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
|
||||
|
||||
# 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.
|
||||
|
||||
|
||||
## Livrables du Projet
|
||||
1. **Application fonctionnelle** : Streamlit + Azure Functions déployée
|
||||
2. **Code source versionné** : Repository GitHub complet
|
||||
3. **Présentation technique** : Support PDF (docs/presentation.pdf)
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
## Analyse et Développement
|
||||
|
||||
### Notebook Jupyter
|
||||
- **Exploration des données** : Analyse du dataset Kaggle Globo.com
|
||||
- **Implémentation des algorithmes** : Content-Based et Collaborative Filtering
|
||||
- **Comparaison des approches** : Évaluation et choix technique
|
||||
- **Optimisation ACP** : Réduction dimensionnelle 250D → 50D
|
||||
|
||||
Localisation : `notebook/p10_notebook.ipynb`
|
||||
|
||||
## 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 synthétique 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
|
||||
├── notebook/
|
||||
│ └── p10_notebook.ipynb # Analyse données + développement modèles
|
||||
├── docs/
|
||||
│ ├── presentation.pdf # Support de présentation (15-25 slides)
|
||||
│ └── illustrations/ # Schémas d'architecture et captures
|
||||
├── 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.
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
.git*
|
||||
.vscode
|
||||
__azurite_db*__.json
|
||||
__blobstorage__
|
||||
__queuestorage__
|
||||
local.settings.json
|
||||
test
|
||||
.venv
|
||||
.git*
|
||||
.vscode
|
||||
__azurite_db*__.json
|
||||
__blobstorage__
|
||||
__queuestorage__
|
||||
local.settings.json
|
||||
test
|
||||
.venv
|
||||
test_local.py
|
||||
2
azure-app/.gitignore
vendored
2
azure-app/.gitignore
vendored
|
|
@ -131,5 +131,5 @@ local.settings.json
|
|||
# Azurite artifacts
|
||||
__blobstorage__
|
||||
__queuestorage__
|
||||
__azurite_db*__.json
|
||||
__azurite_db*__.json
|
||||
.python_packages
|
||||
|
|
@ -1,85 +1,85 @@
|
|||
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, get_articles_categories
|
||||
from utils.content_based import get_recommendations_with_scores
|
||||
|
||||
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
|
||||
|
||||
@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 avec scores
|
||||
recommendations = get_recommendations_with_scores(user_id)
|
||||
|
||||
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(
|
||||
json.dumps({
|
||||
"user_id": user_id,
|
||||
"recommendations": enriched_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"
|
||||
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, get_articles_categories
|
||||
from utils.content_based import get_recommendations_with_scores
|
||||
|
||||
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
|
||||
|
||||
@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 avec scores
|
||||
recommendations = get_recommendations_with_scores(user_id)
|
||||
|
||||
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(
|
||||
json.dumps({
|
||||
"user_id": user_id,
|
||||
"recommendations": enriched_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"
|
||||
)
|
||||
|
|
@ -1,92 +1,92 @@
|
|||
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__":
|
||||
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()
|
||||
|
|
@ -1,95 +1,95 @@
|
|||
import numpy as np
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
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_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."""
|
||||
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
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]:
|
||||
"""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é (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:
|
||||
user_id: ID de l'utilisateur
|
||||
n_recommendations: Nombre de recommandations à retourner
|
||||
|
||||
Returns:
|
||||
Liste des recommandations avec article_id et similarity_score 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_with_scores(candidates, n_recommendations)
|
||||
|
||||
logging.info(f"Generated {len(recommendations)} recommendations with scores for user {user_id}")
|
||||
return recommendations
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error generating recommendations for user {user_id}: {str(e)}")
|
||||
import numpy as np
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
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_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."""
|
||||
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
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]:
|
||||
"""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é (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:
|
||||
user_id: ID de l'utilisateur
|
||||
n_recommendations: Nombre de recommandations à retourner
|
||||
|
||||
Returns:
|
||||
Liste des recommandations avec article_id et similarity_score 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_with_scores(candidates, n_recommendations)
|
||||
|
||||
logging.info(f"Generated {len(recommendations)} recommendations with scores for user {user_id}")
|
||||
return recommendations
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error generating recommendations for user {user_id}: {str(e)}")
|
||||
return None
|
||||
|
|
@ -1,131 +1,131 @@
|
|||
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')
|
||||
ARTICLES_METADATA_PATH = os.path.join(DATA_DIR, 'articles_metadata.csv')
|
||||
|
||||
# Cache global
|
||||
_embeddings_cache: Optional[np.ndarray] = 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:
|
||||
"""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 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]]:
|
||||
"""Récupère les articles d'un utilisateur."""
|
||||
user_data = load_user_data()
|
||||
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:
|
||||
"""Vérifie que tous les fichiers de données existent."""
|
||||
files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH, ARTICLES_METADATA_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()
|
||||
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')
|
||||
ARTICLES_METADATA_PATH = os.path.join(DATA_DIR, 'articles_metadata.csv')
|
||||
|
||||
# Cache global
|
||||
_embeddings_cache: Optional[np.ndarray] = 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:
|
||||
"""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 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]]:
|
||||
"""Récupère les articles d'un utilisateur."""
|
||||
user_data = load_user_data()
|
||||
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:
|
||||
"""Vérifie que tous les fichiers de données existent."""
|
||||
files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH, ARTICLES_METADATA_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())
|
||||
394
streamlit_app.py
394
streamlit_app.py
|
|
@ -1,198 +1,198 @@
|
|||
import streamlit as st
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
# Configuration de la page
|
||||
st.set_page_config(
|
||||
page_title="Système de Recommandation",
|
||||
page_icon="🎯",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# Configuration de l'API
|
||||
API_BASE_URL = "http://localhost:7071/api"
|
||||
|
||||
def call_recommendation_api(user_id, base_url=API_BASE_URL):
|
||||
"""
|
||||
Appelle l'API Azure Function pour obtenir des recommandations.
|
||||
"""
|
||||
try:
|
||||
url = f"{base_url}/recommend"
|
||||
params = {"user_id": user_id}
|
||||
|
||||
start_time = time.time()
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json(), response_time, None
|
||||
else:
|
||||
error_msg = f"Erreur {response.status_code}: {response.text}"
|
||||
return None, response_time, error_msg
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return None, None, "Timeout: L'API a mis trop de temps à répondre"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return None, None, "Erreur de connexion: Vérifiez que l'Azure Function est démarrée"
|
||||
except Exception as e:
|
||||
return None, None, f"Erreur inattendue: {str(e)}"
|
||||
|
||||
def main():
|
||||
# Titre principal
|
||||
st.title("🎯 Système de Recommandation de Contenu")
|
||||
st.markdown("---")
|
||||
|
||||
# Configuration dans la sidebar
|
||||
with st.sidebar:
|
||||
st.header("⚙️ Configuration")
|
||||
|
||||
# URL de l'API
|
||||
api_url = st.text_input(
|
||||
"URL de l'API",
|
||||
value=API_BASE_URL,
|
||||
help="URL de base de votre Azure Function"
|
||||
)
|
||||
|
||||
# Test de connexion
|
||||
if st.button("🔍 Tester la connexion"):
|
||||
with st.spinner("Test en cours..."):
|
||||
try:
|
||||
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
|
||||
st.success("✅ API accessible")
|
||||
else:
|
||||
st.error(f"❌ API répond avec le code {test_response.status_code}")
|
||||
except Exception as e:
|
||||
st.error(f"❌ Impossible de joindre l'API: {str(e)}")
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("**Instructions:**")
|
||||
st.markdown("1. Assurez-vous que `func start` est lancé")
|
||||
st.markdown("2. Entrez un ID utilisateur")
|
||||
st.markdown("3. Cliquez sur 'Obtenir des recommandations'")
|
||||
|
||||
# Interface principale
|
||||
col1, col2 = st.columns([1, 2])
|
||||
|
||||
with col1:
|
||||
st.header("👤 Sélection utilisateur")
|
||||
|
||||
# Input pour l'ID utilisateur
|
||||
user_id = st.number_input(
|
||||
"ID de l'utilisateur",
|
||||
min_value=0,
|
||||
max_value=10000,
|
||||
value=0,
|
||||
step=1,
|
||||
help="Entrez l'ID de l'utilisateur pour lequel obtenir des recommandations"
|
||||
)
|
||||
|
||||
# Bouton pour lancer la recommandation
|
||||
if st.button("🚀 Obtenir des recommandations", type="primary"):
|
||||
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)
|
||||
|
||||
# Stocker les résultats dans la session
|
||||
st.session_state['last_result'] = result
|
||||
st.session_state['last_response_time'] = response_time
|
||||
st.session_state['last_error'] = error
|
||||
st.session_state['last_user_id'] = user_id
|
||||
|
||||
with col2:
|
||||
st.header("📊 Résultats")
|
||||
|
||||
# Affichage des résultats
|
||||
if 'last_result' in st.session_state:
|
||||
if st.session_state['last_error']:
|
||||
st.error(f"❌ {st.session_state['last_error']}")
|
||||
elif st.session_state['last_result']:
|
||||
result = st.session_state['last_result']
|
||||
|
||||
# Métriques
|
||||
col_metrics1, col_metrics2 = st.columns(2)
|
||||
with col_metrics1:
|
||||
st.metric("👤 Utilisateur", st.session_state['last_user_id'])
|
||||
with col_metrics2:
|
||||
if st.session_state['last_response_time']:
|
||||
st.metric("⏱️ Temps de réponse", f"{st.session_state['last_response_time']:.2f}s")
|
||||
|
||||
# Recommandations
|
||||
if 'recommendations' in result:
|
||||
st.subheader("🎯 Articles recommandés")
|
||||
recommendations = result['recommendations']
|
||||
|
||||
# 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]:
|
||||
# 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)
|
||||
with st.expander("📝 Réponse JSON complète"):
|
||||
st.json(result)
|
||||
else:
|
||||
st.warning("⚠️ Aucune recommandation trouvée dans la réponse")
|
||||
else:
|
||||
st.info("👆 Sélectionnez un utilisateur et cliquez sur 'Obtenir des recommandations' pour commencer")
|
||||
|
||||
# Section de test avancé
|
||||
st.markdown("---")
|
||||
st.header("🧪 Tests avancés")
|
||||
|
||||
col_test1, col_test2 = st.columns(2)
|
||||
|
||||
with col_test1:
|
||||
st.subheader("📈 Test de performance")
|
||||
if st.button("Tester avec plusieurs utilisateurs"):
|
||||
test_users = [0, 1, 2, 5, 10]
|
||||
progress_bar = st.progress(0)
|
||||
results = []
|
||||
|
||||
for i, uid in enumerate(test_users):
|
||||
result, resp_time, error = call_recommendation_api(uid, api_url)
|
||||
results.append({
|
||||
'user_id': uid,
|
||||
'success': error is None,
|
||||
'response_time': resp_time,
|
||||
'error': error
|
||||
})
|
||||
progress_bar.progress((i + 1) / len(test_users))
|
||||
|
||||
# Affichage des résultats
|
||||
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']])
|
||||
|
||||
st.success(f"✅ {success_count}/{len(test_users)} appels réussis")
|
||||
st.info(f"⏱️ Temps moyen: {avg_time:.2f}s")
|
||||
|
||||
with col_test2:
|
||||
st.subheader("🔍 Test d'erreur")
|
||||
if st.button("Tester utilisateur inexistant"):
|
||||
with st.spinner("Test en cours..."):
|
||||
result, resp_time, error = call_recommendation_api(99999, api_url)
|
||||
if error:
|
||||
st.warning(f"Erreur attendue: {error}")
|
||||
elif result:
|
||||
st.info("Réponse reçue (comportement inattendu)")
|
||||
st.json(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import streamlit as st
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
# Configuration de la page
|
||||
st.set_page_config(
|
||||
page_title="Système de Recommandation",
|
||||
page_icon="🎯",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# Configuration de l'API
|
||||
API_BASE_URL = "http://localhost:7071/api"
|
||||
|
||||
def call_recommendation_api(user_id, base_url=API_BASE_URL):
|
||||
"""
|
||||
Appelle l'API Azure Function pour obtenir des recommandations.
|
||||
"""
|
||||
try:
|
||||
url = f"{base_url}/recommend"
|
||||
params = {"user_id": user_id}
|
||||
|
||||
start_time = time.time()
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json(), response_time, None
|
||||
else:
|
||||
error_msg = f"Erreur {response.status_code}: {response.text}"
|
||||
return None, response_time, error_msg
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return None, None, "Timeout: L'API a mis trop de temps à répondre"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return None, None, "Erreur de connexion: Vérifiez que l'Azure Function est démarrée"
|
||||
except Exception as e:
|
||||
return None, None, f"Erreur inattendue: {str(e)}"
|
||||
|
||||
def main():
|
||||
# Titre principal
|
||||
st.title("🎯 Système de Recommandation de Contenu")
|
||||
st.markdown("---")
|
||||
|
||||
# Configuration dans la sidebar
|
||||
with st.sidebar:
|
||||
st.header("⚙️ Configuration")
|
||||
|
||||
# URL de l'API
|
||||
api_url = st.text_input(
|
||||
"URL de l'API",
|
||||
value=API_BASE_URL,
|
||||
help="URL de base de votre Azure Function"
|
||||
)
|
||||
|
||||
# Test de connexion
|
||||
if st.button("🔍 Tester la connexion"):
|
||||
with st.spinner("Test en cours..."):
|
||||
try:
|
||||
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
|
||||
st.success("✅ API accessible")
|
||||
else:
|
||||
st.error(f"❌ API répond avec le code {test_response.status_code}")
|
||||
except Exception as e:
|
||||
st.error(f"❌ Impossible de joindre l'API: {str(e)}")
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("**Instructions:**")
|
||||
st.markdown("1. Assurez-vous que `func start` est lancé")
|
||||
st.markdown("2. Entrez un ID utilisateur")
|
||||
st.markdown("3. Cliquez sur 'Obtenir des recommandations'")
|
||||
|
||||
# Interface principale
|
||||
col1, col2 = st.columns([1, 2])
|
||||
|
||||
with col1:
|
||||
st.header("👤 Sélection utilisateur")
|
||||
|
||||
# Input pour l'ID utilisateur
|
||||
user_id = st.number_input(
|
||||
"ID de l'utilisateur",
|
||||
min_value=0,
|
||||
max_value=10000,
|
||||
value=0,
|
||||
step=1,
|
||||
help="Entrez l'ID de l'utilisateur pour lequel obtenir des recommandations"
|
||||
)
|
||||
|
||||
# Bouton pour lancer la recommandation
|
||||
if st.button("🚀 Obtenir des recommandations", type="primary"):
|
||||
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)
|
||||
|
||||
# Stocker les résultats dans la session
|
||||
st.session_state['last_result'] = result
|
||||
st.session_state['last_response_time'] = response_time
|
||||
st.session_state['last_error'] = error
|
||||
st.session_state['last_user_id'] = user_id
|
||||
|
||||
with col2:
|
||||
st.header("📊 Résultats")
|
||||
|
||||
# Affichage des résultats
|
||||
if 'last_result' in st.session_state:
|
||||
if st.session_state['last_error']:
|
||||
st.error(f"❌ {st.session_state['last_error']}")
|
||||
elif st.session_state['last_result']:
|
||||
result = st.session_state['last_result']
|
||||
|
||||
# Métriques
|
||||
col_metrics1, col_metrics2 = st.columns(2)
|
||||
with col_metrics1:
|
||||
st.metric("👤 Utilisateur", st.session_state['last_user_id'])
|
||||
with col_metrics2:
|
||||
if st.session_state['last_response_time']:
|
||||
st.metric("⏱️ Temps de réponse", f"{st.session_state['last_response_time']:.2f}s")
|
||||
|
||||
# Recommandations
|
||||
if 'recommendations' in result:
|
||||
st.subheader("🎯 Articles recommandés")
|
||||
recommendations = result['recommendations']
|
||||
|
||||
# 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]:
|
||||
# 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)
|
||||
with st.expander("📝 Réponse JSON complète"):
|
||||
st.json(result)
|
||||
else:
|
||||
st.warning("⚠️ Aucune recommandation trouvée dans la réponse")
|
||||
else:
|
||||
st.info("👆 Sélectionnez un utilisateur et cliquez sur 'Obtenir des recommandations' pour commencer")
|
||||
|
||||
# Section de test avancé
|
||||
st.markdown("---")
|
||||
st.header("🧪 Tests avancés")
|
||||
|
||||
col_test1, col_test2 = st.columns(2)
|
||||
|
||||
with col_test1:
|
||||
st.subheader("📈 Test de performance")
|
||||
if st.button("Tester avec plusieurs utilisateurs"):
|
||||
test_users = [0, 1, 2, 5, 10]
|
||||
progress_bar = st.progress(0)
|
||||
results = []
|
||||
|
||||
for i, uid in enumerate(test_users):
|
||||
result, resp_time, error = call_recommendation_api(uid, api_url)
|
||||
results.append({
|
||||
'user_id': uid,
|
||||
'success': error is None,
|
||||
'response_time': resp_time,
|
||||
'error': error
|
||||
})
|
||||
progress_bar.progress((i + 1) / len(test_users))
|
||||
|
||||
# Affichage des résultats
|
||||
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']])
|
||||
|
||||
st.success(f"✅ {success_count}/{len(test_users)} appels réussis")
|
||||
st.info(f"⏱️ Temps moyen: {avg_time:.2f}s")
|
||||
|
||||
with col_test2:
|
||||
st.subheader("🔍 Test d'erreur")
|
||||
if st.button("Tester utilisateur inexistant"):
|
||||
with st.spinner("Test en cours..."):
|
||||
result, resp_time, error = call_recommendation_api(99999, api_url)
|
||||
if error:
|
||||
st.warning(f"Erreur attendue: {error}")
|
||||
elif result:
|
||||
st.info("Réponse reçue (comportement inattendu)")
|
||||
st.json(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# Azure Functions
|
||||
azure-functions
|
||||
numpy
|
||||
pandas
|
||||
scikit-learn
|
||||
|
||||
# Interface Streamlit
|
||||
streamlit>=1.28.0
|
||||
# Azure Functions
|
||||
azure-functions
|
||||
numpy
|
||||
pandas
|
||||
scikit-learn
|
||||
|
||||
# Interface Streamlit
|
||||
streamlit>=1.28.0
|
||||
requests>=2.25.0
|
||||
Loading…
Add table
Reference in a new issue