mannnon commited on
Commit
d1a9b21
·
verified ·
1 Parent(s): b55ba6d

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -456
app.py DELETED
@@ -1,456 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
- import streamlit as st
4
- import matplotlib.pyplot as plt
5
-
6
- from zebris_extractor import extract_zebris_csv
7
-
8
- st.set_page_config(page_title="Zebris — Profil & Seuils", layout="wide")
9
-
10
- st.title("Zebris — Profil biomécanique & seuils individualisés")
11
- st.caption("Import d’un CSV Zebris brut → fiche profil + seuils personnalisés")
12
-
13
- with st.sidebar:
14
- st.header("Import")
15
- uploaded_file = st.file_uploader(
16
- "Importer un fichier CSV Zebris",
17
- type=["csv"],
18
- accept_multiple_files=False,
19
- )
20
-
21
- if uploaded_file is not None and uploaded_file.size > 20 * 1024 * 1024:
22
- st.error("Fichier trop volumineux (>20 MB)")
23
- st.stop()
24
-
25
- st.header("Contexte athlète")
26
- volume_horaire = st.number_input(
27
- "Volume horaire / semaine",
28
- min_value=0.5,
29
- max_value=40.0,
30
- value=5.0,
31
- step=0.5,
32
- )
33
-
34
- if uploaded_file is None:
35
- st.info("Importe un CSV Zebris brut pour afficher la fiche profil et les seuils.")
36
- st.stop()
37
-
38
- try:
39
- df_std, debug = extract_zebris_csv(uploaded_file)
40
- except Exception as e:
41
- st.error(f"Erreur pendant l’extraction du CSV Zebris : {e}")
42
- st.stop()
43
-
44
- if df_std.empty:
45
- st.error("Aucune ligne exploitable n’a été trouvée dans le CSV.")
46
- st.stop()
47
-
48
- athletes = sorted(df_std["Nom"].dropna().unique().tolist())
49
- selected_athlete = st.selectbox("Sportif", athletes)
50
- sub_df = df_std[df_std["Nom"] == selected_athlete].copy()
51
-
52
- if sub_df.empty:
53
- st.error("Aucune donnée trouvée pour ce sportif.")
54
- st.stop()
55
-
56
- sub_df = sub_df.sort_values("Vitesse (km/h)")
57
- speeds = sub_df["Vitesse (km/h)"].dropna().tolist()
58
- selected_speed = st.selectbox("Allure analysée (km/h)", speeds)
59
- row = sub_df[sub_df["Vitesse (km/h)"] == selected_speed].iloc[0]
60
-
61
- poids_csv = row["Poids (kg)"] if pd.notna(row["Poids (kg)"]) else np.nan
62
- poids_kg = st.number_input(
63
- "Poids du sportif (kg)",
64
- min_value=30.0,
65
- max_value=150.0,
66
- value=float(poids_csv) if pd.notna(poids_csv) else 70.0,
67
- step=0.1,
68
- )
69
- poids_n = poids_kg * 9.81
70
-
71
-
72
- def avg(a, b):
73
- if pd.isna(a) and pd.isna(b):
74
- return np.nan
75
- if pd.isna(a):
76
- return float(b)
77
- if pd.isna(b):
78
- return float(a)
79
- return (float(a) + float(b)) / 2
80
-
81
-
82
- def asym(a, b):
83
- m = avg(a, b)
84
- if pd.isna(m) or m == 0 or pd.isna(a) or pd.isna(b):
85
- return np.nan
86
- return abs(float(a) - float(b)) / m * 100
87
-
88
-
89
- def clamp_score(value, low, high, reverse=False):
90
- if pd.isna(value):
91
- return np.nan
92
- score = (value - low) / (high - low) * 100
93
- score = max(0, min(100, score))
94
- return 100 - score if reverse else score
95
-
96
-
97
- def safe_mean(values):
98
- vals = [v for v in values if pd.notna(v)]
99
- if not vals:
100
- return np.nan
101
- return float(np.mean(vals))
102
-
103
-
104
- def compute_profile_metrics(row, poids_kg):
105
- poids_n = poids_kg * 9.81
106
-
107
- force_talon_moy = avg(row["Force talon G (N)"], row["Force talon D (N)"])
108
- force_avant_moy = avg(row["Force avant-pied G (N)"], row["Force avant-pied D (N)"])
109
- pression_talon_moy = avg(row["Pression talon G (N/cm²)"], row["Pression talon D (N/cm²)"])
110
- cop_moy = avg(row["COP G (mm)"], row["COP D (mm)"])
111
- transition_moy = avg(row["Transition G (s)"], row["Transition D (s)"])
112
-
113
- asym_talon = asym(row["Force talon G (N)"], row["Force talon D (N)"])
114
- asym_avant = asym(row["Force avant-pied G (N)"], row["Force avant-pied D (N)"])
115
- asym_cop = asym(row["COP G (mm)"], row["COP D (mm)"])
116
-
117
- diff_rotation = (
118
- abs(float(row["Rotation G (°)"]) - float(row["Rotation D (°)"]))
119
- if pd.notna(row["Rotation G (°)"]) and pd.notna(row["Rotation D (°)"])
120
- else np.nan
121
- )
122
-
123
- force_talon_bw = force_talon_moy / poids_n if pd.notna(force_talon_moy) and poids_n else np.nan
124
- ratio_talon_avant = (
125
- force_talon_moy / force_avant_moy
126
- if pd.notna(force_talon_moy) and pd.notna(force_avant_moy) and force_avant_moy != 0
127
- else np.nan
128
- )
129
-
130
- # Impact corrigé : fallback sur force brute si BW indisponible
131
- impact_force_score = (
132
- clamp_score(force_talon_bw, 0.6, 1.1)
133
- if pd.notna(force_talon_bw)
134
- else clamp_score(force_talon_moy, 400, 750)
135
- )
136
- impact_pressure_score = clamp_score(pression_talon_moy, 15, 30)
137
-
138
- impact = safe_mean([
139
- 0.6 * impact_force_score if pd.notna(impact_force_score) else np.nan,
140
- 0.4 * impact_pressure_score if pd.notna(impact_pressure_score) else np.nan,
141
- ])
142
- impact = round(impact) if pd.notna(impact) else np.nan
143
-
144
- dynamique = safe_mean([
145
- 0.6 * clamp_score(row["Cadence (pas/min)"], 150, 185),
146
- 0.4 * clamp_score(row["Contact (%)"], 68, 76, reverse=True),
147
- ])
148
- dynamique = round(dynamique) if pd.notna(dynamique) else np.nan
149
-
150
- sym_components = [x for x in [asym_talon, asym_avant, asym_cop, diff_rotation] if pd.notna(x)]
151
- symetrie = round(100 - min(100, np.mean(sym_components) * 2.5)) if sym_components else np.nan
152
-
153
- deroule = safe_mean([
154
- 0.5 * clamp_score(cop_moy, 210, 260),
155
- 0.5 * clamp_score(transition_moy, 0.05, 0.09, reverse=True),
156
- ])
157
- deroule = round(deroule) if pd.notna(deroule) else np.nan
158
-
159
- attaque = "mixte"
160
- if pd.notna(ratio_talon_avant):
161
- if ratio_talon_avant > 1.05:
162
- attaque = "talon"
163
- elif ratio_talon_avant < 0.95:
164
- attaque = "avant-pied"
165
-
166
- return {
167
- "force_talon_moy": force_talon_moy,
168
- "force_avant_moy": force_avant_moy,
169
- "pression_talon_moy": pression_talon_moy,
170
- "cop_moy": cop_moy,
171
- "transition_moy": transition_moy,
172
- "asym_talon": asym_talon,
173
- "asym_avant": asym_avant,
174
- "asym_cop": asym_cop,
175
- "diff_rotation": diff_rotation,
176
- "force_talon_bw": force_talon_bw,
177
- "ratio_talon_avant": ratio_talon_avant,
178
- "impact": impact,
179
- "dynamique": dynamique,
180
- "symetrie": symetrie,
181
- "deroule": deroule,
182
- "attaque": attaque,
183
- }
184
-
185
-
186
- def build_summary(row, metrics):
187
- impact_txt = "marqué" if pd.notna(metrics["impact"]) and metrics["impact"] >= 70 else "modéré" if pd.notna(metrics["impact"]) and metrics["impact"] >= 45 else "faible"
188
- dyn_txt = "bonne" if pd.notna(metrics["dynamique"]) and metrics["dynamique"] >= 70 else "moyenne" if pd.notna(metrics["dynamique"]) and metrics["dynamique"] >= 45 else "faible"
189
- sym_txt = "satisfaisante" if pd.notna(metrics["symetrie"]) and metrics["symetrie"] >= 70 else "perfectible"
190
- der_txt = "favorable" if pd.notna(metrics["deroule"]) and metrics["deroule"] >= 70 else "intermédiaire" if pd.notna(metrics["deroule"]) and metrics["deroule"] >= 45 else "à surveiller"
191
-
192
- return (
193
- f"À {row['Vitesse (km/h)']} km/h, {row['Nom']} présente une attaque {metrics['attaque']}, "
194
- f"un impact {impact_txt}, une dynamique {dyn_txt}, une symétrie {sym_txt} "
195
- f"et un déroulé {der_txt}."
196
- )
197
-
198
-
199
- def draw_radar(metrics):
200
- labels = ["Impact", "Dynamique", "Symétrie", "Déroulé"]
201
- values = [
202
- metrics["impact"] if pd.notna(metrics["impact"]) else 0,
203
- metrics["dynamique"] if pd.notna(metrics["dynamique"]) else 0,
204
- metrics["symetrie"] if pd.notna(metrics["symetrie"]) else 0,
205
- metrics["deroule"] if pd.notna(metrics["deroule"]) else 0,
206
- ]
207
- values += values[:1]
208
- angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist()
209
- angles += angles[:1]
210
-
211
- fig = plt.figure(figsize=(5, 5))
212
- ax = plt.subplot(111, polar=True)
213
- ax.plot(angles, values, linewidth=2)
214
- ax.fill(angles, values, alpha=0.25)
215
- ax.set_xticks(angles[:-1])
216
- ax.set_xticklabels(labels)
217
- ax.set_ylim(0, 100)
218
- ax.set_yticks([25, 50, 75, 100])
219
- ax.set_title("Radar biomécanique", pad=20)
220
- return fig
221
-
222
-
223
- def draw_evolution(df, poids_kg):
224
- data = []
225
- for _, r in df.sort_values("Vitesse (km/h)").iterrows():
226
- m = compute_profile_metrics(r, poids_kg)
227
- data.append({
228
- "Vitesse": r["Vitesse (km/h)"],
229
- "Impact": m["impact"],
230
- "Dynamique": m["dynamique"],
231
- "Symétrie": m["symetrie"],
232
- "Déroulé": m["deroule"],
233
- })
234
-
235
- evo = pd.DataFrame(data)
236
- fig, ax = plt.subplots(figsize=(8, 4))
237
- for col in ["Impact", "Dynamique", "Symétrie", "Déroulé"]:
238
- ax.plot(evo["Vitesse"], evo[col], marker="o", label=col)
239
- ax.set_ylim(0, 100)
240
- ax.set_xlabel("Vitesse (km/h)")
241
- ax.set_ylabel("Score /100")
242
- ax.set_title("Évolution avec l’allure")
243
- ax.legend()
244
- ax.grid(True, alpha=0.3)
245
- return fig
246
-
247
-
248
- def compute_thresholds(poids_kg, volume_horaire):
249
- poids_n = poids_kg * 9.81
250
-
251
- if volume_horaire <= 3:
252
- charge = "faible"
253
- force_bw_low, force_bw_high = 0.75, 1.00
254
- pression_low, pression_high = 20, 26
255
- cadence_low, cadence_high = 160, 172
256
- contact_low, contact_high = 70, 74
257
- flight_low, flight_high = 26, 30
258
- asym_low, asym_high = 6, 10
259
- rotation_low, rotation_high = 6, 10
260
- elif volume_horaire <= 6:
261
- charge = "modérée"
262
- force_bw_low, force_bw_high = 0.70, 0.95
263
- pression_low, pression_high = 20, 25
264
- cadence_low, cadence_high = 165, 176
265
- contact_low, contact_high = 69, 73
266
- flight_low, flight_high = 27, 31
267
- asym_low, asym_high = 5, 9
268
- rotation_low, rotation_high = 5, 9
269
- else:
270
- charge = "élevée"
271
- force_bw_low, force_bw_high = 0.65, 0.90
272
- pression_low, pression_high = 20, 24
273
- cadence_low, cadence_high = 168, 180
274
- contact_low, contact_high = 68, 72
275
- flight_low, flight_high = 28, 32
276
- asym_low, asym_high = 4, 8
277
- rotation_low, rotation_high = 4, 8
278
-
279
- return {
280
- "charge": charge,
281
- "poids_n": poids_n,
282
- "force_bw_low": force_bw_low,
283
- "force_bw_high": force_bw_high,
284
- "force_n_low": force_bw_low * poids_n,
285
- "force_n_high": force_bw_high * poids_n,
286
- "pression_low": pression_low,
287
- "pression_high": pression_high,
288
- "cadence_low": cadence_low,
289
- "cadence_high": cadence_high,
290
- "contact_low": contact_low,
291
- "contact_high": contact_high,
292
- "flight_low": flight_low,
293
- "flight_high": flight_high,
294
- "asym_low": asym_low,
295
- "asym_high": asym_high,
296
- "rotation_low": rotation_low,
297
- "rotation_high": rotation_high,
298
- }
299
-
300
-
301
- metrics = compute_profile_metrics(row, poids_kg)
302
- summary = build_summary(row, metrics)
303
- thresholds = compute_thresholds(poids_kg, volume_horaire)
304
-
305
- tab_profil, tab_seuils = st.tabs(["Profil biomécanique", "Seuils individualisés"])
306
-
307
- with tab_profil:
308
- c1, c2, c3, c4 = st.columns(4)
309
- with c1:
310
- st.metric("Impact", f"{metrics['impact']}/100" if pd.notna(metrics["impact"]) else "N/A")
311
- with c2:
312
- st.metric("Dynamique", f"{metrics['dynamique']}/100" if pd.notna(metrics["dynamique"]) else "N/A")
313
- with c3:
314
- st.metric("Symétrie", f"{metrics['symetrie']}/100" if pd.notna(metrics["symetrie"]) else "N/A")
315
- with c4:
316
- st.metric("Déroulé", f"{metrics['deroule']}/100" if pd.notna(metrics["deroule"]) else "N/A")
317
-
318
- left, right = st.columns([1.2, 1])
319
-
320
- with left:
321
- st.subheader("Carte d’identité biomécanique")
322
- st.write(summary)
323
-
324
- indicators = pd.DataFrame(
325
- {
326
- "Indicateur": [
327
- "Poids",
328
- "Cadence",
329
- "Contact",
330
- "Flight",
331
- "Force talon moyenne",
332
- "Force talon normalisée",
333
- "Pression talon moyenne",
334
- "Asymétrie talon",
335
- "COP moyen",
336
- "Différence rotation",
337
- "Attaque",
338
- ],
339
- "Valeur": [
340
- f"{poids_kg:.1f} kg",
341
- f"{row['Cadence (pas/min)']:.1f} pas/min" if pd.notna(row["Cadence (pas/min)"]) else "N/A",
342
- f"{row['Contact (%)']:.1f} %" if pd.notna(row["Contact (%)"]) else "N/A",
343
- f"{row['Flight (%)']:.1f} %" if pd.notna(row["Flight (%)"]) else "N/A",
344
- f"{metrics['force_talon_moy']:.1f} N" if pd.notna(metrics["force_talon_moy"]) else "N/A",
345
- f"{metrics['force_talon_bw']:.2f} BW" if pd.notna(metrics["force_talon_bw"]) else "N/A",
346
- f"{metrics['pression_talon_moy']:.1f} N/cm²" if pd.notna(metrics["pression_talon_moy"]) else "N/A",
347
- f"{metrics['asym_talon']:.1f} %" if pd.notna(metrics["asym_talon"]) else "N/A",
348
- f"{metrics['cop_moy']:.1f} mm" if pd.notna(metrics["cop_moy"]) else "N/A",
349
- f"{metrics['diff_rotation']:.1f}°" if pd.notna(metrics["diff_rotation"]) else "N/A",
350
- metrics["attaque"],
351
- ],
352
- }
353
- )
354
- st.dataframe(indicators, hide_index=True, use_container_width=True)
355
-
356
- with right:
357
- st.subheader("Radar biomécanique")
358
- st.pyplot(draw_radar(metrics), use_container_width=True)
359
-
360
- st.subheader("Évolution avec l’allure")
361
- st.pyplot(draw_evolution(sub_df, poids_kg), use_container_width=True)
362
-
363
- with tab_seuils:
364
- r1, r2, r3 = st.columns(3)
365
- with r1:
366
- st.metric("Poids", f"{poids_kg:.1f} kg")
367
- with r2:
368
- st.metric("Poids en Newton", f"{thresholds['poids_n']:.1f} N")
369
- with r3:
370
- st.metric("Charge", thresholds["charge"])
371
-
372
- impact_df = pd.DataFrame({
373
- "Variable": [
374
- "Force talon normalisée",
375
- "Force talon convertie",
376
- "Pression talon",
377
- ],
378
- "Zone basse / faible": [
379
- f"< {thresholds['force_bw_low']:.2f} BW",
380
- f"< {thresholds['force_n_low']:.1f} N",
381
- f"< {thresholds['pression_low']} N/cm²",
382
- ],
383
- "Zone attendue": [
384
- f"{thresholds['force_bw_low']:.2f} à {thresholds['force_bw_high']:.2f} BW",
385
- f"{thresholds['force_n_low']:.1f} à {thresholds['force_n_high']:.1f} N",
386
- f"{thresholds['pression_low']} à {thresholds['pression_high']} N/cm²",
387
- ],
388
- "Zone haute / élevée": [
389
- f"> {thresholds['force_bw_high']:.2f} BW",
390
- f"> {thresholds['force_n_high']:.1f} N",
391
- f"> {thresholds['pression_high']} N/cm²",
392
- ],
393
- })
394
-
395
- dynamique_df = pd.DataFrame({
396
- "Variable": [
397
- "Cadence",
398
- "Temps de contact",
399
- "Temps de vol",
400
- ],
401
- "Zone basse / faible": [
402
- f"< {thresholds['cadence_low']} pas/min",
403
- f"< {thresholds['contact_low']} %",
404
- f"< {thresholds['flight_low']} %",
405
- ],
406
- "Zone attendue": [
407
- f"{thresholds['cadence_low']} à {thresholds['cadence_high']} pas/min",
408
- f"{thresholds['contact_low']} à {thresholds['contact_high']} %",
409
- f"{thresholds['flight_low']} à {thresholds['flight_high']} %",
410
- ],
411
- "Zone haute / élevée": [
412
- f"> {thresholds['cadence_high']} pas/min",
413
- f"> {thresholds['contact_high']} %",
414
- f"> {thresholds['flight_high']} %",
415
- ],
416
- })
417
-
418
- symetrie_df = pd.DataFrame({
419
- "Variable": [
420
- "Asymétrie force talon",
421
- "Asymétrie force avant-pied",
422
- "Asymétrie COP",
423
- "Différence rotation G/D",
424
- ],
425
- "Zone faible": [
426
- f"< {thresholds['asym_low']} %",
427
- f"< {thresholds['asym_low']} %",
428
- f"< {thresholds['asym_low']} %",
429
- f"< {thresholds['rotation_low']}°",
430
- ],
431
- "Zone modérée": [
432
- f"{thresholds['asym_low']} à {thresholds['asym_high']} %",
433
- f"{thresholds['asym_low']} à {thresholds['asym_high']} %",
434
- f"{thresholds['asym_low']} à {thresholds['asym_high']} %",
435
- f"{thresholds['rotation_low']} à {thresholds['rotation_high']}°",
436
- ],
437
- "Zone marquée": [
438
- f"> {thresholds['asym_high']} %",
439
- f"> {thresholds['asym_high']} %",
440
- f"> {thresholds['asym_high']} %",
441
- f"> {thresholds['rotation_high']}°",
442
- ],
443
- })
444
-
445
- s1, s2, s3 = st.tabs(["Impact", "Dynamique", "Symétrie"])
446
- with s1:
447
- st.dataframe(impact_df, hide_index=True, use_container_width=True)
448
- with s2:
449
- st.dataframe(dynamique_df, hide_index=True, use_container_width=True)
450
- with s3:
451
- st.dataframe(symetrie_df, hide_index=True, use_container_width=True)
452
-
453
- st.write(
454
- "Ces seuils sont individualisés à partir du poids et du volume horaire d’entraînement hebdomadaire. "
455
- "Ils constituent une base de référence personnelle avant toute analyse ou recommandation."
456
- )