Seleccione por cadena parcial de un marco de datos de pandas

448

Tengo un DataFramecon 4 columnas de las cuales 2 contienen valores de cadena. Me preguntaba si había una manera de seleccionar filas en función de una coincidencia de cadena parcial contra una columna en particular.

En otras palabras, una función o función lambda que haría algo como

re.search(pattern, cell_in_question) 

devolviendo un booleano. Estoy familiarizado con la sintaxis de, df[df['A'] == "hello world"]pero parece que no puedo encontrar una manera de hacer lo mismo con una coincidencia de cadena parcial, por ejemplo 'hello'.

¿Alguien podría señalarme en la dirección correcta?

euforia
fuente

Respuestas:

786

Según el problema 620 de github , parece que pronto podrá hacer lo siguiente:

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

Actualización: los métodos de cadena vectorizados (es decir, Series.str) están disponibles en pandas 0.8.1 y posteriores.

Garrett
fuente
1
¿Cómo hacemos para "Hola" y "Gran Bretaña" si quiero encontrarlos con la condición "O"?
LonelySoul
56
Como los métodos str. * Tratan el patrón de entrada como una expresión regular, puede usardf[df['A'].str.contains("Hello|Britain")]
Garrett
77
¿Es posible convertir .str.containspara usar .query()api ?
zyxue
3
df[df['value'].astype(str).str.contains('1234.+')]para filtrar columnas que no son de tipo cadena.
François Leblanc
213

Probé la solución propuesta arriba:

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

y recibí un error:

ValueError: no se puede enmascarar con una matriz que contiene valores NA / NaN

puedes transformar los valores de NA en False, así:

df[df["A"].str.contains("Hello|Britain", na=False)]
sharon
fuente
54
O puede hacer: df [df ['A']. Str.contains ("Hola | Gran Bretaña", na = False)]
joshlk
2
df[df['A'].astype(str).str.contains("Hello|Britain")]funcionó bien
Nagabhushan SN
108

¿Cómo selecciono por cadena parcial de un DataFrame de pandas?

Esta publicación está destinada a lectores que quieran

  • buscar una subcadena en una columna de cadena (el caso más simple)
  • buscar múltiples subcadenas (similar a isin)
  • coincide con una palabra completa del texto (por ejemplo, "azul" debe coincidir con "el cielo es azul" pero no con "bluejay")
  • unir varias palabras enteras
  • Comprenda la razón detrás de "ValueError: no se puede indexar con un vector que contiene valores NA / NaN"

... y me gustaría saber más sobre qué métodos deberían preferirse sobre otros.

(PD: he visto muchas preguntas sobre temas similares, pensé que sería bueno dejar esto aquí).


Búsqueda básica de subcadenas

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

      col
0     foo
1  foobar
2     bar
3     baz

str.containsse puede usar para realizar búsquedas de subcadenas o búsquedas basadas en expresiones regulares. La búsqueda por defecto es basada en expresiones regulares a menos que la desactive explícitamente.

Aquí hay un ejemplo de búsqueda basada en expresiones regulares,

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

      col
1  foobar

A veces, la búsqueda de expresiones regulares no es necesaria, así que especifique regex=Falsedeshabilitarla.

#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 cuanto al rendimiento, la búsqueda de expresiones regulares es más lenta que la búsqueda de subcadenas:

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)

Evite usar la búsqueda basada en expresiones regulares si no la necesita.

Direccionamiento ValueErrors
A veces, realizar una búsqueda de subcadenas y filtrar el resultado dará como resultado

ValueError: cannot index with vector containing NA / NaN values

Esto generalmente se debe a datos mixtos o NaNs en su columna de objeto,

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)

Cualquier cosa que no sea una cadena no puede tener métodos de cadena aplicados, por lo que el resultado es NaN (naturalmente). En este caso, especifique na=Falseignorar datos que no sean cadenas,

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

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

Búsqueda de subcadenas múltiples

