| 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, Any, List |
| 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 |
|
|
| |
| class ValidationError(Exception): |
| pass |
|
|
| |
| def create_excel_template() -> bytes: |
| 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é"], |
| [" - Vérifier la cohérence des unités (grammes)"], |
| [" - Pour les produits avec signe 'e', respecter les exigences additionnelles"], |
| ] |
|
|
| workbook.close() |
| output.seek(0) |
| return output.getvalue() |
|
|
| |
| def get_emt(qn: float) -> float: |
| """ |
| Détermine l'erreur maximale tolérée selon l'annexe 5 du guide DGCCRF. |
| |
| Args: |
| qn: Quantité nominale en grammes ou millilitres |
| |
| Returns: |
| EMT en grammes ou millilitres |
| """ |
| if qn >= 5 and qn <= 50: |
| return qn * 0.09 |
| elif qn > 50 and qn <= 100: |
| return 4.5 |
| elif qn > 100 and qn <= 200: |
| return qn * 0.045 |
| elif qn > 200 and qn <= 300: |
| return 9 |
| elif qn > 300 and qn <= 500: |
| return qn * 0.03 |
| elif qn > 500 and qn <= 1000: |
| return 15 |
| elif qn > 1000 and qn <= 10000: |
| return qn * 0.015 |
| elif qn > 10000 and qn <= 15000: |
| return 150 |
| elif qn > 15000: |
| return qn * 0.01 |
| else: |
| raise ValueError(f"Quantité nominale {qn} hors plage valide (doit être ≥ 5).") |
|
|
| |
| class MetrologicalCalculations: |
| def __init__(self, qn, emt=None, surpoids_max=0, n=5, frequence=1.0, use_e_sign=False): |
| """ |
| Initialise les calculs métrologiques. |
| |
| Args: |
| qn: Quantité nominale |
| emt: Erreur maximale tolérée (calculée automatiquement si None) |
| surpoids_max: Tolérance supérieure acceptée |
| n: Taille d'échantillon pour les contrôles |
| frequence: Fréquence d'échantillonnage (par heure) |
| use_e_sign: True si le produit porte le signe 'e' |
| """ |
| self.qn = qn |
| self.emt = emt if emt is not None else get_emt(qn) |
| self.surpoids_max = surpoids_max |
| self.n = n |
| self.frequence = frequence |
| self.use_e_sign = use_e_sign |
|
|
| def calculate_pol(self): |
| """ |
| Calcule la Période Opérationnelle Limite (POl) |
| selon la section VII.B.2.3 du guide DGCCRF. |
| """ |
| base_pol = 4 |
| 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.5, max(0.5, self.n / 5)) |
| pol_adjusted = base_pol * qn_factor * emt_factor * n_factor |
| return pol_adjusted * self.frequence |
|
|
| def calculate_pom(self, sigma_0): |
| """ |
| Calcule la Période Opérationnelle Moyenne (POM) |
| selon la section VII.B.2.3 du guide DGCCRF. |
| |
| Args: |
| sigma_0: Écart-type du processus |
| |
| Returns: |
| Valeur POM |
| """ |
| |
| if sigma_0 <= self.emt / 2.05: |
| ms = self.qn |
| else: |
| ms = 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 * np.sqrt(self.n) |
|
|
| |
| freq_factor = max(0.5, min(2.0, 1 / self.frequence)) |
|
|
| |
| |
| if delta_sqrt_n <= 0.5: |
| return 100 * freq_factor |
| elif delta_sqrt_n <= 1.0: |
| return 15 * freq_factor |
| elif delta_sqrt_n <= 1.5: |
| return 8 * freq_factor |
| elif delta_sqrt_n <= 2.0: |
| return 5 * freq_factor |
| elif delta_sqrt_n <= 2.5: |
| return 3 * freq_factor |
| elif delta_sqrt_n <= 3.0: |
| return 2 * freq_factor |
| else: |
| return 1 * freq_factor |
|
|
| def evaluate_sampling_plan(self, sigma_0): |
| """ |
| Évalue l'efficacité du plan d'échantillonnage. |
| |
| Args: |
| sigma_0: Écart-type du processus |
| |
| Returns: |
| Dictionnaire avec les résultats d'évaluation |
| """ |
| pol = self.calculate_pol() |
| pom = self.calculate_pom(sigma_0) |
| is_valid = pom <= pol |
| recommendations = [] |
|
|
| if not is_valid: |
| if pom > pol * 2: |
| recommendations.append("🚨 Plan d'échantillonnage très insuffisant") |
| recommendations.append(f"- Augmenter la fréquence à au moins {max(2, self.frequence * 2):.1f} contrôles/heure") |
| recommendations.append(f"- Augmenter la taille d'échantillon à {max(5, self.n + 2)} unités minimum") |
| else: |
| recommendations.append("⚠️ Plan d'échantillonnage légèrement insuffisant") |
| recommendations.append(f"- Envisager d'augmenter la fréquence à {max(1, self.frequence * 1.5):.1f} contrôles/heure") |
|
|
| return { |
| 'is_valid': is_valid, |
| 'pom': pom, |
| 'pol': pol, |
| 'recommendations': recommendations |
| } |
|
|
| def analyze_tare_variability(self, tare_values): |
| """ |
| Analyse la variabilité de la tare selon la section VI.A du guide DGCCRF. |
| |
| Args: |
| tare_values: Liste des valeurs de tare |
| |
| Returns: |
| Dictionnaire avec les résultats de l'analyse |
| """ |
| tare_mean = np.mean(tare_values) |
| tare_std = np.std(tare_values, ddof=1) |
|
|
| |
| |
| use_individual_tare = tare_std > (self.emt / 5) |
|
|
| return { |
| 'mean': tare_mean, |
| 'std': tare_std, |
| 'use_individual_tare': use_individual_tare, |
| 'threshold': self.emt / 5 |
| } |
|
|
| def calculate_threshold_centering(self, sigma_0): |
| """ |
| Calcule le seuil de centrage (ms) selon la section V du guide DGCCRF. |
| |
| Args: |
| sigma_0: Écart-type du processus |
| |
| Returns: |
| Seuil de centrage (ms) |
| """ |
| |
| if sigma_0 <= self.emt / 2.05: |
| return self.qn |
| else: |
| |
| return self.qn - self.emt + 2.05 * sigma_0 |
|
|
| def calculate_target_quantity(self, sigma_0, lot_size=None): |
| """ |
| Calcule la quantité cible (QC) selon la section VII.B.2.1 du guide DGCCRF. |
| |
| Args: |
| sigma_0: Écart-type du processus |
| lot_size: Taille du lot (pour critère des superdéfectueux) |
| |
| Returns: |
| Quantité cible (QC) |
| """ |
| ms = self.calculate_threshold_centering(sigma_0) |
|
|
| |
| if self.use_e_sign and lot_size: |
| |
| if lot_size <= 1000: |
| u_value = 3.09 |
| elif lot_size <= 10000: |
| u_value = 3.71 |
| else: |
| u_value = 4.26 |
|
|
| |
| ms_super = self.qn - 2 * self.emt + u_value * sigma_0 |
|
|
| |
| ms = max(ms, ms_super) |
|
|
| |
| k_factor = 0.5 |
|
|
| return ms + k_factor * sigma_0 |
|
|
| def calculate_capability_indices(self, data, qn=None, emt=None): |
| """ |
| Calcule les indices de capabilité du processus. |
| |
| Args: |
| data: Liste des poids nets |
| qn: Quantité nominale (utilise self.qn si None) |
| emt: Erreur maximale tolérée (utilise self.emt si None) |
| |
| Returns: |
| Dictionnaire avec les indices de capabilité |
| """ |
| if qn is None: |
| qn = self.qn |
| if emt is None: |
| emt = self.emt |
|
|
| mean = np.mean(data) |
| std = np.std(data, ddof=1) |
|
|
| |
| cp = emt / (3 * std) |
|
|
| |
| cpk_inf = (mean - (qn - emt)) / (3 * std) |
| cpk_sup = ((qn + emt) - mean) / (3 * std) |
| cpk = min(cpk_inf, cpk_sup) |
|
|
| |
| process_centered = abs(mean - qn) <= emt/4 |
|
|
| return { |
| 'mean': mean, |
| 'std': std, |
| 'cp': cp, |
| 'cpk': cpk, |
| 'process_centered': process_centered, |
| 'capable': cp >= 1.33, |
| 'centered_capable': cpk >= 1.33 |
| } |
|
|
| def analyze_lot_conformity(self, net_weights): |
| """ |
| Analyse la conformité du lot selon les 3 critères du guide DGCCRF. |
| |
| Args: |
| net_weights: Liste des poids nets |
| |
| Returns: |
| Dictionnaire avec les résultats d'analyse |
| """ |
| |
| mean = np.mean(net_weights) |
| std = np.std(net_weights, ddof=1) |
|
|
| |
| mean_conformity = mean >= self.qn |
|
|
| |
| defectives = [w for w in net_weights if (self.qn - w) > self.emt] |
| defective_rate = len(defectives) / len(net_weights) * 100 |
| defective_conformity = defective_rate <= 2.0 |
|
|
| |
| super_defectives = [w for w in net_weights if (self.qn - w) > 2 * self.emt] |
| super_defective_conformity = len(super_defectives) == 0 if self.use_e_sign else True |
|
|
| |
| ms = self.calculate_threshold_centering(std) |
|
|
| |
| qc = self.calculate_target_quantity(std, len(net_weights)) |
|
|
| |
| stat, p_value = stats.shapiro(net_weights) |
| is_normal = p_value > 0.05 |
|
|
| return { |
| 'mean': mean, |
| 'std': std, |
| 'min': min(net_weights), |
| 'max': max(net_weights), |
| 'criteria': { |
| 'mean': mean_conformity, |
| 'defectives': defective_conformity, |
| 'super_defectives': super_defective_conformity |
| }, |
| 'defectives': { |
| 'count': len(defectives), |
| 'rate': defective_rate, |
| 'values': defectives if len(defectives) < 10 else defectives[:10] |
| }, |
| 'super_defectives': { |
| 'count': len(super_defectives), |
| 'values': super_defectives |
| }, |
| 'threshold_centering': ms, |
| 'target_quantity': qc, |
| 'overall_conformity': mean_conformity and defective_conformity and super_defective_conformity, |
| 'normality_test': { |
| 'is_normal': is_normal, |
| 'p_value': p_value |
| } |
| } |
|
|
| |
| def show_help(): |
| """Affiche l'aide contextuelle""" |
| st.sidebar.markdown(""" |
| ## 💡 Aide rapide |
| |
| ### Paramètres essentiels |
| - **Quantité Nominale**: Poids cible du produit |
| - **EMT**: Erreur maximale tolérée |
| - **Surpoids max**: Tolérance supérieure acceptée |
| - **Signe e**: Apposé pour garantir la conformité métrologique |
| |
| ### Procédure d'utilisation |
| 1. Remplir les paramètres |
| 2. Télécharger le template Excel |
| 3. Compléter les données |
| 4. Importer le fichier |
| 5. Analyser les résultats |
| |
| ### Interprétation des résultats |
| - **Conformité moyenne**: Moyenne ≥ QN |
| - **Conformité défectueux**: Défectueux ≤ 2% |
| - **Cp > 1.33**: Processus capable |
| - **Cpk > 1.33**: Processus centré et capable |
| - **POM ≤ POL**: Plan d'échantillonnage efficace |
| |
| ### Critères DGCCRF (Décret 78-166) |
| - **Critère de la moyenne**: Le contenu effectif des préemballages du lot ne doit pas être inférieur, en moyenne, à la quantité nominale. |
| - **Critère des défectueux**: Le taux de préemballages défectueux doit être inférieur à 2%. |
| - **Critère des superdéfectueux**: Aucun préemballage ne doit présenter un manquant supérieur à 2× l'EMT (pour produits avec signe "e"). |
| |
| ### Besoin d'aide ? |
| [Guide DGCCRF](https://www.economie.gouv.fr/dgccrf/controle-des-preemballages) |
| """) |
|
|
| |
| 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 et POL (Période Opérationnelle) |
| #### 3.1 Qu'est-ce que le POM et POL ? |
| Le POM (Période Opérationnelle Moyenne) et le POL (Période Opérationnelle Limite) sont des indicateurs qui permettent de savoir combien d'échantillons vous devez prélever en moyenne pour détecter un problème de poids dans un lot de produits. Pour que le contrôle soit efficace, il faut que POM ≤ POL. |
| |
| #### 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 |
| ``` |
| |
| ### 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 main(): |
| st.set_page_config( |
| page_title="Contrôle Métrologique - DGCCRF", |
| 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;} |
| .stProgress .st-bo {background-color: #4B8BBE;} |
| |
| .success-box { |
| background-color: #D4EDDA; |
| color: #155724; |
| padding: 15px; |
| border-radius: 5px; |
| border-left: 5px solid #28A745; |
| margin: 10px 0; |
| } |
| .warning-box { |
| background-color: #FFF3CD; |
| color: #856404; |
| padding: 15px; |
| border-radius: 5px; |
| border-left: 5px solid #FFC107; |
| margin: 10px 0; |
| } |
| .error-box { |
| background-color: #F8D7DA; |
| color: #721C24; |
| padding: 15px; |
| border-radius: 5px; |
| border-left: 5px solid #DC3545; |
| margin: 10px 0; |
| } |
| .info-box { |
| background-color: #D1ECF1; |
| color: #0C5460; |
| padding: 15px; |
| border-radius: 5px; |
| border-left: 5px solid #17A2B8; |
| margin: 10px 0; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| st.title("🎯 Contrôle Métrologique des Préemballages") |
| st.markdown("*Conforme au Guide DGCCRF et au Décret n°78-166 du 31 janvier 1978*") |
|
|
| |
| show_help() |
|
|
| |
| show_detailed_documentation() |
|
|
| |
| 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) |
|
|
| |
| auto_emt = st.checkbox("Calculer l'EMT automatiquement", value=True) |
|
|
| if auto_emt: |
| emt = get_emt(qn) |
| st.info(f"EMT calculée: {emt:.2f}g") |
| else: |
| emt = st.number_input("Erreur maximale tolérée (g):", value=15.0, min_value=0.0) |
|
|
| surpoids_max = st.number_input("Surpoids max toléré (g):", value=2.0, min_value=0.0) |
| n = st.number_input("Effectif d'échantillon:", value=5, min_value=1) |
| frequence = st.number_input("Fréquence d'échantillonnage (par heure):", value=1.0, min_value=0.1) |
| use_e_sign = st.checkbox("Produit avec signe 'e'") |
|
|
| 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") |
| uploaded_file = st.file_uploader("Téléchargez votre fichier Excel", type=["xlsx"]) |
|
|
| if submitted or 'control' in st.session_state: |
| |
| if submitted: |
| st.session_state.control = MetrologicalCalculations( |
| qn=qn, |
| emt=emt, |
| surpoids_max=surpoids_max, |
| n=n, |
| frequence=frequence, |
| use_e_sign=use_e_sign |
| ) |
|
|
| control = st.session_state.control |
|
|
| if uploaded_file: |
| try: |
| |
| df = pd.read_excel(uploaded_file) |
|
|
| |
| required_columns = ["Tare (g)", "Poids Brut (g)"] |
| if not all(col in df.columns for col in required_columns): |
| st.error("❌ Format de fichier incorrect. Utilisez le template fourni.") |
| else: |
| |
| df_valid = df.dropna(subset=required_columns) |
|
|
| if len(df_valid) < 3: |
| st.warning("⚠️ Pas assez de mesures valides. Au moins 3 mesures sont nécessaires pour l'analyse.") |
| else: |
| st.success(f"✅ {len(df_valid)} mesures valides trouvées dans le fichier.") |
|
|
| |
| df_valid['Poids Net (g)'] = df_valid['Poids Brut (g)'] - df_valid['Tare (g)'] |
|
|
| |
| results = control.analyze_lot_conformity(df_valid['Poids Net (g)']) |
|
|
| |
| st.subheader("📊 Résultats de l'analyse") |
|
|
| |
| conformity = results['criteria'] |
|
|
| col_sum1, col_sum2, col_sum3 = st.columns(3) |
|
|
| with col_sum1: |
| if conformity['mean']: |
| st.markdown('<div class="success-box">✅ Critère de la moyenne: CONFORME</div>', unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="error-box">❌ Critère de la moyenne: NON CONFORME</div>', unsafe_allow_html=True) |
|
|
| with col_sum2: |
| if conformity['defectives']: |
| st.markdown('<div class="success-box">✅ Critère des défectueux: CONFORME</div>', unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="error-box">❌ Critère des défectueux: NON CONFORME</div>', unsafe_allow_html=True) |
|
|
| with col_sum3: |
| if control.use_e_sign: |
| if conformity['super_defectives']: |
| st.markdown('<div class="success-box">✅ Critère des superdéfectueux: CONFORME</div>', unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="error-box">❌ Critère des superdéfectueux: NON CONFORME</div>', unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="info-box">ℹ️ Critère des superdéfectueux: Non applicable (pas de signe "e")</div>', unsafe_allow_html=True) |
|
|
| |
| st.subheader("📈 Statistiques générales") |
|
|
| col_stat1, col_stat2, col_stat3, col_stat4 = st.columns(4) |
|
|
| with col_stat1: |
| st.metric("Moyenne", f"{results['mean']:.2f}g", f"{results['mean'] - control.qn:.2f}g") |
|
|
| with col_stat2: |
| st.metric("Écart-type", f"{results['std']:.2f}g") |
|
|
| with col_stat3: |
| st.metric("Min", f"{results['min']:.2f}g", f"{results['min'] - control.qn:.2f}g") |
|
|
| with col_stat4: |
| st.metric("Max", f"{results['max']:.2f}g", f"{results['max'] - control.qn:.2f}g") |
|
|
| |
| if 'tare_analysis' in results: |
| st.subheader("🧮 Analyse de la tare") |
|
|
| tare_results = results['tare_analysis'] |
|
|
| col_tare1, col_tare2 = st.columns(2) |
|
|
| with col_tare1: |
| st.metric("Moyenne de la tare", f"{tare_results['mean']:.2f}g") |
| st.metric("Écart-type de la tare", f"{tare_results['std']:.2f}g") |
|
|
| with col_tare2: |
| st.metric("Seuil EMT/5", f"{tare_results['threshold']:.2f}g") |
|
|
| if tare_results['use_individual_tare']: |
| st.warning("⚠️ Contrôle avec tare individuelle requis") |
| st.write("L'écart-type de la tare est supérieur au seuil EMT/5.") |
| else: |
| st.success("✅ Contrôle avec tare moyenne possible") |
| st.write("L'écart-type de la tare est inférieur au seuil EMT/5.") |
|
|
| |
| st.subheader("📊 Test de normalité") |
|
|
| normality = results['normality_test'] |
|
|
| if normality['is_normal']: |
| st.success(f"✅ Distribution normale (p-value = {normality['p_value']:.4f})") |
| else: |
| st.warning(f"⚠️ Distribution non normale (p-value = {normality['p_value']:.4f})") |
| st.write("La non-normalité peut affecter la fiabilité des calculs statistiques.") |
|
|
| |
| st.subheader("⚙️ Capabilité du processus") |
|
|
| capability = results['capability'] |
|
|
| col_cap1, col_cap2 = st.columns(2) |
|
|
| with col_cap1: |
| if capability['cp'] >= 1.33: |
| st.success(f"✅ Cp = {capability['cp']:.2f} ≥ 1.33 : Processus CAPABLE") |
| elif capability['cp'] >= 1.0: |
| st.warning(f"⚠️ Cp = {capability['cp']:.2f} : Processus MARGINALEMENT CAPABLE") |
| else: |
| st.error(f"❌ Cp = {capability['cp']:.2f} < 1.0 : Processus NON CAPABLE") |
|
|
| with col_cap2: |
| if capability['cpk'] >= 1.33: |
| st.success(f"✅ Cpk = {capability['cpk']:.2f} ≥ 1.33 : Processus BIEN CENTRÉ") |
| elif capability['cpk'] >= 1.0: |
| st.warning(f"⚠️ Cpk = {capability['cpk']:.2f} : Processus MARGINALEMENT CENTRÉ") |
| else: |
| st.error(f"❌ Cpk = {capability['cpk']:.2f} < 1.0 : Processus MAL CENTRÉ") |
|
|
| |
| if 'graphics' in results and 'capability' in results['graphics']: |
| st.plotly_chart(results['graphics']['capability'], use_container_width=True) |
|
|
| |
| st.subheader("🔍 Évaluation du plan d'échantillonnage") |
|
|
| sampling = results['sampling_plan'] |
|
|
| col_samp1, col_samp2 = st.columns(2) |
|
|
| with col_samp1: |
| st.metric("POM (Période Opérationnelle Moyenne)", f"{sampling['pom']:.2f}") |
|
|
| with col_samp2: |
| st.metric("POL (Période Opérationnelle Limite)", f"{sampling['pol']:.2f}") |
|
|
| if sampling['is_valid']: |
| st.success("✅ Plan d'échantillonnage VALIDÉ (POM ≤ POL)") |
| st.write("Le plan d'échantillonnage actuel est suffisant pour détecter les déréglages du processus.") |
| else: |
| st.error("❌ Plan d'échantillonnage INSUFFISANT (POM > POL)") |
| for rec in sampling['recommendations']: |
| st.write(rec) |
|
|
| |
| st.subheader("🎯 Seuil de centrage et quantité cible") |
|
|
| col_target1, col_target2 = st.columns(2) |
|
|
| with col_target1: |
| st.metric("Ms (Seuil de centrage)", f"{results['threshold_centering']:.2f}g", f"{results['threshold_centering'] - control.qn:.2f}g") |
|
|
| with col_target2: |
| st.metric("QC (Quantité cible)", f"{results['target_quantity']:.2f}g", f"{results['target_quantity'] - control.qn:.2f}g") |
|
|
| |
| st.subheader("📉 Analyse de tendance") |
|
|
| regression = results['regression'] |
|
|
| if regression['significant']: |
| if regression['slope'] > 0: |
| st.warning(f"⚠️ {regression['description']}") |
| else: |
| st.error(f"❌ {regression['description']}") |
| else: |
| st.success(f"✅ {regression['description']}") |
|
|
| st.write(f"R² = {regression['r_squared']:.4f}, p-value = {regression['p_value']:.4f}") |
|
|
| |
| if 'graphics' in results: |
| st.subheader("📊 Graphiques d'analyse") |
|
|
| tab1, tab2, tab3, tab4 = st.tabs(["Distribution", "Évolution", "Box Plot", "Cartes de contrôle"]) |
|
|
| with tab1: |
| st.plotly_chart(results['graphics']['distribution'], use_container_width=True) |
|
|
| with tab2: |
| st.plotly_chart(results['graphics']['scatter'], use_container_width=True) |
|
|
| with tab3: |
| st.plotly_chart(results['graphics']['boxplot'], use_container_width=True) |
|
|
| with tab4: |
| st.plotly_chart(results['graphics']['control_chart_mean'], use_container_width=True) |
| st.plotly_chart(results['graphics']['control_chart_std'], use_container_width=True) |
|
|
| |
| st.subheader("📝 Recommandations") |
|
|
| recommendations = [] |
|
|
| |
| if not conformity['mean']: |
| recommendations.append("- Augmenter la quantité cible de remplissage pour garantir que la moyenne soit ≥ QN.") |
|
|
| if not conformity['defectives']: |
| recommendations.append("- Réduire la variabilité du processus pour diminuer le taux de défectueux.") |
| recommendations.append(f"- Augmenter la quantité cible à au moins {results['threshold_centering']:.2f}g.") |
|
|
| |
| if capability['cp'] < 1.33: |
| recommendations.append("- Améliorer la capabilité du processus en réduisant la variabilité.") |
|
|
| if capability['cpk'] < 1.33: |
| if capability['mean'] > control.qn: |
| if (capability['mean'] - control.qn) > control.emt: |
| recommendations.append("- Ajuster le centrage du processus à la baisse (surdosage excessif).") |
| else: |
| recommendations.append("- Ajuster le centrage du processus à la hausse (sous-dosage).") |
|
|
| |
| if regression['significant'] and regression['slope'] < 0: |
| recommendations.append("- Investiguer la cause de la tendance à la baisse des poids.") |
|
|
| |
| if not sampling['is_valid']: |
| recommendations.extend(sampling['recommendations']) |
|
|
| if len(recommendations) > 0: |
| for rec in recommendations: |
| st.write(rec) |
| else: |
| st.success("✅ Aucune recommandation particulière. Le processus est conforme et bien maîtrisé.") |
|
|
| 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()) |
|
|
| if __name__ == "__main__": |
| main() |
|
|