web-dev-qa-db-fra.com

Sélectionner par chaîne partielle à partir d'un pandas DataFrame

J'ai un DataFrame avec 4 colonnes dont 2 contiennent des valeurs de chaîne. Je me demandais s'il y avait un moyen de sélectionner des lignes en fonction d'une correspondance partielle de chaîne avec 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) 

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

Est-ce que quelqu'un pourrait me diriger dans la bonne direction?

316
euforia

Basé sur le problème de github # 62 , il semble que vous puissiez bientôt faire ce qui suit:

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

Mise à jour: les méthodes de chaîne vectorisées (c'est-à-dire, Series.str) sont disponibles dans pandas 0.8.1 et versions ultérieures.

615
Garrett

J'utilise pandas 0.14.1 sur macos dans ipython notebook. J'ai essayé la ligne proposée ci-dessus:

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

et j'ai eu une erreur:

"cannot index with vector containing NA / NaN values"

mais cela fonctionnait parfaitement quand une condition "== True" était ajoutée, comme ceci:

df[df['A'].str.contains("Hello|Britain")==True]
152
sharon

Si quelqu'un se demande comment résoudre un problème connexe: "Sélection d'une colonne par une chaîne partielle"

Utilisation:

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

Et pour sélectionner les lignes par correspondance partielle, passez axis=0 pour filtrer:

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

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

Cet article est destiné aux lecteurs qui veulent

  • recherche d'une sous-chaîne dans une colonne de chaîne (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")
  • correspond à plusieurs mots entiers
  • Comprendre la raison derrière "ValueError: impossible d'indexer avec un vecteur contenant des valeurs NA/NaN"

... et voudrait 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 ç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 une recherche basée sur les expressions rationnelles. La recherche par défaut est basée sur regex sauf si vous la désactivez explicitement.

Voici un exemple de recherche basée sur regex,

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

      col
1  foobar
_

Parfois, la recherche par expression rationnelle n’est pas requise; spécifiez donc _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 regex est plus lente que la recherche 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 aura pour résultat

_ValueError: cannot index with vector containing NA / NaN values
_

C’est généralement à cause de données mélangées ou de NaN dans la colonne de votre 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)
_

Les méthodes de chaîne ne peuvent pas être appliquées sur tout ce qui n'est pas une chaîne. Le résultat est donc NaN (naturellement). Dans ce cas, spécifiez _na=False_ pour ignorer les données autres que des chaînes,

_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é par une recherche de regex utilisant le 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 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 ont des caractères qui peuvent ê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 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\\^'
_

Correspond à tout le mot (s)

Par défaut, la recherche de sous-chaîne recherche la sous-chaîne/le modèle spécifié, qu'il s'agisse de Word ou non. Pour ne faire correspondre que les mots entiers, nous devrons utiliser des expressions régulières ici. En particulier, notre modèle devra spécifier les limites du mot (_\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érons 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

Semblable à ce qui précède, sauf que nous ajoutons une limite de mot (_\b_) au modèle joint.

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

       col
0  foo abc
3   baz 45
_

p ressemble à ceci,

_p
# '\\b(?:foo|baz)\\b'
_

Une excellente alternative: Utilisez Liste Compréhensions !

Parce que vous pouvez! Et vous devriez! 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'opérateur in 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) + Pattern.search dans 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" a NaNs, 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 des chaînes: np.char.find , np.vectorize , DataFrame.query .

Outre _str.contains_ et la liste des compréhensions, vous pouvez également utiliser les alternatives suivantes.

np.char.find
Prend en charge les recherches de sous-chaînes (lire: pas de regex) 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 moins de surcharge que la plupart des méthodes pandas str.

_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 de 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 il est néanmoins utile de savoir si vous devez générer dynamiquement vos requêtes.

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

      col
0     foo
1  foobar
_

Pour plus d'informations sur les familles de méthodes query et eval, reportez-vous à la section Évaluation de l'expression dynamique dans pandas à l'aide de pd.eval () .


Priorité d'utilisation recommandée

  1. (First) _str.contains_, pour sa simplicité et sa facilité de traitement des NaN et des données mixtes
  2. Liste des compréhensions, pour ses performances (surtout si vos données sont purement des chaînes)
  3. _np.vectorize_
  4. (Last) _df.query_
33
cs95

Remarque rapide: si vous souhaitez effectuer une sélection en fonction d'une chaîne partielle contenue dans l'index, procédez comme suit:

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

Disons que vous avez le code DataFrame suivant:

>>> 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'opérateur in 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 ici à utiliser l'option axis=1 de la apply pour transmettre des éléments à la fonction lambda ligne par ligne, par opposition à colonne par colonne.

20
Mike

Voici ce que j'ai fini par faire pour des correspondances partielles de chaînes. Si quelqu'un a un moyen plus efficace de le faire, s'il vous plaît faites le moi 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
7
euforia

Il y a des réponses avant cela qui accomplissent la fonctionnalité demandée, de toute façon j'aimerais 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.

(De toute évidence, vous devez écrire l'expression de regex appropriée pour chaque cas)

0
xpeiro