Esto se logra más fácilmente a través de una búsqueda de expresiones regulares utilizando la tubería OR de expresiones regulares.

# 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

También puede crear una lista de términos, luego unirse a ellos:

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

          col
0     foo abc
1  foobar xyz
3      baz 45

A veces, es aconsejable escapar de sus términos en caso de que tengan caracteres que puedan interpretarse como metacaracteres regex . Si sus términos contienen alguno de los siguientes caracteres ...

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

Luego, deberás usar re.escapepara escapar de ellos:

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

          col
0     foo abc
1  foobar xyz
3      baz 45

re.escape tiene el efecto de escapar de los caracteres especiales para que sean tratados literalmente.

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

Palabra (s) completa (s)

De forma predeterminada, la búsqueda de subcadena busca la subcadena / patrón especificado, independientemente de si es palabra completa o no. Para que solo coincidan las palabras completas, tendremos que utilizar expresiones regulares aquí, en particular, nuestro patrón deberá especificar los límites de las palabras ( \b).

Por ejemplo,

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

                     col
0        the sky is blue
1  bluejay by the window

Ahora considere

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

Búsqueda de palabras completas múltiples

Similar al anterior, excepto que agregamos un límite de palabra ( \b) al patrón unido.

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

       col
0  foo abc
3   baz 45

Donde se pve así,

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

Una gran alternativa: ¡Usar las comprensiones de la lista !

¡Porque tú puedes! ¡Y deberías! Por lo general, son un poco más rápidos que los métodos de cadena, porque los métodos de cadena son difíciles de vectorizar y generalmente tienen implementaciones en bucle.

En vez de,

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

Utilice el inoperador dentro de una lista de comp.

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

       col
0  foo abc
1   foobar

En vez de,

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

Use re.compile(para almacenar en caché su expresión regular) + Pattern.searchdentro de una lista de comp,

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

      col
1  foobar

Si "col" tiene NaN, entonces en lugar de

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

Utilizar,

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

Más opciones para la coincidencia parcial de la secuencia: np.char.find, np.vectorize, DataFrame.query.

Además de una str.containslista de comprensiones, también puede usar las siguientes alternativas.

np.char.find
Solo admite búsquedas de subcadenas (lectura: sin expresiones regulares).

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

          col
0     foo abc
1  foobar xyz

np.vectorize
Este es un contenedor alrededor de un bucle, pero con una sobrecarga menor que la mayoría de los strmétodos 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

Posibles soluciones Regex:

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
Admite métodos de cadena a través del motor de Python. Esto no ofrece beneficios de rendimiento visibles, pero es útil saber si necesita generar dinámicamente sus consultas.

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

      col
0     foo
1  foobar

Se puede encontrar más información queryy una evalfamilia de métodos en Evaluación de expresión dinámica en pandas usando pd.eval () .


Precedencia de uso recomendada

  1. (Primero) str.contains, por su simplicidad y facilidad de manejo de NaNs y datos mixtos
  2. Enumere las comprensiones, por su rendimiento (especialmente si sus datos son puramente cadenas)
  3. np.vectorize
  4. (Último) df.query
cs95
fuente
¿Podría editar en el método correcto para usar al buscar una cadena en dos o más columnas? Básicamente: any(needle in haystack for needling in ['foo', 'bar'] and haystack in (df['col'], df['col2']))y las variaciones que probé con todos los estranguladores (se queja any()y con razón ... Pero el documento no es muy claro sobre cómo hacer esa consulta.)
Denis de Bernardy
@DenisdeBernardydf[['col1', 'col2']].apply(lambda x: x.str.contains('foo|bar')).any(axis=1)
cs95
@ cs95 Extrayendo filas con una subcadena que contiene espacios en blanco después de + en pandas df Se respondió pronto, pero es posible que desee echarle un vistazo.
ankii
@ankiiiiiii Parece que te perdiste la parte de mi respuesta donde mencioné metacaracteres regex: "A veces, es aconsejable escapar de tus términos en caso de que tengan caracteres que puedan interpretarse como metacaracteres regex".
cs95
1
@ 00schneider r en este caso se utiliza para indicar un literal de cadena sin formato. Esto facilita la escritura de cadenas de expresiones regulares. stackoverflow.com/q/2081640
cs95
53

