[pandas] Surligner des lignes d'une DataFrame

Résolu
Inuwashi Messages postés 3 Date d'inscription jeudi 6 juin 2024 Statut Membre Dernière intervention 8 juin 2024 - Modifié le 7 juin 2024 à 16:35
mamiemando Messages postés 33346 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 8 novembre 2024 - 10 juin 2024 à 16:47

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

mamiemando Messages postés 33346 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 8 novembre 2024 7 803
Modifié le 1 oct. 2024 à 16:47

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

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
Inuwashi Messages postés 3 Date d'inscription jeudi 6 juin 2024 Statut Membre Dernière intervention 8 juin 2024
Modifié le 10 juin 2024 à 15:16

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