Demande pour code python aille 10 million de fois plus vite
mamiemando Messages postés 33535 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 12 février 2025 - 12 févr. 2025 à 18:31
- Demande pour code python aille 10 million de fois plus vite
- Code activation windows 10 - Guide
- Difference million milliard - Accueil - Technologies
- Code ascii de a - Guide
- Windows 10 ne démarre plus - Guide
- Code puk bloqué - Guide
8 réponses
Modifié le 5 févr. 2025 à 22:39
Bonjour,
En réponse à #20, voici un début de code que je te propose et qui repart de #12.
Proposition de code
#!/usr/bin/env python3 import argparse import pandas as pd from pathlib import Path class DfRebase(pd.DataFrame): """ The :py:class:`DfRebase` class is the dataframe used to wrap a rebase Excel file. """ def __init__(self, filename: Path): """ Constructor. Args: filename (Path): Path of the input rebase Excel file. Example: >>> df_rebase = DfRebase("OBfoz18rz4r_REBASE.xlsx") """ df = pd.read_excel(filename, header=None) cols = df.head(1).dropna(axis=1).columns columns = df[cols].values[0] data = df[2:][cols + 1].dropna(axis=0) index = df[0][data.index] super().__init__( data=data.values, columns=columns, index=index ) class DfSource(pd.DataFrame): """ The :py:class:`DfSource` class is the dataFrame used to wrap a source Excel file. """ def __init__(self, filename: Path): """ Constructor. Args: filename (Path): Path of the input source Excel file. Example: >>> df_source = DfSource("OBfoAYcDp4r_Classeur1.xlsx") """ data = pd.read_excel(filename, header=None).dropna(axis=1) index = data[1].values super().__init__( data=data.values, index=index, ) def compute(self, df_rebase: DfRebase, row_start): print("DbRebase:") print(df_rebase) print(df_rebase.index) print("DbSource:") print(self) print(self.index) print("Index:") for index in self.index: row = df_rebase.loc[index].values print(f"{index:25s} {row}") print("Results") df_result = df_rebase.loc[self.index].sum(axis=1) print(df_result) return df_result def main(): parser = argparse.ArgumentParser() parser.add_argument( "--rebase-filename", required=False, type=Path, default="OBfoz18rz4r_REBASE.xlsx", help="Path to the input xlsx rebase file." ) parser.add_argument( "--source-filename", required=False, type=Path, default="OBfoAYcDp4r_Classeur1.xlsx", help="Path to the input xlsx source file." ) args = parser.parse_args() assert args.rebase_filename.exists(), args.rebase_filename df_rebase = DfRebase(args.rebase_filename) assert args.source_filename.exists(), args.source_filename df_source = DfSource(args.source_filename) df_results = df_source.compute(df_rebase, row_start=21) if __name__ == "__main__": main()
Explications
- On définit une classe de dataframe par type de fichier (donc source et rebase)
- On commence par remettre en forme la dataframe "rebase" dans DfRebase.__init__:
- on récupère les noms de colonnes à partir des cellules A1, D1, ...
- on supprime les lignes vides
- on indexe les lignes par les valeurs de la colonne A (qui sont répliquées dans les colonnes D, G, ....). Cette indexation sera exploitée ultérieurement pour faire la jointure avec les données d'une DfSource.
- les valeurs associés à un nom de colonne sont décalées d'une colonne ce qui oblige à décaler les index de colonnes
- On fait le même travail pour remettre en forme une dataframe "source" dans DfSource.__init__.
- on indexe les lignes par les valeurs de la colonne A (qui coïncident avec les index définis dans DfRebase)
- je n'ai pas l'impression que les valeurs aient d'utilité (dans l'exemple que tu donnes, toutes les valeurs d'une ligne sont identiques)
- On définit la méthode DfSource.compute qui permet de croiser les données. Pour être honnête je n'ai pas compris ton code entre les lignes 58 et 100 (trop compliqué à lire) mais c'est dans cette méthode qu'il faut le traduire en pandas. J'ai donc mis un code arbitraire :
- D'abord, je montre accéder aux différentes valeurs "intéressantes" dans chacune des deux dataframe.
- Ensuite, pour chaque identifiant de ligne (colonne A de DfSource) comment récupérer les valeurs correspondantes de DfRebase et comment les sommer.
- Si ce n'est pas ce que tu veux calculer, merci de clarifier, avec ces nouvelles notations, ce que tu souhaites calculer.
Résultat
python script.py --rebase-filename OBfoz18rz4r_REBASE.xlsx --source-filename OBfoAYcDp4r_Classeur1.xlsx
DbRebase: 2024-09-20 2024-09-19 2024-09-18 2024-09-17 2024-09-16 2024-09-15 2024-09-14 2024-09-13 0 2024 :R1 PIERRES 1.0 2.0 3.0 1.0 1.0 2.0 3.0 1.0 2024 :R2 PIERRES 1.0 2.0 3.0 2.0 1.0 2.0 3.0 2.0 2024 :R3 PIERRES 1.0 2.0 3.0 3.0 1.0 2.0 3.0 3.0 2024 :R4 PIERRES 1.0 2.0 3.0 4.0 1.0 2.0 3.0 4.0 2024 :R5 PIERRES 1.0 2.0 3.0 5.0 1.0 2.0 3.0 5.0 ... ... ... ... ... ... ... ... ... 2021 :GOMME 1.0 2.0 3.0 63.0 1.0 2.0 3.0 63.0 2021 :AGRAFES 1.0 2.0 3.0 64.0 1.0 2.0 3.0 64.0 2021 :POST IT 1.0 2.0 3.0 65.0 1.0 2.0 3.0 65.0 2021 :MARQUEUR 1.0 2.0 3.0 66.0 1.0 2.0 3.0 66.0 2021 :CORRECTEUR 1.0 2.0 3.0 67.0 1.0 2.0 3.0 67.0 [268 rows x 8 columns] Index(['2024 :R1 PIERRES', '2024 :R2 PIERRES', '2024 :R3 PIERRES', '2024 :R4 PIERRES', '2024 :R5 PIERRES', '2024 :R1 STYLO BLEU', '2024 :R2 STYLO BLEU', '2024 :R3 STYLO BLEU', '2024 :R4 STYLO BLEU', '2024 :R5 STYLO BLEU', ... '2021 :R3 SURLIGNEUR', '2021 :R4 SURLIGNEUR', '2021 :R5 SURLIGNEUR', '2021 :GOMME', '2021 :OTE AGRAFE', '2021 :GOMME', '2021 :AGRAFES', '2021 :POST IT', '2021 :MARQUEUR', '2021 :CORRECTEUR'], dtype='object', name=0, length=268) DbSource: 0 1 2 ... 17 18 19 2024 :R1 PIERRES 2024 :R1 PIERRES 2024 :R1 PIERRES 2024 :R1 PIERRES ... 2024 :R1 PIERRES 2024 :R1 PIERRES 2024 :R1 PIERRES 2024 :R1 STYLO BLEU 2024 :R1 STYLO BLEU 2024 :R1 STYLO BLEU 2024 :R1 STYLO BLEU ... 2024 :R1 STYLO BLEU 2024 :R1 STYLO BLEU 2024 :R1 STYLO BLEU 2024 :R1 CRAYONS 2024 :R1 CRAYONS 2024 :R1 CRAYONS 2024 :R1 CRAYONS ... 2024 :R1 CRAYONS 2024 :R1 CRAYONS 2024 :R1 CRAYONS 2024 :R1 STYLO VERT 2024 :R1 STYLO VERT 2024 :R1 STYLO VERT 2024 :R1 STYLO VERT ... 2024 :R1 STYLO VERT 2024 :R1 STYLO VERT 2024 :R1 STYLO VERT 2024 :R1 STYLO ROUGE 2024 :R1 STYLO ROUGE 2024 :R1 STYLO ROUGE 2024 :R1 STYLO ROUGE ... 2024 :R1 STYLO ROUGE 2024 :R1 STYLO ROUGE 2024 :R1 STYLO ROUGE 2024 :R1 STYLO NOIR 2024 :R1 STYLO NOIR 2024 :R1 STYLO NOIR 2024 :R1 STYLO NOIR ... 2024 :R1 STYLO NOIR 2024 :R1 STYLO NOIR 2024 :R1 STYLO NOIR 2024 :R1 STYLO FUSCHIA 2024 :R1 STYLO FUSCHIA 2024 :R1 STYLO FUSCHIA 2024 :R1 STYLO FUSCHIA ... 2024 :R1 STYLO FUSCHIA 2024 :R1 STYLO FUSCHIA 2024 :R1 STYLO FUSCHIA 2024 :R1 STYLO JAUNE 2024 :R1 STYLO JAUNE 2024 :R1 STYLO JAUNE 2024 :R1 STYLO JAUNE ... 2024 :R4 STYLO JAUNE 2024 :R4 STYLO JAUNE 2024 :R4 STYLO JAUNE 2024 :R1 STYLO ORANGE 2024 :R1 STYLO ORANGE 2024 :R2 STYLO ORANGE 2024 :R3 STYLO ORANGE ... 2024 :R3 STYLO ORANGE 2024 :R4 STYLO ORANGE 2024 :R5 STYLO ORANGE [9 rows x 20 columns] Index(['2024 :R1 PIERRES', '2024 :R1 STYLO BLEU', '2024 :R1 CRAYONS', '2024 :R1 STYLO VERT', '2024 :R1 STYLO ROUGE', '2024 :R1 STYLO NOIR', '2024 :R1 STYLO FUSCHIA', '2024 :R1 STYLO JAUNE', '2024 :R1 STYLO ORANGE'], dtype='object') Index: 2024 :R1 PIERRES [1. 2. 3. 1. 1. 2. 3. 1.] 2024 :R1 STYLO BLEU [1. 2. 3. 6. 1. 2. 3. 6.] 2024 :R1 CRAYONS [ 1. 2. 3. 11. 1. 2. 3. 11.] 2024 :R1 STYLO VERT [ 1. 2. 3. 16. 1. 2. 3. 16.] 2024 :R1 STYLO ROUGE [ 1. 2. 3. 21. 1. 2. 3. 21.] 2024 :R1 STYLO NOIR [ 1. 2. 3. 26. 1. 2. 3. 26.] 2024 :R1 STYLO FUSCHIA [ 1. 2. 3. 31. 1. 2. 3. 31.] 2024 :R1 STYLO JAUNE [ 1. 2. 3. 36. 1. 2. 3. 36.] 2024 :R1 STYLO ORANGE [ 1. 2. 3. 41. 1. 2. 3. 41.] Results 2024 :R1 PIERRES 14.0 2024 :R1 STYLO BLEU 24.0 2024 :R1 CRAYONS 34.0 2024 :R1 STYLO VERT 44.0 2024 :R1 STYLO ROUGE 54.0 2024 :R1 STYLO NOIR 64.0 2024 :R1 STYLO FUSCHIA 74.0 2024 :R1 STYLO JAUNE 84.0 2024 :R1 STYLO ORANGE 94.0 dtype: float64
Bonne chance
5 févr. 2025 à 12:21
une suggestion:
import multiprocessing import os import pandas as pd from openpyxl import load_workbook from openpyxl.styles import NamedStyle def m(x,mod): # ???? Définition des chemins dossier_source = "D:/PYTHON/VALEUR REMPLACER ZIP" fichier_rebase = "D:/PYTHON/REBASE.xlsx" # ???? Chargement des valeurs de REBASE.xlsx en RAM wb_rebase = load_workbook(fichier_rebase, data_only=True) ws_rebase = wb_rebase.active # ???? Extraction des dates des tableaux (Ligne 1 de REBASE.xlsx) avec formatage JJ/MM/AAAA dates_tableaux = { 1: ws_rebase.cell(row=1, column=1).value.strftime("%d/%m/%Y") if isinstance(ws_rebase.cell(row=1, column=1).value, (pd.Timestamp, str)) else ws_rebase.cell(row=1, column=1).value, 2: ws_rebase.cell(row=1, column=4).value.strftime("%d/%m/%Y") if isinstance(ws_rebase.cell(row=1, column=4).value, (pd.Timestamp, str)) else ws_rebase.cell(row=1, column=4).value, 3: ws_rebase.cell(row=1, column=7).value.strftime("%d/%m/%Y") if isinstance(ws_rebase.cell(row=1, column=7).value, (pd.Timestamp, str)) else ws_rebase.cell(row=1, column=7).value } # ???? Stockage des tableaux sous forme de dictionnaires pour un accès rapide tableaux = { 1: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=1, max_col=2, values_only=True) if row[0] is not None}, 2: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=4, max_col=5, values_only=True) if row[0] is not None}, 3: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=7, max_col=8, values_only=True) if row[0] is not None} } wb_rebase.close() # ???? Traitement des fichiers dans le dossier source for fichier in os.listdir(dossier_source): if fichier.endswith(".xlsx") and hash(fichier)%mod==x: chemin_fichier = os.path.join(dossier_source, fichier) # ???? Charger le fichier Excel une seule fois wb = load_workbook(chemin_fichier) ws = wb.active # ???? **Appliquer le format "Date courte" à toute la colonne A** date_style = NamedStyle(name="date_style") date_style.number_format = "DD/MM/YYYY" for row in range(1, ws.max_row + 1): cell = ws.cell(row=row, column=1) cell.style = date_style # Appliquer le style à la colonne A # ???? Identifier la dernière ligne contenant des données (entre 1 et 19) last_data_row = max( (i for i in range(1, 20) if any(ws.cell(row=i, column=j).value for j in range(2, ws.max_column + 1))), default=1 ) # ???? Récupérer toutes les cellules en RAM pour un traitement rapide data = [[ws.cell(row=row, column=col).value for col in range(2, ws.max_column + 1)] for row in range(1, last_data_row + 1)] ligne_depart = 21 # ???? Début d'écriture des valeurs for idx, mapping in tableaux.items(): # ???? Appliquer les remplacements **sans modifier les valeurs d'origine** data_remplacee = [[mapping.get(val, val) for val in row] for row in data] # ???? Trouver la prochaine ligne vide **sans écraser les données** while any(ws.cell(row=ligne_depart, column=j).value for j in range(2, ws.max_column + 1)): ligne_depart += 1 ligne_debut_bloc = ligne_depart # ???? **Écriture optimisée des nouvelles valeurs** for i, row in enumerate(data_remplacee): for j, val in enumerate(row, start=2): ws.cell(row=ligne_depart + i, column=j, value=val) # ???? Calcul de la somme somme_bloc = [sum(cell for cell in col if isinstance(cell, (int, float))) for col in zip(*data_remplacee)] # ???? Trouver une ligne vide **sans écraser de données** ligne_somme = ligne_depart + last_data_row while any(ws.cell(row=ligne_somme, column=j).value for j in range(2, ws.max_column + 1)): ligne_somme += 1 # ???? **Suppression correcte du bloc** ws.delete_rows(ligne_debut_bloc, last_data_row) # ???? **Écriture optimisée des sommes** for j, somme in enumerate(somme_bloc, start=2): ws.cell(row=ligne_debut_bloc, column=j, value=somme) # ✅ **Ajout de la date formatée en colonne A** date_tableau = dates_tableaux[idx] # Date associée au tableau cell_date = ws.cell(row=ligne_debut_bloc, column=1, value=date_tableau) cell_date.style = date_style # Appliquer le style "Date courte" # ???? Mise à jour de la ligne de départ pour le bloc suivant ligne_depart = ligne_debut_bloc + 1 # ???? **Sauvegarde du fichier après toutes les opérations** wb.save(chemin_fichier) print(f"✅ {fichier} mis à jour avec succès.") if __name__ == '__main__': nproc=10 prc=[] for i in range(nproc): p=multiprocessing.Process(target=m, args=(i,nproc)) p.start() prc.append(p) for pp in prc: pp.join() print("✔ Tous les fichiers ont été traités correctement.")
5 févr. 2025 à 14:52
Merci pour ton code, en l'exécutant j'ai eu ce message d'erreur:
PS C:\Users\dalin> py "D:/PYTHON/mon_script.py" >> Process Process-5: Traceback (most recent call last): File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 313, in _bootstrap self.run() ~~~~~~~~^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 108, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\PYTHON\mon_script.py", line 45, in m cell.style = date_style # Appliquer le style à la colonne A ^^^^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\styles\styleable.py", line 77, in __set__ instance.parent.parent.add_named_style(style) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\workbook\workbook.py", line 347, in add_named_style self._named_styles.append(style) ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\styles\named_styles.py", line 186, in append raise ValueError("""Style {0} exists already""".format(style.name)) ValueError: Style date_style exists already Process Process-8: Traceback (most recent call last): File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 313, in _bootstrap self.run() ~~~~~~~~^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 108, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\PYTHON\mon_script.py", line 45, in m cell.style = date_style # Appliquer le style à la colonne A ^^^^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\styles\styleable.py", line 77, in __set__ instance.parent.parent.add_named_style(style) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\workbook\workbook.py", line 347, in add_named_style self._named_styles.append(style) ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\styles\named_styles.py", line 186, in append raise ValueError("""Style {0} exists already""".format(style.name)) ValueError: Style date_style exists already Process Process-7: Traceback (most recent call last): File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 313, in _bootstrap self.run() ~~~~~~~~^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 108, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\PYTHON\mon_script.py", line 45, in m cell.style = date_style # Appliquer le style à la colonne A ^^^^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\styles\styleable.py", line 77, in __set__ instance.parent.parent.add_named_style(style) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\workbook\workbook.py", line 347, in add_named_style self._named_styles.append(style) ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^ File "C:\Users\dalin\AppData\Local\Programs\Python\Python313\Lib\site-packages\openpyxl\styles\named_styles.py", line 186, in append raise ValueError("""Style {0} exists already""".format(style.name)) ValueError: Style date_style exists already ✔ Tous les fichiers ont été traités correctement. PS C:\Users\dalin>
3 févr. 2025 à 16:50
D'accord merci, je vais refaire une autre discussion
3 févr. 2025 à 12:49
Bonjour,
J'ai tenté de remettre ton code en forme mais vu qu'il n'était pas indenté, j'espère qu'il est toujours tel que tu souhaitais le partager. Merci de corriger le message initial ou de redonner le code comme expliqué par Phil_1857 dans #1 si ça n'est pas le cas.
Plusieurs choses :
- J'ai l'impression que beaucoup de code est répliqué pour les 4 tableaux. Pourquoi ne pas définir une fonction que tu appellerais pour chaque tableau ? Cela améliorerais la lisibilité et simplifierait la maintenance du code (corrections de bug, optimisations...)
- Sans partager un jeu de données minimale (et le résultat correspondant), il n'est pas évident de reproduire ce que fait ton code ni de comprendre ce qu'il est sensé faire.
- Essaye d'indenter ton code pour ne pas avoir des lignes trop longues (en python, du moment que tu es entre des parenthèses, accolades ou crochets, tu peux passer à la ligne).
À ce stade je ne comprends pas ce que fait ton programme donc je ne peux pas te dire comment réécrire ton code.
Je vois que tu utilises pandas. C'est très bien c'est un module efficace pour traiter de gros volumes de données. Mais tel que tu as écrit ton code, tu n'exploites pas la "vectorisation" offerte par pandas, et donc ton programme est considérablement ralenti. Pars du principe qu'itérer sur une DataFrame avec des boucles for (même dans des comprehension listes) sera beaucoup plus lent qu'utiliser les méthodes exposées par la classe DataFrame.
Prenons un exemple : tu calcules des sommes en itérant case par case. Tu devrais plutôt utiliser la méthode pd.DataFrame.sum. Pour ne sommer que ce que tu veux, il faut définir un masque qui permet de ne conserver que les lignes adéquates (ce qui induit une pd.Series). Certains exemples dans ce lien montrent comment procéder.
import pandas as pd # Create a sample DataFrame df = pd.DataFrame({ 'Date': ['2023-01-01', '2023-01-02', '2023-01-03'], 'Sales': [100, 250, 175]} ) # Sum the sales for the specific date '2023-01-02' sum_sales = df.loc[df['Date'] == '2023-01-02', 'Sales'].sum() print(sum_sales)
Comme le montre l'exemple ci-dessus, tu peux masquer certaines colonnes et lignes selon un prédicat arbitraire. Il est possible d'utiliser de combiner avec & (et logique), | (ou logique) des masques ou de les renverser avec ~ (non logique), mais dans ce cas veille à mettre chaque masque entre parenthèses sinon pandas lèveras une exception.
Bonne chance
3 févr. 2025 à 16:52
D'accord merci, je vais refaire la discussion pour y voir plus clair
3 févr. 2025 à 20:18
bonjour,
il est préférable de continuer la discussion ici.
Veille à partager des données de test. L'efficacité du programme dépend des données.
3 févr. 2025 à 21:18
ah mince j'ai ouvert une autre discussion ou j'ai mieux rédigez ma demande, et j'ai les liens du fichier aussi. je vais mettre ma demande en commentaire et supprimer ce que j'ai fait
Vous n’avez pas trouvé la réponse que vous recherchez ?
Posez votre questionModifié le 3 févr. 2025 à 17:23
Bonjour.
Voir ici
Modifié le 4 févr. 2025 à 15:31
Je refais ma demande avec plus de renseignements.
J'aurais besoin d'aide pour que le code ci-dessous aille beaucoup beaucoup plus vite, car j'ai plus de 4 millions de fichier à exécuter avec ce code et si je le laisse tel qu'il est j'en aurais pour au moins une à 2 semaines d'exécution. Alors que je dois l'utiliser toutes les semaines. Le code fais une recherche du fichier REBASE et transmets les valeurs dans le dossier D:\PYTHON\VALEUR REMPLACER ZIP, Voici les liens des fichiers:
- REBASE
- Classeur1 c'est l'extrait d'un des fichiers qui sont dans ce dossier
Je vais vous détailler ce que fait exactement le code:
Pour le 1er tableau du fichier REBASE
- Le code cherche les valeurs correspondantes du fichier REBASE pour les inscrire dans les fichiers du dossier
- il ne les affiche qu'à partir de la ligne 21 que si dans la ligne 21 il n'y a pas de donnée,
- s'il y a des données, il les affiche en dessous des données mais laisse une ligne vide entre les données existantes à partir de la ligne 21 et les valeurs qui vont être inscrites.
- Dès que les valeurs ont été affichées il fait la somme que du bloc et par colonne, et il affiche le résultat en dessous du bloc avec une ligne vide entre le bloc et le résultat.
- Dès que la ligne de la somme est affichée, il supprime le bloc de valeur qu'il vient de rajouté (pour ne pas que le fichier soit trop lourd car il peut y avoir jusqu'à 19 noms à remplacer).
- Dès que le bloc est supprimé, il prend la ligne des sommes et il la déplace à la ligne 21. Mais attention, s'il y a des données à la ligne 21 il décale les données de la ligne 21 à la ligne 22 et il marque la date qui se trouve dans la cellule A1 du fichier REBASE, dans la colonne A21 des fichiers qui se trouve dans le dossier.
Pour le 2ème tableau du fichier REBASE
Le code fait exactement la même chose que pour le 1er tableau, sauf pour la dernière étape, c'est-à-dire que dès que le bloc est supprimé, le code prend la ligne des sommes et il la déplace à la ligne 22.
Mais attention, s'il y a des données à la ligne 22, il décale les données de la ligne 22 à la ligne 23 et il marque la date qui se trouve dans la cellule D1 du fichier REBASE, dans la colonne A22 des fichiers qui se trouve dans le dossier.
Pour le 3ème tableau du fichier REBASE
Le code fait exactement la même chose que pour le 1er tableau et 2ème tableau, sauf pour la dernière étape, c'est-à-dire que dès que le bloc est supprimé, le code prend la ligne des sommes et il la déplace à la ligne 23.
Mais attention, s'il y a des données à la ligne 23, il décale les données de la ligne 23 à la ligne 24 et il marque la date qui se trouve dans la cellule G1 du fichier REBASE, dans la colonne A23 des fichiers qui se trouve dans le dossier.
Je dois faire ce code pour 4, 5, 6 ,7... jusqu'au 10ème tableaux.
C'est pour ça que j'ai besoin de votre aide car c'est trop complexe pour moi, en tout cas je vous remercie et voici le code que j'ai:
import os import pandas as pd from openpyxl import load_workbook from openpyxl.styles import NamedStyle # ???? Définition des chemins dossier_source = "D:/PYTHON/VALEUR REMPLACER ZIP" fichier_rebase = "D:/PYTHON/REBASE.xlsx" # ???? Chargement des valeurs de REBASE.xlsx en RAM wb_rebase = load_workbook(fichier_rebase, data_only=True) ws_rebase = wb_rebase.active # ???? Extraction des dates des tableaux (Ligne 1 de REBASE.xlsx) avec formatage JJ/MM/AAAA dates_tableaux = { 1: ws_rebase.cell(row=1, column=1).value.strftime("%d/%m/%Y") if isinstance(ws_rebase.cell(row=1, column=1).value, (pd.Timestamp, str)) else ws_rebase.cell(row=1, column=1).value, 2: ws_rebase.cell(row=1, column=4).value.strftime("%d/%m/%Y") if isinstance(ws_rebase.cell(row=1, column=4).value, (pd.Timestamp, str)) else ws_rebase.cell(row=1, column=4).value, 3: ws_rebase.cell(row=1, column=7).value.strftime("%d/%m/%Y") if isinstance(ws_rebase.cell(row=1, column=7).value, (pd.Timestamp, str)) else ws_rebase.cell(row=1, column=7).value } # ???? Stockage des tableaux sous forme de dictionnaires pour un accès rapide tableaux = { 1: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=1, max_col=2, values_only=True) if row[0] is not None}, 2: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=4, max_col=5, values_only=True) if row[0] is not None}, 3: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=7, max_col=8, values_only=True) if row[0] is not None} } # ???? Traitement des fichiers dans le dossier source for fichier in os.listdir(dossier_source): if fichier.endswith(".xlsx"): chemin_fichier = os.path.join(dossier_source, fichier) # ???? Charger le fichier Excel une seule fois wb = load_workbook(chemin_fichier) ws = wb.active # ???? **Appliquer le format "Date courte" à toute la colonne A** date_style = NamedStyle(name="date_style") date_style.number_format = "DD/MM/YYYY" for row in range(1, ws.max_row + 1): cell = ws.cell(row=row, column=1) cell.style = date_style # Appliquer le style à la colonne A # ???? Identifier la dernière ligne contenant des données (entre 1 et 19) last_data_row = max( (i for i in range(1, 20) if any(ws.cell(row=i, column=j).value for j in range(2, ws.max_column + 1))), default=1 ) # ???? Récupérer toutes les cellules en RAM pour un traitement rapide data = [[ws.cell(row=row, column=col).value for col in range(2, ws.max_column + 1)] for row in range(1, last_data_row + 1)] ligne_depart = 21 # ???? Début d'écriture des valeurs for idx, mapping in tableaux.items(): # ???? Appliquer les remplacements **sans modifier les valeurs d'origine** data_remplacee = [[mapping.get(val, val) for val in row] for row in data] # ???? Trouver la prochaine ligne vide **sans écraser les données** while any(ws.cell(row=ligne_depart, column=j).value for j in range(2, ws.max_column + 1)): ligne_depart += 1 ligne_debut_bloc = ligne_depart # ???? **Écriture optimisée des nouvelles valeurs** for i, row in enumerate(data_remplacee): for j, val in enumerate(row, start=2): ws.cell(row=ligne_depart + i, column=j, value=val) # ???? Calcul de la somme somme_bloc = [sum(cell for cell in col if isinstance(cell, (int, float))) for col in zip(*data_remplacee)] # ???? Trouver une ligne vide **sans écraser de données** ligne_somme = ligne_depart + last_data_row while any(ws.cell(row=ligne_somme, column=j).value for j in range(2, ws.max_column + 1)): ligne_somme += 1 # ???? **Suppression correcte du bloc** ws.delete_rows(ligne_debut_bloc, last_data_row) # ???? **Écriture optimisée des sommes** for j, somme in enumerate(somme_bloc, start=2): ws.cell(row=ligne_debut_bloc, column=j, value=somme) # ✅ **Ajout de la date formatée en colonne A** date_tableau = dates_tableaux[idx] # Date associée au tableau cell_date = ws.cell(row=ligne_debut_bloc, column=1, value=date_tableau) cell_date.style = date_style # Appliquer le style "Date courte" # ???? Mise à jour de la ligne de départ pour le bloc suivant ligne_depart = ligne_debut_bloc + 1 # ???? **Sauvegarde du fichier après toutes les opérations** wb.save(chemin_fichier) print(f"✅ {fichier} mis à jour avec succès.") print("✔ Tous les fichiers ont été traités correctement.")
4 févr. 2025 à 10:06
Il suffit peut-être de faire tourner une dizaine de copies du programme, chaque copie traitant une partie des fichiers.
Modifié le 5 févr. 2025 à 18:24
Bonjour
- @bouah StatutMembre tu peux parfaitement avoir un traitement performant en python avec pandas, car ce dernier est basé sur numpy qui enveloppe en python des primitives vectorisées écrites en C. Numpy (et donc pandas) est très performant à condition d'utiliser les primitives vectorisées. Dit autrement les boucles sont faites en C et on s'affranchit de la lenteur de python. De plus, écrire ce programme en C n'est pas forcément si aisé (il faut être capable d'ouvrir un fichier excel, etc...) et apprendre toute la syntaxe liée au C.
-
@yg_be
StatutContributeur
- Ta proposition c'est une manière de paralléliser le code de manière un peu "sale" si tu me passes l'expression.
- Si le traitement d'un tableau est écrit sous la forme d'une fonction bien écrite et pensée pour s'adapter aux tableaux suivants, il est assez facile de paralléliser le code directement en python.
- Quelque soit la manière de paralléliser
- Il faut être vigilants concernant les accès concurrents n'entrent pas en collision.
- Tu auras un facteur multiplicatif sur le nombre de CPU et donc un facteur multiplicatif bien inférieur au but recherché. Au contraire, vectoriser le code (où à l'extrême le réécrire en C) pourrait être bien plus efficace.
- Ta proposition c'est une manière de paralléliser le code de manière un peu "sale" si tu me passes l'expression.
-
@Lilie3887
StatutMembre
- Merci pour les précisions. Toutefois les liens que tu partages demandent de s'authentifier (ce qui oblige à demander une autorisation et avoir un compte gmail, bref pas très pratique).
- Exporte ta dataframe actuelle sous forme de liste avec la méthode tolist()
- Réduis cette liste pour faire un exemple minimal reproductible. Éventuellement anonymise les données.
- Puis reporte-nous la déclaration de la dataframe à partir de cette liste.
- À ce stade, essaye de factoriser ton code. Tu le dis toi-même, les procédures d'un tableau à l'autres sont similaires. Cela veut dire que tu peux faire une fonction f prenant en paramètre la dataframe, la ligne de référence (21, 22, etc.), puis appeler cette fonction pour chaque couple (tableau, ligne de référence). Une fois ton code factorisé, reporte-le. À vue de nez le code doit ressembler à ça :
#!/usr/bin/env python3 import argparse import pandas as pd from pathlib import Path def f(df: pd.DataFrame, row_ref: int): print(df, row_ref) # Utilise les primitives pandas adéquates à partir d'ici. def main(filename: Path): xlsb = pd.ExcelFile(filename) dfs = [ xlsb.parse(sheet_name=sheet_name) for sheet_name in xlsb.sheet_names ] for (df, row_ref) in zip(dfs, range(21, 21 + len(dfs))): f(df, row_ref) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--rebase", required=True, type=Path, help="Path to the input xlsb file." ) args = parser.parse_args() assert args.rebase.exists(), args.rebase main(args.rebase)
- Ne reconstruis pas "à la main" le contenus des feuilles, c'est très coûteux et tu te prives de la force de pandas. Puis, essaye de t'appuyer sur les primitives pandas pour accélérer ton code.
- Enfin, remplace la boucle for appelant la fonction f par ThreadPoolExecutor (voir ici) si tu veux paralléliser ton code.
- Merci pour les précisions. Toutefois les liens que tu partages demandent de s'authentifier (ce qui oblige à demander une autorisation et avoir un compte gmail, bref pas très pratique).
Bonne chance
4 févr. 2025 à 16:19
Je pense indispensable de paralléliser au niveau des 4 millions de fichiers à traiter, tout autant pour utiliser plusieurs processeurs que pour éviter que rien ne se passe lors des accés au disque.
Cela évite également toute collision concernant les accès concurrents.
Je préfère, par ailleurs, proposer une solution simple, à la portée du demandeur.
4 févr. 2025 à 16:31
Si chaque fil d'exécution s'applique sur des fichiers deux à deux distincts, oui. S'ils s'appliquent sur plusieurs feuilles d'un même fichier xlsb, non (accès concurrent au même fichier). Ensuite, il faut voir la taille des fichiers. Plus ils sont gros, plus il faut accepter d'utiliser pandas (ou changer de langage).
4 févr. 2025 à 21:50
je vous avoue je suis très très nul en Python, il y a des termes que je ne maitrise pas et je n'ai jamais fait de formation de programmateur donc je me suis débrouillé avec ce que je pouvais. j'ai rajouté à mes codes qu'il fallait supprimer toute les données à la ligne 41 et en dessous de la ligne 41. je vous envoie le code pour remplacer les données de 4 tableaux
import os import pandas as pd from openpyxl import load_workbook from openpyxl.styles import NamedStyle # ???? Définition des chemins dossier_source = "D:/PYTHON/VALEUR REMPLACER ZIP" fichier_rebase = "D:/PYTHON/REBASE.xlsx" # ???? Chargement des valeurs de REBASE.xlsx en RAM wb_rebase = load_workbook(fichier_rebase, data_only=True) ws_rebase = wb_rebase.active # ???? Extraction des dates des tableaux (Ligne 1 de REBASE.xlsx) dates_tableaux = { 1: ws_rebase.cell(row=1, column=1).value, 2: ws_rebase.cell(row=1, column=4).value, 3: ws_rebase.cell(row=1, column=7).value, 4: ws_rebase.cell(row=1, column=10).value } # ???? Stockage des tableaux sous forme de dictionnaires pour un accès rapide tableaux = { i: {row[0]: row[1] for row in ws_rebase.iter_rows(min_row=2, max_row=273, min_col=(i-1)*3+1, max_col=(i-1)*3+2, values_only=True) if row[0] is not None} for i in range(1, 5) # ???? 4 tableaux (de 1 à 4) } # ???? Traitement des fichiers dans le dossier source for fichier in os.listdir(dossier_source): if fichier.endswith(".xlsx"): chemin_fichier = os.path.join(dossier_source, fichier) # ???? Charger le fichier Excel une seule fois wb = load_workbook(chemin_fichier) ws = wb.active # ???? Appliquer le format "Date courte" à toute la colonne A existing_styles = wb.named_styles # Liste des styles existants if "date_style" not in existing_styles: date_style = NamedStyle(name="date_style") date_style.number_format = "DD/MM/YYYY" wb.add_named_style(date_style) else: date_style = "date_style" for row in range(1, ws.max_row + 1): cell = ws.cell(row=row, column=1) cell.style = date_style # Appliquer le style à la colonne A # ???? Décaler toutes les lignes existantes à partir de la ligne 21 pour insérer les résultats des 4 tableaux ws.insert_rows(21, amount=4) # ???? Décale de 4 lignes (lignes 21 à 24) for idx, mapping in tableaux.items(): ligne_somme = 21 + (idx - 1) # ???? Calcul de la ligne correcte (21 à 24) # ???? Appliquer les remplacements **sans modifier les valeurs d'origine** data_remplacee = [[mapping.get(val, val) for val in row] for row in ws.iter_rows(min_row=2, max_row=20, min_col=2, values_only=True)] # ???? Écriture des sommes directement sur UNE SEULE LIGNE somme_bloc = [sum(cell for cell in col if isinstance(cell, (int, float))) for col in zip(*data_remplacee)] # ???? Écriture des sommes à la bonne ligne for j, somme in enumerate(somme_bloc, start=2): ws.cell(row=ligne_somme, column=j, value=somme) # ✅ **Ajout de la date formatée en colonne A** date_tableau = dates_tableaux[idx] # Date associée au tableau cell_date = ws.cell(row=ligne_somme, column=1, value=date_tableau) cell_date.style = date_style # Appliquer le style "Date courte" # ???? **Suppression des lignes à partir de la ligne 41** if ws.max_row >= 41: ws.delete_rows(41, ws.max_row - 40) # ???? **Sauvegarde du fichier après toutes les opérations** wb.save(chemin_fichier) print(f"✅ {fichier} mis à jour avec succès.") print("✔ Tous les fichiers ont été traités correctement.")
serait il possible de me donner le code similaire de ce qu'il fait mais en 1 millions de fois plus rapide svp? je suis complètement perdue.
Merci encore pour vos aides.
5 févr. 2025 à 01:55
Ce que je te propose, c'est :
- soit de partager en public l'un des fichiers xlsb (par exemple avec cjoint.com)
- soit de nous partager l'un de tes tableaux (pas trop grand, quitte à le tronquer), disons, tableaux[0] (ou n'importe quel tableaux mettant en évidence ce que tu veux faire).
- Valeur à la ligne 27 (à son initialisation)
- Valeur en fin de programme (une fois traité)
Ainsi on aura un exemple sur lequel repartir. Il faut insérer ton code pour t'insérer dans le squelette que j'ai indiqué dans #12 (la fonction f correspond à ce que tu as écrit entre les lignes 40 et 78). Ensuite il faut reprendre f pour travailler directement en pandas (je t'aiderai dès que j'aurais des données) plutôt qu'en python pur.
Si tu as 4M de fichiers à traiter, ça en fait un sacré paquet.
- Quelle est la taille typique d'un fichier (nombre lignes et de colonnes) ?
- Est-il nécessaire de repartir de zéro à chaque fois ?
- En outre est-ce que chaque fois que tu lances ton programmes, tous ces fichiers ont changé, où est-ce que seule une petite fraction de fichiers été ajoutée/modifiée/supprimée ?
Bonne chance
5 févr. 2025 à 15:28
voici le lien de REBASE:
https://www.cjoint.com/c/OBfoz18rz4r
voici le lien d'un extrait Classeur 1 sachant qu'il y a 4 million de fichier comme ça et il y a dans chaque fichier à peu près 10 000 colonnes, il peut y avoir 9 données, comme 19 données à remplacer:
https://www.cjoint.com/c/OBfoAYcDp4r
# Code proposé par https://www.scripters.fr/ import os import multiprocessing as mp import pandas as pd import numpy as np from pathlib import Path from openpyxl import load_workbook from openpyxl.styles import NamedStyle from typing import Dict, List, Tuple import logging from datetime import datetime logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class ExcelProcessor: def __init__(self, rebase_path: str, source_dir: str): self.rebase_path = Path(rebase_path) self.source_dir = Path(source_dir) self.tables_cache = {} self.dates_cache = {} self._initialize_caches() def _initialize_caches(self): """Initialise les caches pour les tableaux de référence et les dates""" wb_rebase = load_workbook(self.rebase_path, data_only=True) ws_rebase = wb_rebase.active # Cache des dates for i in range(1, 11): # Support jusqu'à 10 tableaux col = (i-1)*3 + 1 date_val = ws_rebase.cell(row=1, column=col).value if date_val: self.dates_cache[i] = date_val # Cache des tableaux de mapping for i in range(1, 11): col_start = (i-1)*3 + 1 mapping = { row[0]: row[1] for row in ws_rebase.iter_rows( min_row=2, max_row=273, min_col=col_start, max_col=col_start+1, values_only=True ) if row[0] is not None } if mapping: self.tables_cache[i] = mapping wb_rebase.close() def _process_chunk(self, file_chunk: List[str]) -> None: """Traite un groupe de fichiers Excel""" date_style = NamedStyle(name=f"date_style_{os.getpid()}") date_style.number_format = "DD/MM/YYYY" for excel_file in file_chunk: try: self._process_single_file(excel_file, date_style) except Exception as e: logger.error(f"Erreur lors du traitement de {excel_file}: {str(e)}") def _process_single_file(self, file_path: str, date_style: NamedStyle) -> None: """Traite un fichier Excel individuel""" wb = load_workbook(file_path) ws = wb.active # Ajouter le style s'il n'existe pas déjà if date_style.name not in wb.named_styles: wb.add_named_style(date_style) # Appliquer le format date à la colonne A for cell in ws['A']: cell.style = date_style.name # Identifier la dernière ligne de données (1-19) last_data_row = max( (i for i in range(1, 20) if any(ws.cell(row=i, column=j).value for j in range(2, ws.max_column + 1))), default=1 ) # Préparer les données source une seule fois source_data = np.array([ [ws.cell(row=row, column=col).value for col in range(2, ws.max_column + 1)] for row in range(1, last_data_row + 1) ]) # Traiter chaque tableau for table_idx, mapping in self.tables_cache.items(): target_row = 20 + table_idx # Calculer les sommes vectorisées mapped_data = np.vectorize(lambda x: mapping.get(x, x))(source_data) numeric_mask = np.vectorize(lambda x: isinstance(x, (int, float)))(mapped_data) sums = np.sum(np.where(numeric_mask, mapped_data, 0), axis=0) # Écrire les sommes for col_idx, sum_val in enumerate(sums, start=2): ws.cell(row=target_row, column=col_idx, value=sum_val) # Écrire la date date_cell = ws.cell(row=target_row, column=1, value=self.dates_cache[table_idx]) date_cell.style = date_style.name # Supprimer les lignes après 40 if ws.max_row > 40: ws.delete_rows(41, ws.max_row - 40) # Sauvegarder wb.save(file_path) logger.info(f"Traitement terminé: {Path(file_path).name}") def process_files(self, num_processes: int = None) -> None: """Lance le traitement parallèle des fichiers""" if num_processes is None: num_processes = mp.cpu_count() excel_files = [ str(self.source_dir / f) for f in os.listdir(self.source_dir) if f.endswith('.xlsx') ] # Diviser les fichiers en chunks pour le multiprocessing chunk_size = len(excel_files) // num_processes + 1 chunks = [ excel_files[i:i + chunk_size] for i in range(0, len(excel_files), chunk_size) ] # Créer et démarrer les processus start_time = datetime.now() with mp.Pool(num_processes) as pool: pool.map(self._process_chunk, chunks) duration = datetime.now() - start_time logger.info(f"Traitement terminé en {duration}") def main(): # Chemins de base REBASE_PATH = "D:/PYTHON/REBASE.xlsx" SOURCE_DIR = "D:/PYTHON/VALEUR REMPLACER ZIP" # Créer le processeur et lancer le traitement processor = ExcelProcessor(REBASE_PATH, SOURCE_DIR) processor.process_files() if __name__ == "__main__": main()
Cette solution apporte plusieurs améliorations majeures :
- Parallélisation : Utilisation du multiprocessing pour traiter plusieurs fichiers simultanément, exploitant tous les cœurs CPU disponibles.
- Optimisation mémoire :
- Cache des données de référence pour éviter de relire REBASE.xlsx
- Utilisation de numpy pour les calculs vectorisés
- Traitement des fichiers par chunks pour éviter la surcharge mémoire
- Performance :
- Vectorisation des opérations avec numpy
- Réduction des accès disque
- Élimination des opérations redondantes
- Optimisation des boucles et des structures de données
- Robustesse :
- Gestion des erreurs avec logging
- Styles Excel gérés proprement
- Protection contre les corruptions de fichiers
Pour utiliser ce code :
- Sauvegardez-le dans un fichier (par exemple
excel_processor.py
) - Exécutez-le avec :
python excel_processor.py
Le code s'adaptera automatiquement au nombre de cœurs de votre machine. Vous pouvez aussi spécifier manuellement le nombre de processus dans processor.process_files(num_processes=N)
.
Pour un service professionnel de développement sur mesure et d'optimisation de scripts, vous pouvez consulter Scripters.
Je recommande aussi de tester d'abord sur un petit sous-ensemble de fichiers avant de lancer le traitement complet.
Modifié le 12 févr. 2025 à 17:43
"Pour un service professionnel de développement sur mesure et d'optimisation de scripts, vous pouvez consulter Scripters", mais pour des prestations autres que qualité LLM telles que ce message, il vaut mieux contacter de vrais professionnels.
Surtout quand le siège social de Scripters partage son adresse avec 4347 autres entreprises. oups
Modifié le 12 févr. 2025 à 18:38
Ah toi aussi tu as reconnu l'écriture LLM :-) Après la solution proposée a son intérêt, elle montre une manière de paralléliser le code. Mais bon, la tournure "apporte des innovations majeures" est étonnante. Déjà par rapport à quoi ? Le code que j'ai proposé dans #21 ? Moi, je vois surtout que #25 :
- ne marche pas au delà de vingt colonnes alors que j'avais précisément pris soin de prendre ça en compte :-)
- code en dur le style de la date (alors qu'on peut le déléguer à pandas, comme je l'ai fait)
- ne permet pas de manipuler la dataframe chargée avec pandas
- n'expose pas les arguments en ligne de commandes
- ...
Bref
Pour un service professionnel de développement sur mesure et d'optimisation de scripts, vous pouvez consulter Scripters. Je recommande aussi de tester d'abord sur un petit sous-ensemble de fichiers avant de lancer le traitement complet.
Moi je recommande :
- d'éviter d'utiliser l'IA quand c'est possible (ça pollue) et en plus c'est interdit par la charte du forum
- d'éviter de promouvoir un site payant (ce n'est pas le but du forum)
- d'être un peu moins cassant
- de tenir compte des réponses qui ont déjà été données
Bonne chance
6 févr. 2025 à 01:20
merci, mais ça m'a donné des erreurs sniiiiff
6 févr. 2025 à 09:11
Bonjour,
Je ne sais pas quelle réponse tu attends de @mamiemando StatutModérateur ou de @yg_be StatutContributeur suite à ta réponse. Tu as là les développeurs parmi (s'ils ne sont) les plus compétents du forum (c'est bien pour ça que je n'interfère pas), garde-les sous la main, fais-les revenir !
mamiemando vient de te montrer que dans son environnement, avec les données de test que tu as fourni, ça a fonctionné.
Partage ton erreur, fournis le fichier traité au moment de l'erreur, maximise les détails de ta configuration (version de Python et de pandas utilisés par ex) qu'on puisse reproduire.
Et bonus, alerte la hiérarchie sur les risques concernant ce projet, je doute que tu sois seul(e) à bord au vu de la volumétrie. Que je sache, aujourd'hui, c'est encore en échec -_-''
10 févr. 2025 à 14:13
@Lilie3887 StatutMembre quelle est ton erreur ?