Si alguien se pregunta cómo realizar un problema relacionado: "Seleccionar columna por cadena parcial"

Utilizar:

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

Y para seleccionar filas por coincidencia parcial de cadenas, pase axis=0al filtro:

# selects rows which contain the word hello in their index label
df.filter(like='hello', axis=0)  
Philipp Schwarz
fuente
66
Esto se puede destilar a:df.loc[:, df.columns.str.contains('a')]
elPastor
18
que se puede destilar aún másdf.filter(like='a')
Ted Petrou
esto debería ser una pregunta + respuesta propia, ya lo buscaron 50 personas ...
PV8
1
La pregunta @ PV8 ya existe: stackoverflow.com/questions/31551412/… . Pero cuando busco en Google "pandas Seleccionar columna por cadena parcial", este hilo aparece primero
Philipp Schwarz
28

Nota rápida: si desea hacer una selección basada en una cadena parcial contenida en el índice, intente lo siguiente:

df['stridx']=df.index
df[df['stridx'].str.contains("Hello|Britain")]
cristiano
fuente
55
Puede simplemente df [df.index.to_series (). Str.contains ('LLChit')]
Yury Bayda
21

Digamos que tiene lo siguiente DataFrame:

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

Siempre puede usar el inoperador en una expresión lambda para crear su filtro.

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

El truco aquí es usar la axis=1opción applypara pasar elementos a la función lambda fila por fila, en lugar de columna por columna.

Miguel
fuente
¿Cómo modifico arriba para decir que x ['a'] existe solo al comienzo de x ['b']?
ComplexData
1
aplicar es una mala idea aquí en términos de rendimiento y memoria. Mira esta respuesta .
cs95
8

Esto es lo que terminé haciendo para coincidencias parciales de cadenas. Si alguien tiene una forma más eficiente de hacerlo, hágamelo saber.

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
euforia
fuente
3
Debería ser 2x a 3x más rápido si compila regex antes del ciclo: regex = re.compile (regex) y luego si regex.search (record)
MarkokraM
1
@MarkokraM docs.python.org/3.6/library/re.html#re.compile dice que las expresiones regulares más recientes están almacenadas en caché para usted, por lo que no necesita compilarse.
Teepeemm
No use iteritems para iterar sobre un DataFrame. Ocupa el último lugar en términos de capacidad de pago y rendimiento
cs95
5

El uso de contiene no funcionó bien para mi cadena con caracteres especiales. Encontrar funcionó sin embargo.

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

Hay respuestas antes de esto que cumplen la función solicitada, de todos modos me gustaría mostrar la forma más general:

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

De esta forma, le permite obtener la columna que busca, independientemente de la forma en que se escriba.

(Obviamente, debe escribir la expresión de expresión regular adecuada para cada caso)

xpeiro
fuente
1
Esto se filtra en los encabezados de columna . No es general, es incorrecto.
cs95
¡@MicheldeRuiter sigue siendo incorrecto, en su lugar se filtraría en las etiquetas de índice!
cs95
No responde la pregunta. Pero aprendí algo. :)
Michel de Ruiter
2

Tal vez desee buscar texto en todas las columnas del marco de datos de Pandas, y no solo en el subconjunto de ellas. En este caso, el siguiente código será de ayuda.

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

Advertencia. Este método es relativamente lento, aunque conveniente.

Serhii Kushchenko
fuente
2

Si necesita hacer una búsqueda sin distinción entre mayúsculas y minúsculas para una cadena en una columna de marco de datos de pandas:

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