FRANCKYPRO commited on
Commit
dc1ed2e
·
verified ·
1 Parent(s): e7deb5a

Upload 3 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
 
 
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
+ data/dataset_f1.csv filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import joblib
5
+ import requests
6
+ import seaborn as sns
7
+ import matplotlib.pyplot as plt
8
+ import io
9
+ from sqlalchemy import create_engine, text
10
+ from sqlalchemy import create_engine
11
+
12
+ # --- Connexion à la base de données ---
13
+ engine = create_engine("mysql+pymysql://root:@localhost/doctolib")
14
+
15
+ # --- Chargement du modèle ---
16
+ model = joblib.load('../models/xgb_oversampled_model.joblib')
17
+
18
+ # --- Configuration de la page ---
19
+ st.set_page_config(page_title="Doctolib Annulation Prediction", layout="wide", page_icon="🩺")
20
+
21
+ # --- Ajout de style riche aux couleurs Doctolib et image de fond sur sidebar ---
22
+ st.markdown("""
23
+ <style>
24
+ [data-testid="stSidebar"] > div:first-child {
25
+ background-image: url('https://assets.entrepreneur.com/content/3x2/2000/1623253746-GettyImages-1273886962.jpg');
26
+ background-size: cover;
27
+ background-position: center;
28
+ padding-top: 60px;
29
+ }
30
+ [data-testid="stSidebar"] .css-ng1t4o {
31
+ background-color: rgba(0, 123, 255, 0.8);
32
+ border-radius: 12px;
33
+ padding: 10px;
34
+ color: white;
35
+ }
36
+ [data-testid="stSidebar"] .stSelectbox > div > div {
37
+ background-color: white;
38
+ color: #007bff;
39
+ border-radius: 8px;
40
+ }
41
+ .main { padding: 20px; }
42
+ h1, h2, h3, h4 { color: #0069d9; text-align: center; animation: fadeIn 1.5s ease-in-out; }
43
+ .stButton>button { background: linear-gradient(90deg, #0069d9, #2b9cd8); color: white; border-radius: 25px; padding: 12px 25px; font-size: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: none; }
44
+ .stButton>button:hover { background: linear-gradient(90deg, #2b9cd8, #0069d9); }
45
+ .highlight-box { background-color: #ffffff; border-left: 8px solid #0069d9; border-radius: 12px; padding: 25px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px; }
46
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
47
+ .footer { text-align: center; margin-top: 50px; font-size: 12px; color: #0069d9; }
48
+ </style>
49
+ """, unsafe_allow_html=True)
50
+
51
+ # --- Sidebar Menu avec fond illustré ---
52
+ menu = st.sidebar.selectbox("Menu", ["Prédiction Temps Réel", "Classification sur CSV", "Système Automatique (Notifications)", "Tableaux de bord statistiques"])
53
+
54
+ # --- Affichage du logo ---
55
+ st.image("https://www.osteo-var.com/wp-content/uploads/2019/07/logo-doctolib.png", width=300)
56
+
57
+ # --- Section décorative ---
58
+ st.markdown("""
59
+ <div class="highlight-box" style="text-align:center; animation: fadeIn 2s ease-in-out; color: #0069d9;">
60
+ <h3>🩺 Prévoyez mieux. Évitez les annulations. Améliorez votre planning.</h3>
61
+ <p>Notre application vous aide à prédire et prévenir les absences, pour une meilleure organisation médicale.</p>
62
+ </div>
63
+ """, unsafe_allow_html=True)
64
+
65
+ st.title("Application Doctolib – Prédiction des Annulations de Rendez-vous")
66
+
67
+
68
+ required_cols = [
69
+ 'Scholarship', 'Hypertension', 'Diabetes', 'Alcoholism', 'Disability',
70
+ 'Days_Between_Scheduling_and_Appointment', 'Hospital_Area', 'Specialty',
71
+ 'Facility_Type', 'Distance_km', 'Type_of_Care', 'Previously_Treated', 'Age',
72
+ 'Social_Status', 'SMS_Received', 'Weather_Conditions', 'Appointment_Time',
73
+ 'Gender', 'Consultations_Last_12_Months', 'Waiting_Time_Minutes',
74
+ 'Hospital_Rating', 'Average_Fee', 'Number_days'
75
+ ]
76
+
77
+ category_mappings = {
78
+ 'Hospital_Area': {'Pigalle': 13760, 'Bastille': 13887, 'Saint-Germain': 13846, 'Belleville': 13885, 'La Défense': 13835, 'Châtelet': 13768, 'Montparnasse': 13810},
79
+ 'Specialty': {'Pédiatrie': 15772, 'Gynécologie': 15785, 'Dermatologie': 15697, 'Cardiologie': 15892, 'Psychiatrie': 15771, 'Neurologie': 15778, 'Ophtalmologie': 15832},
80
+ 'Facility_Type': {'Conventionné': 0, 'Non conventionné': 1},
81
+ 'Type_of_Care': {'Vaccination': 21941, 'Urgence': 22224, 'Suivi': 22173, 'Bilan': 22018, 'Consultation': 22171},
82
+ 'Social_Status': {'Indépendant': 22195, 'Étudiant': 21999, 'Retraité': 22048, 'Sans emploi': 22007, 'Salarié': 22278},
83
+ 'Gender': {'Homme': 1, 'Femme': 0}
84
+ }
85
+
86
+ reverse_mappings = {col: {v: k for k, v in mapping.items()} for col, mapping in category_mappings.items()}
87
+
88
+ def encode_categories(df):
89
+ for col, mapping in category_mappings.items():
90
+ if col in df.columns:
91
+ df[col] = df[col].map(mapping).fillna(0)
92
+ return df
93
+
94
+ def decode_categories(df):
95
+ for col, mapping in reverse_mappings.items():
96
+ if col in df.columns:
97
+ df[col] = df[col].map(mapping).fillna(df[col])
98
+ return df
99
+
100
+ def seconds_to_time(seconds):
101
+ h = seconds // 3600
102
+ m = (seconds % 3600) // 60
103
+ s = seconds % 60
104
+ return f"{h:02d}:{m:02d}:{s:02d}"
105
+
106
+ def time_to_seconds(time_str):
107
+ parts = time_str.split(':')
108
+ if len(parts) == 2:
109
+ h, m = map(int, parts)
110
+ s = 0
111
+ elif len(parts) == 3:
112
+ h, m, s = map(int, parts)
113
+ else:
114
+ raise ValueError("Format d'heure invalide. Utilisez HH:MM ou HH:MM:SS")
115
+ return h * 3600 + m * 60 + s
116
+ # Traductions françaises des champs
117
+ french_labels = {
118
+ 'Scholarship': "Bourse d'étude",
119
+ 'Hypertension': "Hypertension",
120
+ 'Diabetes': "Diabète",
121
+ 'Alcoholism': "Alcoolisme",
122
+ 'Disability': "Handicap",
123
+ 'Days_Between_Scheduling_and_Appointment': "Jours entre la prise et le rendez-vous",
124
+ 'Hospital_Area': "Zone hospitalière",
125
+ 'Specialty': "Spécialité",
126
+ 'Facility_Type': "Type d'établissement",
127
+ 'Distance_km': "Distance en km",
128
+ 'Type_of_Care': "Type de soin",
129
+ 'Previously_Treated': "Déjà traité",
130
+ 'Age': "Âge",
131
+ 'Social_Status': "Statut social",
132
+ 'SMS_Received': "SMS reçu",
133
+ 'Weather_Conditions': "Conditions météorologiques (0=Favorable, 1=Défavorable)",
134
+ 'Appointment_Time': "Heure du rendez-vous",
135
+ 'Gender': "Genre",
136
+ 'Consultations_Last_12_Months': "Consultations sur 12 mois",
137
+ 'Waiting_Time_Minutes': "Temps d'attente (min)",
138
+ 'Hospital_Rating': "Note de l'hôpital",
139
+ 'Average_Fee': "Frais moyens",
140
+ 'Number_days': "Nombre de jours"
141
+ }
142
+
143
+ # Ajout des champs français dans la prédiction
144
+ if menu == "Prédiction Temps Réel":
145
+ st.subheader("Prédiction en Temps Réel")
146
+
147
+ user_input = {}
148
+ booking_date = st.date_input("Date de prise de rendez-vous")
149
+ appointment_date = st.date_input("Date du rendez-vous")
150
+ number_days = (appointment_date - booking_date).days
151
+ user_input['Number_days'] = number_days
152
+
153
+ for col in required_cols:
154
+ if col == 'Number_days':
155
+ continue
156
+ label = french_labels.get(col, col)
157
+ if col == 'Appointment_Time':
158
+ time_str = st.text_input(f"{label} (HH:MM ou HH:MM:SS)", value="09:00")
159
+ user_input[col] = time_to_seconds(time_str)
160
+ elif col in ['Scholarship', 'Hypertension', 'Diabetes', 'Alcoholism', 'Disability', 'SMS_Received', 'Previously_Treated']:
161
+ user_input[col] = st.selectbox(f"{label} (Oui=1, Non=0)", [0, 1])
162
+ elif col in category_mappings:
163
+ user_input[col] = st.selectbox(label, list(category_mappings[col].keys()))
164
+ else:
165
+ user_input[col] = st.number_input(label, value=0)
166
+
167
+ if st.button("Lancer la prédiction"):
168
+ input_df = pd.DataFrame([user_input])
169
+ input_df = encode_categories(input_df)
170
+ input_df = input_df[required_cols].apply(pd.to_numeric, errors='coerce').fillna(0)
171
+ prediction = model.predict(input_df)[0]
172
+ probas = model.predict_proba(input_df)[0]
173
+ st.success(f"Résultat : {'Annulation probable' if prediction == 1 else 'Présence probable'}")
174
+ st.write(f"Probabilité d'annulation : {probas[1]*100:.2f}%")
175
+
176
+ elif menu == "Classification sur CSV":
177
+ st.subheader("Classification en Masse (CSV)")
178
+ uploaded_file = st.file_uploader("Téléverser un fichier CSV (avec colonnes exactes)", type=["csv"])
179
+ if uploaded_file:
180
+ st.write(" Fichier reçu côté Streamlit :")
181
+ st.write(f"Nom du fichier : {uploaded_file.name}")
182
+ st.write(f"Type de fichier : {uploaded_file.type}")
183
+ st.write(f"Taille : {uploaded_file.size} octets")
184
+
185
+ try:
186
+ df_original = pd.read_csv(uploaded_file)
187
+ df = df_original.copy()
188
+ st.write(" Aperçu des premières lignes :")
189
+ st.dataframe(df.head())
190
+ st.write(" Colonnes détectées :", df.columns.tolist())
191
+
192
+ if 'Appointment_Booking_Date' in df.columns and 'Appointment_Date' in df.columns:
193
+ df['Appointment_Booking_Date'] = pd.to_datetime(df['Appointment_Booking_Date'])
194
+ df['Appointment_Date'] = pd.to_datetime(df['Appointment_Date'])
195
+ df['Number_days'] = (df['Appointment_Date'] - df['Appointment_Booking_Date']).dt.days
196
+ if 'Appointment_Time' in df.columns:
197
+ df['Appointment_Time'] = df['Appointment_Time'].apply(time_to_seconds)
198
+ df_encoded = encode_categories(df)
199
+ df_encoded = df_encoded[required_cols].apply(pd.to_numeric, errors='coerce').fillna(0)
200
+
201
+ predictions = model.predict(df_encoded)
202
+ probas = model.predict_proba(df_encoded)[:,1]
203
+
204
+ df_original['prediction'] = predictions
205
+ df_original['proba_annulation'] = probas
206
+
207
+ if 'Appointment_Time' in df_original.columns:
208
+ df_original['Appointment_Time'] = df['Appointment_Time'].apply(seconds_to_time)
209
+ df_original = decode_categories(df_original)
210
+
211
+ st.success("✅ Prédictions terminées ! Voici les résultats :")
212
+
213
+ def highlight_proba(val):
214
+ return 'background-color: lightblue; color: black;'
215
+
216
+ def highlight_prediction(val):
217
+ color = 'background-color: red; color: white;' if val == 1 else 'background-color: green; color: white;'
218
+ return color
219
+
220
+ styled_df = df_original.style.applymap(highlight_proba, subset=['proba_annulation'])
221
+ styled_df = styled_df.applymap(highlight_prediction, subset=['prediction'])
222
+
223
+ st.dataframe(styled_df)
224
+
225
+ csv_data = df_original.to_csv(index=False).encode('utf-8')
226
+ excel_buffer = io.BytesIO()
227
+ with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
228
+ df_original.to_excel(writer, index=False, sheet_name='Predictions')
229
+ excel_data = excel_buffer.getvalue()
230
+
231
+ st.download_button("Télécharger en CSV", csv_data, file_name="predictions.csv", mime="text/csv")
232
+ st.download_button("Télécharger en Excel", excel_data, file_name="predictions.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
233
+
234
+ except Exception as e:
235
+ st.error(f" Erreur lors du traitement du fichier : {e}")
236
+
237
+ elif menu == "Système Automatique (Notifications)":
238
+ st.subheader("Système Automatique avec Notifications")
239
+ st.write("⚠ Note : Cette fonction contacte une API locale. Assurez-vous que l'API est active et accepte les requêtes locales sans restriction (vérifiez les CORS et les permissions).")
240
+
241
+ if st.button("Vérifier les rendez-vous à risque"):
242
+ try:
243
+ response = requests.get("http://localhost:8000/pending_appointments")
244
+ response.raise_for_status()
245
+ data = response.json()
246
+ st.write(" Réponse reçue de l'API:", data)
247
+
248
+ for appt in data['appointments']:
249
+ input_df = pd.DataFrame([appt['features']])
250
+ input_df = encode_categories(input_df)
251
+ input_df = input_df[required_cols].apply(pd.to_numeric, errors='coerce').fillna(0)
252
+ prediction = model.predict(input_df)[0]
253
+ if prediction == 1:
254
+ notif_response = requests.post("http://localhost:8000/send_notification", json={"appointment_id": appt['id']})
255
+ st.write(f"➡ Notification POST response: {notif_response.text}")
256
+
257
+ if notif_response.status_code == 200:
258
+ result = notif_response.json()
259
+ st.success(f"Notification envoyée pour le rendez-vous ID {appt['id']} - Statut: {result.get('status', 'OK')}")
260
+ else:
261
+ st.error(f"Erreur d'envoi pour ID {appt['id']} : {notif_response.status_code}, réponse : {notif_response.text}")
262
+ else:
263
+ st.info(f"Aucun risque détecté pour le rendez-vous ID {appt['id']}")
264
+ except requests.exceptions.RequestException as e:
265
+ st.error(f"Erreur lors de la récupération ou de l'envoi : {e}")
266
+
267
+
268
+ # === DASHBOARD ===
269
+ elif menu == "Tableaux de bord statistiques":
270
+ st.subheader("📊 Statistiques des rendez-vous depuis la base de données")
271
+ try:
272
+ with engine.connect() as conn:
273
+ df = pd.read_sql(text("SELECT * FROM appointments"), conn)
274
+
275
+ st.markdown("### Nombre de rendez-vous par spécialité")
276
+ fig1, ax1 = plt.subplots()
277
+ df['specialty'].value_counts().plot(kind='bar', color='#2b9cd8', ax=ax1)
278
+ ax1.set_ylabel("Nombre de rendez-vous")
279
+ ax1.set_xlabel("Spécialité")
280
+ ax1.set_title("Répartition par spécialité")
281
+ st.pyplot(fig1)
282
+
283
+ st.markdown("### Statut des rendez-vous")
284
+ fig2, ax2 = plt.subplots()
285
+ df['status'].value_counts().plot.pie(autopct='%1.1f%%', colors=["#0069d9", "#28a745", "#dc3545"], ax=ax2)
286
+ ax2.set_ylabel("")
287
+ ax2.set_title("Répartition par statut")
288
+ st.pyplot(fig2)
289
+
290
+ st.markdown("### Répartition par zone hospitalière")
291
+ fig3, ax3 = plt.subplots()
292
+ sns.countplot(data=df, y="hospital_area", palette="Blues_r", order=df['hospital_area'].value_counts().index, ax=ax3)
293
+ ax3.set_title("Zones hospitalières les plus utilisées")
294
+ st.pyplot(fig3)
295
+
296
+ st.markdown("### Âge des patients")
297
+ fig4, ax4 = plt.subplots()
298
+ sns.histplot(df['age'], bins=20, kde=True, color='#007bff', ax=ax4)
299
+ ax4.set_title("Distribution des âges des patients")
300
+ st.pyplot(fig4)
301
+
302
+ except Exception as e:
303
+ st.error(f"Erreur lors du chargement des données : {e}")
304
+
305
+ # --- Pied de page ---
306
+ st.markdown("""
307
+ <div class="footer">
308
+ © 2025 Doctolib Predictor | Créé pour améliorer la santé numérique
309
+ </div>
310
+ """, unsafe_allow_html=True)
311
+ # --- Fin de l'application Streamlit ---
data/dataset_f1.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a8b682bd0855326fd6636f7217282045ac64cbe537ad416b4280c94621850808
3
+ size 15307746
models/xgb_oversampled_model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb7510bc9e28e0146724d8177d2038a88d2a5a445d08521d685c1b5febfc3eec
3
+ size 121682