Baza wiedzy / Python
pandas replace: zamiana wartości, regex i kontrola zmian
Pokazuję, które wartości replace zmieni, które zachowa i jak policzyć efekt. Aktualne przykłady obejmują regex, nullable dtype oraz maskę zamiast usuniętego limit.

replace w pandas służy do zamiany wartości według dopasowania. Przydaje się, gdy źródło zapisuje tę samą kategorię kilkoma etykietami, używa umownego kodu braku albo wymaga poprawienia wybranych napisów. Zaczynam od określenia zakresu: co dokładnie ma zostać zmienione, w której kolumnie i jak rozpoznam prawidłowy wynik. Krótka instrukcja potrafi objąć znacznie więcej danych, niż zakładał autor.
Wszystkie poniższe próbki są syntetyczne, a każdy blok kodu jest samodzielny. Przykłady wykonałem w Pythonie 3.14.4, pandas 3.0.6 i NumPy 2.5.4. Nie wymagają plików zewnętrznych ani danych klientów. Najpierw pokazuję zwykłe dopasowanie wartości, potem zakres kolumn, regex i kontrolę zmian. Zachowuję dane wejściowe, aby można było porównać je z wynikiem.
Najpierw zamieniam jedną konkretną wartość
Wywołujesz metodę na Series albo DataFrame. To nie jest osobna funkcja pd.replace. Dwa podstawowe argumenty mówią, jakiej wartości szukasz i czym ją zastąpić. Domyślnie wynik wraca jako nowy obiekt. Jeśli chcesz używać go dalej, przypisz go do zmiennej lub do właściwej kolumny tabeli.
# example: scalar
import pandas as pd
source = pd.Series([0, -1, 4, -1], dtype="Int64")
result = source.replace(-1, pd.NA)
print(source.tolist())
print(result.tolist(), str(result.dtype))
Pierwsza lista to [0, -1, 4, -1], a druga [0, <NA>, 4, <NA>] z typem Int64. Przyjąłem umowę, że -1 oznacza brak pomiaru. Zero jest prawidłową obserwacją i zostaje zachowane. W innej dziedzinie liczba ujemna może być poprawna, więc znaczenia kodu nie wyciągamy z samego wyglądu danych.
Jawny nullable dtype pozwala przechować liczby całkowite i braki w tej samej kolumnie. To nie znaczy, że dowolna zamiana będzie zgodna z każdym typem. Wstawienie tekstu do ściśle liczbowej kolumny może się nie udać. Warto najpierw ustalić docelowy model wartości, zamiast liczyć na przypadkowe rozszerzenie typu podczas czyszczenia.
Słownik opisuje kilka zamian jednocześnie
Słownik jest czytelny, gdy kilka starych etykiet ma dostać uzgodnione nazwy. Klucze opisują wartości wejściowe, a wartości słownika wynik zamiany. Nie musisz pisać osobnego wywołania dla każdej etykiety. Zwykłe replace zachowuje wartości, dla których słownik nie zawiera dopasowania. Ta cecha ma znaczenie przy niepełnej mapie kategorii.
# example: mapping
import pandas as pd
source = pd.Series(["new", "done", "hold", "new"], dtype="string")
result = source.replace({"new": "open", "done": "closed"})
print(result.tolist())
print(source.tolist())
Dostaniesz ['open', 'closed', 'hold', 'open'], a źródło nadal zawiera stare etykiety. hold nie stało się brakiem. Jeżeli dopuszczasz wyłącznie trzy uzgodnione kategorie, sprawdź dodatkowo zbiór wartości po zamianie. Zachowanie nieznanej etykiety jest przewidywalnym działaniem metody, ale nie oznacza automatycznej akceptacji tej etykiety przez reguły raportu.
Gdy słownik obejmuje wartości, które są jednocześnie wynikami innych zamian, ustal oczekiwaną kolejność. Jedno mapowanie i kilka kolejnych wywołań mogą dać inny rezultat. Na przykład osobne kroki new na open, a potem open na active, obejmą również wartości utworzone w pierwszym kroku. Nie buduj przypadkowo łańcucha korekt, jeżeli potrzebujesz jednego tłumaczenia oryginalnych kategorii.
Ograniczam zmianę do właściwych kolumn
Globalne df.replace jest wygodne, lecz może dotknąć identycznych wartości w różnych kolumnach. Kod 0 może oznaczać brak statusu w jednej kolumnie i poprawną liczbę zamówień w drugiej. Zagnieżdżony słownik pozwala przypisać osobną mapę do nazwy kolumny. Alternatywnie wybierz pojedynczą Series i przypisz jej przekształcony wynik.
# example: columns
import pandas as pd
source = pd.DataFrame({"status": ["N", "D", "N"], "note": ["N", "D", "keep"]})
result = source.replace({"status": {"N": "new", "D": "done"}})
print(result["status"].tolist())
print(result["note"].tolist())
Wynik statusów to ['new', 'done', 'new'], a notatek ['N', 'D', 'keep']. Przykład celowo powtarza stare kody w notatkach. Dzięki temu widać, czy reguła rzeczywiście respektuje granicę kolumny. Test oparty wyłącznie na statusach nie wykryłby zbyt szerokiej zamiany w pozostałej części tabeli.
Dokumentacja DataFrame.replace opisuje mapowania zagnieżdżone i różne formy argumentów. Ja wybieram formę, która jasno pokazuje intencję. Dwie dodatkowe linie z nazwą kolumny są łatwiejsze do sprawdzenia niż krótki globalny zapis, którego skutki trzeba odtwarzać z całej tabeli. Szczególnie dotyczy to identyfikatorów i swobodnych notatek.
Replace i map inaczej traktują nieznane wartości
Series.map ze zwykłym słownikiem buduje wynik według tego słownika. Wartość spoza kluczy staje się brakiem. replace podmienia dopasowania i zachowuje resztę. Obydwa zachowania są użyteczne, ale odpowiadają na inne potrzeby. Zanim wybierzesz metodę, zdecyduj, czy nieznana etykieta ma zostać zachowana, odrzucona, czy oznaczona do wyjaśnienia.
# example: map-versus-replace
import pandas as pd
source = pd.Series(["N", "D", "X"], dtype="string")
rules = {"N": "new", "D": "done"}
replaced = source.replace(rules)
mapped = source.map(rules)
print(replaced.tolist())
print(mapped.iloc[:2].tolist(), bool(pd.isna(mapped.iloc[2])))
replace zwraca ['new', 'done', 'X']. Dwie pierwsze wartości z map to ['new', 'done'], a trzecia jest brakiem. Dokumentacja map opisuje także wyjątki związane ze słownikami definiującymi własną obsługę brakujących kluczy. Tutaj używamy zwykłego słownika, więc wynik jest jednoznaczny.
Nie naprawiam automatycznie każdego braku powstałego po map przez fillna(source). Taki zapis może być właściwy, ale może też ukryć niekompletne mapowanie albo nadpisać brak, który mapa miała celowo wytworzyć. Lepiej najpierw policzyć nieznane wartości i ustalić regułę. Jeżeli celem jest zachowanie wszystkich niedopasowanych kategorii, replace komunikuje to bezpośrednio.
Regex wymaga świadomego zakresu dopasowania
Domyślna zamiana napisu szuka całej wartości. Z regex=True używasz wzorca, który może dopasować fragment tekstu. Kotwice pomagają ograniczyć regułę do pełnej komórki. W przykładzie porównuję pojedynczą literę, słowo kończące się tą literą oraz słowo ją zawierające. Dzięki temu widać różnicę między pełnym dopasowaniem a zamianą fragmentu.
# example: regex
import pandas as pd
source = pd.Series(["A", "BA", "CAT", "B"], dtype="string")
exact = source.replace({r"^A$": "X"}, regex=True)
fragment = source.replace({r"A": "X"}, regex=True)
print(exact.tolist())
print(fragment.tolist())
Pierwszy wynik to ['X', 'BA', 'CAT', 'B'], drugi ['X', 'BX', 'CXT', 'B']. Wzorzec ^A$ w tych danych obejmuje całą wartość, a A trafia także we fragmenty. Surowy napis z prefiksem r ułatwia zapisywanie backslashy w bardziej złożonych wzorcach; nie oznacza automatycznego wyłączenia semantyki wyrażeń regularnych.
Zamiany regex dotyczą tekstu. Nie są sposobem na konwersję wszystkich liczb w tabeli. Jeżeli chcesz zamienić liczbowe 1, podaj liczbowe 1 albo użyj warunku liczbowego. Podanie napisu "1" nie dowodzi, że wszystkie jedynki znikną. Brak trafień często nie powoduje wyjątku, dlatego sprawdzam liczbę dopasowań przed i po operacji.

