3
votes

Recherche de correspondances partielles entre les colonnes de deux dataframes différentes et attribution de valeurs lorsque des correspondances sont trouvées

Je souhaite remplir la colonne "Category" de df1 dataframe avec les valeurs correctes de la colonne "Category" de df2 dataframe.

df1
Receiver                Category
0   Insurance company   Insurances
1   Shop                Groceries
2   Pizza place         Fastfood
3   Library             
4   Gas station 24/7    Car
5   Something else      
6   Whatever receiver   

Sortie:

df1
Receiver                Category
0   Insurance company   
1   Shop    
2   Pizza place 
3   Library 
4   Gas station 24/7    
5   Something else  
6   Whatever receiver   

df2
    Category    Searchterm
0   Insurances  Insur
1   Groceries   Shop
2   Groceries   Market
3   Fastfood    Pizza
4   Fastfood    Burger
5   Car         Gas

Je veux comparer df1 ["Receiver"] à df2 ["Searchterm"] ligne par et lorsque ce dernier correspond même partiellement au premier , attribuez df2 ["Category"] de cette ligne à df1 ["Category"] .

Par exemple, "Pizza" dans df2 ["Searchterm"] correspond partiellement à "Pizza place" dans df1 ["Receiver"] , donc je voulez attribuer "Fastfood" (qui est la catégorie Pizza dans df2 ["Category"] ) à la catégorie "Pizza place" dans df1 ["Category"] .

Le résultat souhaité serait:

import pandas as pd

df1 = pd.DataFrame({"Receiver": ["Insurance company", "Shop", "Pizza place", "Library", "Gas station 24/7", "Something else", "Whatever receiver"], "Category": ["","","","","","",""]}) 
df2 = pd.DataFrame({"Category": ["Insurances", "Groceries", "Groceries", "Fastfood", "Fastfood", "Car"], "Searchterm": ["Insurance", "Shop", "Market", "Pizza", "Burger", "Gas"]})

Alors, comment puis-je remplir df1 ["Category"] avec les bonnes catégories? Merci.


0 commentaires

3 Réponses :


6
votes

Itérer les catégories

Sous l'hypothèse que le nombre de catégories est petit par rapport au nombre de récepteurs, une stratégie consiste à itérer les catégories. Avec cette solution, notez que la dernière correspondance ne restera que là où plusieurs catégories sont trouvées.

def jpp(df1, df2):
    for tup in df2.itertuples(index=False):
        df1.loc[df1['Receiver'].str.contains(tup.Searchterm, regex=False), 'Category'] = tup.Category
    return df1

def user347(df1, df2):
    df1['Category'] = df1['Receiver'].replace((df2['Searchterm'] + r'.*').values,
                                              df2['Category'].values,
                                              regex=True)
    df1.loc[df1['Receiver'].isin(df1['Category']), 'Category'] = ''
    return df1

df1 = pd.concat([df1]*10**4, ignore_index=True)
df2 = pd.concat([df2], ignore_index=True)

%timeit jpp(df1, df2)      # 145 ms per loop
%timeit user347(df1, df2)  # 364 ms per loop

df1 = pd.concat([df1], ignore_index=True)
df2 = pd.concat([df2]*100, ignore_index=True)

%timeit jpp(df1, df2)      # 666 ms per loop
%timeit user347(df1, df2)  # 88 ms per loop

Analyse comparative des performances

Comme indiqué , cette solution évolue mieux avec les lignes de df1 qu'avec les catégories de df2 . Pour illustrer cela, examinez les performances ci-dessous pour des cadres de données d'entrée de tailles différentes.

for tup in df2.itertuples(index=False):
    mask = df1['Receiver'].str.contains(tup.Searchterm, regex=False)
    df1.loc[mask, 'Category'] = tup.Category

print(df1)

#      Category           Receiver
# 0  Insurances  Insurance company
# 1   Groceries               Shop
# 2    Fastfood        Pizza place
# 3                        Library
# 4         Car   Gas station 24/7
# 5                 Something else
# 6              Whatever receiver

8 commentaires

Merci beaucoup, j'ai passé des heures et des heures et essayé des centaines de lignes de méthodes différentes sans succès, mais cela a finalement réussi. J'ai essayé d'éviter les boucles for avec des dataframes, mais cela semble beaucoup plus simple que les propres méthodes des pandas, et n'a probablement aucun problème à moins qu'il n'y ait des milliers de récepteurs? Pouvez-vous expliquer ce que vous entendez par "la dernière correspondance ne restera que là où plusieurs catégories se trouvent"?


