| 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 |
|
|
| |
| class ValidationError(Exception): |
| """Exception levée lorsque les données ne respectent pas les critères de validation.""" |
| pass |
|
|
| |
| 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) |
|
|
| |
| 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' |
| }) |
|
|
| |
| ws_pesees = workbook.add_worksheet("Pesées") |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|
| |
| 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 |
| """ |
| |
| base_pol = 4.0 |
| |
| |
| qn_factor = min(2.0, max(0.5, self.qn / 1000)) |
| emt_factor = min(1.5, max(0.5, self.emt / (self.qn * 0.01))) |
| |
| |
| n_factor = min(1.2, max(0.8, 1 + 0.05 * math.log(self.n / 5))) |
| |
| |
| 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 |
| """ |
| |
| ms = self.qn if sigma_0 <= self.emt / 2.05 else self.qn - self.emt + 2.05 * sigma_0 |
| |
| |
| qc = ms + self.surpoids_max |
| |
| |
| delta = (qc - (self.qn - self.emt + 2.05 * sigma_0)) / sigma_0 |
| |
| |
| delta_sqrt_n = delta * math.sqrt(self.n) |
| |
| |
| |
| if delta_sqrt_n <= 0.17: |
| return 1000 |
| elif delta_sqrt_n <= 0.5: |
| return 200 / (delta_sqrt_n**2) |
| elif delta_sqrt_n <= 3.38: |
| |
| 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 |
|
|
| 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) |
| |
| |
| recommended_n = int(max(10, current_n * math.sqrt(ratio))) |
| recommended_freq = min(4, max(1, current_freq * math.sqrt(ratio))) |
| |
| |
| recommended_n = ((recommended_n + 4) // 5) * 5 |
| recommended_freq = math.ceil(recommended_freq * 2) / 2 |
| |
| 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 |
| """ |
| |
| improved_sigma = max(sigma_0 * 0.7, self.emt / 3.5) |
| |
| |
| sim = MetrologicalCalculations(self.qn, self.emt, surpoids, n, freq) |
| |
| |
| 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 |
| recommended_freq = self.frequence |
| |
| if not is_valid: |
| pom_pol_ratio = pom / pol |
| |
| |
| 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") |
| |
| |
| recommended_n = max(5, int(self.n * (pom_pol_ratio ** 0.7))) |
| |
| |
| recommended_freq = max(1, self.frequence * pom_pol_ratio ** 0.5) |
| |
| |
| recommended_n = ((recommended_n + 4) // 5) * 5 |
| recommended_freq = round(recommended_freq * 2) / 2 |
| |
| 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") |
| |
| |
| 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) |
| |
| |
| cp = emt / (6 * std) |
| cpk = min((qn + emt - mean) / (3 * std), (mean - (qn - emt)) / (3 * std)) |
| |
| |
| 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 |
| """ |
| |
| if sigma_0 <= self.emt / 2.05: |
| ms = self.qn |
| else: |
| ms = self.qn - self.emt + 2.05 * sigma_0 |
| |
| |
| if with_e_mark: |
| |
| |
| lot_size_factor = 3.09 |
| super_defective_threshold = self.qn - 2 * self.emt + lot_size_factor * sigma_0 |
| |
| |
| 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() |
| |
| |
| fig.add_trace(go.Histogram( |
| x=data, |
| nbinsx=30, |
| marker_color='#4B8BBE', |
| name="Fréquence" |
| )) |
| |
| |
| if len(data) > 1: |
| 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é" |
| )) |
|
|
| |
| if qn is not None and emt is not None: |
| |
| 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') |
| )) |
| |
| |
| 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') |
| )) |
| |
| |
| 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') |
| )) |
|
|
| |
| 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 |
| """ |
| |
| fig = go.Figure() |
| |
| |
| 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'} |
| ] |
| |
| |
| 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 |
| } |
| } |
| )) |
| |
| |
| 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 |
| """ |
| |
| 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: |
| |
| data = data.reset_index() |
| x_values = data.index |
| x_title = "Index de mesure" |
| |
| |
| mean_value = data['Poids Brut (g)'].mean() |
| std_value = data['Poids Brut (g)'].std() |
| |
| ucl = mean_value + 3 * std_value |
| lcl = mean_value - 3 * std_value |
| |
| |
| fig = go.Figure() |
| |
| |
| fig.add_trace(go.Scatter( |
| x=x_values, |
| y=data['Poids Brut (g)'], |
| mode='lines+markers', |
| name='Poids brut', |
| line=dict(color='blue') |
| )) |
| |
| |
| 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') |
| )) |
| |
| |
| 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') |
| )) |
| |
| |
| 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 |
| """ |
| |
| ratio = pom / pol |
| risk_level = min(100, max(0, (ratio - 1) * 50)) |
| |
| |
| 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"}} |
| )) |
| |
| |
| 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é<br>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 |
| """ |
| |
| data_reset = data.reset_index() if 'index' not in data.columns else data.copy() |
| |
| |
| model = sm.OLS(data_reset['Poids Brut (g)'], sm.add_constant(data_reset['index'])).fit() |
| |
| |
| 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: |
| |
| model = ols('`Poids Brut (g)` ~ C(Lot)', data=data).fit() |
| anova_table = sm.stats.anova_lm(model, typ=2) |
| |
| |
| 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) |
| |
| if qn < 5: |
| |
| return 0.0 |
| elif qn <= 50: |
| return qn * 0.09 |
| elif qn <= 100: |
| return 4.5 |
| elif qn <= 200: |
| return qn * 0.045 |
| elif qn <= 300: |
| return 9.0 |
| elif qn <= 500: |
| return qn * 0.03 |
| elif qn <= 1000: |
| return 15.0 |
| elif qn <= 10000: |
| return qn * 0.015 |
| elif qn <= 15000: |
| return 150.0 |
| else: |
| return qn * 0.01 |
|
|
| |
| 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 |
| """) |
|
|
| |
| 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) |
| """) |
| |
| |
| 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" |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| .stAlert {border-radius: 10px;} |
| .stButton>button {border-radius: 5px;} |
| .stTextInput>div>div>input {border-radius: 5px;} |
| .reportview-container .main .block-container {padding-top: 2rem;} |
| .st-emotion-cache-16idsys p {margin-bottom: 0.5rem;} |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| 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*") |
|
|
| |
| show_help() |
|
|
| |
| tab1, tab2, tab3 = st.tabs(["📊 Contrôle et Analyse", "📝 Documentation", "ℹ️ À propos"]) |
| |
| with tab1: |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.subheader("📝 Paramètres du contrôle") |
| |
| with st.form("parametres"): |
| |
| qn = st.number_input( |
| "Quantité Nominale (g):", |
| value=500.0, |
| min_value=5.0, |
| help="Poids net indiqué sur l'emballage" |
| ) |
| |
| |
| default_emt = float(lookup_emt(qn)) |
| 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" |
| ) |
| |
| |
| 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" |
| ) |
| |
| |
| 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) |
|
|
| |
| if uploaded_file and submitted: |
| try: |
| |
| control = MetrologicalCalculations(qn, emt, surpoids_max, n, frequence) |
| |
| |
| df = pd.read_excel(uploaded_file, sheet_name="Pesées") |
| |
| |
| 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.") |
|
|
| |
| df = df.dropna(subset=["Poids Brut (g)"]) |
| |
| |
| st.subheader("📊 Données importées") |
| st.dataframe(df) |
|
|
| |
| tare_poids = df["Tare (g)"].dropna() |
| poids_bruts = df["Poids Brut (g)"].dropna() |
| |
| |
| if 'Poids Net (g)' not in df.columns: |
| |
| if len(tare_poids) == len(poids_bruts): |
| poids_nets = poids_bruts - tare_poids |
| else: |
| |
| 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() |
| |
| |
| if len(poids_nets) < 5: |
| st.warning("⚠️ Il est recommandé d'avoir au moins 5 mesures pour une analyse fiable.") |
| |
| |
| st.markdown("---") |
| |
| |
| st.subheader("📊 Distribution et statistiques de base") |
| col_stats, col_dist = st.columns([1, 2]) |
| |
| with col_stats: |
| |
| 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) |
| |
| |
| if len(poids_nets) >= 3: |
| 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: |
| |
| fig_poids = control.plot_distribution(poids_nets, "Distribution des Poids Nets", qn, emt) |
| st.plotly_chart(fig_poids, use_container_width=True) |
| |
| |
| st.markdown("---") |
| st.subheader("📈 Capabilité du processus") |
| |
| col_cap, col_gauge = st.columns([1, 1]) |
| |
| with col_cap: |
| |
| capability = control.calculate_capability_indices(poids_nets, qn, emt) |
| |
| |
| defective_stats = control.calculate_defective_count(poids_nets, qn, emt) |
| |
| |
| sigma_0 = capability['std'] |
| sampling_plan_evaluation = control.evaluate_sampling_plan(sigma_0) |
| |
| |
| target_weight = control.determine_target_weight(sigma_0, has_e_mark) |
| |
| |
| 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) |
| |
| |
| st.subheader("🔎 Interprétation des résultats") |
| |
| |
| 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).") |
| |
| |
| 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%.") |
| |
| |
| 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.") |
| |
| |
| if sampling_plan_evaluation['is_valid']: |
| st.success("✅ **Plan d'échantillonnage**: Validé - La fréquence et la taille d'échantillon sont suffisantes.") |
| |
| |
| with st.expander("ℹ️ Détails du plan d'échantillonnage"): |
| st.markdown(sampling_plan_evaluation['explanation']) |
| |
| |
| 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) |
| |
| |
| with st.expander("📊 Comprendre pourquoi le plan est insuffisant", expanded=True): |
| st.markdown(sampling_plan_evaluation['explanation']) |
| |
| |
| 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"} |
| ) |
| |
| 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) |
| |
| |
| 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. |
| """) |
| |
| |
| 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) |
| |
| |
| 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 |
| """) |
| |
| |
| 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) |
| |
| |
| surp_percent = st.slider("Surpoids simulé (% de QN):", |
| min_value=0.0, |
| max_value=1.0, |
| value=0.17, |
| step=0.01, |
| format="%.2f%%") |
| |
| |
| sim_surpoids = qn * surp_percent / 100.0 |
| |
| |
| sim_results = control.simulate_improved_plan(sigma_0, sim_n, sim_freq, sim_surpoids) |
| |
| |
| result_color = "green" if sim_results['is_valid'] else "red" |
| st.markdown(f"<h4 style='color:{result_color}'>Résultat de la simulation</h4>", 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) |
| |
| |
| 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: |
| |
| fig_capability = control.plot_capability_indices(capability['cp'], capability['cpk'], "Indices de Capabilité") |
| st.plotly_chart(fig_capability, use_container_width=True) |
| |
| |
| fig_boxplot = control.plot_boxplot(poids_nets, "Dispersion des Poids Nets") |
| st.plotly_chart(fig_boxplot, use_container_width=True) |
| |
| |
| st.markdown("---") |
| st.subheader("📈 Analyse temporelle et tendances") |
| |
| |
| 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: |
| |
| 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) |
| |
| |
| 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.") |
| |
| |
| 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})") |
| |
| |
| fig_lot = px.box(df, x='Lot', y='Poids Brut (g)', title="Comparaison des lots") |
| st.plotly_chart(fig_lot, use_container_width=True) |
| |
| |
| st.markdown("---") |
| st.subheader("🔍 Conclusions et recommandations") |
| |
| |
| 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.") |
| |
| |
| recommendations = [] |
| |
| |
| if capability['mean'] < qn: |
| recommendations.append(f"• Ajuster la cible de remplissage à au moins {target_weight:.2f}g (actuellement: {capability['mean']:.2f}g)") |
| |
| |
| if capability['cp'] < 1.33: |
| recommendations.append("• Réduire la variabilité du processus en améliorant la précision de la machine d'emplissage") |
| |
| |
| if not sampling_plan_evaluation['is_valid']: |
| for rec in sampling_plan_evaluation['recommendations']: |
| if rec.startswith("-"): |
| recommendations.append(f"• {rec[2:]}") |
| |
| |
| 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") |
| |
| |
| st.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" |
| |
| |
| 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: |
| |
| st.subheader("📚 Documentation") |
| |
| |
| show_regulatory_requirements() |
| |
| |
| show_detailed_documentation() |
| |
| with tab3: |
| |
| 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() |
|
|