Sélectionner par chaîne partielle dans un DataFrame pandas


451

J'ai un DataFrameavec 4 colonnes dont 2 contiennent des valeurs de chaîne. Je me demandais s'il y avait un moyen de sélectionner des lignes sur la base d'une correspondance de chaîne partielle contre une colonne particulière?

En d'autres termes, une fonction ou une fonction lambda qui ferait quelque chose comme

re.search(pattern, cell_in_question) 

renvoyant un booléen. Je connais bien la syntaxe de df[df['A'] == "hello world"]mais je n'arrive pas à trouver un moyen de faire de même avec une correspondance de chaîne partielle 'hello'.

Quelqu'un pourrait-il m'orienter dans la bonne direction?

Réponses:


788

D'après le problème n ° 620 de github , il semble que vous pourrez bientôt faire ce qui suit:

df[df['A'].str.contains("hello")]

Mise à jour: les méthodes de chaîne vectorisées (ie, Series.str) sont disponibles dans les pandas 0.8.1 et plus.


1
Comment allons-nous sur "Bonjour" et "Grande-Bretagne" si je veux les trouver avec la condition "OU".
LonelySoul

56
Étant donné que les méthodes str. * Traitent le modèle d'entrée comme une expression régulière, vous pouvez utiliserdf[df['A'].str.contains("Hello|Britain")]
Garrett

7
Est-il possible de convertir .str.contains pour utiliser l' .query()API ?
zyxue


3
df[df['value'].astype(str).str.contains('1234.+')]pour filtrer les colonnes non de type chaîne.
François Leblanc

214

J'ai essayé la solution proposée ci-dessus:

df[df["A"].str.contains("Hello|Britain")]

et a obtenu une erreur:

ValueError: impossible de masquer avec un tableau contenant des valeurs NA / NaN

vous pouvez transformer les valeurs NA en False, comme ceci:

df[df["A"].str.contains("Hello|Britain", na=False)]

54
Ou vous pouvez faire: df [df ['A']. Str.contains ("Hello | Britain", na = False)]
joshlk

2
df[df['A'].astype(str).str.contains("Hello|Britain")]a aussi bien fonctionné
Nagabhushan SN

108

Comment sélectionner par chaîne partielle à partir d'un DataFrame pandas?

Ce message est destiné aux lecteurs qui souhaitent

  • rechercher une sous-chaîne dans une colonne de chaîne (le cas le plus simple)
  • rechercher plusieurs sous-chaînes (similaire à isin)
  • correspondre à un mot entier du texte (par exemple, "bleu" doit correspondre à "le ciel est bleu" mais pas "bluejay")
  • faire correspondre plusieurs mots entiers
  • Comprendre la raison de "ValueError: impossible d'indexer avec un vecteur contenant des valeurs NA / NaN"

... et j'aimerais en savoir plus sur les méthodes à privilégier par rapport aux autres.

