Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -418,6 +418,692 @@ def match_pdf_to_athlete(pdfs_data, athlete_name):
|
|
| 418 |
return pdf
|
| 419 |
return None
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
# Charge CSV
|
| 423 |
dfs = []
|
|
@@ -489,6 +1175,12 @@ attaque_finale = metrics["attaque_csv"]
|
|
| 489 |
if matched_pdf and matched_pdf.get("attaque_pdf") and matched_pdf["attaque_pdf"] != "indéterminée":
|
| 490 |
attaque_finale = matched_pdf["attaque_pdf"]
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
def build_summary(row, metrics, attaque_finale):
|
| 494 |
contraintes_txt = (
|
|
@@ -516,8 +1208,8 @@ def build_summary(row, metrics, attaque_finale):
|
|
| 516 |
|
| 517 |
summary = build_summary(row, metrics, attaque_finale)
|
| 518 |
|
| 519 |
-
tab_profil, tab_seuils, tab_pdf = st.tabs(
|
| 520 |
-
["Profil biomécanique", "Seuils individualisés", "Apports du PDF Zebris"]
|
| 521 |
)
|
| 522 |
|
| 523 |
with tab_profil:
|
|
@@ -666,6 +1358,9 @@ with tab_seuils:
|
|
| 666 |
with s3:
|
| 667 |
st.dataframe(symetrie_df, hide_index=True, use_container_width=True)
|
| 668 |
|
|
|
|
|
|
|
|
|
|
| 669 |
with tab_pdf:
|
| 670 |
if not matched_pdf:
|
| 671 |
st.info("Aucun PDF Zebris associé à cet athlète n’a été trouvé.")
|
|
|
|
| 418 |
return pdf
|
| 419 |
return None
|
| 420 |
|
| 421 |
+
# =========================================================
|
| 422 |
+
# ANALYSE V3
|
| 423 |
+
# =========================================================
|
| 424 |
+
|
| 425 |
+
ANALYSIS_CONFIG = {
|
| 426 |
+
"heel_force_N": {
|
| 427 |
+
"label": "Force talon",
|
| 428 |
+
"unit": "N",
|
| 429 |
+
"description_low": "Force talon plutôt faible par rapport à la zone attendue.",
|
| 430 |
+
"description_normal": "Force talon dans la zone attendue.",
|
| 431 |
+
"description_high": "Force talon élevée, pouvant refléter une contrainte d'impact majorée."
|
| 432 |
+
},
|
| 433 |
+
"heel_pressure_N_cm2": {
|
| 434 |
+
"label": "Pression talon",
|
| 435 |
+
"unit": "N/cm²",
|
| 436 |
+
"description_low": "Pression talon plutôt faible.",
|
| 437 |
+
"description_normal": "Pression talon dans la zone attendue.",
|
| 438 |
+
"description_high": "Pression talon élevée, pouvant indiquer une concentration de charge accrue."
|
| 439 |
+
},
|
| 440 |
+
"cadence_spm": {
|
| 441 |
+
"label": "Cadence",
|
| 442 |
+
"unit": "pas/min",
|
| 443 |
+
"description_low": "Cadence basse par rapport à la zone attendue.",
|
| 444 |
+
"description_normal": "Cadence dans la zone attendue.",
|
| 445 |
+
"description_high": "Cadence élevée par rapport à la zone attendue."
|
| 446 |
+
},
|
| 447 |
+
"contact_pct": {
|
| 448 |
+
"label": "Temps de contact",
|
| 449 |
+
"unit": "%",
|
| 450 |
+
"description_low": "Temps de contact plutôt faible.",
|
| 451 |
+
"description_normal": "Temps de contact dans la zone attendue.",
|
| 452 |
+
"description_high": "Temps de contact élevé, pouvant traduire une dynamique de course réduite."
|
| 453 |
+
},
|
| 454 |
+
"flight_pct": {
|
| 455 |
+
"label": "Temps de vol",
|
| 456 |
+
"unit": "%",
|
| 457 |
+
"description_low": "Temps de vol plutôt faible.",
|
| 458 |
+
"description_normal": "Temps de vol dans la zone attendue.",
|
| 459 |
+
"description_high": "Temps de vol élevé par rapport à la zone attendue."
|
| 460 |
+
},
|
| 461 |
+
"asymmetry_pct": {
|
| 462 |
+
"label": "Asymétrie talon",
|
| 463 |
+
"unit": "%",
|
| 464 |
+
"description_low": "Asymétrie faible.",
|
| 465 |
+
"description_normal": "Asymétrie dans la zone acceptable.",
|
| 466 |
+
"description_high": "Asymétrie élevée, à surveiller."
|
| 467 |
+
},
|
| 468 |
+
"foot_rotation_deg": {
|
| 469 |
+
"label": "Différence rotation G/D",
|
| 470 |
+
"unit": "°",
|
| 471 |
+
"description_low": "Différence de rotation plutôt faible.",
|
| 472 |
+
"description_normal": "Différence de rotation dans la zone attendue.",
|
| 473 |
+
"description_high": "Différence de rotation élevée, pouvant majorer certaines contraintes mécaniques."
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def clamp(value, min_value=0, max_value=100):
|
| 479 |
+
return max(min_value, min(max_value, value))
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def classify_value(value, low, high):
|
| 483 |
+
if value is None or pd.isna(value):
|
| 484 |
+
return "non disponible"
|
| 485 |
+
if value < low:
|
| 486 |
+
return "basse"
|
| 487 |
+
if value > high:
|
| 488 |
+
return "élevée"
|
| 489 |
+
return "normale"
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def compute_deviation_score(value, low, high):
|
| 493 |
+
if value is None or pd.isna(value):
|
| 494 |
+
return 0.0
|
| 495 |
+
|
| 496 |
+
if low <= value <= high:
|
| 497 |
+
return 0.0
|
| 498 |
+
|
| 499 |
+
if value < low:
|
| 500 |
+
if low == 0:
|
| 501 |
+
return 0.0
|
| 502 |
+
return round((low - value) / low, 3)
|
| 503 |
+
|
| 504 |
+
if value > high:
|
| 505 |
+
if high == 0:
|
| 506 |
+
return 0.0
|
| 507 |
+
return round((value - high) / high, 3)
|
| 508 |
+
|
| 509 |
+
return 0.0
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def get_priority(status, deviation_score, variable_key=None):
|
| 513 |
+
if status == "normale":
|
| 514 |
+
return "RAS"
|
| 515 |
+
|
| 516 |
+
if status == "basse":
|
| 517 |
+
if variable_key in ["cadence_spm", "flight_pct"]:
|
| 518 |
+
return "modérée"
|
| 519 |
+
return "faible"
|
| 520 |
+
|
| 521 |
+
if status == "élevée":
|
| 522 |
+
if deviation_score >= 0.20:
|
| 523 |
+
return "élevée"
|
| 524 |
+
return "modérée"
|
| 525 |
+
|
| 526 |
+
return "RAS"
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def priority_to_points(priority):
|
| 530 |
+
mapping = {"RAS": 0, "faible": 1, "modérée": 2, "élevée": 3}
|
| 531 |
+
return mapping.get(priority, 0)
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def pattern_priority_to_points(priority):
|
| 535 |
+
mapping = {"modérée": 2, "élevée": 3}
|
| 536 |
+
return mapping.get(priority, 0)
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def build_analysis_inputs(row, metrics, thresholds, attaque_finale):
|
| 540 |
+
merged_data = {
|
| 541 |
+
"heel_force_N": metrics["force_talon_moy"],
|
| 542 |
+
"heel_pressure_N_cm2": metrics["pression_talon_moy"],
|
| 543 |
+
"cadence_spm": row["Cadence (pas/min)"],
|
| 544 |
+
"contact_pct": row["Contact (%)"],
|
| 545 |
+
"flight_pct": row["Flight (%)"],
|
| 546 |
+
"asymmetry_pct": metrics["asym_talon"],
|
| 547 |
+
"foot_rotation_deg": metrics["diff_rotation"],
|
| 548 |
+
"impact_score": metrics["contraintes"],
|
| 549 |
+
"dynamic_score": metrics["dynamique"],
|
| 550 |
+
"symmetry_score": metrics["symetrie"],
|
| 551 |
+
"rollover_score": metrics["deroule"],
|
| 552 |
+
"attack_type": attaque_finale,
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
thresholds_analysis = {
|
| 556 |
+
"heel_force_N": {
|
| 557 |
+
"low": thresholds["force_n_low"],
|
| 558 |
+
"high": thresholds["force_n_high"],
|
| 559 |
+
},
|
| 560 |
+
"heel_pressure_N_cm2": {
|
| 561 |
+
"low": thresholds["pression_low"],
|
| 562 |
+
"high": thresholds["pression_high"],
|
| 563 |
+
},
|
| 564 |
+
"cadence_spm": {
|
| 565 |
+
"low": thresholds["cadence_low"],
|
| 566 |
+
"high": thresholds["cadence_high"],
|
| 567 |
+
},
|
| 568 |
+
"contact_pct": {
|
| 569 |
+
"low": thresholds["contact_low"],
|
| 570 |
+
"high": thresholds["contact_high"],
|
| 571 |
+
},
|
| 572 |
+
"flight_pct": {
|
| 573 |
+
"low": thresholds["flight_low"],
|
| 574 |
+
"high": thresholds["flight_high"],
|
| 575 |
+
},
|
| 576 |
+
"asymmetry_pct": {
|
| 577 |
+
"low": thresholds["asym_low"],
|
| 578 |
+
"high": thresholds["asym_high"],
|
| 579 |
+
},
|
| 580 |
+
"foot_rotation_deg": {
|
| 581 |
+
"low": thresholds["rotation_low"],
|
| 582 |
+
"high": thresholds["rotation_high"],
|
| 583 |
+
},
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
return merged_data, thresholds_analysis
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def analyze_variable(key, value, thresholds):
|
| 590 |
+
if key not in ANALYSIS_CONFIG:
|
| 591 |
+
return None
|
| 592 |
+
if key not in thresholds:
|
| 593 |
+
return None
|
| 594 |
+
|
| 595 |
+
config = ANALYSIS_CONFIG[key]
|
| 596 |
+
low = thresholds[key]["low"]
|
| 597 |
+
high = thresholds[key]["high"]
|
| 598 |
+
|
| 599 |
+
status = classify_value(value, low, high)
|
| 600 |
+
deviation_score = compute_deviation_score(value, low, high)
|
| 601 |
+
priority = get_priority(status, deviation_score, variable_key=key)
|
| 602 |
+
|
| 603 |
+
if status == "basse":
|
| 604 |
+
interpretation = config["description_low"]
|
| 605 |
+
elif status == "élevée":
|
| 606 |
+
interpretation = config["description_high"]
|
| 607 |
+
elif status == "normale":
|
| 608 |
+
interpretation = config["description_normal"]
|
| 609 |
+
else:
|
| 610 |
+
interpretation = "Donnée non disponible."
|
| 611 |
+
|
| 612 |
+
return {
|
| 613 |
+
"variable": key,
|
| 614 |
+
"label": config["label"],
|
| 615 |
+
"value": value,
|
| 616 |
+
"unit": config["unit"],
|
| 617 |
+
"low": low,
|
| 618 |
+
"high": high,
|
| 619 |
+
"status": status,
|
| 620 |
+
"priority": priority,
|
| 621 |
+
"deviation_score": deviation_score,
|
| 622 |
+
"interpretation": interpretation,
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def run_biomech_analysis(merged_data, thresholds_analysis):
|
| 627 |
+
results = []
|
| 628 |
+
for key in ANALYSIS_CONFIG.keys():
|
| 629 |
+
result = analyze_variable(key, merged_data.get(key), thresholds_analysis)
|
| 630 |
+
if result is not None:
|
| 631 |
+
results.append(result)
|
| 632 |
+
return pd.DataFrame(results)
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def get_status_map(df_analysis):
|
| 636 |
+
if df_analysis.empty:
|
| 637 |
+
return {}
|
| 638 |
+
return dict(zip(df_analysis["variable"], df_analysis["status"]))
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def is_high(status_map, key):
|
| 642 |
+
return status_map.get(key) == "élevée"
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
def is_low(status_map, key):
|
| 646 |
+
return status_map.get(key) == "basse"
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def detect_combined_patterns(merged_data, df_analysis):
|
| 650 |
+
patterns = []
|
| 651 |
+
status_map = get_status_map(df_analysis)
|
| 652 |
+
|
| 653 |
+
impact_score = merged_data.get("impact_score")
|
| 654 |
+
dynamic_score = merged_data.get("dynamic_score")
|
| 655 |
+
symmetry_score = merged_data.get("symmetry_score")
|
| 656 |
+
rollover_score = merged_data.get("rollover_score")
|
| 657 |
+
attack_type = merged_data.get("attack_type", "indéterminée")
|
| 658 |
+
|
| 659 |
+
if is_high(status_map, "heel_force_N") and is_high(status_map, "heel_pressure_N_cm2"):
|
| 660 |
+
patterns.append({
|
| 661 |
+
"name": "impact_load_flag",
|
| 662 |
+
"title": "Contrainte d'impact majorée",
|
| 663 |
+
"priority": "élevée",
|
| 664 |
+
"category": "impact",
|
| 665 |
+
"message": "La combinaison d'une force talon élevée et d'une pression talon élevée suggère une contrainte d'impact majorée."
|
| 666 |
+
})
|
| 667 |
+
elif is_high(status_map, "heel_force_N") or is_high(status_map, "heel_pressure_N_cm2"):
|
| 668 |
+
patterns.append({
|
| 669 |
+
"name": "impact_signal_flag",
|
| 670 |
+
"title": "Signal d'impact à surveiller",
|
| 671 |
+
"priority": "modérée",
|
| 672 |
+
"category": "impact",
|
| 673 |
+
"message": "Un marqueur d'impact talonnier est au-dessus de la zone attendue."
|
| 674 |
+
})
|
| 675 |
+
|
| 676 |
+
if is_low(status_map, "cadence_spm") and is_high(status_map, "contact_pct"):
|
| 677 |
+
patterns.append({
|
| 678 |
+
"name": "low_dynamics_flag",
|
| 679 |
+
"title": "Dynamique de course possiblement réduite",
|
| 680 |
+
"priority": "élevée",
|
| 681 |
+
"category": "dynamics",
|
| 682 |
+
"message": "La combinaison d'une cadence basse et d'un temps de contact élevé évoque une dynamique de course potentiellement réduite."
|
| 683 |
+
})
|
| 684 |
+
|
| 685 |
+
if is_low(status_map, "flight_pct") and is_high(status_map, "contact_pct"):
|
| 686 |
+
patterns.append({
|
| 687 |
+
"name": "reactivity_flag",
|
| 688 |
+
"title": "Réactivité mécanique possiblement diminuée",
|
| 689 |
+
"priority": "modérée",
|
| 690 |
+
"category": "dynamics",
|
| 691 |
+
"message": "Le temps de vol bas associé à un temps de contact élevé évoque une moindre réactivité mécanique."
|
| 692 |
+
})
|
| 693 |
+
|
| 694 |
+
if pd.notna(dynamic_score) and dynamic_score < 50:
|
| 695 |
+
if is_high(status_map, "contact_pct") or is_low(status_map, "flight_pct"):
|
| 696 |
+
patterns.append({
|
| 697 |
+
"name": "global_dynamic_flag",
|
| 698 |
+
"title": "Déficit dynamique renforcé",
|
| 699 |
+
"priority": "élevée",
|
| 700 |
+
"category": "dynamics",
|
| 701 |
+
"message": "Le score de dynamique bas renforce l'hypothèse d'une dynamique de course altérée."
|
| 702 |
+
})
|
| 703 |
+
|
| 704 |
+
if is_high(status_map, "asymmetry_pct"):
|
| 705 |
+
if pd.notna(symmetry_score) and symmetry_score < 60:
|
| 706 |
+
patterns.append({
|
| 707 |
+
"name": "asymmetry_flag",
|
| 708 |
+
"title": "Asymétrie renforcée",
|
| 709 |
+
"priority": "élevée",
|
| 710 |
+
"category": "symmetry",
|
| 711 |
+
"message": "L'asymétrie mesurée est élevée et cohérente avec un score de symétrie faible."
|
| 712 |
+
})
|
| 713 |
+
else:
|
| 714 |
+
patterns.append({
|
| 715 |
+
"name": "asymmetry_watch_flag",
|
| 716 |
+
"title": "Asymétrie à surveiller",
|
| 717 |
+
"priority": "modérée",
|
| 718 |
+
"category": "symmetry",
|
| 719 |
+
"message": "Une asymétrie au-dessus de la zone attendue est observée."
|
| 720 |
+
})
|
| 721 |
+
|
| 722 |
+
if is_high(status_map, "foot_rotation_deg"):
|
| 723 |
+
if pd.notna(rollover_score) and rollover_score < 60:
|
| 724 |
+
patterns.append({
|
| 725 |
+
"name": "mechanical_pattern_flag",
|
| 726 |
+
"title": "Pattern mécanique distal à surveiller",
|
| 727 |
+
"priority": "modérée",
|
| 728 |
+
"category": "mechanics",
|
| 729 |
+
"message": "La différence de rotation élevée associée à un déroulé peu efficient suggère un pattern mécanique distal à surveiller."
|
| 730 |
+
})
|
| 731 |
+
else:
|
| 732 |
+
patterns.append({
|
| 733 |
+
"name": "rotation_flag",
|
| 734 |
+
"title": "Différence de rotation élevée",
|
| 735 |
+
"priority": "modérée",
|
| 736 |
+
"category": "mechanics",
|
| 737 |
+
"message": "La différence de rotation est au-dessus de la zone attendue."
|
| 738 |
+
})
|
| 739 |
+
|
| 740 |
+
if attack_type == "attaque talon":
|
| 741 |
+
if is_high(status_map, "heel_force_N") or is_high(status_map, "heel_pressure_N_cm2"):
|
| 742 |
+
patterns.append({
|
| 743 |
+
"name": "rearfoot_impact_context",
|
| 744 |
+
"title": "Attaque talon avec charge d'impact marquée",
|
| 745 |
+
"priority": "modérée",
|
| 746 |
+
"category": "attack",
|
| 747 |
+
"message": "Le profil d'attaque talon est associé à des marqueurs d'impact élevés."
|
| 748 |
+
})
|
| 749 |
+
|
| 750 |
+
if pd.notna(dynamic_score) and pd.notna(rollover_score):
|
| 751 |
+
if dynamic_score < 50 and rollover_score < 55:
|
| 752 |
+
patterns.append({
|
| 753 |
+
"name": "global_efficiency_flag",
|
| 754 |
+
"title": "Efficience mécanique possiblement réduite",
|
| 755 |
+
"priority": "modérée",
|
| 756 |
+
"category": "global",
|
| 757 |
+
"message": "La combinaison d'un score de dynamique bas et d'un déroulé faible évoque une efficience mécanique possiblement réduite."
|
| 758 |
+
})
|
| 759 |
+
|
| 760 |
+
return deduplicate_patterns(patterns)
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
def deduplicate_patterns(patterns):
|
| 764 |
+
seen = set()
|
| 765 |
+
unique_patterns = []
|
| 766 |
+
for pattern in patterns:
|
| 767 |
+
key = (pattern["name"], pattern["title"])
|
| 768 |
+
if key not in seen:
|
| 769 |
+
seen.add(key)
|
| 770 |
+
unique_patterns.append(pattern)
|
| 771 |
+
return unique_patterns
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
def compute_domain_scores(merged_data, df_analysis, patterns):
|
| 775 |
+
if df_analysis.empty:
|
| 776 |
+
return {
|
| 777 |
+
"impact": 0,
|
| 778 |
+
"dynamics": 0,
|
| 779 |
+
"symmetry": 0,
|
| 780 |
+
"mechanics": 0,
|
| 781 |
+
"attack": 0,
|
| 782 |
+
"global": 0,
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
row_map = {row["variable"]: row for _, row in df_analysis.iterrows()}
|
| 786 |
+
|
| 787 |
+
def var_points(var_name, weight=1.0):
|
| 788 |
+
row = row_map.get(var_name)
|
| 789 |
+
if not row:
|
| 790 |
+
return 0.0
|
| 791 |
+
base = priority_to_points(row["priority"]) * 10
|
| 792 |
+
bonus = row["deviation_score"] * 20
|
| 793 |
+
return (base + bonus) * weight
|
| 794 |
+
|
| 795 |
+
def pattern_points(category):
|
| 796 |
+
total = 0
|
| 797 |
+
for p in patterns:
|
| 798 |
+
if p["category"] == category:
|
| 799 |
+
total += pattern_priority_to_points(p["priority"]) * 10
|
| 800 |
+
return total
|
| 801 |
+
|
| 802 |
+
impact_score_profile = merged_data.get("impact_score")
|
| 803 |
+
dynamic_score_profile = merged_data.get("dynamic_score")
|
| 804 |
+
symmetry_score_profile = merged_data.get("symmetry_score")
|
| 805 |
+
rollover_score_profile = merged_data.get("rollover_score")
|
| 806 |
+
|
| 807 |
+
impact = 0
|
| 808 |
+
impact += var_points("heel_force_N", 1.2)
|
| 809 |
+
impact += var_points("heel_pressure_N_cm2", 1.2)
|
| 810 |
+
impact += pattern_points("impact")
|
| 811 |
+
impact += pattern_points("attack")
|
| 812 |
+
if pd.notna(impact_score_profile) and impact_score_profile >= 70:
|
| 813 |
+
impact += (impact_score_profile - 70) * 0.2
|
| 814 |
+
|
| 815 |
+
dynamics = 0
|
| 816 |
+
dynamics += var_points("cadence_spm", 1.0)
|
| 817 |
+
dynamics += var_points("contact_pct", 1.2)
|
| 818 |
+
dynamics += var_points("flight_pct", 1.0)
|
| 819 |
+
dynamics += pattern_points("dynamics")
|
| 820 |
+
if pd.notna(dynamic_score_profile) and dynamic_score_profile < 60:
|
| 821 |
+
dynamics += (60 - dynamic_score_profile) * 0.5
|
| 822 |
+
|
| 823 |
+
symmetry = 0
|
| 824 |
+
symmetry += var_points("asymmetry_pct", 1.5)
|
| 825 |
+
symmetry += pattern_points("symmetry")
|
| 826 |
+
if pd.notna(symmetry_score_profile) and symmetry_score_profile < 70:
|
| 827 |
+
symmetry += (70 - symmetry_score_profile) * 0.5
|
| 828 |
+
|
| 829 |
+
mechanics = 0
|
| 830 |
+
mechanics += var_points("foot_rotation_deg", 1.4)
|
| 831 |
+
mechanics += pattern_points("mechanics")
|
| 832 |
+
if pd.notna(rollover_score_profile) and rollover_score_profile < 65:
|
| 833 |
+
mechanics += (65 - rollover_score_profile) * 0.35
|
| 834 |
+
|
| 835 |
+
attack = 0
|
| 836 |
+
attack += pattern_points("attack")
|
| 837 |
+
attack += 0.5 * var_points("heel_force_N", 1.0)
|
| 838 |
+
attack += 0.5 * var_points("heel_pressure_N_cm2", 1.0)
|
| 839 |
+
|
| 840 |
+
global_score = (
|
| 841 |
+
impact * 0.30 +
|
| 842 |
+
dynamics * 0.30 +
|
| 843 |
+
symmetry * 0.20 +
|
| 844 |
+
mechanics * 0.20
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
return {
|
| 848 |
+
"impact": int(clamp(round(impact))),
|
| 849 |
+
"dynamics": int(clamp(round(dynamics))),
|
| 850 |
+
"symmetry": int(clamp(round(symmetry))),
|
| 851 |
+
"mechanics": int(clamp(round(mechanics))),
|
| 852 |
+
"attack": int(clamp(round(attack))),
|
| 853 |
+
"global": int(clamp(round(global_score))),
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def get_domain_label(score):
|
| 858 |
+
if score >= 75:
|
| 859 |
+
return "élevé"
|
| 860 |
+
if score >= 45:
|
| 861 |
+
return "modéré"
|
| 862 |
+
if score >= 20:
|
| 863 |
+
return "léger"
|
| 864 |
+
return "faible"
|
| 865 |
+
|
| 866 |
+
|
| 867 |
+
def get_primary_domains(domain_scores, top_n=3):
|
| 868 |
+
filtered = {k: v for k, v in domain_scores.items() if k != "global"}
|
| 869 |
+
return sorted(filtered.items(), key=lambda x: x[1], reverse=True)[:top_n]
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
def compute_global_summary_v3(df_analysis, patterns, domain_scores):
|
| 873 |
+
if df_analysis.empty:
|
| 874 |
+
return {
|
| 875 |
+
"normal_count": 0,
|
| 876 |
+
"attention_count": 0,
|
| 877 |
+
"high_priority_count": 0,
|
| 878 |
+
"moderate_priority_count": 0,
|
| 879 |
+
"pattern_count": 0,
|
| 880 |
+
"global_level": "indéterminé",
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
normal_count = int((df_analysis["status"] == "normale").sum())
|
| 884 |
+
attention_count = int((df_analysis["status"] != "normale").sum())
|
| 885 |
+
high_priority_count = int((df_analysis["priority"] == "élevée").sum())
|
| 886 |
+
moderate_priority_count = int((df_analysis["priority"] == "modérée").sum())
|
| 887 |
+
|
| 888 |
+
pattern_high = sum(1 for p in patterns if p["priority"] == "élevée")
|
| 889 |
+
pattern_moderate = sum(1 for p in patterns if p["priority"] == "modérée")
|
| 890 |
+
global_domain_score = domain_scores.get("global", 0)
|
| 891 |
+
|
| 892 |
+
total_high = high_priority_count + pattern_high
|
| 893 |
+
total_moderate = moderate_priority_count + pattern_moderate
|
| 894 |
+
|
| 895 |
+
if total_high >= 2 or global_domain_score >= 75:
|
| 896 |
+
global_level = "élevé"
|
| 897 |
+
elif total_high == 1 or total_moderate >= 3 or global_domain_score >= 45:
|
| 898 |
+
global_level = "modéré"
|
| 899 |
+
elif attention_count >= 1 or len(patterns) >= 1 or global_domain_score >= 20:
|
| 900 |
+
global_level = "léger"
|
| 901 |
+
else:
|
| 902 |
+
global_level = "faible"
|
| 903 |
+
|
| 904 |
+
return {
|
| 905 |
+
"normal_count": normal_count,
|
| 906 |
+
"attention_count": attention_count,
|
| 907 |
+
"high_priority_count": total_high,
|
| 908 |
+
"moderate_priority_count": total_moderate,
|
| 909 |
+
"pattern_count": len(patterns),
|
| 910 |
+
"global_level": global_level,
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
def generate_global_narrative(summary, domain_scores):
|
| 915 |
+
if summary["global_level"] == "faible":
|
| 916 |
+
return (
|
| 917 |
+
"Le profil est globalement cohérent par rapport aux seuils individualisés, "
|
| 918 |
+
"sans signal biomécanique majeur détecté à ce stade."
|
| 919 |
+
)
|
| 920 |
+
|
| 921 |
+
label_map = {
|
| 922 |
+
"impact": "contrainte d'impact",
|
| 923 |
+
"dynamics": "dynamique de course",
|
| 924 |
+
"symmetry": "symétrie",
|
| 925 |
+
"mechanics": "mécanique distale",
|
| 926 |
+
"attack": "organisation de l'attaque",
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
top_domains = get_primary_domains(domain_scores, top_n=3)
|
| 930 |
+
top_labels = [label_map.get(name, name) for name, score in top_domains if score >= 20]
|
| 931 |
+
domains_text = ", ".join(top_labels) if top_labels else "plusieurs dimensions biomécaniques"
|
| 932 |
+
|
| 933 |
+
if summary["global_level"] == "léger":
|
| 934 |
+
return f"Le profil met en évidence quelques signaux isolés, principalement autour de : {domains_text}."
|
| 935 |
+
if summary["global_level"] == "modéré":
|
| 936 |
+
return f"Le profil présente plusieurs points d'attention cohérents, notamment sur : {domains_text}."
|
| 937 |
+
return (
|
| 938 |
+
f"Le profil présente plusieurs signaux convergents, en particulier sur : {domains_text}. "
|
| 939 |
+
"Une interprétation approfondie est justifiée avant la phase de recommandations."
|
| 940 |
+
)
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
def prepare_analysis_table(df_analysis):
|
| 944 |
+
if df_analysis.empty:
|
| 945 |
+
return df_analysis
|
| 946 |
+
|
| 947 |
+
priority_order = {"élevée": 3, "modérée": 2, "faible": 1, "RAS": 0}
|
| 948 |
+
status_order = {"élevée": 2, "basse": 1, "normale": 0, "non disponible": -1}
|
| 949 |
+
|
| 950 |
+
df = df_analysis.copy()
|
| 951 |
+
df["priority_rank"] = df["priority"].map(priority_order).fillna(0)
|
| 952 |
+
df["status_rank"] = df["status"].map(status_order).fillna(-1)
|
| 953 |
+
|
| 954 |
+
df = df.sort_values(
|
| 955 |
+
by=["priority_rank", "status_rank", "deviation_score"],
|
| 956 |
+
ascending=[False, False, False]
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
return df.drop(columns=["priority_rank", "status_rank"])
|
| 960 |
+
|
| 961 |
+
|
| 962 |
+
def display_status_badge(status):
|
| 963 |
+
if status == "normale":
|
| 964 |
+
st.success("Normale")
|
| 965 |
+
elif status == "basse":
|
| 966 |
+
st.warning("Basse")
|
| 967 |
+
elif status == "élevée":
|
| 968 |
+
st.error("Élevée")
|
| 969 |
+
else:
|
| 970 |
+
st.info("Non disponible")
|
| 971 |
+
|
| 972 |
+
|
| 973 |
+
def display_priority_badge(priority):
|
| 974 |
+
if priority == "RAS":
|
| 975 |
+
st.success("RAS")
|
| 976 |
+
elif priority == "faible":
|
| 977 |
+
st.info("Faible")
|
| 978 |
+
elif priority == "modérée":
|
| 979 |
+
st.warning("Modérée")
|
| 980 |
+
elif priority == "élevée":
|
| 981 |
+
st.error("Élevée")
|
| 982 |
+
else:
|
| 983 |
+
st.info(priority)
|
| 984 |
+
|
| 985 |
+
|
| 986 |
+
def display_pattern_badge(priority):
|
| 987 |
+
if priority == "élevée":
|
| 988 |
+
st.error("Pattern prioritaire")
|
| 989 |
+
elif priority == "modérée":
|
| 990 |
+
st.warning("Pattern à surveiller")
|
| 991 |
+
else:
|
| 992 |
+
st.info(priority)
|
| 993 |
+
|
| 994 |
+
|
| 995 |
+
def render_analysis_tab_v3(merged_data, thresholds_analysis):
|
| 996 |
+
st.subheader("📈 Analyse des données")
|
| 997 |
+
|
| 998 |
+
df_analysis = run_biomech_analysis(merged_data, thresholds_analysis)
|
| 999 |
+
patterns = detect_combined_patterns(merged_data, df_analysis)
|
| 1000 |
+
domain_scores = compute_domain_scores(merged_data, df_analysis, patterns)
|
| 1001 |
+
summary = compute_global_summary_v3(df_analysis, patterns, domain_scores)
|
| 1002 |
+
narrative = generate_global_narrative(summary, domain_scores)
|
| 1003 |
+
|
| 1004 |
+
c1, c2, c3, c4, c5 = st.columns(5)
|
| 1005 |
+
c1.metric("Variables normales", summary["normal_count"])
|
| 1006 |
+
c2.metric("Points d'attention", summary["attention_count"])
|
| 1007 |
+
c3.metric("Priorités hautes", summary["high_priority_count"])
|
| 1008 |
+
c4.metric("Patterns détectés", summary["pattern_count"])
|
| 1009 |
+
c5.metric("Niveau global", summary["global_level"].capitalize())
|
| 1010 |
+
|
| 1011 |
+
st.markdown("---")
|
| 1012 |
+
st.markdown("### Conclusion synthétique")
|
| 1013 |
+
|
| 1014 |
+
if summary["global_level"] == "faible":
|
| 1015 |
+
st.success(narrative)
|
| 1016 |
+
elif summary["global_level"] == "léger":
|
| 1017 |
+
st.info(narrative)
|
| 1018 |
+
elif summary["global_level"] == "modéré":
|
| 1019 |
+
st.warning(narrative)
|
| 1020 |
+
else:
|
| 1021 |
+
st.error(narrative)
|
| 1022 |
+
|
| 1023 |
+
st.markdown("---")
|
| 1024 |
+
st.markdown("### Scores par domaine")
|
| 1025 |
+
|
| 1026 |
+
d1, d2, d3, d4, d5 = st.columns(5)
|
| 1027 |
+
d1.metric("Impact", f"{domain_scores['impact']}/100")
|
| 1028 |
+
d2.metric("Dynamique", f"{domain_scores['dynamics']}/100")
|
| 1029 |
+
d3.metric("Symétrie", f"{domain_scores['symmetry']}/100")
|
| 1030 |
+
d4.metric("Mécanique", f"{domain_scores['mechanics']}/100")
|
| 1031 |
+
d5.metric("Attaque", f"{domain_scores['attack']}/100")
|
| 1032 |
+
|
| 1033 |
+
st.markdown("---")
|
| 1034 |
+
st.markdown("### Synthèse par variable")
|
| 1035 |
+
|
| 1036 |
+
if df_analysis.empty:
|
| 1037 |
+
st.info("Aucune donnée disponible pour l'analyse.")
|
| 1038 |
+
else:
|
| 1039 |
+
df_display = prepare_analysis_table(df_analysis)[[
|
| 1040 |
+
"label", "value", "unit", "low", "high", "status", "priority"
|
| 1041 |
+
]].copy()
|
| 1042 |
+
|
| 1043 |
+
df_display.columns = [
|
| 1044 |
+
"Variable", "Valeur", "Unité", "Seuil bas", "Seuil haut", "Statut", "Priorité"
|
| 1045 |
+
]
|
| 1046 |
+
st.dataframe(df_display, hide_index=True, use_container_width=True)
|
| 1047 |
+
|
| 1048 |
+
st.markdown("---")
|
| 1049 |
+
st.markdown("### Patterns biomécaniques détectés")
|
| 1050 |
+
|
| 1051 |
+
if not patterns:
|
| 1052 |
+
st.success("Aucun pattern combiné majeur détecté.")
|
| 1053 |
+
else:
|
| 1054 |
+
for pattern in patterns:
|
| 1055 |
+
col1, col2 = st.columns([4, 1])
|
| 1056 |
+
with col1:
|
| 1057 |
+
st.markdown(f"**{pattern['title']}**")
|
| 1058 |
+
st.write(pattern["message"])
|
| 1059 |
+
with col2:
|
| 1060 |
+
display_pattern_badge(pattern["priority"])
|
| 1061 |
+
st.markdown("---")
|
| 1062 |
+
|
| 1063 |
+
st.markdown("### Détail par variable")
|
| 1064 |
+
if not df_analysis.empty:
|
| 1065 |
+
df_sorted = prepare_analysis_table(df_analysis)
|
| 1066 |
+
for _, row in df_sorted.iterrows():
|
| 1067 |
+
col1, col2, col3 = st.columns([2.5, 1, 1])
|
| 1068 |
+
|
| 1069 |
+
with col1:
|
| 1070 |
+
st.markdown(f"**{row['label']}**")
|
| 1071 |
+
st.write(
|
| 1072 |
+
f"Valeur mesurée : **{row['value']:.2f} {row['unit']}** \n"
|
| 1073 |
+
f"Zone attendue : **{row['low']:.2f} à {row['high']:.2f} {row['unit']}**"
|
| 1074 |
+
if pd.notna(row["value"]) else
|
| 1075 |
+
f"Valeur mesurée : **N/A** \nZone attendue : **{row['low']:.2f} à {row['high']:.2f} {row['unit']}**"
|
| 1076 |
+
)
|
| 1077 |
+
st.write(row["interpretation"])
|
| 1078 |
+
|
| 1079 |
+
with col2:
|
| 1080 |
+
st.markdown("**Statut**")
|
| 1081 |
+
display_status_badge(row["status"])
|
| 1082 |
+
|
| 1083 |
+
with col3:
|
| 1084 |
+
st.markdown("**Priorité**")
|
| 1085 |
+
display_priority_badge(row["priority"])
|
| 1086 |
+
|
| 1087 |
+
st.markdown("---")
|
| 1088 |
+
|
| 1089 |
+
st.markdown("### Axes dominants à prioriser")
|
| 1090 |
+
top_domains = get_primary_domains(domain_scores, top_n=3)
|
| 1091 |
+
domain_name_map = {
|
| 1092 |
+
"impact": "Impact",
|
| 1093 |
+
"dynamics": "Dynamique",
|
| 1094 |
+
"symmetry": "Symétrie",
|
| 1095 |
+
"mechanics": "Mécanique distale",
|
| 1096 |
+
"attack": "Attaque",
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
shown = False
|
| 1100 |
+
for domain_key, score in top_domains:
|
| 1101 |
+
if score >= 20:
|
| 1102 |
+
shown = True
|
| 1103 |
+
st.markdown(f"- **{domain_name_map.get(domain_key, domain_key)}** : {score}/100 (**{get_domain_label(score)}**)")
|
| 1104 |
+
|
| 1105 |
+
if not shown:
|
| 1106 |
+
st.success("Aucun axe dominant majeur ne se dégage à ce stade.")
|
| 1107 |
|
| 1108 |
# Charge CSV
|
| 1109 |
dfs = []
|
|
|
|
| 1175 |
if matched_pdf and matched_pdf.get("attaque_pdf") and matched_pdf["attaque_pdf"] != "indéterminée":
|
| 1176 |
attaque_finale = matched_pdf["attaque_pdf"]
|
| 1177 |
|
| 1178 |
+
merged_data, thresholds_analysis = build_analysis_inputs(
|
| 1179 |
+
row=row,
|
| 1180 |
+
metrics=metrics,
|
| 1181 |
+
thresholds=thresholds,
|
| 1182 |
+
attaque_finale=attaque_finale,
|
| 1183 |
+
)
|
| 1184 |
|
| 1185 |
def build_summary(row, metrics, attaque_finale):
|
| 1186 |
contraintes_txt = (
|
|
|
|
| 1208 |
|
| 1209 |
summary = build_summary(row, metrics, attaque_finale)
|
| 1210 |
|
| 1211 |
+
tab_profil, tab_seuils, tab_analyse, tab_pdf = st.tabs(
|
| 1212 |
+
["Profil biomécanique", "Seuils individualisés", "Analyse", "Apports du PDF Zebris"]
|
| 1213 |
)
|
| 1214 |
|
| 1215 |
with tab_profil:
|
|
|
|
| 1358 |
with s3:
|
| 1359 |
st.dataframe(symetrie_df, hide_index=True, use_container_width=True)
|
| 1360 |
|
| 1361 |
+
with tab_analyse:
|
| 1362 |
+
render_analysis_tab_v3(merged_data, thresholds_analysis)
|
| 1363 |
+
|
| 1364 |
with tab_pdf:
|
| 1365 |
if not matched_pdf:
|
| 1366 |
st.info("Aucun PDF Zebris associé à cet athlète n’a été trouvé.")
|