Usuń niechciane części ze strun w kolumnie

129

Szukam skutecznego sposobu na usunięcie niechcianych części z ciągów w kolumnie DataFrame.

Dane wyglądają następująco:

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

Muszę przyciąć te dane do:

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

Próbowałem .str.lstrip('+-')i. str.rstrip('aAbBcC'), ale pojawił się błąd:

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

Wszelkie wskazówki byłyby bardzo mile widziane!

Yannan Wang
źródło

Odpowiedzi:

167
data['result'] = data['result'].map(lambda x: x.lstrip('+-').rstrip('aAbBcC'))
eumiro
źródło
dzięki! to działa. Nadal owijam umysł wokół mapy (), nie jestem pewien, kiedy jej użyć lub nie ...
Yannan Wang
Ucieszyło mnie, że ta metoda działa również z funkcją zamiany.
BKay
@eumiro jak zastosować ten wynik przy iteracji każdej kolumny?
medev21
Czy mogę użyć tej funkcji do zastąpienia liczby, takiej jak liczba 12? Jeśli zrobię x.lstrip ('12 '), usunie wszystkie 1 i 2s.
Dave
76

Jak usunąć niechciane części z ciągów w kolumnie?

Sześć lat po opublikowaniu pierwotnego pytania pandy mają teraz dużą liczbę „wektoryzowanych” funkcji ciągów, które mogą zwięźle wykonywać te operacje na łańcuchach.

Ta odpowiedź pozwoli zbadać niektóre z tych funkcji ciągów, zasugerować szybsze alternatywy, a na końcu przejdzie do porównania czasów.


.str.replace

Określ podciąg / wzorzec do dopasowania oraz podciąg, którym ma zostać zastąpiony.

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

Jeśli potrzebujesz konwersji wyniku na liczbę całkowitą, możesz użyć Series.astype,

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

df.dtypes
time      object
result     int64
dtype: object

Jeśli nie chcesz modyfikować dfw miejscu, użyj DataFrame.assign:

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

.str.extract

Przydatne do wyodrębniania podciągów, które chcesz zachować.

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

W extractprzypadku należy określić przynajmniej jedną grupę przechwytywania. expand=Falsezwróci serię z przechwyconymi przedmiotami z pierwszej grupy przechwytywania.


.str.split i .str.get

Dzielenie działa przy założeniu, że wszystkie struny mają tę spójną strukturę.

# 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

Nie polecaj, jeśli szukasz ogólnego rozwiązania.


Jeśli podoba Ci się zwięzłe i czytelne str rozwiązania oparte na akcesoriach powyżej, możesz zatrzymać się tutaj. Jeśli jednak interesują Cię szybsze, bardziej wydajne alternatywy, czytaj dalej.


Optymalizacja: listy składane

W pewnych okolicznościach listy składane powinny być preferowane w stosunku do funkcji łańcuchowych pandy. Powodem jest to, że funkcje łańcuchowe są z natury trudne do wektoryzacji (w prawdziwym tego słowa znaczeniu), więc większość funkcji ciągów i wyrażeń regularnych jest tylko opakowaniem wokół pętli z większym narzutem.

Mój opis: Czy pętle for w pandach są naprawdę złe? Kiedy powinno mnie to obchodzić?, omawia bardziej szczegółowo.

str.replaceOpcja może być ponownie napisane przy użyciure.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

str.extractPrzykładem może być ponownie napisane przy użyciu wyrażeń listowych z 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

Jeśli istnieje możliwość uzyskania wartości NaN lub braku dopasowań, konieczne będzie ponowne napisanie powyższego, aby uwzględnić sprawdzanie błędów. Robię to za pomocą funkcji.

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

Możemy również przepisać odpowiedzi @ eumiro i @ MonkeyButter, używając wyrażeń listowych:

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

I,

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

Obowiązują te same zasady obsługi NaN itp.


Porównanie wydajności

wprowadź opis obrazu tutaj

Wykresy generowane za pomocą perfplot . Pełna lista kodów w celach informacyjnych.Odpowiednie funkcje są wymienione poniżej.

Niektóre z tych porównań są niesprawiedliwe, ponieważ wykorzystują strukturę danych OP, ale wyciągają z nich to, co chcesz. Należy zauważyć, że każda funkcja rozumienia listy jest szybsza lub porównywalna niż jej odpowiednik w wersji pandy.

