Remover partes indesejadas de strings em uma coluna

129

Estou procurando uma maneira eficiente de remover partes indesejadas de seqüências de caracteres em uma coluna DataFrame.

Os dados se parecem com:

    time    result
1    09:00   +52A
2    10:00   +62B
3    11:00   +44a
4    12:00   +30b
5    13:00   -110a

Preciso aparar esses dados para:

    time    result
1    09:00   52
2    10:00   62
3    11:00   44
4    12:00   30
5    13:00   110

Eu tentei .str.lstrip('+-')e. str.rstrip('aAbBcC'), mas ocorreu um erro:

TypeError: wrapper() takes exactly 1 argument (2 given)

Qualquer ponteiro seria muito apreciado!

Yannan Wang
fonte

Respostas:

167
data['result'] = data['result'].map(lambda x: x.lstrip('+-').rstrip('aAbBcC'))
eumiro
fonte
THX! isso funciona. Eu ainda estou embrulhando minha mente em torno do mapa (), não tenho certeza quando usar ou não usá-lo ...
Yannan Wang
Fiquei satisfeito ao ver que esse método também funciona com a função de substituição.
BKay
@eumiro como você aplica esse resultado se iterar cada coluna?
medev21
Posso usar esta função para substituir um número como o número 12? Se eu fizer x.lstrip ('12 '), ele remove todos os 1 e 2s.
Dave
76

Como faço para remover partes indesejadas de seqüências de caracteres em uma coluna?

Seis anos após a publicação da pergunta original, o pandas agora possui um bom número de funções de string "vetorizadas" que podem executar sucintamente essas operações de manipulação de strings.

Esta resposta irá explorar algumas dessas funções de string, sugerir alternativas mais rápidas e entrar em uma comparação de tempos no final.


.str.replace

Especifique a substring / padrão para corresponder e a substring para substituí-lo.

pd.__version__
# '0.24.1'

df    
    time result
1  09:00   +52A
2  10:00   +62B
3  11:00   +44a
4  12:00   +30b
5  13:00  -110a

df['result'] = df['result'].str.replace(r'\D', '')
df

    time result
1  09:00     52
2  10:00     62
3  11:00     44
4  12:00     30
5  13:00    110

Se você precisar converter o resultado em um número inteiro, poderá usar Series.astype,

df['result'] = df['result'].str.replace(r'\D', '').astype(int)

df.dtypes
time      object
result     int64
dtype: object

Se você não deseja modificar dfno local, use DataFrame.assign:

df2 = df.assign(result=df['result'].str.replace(r'\D', ''))
df
# Unchanged

.str.extract

Útil para extrair a substring (s) que você deseja manter.

df['result'] = df['result'].str.extract(r'(\d+)', expand=False)
df

    time result
1  09:00     52
2  10:00     62
3  11:00     44
4  12:00     30
5  13:00    110

Com extract, é necessário especificar pelo menos um grupo de captura. expand=Falseretornará uma série com os itens capturados do primeiro grupo de capturas.


.str.split e .str.get

A divisão funciona assumindo que todas as suas strings seguem essa estrutura consistente.

# df['result'] = df['result'].str.split(r'\D').str[1]
df['result'] = df['result'].str.split(r'\D').str.get(1)
df

    time result
1  09:00     52
2  10:00     62
3  11:00     44
4  12:00     30
5  13:00    110

Não recomende se você estiver procurando uma solução geral.


Se você estiver satisfeito com as str soluções baseadas em acessor sucintas e legíveis acima, você pode parar por aqui. No entanto, se você estiver interessado em alternativas mais rápidas e com melhor desempenho, continue lendo.


Otimizando: compreensões da lista

Em algumas circunstâncias, a compreensão da lista deve ser preferida às funções de string dos pandas. O motivo é que as funções de string são inerentemente difíceis de vetorizar (no verdadeiro sentido da palavra), portanto, a maioria das funções de string e regex são apenas wrappers em torno de loops com mais sobrecarga.

Meu artigo: Os loops de pandas são realmente ruins? Quando devo me importar? , entra em maiores detalhes.

A str.replaceopção pode ser reescrita usandore.sub

import re

# Pre-compile your regex pattern for more performance.
p = re.compile(r'\D')
df['result'] = [p.sub('', x) for x in df['result']]
df

    time result
1  09:00     52
2  10:00     62
3  11:00     44
4  12:00     30
5  13:00    110

