import streamlit as st import pandas as pd import numpy as np from scipy import stats from scipy.interpolate import interp1d from typing import Tuple, Optional, Dict, List, Any, Union import plotly.graph_objects as go import plotly.express as px import io import xlsxwriter from PIL import Image import statsmodels.api as sm from statsmodels.formula.api import ols import math # Classe personnalisée pour les erreurs de validation class ValidationError(Exception): """Exception levée lorsque les données ne respectent pas les critères de validation.""" pass # Fonction pour créer un template Excel pour l'import des données def create_excel_template() -> bytes: """ Crée un modèle Excel pour la saisie des données de pesées. Returns: bytes: Contenu binaire du fichier Excel généré """ output = io.BytesIO() workbook = xlsxwriter.Workbook(output) # Formats Excel header_format = workbook.add_format({ 'bold': True, 'bg_color': '#4B8BBE', 'font_color': 'white', 'border': 1, 'align': 'center' }) data_format = workbook.add_format({ 'border': 1, 'align': 'center' }) # Feuille Pesées ws_pesees = workbook.add_worksheet("Pesées") # En-têtes headers = ["Date", "Heure", "Lot", "Tare (g)", "Poids Brut (g)", "Remarques"] for col, header in enumerate(headers): ws_pesees.write(0, col, header, header_format) ws_pesees.set_column(col, col, 15) # Largeur de colonne # Données exemple example_data = [ ["2024-01-01", "08:00", "LOT001", 10.5, 510.5, ""], ["2024-01-01", "09:00", "LOT001", 10.4, 509.8, ""], ["2024-01-01", "10:00", "LOT001", 10.6, 511.2, ""], ] for row, data in enumerate(example_data, start=1): for col, value in enumerate(data): ws_pesees.write(row, col, value, data_format) # Feuille Instructions ws_instructions = workbook.add_worksheet("Instructions") instructions = [ ["Guide d'utilisation du fichier de pesées"], [""], ["1. Format des colonnes:"], [" - Date: AAAA-MM-JJ"], [" - Heure: HH:MM"], [" - Lot: Texte libre"], [" - Tare (g): Nombre décimal"], [" - Poids Brut (g): Nombre décimal"], [" - Remarques: Texte libre optionnel"], [""], ["2. Consignes importantes:"], [" - Ne pas modifier les en-têtes"], [" - Remplir toutes les colonnes obligatoires"], [" - Un minimum de 5 mesures est recommandé pour un échantillon représentatif"], [" - Vérifier la cohérence des unités (grammes)"], [" - Pour une pesée en volume, préciser la masse volumique du produit dans les remarques"], ] for row, instruction in enumerate(instructions): ws_instructions.write(row, 0, instruction[0]) workbook.close() output.seek(0) return output.getvalue() # Classe pour effectuer les calculs métrologiques class MetrologicalCalculations: """ Classe gérant les calculs métrologiques conformément aux directives DGCCRF. Cette classe implémente les calculs nécessaires pour vérifier la conformité des lots selon les trois critères fondamentaux: 1. Critère de la moyenne: le contenu effectif des préemballages ne doit pas être inférieur, en moyenne, à la quantité nominale 2. Critère des défectueux: proportion de préemballages défectueux ≤ 2% 3. Critère des super-défectueux (pour les produits avec signe "e"): aucun produit ne doit avoir un manquant supérieur à 2x l'EMT """ def __init__(self, qn: float, emt: float, surpoids_max: float, n: int, frequence: float): """ Initialise les calculs métrologiques avec les paramètres de contrôle. Args: qn: Quantité nominale en grammes emt: Erreur maximale tolérée en grammes surpoids_max: Surpoids maximum toléré en grammes n: Taille d'échantillon frequence: Fréquence d'échantillonnage par heure """ self.qn = float(qn) self.emt = float(emt) self.surpoids_max = float(surpoids_max) self.n = int(n) self.frequence = float(frequence) def calculate_pol(self) -> float: """ Calcule la Période Opérationnelle Limite (POL) conformément au guide DGCCRF. La POL représente le nombre d'échantillonnages par heure de fabrication. Returns: float: Valeur de la POL """ # Valeur de base pour la POL (4 heures de production standard) base_pol = 4.0 # Facteurs d'ajustement qn_factor = min(2.0, max(0.5, self.qn / 1000)) # Facteur lié à la quantité nominale emt_factor = min(1.5, max(0.5, self.emt / (self.qn * 0.01))) # Facteur lié à l'EMT # Facteur n ajusté pour éviter la "cible mouvante" n_factor = min(1.2, max(0.8, 1 + 0.05 * math.log(self.n / 5))) # Calcul de la POL ajustée (nombre d'échantillonnages par heure) return base_pol * qn_factor * emt_factor * n_factor def calculate_pom(self, sigma_0: float) -> float: """ Calcule la Période Opérationnelle Moyenne (POM) conformément au guide DGCCRF. Args: sigma_0: Écart-type du processus Returns: float: Valeur du POM """ # Calcul du seuil de centrage (ms) ms = self.qn if sigma_0 <= self.emt / 2.05 else self.qn - self.emt + 2.05 * sigma_0 # Calcul de la cible de production avec le surpoids qc = ms + self.surpoids_max # Calcul du delta selon la formule du guide DGCCRF delta = (qc - (self.qn - self.emt + 2.05 * sigma_0)) / sigma_0 # Calcul du delta multiplié par la racine de n delta_sqrt_n = delta * math.sqrt(self.n) # Détermination du POM en fonction de la valeur de delta_sqrt_n # (Tableau issu du guide DGCCRF, Annexe 4) if delta_sqrt_n <= 0.17: return 1000 # Valeur très élevée pour indiquer un plan clairement insuffisant elif delta_sqrt_n <= 0.5: return 200 / (delta_sqrt_n**2) # Approximation pour les faibles delta_sqrt_n elif delta_sqrt_n <= 3.38: # Interpolation des valeurs du tableau du guide if delta_sqrt_n <= 1.0: return 107.8 - 90 * (delta_sqrt_n - 0.63) elif delta_sqrt_n <= 2.0: return 17.8 - 11 * (delta_sqrt_n - 1.0) else: return 6.8 - 3.8 * (delta_sqrt_n - 2.0) / 1.38 else: return 1 # Valeur minimale pour les grands delta_sqrt_n def explain_sampling_improvement(self, pom: float, pol: float, current_n: int, current_freq: float) -> str: """ Génère une explication claire de l'amélioration nécessaire du plan d'échantillonnage. Args: pom: Période Opérationnelle Moyenne calculée pol: Période Opérationnelle Limite calculée current_n: Taille d'échantillon actuelle current_freq: Fréquence d'échantillonnage actuelle Returns: str: Explication textuelle formatée en markdown """ ratio = pom / pol if ratio <= 1: return """ **Plan d'échantillonnage valide** Votre plan d'échantillonnage actuel est suffisant pour détecter un déréglage en temps utile (POM = {:.2f} ≤ POL = {:.2f}). """.format(pom, pol) # Calcul des recommandations recommended_n = int(max(10, current_n * math.sqrt(ratio))) recommended_freq = min(4, max(1, current_freq * math.sqrt(ratio))) # Arrondir à des valeurs pratiques recommended_n = ((recommended_n + 4) // 5) * 5 # Multiple de 5 recommended_freq = math.ceil(recommended_freq * 2) / 2 # Multiple de 0.5 return """ **Plan d'échantillonnage insuffisant** (POM = {:.2f} > POL = {:.2f}) D'après le guide DGCCRF (Annexe 4), votre plan actuel serait insuffisant pour détecter en temps utile un déréglage pouvant conduire à la production d'un lot non conforme. Pour améliorer votre plan d'échantillonnage: 1. **Optimisez d'abord la variabilité du processus** - La réduction de l'écart-type est la première action à entreprendre - Vérifiez le centrage des becs de remplissage (voir l'exemple p.26 du guide) 2. **Augmentez la taille d'échantillon** à environ {:.0f} unités - L'exemple du guide recommande un minimum de 10 unités 3. **Ajustez la fréquence d'échantillonnage** à {:.1f} contrôles/heure - Le guide recommande un échantillonnage toutes les 15 minutes 4. **Ajustez la quantité cible** avec un surdosage approprié - Le guide recommande un surdosage d'environ 0,17% pour compenser la variabilité """.format(pom, pol, recommended_n, recommended_freq) def simulate_improved_plan(self, sigma_0: float, n: int, freq: float, surpoids: float) -> Dict[str, Any]: """ Simule un plan d'échantillonnage amélioré selon les recommandations du guide DGCCRF. Args: sigma_0: Écart-type actuel du processus n: Nouvelle taille d'échantillon freq: Nouvelle fréquence d'échantillonnage surpoids: Surpoids proposé Returns: Dict: Résultats de la simulation """ # Simuler un écart-type optimisé (comme suggéré dans l'exemple p.25 du guide) improved_sigma = max(sigma_0 * 0.7, self.emt / 3.5) # Réduction typique ou valeur cible # Créer une instance temporaire avec les nouveaux paramètres sim = MetrologicalCalculations(self.qn, self.emt, surpoids, n, freq) # Calculer POM et POL sim_pol = sim.calculate_pol() sim_pom = sim.calculate_pom(improved_sigma) return { 'optimized_sigma': improved_sigma, 'pom': sim_pom, 'pol': sim_pol, 'is_valid': sim_pom <= sim_pol, 'ratio': sim_pom / sim_pol, 'target_weight': self.qn + surpoids } def evaluate_sampling_plan(self, sigma_0: float) -> Dict[str, Any]: """ Évalue le plan d'échantillonnage en comparant POM et POL. Args: sigma_0: Écart-type du processus Returns: Dict: Résultats de l'évaluation et recommandations """ pol = self.calculate_pol() pom = self.calculate_pom(sigma_0) is_valid = pom <= pol recommendations = [] explanation = "" severity = "valide" recommended_n = self.n # Valeur par défaut recommended_freq = self.frequence # Valeur par défaut if not is_valid: pom_pol_ratio = pom / pol # Détermination de la gravité de l'insuffisance if pom_pol_ratio > 4: severity = "critique" recommendations.append("🚨 Plan d'échantillonnage critiquement insuffisant") elif pom_pol_ratio > 2: severity = "très insuffisant" recommendations.append("🚨 Plan d'échantillonnage très insuffisant") else: severity = "légèrement insuffisant" recommendations.append("⚠️ Plan d'échantillonnage légèrement insuffisant") # Calcul de la taille d'échantillon recommandée en fonction du ratio POM/POL recommended_n = max(5, int(self.n * (pom_pol_ratio ** 0.7))) # Calcul de la fréquence recommandée en fonction du ratio POM/POL recommended_freq = max(1, self.frequence * pom_pol_ratio ** 0.5) # Arrondir à un nombre entier plus facile à implémenter recommended_n = ((recommended_n + 4) // 5) * 5 # Arrondir au multiple de 5 supérieur recommended_freq = round(recommended_freq * 2) / 2 # Arrondir au demi supérieur recommendations.append(f"- Augmenter la fréquence à au moins {recommended_freq:.1f} contrôles/heure") recommendations.append(f"- Augmenter la taille d'échantillon à {recommended_n} unités minimum") # Utiliser la fonction d'explication pédagogique explanation = self.explain_sampling_improvement(pom, pol, self.n, self.frequence) else: explanation = """ ### Votre plan d'échantillonnage est valide Le POM calculé ({:.2f}) est inférieur au POL requis ({:.2f}), ce qui signifie que votre plan d'échantillonnage est suffisant pour détecter un déréglage en temps utile. Cela indique que votre combinaison de {:.0f} unités par échantillon et {:.1f} contrôles/heure est adaptée à la variabilité de votre processus de production. """.format(pom, pol, self.n, self.frequence) return { 'is_valid': is_valid, 'pom': pom, 'pol': pol, 'recommendations': recommendations, 'explanation': explanation, 'severity': severity, 'recommended_n': recommended_n, 'recommended_freq': recommended_freq } def calculate_capability_indices(self, data: pd.Series, qn: float, emt: float) -> Dict[str, float]: """ Calcule les indices de capabilité du processus. Args: data: Série de données des pesées qn: Quantité nominale emt: Erreur maximale tolérée Returns: Dict: Indices de capabilité et statistiques associées """ mean = np.mean(data) std = np.std(data, ddof=1) # Calcul des indices de capabilité cp = emt / (6 * std) # Indice de capabilité cpk = min((qn + emt - mean) / (3 * std), (mean - (qn - emt)) / (3 * std)) # Indice de centrage # Calcul du pourcentage de défectueux théorique z_score_lower = (mean - (qn - emt)) / std z_score_upper = ((qn + emt) - mean) / std defective_rate = (1 - stats.norm.cdf(z_score_lower) + stats.norm.cdf(-z_score_upper)) * 100 return { 'mean': mean, 'std': std, 'cp': cp, 'cpk': cpk, 'defective_rate': defective_rate, 'process_centered': abs(mean - qn) <= emt/4 } def determine_target_weight(self, sigma_0: float, with_e_mark: bool = False) -> float: """ Détermine la valeur cible de remplissage adaptée en fonction de l'écart-type. Cette fonction implémente les formules du guide DGCCRF pour déterminer le seuil de centrage optimal pour garantir la conformité. Args: sigma_0: Écart-type du processus with_e_mark: Indique si le préemballage porte le signe "e" Returns: float: Valeur cible de remplissage """ # Détermination du seuil de centrage (ms) if sigma_0 <= self.emt / 2.05: ms = self.qn else: ms = self.qn - self.emt + 2.05 * sigma_0 # Ajustement supplémentaire pour les produits avec signe "e" if with_e_mark: # Calcul pour éviter les super-défectueux selon la taille du lot # (cf. Annexe 4 du guide DGCCRF) lot_size_factor = 3.09 # Valeur par défaut pour lots ≤ 1000 unités super_defective_threshold = self.qn - 2 * self.emt + lot_size_factor * sigma_0 # On prend la valeur la plus élevée entre les deux seuils ms = max(ms, super_defective_threshold) return ms def plot_distribution(self, data: pd.Series, title: str, qn: Optional[float] = None, emt: Optional[float] = None) -> go.Figure: """ Crée un graphique de distribution avec histogramme et courbe de densité. Args: data: Données à visualiser title: Titre du graphique qn: Quantité nominale (optionnel) emt: Erreur maximale tolérée (optionnel) Returns: go.Figure: Figure Plotly du graphique """ fig = go.Figure() # Histogramme fig.add_trace(go.Histogram( x=data, nbinsx=30, marker_color='#4B8BBE', name="Fréquence" )) # Courbe de densité (KDE) if len(data) > 1: # Vérification pour éviter les erreurs avec des données insuffisantes kde = stats.gaussian_kde(data) x_range = np.linspace(min(data), max(data), 100) fig.add_trace(go.Scatter( x=x_range, y=kde(x_range) * len(data), line=dict(color='red', width=2), name="Densité" )) # Ajout des lignes de référence si Qn et EMT sont fournis if qn is not None and emt is not None: # Quantité nominale fig.add_trace(go.Scatter( x=[qn, qn], y=[0, max(kde(x_range) * len(data)) if len(data) > 1 else len(data)], mode='lines', name='Qn', line=dict(color='green', dash='dash') )) # Limite inférieure (LSL) fig.add_trace(go.Scatter( x=[qn - emt, qn - emt], y=[0, max(kde(x_range) * len(data)) if len(data) > 1 else len(data)], mode='lines', name='LSL (Qn-EMT)', line=dict(color='orange', dash='dash') )) # Limite supérieure (USL) fig.add_trace(go.Scatter( x=[qn + emt, qn + emt], y=[0, max(kde(x_range) * len(data)) if len(data) > 1 else len(data)], mode='lines', name='USL (Qn+EMT)', line=dict(color='orange', dash='dash') )) # Mise en page fig.update_layout( title=title, xaxis_title='Poids (g)', yaxis_title='Fréquence', legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 ) ) return fig def plot_capability_indices(self, cp: float, cpk: float, title: str) -> go.Figure: """ Crée un graphique pour visualiser les indices de capabilité. Args: cp: Indice de capabilité du processus cpk: Indice de centrage du processus title: Titre du graphique Returns: go.Figure: Figure Plotly du graphique """ # Création de la figure fig = go.Figure() # Définition des seuils de performance thresholds = [ {'value': 1.0, 'color': 'red', 'text': 'Insuffisant'}, {'value': 1.33, 'color': 'orange', 'text': 'Acceptable'}, {'value': 1.67, 'color': 'green', 'text': 'Bon'}, {'value': 2.0, 'color': 'blue', 'text': 'Excellent'} ] # Ajout des indicateurs fig.add_trace(go.Indicator( mode="gauge+number", value=cp, title={'text': "Cp (Capabilité)"}, domain={'x': [0, 0.45], 'y': [0, 1]}, gauge={ 'axis': {'range': [None, 2.5], 'tickwidth': 1}, 'bar': {'color': "darkblue"}, 'steps': [ {'range': [0, 1.0], 'color': "red"}, {'range': [1.0, 1.33], 'color': "orange"}, {'range': [1.33, 1.67], 'color': "yellow"}, {'range': [1.67, 2.5], 'color': "green"} ], 'threshold': { 'line': {'color': "black", 'width': 4}, 'thickness': 0.75, 'value': 1.33 } } )) fig.add_trace(go.Indicator( mode="gauge+number", value=cpk, title={'text': "Cpk (Centrage)"}, domain={'x': [0.55, 1], 'y': [0, 1]}, gauge={ 'axis': {'range': [None, 2.5], 'tickwidth': 1}, 'bar': {'color': "darkblue"}, 'steps': [ {'range': [0, 1.0], 'color': "red"}, {'range': [1.0, 1.33], 'color': "orange"}, {'range': [1.33, 1.67], 'color': "yellow"}, {'range': [1.67, 2.5], 'color': "green"} ], 'threshold': { 'line': {'color': "black", 'width': 4}, 'thickness': 0.75, 'value': 1.33 } } )) # Mise en page fig.update_layout( title=title, height=300 ) return fig def plot_boxplot(self, data: pd.Series, title: str) -> go.Figure: """ Crée un graphique en boîte à moustaches. Args: data: Données à visualiser title: Titre du graphique Returns: go.Figure: Figure Plotly du graphique """ fig = px.box( data, points="all", labels={"value": "Poids (g)"} ) fig.update_layout( title=title, xaxis_title='', yaxis_title='Poids (g)' ) return fig def plot_control_chart(self, data: pd.DataFrame, title: str) -> go.Figure: """ Crée une carte de contrôle pour suivre l'évolution des pesées. Args: data: DataFrame contenant les données temporelles title: Titre du graphique Returns: go.Figure: Figure Plotly du graphique """ # Assurons-nous d'avoir un index temporel ou numérique if 'Date' in data.columns and 'Heure' in data.columns: data['Datetime'] = pd.to_datetime(data['Date'] + ' ' + data['Heure']) x_values = data['Datetime'] x_title = "Date et heure" else: # Création d'un index numérique si pas de date/heure data = data.reset_index() x_values = data.index x_title = "Index de mesure" # Calcul des limites de contrôle mean_value = data['Poids Brut (g)'].mean() std_value = data['Poids Brut (g)'].std() ucl = mean_value + 3 * std_value # Limite de contrôle supérieure lcl = mean_value - 3 * std_value # Limite de contrôle inférieure # Création du graphique fig = go.Figure() # Ligne des valeurs de mesure fig.add_trace(go.Scatter( x=x_values, y=data['Poids Brut (g)'], mode='lines+markers', name='Poids brut', line=dict(color='blue') )) # Ligne de la moyenne fig.add_trace(go.Scatter( x=[x_values.iloc[0], x_values.iloc[-1]], y=[mean_value, mean_value], mode='lines', name='Moyenne', line=dict(color='green', dash='dash') )) # Limites de contrôle fig.add_trace(go.Scatter( x=[x_values.iloc[0], x_values.iloc[-1]], y=[ucl, ucl], mode='lines', name='UCL (Lim. sup.)', line=dict(color='red', dash='dash') )) fig.add_trace(go.Scatter( x=[x_values.iloc[0], x_values.iloc[-1]], y=[lcl, lcl], mode='lines', name='LCL (Lim. inf.)', line=dict(color='red', dash='dash') )) # Mise en page fig.update_layout( title=title, xaxis_title=x_title, yaxis_title='Poids (g)', legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 ) ) return fig def plot_scatter(self, data: pd.DataFrame, title: str) -> go.Figure: """ Crée un graphique de dispersion avec ligne de tendance. Args: data: DataFrame avec colonnes index et valeurs title: Titre du graphique Returns: go.Figure: Figure Plotly du graphique """ fig = px.scatter( data, x='index', y='Poids Brut (g)', trendline="ols", labels={"index": "Index", "Poids Brut (g)": "Poids Brut (g)"} ) fig.update_layout( title=title, xaxis_title='Index', yaxis_title='Poids Brut (g)' ) return fig def visualize_sampling_risk(self, sigma_0: float, pom: float, pol: float) -> go.Figure: """ Crée une visualisation du risque associé au plan d'échantillonnage. Args: sigma_0: Écart-type du processus pom: Période Opérationnelle Moyenne calculée pol: Période Opérationnelle Limite requise Returns: go.Figure: Figure Plotly de la visualisation """ # Calcul du risque en fonction du ratio POM/POL ratio = pom / pol risk_level = min(100, max(0, (ratio - 1) * 50)) # 0% si POM≤POL, jusqu'à 100% pour POM≥3*POL # Création de la jauge de risque fig = go.Figure() fig.add_trace(go.Indicator( mode="gauge+number+delta", value=risk_level, domain={'x': [0, 1], 'y': [0, 1]}, title={'text': "Risque de non-détection d'un déréglage"}, gauge={ 'axis': {'range': [0, 100], 'tickwidth': 1}, 'bar': {'color': "darkred"}, 'steps': [ {'range': [0, 20], 'color': "green"}, {'range': [20, 50], 'color': "yellow"}, {'range': [50, 100], 'color': "red"} ], 'threshold': { 'line': {'color': "black", 'width': 4}, 'thickness': 0.75, 'value': 20 } }, delta={'reference': 20, 'increasing': {'color': "red"}, 'decreasing': {'color': "green"}} )) # Mise en page fig.update_layout( height=300, annotations=[ dict( x=0.5, y=-0.15, xref="paper", yref="paper", text="Un risque élevé signifie qu'un déréglage pourrait rester non détecté
trop longtemps, conduisant à la production de lots non conformes", showarrow=False ) ] ) return fig def calculate_defective_count(self, data: pd.Series, qn: float, emt: float) -> Dict[str, int]: """ Calcule le nombre de préemballages défectueux et super-défectueux. Args: data: Série de données des pesées qn: Quantité nominale emt: Erreur maximale tolérée Returns: Dict: Nombre de défectueux et super-défectueux """ defective_threshold = qn - emt super_defective_threshold = qn - 2 * emt defective_count = sum(data < defective_threshold) super_defective_count = sum(data < super_defective_threshold) return { 'defective_count': defective_count, 'super_defective_count': super_defective_count, 'defective_percentage': (defective_count / len(data)) * 100 if len(data) > 0 else 0, 'super_defective_percentage': (super_defective_count / len(data)) * 100 if len(data) > 0 else 0 } def perform_regression_analysis(self, data: pd.DataFrame) -> Dict[str, Any]: """ Effectue une analyse de régression linéaire. Args: data: DataFrame contenant les données Returns: Dict: Résultats de la régression """ # Préparation des données data_reset = data.reset_index() if 'index' not in data.columns else data.copy() # Modèle de régression model = sm.OLS(data_reset['Poids Brut (g)'], sm.add_constant(data_reset['index'])).fit() # Récupération des résultats clés results = { 'model': model, 'coefficient': model.params['index'], 'intercept': model.params['const'], 'r_squared': model.rsquared, 'p_value': model.pvalues['index'], 'has_trend': model.pvalues['index'] < 0.05 } return results def perform_anova(self, data: pd.DataFrame) -> Dict[str, Any]: """ Effectue une analyse de variance (ANOVA) entre les lots. Args: data: DataFrame contenant les données avec colonne 'Lot' Returns: Dict: Résultats de l'ANOVA """ if 'Lot' not in data.columns or len(data['Lot'].unique()) <= 1: return { 'valid': False, 'message': "Impossible de réaliser l'ANOVA: moins de 2 lots distincts." } try: # Création du modèle model = ols('`Poids Brut (g)` ~ C(Lot)', data=data).fit() anova_table = sm.stats.anova_lm(model, typ=2) # Détermination de la significativité p_value = anova_table.loc['C(Lot)', 'PR(>F)'] return { 'valid': True, 'anova_table': anova_table, 'p_value': p_value, 'significant_difference': p_value < 0.05, 'model': model } except Exception as e: return { 'valid': False, 'message': f"Erreur lors de l'ANOVA: {str(e)}" } def lookup_emt(qn: float) -> float: """ Recherche l'erreur maximale tolérée (EMT) en fonction de la quantité nominale. Implémentation selon l'article 4 du décret du 31/01/1978 (Annexe 5 du guide DGCCRF). Args: qn: Quantité nominale en grammes ou millilitres Returns: float: Erreur maximale tolérée """ qn = float(qn) # Conversion explicite en float pour éviter les erreurs de type if qn < 5: # Hors champ d'application return 0.0 elif qn <= 50: return qn * 0.09 # 9% de QN elif qn <= 100: return 4.5 # 4.5g elif qn <= 200: return qn * 0.045 # 4.5% de QN elif qn <= 300: return 9.0 # 9g elif qn <= 500: return qn * 0.03 # 3% de QN elif qn <= 1000: return 15.0 # 15g elif qn <= 10000: return qn * 0.015 # 1.5% de QN elif qn <= 15000: return 150.0 # 150g else: return qn * 0.01 # 1% de QN # Fonction d'affichage de l'aide def show_help(): """Affiche l'aide contextuelle dans la sidebar""" st.sidebar.markdown(""" ## 💡 Aide rapide ### Paramètres essentiels - **Quantité Nominale**: Poids cible du produit (Qn) - **EMT**: Erreur maximale tolérée selon l'article 4 du décret du 31/01/1978 - **Surpoids max**: Tolérance supérieure acceptée pour le centrage ### Procédure d'utilisation 1. Remplir les paramètres 2. Télécharger le template Excel 3. Compléter les données de pesées 4. Importer le fichier 5. Analyser les résultats ### Interprétation des résultats - **POM ≤ POL**: Plan d'échantillonnage valide - **Cp > 1.33**: Processus capable - **Cpk > 1.33**: Processus centré et capable - **Taux défectueux < 2%**: Conformité critère défectueux - **Aucun super-défectueux**: Requis pour produits avec signe "e" ### Références réglementaires - Décret n°78-166 du 31 janvier 1978 - Guide DGCCRF des bonnes pratiques """) # Fonction d'affichage de la documentation détaillée def show_detailed_documentation(): """Affiche la documentation détaillée dans un expander""" with st.expander("📚 Guide simplifié du contrôle métrologique des préemballages"): st.markdown(""" ## Guide simplifié du contrôle métrologique des préemballages ### 1. Cadre réglementaire et références #### 1.1 Textes réglementaires - **Décret n°78-166** du 31 janvier 1978 : Ce décret fixe les règles de contrôle des produits préemballés, comme les produits alimentaires. - **Directive 76/211/CEE** : Cette directive européenne concerne le poids ou le volume indiqué sur les emballages des produits. - **Guide DGCCRF** : Guide officiel pour vérifier que les emballages respectent les poids ou volumes annoncés. #### 1.2 Normes applicables - **ISO 2859** : Règles pour vérifier si un lot de produits est conforme à l'aide de quelques échantillons. - **ISO 3951** : Règles pour contrôler la qualité des produits en mesurant précisément certains paramètres (comme le poids). ### 2. Définitions simples et concepts clés #### 2.1 Les principaux paramètres du contrôle - **Quantité nominale (Qn)** : C'est le poids ou volume indiqué sur l'emballage (par exemple, 500 g). - **Erreur maximale tolérée (T)** : Il est normal qu'il y ait de légères différences de poids entre les emballages. L'erreur maximale tolérée (ou EMT) est la marge d'erreur autorisée par la loi. - **Qn - T** : C'est le poids minimum acceptable pour chaque produit. Si un produit est en dessous de ce poids, il est considéré comme non conforme. - **Ms (Moyenne minimale acceptable)** : C'est le poids moyen minimum que doit atteindre l'ensemble des produits d'un lot pour être jugé conforme. #### 2.2 Comment calculer la moyenne minimale (Ms) Pour vérifier si un lot est conforme, nous devons calculer la **moyenne minimale acceptable (Ms)**. Voici comment elle est calculée : ```plaintext Si les variations de poids (appelées écart-type) sont faibles : Ms = Qn Si les variations de poids sont importantes : Ms = Qn - T + 2.05 * écart-type ``` **Exemple** : Si la quantité nominale (Qn) est 500 g et l'erreur tolérée (T) est 15 g, et que l'écart-type (σ₀) est faible, la moyenne acceptable du lot sera simplement 500 g. ### 3. Calcul du POM (Période Opérationnelle Moyenne) #### 3.1 Qu'est-ce que le POM ? Le POM est un indicateur qui permet de savoir combien d'échantillons vous devez prélever en moyenne pour détecter un problème de poids dans un lot de produits. Plus il est faible, mieux c'est. #### 3.2 Comment calculer le POM ? Pour calculer le POM, il faut utiliser une méthode mathématique qui tient compte de la variation de poids dans le lot. Le calcul se fait en plusieurs étapes : - **Étape 1** : Calculer le **delta** (c'est la différence entre la cible de production et le poids minimal acceptable). ```plaintext delta = (Ms + surpoids_max - (Qn - T + 2.05 * écart-type)) / écart-type ``` - **Étape 2** : Calculer le **delta_sqrt_n** qui tient compte de la taille de l'échantillon (combien de produits on pèse). ```plaintext delta_sqrt_n = delta * √n ``` - **Étape 3** : On utilise la valeur du delta_sqrt_n et une table de référence (fournie par la DGCCRF) pour trouver la valeur du POM. ### 4. Analyse des résultats (vérification des données) #### 4.1 Test de normalité (Shapiro-Wilk) Avant de valider le contrôle, on vérifie si les poids des produits sont "répartis normalement". Ce test permet de savoir si les poids suivent une distribution habituelle. Si le test dit "oui", on peut utiliser les calculs habituels pour valider le contrôle. #### 4.2 Indices de capabilité (Cp et Cpk) Les indices de capabilité permettent de savoir si le processus de production est capable de fabriquer des produits qui respectent les limites légales de poids. - **Cp** : Il mesure la capacité du processus à respecter l'erreur maximale tolérée (T). - **Cpk** : Il mesure si le processus est bien centré par rapport à la quantité nominale (Qn). ```plaintext Cp = T / (6 * écart-type) Cpk = min((USL - moyenne du processus)/(3 * écart-type), (moyenne du processus - LSL)/(3 * écart-type)) Où : - **USL** (Upper Specification Limit) = Qn + T (limite supérieure de poids) - **LSL** (Lower Specification Limit) = Qn - T (limite inférieure de poids) ``` ### 5. Critères de validation du lot #### 5.1 Règle pour valider ou rejeter le lot Une fois le POM calculé, on le compare au POl (la période opérationnelle limite). Le POl dépend de la fréquence des prélèvements. ```plaintext Si POM ≤ POl : Le contrôle est validé (le lot est conforme). Si POM > POl : Le contrôle est insuffisant (il faut prélever plus d'échantillons ou revoir le processus). ``` #### 5.2 Interprétation des indices Cp et Cpk - **Cp ≥ 1.33** : Le processus est capable de respecter les tolérances. - **Cpk ≥ 1.33** : Le processus est bien centré et les produits sont conformes. ### 6. Limites et précautions #### 6.1 Validité des calculs - Le calcul du POM n'est valable que pour certaines valeurs (delta_sqrt_n doit être compris entre 0.17 et 3.38). - Si la valeur est en dehors de cette plage, des ajustements conservateurs sont faits. #### 6.2 Hypothèses importantes - Il est important que les poids des produits suivent une distribution normale pour que les calculs fonctionnent correctement. - Le processus de production doit être stable (sans grandes variations) pour garantir la validité des résultats. ### 7. Recommandations pratiques pour la production #### 7.1 Fréquence d'échantillonnage - **Recommandation minimum** : 1 prélèvement toutes les 4 heures pour vérifier que le lot reste conforme. - Si le processus est plus variable, il peut être nécessaire de faire des prélèvements plus fréquents. #### 7.2 Taille des échantillons - **Minimum recommandé** : 5 échantillons par lot. - Si le processus est plus variable ou que les exigences sont plus strictes, il peut être nécessaire d'augmenter la taille des échantillons. """) def show_regulatory_requirements(): """Affiche un résumé des exigences réglementaires""" with st.expander("📋 Exigences réglementaires (Décret n°78-166)"): st.markdown(""" ## Critères fondamentaux de conformité des préemballages Selon le décret n°78-166 du 31 janvier 1978, trois critères doivent être respectés : ### 1. Critère de la moyenne Le contenu effectif des préemballages du lot ne doit pas être inférieur, en moyenne, à la quantité nominale. ### 2. Critère des défectueux Un préemballage est "défectueux" s'il présente un manquant supérieur à l'erreur maximale tolérée (EMT). La proportion de préemballages défectueux doit être inférieure à 2% pour garantir l'acceptation du lot. ### 3. Critère des super-défectueux (pour les produits avec signe "e") Un préemballage est "super-défectueux" s'il présente un manquant supérieur à deux fois l'EMT. Aucun préemballage portant le signe "e" ne peut être super-défectueux. ### Erreurs maximales tolérées (EMT) """) # Tableau des EMT emt_data = { "Contenu nominal (g/ml)": ["5 à 50", "50 à 100", "100 à 200", "200 à 300", "300 à 500", "500 à 1000", "1000 à 10000", "10000 à 15000", "Supérieur à 15000"], "EMT en % de QN": ["9%", "-", "4.5%", "-", "3%", "-", "1.5%", "-", "1%"], "EMT en g/ml": ["-", "4.5g", "-", "9g", "-", "15g", "-", "150g", "-"] } emt_df = pd.DataFrame(emt_data) st.table(emt_df) st.markdown(""" ### Signe "e" Le signe "e" est une déclaration de garantie de conformité aux exigences métrologiques. Il ne peut être apposé que sur des préemballages dont la quantité nominale est ≤ 10 kg ou 10 L. Son utilisation implique des contrôles plus stricts et l'absence totale de super-défectueux. """) def main(): """Fonction principale de l'application Streamlit""" st.set_page_config( page_title="Contrôle Métrologique des Préemballages", layout="wide", initial_sidebar_state="expanded" ) # Style CSS personnalisé st.markdown(""" """, unsafe_allow_html=True) # Titre principal avec logo/icône st.title("🎯 Contrôle Métrologique des Préemballages -VERSION DEMO- nous contacter pour full version") st.markdown("*Application conforme aux directives DGCCRF pour le contrôle des lots préemballés*") # Affichage de l'aide dans la sidebar show_help() # Onglets principaux tab1, tab2, tab3 = st.tabs(["📊 Contrôle et Analyse", "📝 Documentation", "ℹ️ À propos"]) with tab1: # Interface principale col1, col2 = st.columns(2) with col1: st.subheader("📝 Paramètres du contrôle") with st.form("parametres"): # Affichage d'info-bulles pour chaque paramètre qn = st.number_input( "Quantité Nominale (g):", value=500.0, min_value=5.0, help="Poids net indiqué sur l'emballage" ) # Calcul automatique de l'EMT en fonction de la QN (avec possibilité de modification) default_emt = float(lookup_emt(qn)) # Conversion explicite en float emt = st.number_input( "Erreur maximale tolérée (g):", value=default_emt, min_value=0.0, help="Selon art. 4 du décret 78-166 (calculée automatiquement)" ) surpoids_max = st.number_input( "Surpoids max toléré (g):", value=2.0, min_value=0.0, help="Tolérance supérieure pour le centrage du processus" ) col_n, col_freq = st.columns(2) with col_n: n = st.number_input( "Taille d'échantillon:", value=5, min_value=1, help="Nombre d'unités par échantillon (min. recommandé: 5)" ) with col_freq: frequence = st.number_input( "Fréquence (par heure):", value=1.0, min_value=0.1, help="Nombre d'échantillonnages par heure" ) # Option pour indiquer si les produits portent le signe "e" has_e_mark = st.checkbox( "Produits avec signe 'e'", value=False, help="Le signe 'e' indique une garantie de conformité métrologique" ) submitted = st.form_submit_button("Valider les paramètres") with col2: st.subheader("📥 Import des données") template = create_excel_template() st.download_button( label="📋 Télécharger le template Excel", data=template, file_name="template_pesees.xlsx", help="Téléchargez ce modèle pour saisir vos données de pesées" ) uploaded_file = st.file_uploader( "Téléchargez votre fichier Excel", type=["xlsx"], help="Utilisez le template Excel rempli avec vos données" ) # Affichage d'un exemple de données attendues with st.expander("ℹ️ Format des données attendues"): st.markdown(""" Le fichier Excel doit contenir au minimum les colonnes suivantes: - **Tare (g)**: Poids de l'emballage vide - **Poids Brut (g)**: Poids total (produit + emballage) Les colonnes optionnelles incluent: - **Date**: Date de la pesée (AAAA-MM-JJ) - **Heure**: Heure de la pesée (HH:MM) - **Lot**: Identification du lot - **Remarques**: Commentaires supplémentaires """) example_data = pd.DataFrame({ 'Date': ['2024-01-01', '2024-01-01', '2024-01-01'], 'Heure': ['08:00', '09:00', '10:00'], 'Lot': ['LOT001', 'LOT001', 'LOT001'], 'Tare (g)': [10.5, 10.4, 10.6], 'Poids Brut (g)': [510.5, 509.8, 511.2], 'Remarques': ['', '', ''] }) st.dataframe(example_data) # Traitement et analyse des données if uploaded_file and submitted: try: # Création de l'objet de calcul control = MetrologicalCalculations(qn, emt, surpoids_max, n, frequence) # Lecture du fichier Excel df = pd.read_excel(uploaded_file, sheet_name="Pesées") # Validation des colonnes requises required_columns = ["Tare (g)", "Poids Brut (g)"] if not all(col in df.columns for col in required_columns): raise ValidationError("❌ Format de fichier incorrect. Utilisez le template fourni.") # Nettoyage des données df = df.dropna(subset=["Poids Brut (g)"]) # Affichage des données importées st.subheader("📊 Données importées") st.dataframe(df) # Extraction des données pertinentes tare_poids = df["Tare (g)"].dropna() poids_bruts = df["Poids Brut (g)"].dropna() # Calcul des poids nets si nécessaire if 'Poids Net (g)' not in df.columns: # Vérification de la compatibilité des longueurs if len(tare_poids) == len(poids_bruts): poids_nets = poids_bruts - tare_poids else: # Utilisation de la tare moyenne si les longueurs ne correspondent pas tare_moyenne = tare_poids.mean() poids_nets = poids_bruts - tare_moyenne st.warning(f"⚠️ Utilisation de la tare moyenne ({tare_moyenne:.2f}g) pour le calcul des poids nets.") else: poids_nets = df['Poids Net (g)'].dropna() # Vérification de la taille minimale des données if len(poids_nets) < 5: st.warning("⚠️ Il est recommandé d'avoir au moins 5 mesures pour une analyse fiable.") # Répartition des analyses en sections st.markdown("---") # 1. Distribution et statistiques de base st.subheader("📊 Distribution et statistiques de base") col_stats, col_dist = st.columns([1, 2]) with col_stats: # Statistiques descriptives stats_df = pd.DataFrame({ 'Statistique': [ 'Nombre de mesures', 'Poids moyen', 'Écart-type', 'Minimum', 'Maximum', 'Médiane', 'Coefficient de variation' ], 'Valeur': [ len(poids_nets), f"{poids_nets.mean():.2f} g", f"{poids_nets.std():.2f} g", f"{poids_nets.min():.2f} g", f"{poids_nets.max():.2f} g", f"{poids_nets.median():.2f} g", f"{(poids_nets.std() / poids_nets.mean() * 100):.2f} %" ] }) st.dataframe(stats_df, hide_index=True) # Test de normalité if len(poids_nets) >= 3: # Le test de Shapiro nécessite au moins 3 observations stat, p_value = stats.shapiro(poids_nets) normality_result = "Normal" if p_value > 0.05 else "Non normal" st.write(f"**Test de normalité (Shapiro-Wilk)**: {normality_result} (p-value: {p_value:.4f})") if p_value <= 0.05: st.warning("⚠️ Les données ne suivent pas une distribution normale. Les calculs statistiques peuvent être moins fiables.") with col_dist: # Graphique de distribution fig_poids = control.plot_distribution(poids_nets, "Distribution des Poids Nets", qn, emt) st.plotly_chart(fig_poids, use_container_width=True) # 2. Analyse de capabilité st.markdown("---") st.subheader("📈 Capabilité du processus") col_cap, col_gauge = st.columns([1, 1]) with col_cap: # Calcul de la capabilité capability = control.calculate_capability_indices(poids_nets, qn, emt) # Détection des défectueux defective_stats = control.calculate_defective_count(poids_nets, qn, emt) # Analyse du plan d'échantillonnage sigma_0 = capability['std'] sampling_plan_evaluation = control.evaluate_sampling_plan(sigma_0) # Calcul de la cible recommandée target_weight = control.determine_target_weight(sigma_0, has_e_mark) # Affichage des résultats cap_results = pd.DataFrame({ 'Indicateur': [ 'Moyenne du processus', 'Écart-type du processus', 'Cp (Indice de capabilité)', 'Cpk (Indice de centrage)', 'Défectueux', 'Super-défectueux', 'POM Calculé', 'POL Requis', 'Cible recommandée' ], 'Valeur': [ f"{capability['mean']:.2f} g", f"{capability['std']:.2f} g", f"{capability['cp']:.2f}", f"{capability['cpk']:.2f}", f"{defective_stats['defective_count']} ({defective_stats['defective_percentage']:.1f}%)", f"{defective_stats['super_defective_count']} ({defective_stats['super_defective_percentage']:.1f}%)", f"{sampling_plan_evaluation['pom']:.2f}", f"{sampling_plan_evaluation['pol']:.2f}", f"{target_weight:.2f} g" ] }) st.dataframe(cap_results, hide_index=True) # Interprétation des résultats st.subheader("🔎 Interprétation des résultats") # Critère de la moyenne if capability['mean'] >= qn: st.success("✅ **Critère de la moyenne**: Conforme - La moyenne est supérieure à la quantité nominale.") else: st.error(f"❌ **Critère de la moyenne**: Non conforme - La moyenne ({capability['mean']:.2f}g) est inférieure à la quantité nominale ({qn}g).") # Critère des défectueux if defective_stats['defective_percentage'] <= 2.0: st.success(f"✅ **Critère des défectueux**: Conforme - Taux de défectueux ({defective_stats['defective_percentage']:.1f}%) inférieur au seuil de 2%.") else: st.error(f"❌ **Critère des défectueux**: Non conforme - Taux de défectueux ({defective_stats['defective_percentage']:.1f}%) supérieur au seuil de 2%.") # Critère des super-défectueux (pour produits avec "e") if has_e_mark: if defective_stats['super_defective_count'] == 0: st.success("✅ **Critère des super-défectueux**: Conforme - Aucun super-défectueux détecté.") else: st.error(f"❌ **Critère des super-défectueux**: Non conforme - {defective_stats['super_defective_count']} super-défectueux détectés.") # Évaluation du plan d'échantillonnage if sampling_plan_evaluation['is_valid']: st.success("✅ **Plan d'échantillonnage**: Validé - La fréquence et la taille d'échantillon sont suffisantes.") # Affichage de l'explication même pour les plans valides with st.expander("ℹ️ Détails du plan d'échantillonnage"): st.markdown(sampling_plan_evaluation['explanation']) # Utilisation de la fonction px.bar au lieu de plot_pom_pol_comparison fig_pom_pol = px.bar( x=["POM Calculé", "POL Requis"], y=[sampling_plan_evaluation['pom'], sampling_plan_evaluation['pol']], color=["POM", "POL"], title="Comparaison POM vs POL", labels={"x": "", "y": "Valeur"}, color_discrete_map={"POM": "red" if sampling_plan_evaluation['pom'] > sampling_plan_evaluation['pol'] else "green", "POL": "green"} ) st.plotly_chart(fig_pom_pol, use_container_width=True) else: st.error("❌ **Plan d'échantillonnage**: Insuffisant - Voir recommandations ci-dessous.") for rec in sampling_plan_evaluation['recommendations']: st.write(rec) # Ajout d'une section explicative pour l'utilisateur with st.expander("📊 Comprendre pourquoi le plan est insuffisant", expanded=True): st.markdown(sampling_plan_evaluation['explanation']) # Utilisation de la fonction px.bar au lieu de plot_pom_pol_comparison fig_pom_pol = px.bar( x=["POM Calculé", "POL Requis"], y=[sampling_plan_evaluation['pom'], sampling_plan_evaluation['pol']], color=["POM", "POL"], title="Comparaison POM vs POL", labels={"x": "", "y": "Valeur"}, color_discrete_map={"POM": "red", "POL": "green"} ) # Ajout d'une ligne horizontale pour la valeur POL (référence) fig_pom_pol.add_shape( type="line", x0=-0.5, y0=sampling_plan_evaluation['pol'], x1=1.5, y1=sampling_plan_evaluation['pol'], line=dict( color="green", width=2, dash="dash", ) ) fig_pom_pol.update_layout( annotations=[ dict( x=0.5, y=1.1, xref="paper", yref="paper", text="Pour un plan d'échantillonnage valide, POM doit être ≤ POL", showarrow=False, font=dict(size=12) ) ] ) st.plotly_chart(fig_pom_pol, use_container_width=True) # Ajout d'une explication sur le comportement paradoxal st.markdown(""" ### Pourquoi le plan reste insuffisant malgré l'augmentation des paramètres? Un phénomène important à comprendre est que le POL (Période Opérationnelle Limite) **augmente également** lorsque vous augmentez la taille d'échantillon et la fréquence. C'est normal car: 1. Une plus grande taille d'échantillon nécessite un contrôle plus strict (POL plus élevé) 2. Une fréquence plus élevée implique une surveillance plus intense (POL ajusté en conséquence) Pour cette raison, l'augmentation des paramètres peut parfois sembler "déplacer la cible". Continuez à suivre les recommandations jusqu'à ce que POM ≤ POL. """) # Visualisation du risque (utilisation d'une fonction de px au lieu de visualize_sampling_risk) pom_pol_ratio = sampling_plan_evaluation['pom'] / sampling_plan_evaluation['pol'] risk_level = min(100, max(0, (pom_pol_ratio - 1) * 50)) fig_risk = go.Figure(go.Indicator( mode="gauge+number", value=risk_level, title={'text': "Risque de non-détection d'un déréglage (%)"}, gauge={ 'axis': {'range': [0, 100]}, 'bar': {'color': "darkred"}, 'steps': [ {'range': [0, 20], 'color': "green"}, {'range': [20, 50], 'color': "yellow"}, {'range': [50, 100], 'color': "red"} ], 'threshold': { 'line': {'color': "black", 'width': 2}, 'thickness': 0.75, 'value': 20 } } )) st.plotly_chart(fig_risk, use_container_width=True) # Informations supplémentaires sur l'amélioration de l'échantillonnage st.markdown(""" ### Impact de l'amélioration de l'échantillonnage Augmenter la **taille d'échantillon** permet de: - Réduire le risque de non-détection d'un déréglage - Améliorer la précision de l'estimation de la moyenne - Mieux évaluer la variabilité du processus Augmenter la **fréquence** permet de: - Réduire le délai de détection d'un déréglage - Diminuer le nombre de produits non conformes fabriqués entre les contrôles - Mieux suivre les évolutions du processus au cours du temps """) # Simulateur d'amélioration with st.expander("🔄 Simuler l'amélioration du plan d'échantillonnage", expanded=True): st.markdown("Ajustez les paramètres pour voir l'impact sur le POM et le POL:") col_sim1, col_sim2 = st.columns(2) with col_sim1: sim_n = st.slider("Taille d'échantillon simulée:", min_value=control.n, max_value=max(50, control.n*3), value=sampling_plan_evaluation['recommended_n']) with col_sim2: sim_freq = st.slider("Fréquence simulée (contrôles/heure):", min_value=float(control.frequence), max_value=max(5.0, float(control.frequence)*3), value=float(sampling_plan_evaluation['recommended_freq']), step=0.5) # Nouveau slider pour le surpoids surp_percent = st.slider("Surpoids simulé (% de QN):", min_value=0.0, max_value=1.0, value=0.17, # 0.17% recommandé par le guide DGCCRF step=0.01, format="%.2f%%") # Conversion du pourcentage en valeur absolue sim_surpoids = qn * surp_percent / 100.0 # Simulation avec les nouveaux paramètres en utilisant la nouvelle fonction simulate_improved_plan sim_results = control.simulate_improved_plan(sigma_0, sim_n, sim_freq, sim_surpoids) # Affichage des résultats simulés result_color = "green" if sim_results['is_valid'] else "red" st.markdown(f"

