Como faço para selecionar por string parcial de um DataFrame do pandas?
Este post é destinado a leitores que desejam
- procure uma substring em uma coluna de string (o caso mais simples)
- procure várias substrings (semelhante a
isin
)
- corresponde a uma palavra inteira do texto (por exemplo, "azul" deve corresponder a "o céu é azul", mas não "bluejay")
- corresponder várias palavras inteiras
- Entenda o motivo por trás de "ValueError: não é possível indexar com vetor contendo valores NA / NaN"
... e gostaria de saber mais sobre quais métodos devem ser preferidos a outros.
(PS: Eu já vi muitas perguntas sobre tópicos semelhantes, achei que seria bom deixar isso aqui.)
Pesquisa Básica de Substring
# setup
df1 = pd.DataFrame({'col': ['foo', 'foobar', 'bar', 'baz']})
df1
col
0 foo
1 foobar
2 bar
3 baz
str.contains
pode ser usado para executar pesquisas de substring ou pesquisa baseada em regex. O padrão da pesquisa é baseado em regex, a menos que você a desative explicitamente.
Aqui está um exemplo de pesquisa baseada em regex,
# find rows in `df1` which contain "foo" followed by something
df1[df1['col'].str.contains(r'foo(?!$)')]
col
1 foobar
Às vezes, a pesquisa regex não é necessária, então especifique regex=False
para desativá-la.
#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
Em termos de desempenho, a pesquisa regex é mais lenta que a pesquisa de substring:
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 a pesquisa baseada em regex se você não precisar.
Endereçamento ValueError
s
Às vezes, realizar uma pesquisa de substring e filtrar o resultado resultará em
ValueError: cannot index with vector containing NA / NaN values
Isso geralmente ocorre devido a dados mistos ou NaNs na coluna do 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)
Qualquer coisa que não seja uma sequência não pode ter métodos de sequência aplicados, portanto o resultado é NaN (naturalmente). Nesse caso, especifique na=False
para ignorar dados que não sejam de sequência,
s.str.contains('foo|bar', na=False)
0 True
1 True
2 False
3 True
4 False
5 False
dtype: bool
Pesquisa por várias seqüências de caracteres
Isso é mais facilmente alcançado por meio de uma pesquisa de regex usando o canal 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
Você também pode criar uma lista de termos e juntá-los:
terms = ['foo', 'baz']
df4[df4['col'].str.contains('|'.join(terms))]
col
0 foo abc
1 foobar xyz
3 baz 45
Às vezes, é aconselhável escapar dos seus termos, caso eles tenham caracteres que possam ser interpretados como metacaracteres de expressões regulares . Se seus termos contiverem um dos seguintes caracteres ...
. ^ $ * + ? { } [ ] \ | ( )
Então, você precisará usar re.escape
para escapar deles:
import re
df4[df4['col'].str.contains('|'.join(map(re.escape, terms)))]
col
0 foo abc
1 foobar xyz
3 baz 45
re.escape
tem o efeito de escapar dos caracteres especiais para serem tratados literalmente.
re.escape(r'.foo^')
# '\\.foo\\^'
Correspondência de palavras inteiras
Por padrão, a pesquisa de substring procura o substring / padrão especificado, independentemente de ser palavra completa ou não. Para combinar apenas palavras completas, precisaremos usar expressões regulares aqui - em particular, nosso padrão precisará especificar limites de palavras ( \b
).
Por exemplo,
df3 = pd.DataFrame({'col': ['the sky is blue', 'bluejay by the window']})
df3
col
0 the sky is blue
1 bluejay by the window
Agora 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
Pesquisa de Múltiplas Palavras Inteiras
Semelhante ao acima, exceto que adicionamos um limite de palavra ( \b
) ao padrão unido.
p = r'\b(?:{})\b'.format('|'.join(map(re.escape, terms)))
df4[df4['col'].str.contains(p)]
col
0 foo abc
3 baz 45
Onde p
fica assim,
p
# '\\b(?:foo|baz)\\b'
Porque você pode! E você deveria! Eles geralmente são um pouco mais rápidos que os métodos de string, porque os métodos de string são difíceis de vetorizar e geralmente têm implementações em loop.
Ao invés de,
df1[df1['col'].str.contains('foo', regex=False)]
Use o in
operador dentro de uma lista comp,
df1[['foo' in x for x in df1['col']]]
col
0 foo abc
1 foobar
Ao invés de,
regex_pattern = r'foo(?!$)'
df1[df1['col'].str.contains(regex_pattern)]
Use re.compile
(para armazenar em cache seu regex) + Pattern.search
dentro de uma lista de composição,
p = re.compile(regex_pattern, flags=re.IGNORECASE)
df1[[bool(p.search(x)) for x in df1['col']]]
col
1 foobar
Se "col" tiver NaNs, em vez 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
Além de str.contains
e listar compreensões, você também pode usar as seguintes alternativas.
np.char.find
Suporta apenas pesquisas de substring (leia-se: sem regex).
df4[np.char.find(df4['col'].values.astype(str), 'foo') > -1]
col
0 foo abc
1 foobar xyz
np.vectorize
Este é um invólucro em torno de um loop, mas com menos sobrecarga do que a maioria dos 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
Soluções Regex possíveis:
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
Suporta métodos de string por meio do mecanismo python. Isso não oferece benefícios visíveis de desempenho, mas é útil para saber se você precisa gerar dinamicamente suas consultas.
df1.query('col.str.contains("foo")', engine='python')
col
0 foo
1 foobar
Mais informações query
e eval
família de métodos podem ser encontradas em Avaliação de Expressão Dinâmica em pandas usando pd.eval () .
Precedência de uso recomendada
- (Primeiro)
str.contains
, por sua simplicidade e facilidade de manipulação de NaNs e dados mistos
- Listagens de compreensão, por seu desempenho (especialmente se seus dados são puramente sequências de caracteres)
np.vectorize
- (Último)
df.query