app
Browse files- app/app.py +46 -0
- app/components/__pycache__/layout.cpython-38.pyc +0 -0
- app/components/__pycache__/visualization.cpython-38.pyc +0 -0
- app/components/charts.py +75 -0
- app/components/layout.py +35 -0
- app/components/visualization.py +209 -0
- app/config/__init__.py +1 -0
- app/config/__pycache__/__init__.cpython-38.pyc +0 -0
- app/config/__pycache__/config.cpython-38.pyc +0 -0
- app/config/config.py +96 -0
- app/utils/__init__.py +0 -0
- app/utils/__pycache__/__init__.cpython-38.pyc +0 -0
- app/utils/__pycache__/data_processing.cpython-38.pyc +0 -0
- app/utils/data_processing.py +77 -0
- data/stats.xlsx +0 -0
app/app.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from utils.data_processing import load_data
|
| 3 |
+
from components.layout import create_sidebar, create_main_layout
|
| 4 |
+
from components.visualization import create_pizza_chart, display_metrics, get_max_values_for_ville
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
+
# Configuration de la page Streamlit
|
| 8 |
+
st.set_page_config(
|
| 9 |
+
page_title="Statistiques Rugby",
|
| 10 |
+
page_icon="🏉",
|
| 11 |
+
layout="wide"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
"""Fonction principale de l'application"""
|
| 16 |
+
st.title("🏉 Analyse des Statistiques Rugby")
|
| 17 |
+
|
| 18 |
+
# Chargement des données
|
| 19 |
+
data = load_data()
|
| 20 |
+
# Conversion des colonnes numériques
|
| 21 |
+
numeric_columns = data.select_dtypes(include=['object']).columns
|
| 22 |
+
for col in numeric_columns:
|
| 23 |
+
if col != 'name' and col != 'Ville':
|
| 24 |
+
data[col] = pd.to_numeric(data[col], errors='coerce')
|
| 25 |
+
|
| 26 |
+
# Création de l'interface
|
| 27 |
+
selected_ville, selected_player = create_sidebar(data)
|
| 28 |
+
col1, col2 = create_main_layout(data, selected_ville)
|
| 29 |
+
|
| 30 |
+
with col1:
|
| 31 |
+
st.subheader(f"Statistiques pour {selected_player} ({selected_ville})")
|
| 32 |
+
# Récupérer uniquement les valeurs maximales
|
| 33 |
+
max_values = get_max_values_for_ville(data, selected_ville)
|
| 34 |
+
fig = create_pizza_chart(data, selected_ville, selected_player, max_values)
|
| 35 |
+
if fig is not None:
|
| 36 |
+
st.pyplot(fig)
|
| 37 |
+
|
| 38 |
+
with col2:
|
| 39 |
+
st.subheader("Valeurs maximales pour ce match")
|
| 40 |
+
if max_values:
|
| 41 |
+
display_metrics(max_values)
|
| 42 |
+
else:
|
| 43 |
+
st.warning("Aucune donnée maximale disponible")
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
app/components/__pycache__/layout.cpython-38.pyc
ADDED
|
Binary file (1.02 kB). View file
|
|
|
app/components/__pycache__/visualization.cpython-38.pyc
ADDED
|
Binary file (5.32 kB). View file
|
|
|
app/components/charts.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
def create_pizza_chart(final_df, selected_ville):
|
| 4 |
+
"""Crée le graphique pizza pour une ville donnée"""
|
| 5 |
+
ville_data = final_df[final_df['Ville'] == selected_ville].iloc[0]
|
| 6 |
+
|
| 7 |
+
params, values, slice_colors = [], [], []
|
| 8 |
+
max_values = {}
|
| 9 |
+
|
| 10 |
+
# Calcul des valeurs maximales
|
| 11 |
+
for cat_name, cat_info in CATEGORIES.items():
|
| 12 |
+
cat_values = []
|
| 13 |
+
for subcat in cat_info['subcats'].keys():
|
| 14 |
+
if subcat in final_df.columns and pd.notna(ville_data[subcat]):
|
| 15 |
+
cat_values.append(float(ville_data[subcat]))
|
| 16 |
+
if cat_values:
|
| 17 |
+
max_values[cat_name] = max(cat_values)
|
| 18 |
+
|
| 19 |
+
# Préparation des données pour le graphique
|
| 20 |
+
for cat_name, cat_info in CATEGORIES.items():
|
| 21 |
+
for subcat, display_text in cat_info['subcats'].items():
|
| 22 |
+
params.append(display_text)
|
| 23 |
+
if subcat in final_df.columns and pd.notna(ville_data[subcat]):
|
| 24 |
+
value = float(ville_data[subcat])
|
| 25 |
+
max_val = max_values[cat_name]
|
| 26 |
+
normalized_value = (value / max_val * 100) if max_val > 0 else 0
|
| 27 |
+
values.append(round(normalized_value, 2))
|
| 28 |
+
else:
|
| 29 |
+
values.append(0)
|
| 30 |
+
slice_colors.append(cat_info['color'])
|
| 31 |
+
|
| 32 |
+
# Création du graphique
|
| 33 |
+
text_colors = ["#000000"] * len(params)
|
| 34 |
+
fig, ax = plt.subplots(figsize=CHART_CONFIG["figsize"])
|
| 35 |
+
|
| 36 |
+
# Créer un dictionnaire de configuration sans figsize pour PyPizza
|
| 37 |
+
pizza_config = {k: v for k, v in CHART_CONFIG.items() if k != "figsize"}
|
| 38 |
+
|
| 39 |
+
baker = PyPizza(
|
| 40 |
+
params=params,
|
| 41 |
+
**pizza_config
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
fig, ax = baker.make_pizza(
|
| 45 |
+
values,
|
| 46 |
+
figsize=(10, 10),
|
| 47 |
+
color_blank_space="same",
|
| 48 |
+
slice_colors=slice_colors,
|
| 49 |
+
value_colors=text_colors,
|
| 50 |
+
value_bck_colors=slice_colors,
|
| 51 |
+
blank_alpha=0.4,
|
| 52 |
+
kwargs_slices=dict(
|
| 53 |
+
edgecolor="#F2F2F2",
|
| 54 |
+
zorder=2,
|
| 55 |
+
linewidth=1
|
| 56 |
+
),
|
| 57 |
+
kwargs_params=dict(
|
| 58 |
+
color="#000000",
|
| 59 |
+
fontsize=8,
|
| 60 |
+
va="center"
|
| 61 |
+
),
|
| 62 |
+
kwargs_values=dict(
|
| 63 |
+
color="#000000",
|
| 64 |
+
fontsize=8,
|
| 65 |
+
zorder=3,
|
| 66 |
+
bbox=dict(
|
| 67 |
+
edgecolor="#000000",
|
| 68 |
+
facecolor="white",
|
| 69 |
+
boxstyle="round,pad=0.2",
|
| 70 |
+
lw=1
|
| 71 |
+
)
|
| 72 |
+
)
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return fig, max_values
|
app/components/layout.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from difflib import get_close_matches
|
| 3 |
+
|
| 4 |
+
def create_sidebar(data):
|
| 5 |
+
"""Crée la barre latérale avec les contrôles"""
|
| 6 |
+
with st.sidebar:
|
| 7 |
+
st.header("Configuration")
|
| 8 |
+
|
| 9 |
+
# Sélection de la ville
|
| 10 |
+
selected_ville = st.selectbox(
|
| 11 |
+
"Sélectionner une ville",
|
| 12 |
+
options=data['Ville'].unique()
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Filtrer les joueurs pour la ville sélectionnée
|
| 16 |
+
ville_players = data[data['Ville'] == selected_ville]['name'].unique()
|
| 17 |
+
|
| 18 |
+
# Sélection du joueur
|
| 19 |
+
selected_player = st.selectbox(
|
| 20 |
+
"Sélectionner un joueur",
|
| 21 |
+
options=ville_players
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
return selected_ville, selected_player
|
| 25 |
+
|
| 26 |
+
def create_main_layout(data, selected_ville):
|
| 27 |
+
"""Crée la disposition principale de l'application"""
|
| 28 |
+
# Affichage des données brutes dans un expander
|
| 29 |
+
with st.expander("Voir les données brutes"):
|
| 30 |
+
st.dataframe(data[data['Ville'] == selected_ville])
|
| 31 |
+
|
| 32 |
+
# Création des colonnes pour l'affichage
|
| 33 |
+
col1, col2 = st.columns([3, 4])
|
| 34 |
+
|
| 35 |
+
return col1, col2
|
app/components/visualization.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
from mplsoccer import PyPizza
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from config.config import CATEGORIES, CHART_CONFIG
|
| 5 |
+
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
from mplsoccer import PyPizza
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import streamlit as st
|
| 10 |
+
from config.config import CATEGORIES, CHART_CONFIG
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def create_pizza_chart(final_df, selected_ville, selected_player, max_values):
|
| 14 |
+
try:
|
| 15 |
+
from urllib.request import urlopen
|
| 16 |
+
from PIL import Image
|
| 17 |
+
import matplotlib.image as image
|
| 18 |
+
|
| 19 |
+
player_data = final_df[(final_df['Ville'] == selected_ville) &
|
| 20 |
+
(final_df['name'] == selected_player)]
|
| 21 |
+
|
| 22 |
+
if player_data.empty:
|
| 23 |
+
st.warning(f"Aucune donnée trouvée pour {selected_player} dans {selected_ville}")
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
player_data = player_data.iloc[0]
|
| 27 |
+
|
| 28 |
+
params, values, slice_colors = [], [], []
|
| 29 |
+
min_ranges, max_ranges = [], []
|
| 30 |
+
|
| 31 |
+
# Préparation des données pour le graphique
|
| 32 |
+
for cat_name, cat_info in CATEGORIES.items():
|
| 33 |
+
for subcat, display_text in cat_info['subcats'].items():
|
| 34 |
+
params.append(display_text)
|
| 35 |
+
if subcat in final_df.columns and pd.notna(player_data[subcat]):
|
| 36 |
+
value = float(player_data[subcat])
|
| 37 |
+
values.append(value)
|
| 38 |
+
min_ranges.append(0)
|
| 39 |
+
max_ranges.append(max_values[cat_name][subcat])
|
| 40 |
+
else:
|
| 41 |
+
values.append(0)
|
| 42 |
+
min_ranges.append(0)
|
| 43 |
+
max_ranges.append(100)
|
| 44 |
+
slice_colors.append(cat_info['color'])
|
| 45 |
+
|
| 46 |
+
# Création du graphique
|
| 47 |
+
text_colors = ["#000000"] * len(params)
|
| 48 |
+
fig, ax = plt.subplots(figsize=CHART_CONFIG["figsize"])
|
| 49 |
+
|
| 50 |
+
pizza_config = {k: v for k, v in CHART_CONFIG.items() if k != "figsize"}
|
| 51 |
+
pizza_config['inner_circle_size'] = 30 # Augmenter la taille du cercle intérieur
|
| 52 |
+
|
| 53 |
+
baker = PyPizza(
|
| 54 |
+
params=params,
|
| 55 |
+
min_range=min_ranges,
|
| 56 |
+
max_range=max_ranges,
|
| 57 |
+
**pizza_config
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
fig, ax = baker.make_pizza(
|
| 61 |
+
values,
|
| 62 |
+
figsize=(10, 10),
|
| 63 |
+
color_blank_space="same",
|
| 64 |
+
slice_colors=slice_colors,
|
| 65 |
+
value_colors=text_colors,
|
| 66 |
+
value_bck_colors=slice_colors,
|
| 67 |
+
blank_alpha=0.4,
|
| 68 |
+
kwargs_slices=dict(
|
| 69 |
+
edgecolor="#F2F2F2",
|
| 70 |
+
zorder=2,
|
| 71 |
+
linewidth=1
|
| 72 |
+
),
|
| 73 |
+
kwargs_params=dict(
|
| 74 |
+
color="#000000",
|
| 75 |
+
fontsize=8,
|
| 76 |
+
va="center"
|
| 77 |
+
),
|
| 78 |
+
kwargs_values=dict(
|
| 79 |
+
color="#000000",
|
| 80 |
+
fontsize=8,
|
| 81 |
+
zorder=3,
|
| 82 |
+
bbox=dict(
|
| 83 |
+
edgecolor="#000000",
|
| 84 |
+
facecolor="white",
|
| 85 |
+
boxstyle="round,pad=0.2",
|
| 86 |
+
lw=1
|
| 87 |
+
)
|
| 88 |
+
)
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
# Ajouter l'image au centre
|
| 92 |
+
try:
|
| 93 |
+
# Charger et ajouter le logo au centre
|
| 94 |
+
logo_url = "https://upload.wikimedia.org/wikipedia/fr/thumb/0/01/Logo_Stade_Toulousain_Rugby.svg/775px-Logo_Stade_Toulousain_Rugby.svg.png?20180529221555"
|
| 95 |
+
logo = Image.open(urlopen(logo_url))
|
| 96 |
+
|
| 97 |
+
# Créer un nouvel axe pour l'image
|
| 98 |
+
ax_image = fig.add_axes([0.4478, 0.4315, 0.13, 0.127], zorder=2)
|
| 99 |
+
ax_image.imshow(logo)
|
| 100 |
+
ax_image.axis('off')
|
| 101 |
+
except Exception as e:
|
| 102 |
+
st.warning(f"Impossible de charger l'image: {str(e)}")
|
| 103 |
+
|
| 104 |
+
return fig
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
st.error(f"Erreur lors de la création du graphique: {str(e)}")
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
def get_max_values_for_ville(final_df, selected_ville):
|
| 111 |
+
"""Calcule les valeurs maximales pour une ville donnée"""
|
| 112 |
+
# Filtrer les données pour la ville sélectionnée et exclure "GENERAL"
|
| 113 |
+
ville_data = final_df[(final_df['Ville'] == selected_ville) &
|
| 114 |
+
(final_df['name'] != "GENERAL")]
|
| 115 |
+
max_values = {}
|
| 116 |
+
|
| 117 |
+
for cat_name, cat_info in CATEGORIES.items():
|
| 118 |
+
cat_values = {} # Utiliser un dictionnaire pour stocker les max par sous-catégorie
|
| 119 |
+
for subcat in cat_info['subcats'].keys():
|
| 120 |
+
if subcat in final_df.columns:
|
| 121 |
+
values = ville_data[subcat].dropna()
|
| 122 |
+
if not values.empty:
|
| 123 |
+
cat_values[subcat] = max(values.astype(float))
|
| 124 |
+
if cat_values:
|
| 125 |
+
max_values[cat_name] = cat_values
|
| 126 |
+
|
| 127 |
+
return max_values
|
| 128 |
+
|
| 129 |
+
def display_metrics(max_values):
|
| 130 |
+
"""Affiche les valeurs maximales par catégorie de manière esthétique en deux colonnes"""
|
| 131 |
+
|
| 132 |
+
st.markdown("""
|
| 133 |
+
<style>
|
| 134 |
+
div[data-testid="stVerticalBlock"] div[style*="flex-direction: column"] div[data-testid="stVerticalBlock"] {
|
| 135 |
+
background-color: white;
|
| 136 |
+
padding: 20px;
|
| 137 |
+
border-radius: 10px;
|
| 138 |
+
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
|
| 139 |
+
margin-bottom: 20px;
|
| 140 |
+
}
|
| 141 |
+
</style>
|
| 142 |
+
""", unsafe_allow_html=True)
|
| 143 |
+
|
| 144 |
+
# Diviser les catégories en deux groupes
|
| 145 |
+
categories = list(max_values.items())
|
| 146 |
+
mid = (len(categories) + 1) // 2
|
| 147 |
+
|
| 148 |
+
# Créer deux colonnes principales
|
| 149 |
+
col_left, col_right = st.columns(2)
|
| 150 |
+
|
| 151 |
+
# Remplir la colonne gauche
|
| 152 |
+
with col_left:
|
| 153 |
+
for cat_name, subcats in categories[:mid]:
|
| 154 |
+
cat_color = CATEGORIES[cat_name]['color']
|
| 155 |
+
with st.container():
|
| 156 |
+
st.markdown(f"""
|
| 157 |
+
<h3 style='
|
| 158 |
+
color: {cat_color};
|
| 159 |
+
font-size: 20px;
|
| 160 |
+
padding-bottom: 10px;
|
| 161 |
+
border-bottom: 2px solid {cat_color};
|
| 162 |
+
margin-bottom: 15px;
|
| 163 |
+
'>
|
| 164 |
+
{cat_name}
|
| 165 |
+
</h3>
|
| 166 |
+
""", unsafe_allow_html=True)
|
| 167 |
+
|
| 168 |
+
for subcat, value in subcats.items():
|
| 169 |
+
st.markdown(f"""
|
| 170 |
+
<div style='display: flex; justify-content: space-between; padding: 5px 0;'>
|
| 171 |
+
<span style='color: #666; font-size: 16px;'>
|
| 172 |
+
{CATEGORIES[cat_name]['subcats'][subcat].replace(chr(10), ' ')}
|
| 173 |
+
</span>
|
| 174 |
+
<span style='color: #333; font-weight: bold; font-size: 16px;'>
|
| 175 |
+
{value}
|
| 176 |
+
</span>
|
| 177 |
+
</div>
|
| 178 |
+
""", unsafe_allow_html=True)
|
| 179 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 180 |
+
|
| 181 |
+
# Remplir la colonne droite
|
| 182 |
+
with col_right:
|
| 183 |
+
for cat_name, subcats in categories[mid:]:
|
| 184 |
+
cat_color = CATEGORIES[cat_name]['color']
|
| 185 |
+
with st.container():
|
| 186 |
+
st.markdown(f"""
|
| 187 |
+
<h3 style='
|
| 188 |
+
color: {cat_color};
|
| 189 |
+
font-size: 20px;
|
| 190 |
+
padding-bottom: 10px;
|
| 191 |
+
border-bottom: 2px solid {cat_color};
|
| 192 |
+
margin-bottom: 15px;
|
| 193 |
+
'>
|
| 194 |
+
{cat_name}
|
| 195 |
+
</h3>
|
| 196 |
+
""", unsafe_allow_html=True)
|
| 197 |
+
|
| 198 |
+
for subcat, value in subcats.items():
|
| 199 |
+
st.markdown(f"""
|
| 200 |
+
<div style='display: flex; justify-content: space-between; padding: 5px 0;'>
|
| 201 |
+
<span style='color: #666; font-size: 16px;'>
|
| 202 |
+
{CATEGORIES[cat_name]['subcats'][subcat].replace(chr(10), ' ')}
|
| 203 |
+
</span>
|
| 204 |
+
<span style='color: #333; font-weight: bold; font-size: 16px;'>
|
| 205 |
+
{value}
|
| 206 |
+
</span>
|
| 207 |
+
</div>
|
| 208 |
+
""", unsafe_allow_html=True)
|
| 209 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
app/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .config import CATEGORIES, CHART_CONFIG, ERROR_MESSAGES, DATA_DIR, EXCEL_PATH
|
app/config/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (284 Bytes). View file
|
|
|
app/config/__pycache__/config.cpython-38.pyc
ADDED
|
Binary file (1.86 kB). View file
|
|
|
app/config/config.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# Chemins des fichiers
|
| 5 |
+
DATA_DIR = Path("./data") # Conversion en objet Path
|
| 6 |
+
EXCEL_PATH = DATA_DIR / "stats.xlsx"
|
| 7 |
+
|
| 8 |
+
# Ajout d'un print pour debug
|
| 9 |
+
print(f"Chemin du fichier Excel: {EXCEL_PATH}")
|
| 10 |
+
|
| 11 |
+
# Vérification et création du dossier data s'il n'existe pas
|
| 12 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Ajout d'un print pour debug
|
| 16 |
+
print(f"Chemin du fichier Excel: {EXCEL_PATH}")
|
| 17 |
+
|
| 18 |
+
# Vérification et création du dossier data s'il n'existe pas
|
| 19 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 20 |
+
|
| 21 |
+
# Configuration des catégories et leurs couleurs
|
| 22 |
+
CATEGORIES = {
|
| 23 |
+
'Duel': {
|
| 24 |
+
'color': "#1A78CF",
|
| 25 |
+
'subcats': {
|
| 26 |
+
'Duel_0': 'Duel\nPerdu',
|
| 27 |
+
'Duel_1.0': 'Duel\nNeutre',
|
| 28 |
+
'Duel_2.0': 'Duel\nGagné',
|
| 29 |
+
'Duel_3.0': 'Duel\nDécisif',
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
'Passe': {
|
| 33 |
+
'color': "#FF9300",
|
| 34 |
+
'subcats': {
|
| 35 |
+
'Passe_0': 'Passe\nPerdue',
|
| 36 |
+
'Passe_1.0': 'Passe\nNeutre',
|
| 37 |
+
'Passe_2.0': 'Passe\nGagnée',
|
| 38 |
+
'Passe_3.0': 'Passe\nDécisive',
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
'Plaquage': {
|
| 42 |
+
'color': "#D70232",
|
| 43 |
+
'subcats': {
|
| 44 |
+
'Plaquage_0': 'Plaquage\nPerdu',
|
| 45 |
+
'Plaquage_1.0': 'Plaquage\nNeutre',
|
| 46 |
+
'Plaquage_2.0': 'Plaquage\nGagné',
|
| 47 |
+
'Plaquage_3.0': 'Plaquage\nDécisif',
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
'Ruck': {
|
| 51 |
+
'color': "#2ECC71",
|
| 52 |
+
'subcats': {
|
| 53 |
+
'Ruck_0': 'Ruck\nPerdu',
|
| 54 |
+
'Ruck_1.0': 'Ruck\nNeutre',
|
| 55 |
+
'Ruck_2.0': 'Ruck\nGagné',
|
| 56 |
+
'Ruck_3.0': 'Ruck\nDécisif',
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
'JAP': {
|
| 60 |
+
'color': "#9B59B6",
|
| 61 |
+
'subcats': {
|
| 62 |
+
'JAP_0': 'JAP\nPerdu',
|
| 63 |
+
'JAP_1.0': 'JAP\nNeutre',
|
| 64 |
+
'JAP_2.0': 'JAP\nGagné',
|
| 65 |
+
'JAP_3.0': 'JAP\nDécisif',
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
'Reception JAP': {
|
| 69 |
+
'color': "#F1C40F",
|
| 70 |
+
'subcats': {
|
| 71 |
+
'Réception JAP_0': 'Récep\nPerdue',
|
| 72 |
+
'Réception JAP_1.0': 'Récep\nNeutre',
|
| 73 |
+
'Réception JAP_2.0': 'Récep\nGagnée',
|
| 74 |
+
'Réception JAP_3.0': 'Récep\nDécisive',
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# Configuration du graphique
|
| 80 |
+
CHART_CONFIG = {
|
| 81 |
+
"background_color": "#EBEBE9",
|
| 82 |
+
"straight_line_color": "#EBEBE9",
|
| 83 |
+
"straight_line_lw": 1,
|
| 84 |
+
"last_circle_lw": 0,
|
| 85 |
+
"other_circle_lw": 0,
|
| 86 |
+
"inner_circle_size": 20,
|
| 87 |
+
"straight_line_limit": 100,
|
| 88 |
+
"figsize": (10, 10)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
# Messages d'erreur
|
| 92 |
+
ERROR_MESSAGES = {
|
| 93 |
+
"data_load": "Impossible de charger les données",
|
| 94 |
+
"file_not_found": "Le fichier de données n'a pas été trouvé",
|
| 95 |
+
"data_processing": "Erreur lors du traitement des données"
|
| 96 |
+
}
|
app/utils/__init__.py
ADDED
|
File without changes
|
app/utils/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
app/utils/__pycache__/data_processing.cpython-38.pyc
ADDED
|
Binary file (1.99 kB). View file
|
|
|
app/utils/data_processing.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from config.config import EXCEL_PATH
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def clean_dataframe(df):
|
| 7 |
+
# Récupérer les catégories principales (ligne 0)
|
| 8 |
+
categories = df.iloc[0]
|
| 9 |
+
# Récupérer les sous-catégories (ligne 2)
|
| 10 |
+
sub_categories = df.iloc[2]
|
| 11 |
+
|
| 12 |
+
# Créer un dictionnaire pour stocker les nouvelles colonnes
|
| 13 |
+
new_columns = {}
|
| 14 |
+
current_category = None
|
| 15 |
+
|
| 16 |
+
# Parcourir toutes les colonnes pour créer les nouveaux noms
|
| 17 |
+
for i, (cat, subcat) in enumerate(zip(categories, sub_categories)):
|
| 18 |
+
if pd.notna(cat):
|
| 19 |
+
current_category = cat
|
| 20 |
+
if pd.notna(subcat) and current_category is not None:
|
| 21 |
+
new_name = f"{current_category}_{subcat}"
|
| 22 |
+
new_columns[df.columns[i]] = new_name
|
| 23 |
+
|
| 24 |
+
# Garder la colonne 'name' telle quelle
|
| 25 |
+
new_columns[df.columns[0]] = 'name'
|
| 26 |
+
|
| 27 |
+
# Renommer les colonnes
|
| 28 |
+
df = df.rename(columns=new_columns)
|
| 29 |
+
|
| 30 |
+
# Supprimer les trois premières lignes et réinitialiser l'index
|
| 31 |
+
df = df.iloc[3:].reset_index(drop=True)
|
| 32 |
+
|
| 33 |
+
# Supprimer les lignes où le nom est vide ou NaN
|
| 34 |
+
df = df.dropna(subset=['name']).reset_index(drop=True)
|
| 35 |
+
|
| 36 |
+
# Extraire le numéro et nettoyer le nom AVANT d'ajouter la colonne Ville
|
| 37 |
+
df['numero'] = df['name'].str.extract(r'^(\d+)').fillna('')
|
| 38 |
+
df['name'] = df['name'].str.replace(r'^\d+\s*-\s*', '', regex=True).str.strip()
|
| 39 |
+
|
| 40 |
+
# Ne garder que les colonnes nécessaires
|
| 41 |
+
columns_to_keep = ['numero', 'name'] + [col for col in df.columns if '_' in str(col)]
|
| 42 |
+
df = df[columns_to_keep]
|
| 43 |
+
|
| 44 |
+
return df
|
| 45 |
+
|
| 46 |
+
@st.cache_data
|
| 47 |
+
def load_data():
|
| 48 |
+
"""Charge et prépare les données depuis le fichier Excel"""
|
| 49 |
+
try:
|
| 50 |
+
if not EXCEL_PATH.exists():
|
| 51 |
+
st.error(f"Le fichier Excel n'a pas été trouvé à l'emplacement : {EXCEL_PATH}")
|
| 52 |
+
st.info("Veuillez placer votre fichier stats.xlsx dans le dossier 'data'")
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
all_sheets = pd.read_excel(EXCEL_PATH, sheet_name=None)
|
| 56 |
+
clean_data = []
|
| 57 |
+
|
| 58 |
+
for ville, df in all_sheets.items():
|
| 59 |
+
if ville != "Promedio partidos":
|
| 60 |
+
clean_df = clean_dataframe(df)
|
| 61 |
+
if not clean_df.empty:
|
| 62 |
+
clean_df['Ville'] = ville
|
| 63 |
+
clean_data.append(clean_df)
|
| 64 |
+
|
| 65 |
+
# st.write(clean_data)
|
| 66 |
+
|
| 67 |
+
if not clean_data:
|
| 68 |
+
st.warning("Aucune donnée trouvée")
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
return pd.concat(clean_data, ignore_index=True)
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
st.error(f"Erreur lors du chargement des données: {str(e)}")
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
|
data/stats.xlsx
ADDED
|
Binary file (215 kB). View file
|
|
|