POMPOL / app.py
MMOON's picture
Update app.py
c3d02e8 verified
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é<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
"""
# 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("""
<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)
# 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"<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)
# 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()