@KMFR, Premier point, la méthode ralentit davantage lorsque vous avez un plus grand nombre de catégories; sinon devrait être bien. Deuxième point, disons que vous avez un récepteur appelé "Insurance Shop" ... il sera mappé à l'épicerie au lieu de la compagnie d'assurance.


Ok merci @jpp, j'éviterai les termes de recherche trop vagues.


Peut-être Series.replace pour une solution vectorisée lorsque les données sont volumineuses?


@ user3471881, Non, car OP veut des correspondances partielles, replace nécessite des correspondances exactes.


J'allais juste vous demander de montrer les performances lorsque les catégories sont grandes - très bonne mise à jour et merci de l'avoir fait pour que je n'ai pas à: D


@Vaishali, j'ai essayé mais il y a eu une erreur d'indexation: (. Je vous laisse libre cours pour mettre à jour le benchmarking si vous le gérez. Ou ajoutez-le au vôtre et je le supprimerai de mon message.


Oh ok, puisque le df2 a été mis à jour en utilisant concat, il contenait des index en double qui ne fonctionneraient pas avec map. Création d'un mapper dict et mise à jour du benchmark dans mai post. Je ne voulais pas modifier le vôtre



3
votes

Une autre solution utilisant str.extract

def jpp(df1, df2):
    for tup in df2.itertuples(index=False):
        df1.loc[df1['Receiver'].str.contains(tup.Searchterm, regex=False), 'Category'] = tup.Category
    return df1

def user347(df1, df2):
    df1['Category'] = df1['Receiver'].replace((df2['Searchterm'] + r'.*').values,
                                              df2['Category'].values,
                                              regex=True)
    df1.loc[df1['Receiver'].isin(df1['Category']), 'Category'] = ''
    return df1

def vai(df1, df2):
    pat = '('+'|'.join(df2['Searchterm'])+')'
    df1["Category"] = df1['Receiver'].str.extract(pat)[0].map(df2.set_index('Searchterm')['Category'].to_dict()).fillna('')

df1 = pd.concat([df1]*10**4, ignore_index=True)
df2 = pd.concat([df2], ignore_index=True)

%timeit jpp(df1, df2)    
%timeit user347(df1, df2)
%timeit vai(df1, df2)


120 ms ± 2.26 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
221 ms ± 4.74 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
78.2 ms ± 1.56 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

df1 = pd.concat([df1], ignore_index=True)
df2 = pd.concat([df2]*100, ignore_index=True)

%timeit jpp(df1, df2)
%timeit user347(df1, df2)
%timeit vai(df1, df2)

11.4 s ± 276 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
20.4 s ± 296 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
98.3 ms ± 408 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Analyse comparative des performances

pat = '('+'|'.join(df2['Searchterm'])+')'
df1["Category"] = df1['Receiver'].str.extract(pat)[0].map(df2.set_index('Searchterm')['Category'].to_dict()).fillna('')

    Receiver            Category
0   Insurance company   Insurances
1   Shop                Groceries
2   Pizza place         Fastfood
3   Library 
4   Gas station 24/7    Car
5   Something else  
6   Whatever receiver   


3 commentaires

Très cool, merci! J'ai essayé d'utiliser str.contains (), str.extract () est nouveau pour moi.


Très agréable :). Je ne suis pas sûr que les résultats soient exactement comparables car df2.set_index ('Searchterm') ['Category']. To_dict () se réduit à un petit conteneur. Aussi pour 10 ** 5 je vois la performance jpp: 1,52 s par boucle; user347: 3,82 s par boucle; vai: 1,62 s par boucle . Donc un peu un peu entre nos méthodes.


Le deuxième critère ici est totalement inutile pour être juste. Peut-être le supprimer ou trouver une solution différente avec str.extract ?



3
votes

Vous pouvez utiliser Series.replace avec regex pour une approche vectorisée:

df1['Category'] = df1['Receiver'].replace(
    (df2['Searchterm'] + r'.*').values,
    df2['Category'].values,
    regex=True
)

df1.loc[df1['Receiver'].isin(df1['Category']), 'Category'] = ''

print(df1)

     Category           Receiver
0  Insurances  Insurance company
1   Groceries               Shop
2    Fastfood        Pizza place
3                        Library
4         Car   Gas station 24/7
5                 Something else
6              Whatever receiver

Notez que cela suppose que chaque La chaîne Searchterm se trouve au début de chaque chaîne Receiver . Si ce n'est pas le cas, ajustez l'expression régulière en conséquence.


1 commentaires

C'est en effet une bonne solution, surtout si vous avez un grand nombre de catégories . J'ai ajouté quelques points de repère dans ma réponse pour illustrer.