Elkristobal59 commited on
Commit
3ddc50c
·
0 Parent(s):
Files changed (9) hide show
  1. .gitattributes +36 -0
  2. Dockerfile +22 -0
  3. README.md +10 -0
  4. api.py +33 -0
  5. app.py +192 -0
  6. logo_gdm.png +0 -0
  7. requirements.txt +18 -0
  8. setup.sh +10 -0
  9. train_V3.py +159 -0
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.plk filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # On ajoute 'curl' qui est parfois nécessaire pour les téléchargements de modèles
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ git \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # On upgrade pip AVANT d'installer le reste
13
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel
14
+
15
+ COPY . /app
16
+
17
+ # On installe le requirements
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: GDM Aide RUN V2
3
+ emoji: 📊
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
api.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from pydantic import BaseModel
3
+ from app import predire_ticket # On importe ta fonction depuis app.py
4
+
5
+ # Initialisation de l'API
6
+ app = FastAPI(title="GDM Ticket Classifier API")
7
+
8
+ # Définition du format de donnée attendu (JSON)
9
+ class TicketInput(BaseModel):
10
+ objet: str
11
+ description: str
12
+
13
+ @app.get("/")
14
+ def home():
15
+ return {"message": "API GDM opérationnelle. Utilisez l'endpoint /predict en POST."}
16
+
17
+ @app.post("/predict")
18
+ def api_predict(ticket: TicketInput):
19
+ # Sécurité : Si le modèle n'est pas chargé, on renvoie une erreur propre
20
+ from app import MODELE, MTTR_MOYENS
21
+ if MODELE is None:
22
+ return {"error": "Le modèle n'est pas chargé. Vérifiez les fichiers .pkl"}
23
+
24
+ obj = str(ticket.objet)
25
+ desc = str(ticket.description)
26
+
27
+ categorie, mttr, confiance = predire_ticket(obj, desc)
28
+
29
+ return {
30
+ "pole_expertise": str(categorie),
31
+ "temps_resolution_estime_h": float(mttr) if mttr is not None else 0.0,
32
+ "score_confiance": round(float(confiance), 4)
33
+ }
app.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pickle
3
+ import pandas as pd
4
+ import numpy as np
5
+ import torch
6
+ import os
7
+ import io
8
+ from transformers import AutoTokenizer, AutoModel
9
+ from sklearn.base import BaseEstimator, TransformerMixin
10
+
11
+ # --- 0. DÉFINITION DE LA CLASSE CUSTOM (Indispensable pour charger le PKL) ---
12
+ class LLMFeatureExtractor(BaseEstimator, TransformerMixin):
13
+ def __init__(self, model_name='distilbert-base-multilingual-cased', max_length=128):
14
+ self.model_name = model_name
15
+ self.max_length = max_length
16
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
17
+ self.model = AutoModel.from_pretrained(model_name).to("cpu")
18
+
19
+ def fit(self, X, y=None):
20
+ return self
21
+
22
+ def transform(self, X):
23
+ m_len = getattr(self, 'max_length', 128)
24
+ texts = X.tolist() if hasattr(X, 'tolist') else list(X)
25
+ texts = [str(t)[:1000] for t in texts]
26
+ inputs = self.tokenizer(
27
+ texts,
28
+ padding=True,
29
+ truncation=True,
30
+ max_length=m_len,
31
+ return_tensors="pt"
32
+ ).to("cpu")
33
+ with torch.no_grad():
34
+ outputs = self.model(**inputs)
35
+ return outputs.last_hidden_state[:, 0, :].detach().numpy()
36
+
37
+ # Sécurité pour le désarchivage du modèle sur CPU
38
+ class CPU_Unpickler(pickle.Unpickler):
39
+ def find_class(self, module, name):
40
+ # Force Python à chercher LLMFeatureExtractor dans le module app
41
+ if name == 'LLMFeatureExtractor':
42
+ import app
43
+ return app.LLMFeatureExtractor
44
+ if module == 'torch.storage' and name == '_load_from_bytes':
45
+ return lambda b: torch.load(io.BytesIO(b), map_location='cpu')
46
+ return super().find_class(module, name)
47
+
48
+ # --- 1. CHARGEMENT DYNAMIQUE ---
49
+ @st.cache_resource
50
+ def charger_modele_et_data():
51
+ # Chemins robustes pour environnement local et cloud
52
+ base_path = os.path.dirname(__file__)
53
+ MODELE_PATH = os.path.join(base_path, 'modele_classification_taln_llm.pkl')
54
+ MTTR_PATH = os.path.join(base_path, 'mttr_moyennes.pkl')
55
+
56
+ try:
57
+ with open(MODELE_PATH, 'rb') as file:
58
+ modele = CPU_Unpickler(file).load()
59
+
60
+ mttr_moyens = {}
61
+ if os.path.exists(MTTR_PATH):
62
+ with open(MTTR_PATH, 'rb') as f:
63
+ mttr_moyens = pickle.load(f)
64
+
65
+ return modele, mttr_moyens
66
+ except Exception as e:
67
+ print(f"Erreur de chargement : {e}")
68
+ return None, None
69
+
70
+ # Chargement global pour être accessible par api.py
71
+ MODELE, MTTR_MOYENS = charger_modele_et_data()
72
+
73
+ # --- 2. FONCTION DE PRÉDICTION ---
74
+ def predire_ticket(objet: str, description: str):
75
+ texte_complet_saisi = str(objet).strip() + " " + str(description).strip()
76
+ if not texte_complet_saisi.strip():
77
+ return "Saisie vide", 0.0, 0.0
78
+
79
+ X_inference = pd.Series([texte_complet_saisi])
80
+
81
+ try:
82
+ probabilites = MODELE.predict_proba(X_inference)[0]
83
+ confiance = np.max(probabilites)
84
+ prediction_brute = MODELE.classes_[np.argmax(probabilites)]
85
+ except Exception as e:
86
+ prediction_brute = "Erreur"
87
+ confiance = 0.0
88
+
89
+ # SEUIL DE CONFIANCE GDM
90
+ SEUIL_CRITIQUE = 0.65
91
+ if confiance < SEUIL_CRITIQUE or str(prediction_brute).upper() == "AUTRES":
92
+ categorie_finale = "À qualifier (Hors catalogue)"
93
+ else:
94
+ categorie_finale = prediction_brute
95
+
96
+ mttr_predit = MTTR_MOYENS.get(prediction_brute, 0.0)
97
+ return categorie_finale, mttr_predit, confiance
98
+
99
+ # --- 3. INTERFACE STREAMLIT ---
100
+ # Cette partie ne s'exécute QUE si on lance 'streamlit run app.py'
101
+ if __name__ == "__main__":
102
+ st.set_page_config(page_title="GDM Aide au RUN V4", layout="wide", page_icon="👗")
103
+
104
+ st.markdown("""
105
+ <style>
106
+ .stApp { background-color: #FFFFFF !important; }
107
+ html, body, [data-testid="stWidgetLabel"], .stText, p { color: #1A1A1A !important; }
108
+ .stButton>button { background-color: #E29792 !important; color: white !important; border-radius: 20px; font-weight: bold; }
109
+ .stMetric { background-color: #FDF2F1 !important; padding: 15px; border-radius: 10px; border-left: 5px solid #E29792 !important; }
110
+ [data-testid="stMetricValue"], [data-testid="stMetricLabel"] { color: #1A1A1A !important; }
111
+ </style>
112
+ """, unsafe_allow_html=True)
113
+
114
+ st.title("👗 Grain de Malice - Aide au RUN")
115
+ st.subheader("Optimisation par Pôles d'Expertise Consolidés")
116
+
117
+ with st.sidebar:
118
+ # 1. Logo et En-tête
119
+ if os.path.exists("logo_gdm.png"):
120
+ st.image("logo_gdm.png", width=150)
121
+ st.header("🚀 Spécifications V4")
122
+ st.success("✅ **Modèle :** DistilBERT Multilingual")
123
+ st.info("📊 **Mapping :** 3 Pôles Stratégiques")
124
+
125
+ # 2. Section Expertise (Ton tableau MTTR d'origine)
126
+ st.markdown("---")
127
+ st.subheader("📊 MTTR par Pôle")
128
+ if MTTR_MOYENS:
129
+ df_mttr = pd.DataFrame([
130
+ {"Pôle": k, "Délai Moyen": f"{v:.1f} h"}
131
+ for k, v in MTTR_MOYENS.items() if v is not None
132
+ ])
133
+ st.table(df_mttr)
134
+
135
+ # 3. NOUVEAU : Architecture & Industrialisation (Pour la certif)
136
+ st.markdown("---")
137
+ st.subheader("🛠️ Industrialisation")
138
+
139
+ with st.expander("🌐 API & Intégration"):
140
+ st.write("""
141
+ Exposition via **FastAPI** :
142
+ - Endpoint : `/predict`
143
+ - Format : JSON REST
144
+ - Compatible : ServiceNow, Jira, Apps Magasins.
145
+ """)
146
+
147
+ with st.expander("📦 Déploiement Docker"):
148
+ st.write("""
149
+ - Conteneur : `python:3.10-slim`
150
+ - Stockage : Git LFS (Large File Storage)
151
+ - Scalabilité : Prêt pour Kubernetes.
152
+ """)
153
+
154
+ st.caption("Innovation IT - Grain de Malice © 2026")
155
+
156
+ st.markdown("---")
157
+
158
+ col_input1, col_input2 = st.columns(2)
159
+ with col_input1:
160
+ objet_saisi = st.text_input("📍 Objet du ticket :")
161
+ with col_input2:
162
+ description_saisie = st.text_area("📝 Description détaillée :")
163
+
164
+ if st.button("🚀 Lancer le Diagnostic IA"):
165
+ if not objet_saisi and not description_saisie:
166
+ st.warning("Veuillez saisir des informations.")
167
+ else:
168
+ with st.spinner('Analyse sémantique en cours...'):
169
+ categorie, mttr, confiance = predire_ticket(objet_saisi, description_saisie)
170
+
171
+ st.markdown("### ✅ Résultat du Diagnostic")
172
+ res1, res2, res3 = st.columns([2, 1, 1])
173
+
174
+ with res1:
175
+ st.metric("Pôle d'Expertise Prédit", categorie)
176
+ with res2:
177
+ st.metric("MTTR Estimé", f"{mttr:.1f} h" if mttr else "N/A")
178
+ with res3:
179
+ st.metric("Confiance IA", f"{confiance*100:.1f} %")
180
+
181
+ # ALERTES CONTEXTUELLES V4
182
+ if "À qualifier" in categorie:
183
+ st.warning("⚠️ **VÉRIFICATION :** Routage manuel nécessaire (Confiance trop faible).")
184
+ elif "Business & Ventes" in categorie:
185
+ st.success("💰 **COMMERCE :** Ticket orienté vers le pôle Business (Impact CA).")
186
+ elif "Data & Finance" in categorie:
187
+ st.info("📊 **DATA :** Analyse de flux financiers ou reporting BI requis.")
188
+ elif "Opérations & Support" in categorie:
189
+ st.error("🚨 **OPS & RUN :** Incident sur les flux techniques ou le support RUN.")
190
+
191
+ st.info("💡 **Note :** Cette version 3-pôles réduit le bruit et augmente la précision du routage automatique.")
192
+ st.sidebar.caption("🟢 Container Status: Healthy (Port 7860)")
logo_gdm.png ADDED
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Base ---
2
+ streamlit
3
+ pandas
4
+ numpy
5
+ scikit-learn
6
+
7
+ # --- TALN (Ordre spécifique pour éviter les conflits) ---
8
+ huggingface-hub
9
+ tokenizers
10
+ transformers
11
+ torch
12
+ spacy
13
+ gradio_client
14
+ fastapi
15
+ uvicorn
16
+
17
+ # --- Modèle SpaCy ---
18
+ https://github.com/explosion/spacy-models/releases/download/fr_core_news_sm-3.7.0/fr_core_news_sm-3.7.0.tar.gz
setup.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Mise à jour de pip
4
+ pip install --upgrade pip
5
+
6
+ # Installation des dépendances du requirements.txt
7
+ pip install -r requirements.txt
8
+
9
+ # Téléchargement explicite du modèle SpaCy français
10
+ python -m spacy download fr_core_news_sm
train_V3.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import pickle
3
+ import numpy as np
4
+ import torch
5
+ import os
6
+ import csv
7
+ from transformers import AutoTokenizer, AutoModel
8
+ from sklearn.base import BaseEstimator, TransformerMixin
9
+ from sklearn.pipeline import Pipeline
10
+ from sklearn.linear_model import LogisticRegression
11
+
12
+ # 1. Sécurité pour les gros volumes de texte (évite les erreurs de lecture)
13
+ csv.field_size_limit(10000000)
14
+
15
+ # --- CLASSE D'EXTRACTION LLM (DISTILBERT) ---
16
+ class LLMFeatureExtractor(BaseEstimator, TransformerMixin):
17
+ def __init__(self, model_name='distilbert-base-multilingual-cased', max_length=128, batch_size=32):
18
+ self.model_name = model_name
19
+ self.max_length = max_length
20
+ self.batch_size = batch_size # On traite par petits paquets
21
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
22
+ self.model = AutoModel.from_pretrained(model_name).to("cpu")
23
+
24
+ def fit(self, X, y=None):
25
+ return self
26
+
27
+ def transform(self, X):
28
+ texts = [str(t)[:1000] for t in X]
29
+ all_embeddings = []
30
+
31
+ # Traitement par lots pour économiser la RAM
32
+ for i in range(0, len(texts), self.batch_size):
33
+ batch_texts = texts[i : i + self.batch_size]
34
+ inputs = self.tokenizer(
35
+ batch_texts,
36
+ padding=True,
37
+ truncation=True,
38
+ max_length=self.max_length,
39
+ return_tensors="pt"
40
+ ).to("cpu")
41
+
42
+ with torch.no_grad():
43
+ outputs = self.model(**inputs)
44
+ # On récupère les embeddings et on les remet sur CPU/Numpy
45
+ embeddings = outputs.last_hidden_state[:, 0, :].detach().numpy()
46
+ all_embeddings.append(embeddings)
47
+
48
+ if i % 512 == 0:
49
+ print(f"⏳ Progression : {i}/{len(texts)} tickets vectorisés...")
50
+
51
+ return np.vstack(all_embeddings)
52
+
53
+
54
+ # --- CONFIGURATION V3 ---
55
+ # Liste tes deux fichiers d'export ici
56
+ FICHIERS_ENTREE = ["Tickets_1.csv", "Tickets_2.csv"]
57
+ NOM_MODELE_GENERE = "modele_classification_taln_llm.pkl"
58
+ NOM_MTTR_GENERE = "mttr_moyennes.pkl"
59
+
60
+ def normaliser(texte):
61
+ """ Nettoyage pour matcher les colonnes peu importe l'encodage """
62
+ return str(texte).strip().lower().replace('é', 'e').replace('è', 'e').replace('ê', 'e')
63
+
64
+ def map_pole_expertise(groupe):
65
+ """ Regroupement sémantique en 3 pôles majeurs pour optimiser l'apprentissage """
66
+ g = str(groupe).upper()
67
+
68
+ # Pôle 1 : COMMERCE & BUSINESS (Le plus gros volume)
69
+ if "COMMERCE" in g:
70
+ return "Business & Ventes"
71
+
72
+ # Pôle 2 : DATA & FINANCE (Gestion de la donnée et des chiffres)
73
+ elif any(keyword in g for keyword in ["DATA", "BI", "FINANCE"]):
74
+ return "Data & Finance"
75
+
76
+ # Pôle 3 : OPS & SUPPORT (Maintenance et flux techniques)
77
+ elif any(keyword in g for keyword in ["RUN", "OPCON", "FLUX", "AUTOMATE"]):
78
+ return "Opérations & Support"
79
+
80
+ else:
81
+ return "AUTRES"
82
+
83
+ def lancer_entrainement_v3():
84
+ print(f"🚀 Initialisation du Pipeline V3...")
85
+
86
+ # 1. CHARGEMENT ET FUSION DES DEUX SOURCES
87
+ liste_df = []
88
+ for f in FICHIERS_ENTREE:
89
+ if os.path.exists(f):
90
+ try:
91
+ temp_df = pd.read_csv(f, sep=None, engine='python', encoding='utf-8-sig')
92
+ liste_df.append(temp_df)
93
+ print(f"📖 Chargé : {f} ({len(temp_df)} lignes)")
94
+ except Exception as e:
95
+ print(f"⚠️ Erreur sur {f} : {e}")
96
+
97
+ if not liste_df:
98
+ print("❌ Aucun fichier trouvé. Vérifiez les noms des CSV.")
99
+ return
100
+
101
+ df = pd.concat(liste_df, ignore_index=True)
102
+ print(f"🔗 Fusion réussie : {len(df)} tickets consolidés.")
103
+
104
+ # 2. DÉTECTION DES COLONNES
105
+ mapping = {normaliser(c): c for c in df.columns}
106
+ col_desc = mapping.get('description')
107
+ col_grp = mapping.get('groupe')
108
+ col_mttr = next((c for norm, c in mapping.items() if 'delai' in norm or 'resolution' in norm), None)
109
+
110
+ if not all([col_desc, col_grp]):
111
+ print(f"❌ Colonnes 'Description' ou 'Groupe' introuvables. Colonnes vues : {list(mapping.keys())}")
112
+ return
113
+
114
+ # 3. NETTOYAGE ET MAPPING DES CLASSES
115
+ df = df.dropna(subset=[col_desc, col_grp])
116
+ df = df.drop_duplicates(subset=[col_desc]) # Supprime les doublons pour un meilleur apprentissage
117
+ df['categorie'] = df[col_grp].apply(map_pole_expertise)
118
+
119
+ print(f"🧹 Nettoyage terminé : {len(df)} tickets uniques.")
120
+
121
+ # 4. CALCUL DU MTTR DYNAMIQUE
122
+ if col_mttr:
123
+ def convertir_en_heures(val):
124
+ try:
125
+ if ":" in str(val): # Gestion format HH:MM:SS
126
+ p = str(val).split(':')
127
+ return float(p[0]) + float(p[1])/60 + float(p[2])/3600
128
+ return float(val) # Gestion format décimal
129
+ except: return np.nan
130
+
131
+ df['mttr_h'] = df[col_mttr].apply(convertir_en_heures)
132
+ # On ignore les valeurs aberrantes (ex: tickets restés ouverts 2 ans)
133
+ df_mttr = df[df['mttr_h'] < 1500].dropna(subset=['mttr_h'])
134
+
135
+ mttr_moyennes = df_mttr.groupby('categorie')['mttr_h'].mean().to_dict()
136
+ with open(NOM_MTTR_GENERE, 'wb') as f:
137
+ pickle.dump(mttr_moyennes, f)
138
+ print(f"📊 Référentiel MTTR généré pour {len(mttr_moyennes)} pôles.")
139
+
140
+ # 5. ENTRAÎNEMENT HYBRIDE (LLM + LOGISTIC REGRESSION)
141
+ print("🧠 Entraînement DistilBERT en cours (cela peut prendre quelques minutes)...")
142
+ X = df[col_desc].astype(str)
143
+ y = df['categorie']
144
+
145
+ pipeline = Pipeline([
146
+ ('feature_extractor', LLMFeatureExtractor()),
147
+ ('classifier', LogisticRegression(max_iter=1000, solver='lbfgs', multi_class='multinomial'))
148
+ ])
149
+
150
+ pipeline.fit(X, y)
151
+
152
+ # 6. SAUVEGARDE FINALE
153
+ with open(NOM_MODELE_GENERE, 'wb') as f:
154
+ pickle.dump(pipeline, f)
155
+
156
+ print(f"✨ MISSION V3 RÉUSSIE ! Modèle et MTTR sauvegardés.")
157
+
158
+ if __name__ == "__main__":
159
+ lancer_entrainement_v3()