O str.extractexemplo pode ser reescrito usando uma compreensão de lista com re.search,

p = re.compile(r'\d+')
df['result'] = [p.search(x)[0] for x in df['result']]
df

    time result
1  09:00     52
2  10:00     62
3  11:00     44
4  12:00     30
5  13:00    110

Se NaNs ou não correspondências forem possíveis, você precisará reescrever o item acima para incluir alguma verificação de erro. Eu faço isso usando uma função.

def try_extract(pattern, string):
    try:
        m = pattern.search(string)
        return m.group(0)
    except (TypeError, ValueError, AttributeError):
        return np.nan

p = re.compile(r'\d+')
df['result'] = [try_extract(p, x) for x in df['result']]
df

    time result
1  09:00     52
2  10:00     62
3  11:00     44
4  12:00     30
5  13:00    110

Também podemos reescrever as respostas de @ eumiro e @ MonkeyButter usando a compreensão de lista:

df['result'] = [x.lstrip('+-').rstrip('aAbBcC') for x in df['result']]

E,

df['result'] = [x[1:-1] for x in df['result']]

Aplicam-se as mesmas regras para o tratamento de NaNs, etc.


Comparação de desempenho

insira a descrição da imagem aqui

Gráficos gerados usando perfplot . Listagem de código completo, para sua referência. As funções relevantes estão listadas abaixo.

Algumas dessas comparações são injustas porque tiram vantagem da estrutura dos dados do OP, mas tiram dela o que você deseja. Uma coisa a notar é que toda função de compreensão de lista é mais rápida ou comparável do que sua variante equivalente de pandas.

Funções

def eumiro(df):
    return df.assign(
        result=df['result'].map(lambda x: x.lstrip('+-').rstrip('aAbBcC')))

def coder375(df):
    return df.assign(
        result=df['result'].replace(r'\D', r'', regex=True))

def monkeybutter(df):
    return df.assign(result=df['result'].map(lambda x: x[1:-1]))

def wes(df):
    return df.assign(result=df['result'].str.lstrip('+-').str.rstrip('aAbBcC'))

def cs1(df):
    return df.assign(result=df['result'].str.replace(r'\D', ''))

def cs2_ted(df):
    # `str.extract` based solution, similar to @Ted Petrou's. so timing together.
    return df.assign(result=df['result'].str.extract(r'(\d+)', expand=False))

def cs1_listcomp(df):
    return df.assign(result=[p1.sub('', x) for x in df['result']])

def cs2_listcomp(df):
    return df.assign(result=[p2.search(x)[0] for x in df['result']])

def cs_eumiro_listcomp(df):
    return df.assign(
        result=[x.lstrip('+-').rstrip('aAbBcC') for x in df['result']])

def cs_mb_listcomp(df):
    return df.assign(result=[x[1:-1] for x in df['result']])
cs95
fonte
qualquer solução alternativa para evitar a configuração com aviso de cópia:Try using .loc[row_indexer,col_indexer] = value instead
PV8
O @ PV8 não tem certeza sobre o seu código, mas verifique isso: stackoverflow.com/questions/20625582/…
cs95 2/19
Para quem é novo no REGEX como eu, \ D é o mesmo que [^ \ d] (qualquer coisa que não seja um dígito) daqui . Então, estamos basicamente substituindo todos os não dígitos da string por nada.
Rishi Latchmepersad
56

eu usaria a função de substituição de pandas, muito simples e poderosa, como você pode usar regex. Abaixo, estou usando o regex \ D para remover caracteres que não sejam dígitos, mas obviamente você pode ser bastante criativo com o regex.

data['result'].replace(regex=True,inplace=True,to_replace=r'\D',value=r'')
Coder375
fonte
Eu tentei isso e não funciona. Gostaria de saber se ele só funciona quando você deseja substituir uma seqüência inteira em vez de apenas substituir uma parte de substring.
Bgenchel
@bgenchel - Eu usei esse método para substituir parte de uma cadeia em um pd.Series: df.loc[:, 'column_a'].replace(regex=True, to_replace="my_prefix", value="new_prefix"). Isso converterá uma string como "my_prefixaaa" em "new_prefixaaa".
jakub
o que o r faz em to_replace = r '\ D'?
Luca Guarro
@LucaGuarro, da documentação do python: "O prefixo r, tornando o literal uma literal de cadeia bruta, é necessário neste exemplo porque sequências de escape em uma literal de cadeia" cozida "normal que não são reconhecidas pelo Python, em vez de expressões regulares, agora resultará em um DeprecationWarning e acabará se tornando um SyntaxError. "
Coder375 17/01
35

