| import streamlit as st |
| import pandas as pd |
| import re |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
| import io |
| import requests |
| from io import StringIO |
| import plotly.express as px |
| import plotly.graph_objects as go |
| from collections import Counter |
| import base64 |
|
|
| |
| st.set_page_config( |
| page_title="Analyse des Non-Conformités IFS", |
| page_icon="📊", |
| layout="wide" |
| ) |
|
|
| |
| def extract_info_from_description(desc): |
| if not isinstance(desc, str): |
| return {} |
| |
| info = {} |
| |
| type_match = re.search(r'Type:\s*(.+?)(?:\n|$)', desc) |
| if type_match: |
| info['Type'] = type_match.group(1).strip() |
| |
| |
| category_match = re.search(r'Category:\s*(.+?)(?:\n|$)', desc) |
| if category_match: |
| info['Category'] = category_match.group(1).strip() |
| |
| |
| lock_date_match = re.search(r'Lock date:\s*(.+?)(?:\n|$)', desc) |
| if lock_date_match: |
| info['Lock_date'] = lock_date_match.group(1).strip() |
| |
| |
| en_desc_match = re.search(r'Lock Description \(en\):\s*(.+?)(?:\nLock Description \(other|$)', desc, re.DOTALL) |
| if en_desc_match: |
| info['Description_en'] = en_desc_match.group(1).strip() |
| |
| other_desc_match = re.search(r'Lock Description \(other language\):\s*(.+?)(?:\n[A-Za-z]+:|$)', desc, re.DOTALL) |
| if other_desc_match: |
| info['Description_fr'] = other_desc_match.group(1).strip() |
| |
| return info |
|
|
| |
| |
| def extract_requirement_numbers(text): |
| if not isinstance(text, str): |
| return [] |
| |
| |
| |
| pattern1 = r'requirement\s+(\d+\.\d+\.\d+(?:\.\d+)?)' |
| |
| pattern2 = r'[-•:]\s*(\d+\.\d+\.\d+(?:\.\d+)?)' |
| |
| pattern3 = r'\b(\d+\.\d+\.\d+(?:\.\d+)?)\b' |
| |
| |
| matches = [] |
| matches.extend(re.findall(pattern1, text, re.IGNORECASE)) |
| matches.extend(re.findall(pattern2, text)) |
| matches.extend(re.findall(pattern3, text)) |
| |
| |
| matches = list(set(matches)) |
| |
| |
| filtered_matches = [] |
| for match in matches: |
| |
| parts = match.split('.') |
| if len(parts) == 3: |
| |
| try: |
| day = int(parts[0]) |
| month = int(parts[1]) |
| if 1 <= day <= 31 and 1 <= month <= 12 and len(parts[2]) <= 4: |
| |
| continue |
| except ValueError: |
| pass |
| filtered_matches.append(match) |
| |
| return filtered_matches if filtered_matches else [] |
|
|
| |
| @st.cache_data |
| def load_nonconformity_data(file): |
| try: |
| df = pd.read_csv(file, encoding='utf-8') |
| except: |
| try: |
| df = pd.read_csv(file, encoding='latin1') |
| except: |
| df = pd.read_csv(file, encoding='ISO-8859-1') |
| |
| |
| if len(df.columns) >= 2: |
| |
| df = df.rename(columns={df.columns[0]: 'COID', df.columns[1]: 'Description'}) |
| |
| |
| extracted_data = [] |
| for idx, row in df.iterrows(): |
| coid = row['COID'] |
| desc = row['Description'] if 'Description' in df.columns else None |
| |
| if desc is not None: |
| info = extract_info_from_description(desc) |
| info['COID'] = coid |
| |
| |
| if 'Description_en' in info: |
| info['requirement_numbers_en'] = extract_requirement_numbers(info['Description_en']) |
| if 'Description_fr' in info: |
| info['requirement_numbers_fr'] = extract_requirement_numbers(info['Description_fr']) |
| |
| |
| all_reqs = set() |
| if 'requirement_numbers_en' in info: |
| all_reqs.update(info['requirement_numbers_en']) |
| if 'requirement_numbers_fr' in info: |
| all_reqs.update(info['requirement_numbers_fr']) |
| |
| info['requirement_numbers'] = list(all_reqs) |
| |
| extracted_data.append(info) |
| |
| |
| if extracted_data: |
| return pd.DataFrame(extracted_data) |
| else: |
| return df |
|
|
| |
| @st.cache_data |
| def load_ifs_recommendations(): |
| url = "https://raw.githubusercontent.com/M00N69/Action-plan/main/Guide%20Checklist_IFS%20Food%20V%208%20-%20CHECKLIST.csv" |
| |
| try: |
| response = requests.get(url) |
| response.raise_for_status() |
| |
| |
| csv_data = StringIO(response.text) |
| |
| |
| df = pd.read_csv(csv_data, sep=',') |
| |
| |
| df['requirement_id'] = df['NUM_REQ'].str.replace('*', '').str.strip() |
| |
| |
| |
| lookup_dict = {} |
| |
| for idx, row in df.iterrows(): |
| req_id = row['requirement_id'] |
| if pd.notna(req_id) and isinstance(req_id, str): |
| lookup_dict[req_id] = row.to_dict() |
| |
| |
| simple_id_match = re.search(r'(\d+\.\d+\.\d+(?:\.\d+)?)', req_id) |
| if simple_id_match: |
| simple_id = simple_id_match.group(1) |
| if simple_id != req_id: |
| lookup_dict[simple_id] = row.to_dict() |
| |
| df.lookup_dict = lookup_dict |
| return df |
| except Exception as e: |
| st.error(f"Erreur lors du chargement des recommandations IFS: {e}") |
| return pd.DataFrame() |
|
|
| |
| def find_matching_recommendations(req_numbers, recommendations_df): |
| if not hasattr(recommendations_df, 'lookup_dict'): |
| |
| matching_recs = recommendations_df[recommendations_df['requirement_id'].isin(req_numbers)] |
| return matching_recs |
| |
| |
| matching_rows = [] |
| |
| for req in req_numbers: |
| |
| if req in recommendations_df.lookup_dict: |
| matching_rows.append(recommendations_df.lookup_dict[req]) |
| else: |
| |
| req_parts = req.split('.') |
| if len(req_parts) >= 3: |
| |
| padded_parts = [] |
| for part in req_parts: |
| try: |
| num = int(part) |
| padded_parts.append(f"{num:02d}") |
| except ValueError: |
| padded_parts.append(part) |
| |
| padded_req = '.'.join(padded_parts) |
| |
| if padded_req in recommendations_df.lookup_dict: |
| matching_rows.append(recommendations_df.lookup_dict[padded_req]) |
| continue |
| |
| |
| unpadded_parts = [] |
| for part in req_parts: |
| try: |
| num = int(part) |
| unpadded_parts.append(str(num)) |
| except ValueError: |
| unpadded_parts.append(part) |
| |
| unpadded_req = '.'.join(unpadded_parts) |
| |
| if unpadded_req in recommendations_df.lookup_dict: |
| matching_rows.append(recommendations_df.lookup_dict[unpadded_req]) |
| continue |
| |
| |
| for key in recommendations_df.lookup_dict: |
| if key.startswith(req) or req.startswith(key): |
| matching_rows.append(recommendations_df.lookup_dict[key]) |
| break |
| |
| if matching_rows: |
| return pd.DataFrame(matching_rows) |
| else: |
| return pd.DataFrame() |
|
|
| |
| st.title("Analyse des Non-Conformités d'Audit IFS") |
|
|
| |
| language = st.sidebar.selectbox( |
| "Langue / Language", |
| ["Français", "English"] |
| ) |
|
|
| |
| st.sidebar.info("Les données sont chargées directement depuis le dépôt GitHub" if language == "Français" else "Data is loaded directly from the GitHub repository") |
|
|
| @st.cache_data |
| def load_github_nonconformity_data(): |
| url = "https://raw.githubusercontent.com/M00N69/Action-plan/main/MAJEURES%20.csv" |
| try: |
| response = requests.get(url) |
| response.raise_for_status() |
| |
| |
| csv_data = StringIO(response.text) |
| |
| |
| return load_nonconformity_data(csv_data) |
| except Exception as e: |
| st.error(f"Erreur lors du chargement des données depuis GitHub: {e}") |
| return pd.DataFrame() |
|
|
| |
| data_source = st.sidebar.radio( |
| "Source des données" if language == "Français" else "Data source", |
| ["GitHub", "Fichier local" if language == "Français" else "Local file"] |
| ) |
|
|
| nonconformity_df = None |
| recommendations_df = None |
|
|
| if data_source == "GitHub": |
| |
| nonconformity_df = load_github_nonconformity_data() |
| recommendations_df = load_ifs_recommendations() |
| |
| with st.sidebar.expander("À propos des données" if language == "Français" else "About the data"): |
| st.write("Source: https://github.com/M00N69/Action-plan/blob/main/MAJEURES%20.csv") |
| else: |
| |
| uploaded_file = st.sidebar.file_uploader( |
| "Importez votre fichier CSV de non-conformités" if language == "Français" else "Upload your non-conformity CSV file", |
| type=["csv"] |
| ) |
| |
| if uploaded_file is not None: |
| |
| nonconformity_df = load_nonconformity_data(uploaded_file) |
| recommendations_df = load_ifs_recommendations() |
| else: |
| st.info( |
| "Veuillez importer un fichier CSV contenant des données de non-conformités IFS" |
| if language == "Français" |
| else "Please upload a CSV file containing IFS non-conformity data" |
| ) |
| st.stop() |
|
|
| |
| if nonconformity_df is not None and recommendations_df is not None: |
| |
| with st.expander("Aperçu des données (débogage)" if language == "Français" else "Data preview (debugging)"): |
| st.write("Colonnes du DataFrame:" if language == "Français" else "DataFrame columns:", nonconformity_df.columns.tolist()) |
| st.dataframe(nonconformity_df.head()) |
| |
| |
| has_requirements = 'requirement_numbers' in nonconformity_df.columns |
| |
| |
| st.subheader("Informations générales" if language == "Français" else "General Information") |
| |
| col1, col2, col3 = st.columns(3) |
| with col1: |
| st.metric( |
| "Nombre total de non-conformités" if language == "Français" else "Total number of non-conformities", |
| len(nonconformity_df) |
| ) |
| |
| with col2: |
| |
| all_requirements = [] |
| if has_requirements: |
| for req_list in nonconformity_df['requirement_numbers']: |
| if isinstance(req_list, list): |
| all_requirements.extend(req_list) |
| |
| st.metric( |
| "Nombre d'exigences différentes concernées" if language == "Français" else "Number of different requirements concerned", |
| len(set(all_requirements)) if all_requirements else 0 |
| ) |
| |
| with col3: |
| |
| if 'COID' in nonconformity_df.columns: |
| num_companies = nonconformity_df['COID'].nunique() |
| else: |
| num_companies = "N/A" |
| |
| st.metric( |
| "Nombre d'entreprises concernées" if language == "Français" else "Number of companies concerned", |
| num_companies |
| ) |
| |
| |
| st.subheader("Points de norme les plus fréquemment concernés" if language == "Français" else "Most frequently concerned standard points") |
| |
| if has_requirements and all_requirements: |
| |
| requirement_counts = Counter(all_requirements) |
| |
| |
| req_freq_df = pd.DataFrame({ |
| 'requirement': list(requirement_counts.keys()), |
| 'count': list(requirement_counts.values()) |
| }).sort_values('count', ascending=False) |
| |
| if not req_freq_df.empty: |
| |
| fig = px.bar( |
| req_freq_df.head(10), |
| x='requirement', |
| y='count', |
| title="Top 10 des points de norme" if language == "Français" else "Top 10 Standard Points", |
| labels={'requirement': 'Exigence' if language == 'Français' else 'Requirement', 'count': 'Fréquence' if language == 'Français' else 'Frequency'} |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
| |
| |
| st.subheader("Filtrer par point de norme" if language == "Français" else "Filter by standard point") |
| |
| |
| all_requirements_list = sorted(list(requirement_counts.keys())) |
| |
| selected_requirement = st.selectbox( |
| "Sélectionnez un point de norme" if language == "Français" else "Select a standard point", |
| all_requirements_list |
| ) |
| |
| if selected_requirement: |
| |
| filtered_rows = [] |
| for idx, row in nonconformity_df.iterrows(): |
| if 'requirement_numbers' in row and isinstance(row['requirement_numbers'], list) and selected_requirement in row['requirement_numbers']: |
| filtered_rows.append(row) |
| |
| filtered_df = pd.DataFrame(filtered_rows) |
| |
| if not filtered_df.empty: |
| st.write(f"{'Non-conformités concernant le point' if language == 'Français' else 'Non-conformities concerning point'} {selected_requirement}: **{len(filtered_df)} cas trouvés**") |
| |
| |
| comp_table = [] |
| desc_col_fr = 'Description_fr' |
| desc_col_en = 'Description_en' |
| |
| |
| for idx, row in filtered_df.iterrows(): |
| desc_fr = row.get(desc_col_fr, '') if pd.notna(row.get(desc_col_fr, '')) else '' |
| desc_en = row.get(desc_col_en, '') if pd.notna(row.get(desc_col_en, '')) else '' |
| |
| |
| description = desc_fr if language == "Français" and desc_fr else desc_en |
| if not description and desc_fr: |
| description = desc_fr |
| |
| |
| highlighted_text = description |
| if description: |
| |
| sentences = re.split(r'(?<=[.!?])\s+', description) |
| highlighted_desc = [] |
| |
| for sentence in sentences: |
| if selected_requirement in sentence: |
| highlighted_desc.append(f"<span style='background-color: #FFFF00'>{sentence}</span>") |
| else: |
| highlighted_desc.append(sentence) |
| |
| highlighted_text = " ".join(highlighted_desc) |
| else: |
| highlighted_text = "<em>Aucune description détaillée disponible</em>" |
| |
| comp_table.append({ |
| "COID": row['COID'], |
| "Type": row.get('Type', ''), |
| "Lock_date": row.get('Lock_date', ''), |
| "Description": highlighted_text |
| }) |
| |
| |
| st.subheader("Tableau comparatif des cas" if language == "Français" else "Comparative table of cases") |
| |
| |
| comp_df = pd.DataFrame(comp_table) |
| |
| if not comp_df.empty: |
| |
| for idx, row in comp_df.iterrows(): |
| with st.container(): |
| cols = st.columns([1, 1, 1, 5]) |
| with cols[0]: |
| st.write(f"**COID:**<br>{row['COID']}", unsafe_allow_html=True) |
| with cols[1]: |
| st.write(f"**Type:**<br>{row['Type']}", unsafe_allow_html=True) |
| with cols[2]: |
| st.write(f"**Date:**<br>{row['Lock_date']}", unsafe_allow_html=True) |
| with cols[3]: |
| st.write(f"**Description:**<br>{row['Description']}", unsafe_allow_html=True) |
| |
| st.markdown("---") |
| else: |
| st.warning( |
| "Aucune description détaillée n'a été trouvée pour ce point de norme. Le point a été détecté mais les phrases spécifiques ne sont pas disponibles." |
| if language == "Français" |
| else "No detailed description was found for this standard point. The point was detected but the specific sentences are not available." |
| ) |
| |
| |
| matching_recs = find_matching_recommendations([selected_requirement], recommendations_df) |
| |
| if not matching_recs.empty: |
| st.subheader("Recommandations IFS" if language == "Français" else "IFS Recommendations") |
| |
| for idx, rec in matching_recs.iterrows(): |
| |
| with st.expander(f"Exigence {rec['NUM_REQ']}", expanded=True): |
| st.markdown(f"**{'Description' if language == 'Français' else 'Description'}:**") |
| st.info(rec['IFS Requirements']) |
| |
| if pd.notna(rec['Good practice']): |
| st.markdown(f"**{'Bonnes pratiques' if language == 'Français' else 'Good practice'}:**") |
| st.success(rec['Good practice']) |
| |
| if pd.notna(rec['Example questions']): |
| st.markdown(f"**{('Questions exemple' if language == 'Français' else 'Example questions')}:**") |
| |
| |
| questions = rec['Example questions'].split('•') |
| for q in questions: |
| if q.strip(): |
| st.write(f"• {q.strip()}") |
| |
| if pd.notna(rec['Elements to check']): |
| st.markdown(f"**{'Éléments à vérifier' if language == 'Français' else 'Elements to check'}:**") |
| |
| |
| elements = rec['Elements to check'].split('•') |
| for e in elements: |
| if e.strip(): |
| st.write(f"• {e.strip()}") |
| |
| if pd.notna(rec['Example for non-conformities']): |
| st.markdown(f"**{'Exemples de non-conformités' if language == 'Français' else 'Example for non-conformities'}:**") |
| st.error(rec['Example for non-conformities']) |
| |
| else: |
| st.info("Aucune recommandation trouvée pour ce point de norme" if language == "Français" else "No recommendations found for this standard point") |
| else: |
| st.info("Aucune non-conformité trouvée pour ce point de norme" if language == "Français" else "No non-conformities found for this standard point") |
| else: |
| st.warning("Aucun point de norme n'a été trouvé dans les données" if language == "Français" else "No standard points were found in the data") |
| else: |
| st.warning("Aucun point de norme n'a été extrait des descriptions" if language == "Français" else "No standard points were extracted from the descriptions") |
|
|
| |
| st.subheader("Exporter les données" if language == "Français" else "Export data") |
| |
| if st.button("Télécharger l'analyse complète en CSV" if language == "Français" else "Download complete analysis as CSV"): |
| |
| export_df = nonconformity_df.copy() |
| |
| |
| if has_requirements: |
| export_df['requirement_numbers'] = export_df['requirement_numbers'].apply( |
| lambda x: ', '.join(x) if isinstance(x, list) else '') |
| |
| |
| csv = export_df.to_csv(index=False) |
| |
| |
| b64 = base64.b64encode(csv.encode()).decode() |
| href = f'<a href="data:file/csv;base64,{b64}" download="ifs_nonconformities_analysis.csv">Télécharger le CSV</a>' |
| st.markdown(href, unsafe_allow_html=True) |
|
|
| |
| st.sidebar.markdown("---") |
| st.sidebar.info( |
| "Cette application analyse les non-conformités majeures ou KO attribuées lors des audits IFS et les compare aux recommandations officielles." |
| if language == "Français" |
| else "This application analyzes major non-conformities or KO attributed during IFS audits and compares them to official recommendations." |
| ) |