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.contains
peut ê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=False
pour 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 ValueError
s
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=False
d'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.escape
pour 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
Où p
ressemble à ça,
p
# '\\b(?:foo|baz)\\b'
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' in
opé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.search
inté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
En plus des str.contains
listes 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 str
mé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 query
et sur la eval
famille de méthodes peuvent être trouvées sur Dynamic Expression Evaluation in pandas en utilisant pd.eval () .
Priorité d'utilisation recommandée
- (Premièrement)
str.contains
, pour sa simplicité et sa facilité de gestion des NaN et des données mixtes
- Lister les compréhensions, pour ses performances (surtout si vos données sont purement des chaînes)
np.vectorize
- (Dernier)
df.query