Résultat de la simulation

", unsafe_allow_html=True) sim_results_df = pd.DataFrame({ 'Paramètre': [ 'Écart-type optimisé', 'POM Simulé', 'POL Simulé', 'Statut', 'Cible de production' ], 'Valeur': [ f"{sim_results['optimized_sigma']:.2f} g", f"{sim_results['pom']:.2f}", f"{sim_results['pol']:.2f}", "✅ VALIDE" if sim_results['is_valid'] else "❌ TOUJOURS INSUFFISANT", f"{sim_results['target_weight']:.2f} g" ] }) st.table(sim_results_df) # Visualisation comparative compare_data = pd.DataFrame({ 'Configuration': ['Actuelle', 'Simulée'], 'POM': [sampling_plan_evaluation['pom'], sim_results['pom']], 'POL': [sampling_plan_evaluation['pol'], sim_results['pol']] }) fig_compare = px.bar( compare_data, x='Configuration', y=['POM', 'POL'], barmode='group', title="Comparaison des configurations", color_discrete_map={'POM': 'blue', 'POL': 'green'} ) st.plotly_chart(fig_compare, use_container_width=True) with col_gauge: # Visualisation des indices de capabilité fig_capability = control.plot_capability_indices(capability['cp'], capability['cpk'], "Indices de Capabilité") st.plotly_chart(fig_capability, use_container_width=True) # Visualisation du boxplot fig_boxplot = control.plot_boxplot(poids_nets, "Dispersion des Poids Nets") st.plotly_chart(fig_boxplot, use_container_width=True) # 3. Analyse temporelle et tendances st.markdown("---") st.subheader("📈 Analyse temporelle et tendances") # Carte de contrôle if 'Date' in df.columns or 'Heure' in df.columns: fig_control = control.plot_control_chart(df, "Carte de contrôle des Poids Bruts") st.plotly_chart(fig_control, use_container_width=True) else: # Scatter plot simple si pas de données temporelles df_scatter = pd.DataFrame({'index': range(len(poids_nets)), 'Poids Brut (g)': poids_nets.values}) fig_scatter = control.plot_scatter(df_scatter, "Évolution des Poids Nets") st.plotly_chart(fig_scatter, use_container_width=True) # Régression linéaire pour détecter des tendances if len(poids_nets) > 2: df_trend = pd.DataFrame({'index': range(len(poids_nets)), 'Poids Brut (g)': poids_nets.values}) regression_results = control.perform_regression_analysis(df_trend) if regression_results['has_trend']: trend_direction = "à la hausse" if regression_results['coefficient'] > 0 else "à la baisse" st.warning(f"⚠️ Tendance significative {trend_direction} détectée (p-value: {regression_results['p_value']:.4f})") st.write(f"Coefficient de pente: {regression_results['coefficient']:.4f} g/unité, R²: {regression_results['r_squared']:.4f}") else: st.info("ℹ️ Aucune tendance significative détectée.") # 4. Analyse par lot (si disponible) if 'Lot' in df.columns and len(df['Lot'].unique()) > 1: st.markdown("---") st.subheader("📊 Analyse par lot") anova_results = control.perform_anova(df) if anova_results['valid']: if anova_results['significant_difference']: st.warning(f"⚠️ Différences significatives entre les lots détectées (p-value: {anova_results['p_value']:.4f})") else: st.info(f"ℹ️ Pas de différences significatives entre les lots (p-value: {anova_results['p_value']:.4f})") # Boxplot par lot fig_lot = px.box(df, x='Lot', y='Poids Brut (g)', title="Comparaison des lots") st.plotly_chart(fig_lot, use_container_width=True) # 5. Recommandations finales st.markdown("---") st.subheader("🔍 Conclusions et recommandations") # Évaluation globale de conformité if (capability['mean'] >= qn and defective_stats['defective_percentage'] <= 2.0 and (not has_e_mark or defective_stats['super_defective_count'] == 0)): st.success("✅ **LOT CONFORME** - Les critères réglementaires sont respectés.") else: st.error("❌ **LOT NON CONFORME** - Un ou plusieurs critères réglementaires ne sont pas respectés.") # Recommandations spécifiques recommendations = [] # Recommandation sur le centrage if capability['mean'] < qn: recommendations.append(f"• Ajuster la cible de remplissage à au moins {target_weight:.2f}g (actuellement: {capability['mean']:.2f}g)") # Recommandation sur la dispersion if capability['cp'] < 1.33: recommendations.append("• Réduire la variabilité du processus en améliorant la précision de la machine d'emplissage") # Recommandation sur le plan d'échantillonnage if not sampling_plan_evaluation['is_valid']: for rec in sampling_plan_evaluation['recommendations']: if rec.startswith("-"): recommendations.append(f"• {rec[2:]}") # Affichage des recommandations if recommendations: st.markdown("### 📝 Recommandations:") for rec in recommendations: st.markdown(rec) else: st.markdown("### 📝 Recommandations:") st.markdown("• Maintenir les paramètres actuels du processus qui sont satisfaisants") # Option d'export des résultats st.markdown("---") # Création d'un résumé des résultats en format markdown results_summary = f""" # Rapport de Contrôle Métrologique - {pd.Timestamp.now().strftime('%d/%m/%Y')} ## Paramètres du contrôle - **Quantité Nominale**: {qn}g - **EMT**: {emt}g - **Produits avec signe "e"**: {"Oui" if has_e_mark else "Non"} ## Résultats - **Nombre d'unités contrôlées**: {len(poids_nets)} - **Moyenne**: {capability['mean']:.2f}g - **Écart-type**: {capability['std']:.2f}g - **Taux de défectueux**: {defective_stats['defective_percentage']:.1f}% - **Capabilité (Cp)**: {capability['cp']:.2f} - **Centrage (Cpk)**: {capability['cpk']:.2f} ## Conformité - **Critère de la moyenne**: {"Conforme" if capability['mean'] >= qn else "Non conforme"} - **Critère des défectueux**: {"Conforme" if defective_stats['defective_percentage'] <= 2.0 else "Non conforme"} """ if has_e_mark: results_summary += f"- **Critère des super-défectueux**: {'Conforme' if defective_stats['super_defective_count'] == 0 else 'Non conforme'}\n" results_summary += f"\n## Conclusion\n" if (capability['mean'] >= qn and defective_stats['defective_percentage'] <= 2.0 and (not has_e_mark or defective_stats['super_defective_count'] == 0)): results_summary += "**LOT CONFORME** - Les critères réglementaires sont respectés.\n" else: results_summary += "**LOT NON CONFORME** - Un ou plusieurs critères réglementaires ne sont pas respectés.\n" # Bouton pour télécharger le rapport st.download_button( label="📥 Télécharger le rapport", data=results_summary, file_name=f"rapport_metrologique_{pd.Timestamp.now().strftime('%Y%m%d')}.md", mime="text/markdown" ) except ValidationError as e: st.error(f"{str(e)}") except Exception as e: st.error(f"❌ Erreur lors de l'analyse : {str(e)}") if st.checkbox("Voir les détails de l'erreur"): import traceback st.code(traceback.format_exc()) with tab2: # Documentation détaillée et exigences réglementaires st.subheader("📚 Documentation") # Affichage des exigences réglementaires show_regulatory_requirements() # Documentation détaillée show_detailed_documentation() with tab3: # À propos de l'application st.subheader("ℹ️ À propos de cette application") st.markdown(""" Cette application a été développée pour faciliter le contrôle métrologique des préemballages conformément aux exigences réglementaires françaises et européennes. ### Fonctionnalités - Calcul automatique des EMT selon le décret 78-166 - Analyse complète de la capabilité du processus - Validation du plan d'échantillonnage (POM vs POL) - Vérification des 3 critères fondamentaux de conformité - Visualisations statistiques avancées - Rapports exportables ### Références - Décret n°78-166 du 31 janvier 1978 - Directive européenne 76/211/CEE - Guide DGCCRF des bonnes pratiques - Normes ISO 2859 et ISO 3951 ### Version - Version: 2.0 (Mars 2025) - Dernière mise à jour: Intégration des nouvelles recommandations DGCCRF ### Contact Pour toute question ou suggestion d'amélioration, veuillez contacter le support technique. """) if __name__ == "__main__": main()