[pandas] Surligner des lignes d'une DataFrame

Résolu
Inuwashi Messages postés 3 Statut Membre -  
mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   -

Bonjour,

J'ai besoin de surligner des lignes d'un tableau excel par rapport à un autre.

J'ai donc importé mes deux tableaux dont l'un a plus de 10 colonnes (nom, prénom, numéro de téléphone, ...) . J'aimerais surligner en rouge toute la ligne de mon tableau si le numéro de téléphon n'apparaît pas dans l'autre tableau.

Comment faire ceci avec des tableaux excel importés en DataFrame ??

Merci de vos réponses !
Windows / Chrome 125.0.0.0

2 réponses

  1. mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   7 944
     

    Bonjour,

    Réponse brève

    Voir la section "Réponse finale" en bas de message, pour les personnes pressées et/ou familières de pandas/numpy/python

    Réponse détaillée

    La question est double car il faut considérer deux sous-questions indépendantes:

    1. Comprendre comment appliquer un style en pandas
    2. Comprendre comment extraire le masque adéquat pour appliquer le style sur les cellules adéquates

    Construire un masque avec pandas

     Le concept de masque (qui en réalité est réalisé grâce à numpy) consiste à construire un tableau de même taille que l'itérable considéré (une pd.DataFrame, une pd.Series, un np.array, etc).

    Dans cette optique, pandas propose un système permettant de faire une requête sur une pd.DataFrame en vue d'en dériver un masque qui ne conserve que les cellules satisfaites par la requête. Si tu as fait des bases de données, c'est un peu là même idée qu'une requête (SELECT ... WHERE en SQL).

    Dans ton cas, on veut sélectionner toutes les rangées telle que la colonne "telephone" de la rangée courante d'une première pd.DataFrame (df1) apparaît n'importe dans la colonne "telephone" d'une autre pd.DataFrame (df2). Pandas propose un opérateur permettant de faire un test d'appartenance de manière vectorisée (c'est-à-dire, de manière efficace) : pd.DataFrame.isin. Cette opération retourne un masque. Il est possible d'appliquer sur un masque n'importe quelle opération "bitwise" (bit-à-bit) :

    • renverser le masque (NOT) : ~
    • faire l'intersection avec un autre masque (AND) : &
    • faire l'union avec un autre masque (OR) : |

    Plus de détails dans ce tutoriel.

    Dans ton cas, le masque s'écrit :

    ~df2["telephone"].isin(df1["telephone"])

    Dans ce qui suit, je vais utiliser la colonne 1 (donc la colonne qui contient des entiers) pour stocker mes "numéros de téléphone". 

    import pandas as pd
        
    df1 = pd.DataFrame([
        ["toto", 2],
        ["tata", 3],
        ["titi", 4],
    ])
    df2 = pd.DataFrame([
        ["toto", 2],
        ["tutu", 5],
        ["titi", 3],
    ])
    
    mask = df2[1].isin(df1[1])
    print(mask)
    
    # Affiche :
    # 0     True
    # 1    False
    # 2     True
    # Name: 1, dtype: bool

    Appliquer un style HTML à des cellules pandas

    Pour commencer, démystifions comment marche l'application d'un style en pandas. Considérons l'un exemple jouet ci-dessous inspiré de cette discussion.

    import numpy as np
    import pandas as pd
    from IPython.display import HTML
    
    df = pd.DataFrame(np.reshape(np.arange(6), (2, 3)))
    
    mask1 = np.array(
        [
            [0, 1, 0],
            [0, 0, 1],
        ], dtype=bool
    )
    mask2 = np.array(
        [
            [0, 0, 1],
            [0, 1, 1],
        ],
        dtype=bool
    )
    
    def styler(s):
        color = "background-color: {}".format
        m = np.select(
            [mask1, mask2],
            list(map(color, ["red", "blue"])),
            default=""
        )
        print(m)
        # Affiche:
        # [['' 'background-color: red' 'background-color: blue']
        # ['' 'background-color: blue' 'background-color: red']]
        return m
    
    df = df.style.apply(styler, axis=None)
    HTML(df.to_html())

    Dans cet exemple on voit que le style à appliquer (styler) retourne un tableau dont

    • les dimensions correspondent à la pd.DataFrame de départ
    • chaque case contient le style HTML à appliquer à chaque cellule.

    On voit que ce code peut être facilement étendu avec un nombre arbitraire de masques (pour le moment, 2 masques, respectivement colorés en rouge et bleu). Un exemple avec plus de paires (masque, couleur) est proposé ici.

    Une case ne peut avoir qu'une couleur, alors quid dans notre exemple de la case en bas à droite, sélectionnée par les deux masques ? Eh bien, cet extrait de code sélectionne la première couleur qui correspond à un masque, donc si une case correspond au masque rouge et au masque bleu, alors elle sera colorée en rouge. Notons aussi que l'attribut default permet d'attribuer le style par défaut (dans cet exemple, aucun style, puisque default="")

    Maintenant, ce code est un peu sale, car styler utilise mask en tant que variable globale. On ne peut pas l'ajouter en paramètre, alors comment faire. Ici, le plus simple est de s'en sortir avec un foncteur, c'est-à-dire en une classe capable de se comporter comme une fonction. En python, cela consiste à définir une classe, le foncteur, qui implémente la méthode __call__ dans ledit objet. Les paramètres de __call__ correspondent à ceux de l'ancienne fonction (il faut aussi passer self, puisqu'il s'agit d'une méthode d'instance).

    Dans notre cas la fonction styler se réécrit avec le foncteur Styler suivant :

    import numpy as np
    import pandas as pd
    from IPython.display import HTML
    
    
    class Styler:
        def __init__(self, masks, colors):
            self.masks = masks
            self.colors = colors
    
        def __call__(self, s):
            color = "background-color: {}".format
            return np.select(
                self.masks,
                list(map(color, self.colors)),
                default=""
            )
    
    
    df = pd.DataFrame(np.reshape(np.arange(6), (2, 3)))
    
    mask1 = np.array(
        [
            [0, 1, 0],
            [0, 0, 1],
        ], dtype=bool
    )
    mask2 = np.array(
        [
            [0, 0, 1],
            [0, 1, 1],
        ],
        dtype=bool
    )
    
    
    styler = Styler([mask1, mask2], ["red", "blue"])
    df = df.style.apply(styler, axis=None)
    HTML(df.to_html())

    Réunir les deux réponses

    Il reste un problème de taille à régler. Dans notre exemple, si df1 est de taille (m, n), alors mask est de taille m. Or nous avons vu que pour appliquer le style, il faut que le masque doit être de la même taille que df1, donc de taille (m, n). Il faut donc revoir comment on construit mask.

    Pour régler ce problème, soit un nouveau masque mask2 de taille (m, n) dans lequel les cellules à de la ligne i valent 1 si et seulement si mask[i] == 1, 0 sinon. Ce nouveau masque peut se construire ainsi.

    mask = ~df2[1].isin(df1[1])
    mask2 = np.zeros(df1.shape, dtype=bool)
    mask2[mask, :] = 1
    print(mask2)

    Afin de garder le code concis, je vais maintenant me passer de mon masque intermédiaire mask et renommer mask2 en mask :

    mask = np.zeros(df1.shape, dtype=bool)
    mask[~df2[1].isin(df1[1]), :] = 1
    print(mask)

    Réponse finale

    import pandas as pd
    import numpy as np
    from IPython.display import HTML
    
    
    class Styler:
        def __init__(self, masks, colors):
            self.masks = masks
            self.colors = colors
            
        def __call__(self, s):
            color = "background-color: {}".format
            return np.select(
                self.masks,
                list(map(color, self.colors)),
                default=""
            )
    
    
    def highlight(df, mask, color="red"):
        m = np.zeros(df.shape, dtype=bool)
        m[mask, :] = 1
        styler = Styler([m], [color])
        return df.style.apply(styler, axis=None)
    
    
    df1 = pd.DataFrame([
        ["toto", 2],
        ["tata", 3],
        ["titi", 4],
    ])
    df2 = pd.DataFrame([
        ["toto", 2],
        ["tutu", 5],
        ["titi", 3],
    ])
    df1 = highlight(df1, ~df2[1].isin(df1[1]), "red")
    HTML(df1.to_html())

    Bonne chance

    1
  2. bruf
     

    Salut, en supposant que ce sont des dataframes panda.

    A-priori, pas d'autres choix que de créer sa propre fonction pour surligner chaque cellules.

    Voir ici ou encore ici.

    0
    1. Inuwashi Messages postés 3 Statut Membre
       

      Merci pour ta réponse mais sans succès, je suis mauvais en python et ca m'agace.

      Je mets mon code :

      import pandas as pd
      
      
      df_clients = pd.read_excel(r"Fichier clients.xlsx", header=0)
      df_retour = pd.read_excel(r"Retour fichier 2.xlsx", header=0)
      
      L = len(df_clients)
      x = df_clients["telephone"]
      x = x.to_frame()
      tel_clients = [int(x.values[k]) for k in range(L)]
      
      l = len(df_retour)
      x = df_retour["telephone"]
      x = x.to_frame()
      tel_retour = [int(x.values[k]) for k in range(l)]
      
      def surlignage_ligne(x):
          if x.telephone not in tel_retour :
              return ["background-color: red"] * len(df_clients.values[0])
          else:
              return ['background-color: ""'] * len(df_clients.values[0])
           
      df_clients.style.apply(surlignage_ligne, axis=1)

      Nouvelle version :

      import pandas as pd
      
      
      df_clients = pd.read_excel(r"Fichier clients.xlsx", header=0)
      df_retour = pd.read_excel(r"Retour fichier 2.xlsx", header=0)
      
      L = len(df_clients)
      x = df_clients["telephone"]
      x = x.to_frame()
      tel_clients = [int(x.values[k]) for k in range(L)]
      
      l = len(df_retour)
      x = df_retour["telephone"]
      x = x.to_frame()
      tel_retour = [int(x.values[k]) for k in range(l)]
      
      ref_manquantes = []
      for i in range(L):
         if tel_clients[i] not in tel_retour:
            ref_manquantes.append(i+1)
      
      def surlignage_ligne(x):
          if x.index in ref_manquantes :
              return["background-color: red"] * len(df_clients.values[0])
          else:
              return['background-color: ""'] * len(df_clients.values[0])
      
      df_clients.style.apply(surlignage_ligne, axis=1)
      writer = pd.ExcelWriter("Fichier clients modifié.xlsx")
      df_clients.to_excel(writer, sheet_name="Feuille1", index=False)
      writer.save()

      Merci beaucoup de la réponse 

      0