Excluir linhas de um DataFrame do pandas com base em uma expressão condicional envolvendo len (string) fornecendo KeyError

303

Eu tenho um DataFrame do pandas e quero excluir linhas dele onde o comprimento da string em uma coluna específica é maior que 2.

Espero poder fazer isso (por esta resposta ):

df[(len(df['column name']) < 2)]

mas acabei de receber o erro:

KeyError: u'no item named False'

O que estou fazendo de errado?

(Nota: eu sei que posso usar df.dropna()para me livrar de linhas que contenham alguma NaN, mas não vi como remover linhas com base em uma expressão condicional.)

sjs
fonte

Respostas:

168

Quando você len(df['column name'])obtém apenas um número, ou seja, o número de linhas no DataFrame (ou seja, o comprimento da própria coluna). Se você deseja aplicar lena cada elemento da coluna, use df['column name'].map(len). Então tente

df[df['column name'].map(len) < 2]
BrenBarn
fonte
3
Eu criei uma maneira de entender a lista: df[[(len(x) < 2) for x in df['column name']]]mas a sua é muito melhor. Obrigado pela ajuda!
SJS
13
Caso alguém precise de uma comparação mais complexa, um lambda sempre pode ser usado. df[df['column name'].map(lambda x: str(x)!=".")]
4lberto
1
Por alguma razão, nenhuma das outras opções funcionou para mim, exceto a postada por @ 4lberto. Eu estou no pandas 0.23.4e python 3.6
goelakash
1
Eu adicionaria um .copy()no final, caso você queira editar posteriormente esse dataframe (por exemplo, atribuir novas colunas aumentaria o aviso "Um valor está tentando ser definido em uma cópia de uma fatia de um DataFrame").
PlasmaBinturong
807

Para responder diretamente ao título original desta pergunta "Como excluir linhas de um DataFrame do pandas com base em uma expressão condicional" (que eu entendo não é necessariamente o problema do OP, mas pode ajudar outros usuários a encontrar essa pergunta), uma maneira de fazer isso é usar o método drop :

df = df.drop(some labels)

df = df.drop(df[<some boolean condition>].index)

Exemplo

Para remover todas as linhas em que a coluna 'score' é <50:

df = df.drop(df[df.score < 50].index)

Versão no local (conforme indicado nos comentários)

df.drop(df[df.score < 50].index, inplace=True)

Várias condições

(consulte Indexação booleana )

Os operadores são: |para or, &para ande ~para not. Estes devem ser agrupados usando parênteses.

Para remover todas as linhas em que a coluna 'score' é <50 e> 20

df = df.drop(df[(df.score < 50) & (df.score > 20)].index)

Do utilizador
fonte
32
Eu só quero observar que a função drop suporta substituição no local. Ou seja, sua solução é a mesma que df.drop (df [df.score <50] .index, inplace = True). No entanto, não conhecia o truque do "índice". Me ajudou muito
Quickbeam2k1
9
Só quero ressaltar que antes de usar esse truque de índice, você precisa ter certeza de que seus valores de índice são únicos (ou chamados reset_index()). Descobri isso da maneira mais difícil quando o caminho para muitas linhas foi retirado do meu quadro de dados.
Jay
3
como eu largo todas as linhas onde o tipo de coluna é str? Eu quero manter apenas os tipos de coluna da lista. Eu tentei test = df.drop(df[df['col1'].dtype == str].index), mas eu recebo o erro KeyError: False Eu também tentei df.drop(df[df.col1.dtype == str].index)e df.drop(df[type(df.cleaned_norm_email) == str].index), mas nada parece funcionar? Alguém pode aconselhar. Obrigado! @User
PyRsquared
1
Esta é uma pergunta antiga, mas ... @ o peixe com desafios aquáticos é muito mais rápido do que este. Observe que você calcula df[(df.score < 50) & (df.score > 20)]como parte de sua resposta. Se você revertesse isso df = df[(df.score >= 50) | (df.score <= 20)], obteria sua resposta muito mais rapidamente.
Roobie Nuby 26/03
1
@ RoobieNuby - eles não são a mesma condição.
Nguai al
106

Você pode atribuir a DataFrameuma versão filtrada de si mesma:

df = df[df.score > 50]

Isso é mais rápido que drop:

%%timeit
test = pd.DataFrame({'x': np.random.randn(int(1e6))})
test = test[test.x < 0]
# 54.5 ms ± 2.02 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit
test = pd.DataFrame({'x': np.random.randn(int(1e6))})
test.drop(test[test.x > 0].index, inplace=True)
# 201 ms ± 17.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit
test = pd.DataFrame({'x': np.random.randn(int(1e6))})
test = test.drop(test[test.x > 0].index)
# 194 ms ± 7.03 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Kabard
fonte
Como verifico várias colunas usando ou condição?
Piyush S. Wanare
9

Expandirei a solução genérica do @ User para fornecer uma dropalternativa gratuita. Isto é para pessoas direcionadas aqui com base no título da pergunta (não no problema do OP)

Digamos que você queira excluir todas as linhas com valores negativos. Uma solução de revestimento é: -

df = df[(df > 0).all(axis=1)]

Explicação passo a passo: -

Vamos gerar um quadro de dados de distribuição normal aleatória 5x5

np.random.seed(0)
df = pd.DataFrame(np.random.randn(5,5), columns=list('ABCDE'))
      A         B         C         D         E
0  1.764052  0.400157  0.978738  2.240893  1.867558
1 -0.977278  0.950088 -0.151357 -0.103219  0.410599
2  0.144044  1.454274  0.761038  0.121675  0.443863
3  0.333674  1.494079 -0.205158  0.313068 -0.854096
4 -2.552990  0.653619  0.864436 -0.742165  2.269755

Deixe a condição excluir negativos. Um DF booleano que satisfaça a condição: -

df > 0
      A     B      C      D      E
0   True  True   True   True   True
1  False  True  False  False   True
2   True  True   True   True   True
3   True  True  False   True  False
4  False  True   True  False   True

Uma série booleana para todas as linhas que satisfazem a condição Observe que, se algum elemento da linha falhar, a condição é marcada como falsa

(df > 0).all(axis=1)
0     True
1    False
2     True
3    False
4    False
dtype: bool

Por fim, filtre as linhas do quadro de dados com base na condição

df[(df > 0).all(axis=1)]
      A         B         C         D         E
0  1.764052  0.400157  0.978738  2.240893  1.867558
2  0.144044  1.454274  0.761038  0.121675  0.443863

Você pode atribuí-lo novamente ao df para realmente excluir vs filtragem feita acima
df = df[(df > 0).all(axis=1)]

Isso pode ser facilmente estendido para filtrar linhas contendo NaN s (entradas não numéricas): -
df = df[(~df.isnull()).all(axis=1)]

Isso também pode ser simplificado para casos como: Exclua todas as linhas em que a coluna E é negativa

df = df[(df.E>0)]

Gostaria de terminar com algumas estatísticas de criação de perfil sobre por que a dropsolução do @ User é mais lenta que a filtragem baseada em coluna bruta: -

%timeit df_new = df[(df.E>0)]
345 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit dft.drop(dft[dft.E < 0].index, inplace=True)
890 µs ± 94.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Uma coluna é basicamente uma matriz, Seriesou seja NumPy, pode ser indexada sem nenhum custo. Para as pessoas interessadas em como a organização da memória subjacente atua na velocidade de execução, aqui está um ótimo link para acelerar o Pandas :

Zakir
fonte
6

Nos pandas, você pode fazer o str.lenseu limite e usar o resultado booleano para filtrá-lo.

df[df['column name'].str.len().lt(2)]
YOBEN_S
fonte
3

Se você deseja descartar linhas do quadro de dados com base em alguma condição complicada no valor da coluna, escrever isso da maneira mostrada acima pode ser complicado. Eu tenho a seguinte solução mais simples que sempre funciona. Vamos supor que você queira soltar a coluna com 'cabeçalho', para que essa coluna seja listada primeiro.

text_data = df['name'].tolist()

Agora aplique alguma função em todos os elementos da lista e coloque-a em uma série de panda:

text_length = pd.Series([func(t) for t in text_data])

no meu caso, eu estava apenas tentando obter o número de tokens:

text_length = pd.Series([len(t.split()) for t in text_data])

agora adicione uma coluna extra com a série acima no quadro de dados:

df = df.assign(text_length = text_length .values)

agora podemos aplicar a condição na nova coluna, como:

df = df[df.text_length  >  10]
def pass_filter(df, label, length, pass_type):

    text_data = df[label].tolist()

    text_length = pd.Series([len(t.split()) for t in text_data])

    df = df.assign(text_length = text_length .values)

    if pass_type == 'high':
        df = df[df.text_length  >  length]

    if pass_type == 'low':
        df = df[df.text_length  <  length]

    df = df.drop(columns=['text_length'])

    return df
Jayanti Prasad
fonte