feat: add depense tagging
This commit is contained in:
1
src/plesna_gerance/services/__init__.py
Normal file
1
src/plesna_gerance/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Services for business logic."""
|
||||
165
src/plesna_gerance/services/tag_predictor.py
Normal file
165
src/plesna_gerance/services/tag_predictor.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Service de prédiction de tags pour les dépenses basé sur l'historique."""
|
||||
|
||||
from typing import Optional
|
||||
from collections import Counter
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database.models import Depense, Tag
|
||||
|
||||
|
||||
class TagPrediction:
|
||||
"""Représente une prédiction de tag."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tag_id: Optional[int],
|
||||
tag_name: Optional[str],
|
||||
confidence: float,
|
||||
reason: str,
|
||||
):
|
||||
self.tag_id = tag_id
|
||||
self.tag_name = tag_name
|
||||
self.confidence = confidence
|
||||
self.reason = reason
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convertit en dictionnaire."""
|
||||
return {
|
||||
"tag_id": self.tag_id,
|
||||
"tag_name": self.tag_name,
|
||||
"confidence": self.confidence,
|
||||
"reason": self.reason,
|
||||
}
|
||||
|
||||
|
||||
class TagPredictor:
|
||||
"""Service de prédiction de tags basé sur l'historique."""
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def predict_for_depense(self, depense_data: dict) -> TagPrediction:
|
||||
"""Prédit le tag pour une dépense donnée.
|
||||
|
||||
Args:
|
||||
depense_data: Dict contenant 'fournisseur', 'sous_categorie', 'description'
|
||||
|
||||
Returns:
|
||||
TagPrediction avec le tag suggéré et la confiance
|
||||
"""
|
||||
fournisseur = (depense_data.get("fournisseur") or "").strip()
|
||||
sous_categorie = (depense_data.get("sous_categorie") or "").strip()
|
||||
|
||||
# Stratégie 1: Recherche par fournisseur exact
|
||||
if fournisseur:
|
||||
prediction = self._predict_by_fournisseur(fournisseur)
|
||||
if prediction:
|
||||
return prediction
|
||||
|
||||
# Stratégie 2: Recherche par sous-catégorie
|
||||
if sous_categorie:
|
||||
prediction = self._predict_by_sous_categorie(sous_categorie)
|
||||
if prediction:
|
||||
return prediction
|
||||
|
||||
# Aucune prédiction trouvée
|
||||
return TagPrediction(
|
||||
tag_id=None,
|
||||
tag_name=None,
|
||||
confidence=0.0,
|
||||
reason="Aucun historique trouvé",
|
||||
)
|
||||
|
||||
def _predict_by_fournisseur(self, fournisseur: str) -> Optional[TagPrediction]:
|
||||
"""Prédit le tag basé sur le fournisseur.
|
||||
|
||||
Retourne le tag le plus fréquemment utilisé pour ce fournisseur.
|
||||
"""
|
||||
# Requête pour trouver toutes les dépenses avec ce fournisseur et un tag
|
||||
stmt = (
|
||||
select(Depense.tag_id, Tag.nom, func.count(Depense.id))
|
||||
.join(Tag, Depense.tag_id == Tag.id)
|
||||
.where(
|
||||
Depense.fournisseur.ilike(f"%{fournisseur}%"),
|
||||
Depense.tag_id.is_not(None),
|
||||
)
|
||||
.group_by(Depense.tag_id, Tag.nom)
|
||||
.order_by(func.count(Depense.id).desc())
|
||||
)
|
||||
|
||||
result = self.session.execute(stmt).first()
|
||||
|
||||
if result:
|
||||
tag_id, tag_name, count = result
|
||||
total_stmt = select(func.count(Depense.id)).where(
|
||||
Depense.fournisseur.ilike(f"%{fournisseur}%")
|
||||
)
|
||||
total = self.session.execute(total_stmt).scalar() or 0
|
||||
|
||||
confidence = (count / total * 100) if total > 0 else 0
|
||||
|
||||
return TagPrediction(
|
||||
tag_id=tag_id,
|
||||
tag_name=tag_name,
|
||||
confidence=round(confidence, 1),
|
||||
reason=f"Basé sur {count} occurrence(s) pour ce fournisseur",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _predict_by_sous_categorie(
|
||||
self, sous_categorie: str
|
||||
) -> Optional[TagPrediction]:
|
||||
"""Prédit le tag basé sur la sous-catégorie.
|
||||
|
||||
Retourne le tag le plus fréquemment utilisé pour cette sous-catégorie.
|
||||
"""
|
||||
# Requête pour trouver toutes les dépenses avec cette sous-catégorie et un tag
|
||||
stmt = (
|
||||
select(Depense.tag_id, Tag.nom, func.count(Depense.id))
|
||||
.join(Tag, Depense.tag_id == Tag.id)
|
||||
.where(
|
||||
Depense.sous_categorie.ilike(f"%{sous_categorie}%"),
|
||||
Depense.tag_id.is_not(None),
|
||||
)
|
||||
.group_by(Depense.tag_id, Tag.nom)
|
||||
.order_by(func.count(Depense.id).desc())
|
||||
)
|
||||
|
||||
result = self.session.execute(stmt).first()
|
||||
|
||||
if result:
|
||||
tag_id, tag_name, count = result
|
||||
total_stmt = select(func.count(Depense.id)).where(
|
||||
Depense.sous_categorie.ilike(f"%{sous_categorie}%")
|
||||
)
|
||||
total = self.session.execute(total_stmt).scalar() or 0
|
||||
|
||||
confidence = (count / total * 100) if total > 0 else 0
|
||||
|
||||
return TagPrediction(
|
||||
tag_id=tag_id,
|
||||
tag_name=tag_name,
|
||||
confidence=round(confidence, 1),
|
||||
reason=f"Basé sur {count} occurrence(s) pour cette sous-catégorie",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def predict_batch(self, depenses_data: list[dict]) -> list[TagPrediction]:
|
||||
"""Prédit les tags pour une liste de dépenses.
|
||||
|
||||
Args:
|
||||
depenses_data: Liste de dicts contenant les infos des dépenses
|
||||
|
||||
Returns:
|
||||
Liste de TagPrediction dans le même ordre
|
||||
"""
|
||||
predictions = []
|
||||
for depense in depenses_data:
|
||||
prediction = self.predict_for_depense(depense)
|
||||
predictions.append(prediction)
|
||||
|
||||
return predictions
|
||||
Reference in New Issue
Block a user