Funkcje

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
źródło
jakiekolwiek obejście, aby uniknąć ustawienia z copywarning:Try using .loc[row_indexer,col_indexer] = value instead
PV8
@ PV8 nie jestem pewien co do Twojego kodu, ale sprawdź to: stackoverflow.com/questions/20625582/ ...
cs95
Dla każdego, kto jest nowy w REGEX, takim jak ja, \ D jest tym samym, co [^ \ d] (wszystko, co nie jest cyfrą) stąd . Więc w zasadzie zastępujemy wszystkie niecyfrowe cyfry w ciągu z niczym.
Rishi Latchmepersad
56

użyłbym funkcji zamiany pandy, bardzo prostej i potężnej, ponieważ możesz użyć wyrażenia regularnego. Poniżej używam wyrażenia regularnego \ D, aby usunąć wszelkie znaki niebędące cyframi, ale oczywiście można uzyskać całkiem kreatywny wynik z wyrażeniem regularnym.

data['result'].replace(regex=True,inplace=True,to_replace=r'\D',value=r'')
Coder375
źródło
Próbowałem tego i to nie działa. Zastanawiam się, czy to działa tylko wtedy, gdy chcesz zamienić cały ciąg zamiast tylko zastąpić część podłańcucha.
bgenchel
@bgenchel - Użyłem tej metody, aby zastąpić część ciąg w pd.Series: df.loc[:, 'column_a'].replace(regex=True, to_replace="my_prefix", value="new_prefix"). Spowoduje to konwersję ciągu, takiego jak „my_prefixaaa” na „new_prefixaaa”.
jakub
co robi r w to_replace = r '\ D'?
Luca Guarro
@LucaGuarro z dokumentacji Pythona: „Przedrostek r, który czyni z literału surowego literału ciągu, jest potrzebny w tym przykładzie, ponieważ sekwencje ucieczki w normalnym„ gotowanym ”literale ciągu, które nie są rozpoznawane przez Pythona, w przeciwieństwie do wyrażeń regularnych, teraz skutkuje DeprecationWarning i ostatecznie stanie się SyntaxError. "
Coder375
35

W szczególnym przypadku, gdy znasz liczbę pozycji, które chcesz usunąć z kolumny dataframe, możesz użyć indeksowania ciągów wewnątrz funkcji lambda, aby pozbyć się tych części:

Ostatni znak:

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

Pierwsze dwa znaki:

data['result'] = data['result'].map(lambda x: str(x)[2:])
prl900
źródło
Muszę przyciąć współrzędne geograficzne do 8 znaków (w tym (.), (-)), aw przypadku, gdy są one mniejsze niż 8, muszę w końcu wstawić „0”, aby wszystkie współrzędne miały 8 znaków. Jaki jest prostszy sposób na zrobienie tego?
Sitz Blogz
Nie do końca rozumiem Twój problem, ale może być konieczna zmiana funkcji lambda na coś takiego jak „{0: .8f}”. Format (x)
prl900
Dziękuję bardzo za odpowiedź. W prostych słowach mam ramkę danych ze współrzędnymi geograficznymi - szerokość i długość geograficzną w dwóch kolumnach. Długość znaków jest większa niż 8 znaków i zostawiłem tylko 8 znaków, zaczynając od pierwszego, które powinny zawierać również (-) i (.).
Sitz Blogz
18

Występuje tu błąd: obecnie nie można przekazywać argumentów do str.lstripi str.rstrip:

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

EDYCJA: 2012-12-07 to działa teraz w gałęzi deweloperów:

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
źródło
11

Bardzo prostą metodą byłoby użycie extractmetody do wybrania wszystkich cyfr. Po prostu podaj wyrażenie regularne, '\d+'które wyodrębni dowolną liczbę cyfr.

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
źródło
7

Często używam list składanych do tego typu zadań, ponieważ często są one szybsze.

Mogą występować duże różnice w wydajności między różnymi metodami wykonywania takich czynności (tj. Modyfikowania każdego elementu serii w ramach DataFrame). Często zrozumienie listy może być najszybsze - zobacz poniższy wyścig kodu dotyczący tego zadania:

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
źródło
4

Przypuśćmy, że twój DF ma te dodatkowe znaki między liczbami. Ostatni wpis.

  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

Możesz spróbować str.replace, aby usunąć znaki nie tylko z początku i końca, ale także pomiędzy.

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

Wynik:

  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
źródło
0

Spróbuj tego, używając wyrażenia regularnego:

import re
data['result'] = data['result'].map(lambda x: re.sub('[-+A-Za-z]',x)
Mr. Prophet
źródło