Filtrar pandas DataFrame por criterios de subcadena
Tengo un DataFrame de pandas con una columna de valores de cadena. Necesito seleccionar filas en función de coincidencias de cadenas parciales.
Algo como este modismo:
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'
.
Los métodos de cadena vectorizados (es decir Series.str
, ) le permiten hacer lo siguiente:
df[df['A'].str.contains("hello")]
Esto está disponible en pandas 0.8.1 y versiones posteriores.
Estoy usando pandas 0.14.1 en macos en el cuaderno ipython. Probé la línea propuesta arriba:
df[df["A"].str.contains("Hello|Britain")]
y obtuve un error:
no se puede indexar con un vector que contenga valores NA/NaN
pero funcionó perfectamente cuando se agregó una condición "==Verdadero", como esta:
df[df['A'].str.contains("Hello|Britain")==True]
¿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) como en
df1[df1['col'].str.contains(r'foo(?!$)')]
- buscar múltiples subcadenas (similar a
isin
), por ejemplo, condf4[df4['col'].str.contains(r'foo|baz')]
- coincidir con una palabra completa del texto (por ejemplo, "azul" debe coincidir con "el cielo es azul" pero no con "arrendajo azul"), por ejemplo, con
df3[df3['col'].str.contains(r'\bblue\b')]
- unir varias palabras completas
- Comprenda el motivo detrás de "ValueError: no se puede indexar con un vector que contiene valores NA/NaN" y corríjalo con
str.contains('pattern',na=False)
...y me gustaría saber más sobre qué métodos deberían preferirse a otros.
(PD: he visto muchas preguntas sobre temas similares, pensé que sería bueno dejar esto aquí).
Descargo de responsabilidad amistoso , esta publicación es larga .
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.contains
se puede utilizar para realizar búsquedas de subcadenas o búsquedas basadas en expresiones regulares. La búsqueda se basa de forma predeterminada en expresiones regulares a menos que la deshabilite explícitamente.
A continuación se muestra 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 no se requiere la búsqueda de expresiones regulares, así que especifíquela regex=False
para deshabilitarla.
#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 utilizar la búsqueda basada en expresiones regulares si no la necesita.
Abordar ValueError
s
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 NaN en la columna de su 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)
A cualquier cosa que no sea una cadena no se le pueden aplicar métodos de cadena, por lo que el resultado es NaN (naturalmente). En este caso, especifique na=False
ignorar los 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
¿Cómo aplico esto a varias columnas a la vez?
La respuesta está en la pregunta. Usar 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
Todas las soluciones siguientes se pueden "aplicar" a varias columnas utilizando el método de columnas apply
(lo cual está bien en mi libro, siempre y cuando no tenga demasiadas columnas).
Si tiene un DataFrame con columnas mixtas y desea seleccionar solo las columnas de objeto/cadena, eche un vistazo a select_dtypes
.
Búsqueda de subcadenas múltiples
Esto se logra más fácilmente mediante 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 puedes crear una lista de términos y luego unirlos:
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 los términos en caso de que tengan caracteres que puedan interpretarse como metacaracteres de expresiones regulares . Si sus términos contienen alguno de los siguientes caracteres...
. ^ $ * + ? { } [ ] \ | ( )
Luego, necesitarás usar re.escape
para 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\\^'
Coincidencia de palabras enteras
De forma predeterminada, la búsqueda de subcadenas busca la subcadena/patrón especificado independientemente de si es una palabra completa o no. Para hacer coincidir solo palabras completas, necesitaremos usar expresiones regulares aquí; en particular, nuestro patrón deberá especificar límites de 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 múltiple de palabras completas
Similar a lo 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
¿ Dónde p
se ve así?
p
# '\\b(?:foo|baz)\\b'
Una gran alternativa: ¡use listas por comprensión !
¡Porque tú puedes! ¡Y deberías hacerlo! Generalmente 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 descabelladas.
En lugar de,
df1[df1['col'].str.contains('foo', regex=False)]
Utilice el in
operador dentro de una lista de compilación,
df1[['foo' in x for x in df1['col']]]
col
0 foo abc
1 foobar
En lugar de,
regex_pattern = r'foo(?!$)'
df1[df1['col'].str.contains(regex_pattern)]
Utilice re.compile
(para almacenar en caché su expresión regular) + Pattern.search
dentro de una lista de compilación,
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)]
Usar,
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 cadenas: np.char.find
, np.vectorize
, DataFrame.query
.
Además de str.contains
las listas por comprensión, también puede utilizar las siguientes alternativas.
np.char.find
Admite búsquedas de subcadenas (léase: sin expresiones regulares) únicamente.
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 menos gastos generales que la mayoría de los str
métodos de 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 de expresiones regulares:
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 Python. Esto no ofrece beneficios de rendimiento visibles, pero aun así es útil para saber si necesita generar sus consultas de forma dinámica.
df1.query('col.str.contains("foo")', engine='python')
col
0 foo
1 foobar
Puede encontrar más información sobre query
una familia de métodos en Evaluar dinámicamente una expresión a partir de una fórmula en Pandas .eval
Prioridad de uso recomendada
- (Primero)
str.contains
, por su sencillez y facilidad de manejo de NaNs y datos mixtos - Lista por comprensión, por su rendimiento (especialmente si sus datos son puramente cadenas)
np.vectorize
- (Último)
df.query