Do fragmentów tekstu mogę wybrać str.replace
Gdy pracujesz wyłącznie z napisami w jednej kolumnie, Series.str.replace często lepiej pokazuje cel operacji. Dla dosłownego fragmentu ustawiam regex=False. Kropka w wyrażeniu regularnym ma specjalne znaczenie; dosłowna kropka w nazwie pliku jest po prostu znakiem. Jawny parametr chroni czytelnika przed zgadywaniem intencji autora.
# example: literal-text
import pandas as pd
source = pd.Series(["report.v1.csv", "plain", pd.NA], dtype="string")
result = source.str.replace(".", "_", regex=False)
print(result.tolist())
Wynik to ['report_v1_csv', 'plain', <NA>]. Brak pozostaje brakiem. Dokumentacja str.replace opisuje także liczbę zamian wewnątrz pojedynczego napisu. Nie myl jej z liczbą wierszy, które chcesz zmienić w całej tabeli. Te dwa ograniczenia działają na różnych poziomach danych.
Przed seryjnym czyszczeniem nazw ustal, czy usuwany znak jest jedynie formatowaniem. Kropka może oddzielać wersję dokumentu, spacja może być częścią identyfikatora, a znak minus może opisywać wartość liczbową. Zamiana tekstowa nie zna tych znaczeń. Zachowanie oryginalnej kolumny ułatwia późniejsze wyjaśnienie, dlaczego dwa różne napisy otrzymały tę samą postać.
Parametry method i limit usunięto z replace
Starsze instrukcje pokazują method="pad", method="bfill" albo limit przekazane do replace. Nie używaj ich w pandas 3. Parametry zostały usunięte, co opisują zmiany pandas 3.0. Jeżeli zadanie dotyczy propagowania wartości do braków, rozważ osobno ffill lub bfill, po ustaleniu kolejności i granic grup.
Gdy wymaganie brzmi „zmień pierwsze dwa trafienia”, zapisz je maską. eq znajduje trafienia, a suma skumulowana liczy je w aktualnej kolejności wierszy. Łączę obydwa warunki: samo porównanie sumy do dwóch zaznaczyłoby również wcześniejsze wiersze, które wcale nie pasują. Parametr dawnego limit nie jest potrzebny do wyrażenia tego wymagania.
# example: first-matches
import pandas as pd
source = pd.Series([0, -1, 4, -1, -1], dtype="Int64")
hits = source.eq(-1).fillna(False)
selected = hits & hits.cumsum().le(2)
result = source.copy()
result.loc[selected] = pd.NA
print(selected.tolist())
print(result.tolist())
Maska to [False, True, False, True, False], a wynik [0, <NA>, 4, <NA>, -1]. Trzecie trafienie zostaje bez zmiany. To reguła dla całej serii w jej bieżącym porządku. Jeżeli potrzebujesz pierwszych dwóch zdarzeń każdego klienta według daty, najpierw określ stabilne sortowanie i osobne liczenie w grupach. Sama pozycja w przypadkowym pliku nie wyznacza czasu zdarzenia.
Przypisuję wynik jawnie i sprawdzam jego zakres
Unikam łańcuchowego df["status"].replace(..., inplace=True). Przy Copy-on-Write taki sposób nie jest poprawną metodą modyfikacji nadrzędnej tabeli. Przypisanie df["status"] = df["status"].replace(...) jasno wskazuje miejsce zapisu. Jeśli pracuję na wybranych wierszach, buduję maskę i używam loc, pilnując dopasowania indeksów po obu stronach przypisania.
# example: audit
import pandas as pd
source = pd.DataFrame({"id": ["001", "002", "003"], "status": ["N", "D", "X"]})
result = source.copy()
result["status"] = result["status"].replace({"N": "new", "D": "done"})
changed = source["status"].ne(result["status"])
unknown = ~result["status"].isin(["new", "done"])
print(int(changed.sum()), result.loc[unknown, "id"].tolist())
print(result["id"].equals(source["id"]), len(result) == len(source))
Wynik 2 ['003'] pokazuje dwie zmiany i identyfikator rekordu z nieznanym statusem. Dwie wartości True potwierdzają zachowanie identyfikatorów i liczby wierszy. To więcej informacji niż sam podgląd pierwszych kilku rekordów. W tym przykładzie nie ma braków statusu; przy nullable danych warunek porównania wymaga osobnej reguły dla par braków i dla przejścia między brakiem a wartością.
Kontrola zamiany obejmuje również znaczenie wyniku
Przed uruchomieniem reguł zapisuję liczności kategorii, liczbę braków i zbiór wartości spoza mapy. Po wykonaniu porównuję te same wielkości. Jeżeli łączę new i pending w jedną kategorię, spadek liczby kategorii jest oczekiwany. Jeśli łączę identyfikatory, taki sam efekt może oznaczać utratę rozróżnienia obiektów. Technicznie udana zamiana nie rozstrzyga, czy agregacja zachowała sens.
Ważna jest także powtarzalność. Reguła normalizująca kategorię zwykle powinna dawać ten sam wynik po drugim uruchomieniu. Zamiana fragmentu A na AA tak nie działa: kolejne wykonanie jeszcze wydłuży napis. Dla procesu cyklicznego sprawdzam więc wynik ponownego zastosowania reguł do już oczyszczonej próbki. Jeżeli kolejne przejście jest potrzebne, musi to wynikać z jawnie opisanych etapów przekształcenia.
Nie usuwam nieznanych kategorii tylko po to, żeby kontrola zwróciła zero problemów. Zachowuję je w zestawieniu do wyjaśnienia i podaję liczbę dotkniętych rekordów. Osoba uzgadniająca słownik może wtedy zdecydować, czy to literówka, nowa poprawna kategoria, czy błąd źródła. replace wykonuje ustaloną decyzję, lecz nie podejmuje jej na podstawie podobieństwa napisów ani częstotliwości występowania.
Przy łączeniu kilku plików warto również zapisać pochodzenie rekordu. Ta sama stara etykieta może mieć inne znaczenie w dwóch systemach. Wówczas globalny słownik jest zbyt ogólny i trzeba dobrać regułę do źródła albo najpierw ujednolicić kontrakt danych. Sprawdzenie pochodzenia jest tańsze niż późniejsze odtwarzanie, dlaczego identyczny kod został zinterpretowany na dwa różne sposoby.
Przygotowuję słownik bez utraty informacji
Słownik zamian powinien być możliwy do sprawdzenia niezależnie od kodu. Dla każdej starej wartości zapisuję nową etykietę i krótkie uzasadnienie. Jeśli dwie osoby dostarczą sprzeczne mapowania tego samego klucza, zwykłe złożenie ich w jeden słownik może ukryć konflikt. Ostatnia przypisana wartość klucza zastąpi poprzednią. Dlatego konflikt sprawdzam przed zbudowaniem mapy używanej do przekształcenia tabeli.
Podobny problem dotyczy wielkości liter i białych znaków. N, n i N to różne napisy. Możesz dodać wszystkie warianty do mapy albo wykonać wcześniejszy etap normalizacji przez obcięcie spacji i uzgodnienie wielkości liter. Taki etap również zmienia dane, więc zapisuję go jawnie. Nie zakładam, że ignorowanie wielkości liter jest poprawne dla nazw użytkowników, kodów produktów albo innych identyfikatorów.
Przykładowy proces dla statusów może mieć trzy kolumny: wartość oryginalną, wartość po normalizacji zapisu i kategorię po uzgodnionej zamianie. To pozwala odróżnić korektę formatowania od decyzji biznesowej. Gdy czytelnik raportu zakwestionuje liczbę otwartych spraw, możesz wskazać dokładnie, które etykiety zostały do tej kategorii przypisane. Nie trzeba wtedy zgadywać, czy różnica powstała podczas importu, obcinania spacji czy mapowania.
Zwracam też uwagę na kolejność reguł regex. Szeroki wzorzec może zmienić tekst, który miał obsłużyć bardziej szczegółowy etap. Przy kilku osobnych transformacjach zapisuję oczekiwany wynik po każdym kroku. Dla prostego słownika kategorii unikam regex, jeżeli wystarcza dopasowanie całych wartości. Mniejsza liczba możliwych dopasowań ułatwia kontrolę i ogranicza liczbę nieoczywistych przypadków do sprawdzenia.
W danych mieszanych patrzę na typ konkretnej wartości, a nie tylko sposób jej wyświetlenia. Liczba jeden i napis zawierający cyfrę jeden mogą wyglądać identycznie w podglądzie tabeli, ale zwykłe dopasowanie nie musi ich traktować jako tę samą wartość. Najpierw sprawdzam pochodzenie różnicy. Masowa konwersja całej tabeli na tekst ujednolici wygląd, lecz może zepsuć daty, braki i późniejsze obliczenia liczbowe.
Kontrola wartości brakujących wymaga jeszcze jednej decyzji: czy dwa braki przed i po operacji liczymy jako zmianę? Dla raportu korekt zwykle nie. Natomiast przejście od kodu -1 do braku jest zmianą, którą chcemy policzyć. Zwykłe porównanie nullable serii może dać brak w masce zamiast wartości logicznej. Buduj więc osobno warunki dla obecności wartości i ich równości, a potem sprawdź małą próbkę obejmującą wszystkie przejścia.
Do zestawu prób dodaję pustą serię i serię bez żadnego dopasowania. W pierwszym przypadku operacja powinna zachować pusty wynik o odpowiednim typie; w drugim dane powinny pozostać niezmienione. Dodaję także próbkę, w której pasuje każdy rekord. Te trzy sytuacje pomagają wykryć założenie, że zawsze istnieje przynajmniej jedno trafienie i przynajmniej jedna wartość pozostająca bez zmiany.
Ćwiczenie: sprawdź granice swojej reguły
Porównaj również kolejność działań. Najpierw połącz dwie kategorie i policz ich wystąpienia, a potem policz stare kategorie i zsumuj odpowiednie wyniki. Przy poprawnej mapie obie drogi powinny dać zgodną liczność kategorii docelowej. Jeżeli wyniki się różnią, sprawdź braki, nieznane etykiety i zakres kolumn. Taka niezależna kontrola agregacji potrafi ujawnić błąd, którego nie widać w pojedynczym wierszu.
Zapisz oczekiwane wyniki jako konkretne wartości, zanim uruchomisz transformację. Dla czterech rekordów możesz policzyć je ręcznie. Nie twórz oczekiwanego wyniku przez powtórzenie tej samej zamiany w drugim fragmencie kodu, bo obie wersje mogą wtedy powtórzyć identyczny błąd. Mała próbka z celowo dobranymi przypadkami daje lepszy punkt odniesienia niż duży losowy zbiór bez znanej odpowiedzi.
Do przykładu kolumnowego dodaj N w kolejnej kolumnie, nową kategorię statusu i brak. Ustal oczekiwany rezultat przed uruchomieniem kodu. Następnie porównaj zamianę globalną z mapą przypisaną do status. Zapisz, które różnice są zamierzone i które ujawniają zbyt szeroki zakres. Nie oceniaj poprawności wyłącznie na podstawie braku wyjątku.
W przykładzie z maską zmień limit na zero, jeden oraz liczbę większą od liczby trafień. Dodaj brak przed pierwszym trafieniem. Sprawdź, czy tylko pasujące pozycje zwiększają licznik i czy źródło pozostaje bez zmian. Na końcu połącz import przez read_json z jedną jasno określoną regułą zamiany. Taki mały proces pozwala prześledzić drogę od danych źródłowych do sprawdzonej tabeli.
Najważniejsze w pigułce
Czy replace zmienia wartości spoza słownika?
Zwykłe mapowanie replace zachowuje wartości bez dopasowania. Series.map ze zwykłym słownikiem zamienia nieznane klucze na braki, więc wymaga innej kontroli.
Jak ograniczyć replace do pierwszych dwóch trafień?
W pandas 3 nie ma parametru limit w replace. Zbuduj maskę dopasowań, wybierz pierwsze dwa przez sumę skumulowaną i przypisz przez loc. Kolejność wierszy musi być określona.
Czy regex=True zamieni również liczby?
Zamiany regex dotyczą wartości tekstowych. Nie są ogólną konwersją typów; dla liczbowych wartości użyj odpowiedniej wartości liczbowej lub jawnego warunku.
Przy pracy nad artykułem użyto w zautomatyzowany sposób sztucznej inteligencji. Jeśli też chcesz tak umieć, sprawdź ten projekt.