(PS: j'ai vu beaucoup de questions sur des sujets similaires, je pensais que ce serait bien de laisser ça ici.)


Recherche de base de sous-chaîne

# setup
df1 = pd.DataFrame({'col': ['foo', 'foobar', 'bar', 'baz']})
df1

      col
0     foo
1  foobar
2     bar
3     baz

str.containspeut être utilisé pour effectuer des recherches de sous-chaîne ou des recherches basées sur des expressions rationnelles. La recherche par défaut est basée sur une expression rationnelle, sauf si vous la désactivez explicitement.

Voici un exemple de recherche basée sur des expressions rationnelles,

# find rows in `df1` which contain "foo" followed by something
df1[df1['col'].str.contains(r'foo(?!$)')]

      col
1  foobar

Parfois, la recherche d'expression régulière n'est pas requise, alors spécifiez-la regex=Falsepour la désactiver.

#select all rows containing "foo"
df1[df1['col'].str.contains('foo', regex=False)]
# same as df1[df1['col'].str.contains('foo')] but faster.

      col
0     foo
1  foobar

En termes de performances, la recherche d'expression régulière est plus lente que la recherche de sous-chaîne:

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

%timeit df2[df2['col'].str.contains('foo')]
%timeit df2[df2['col'].str.contains('foo', regex=False)]

6.31 ms ± 126 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.8 ms ± 241 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Évitez d'utiliser la recherche basée sur les expressions rationnelles si vous n'en avez pas besoin.

Adressage ValueErrors
Parfois, effectuer une recherche de sous-chaîne et filtrer le résultat entraînera

ValueError: cannot index with vector containing NA / NaN values

Cela est généralement dû à des données mixtes ou NaN dans votre colonne objet,

s = pd.Series(['foo', 'foobar', np.nan, 'bar', 'baz', 123])
s.str.contains('foo|bar')

0     True
1     True
2      NaN
3     True
4    False
5      NaN
dtype: object


s[s.str.contains('foo|bar')]
# ---------------------------------------------------------------------------
# ValueError                                Traceback (most recent call last)

Tout ce qui n'est pas une chaîne ne peut pas avoir de méthodes de chaîne appliquées, donc le résultat est NaN (naturellement). Dans ce cas, spécifiez na=Falsed'ignorer les données non-chaîne,

s.str.contains('foo|bar', na=False)

0     True
1     True
2    False
3     True
4    False
5    False
dtype: bool

Recherche de sous-chaînes multiples

Ceci est plus facilement réalisé grâce à une recherche d'expressions rationnelles à l'aide du tuyau OU d'expression régulière.

# Slightly modified example.
df4 = pd.DataFrame({'col': ['foo abc', 'foobar xyz', 'bar32', 'baz 45']})
df4

          col
0     foo abc
1  foobar xyz
2       bar32
3      baz 45

df4[df4['col'].str.contains(r'foo|baz')]

          col
0     foo abc
1  foobar xyz
3      baz 45

Vous pouvez également créer une liste de termes, puis les rejoindre:

terms = ['foo', 'baz']
df4[df4['col'].str.contains('|'.join(terms))]

          col
0     foo abc
1  foobar xyz
3      baz 45

Parfois, il est sage d'échapper à vos termes s'ils contiennent des caractères pouvant être interprétés comme des métacaractères d'expression régulière . Si vos termes contiennent l'un des caractères suivants ...

. ^ $ * + ? { } [ ] \ | ( )

Ensuite, vous devrez utiliser re.escapepour les échapper :

import re
df4[df4['col'].str.contains('|'.join(map(re.escape, terms)))]

          col
0     foo abc
1  foobar xyz
3      baz 45

re.escape a pour effet d'échapper les caractères spéciaux afin qu'ils soient traités littéralement.

re.escape(r'.foo^')
# '\\.foo\\^'

Correspondance de mot (s) entier (s)

Par défaut, la recherche de sous-chaîne recherche la sous-chaîne / le modèle spécifié, qu'il s'agisse d'un mot complet ou non. Pour ne faire correspondre que des mots entiers, nous devrons utiliser des expressions régulières ici - en particulier, notre modèle devra spécifier des limites de mots ( \b).

Par exemple,

df3 = pd.DataFrame({'col': ['the sky is blue', 'bluejay by the window']})
df3

                     col
0        the sky is blue
1  bluejay by the window

Considérez maintenant,

df3[df3['col'].str.contains('blue')]

                     col
0        the sky is blue
1  bluejay by the window

contre

df3[df3['col'].str.contains(r'\bblue\b')]

               col
0  the sky is blue

Recherche de mots entiers multiples

Similaire à ce qui précède, sauf que nous ajoutons un mot limite ( \b) au motif joint.

p = r'\b(?:{})\b'.format('|'.join(map(re.escape, terms)))
df4[df4['col'].str.contains(p)]

       col
0  foo abc
3   baz 45

pressemble à ça,

p
# '\\b(?:foo|baz)\\b'

Une excellente alternative: utilisez les listes de compréhension !

Parce que vous pouvez! Et tu devrais! Elles sont généralement un peu plus rapides que les méthodes de chaîne, car les méthodes de chaîne sont difficiles à vectoriser et ont généralement des implémentations en boucle.

Au lieu de,

df1[df1['col'].str.contains('foo', regex=False)]

Utilisez l' inopérateur dans une liste de composition,

df1[['foo' in x for x in df1['col']]]

       col
0  foo abc
1   foobar

Au lieu de,

regex_pattern = r'foo(?!$)'
df1[df1['col'].str.contains(regex_pattern)]

Utilisez re.compile(pour mettre en cache votre regex) + à l' Pattern.searchintérieur d'une liste de composition,

p = re.compile(regex_pattern, flags=re.IGNORECASE)
df1[[bool(p.search(x)) for x in df1['col']]]

      col
1  foobar

Si "col" a NaN, alors au lieu de

df1[df1['col'].str.contains(regex_pattern, na=False)]

Utilisation,

def try_search(p, x):
    try:
        return bool(p.search(x))
    except TypeError:
        return False

p = re.compile(regex_pattern)
df1[[try_search(p, x) for x in df1['col']]]

      col
1  foobar

Plus d' options pour la correspondance partielle de chaîne: np.char.find, np.vectorize, DataFrame.query.

En plus des str.containslistes et des listes, vous pouvez également utiliser les alternatives suivantes.

np.char.find
Prend en charge les recherches de sous-chaîne (lecture: aucune expression régulière) uniquement.

df4[np.char.find(df4['col'].values.astype(str), 'foo') > -1]

          col
0     foo abc
1  foobar xyz

np.vectorize
Il s'agit d'un wrapper autour d'une boucle, mais avec une charge moindre que la plupart des strméthodes pandas .

f = np.vectorize(lambda haystack, needle: needle in haystack)
f(df1['col'], 'foo')
# array([ True,  True, False, False])

df1[f(df1['col'], 'foo')]

       col
0  foo abc
1   foobar

Solutions Regex possibles:

regex_pattern = r'foo(?!$)'
p = re.compile(regex_pattern)
f = np.vectorize(lambda x: pd.notna(x) and bool(p.search(x)))
df1[f(df1['col'])]

      col
1  foobar

DataFrame.query
Prend en charge les méthodes de chaîne via le moteur python. Cela n'offre aucun avantage visible en termes de performances, mais est néanmoins utile pour savoir si vous devez générer dynamiquement vos requêtes.

df1.query('col.str.contains("foo")', engine='python')

      col
0     foo
1  foobar

Plus d'informations sur queryet sur la evalfamille de méthodes peuvent être trouvées sur Dynamic Expression Evaluation in pandas en utilisant pd.eval () .


Priorité d'utilisation recommandée

  1. (Premièrement) str.contains, pour sa simplicité et sa facilité de gestion des NaN et des données mixtes
  2. Lister les compréhensions, pour ses performances (surtout si vos données sont purement des chaînes)
  3. np.vectorize
  4. (Dernier) df.query

Pourriez-vous modifier la méthode correcte à utiliser lors de la recherche d'une chaîne dans deux colonnes ou plus? Fondamentalement: any(needle in haystack for needling in ['foo', 'bar'] and haystack in (df['col'], df['col2']))et les variantes que j'ai essayées sont toutes étranglées (ça se plaint any()et à juste titre donc ... Mais le doc n'est pas du tout clair sur la façon de faire une telle requête.
Denis de Bernardy

@DenisdeBernardydf[['col1', 'col2']].apply(lambda x: x.str.contains('foo|bar')).any(axis=1)
cs95

@ cs95 Extraction de lignes avec une sous-chaîne contenant des espaces après + dans pandas df Il a été répondu bientôt, mais vous voudrez peut-être y jeter un œil.
ankii

@ankiiiiiii On dirait que vous avez manqué la partie de ma réponse où j'ai mentionné les métacaractères regex: "Parfois, il est sage d'échapper à vos termes au cas où ils auraient des caractères pouvant être interprétés comme des métacaractères regex".
cs95

1
@ 00schneider r dans ce cas est utilisé pour indiquer un littéral de chaîne brut. Celles-ci facilitent l'écriture de chaînes d'expression régulière. stackoverflow.com/q/2081640
cs95

53

Si quelqu'un se demande comment effectuer un problème connexe: "Sélectionner la colonne par chaîne partielle"

Utilisation:

df.filter(like='hello')  # select columns which contain the word hello

Et pour sélectionner des lignes par correspondance de chaîne partielle, passez axis=0au filtre:

# selects rows which contain the word hello in their index label
df.filter(like='hello', axis=0)  

6
Cela peut être distillé à:df.loc[:, df.columns.str.contains('a')]
elPastor

18
qui peut être distillé plus loindf.filter(like='a')
Ted Petrou

cela devrait être une propre question + réponse, déjà 50 personnes l'ont recherchée ...
PV8

1
@ La question PV8 existe déjà: stackoverflow.com/questions/31551412/… . Mais quand je recherche sur google pour "pandas Select column by partial string", ce fil apparaît en premier
Philipp Schwarz

28

Remarque rapide: si vous souhaitez effectuer une sélection basée sur une chaîne partielle contenue dans l'index, essayez ce qui suit:

df['stridx']=df.index
df[df['stridx'].str.contains("Hello|Britain")]

5
Vous pouvez simplement df [df.index.to_series (). Str.contains ('LLChit')]
Yury Bayda

21

Disons que vous disposez des éléments suivants DataFrame:

>>> df = pd.DataFrame([['hello', 'hello world'], ['abcd', 'defg']], columns=['a','b'])
>>> df
       a            b
0  hello  hello world
1   abcd         defg

Vous pouvez toujours utiliser l' inopérateur dans une expression lambda pour créer votre filtre.

>>> df.apply(lambda x: x['a'] in x['b'], axis=1)
0     True
1    False
dtype: bool

L'astuce consiste à utiliser l' axis=1option dans le applypour passer des éléments à la fonction lambda ligne par ligne, par opposition à colonne par colonne.


Comment modifier ci-dessus pour dire que x ['a'] n'existe qu'au début de x ['b']?
ComplexData

1
appliquer est une mauvaise idée ici en termes de performances et de mémoire. Voir cette réponse .
cs95

8

Voici ce que j'ai fini par faire pour les correspondances de chaînes partielles. Si quelqu'un a un moyen plus efficace de le faire, veuillez me le faire savoir.

def stringSearchColumn_DataFrame(df, colName, regex):
    newdf = DataFrame()
    for idx, record in df[colName].iteritems():

        if re.search(regex, record):
            newdf = concat([df[df[colName] == record], newdf], ignore_index=True)

    return newdf

3
Devrait être 2x à 3x plus rapide si vous compilez regex avant la boucle: regex = re.compile (regex) puis si regex.search (record)
MarkokraM

1
@MarkokraM docs.python.org/3.6/library/re.html#re.compile indique que les expressions rationnelles les plus récentes sont mises en cache pour vous, vous n'avez donc pas besoin de vous compiler.
Teepeemm

N'utilisez pas d'itérations pour parcourir un DataFrame. Il se classe dernier en termes de pandorabilité et de performances
cs95

5

L'utilisation de contient n'a pas bien fonctionné pour ma chaîne avec des caractères spéciaux. Mais ça a marché.

df[df['A'].str.find("hello") != -1]

3

Si vous devez effectuer une recherche insensible à la casse pour une chaîne dans une colonne de trame de données pandas:

df[df['A'].str.contains("hello", case=False)]

2

Il y a des réponses avant cela qui accomplissent la fonctionnalité demandée, de toute façon je voudrais montrer la manière la plus générale:

df.filter(regex=".*STRING_YOU_LOOK_FOR.*")

De cette façon, vous obtenez la colonne que vous recherchez, quelle que soit la façon dont elle est écrite.

(Évidemment, vous devez écrire l'expression regex appropriée pour chaque cas)


1
Cela filtre sur les en- têtes de colonne . Ce n'est pas général, c'est incorrect.
cs95

@MicheldeRuiter qui est toujours incorrect, qui filtrerait sur les étiquettes d'index à la place!
cs95

Ne répond pas à la question. Mais j'ai appris quelque chose. :)
Michel de Ruiter

2

Peut-être que vous souhaitez rechercher du texte dans toutes les colonnes de la trame de données Pandas, et pas seulement dans leur sous-ensemble. Dans ce cas, le code suivant vous aidera.

df[df.apply(lambda row: row.astype(str).str.contains('String To Find').any(), axis=1)]

Attention. Cette méthode est relativement lente, bien que pratique.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.