| import streamlit as st |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| import plotly.figure_factory as ff |
| from datetime import datetime |
| import re |
| import io |
|
|
| def extract_numeric_value(value_str): |
| """Extrait la valeur numérique en supprimant l'unité 'g' et les espaces.""" |
| if isinstance(value_str, str): |
| match = re.search(r'(\d+\.?\d*)', value_str.strip()) |
| return float(match.group(1)) if match else None |
| return value_str |
|
|
| def parse_lot_sections(content): |
| """Divise le contenu en sections de lots.""" |
| return [section.strip() for section in content.split('----------') if section.strip()] |
|
|
| def parse_lot_info(section): |
| """Extrait les informations générales du lot.""" |
| lines = section.split('\n') |
| lot_info = { |
| 'lot': None, |
| 'operateur': None, |
| 'article': None, |
| 'poids_cible': None, |
| 'date': None, |
| 'heure': None |
| } |
| |
| for line in lines: |
| if 'LOT\t' in line: |
| lot_info['lot'] = line.split('\t')[1].strip() |
| elif 'OPERATEUR\t' in line: |
| lot_info['operateur'] = line.split('\t')[1].strip() |
| elif 'ARTICLE\t' in line: |
| lot_info['article'] = line.split('\t')[1].strip() |
| elif 'POIDS CIBLE\t' in line: |
| lot_info['poids_cible'] = extract_numeric_value(line.split('\t')[1]) |
| elif 'DEBUT LOT' in line: |
| try: |
| date_line = next(l for l in lines[lines.index(line):] if re.match(r'\d{2}/\d{2}/\d{2}', l.strip())) |
| date_time = date_line.strip().split() |
| lot_info['date'] = datetime.strptime(date_time[0], '%d/%m/%y').strftime('%Y-%m-%d') |
| if len(date_time) > 1: |
| lot_info['heure'] = date_time[1] |
| except: |
| pass |
| |
| return lot_info |
|
|
| def parse_weights(section): |
| """Extrait les données de pesées individuelles.""" |
| weights = [] |
| lot_info = parse_lot_info(section) |
| |
| lines = section.split('\n') |
| for line in lines: |
| if re.match(r'\s*\d+\t', line): |
| parts = line.split('\t') |
| if len(parts) >= 3: |
| weight = extract_numeric_value(parts[1]) |
| target = lot_info['poids_cible'] |
| weight_data = { |
| 'identifiant_lot': f"{lot_info['lot']}_{lot_info['date']}", |
| 'ligne': int(parts[0].strip()), |
| 'poids': weight, |
| 'tare': extract_numeric_value(parts[-1].split()[-2]) if 'TARE' in line else None, |
| 'operateur': lot_info['operateur'], |
| 'article': lot_info['article'], |
| 'poids_cible': target, |
| 'surpoids': weight - target if weight and target else None, |
| 'surpoids_percentage': ((weight - target) / target * 100) if weight and target else None, |
| 'date': lot_info['date'], |
| 'heure': lot_info['heure'] |
| } |
| weights.append(weight_data) |
| |
| return weights |
|
|
| def create_weight_distribution_plot(weights_df): |
| """Crée un graphique de distribution des poids par rapport au poids cible.""" |
| fig = go.Figure() |
| |
| for article in weights_df['article'].unique(): |
| df_article = weights_df[weights_df['article'] == article] |
| |
| fig.add_trace(go.Histogram( |
| x=df_article['surpoids_percentage'], |
| name=article, |
| opacity=0.75, |
| nbinsx=30 |
| )) |
| |
| fig.update_layout( |
| title="Distribution des surpoids par article (%)", |
| xaxis_title="Surpoids (%)", |
| yaxis_title="Nombre de pesées", |
| barmode='overlay' |
| ) |
| |
| return fig |
|
|
| def create_boxplot_by_lot(weights_df): |
| """Crée un box plot des poids par lot.""" |
| fig = px.box(weights_df, |
| x='identifiant_lot', |
| y='surpoids', |
| color='article', |
| title="Distribution des surpoids par lot", |
| labels={'surpoids': 'Surpoids (g)', |
| 'identifiant_lot': 'Lot'}) |
| |
| fig.update_layout(xaxis_tickangle=-45) |
| return fig |
|
|
| def create_trend_plot(weights_df): |
| """Crée un graphique de tendance des surpoids dans le temps.""" |
| |
| df_sorted = weights_df.sort_values(['date', 'heure']) |
| df_sorted['moving_avg'] = df_sorted.groupby('article')['surpoids'].transform( |
| lambda x: x.rolling(window=10, min_periods=1).mean() |
| ) |
| |
| fig = px.line(df_sorted, |
| x=df_sorted.index, |
| y='moving_avg', |
| color='article', |
| title="Évolution des surpoids dans le temps (moyenne mobile)", |
| labels={'moving_avg': 'Surpoids moyen (g)', |
| 'index': 'Séquence de pesée'}) |
| |
| return fig |
|
|
| def create_heatmap(weights_df): |
| """Crée une heatmap des surpoids moyens par article et par lot.""" |
| pivot_table = pd.pivot_table(weights_df, |
| values='surpoids', |
| index='article', |
| columns='identifiant_lot', |
| aggfunc='mean') |
| |
| fig = px.imshow(pivot_table, |
| labels=dict(x="Lot", y="Article", color="Surpoids moyen (g)"), |
| aspect="auto", |
| title="Heatmap des surpoids moyens par lot et article") |
| |
| return fig |
|
|
| def create_control_chart(weights_df): |
| """Crée une carte de contrôle des poids.""" |
| df_sorted = weights_df.sort_values(['date', 'heure']) |
| |
| |
| mean = df_sorted['poids'].mean() |
| std = df_sorted['poids'].std() |
| ucl = mean + 3 * std |
| lcl = mean - 3 * std |
| |
| fig = go.Figure() |
| |
| |
| fig.add_trace(go.Scatter( |
| x=df_sorted.index, |
| y=df_sorted['poids'], |
| mode='markers+lines', |
| name='Poids' |
| )) |
| |
| |
| fig.add_hline(y=mean, line_dash="dash", line_color="green", annotation_text="Moyenne") |
| fig.add_hline(y=ucl, line_dash="dash", line_color="red", annotation_text="UCL") |
| fig.add_hline(y=lcl, line_dash="dash", line_color="red", annotation_text="LCL") |
| |
| fig.update_layout( |
| title="Carte de contrôle des poids", |
| xaxis_title="Séquence de pesée", |
| yaxis_title="Poids (g)" |
| ) |
| |
| return fig |
|
|
| def analyze_overweight_costs(weights_df): |
| """Analyse les coûts liés aux surpoids.""" |
| analysis = weights_df.groupby('article').agg({ |
| 'surpoids': ['mean', 'std', 'sum'], |
| 'poids_cible': 'first', |
| 'poids': 'count' |
| }).round(2) |
| |
| analysis.columns = ['Surpoids moyen (g)', 'Écart-type surpoids (g)', |
| 'Surpoids total (g)', 'Poids cible (g)', 'Nombre de pesées'] |
| |
| analysis['Surpoids moyen (%)'] = (analysis['Surpoids moyen (g)'] / |
| analysis['Poids cible (g)'] * 100).round(2) |
| |
| return analysis |
|
|
| def process_file(content): |
| """Traite le fichier complet et retourne les DataFrames.""" |
| sections = parse_lot_sections(content) |
| |
| |
| all_weights = [] |
| all_summaries = [] |
| |
| for section in sections: |
| if section and 'LOT\t' in section: |
| weights = parse_weights(section) |
| if weights: |
| all_weights.extend(weights) |
| |
| |
| weights_df = pd.DataFrame(all_weights) |
| |
| return weights_df |
|
|
| def main(): |
| st.set_page_config(page_title="Analyse des pesées", layout="wide") |
| st.title("Analyse des données de pesée") |
| |
| uploaded_file = st.file_uploader("Choisir un fichier", type=['txt']) |
| |
| if uploaded_file is not None: |
| content = uploaded_file.read().decode('utf-8') |
| |
| try: |
| weights_df = process_file(content) |
| |
| |
| st.header("Analyse des surpoids") |
| |
| |
| col1, col2, col3 = st.columns(3) |
| with col1: |
| st.metric("Surpoids moyen", f"{weights_df['surpoids'].mean():.2f}g") |
| with col2: |
| st.metric("Surpoids médian", f"{weights_df['surpoids'].median():.2f}g") |
| with col3: |
| st.metric("Écart-type surpoids", f"{weights_df['surpoids'].std():.2f}g") |
| |
| |
| st.subheader("Distribution des surpoids") |
| st.plotly_chart(create_weight_distribution_plot(weights_df), use_container_width=True) |
| |
| st.subheader("Analyse par lot") |
| st.plotly_chart(create_boxplot_by_lot(weights_df), use_container_width=True) |
| |
| st.subheader("Évolution temporelle") |
| st.plotly_chart(create_trend_plot(weights_df), use_container_width=True) |
| |
| st.subheader("Carte de contrôle") |
| st.plotly_chart(create_control_chart(weights_df), use_container_width=True) |
| |
| |
| st.header("Analyse détaillée par article") |
| analysis_df = analyze_overweight_costs(weights_df) |
| st.dataframe(analysis_df) |
| |
| |
| st.subheader("Heatmap des surpoids") |
| st.plotly_chart(create_heatmap(weights_df), use_container_width=True) |
| |
| |
| output = io.BytesIO() |
| with pd.ExcelWriter(output, engine='xlsxwriter') as writer: |
| weights_df.to_excel(writer, sheet_name='Données détaillées', index=False) |
| analysis_df.to_excel(writer, sheet_name='Analyse surpoids') |
| |
| output.seek(0) |
| |
| st.download_button( |
| label="Télécharger l'analyse Excel", |
| data=output, |
| file_name="analyse_surpoids.xlsx", |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
| ) |
| |
| except Exception as e: |
| st.error(f"Une erreur s'est produite lors du traitement du fichier : {str(e)}") |
|
|
| if __name__ == '__main__': |
| main() |