|
import streamlit as st |
|
import pandas as pd |
|
from st_image_carousel import image_carousel |
|
from st_circular_kpi import circular_kpi |
|
import base64 |
|
from analytics.scoring import get_player_match_scores |
|
import plotly.express as px |
|
|
|
|
|
def calculate_stats(df, column, agg_func='sum', round_digits=1): |
|
"""Calcule min, max, mean pour une colonne groupée par joueur""" |
|
stats = df.groupby('Nom').agg({column: agg_func}) |
|
return { |
|
'min': float(stats.min().round(round_digits).values[0]), |
|
'max': float(stats.max().round(round_digits).values[0]), |
|
'mean': float(stats.mean().round(round_digits).values[0]) |
|
} |
|
|
|
def show_player_analysis(df): |
|
"""Composant simple pour l'analyse des joueuses""" |
|
|
|
|
|
actions_stats = calculate_stats(df, 'Nb_actions', 'sum') |
|
matches_stats = calculate_stats(df, 'Match', 'nunique') |
|
|
|
|
|
player_avg_stats = df.groupby('Nom').agg({ |
|
'Nb_actions': 'sum', |
|
'Match': 'nunique' |
|
}).assign( |
|
avg_per_match=lambda x: x['Nb_actions'] / x['Match'] |
|
) |
|
|
|
avg_per_match_stats = { |
|
'min': float(player_avg_stats['avg_per_match'].min()), |
|
'max': float(player_avg_stats['avg_per_match'].max()), |
|
'mean': float(player_avg_stats['avg_per_match'].mean().round(1)) |
|
} |
|
|
|
|
|
joueurs_images = [ |
|
{ |
|
"name": f"{row['Prenom']} {row['Nom']}", |
|
"url": "" |
|
} |
|
for _, row in sorted(df[['Nom', 'Prenom']].drop_duplicates().iterrows(), |
|
key=lambda x: (x[1]['Nom'], x[1]['Prenom'])) |
|
] |
|
|
|
|
|
matchs_images = [ |
|
{ |
|
"name": match_name, |
|
"url": "" |
|
} |
|
for match_name in sorted(df['Match'].unique()) |
|
] |
|
|
|
result = image_carousel( |
|
images=joueurs_images, |
|
selected_image=None, |
|
background_color="#ffffff", |
|
active_border_color="#000000", |
|
active_glow_color="rgba(0, 0, 0, 0.7)", |
|
fallback_background="#ffffff", |
|
fallback_gradient_end="#ffffff", |
|
text_color="#000000", |
|
arrow_color="#31333f", |
|
key="player_carousel" |
|
) |
|
|
|
|
|
if result and result.get('selected_image'): |
|
selected_player = result['selected_image'].split(" ")[1] |
|
else: |
|
selected_player = None |
|
|
|
|
|
if selected_player: |
|
|
|
player_data = df[df['Nom'] == selected_player] |
|
|
|
|
|
player_matches = player_data.groupby('Match')['Nb_actions'].sum() |
|
player_matches = player_matches[player_matches > 0].index.tolist() |
|
|
|
matchs_images_filtered = [ |
|
{ |
|
"name": match_name, |
|
"url": "" |
|
} |
|
for match_name in sorted(player_matches) |
|
] |
|
|
|
|
|
total_actions = player_data['Nb_actions'].sum() |
|
nb_matchs = player_data['Match'].nunique() |
|
avg_per_match = total_actions / nb_matchs if nb_matchs > 0 else 0 |
|
|
|
|
|
player_scores = get_player_match_scores(df) |
|
player_scores_filtered = player_scores[ |
|
(player_scores['Prenom'] == player_data['Prenom'].iloc[0]) & |
|
(player_scores['Nom'] == selected_player) |
|
].sort_values('Match') |
|
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
with col1: |
|
circular_kpi( |
|
value=total_actions, |
|
label="Actions", |
|
range=(-10, actions_stats['max']), |
|
min_value=actions_stats['min'], |
|
max_value=actions_stats['max'], |
|
mean_value=actions_stats['mean'], |
|
color_scheme="blue_purple", |
|
background_color="transparent", |
|
key="actions_kpi" |
|
) |
|
|
|
with col2: |
|
circular_kpi( |
|
value=nb_matchs, |
|
label="Matchs", |
|
range=(0, matches_stats['max']), |
|
min_value=matches_stats['min'], |
|
max_value=matches_stats['max'], |
|
mean_value=matches_stats['mean'], |
|
color_scheme="red", |
|
background_color="transparent", |
|
key="matches_kpi" |
|
) |
|
|
|
with col3: |
|
circular_kpi( |
|
value=avg_per_match.round(1), |
|
label="Moyenne actions par match", |
|
range=(0, avg_per_match_stats['max']), |
|
min_value=avg_per_match_stats['min'], |
|
max_value=avg_per_match_stats['max'], |
|
mean_value=avg_per_match_stats['mean'], |
|
color_scheme="green", |
|
key="avg_per_match_kpi" |
|
) |
|
with col4: |
|
|
|
all_player_scores = get_player_match_scores(df) |
|
|
|
player_averages = all_player_scores.groupby(['Prenom', 'Nom'])['note_match_joueuse'].mean().reset_index() |
|
|
|
global_score_stats = { |
|
'min': float(player_averages['note_match_joueuse'].min().round(1)), |
|
'max': float(player_averages['note_match_joueuse'].max().round(1)), |
|
'mean': float(player_averages['note_match_joueuse'].mean().round(1)) |
|
} |
|
|
|
circular_kpi( |
|
value=player_scores_filtered['note_match_joueuse'].mean().round(1), |
|
label="Note moyenne", |
|
range=(0, 100), |
|
min_value=global_score_stats['min'], |
|
max_value=global_score_stats['max'], |
|
mean_value=global_score_stats['mean'], |
|
color_scheme="blue_purple", |
|
key="note_moyenne_kpi" |
|
) |
|
|
|
|
|
|
|
|
|
if not player_scores_filtered.empty: |
|
|
|
fig = px.line( |
|
player_scores_filtered, |
|
x='Match', |
|
y='note_match_joueuse', |
|
markers=True, |
|
color_discrete_sequence=['black'] |
|
) |
|
|
|
fig.update_layout( |
|
height=400, |
|
showlegend=False, |
|
xaxis_title="", |
|
yaxis_title="Note" |
|
) |
|
|
|
|
|
avg_score = player_scores_filtered['note_match_joueuse'].mean() |
|
fig.add_hline( |
|
y=avg_score, |
|
line_dash="dash", |
|
line_color="red", |
|
annotation_text=f"Moyenne: {avg_score:.1f}", |
|
annotation_position="top right" |
|
) |
|
|
|
|
|
col1, col2, col3 = st.columns([1, 4, 2]) |
|
with col2: |
|
st.plotly_chart(fig, use_container_width=False, key="scores_evolution_chart") |
|
else: |
|
st.info("Aucun score disponible pour cette joueuse.") |
|
|
|
st.divider() |
|
|
|
|
|
col_stats, col_match = st.columns([4, 1]) |
|
|
|
with col_match: |
|
result_match = image_carousel( |
|
images=matchs_images_filtered, |
|
selected_image=None, |
|
background_color="#ffffff", |
|
active_border_color="#000000", |
|
active_glow_color="rgba(0, 0, 0, 0.7)", |
|
fallback_background="#ffffff", |
|
fallback_gradient_end="#ffffff", |
|
text_color="#000000", |
|
arrow_color="#31333f", |
|
orientation="vertical", |
|
key="match_carousel" |
|
) |
|
|
|
|
|
selected_match = None |
|
if result_match and result_match.get('selected_image'): |
|
selected_match = result_match['selected_image'] |
|
|
|
|
|
with col_stats: |
|
|
|
|
|
display_data = player_data |
|
if selected_match: |
|
|
|
display_data = player_data[player_data['Match'] == selected_match] |
|
|
|
|
|
|
|
filtered_total_actions = display_data['Nb_actions'].sum() |
|
filtered_nb_matchs = display_data['Match'].nunique() |
|
filtered_avg_per_match = filtered_total_actions / filtered_nb_matchs if filtered_nb_matchs > 0 else 0 |
|
|
|
|
|
player_match_stats = player_data.groupby('Match')['Nb_actions'].sum() |
|
player_actions_stats = { |
|
'min': float(player_match_stats.min()), |
|
'max': float(player_match_stats.max()), |
|
'mean': float(player_match_stats.mean().round(1)) |
|
} |
|
|
|
|
|
col_actions, col_matchs, col_avg_per_match = st.columns(3) |
|
|
|
with col_actions: |
|
circular_kpi( |
|
value=filtered_total_actions, |
|
label="Actions", |
|
range=(0, player_actions_stats['max']), |
|
min_value=player_actions_stats['min'], |
|
max_value=player_actions_stats['max'], |
|
mean_value=player_actions_stats['mean'], |
|
color_scheme="blue_purple", |
|
background_color="transparent", |
|
key="actions_kpi_by_match" |
|
) |
|
with col_matchs: |
|
|
|
|
|
actions_by_level = display_data.groupby('Niveau')['Nb_actions'].sum().reset_index() |
|
|
|
|
|
all_levels = pd.DataFrame({'Niveau': [0, 1, 2, 3]}) |
|
actions_by_level = all_levels.merge(actions_by_level, on='Niveau', how='left').fillna(0) |
|
|
|
fig = px.bar( |
|
actions_by_level, |
|
x='Niveau', |
|
y='Nb_actions', |
|
color='Niveau', |
|
color_discrete_map={ |
|
0: '#ff6b6b', |
|
1: '#4ecdc4', |
|
2: '#45b7d1', |
|
3: '#96ceb4' |
|
} |
|
) |
|
|
|
|
|
fig.update_layout( |
|
showlegend=False, |
|
coloraxis_showscale=False, |
|
height=300 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True, key="niveau_actions_chart") |
|
|
|
|
|
|
|
with col_avg_per_match: |
|
match_note = player_scores_filtered[player_scores_filtered['Match'] == selected_match]['note_match_joueuse'].mean() |
|
if pd.notna(match_note): |
|
match_note = round(match_note, 1) |
|
else: |
|
match_note = 0.0 |
|
circular_kpi( |
|
value=match_note, |
|
label="Note du match", |
|
range=(0, 100) |
|
) |
|
|
|
|
|
|
|
|
|
actions_with_level = display_data[display_data['Niveau'].notna()] |
|
filtered_total_actions_by_action = actions_with_level.groupby('Action')['Nb_actions'].sum() |
|
|
|
|
|
total_match_score = 0 |
|
for action, value in filtered_total_actions_by_action.items(): |
|
action_data = display_data[display_data['Action'] == action] |
|
actions_by_level = action_data.groupby('Niveau')['Nb_actions'].sum().reset_index() |
|
|
|
|
|
all_levels = pd.DataFrame({'Niveau': [0, 1, 2, 3]}) |
|
actions_by_level = all_levels.merge(actions_by_level, on='Niveau', how='left').fillna(0) |
|
|
|
|
|
if actions_by_level['Nb_actions'].sum() > 0: |
|
score_action = (actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum()/actions_by_level['Nb_actions'].sum() |
|
total_match_score += score_action |
|
|
|
|
|
total_match_score = total_match_score |
|
|
|
|
|
level_descriptions = { |
|
'DUEL': { |
|
0: 'RECULE/PERTE', |
|
1: 'N\'AVANCE PAS', |
|
2: 'AVANCE/PASSE', |
|
3: 'PLAGE CASSÉ' |
|
}, |
|
'PASSE': { |
|
0: 'MANQUEE+PERTE', |
|
1: 'MANQUEE', |
|
2: 'PASSE COURSE/MAINS', |
|
3: 'PASSE COURSE+MAINS' |
|
}, |
|
'JAP': { |
|
0: 'CONTRE/GOBÉ', |
|
1: 'REBOND', |
|
2: 'GAIN TERRAIN', |
|
3: 'GAIN TERRAIN+POS' |
|
}, |
|
'PLAQUAGE': { |
|
0: 'MANQUÉ', |
|
1: 'SUBI', |
|
2: 'NEUTRE', |
|
3: 'DOMINANT' |
|
}, |
|
'RUCK': { |
|
0: 'INSPECTEUR', |
|
1: 'LENT 5+S', |
|
2: 'RAPIDE 3-4S', |
|
3: 'TRÈS RAPIDE 1-2S' |
|
}, |
|
'RECEPTION JAP': { |
|
0: 'REBOND/PERTE', |
|
1: 'REBOND', |
|
2: 'MAUVAIS GOBE', |
|
3: 'GOBE' |
|
} |
|
} |
|
|
|
for action, value in filtered_total_actions_by_action.items(): |
|
|
|
player_action_stats = player_data.groupby(['Match', 'Action'])['Nb_actions'].sum().reset_index() |
|
player_action_stats = player_action_stats[player_action_stats['Action'] == action] |
|
|
|
|
|
action_stats = { |
|
'min': float(player_action_stats['Nb_actions'].min()) if len(player_action_stats) > 0 else 0, |
|
'max': float(player_action_stats['Nb_actions'].max()) if len(player_action_stats) > 0 else 0, |
|
'mean': float(round(player_action_stats['Nb_actions'].mean(), 1)) if len(player_action_stats) > 0 else 0 |
|
} |
|
|
|
|
|
|
|
col_action1, col_action2, col_action3 = st.columns(3) |
|
|
|
with col_action1: |
|
circular_kpi( |
|
value=value, |
|
label=f"{action}", |
|
range=(0, action_stats['max']), |
|
min_value=action_stats['min'], |
|
max_value=action_stats['max'], |
|
mean_value=action_stats['mean'], |
|
color_scheme="blue_purple", |
|
background_color="transparent", |
|
key=f"actions_kpi_{action}" |
|
) |
|
|
|
with col_action2: |
|
|
|
action_data = display_data[display_data['Action'] == action] |
|
actions_by_level = action_data.groupby('Niveau')['Nb_actions'].sum().reset_index() |
|
|
|
|
|
all_levels = pd.DataFrame({'Niveau': [0, 1, 2, 3]}) |
|
actions_by_level = all_levels.merge(actions_by_level, on='Niveau', how='left').fillna(0) |
|
|
|
|
|
if action in level_descriptions: |
|
actions_by_level['Niveau_Desc'] = actions_by_level['Niveau'].map(level_descriptions[action]) |
|
else: |
|
actions_by_level['Niveau_Desc'] = actions_by_level['Niveau'].astype(str) |
|
|
|
fig = px.bar( |
|
actions_by_level, |
|
x='Niveau_Desc', |
|
y='Nb_actions', |
|
color='Niveau', |
|
color_discrete_map={ |
|
0: '#ff6b6b', |
|
1: '#4ecdc4', |
|
2: '#45b7d1', |
|
3: '#96ceb4' |
|
} |
|
) |
|
|
|
|
|
fig.update_layout( |
|
showlegend=False, |
|
coloraxis_showscale=False, |
|
height=300, |
|
xaxis_title="", |
|
yaxis_title="" |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True, key=f"bar_chart_{action}") |
|
|
|
with col_action3: |
|
|
|
|
|
|
|
score_action = (actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum()/actions_by_level['Nb_actions'].sum() |
|
score_action_percent = score_action/total_match_score*100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
circular_kpi( |
|
value=score_action_percent.round(1), |
|
label=f"de la note", |
|
range=(0, 100), |
|
unit="%", |
|
key=f"note_kpi_{action}" |
|
) |
|
|
|
|
|
actions_without_level = display_data[display_data['Niveau'].isna()] |
|
filtered_total_actions_by_action_null = actions_without_level.groupby('Action')['Nb_actions'].sum() |
|
|
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
if len(filtered_total_actions_by_action_null) > 0: |
|
|
|
|
|
num_null_actions = len(filtered_total_actions_by_action_null) |
|
if num_null_actions > 0: |
|
|
|
cols = st.columns(num_null_actions) |
|
|
|
for i, (action, value) in enumerate(filtered_total_actions_by_action_null.items()): |
|
|
|
player_action_stats = player_data.groupby(['Match', 'Action'])['Nb_actions'].sum().reset_index() |
|
player_action_stats = player_action_stats[player_action_stats['Action'] == action] |
|
|
|
|
|
action_stats = { |
|
'min': float(player_action_stats['Nb_actions'].min()) if len(player_action_stats) > 0 else 0, |
|
'max': float(player_action_stats['Nb_actions'].max()) if len(player_action_stats) > 0 else 0, |
|
'mean': float(player_action_stats['Nb_actions'].mean().round(1)) if len(player_action_stats) > 0 else 0 |
|
} |
|
|
|
|
|
with cols[i]: |
|
circular_kpi( |
|
value=value, |
|
label=f"{action}", |
|
range=(0, action_stats['max']), |
|
min_value=action_stats['min'], |
|
max_value=action_stats['max'], |
|
mean_value=action_stats['mean'], |
|
color_scheme="green", |
|
background_color="transparent", |
|
key=f"actions_kpi_null_{action}" |
|
) |
|
|