Comment sélectionner une chaîne partielle dans un DataFrame pandas ?
Ce billet est destiné aux lecteurs qui veulent
- recherche d'une sous-chaîne dans une colonne de chaînes de caractères (le cas le plus simple)
- recherche de sous-chaînes multiples (similaire à
isin
)
- correspondre à un mot entier du texte (par exemple, "bleu" devrait correspondre à "le ciel est bleu" mais pas à "geai bleu")
- associer plusieurs mots entiers
- Comprendre la raison derrière "ValueError : cannot index with vector containing NA / NaN values" (Erreur de valeur : ne peut pas indexer avec un vecteur contenant des valeurs NA / NaN)
...et aimerait en savoir plus sur les méthodes à privilégier par rapport aux autres.
(P.S. : J'ai vu beaucoup de questions sur des sujets similaires, j'ai pensé qu'il serait bon de laisser ceci ici).
Avis de non-responsabilité ce poste est long .
Recherche de base par sous-chaîne
# setup
df1 = pd.DataFrame({'col': ['foo', 'foobar', 'bar', 'baz']})
df1
col
0 foo
1 foobar
2 bar
3 baz
str.contains
peut être utilisé pour effectuer des recherches par substrats ou des recherches basées sur des expressions géographiques. Par défaut, la recherche est basée sur l'expression géographique, sauf si vous la désactivez explicitement.
Voici un exemple de recherche basée sur les 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 par regex n'est pas nécessaire, alors spécifiez regex=False
pour le 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 par expressions rationnelles est plus lente que la recherche par substrats :
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.
Adressez-vous à ValueError
s
Parfois, en effectuant une recherche par sous-chaîne et en filtrant sur le résultat, on obtient
ValueError: cannot index with vector containing NA / NaN values
Cela est généralement dû à des données mixtes ou à des NaN dans votre colonne d'objets,
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 de caractères ne peut pas se voir appliquer des méthodes de chaîne de caractères, le résultat est donc NaN (naturellement). Dans ce cas, il faut spécifier na=False
pour ignorer les données qui ne sont pas des chaînes de caractères,
s.str.contains('foo|bar', na=False)
0 True
1 True
2 False
3 True
4 False
5 False
dtype: bool
Comment puis-je appliquer cela à plusieurs colonnes à la fois ?
La réponse est dans la question. Utilisez DataFrame.apply
:
# `axis=1` tells `apply` to apply the lambda function column-wise.
df.apply(lambda col: col.str.contains('foo|bar', na=False), axis=1)
A B
0 True True
1 True False
2 False True
3 True False
4 False False
5 False False
Toutes les solutions ci-dessous peuvent être "appliquées" à plusieurs colonnes en utilisant la fonction "column-wise". apply
(ce qui n'est pas un problème à mon avis, tant que vous n'avez pas trop de colonnes).
Si vous avez un DataFrame avec des colonnes mixtes et que vous souhaitez sélectionner uniquement les colonnes objet/chaîne, consultez la rubrique select_dtypes
.
Recherche de sous-chaînes multiples
La méthode la plus simple consiste à effectuer une recherche regex à l'aide du tube regex OR.
# 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 joindre :
terms = ['foo', 'baz']
df4[df4['col'].str.contains('|'.join(terms))]
col
0 foo abc
1 foobar xyz
3 baz 45
Parfois, il est judicieux d'échapper vos termes au cas où ils comporteraient des caractères pouvant être interprétés comme métacaractères regex . Si vos termes contiennent l'un des caractères suivants...
. ^ $ * + ? { } [ ] \ | ( )
Ensuite, vous devrez utiliser re.escape
a s'échapper les :
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\\^'
Mot(s) entier(s) correspondant(s)
Par défaut, la recherche de sous-chaîne recherche la sous-chaîne/motif spécifié, qu'il s'agisse d'un mot complet ou non. Pour ne faire correspondre que des mots complets, nous devrons utiliser des expressions régulières. En particulier, notre motif devra spécifier les limites des 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
Maintenant, réfléchissez,
df3[df3['col'].str.contains('blue')]
col
0 the sky is blue
1 bluejay by the window
v/s
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 une limite de mot ( \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
Donde p
ressemble à ça,
p
# '\\b(?:foo|baz)\\b'
Parce que vous le pouvez ! Et vous devriez ! Elles sont généralement un peu plus rapides que les méthodes de type "string", car ces dernières sont difficiles à vectoriser et ont généralement des implémentations bouclées.
Au lieu de,
df1[df1['col'].str.contains('foo', regex=False)]
Utilisez le in
à l'intérieur d'une liste comp,
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) + Pattern.search
à l'intérieur d'une liste comp,
p = re.compile(regex_pattern, flags=re.IGNORECASE)
df1[[bool(p.search(x)) for x in df1['col']]]
col
1 foobar
Si "col" contient des NaN, alors au lieu de
df1[df1['col'].str.contains(regex_pattern, na=False)]
Utilisez,
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
En plus de str.contains
et les compréhensions de liste, vous pouvez également utiliser les alternatives suivantes.
np.char.find
Ne prend en charge que les recherches par substrats (lire : pas de regex).
df4[np.char.find(df4['col'].values.astype(str), 'foo') > -1]
col
0 foo abc
1 foobar xyz
np.vectorize
Il s'agit d'une enveloppe autour d'une boucle, mais avec moins de surcharge que la plupart des pandas. str
méthodes.
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
Supporte les méthodes de chaînes de caractères à travers le moteur python. Cela n'offre aucun avantage visible en termes de performances, mais il est néanmoins utile de le savoir si vous avez besoin de générer dynamiquement vos requêtes.
df1.query('col.str.contains("foo")', engine='python')
col
0 foo
1 foobar
Plus d'informations sur query
y eval
La famille de méthodes se trouve à l'adresse suivante Évaluation dynamique d'expressions dans pandas avec pd.eval() .
Priorité d'utilisation recommandée
- (Première)
str.contains
pour sa simplicité et sa facilité à gérer les NaN et les données mixtes.
- Les compréhensions de listes, pour leurs performances (surtout si vos données sont purement des chaînes de caractères).
np.vectorize
- (Dernière)
df.query