mannnon commited on
Commit
36708f1
·
verified ·
1 Parent(s): 188ab5a

Delete app.py

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