¿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.contains
se 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=False
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 usar la búsqueda basada en expresiones regulares si no la necesita.
Direccionamiento 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 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=False
ignorar 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.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\\^'
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 p
ve así,
p
# '\\b(?:foo|baz)\\b'
¡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 in
operador 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.search
dentro 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
Además de una str.contains
lista 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 str
mé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 query
y una eval
familia de métodos en Evaluación de expresión dinámica en pandas usando pd.eval () .
Precedencia de uso recomendada
- (Primero)
str.contains
, por su simplicidad y facilidad de manejo de NaNs y datos mixtos
- Enumere las comprensiones, por su rendimiento (especialmente si sus datos son puramente cadenas)
np.vectorize
- (Último)
df.query