No caso específico em que você sabe o número de posições que deseja remover da coluna do quadro de dados, pode usar a indexação de cadeias dentro de uma função lambda para se livrar dessas partes:

Último caractere:

data['result'] = data['result'].map(lambda x: str(x)[:-1])

Primeiros dois caracteres:

data['result'] = data['result'].map(lambda x: str(x)[2:])
prl900
fonte
Preciso cortar as coordenadas geográficas em 8 caracteres (incluindo (.), (-)) e, se elas forem menores que 8, eu preciso inserir '0', finalmente, para transformar todas as coordenadas em 8 caracteres. Qual é a maneira mais simples de fazer isso?
Sitz Blogz 28/02
Eu não entendo completamente o seu problema, mas pode ser necessário alterar a função lambda para algo como "{0: .8f}". Formato (x)
prl900
Muito obrigado pela resposta. Em palavras simples, tenho dataframe com coordenadas geográficas - latitude e longitude como duas colunas. O comprimento dos caracteres é superior a 8 caracteres e eu mantive apenas 8 caracteres começando primeiro, que deve incluir (-) e (.) Também.
Sitz Blogz
18

Há um erro aqui: atualmente não é possível transmitir argumentos para str.lstripe str.rstrip:

http://github.com/pydata/pandas/issues/2411

EDIT: 2012-12-07 isso funciona agora no ramo dev:

In [8]: df['result'].str.lstrip('+-').str.rstrip('aAbBcC')
Out[8]: 
1     52
2     62
3     44
4     30
5    110
Name: result
Wes McKinney
fonte
11

Um método muito simples seria usar o extractmétodo para selecionar todos os dígitos. Basta fornecer a expressão regular '\d+'que extrai qualquer número de dígitos.

df['result'] = df.result.str.extract(r'(\d+)', expand=True).astype(int)
df

    time  result
1  09:00      52
2  10:00      62
3  11:00      44
4  12:00      30
5  13:00     110
Ted Petrou
fonte
7

Costumo usar a compreensão da lista para esses tipos de tarefas, porque geralmente são mais rápidas.

Pode haver grandes diferenças no desempenho entre os vários métodos para fazer coisas como esta (isto é, modificar todos os elementos de uma série dentro de um DataFrame). Geralmente, a compreensão da lista pode ser mais rápida - veja a corrida de código abaixo para esta tarefa:

import pandas as pd
#Map
data = pd.DataFrame({'time':['09:00','10:00','11:00','12:00','13:00'], 'result':['+52A','+62B','+44a','+30b','-110a']})
%timeit data['result'] = data['result'].map(lambda x: x.lstrip('+-').rstrip('aAbBcC'))
10000 loops, best of 3: 187 µs per loop
#List comprehension
data = pd.DataFrame({'time':['09:00','10:00','11:00','12:00','13:00'], 'result':['+52A','+62B','+44a','+30b','-110a']})
%timeit data['result'] = [x.lstrip('+-').rstrip('aAbBcC') for x in data['result']]
10000 loops, best of 3: 117 µs per loop
#.str
data = pd.DataFrame({'time':['09:00','10:00','11:00','12:00','13:00'], 'result':['+52A','+62B','+44a','+30b','-110a']})
%timeit data['result'] = data['result'].str.lstrip('+-').str.rstrip('aAbBcC')
1000 loops, best of 3: 336 µs per loop
tim654321
fonte
4

Suponha que seu DF também tenha esses caracteres extras entre os números. A última entrada.

  result   time
0   +52A  09:00
1   +62B  10:00
2   +44a  11:00
3   +30b  12:00
4  -110a  13:00
5   3+b0  14:00

Você pode tentar str.replace para remover caracteres não apenas do início e do fim, mas também do meio.

DF['result'] = DF['result'].str.replace('\+|a|b|\-|A|B', '')

Resultado:

  result   time
0     52  09:00
1     62  10:00
2     44  11:00
3     30  12:00
4    110  13:00
5     30  14:00
Rishi Bansal
fonte
0

Tente isso usando expressão regular:

import re
data['result'] = data['result'].map(lambda x: re.sub('[-+A-Za-z]',x)
Sr. Profeta
fonte