Baza wiedzy / Python
pandas fillna w Pythonie: uzupełnianie braków w pandas 3
Pokazuję, jak w pandas 3 uzupełniam braki przez fillna, ffill i bfill, dlaczego stary kod z method= przestał działać i kiedy celowo zostawiam NaN w danych.

Metodą fillna wstawiam w pandas coś konkretnego w miejsce brakujących wartości: liczbę, napis albo osobną wartość dla każdej kolumny. Wygląda na drobiazg, ale każda taka zamiana jest decyzją o znaczeniu danych. Pusty rabat może oznaczać zamówienie bez rabatu, a pusta ocena w ankiecie zwykle oznacza tylko tyle, że klient nie odpowiedział.
W pandas 3 fillna robi już jedną rzecz: wstawia podaną wartość. Kopiowanie sąsiednich wartości przeszło do osobnych metod ffill i bfill, a stary zapis fillna(method="ffill") kończy się błędem. Wszystkie przykłady niżej uruchomiłem w Pythonie 3.14 z pandas 3.0.6 na małych, syntetycznych danych. Szerzej o decyzjach przy brakach, duplikatach i anomaliach piszę w artykule Czyszczenie danych: pięć kroków, które nie zgubią sensu analizy.
Składnia fillna w pandas 3
DataFrame.fillna(value, *, axis=None, inplace=False, limit=None)
Series.fillna(value, *, axis=None, inplace=False, limit=None)
value: czym uzupełniam braki. Może to być pojedyncza wartość, słownik{kolumna: wartość}, Series albo DataFrame. Listy pandas nie przyjmuje i zgłasza TypeError.axis: oś, wzdłuż której działa uzupełnianie. Przy zwykłym uzupełnianiu kolumn zostawiam wartość domyślną.inplace:Truezmienia obiekt na miejscu. Domyślnie metoda zwraca nowy obiekt, a ja przypisuję wynik.limit: największa liczba braków uzupełnionych wzdłuż osi. W DataFrame oznacza to co najwyżej tyle braków w każdej kolumnie, licząc od góry.
Parametru method już nie ma. Był oznaczony jako przestarzały od pandas 2.1, a w 3.0 został usunięty. Nie ma też downcast. Kod ze starszych poradników kończy się takim błędem:
# example: removed-method
import pandas as pd
prices = pd.Series([10.0, None, 12.0])
try:
prices.fillna(method="ffill")
except TypeError as error:
print(error)
print(prices.ffill().tolist())
NDFrame.fillna() got an unexpected keyword argument 'method'
[10.0, 10.0, 12.0]
Poprawka jest mechaniczna: fillna(method="ffill") zamieniam na ffill(), a fillna(method="bfill") na bfill(). Przy okazji sprawdzam, czy kopiowanie wartości w ogóle ma w tym miejscu sens. Wracam do tego niżej.
Przykład: trzy rodzaje braków w jednej tabeli
Pracuję na pięciu syntetycznych zamówieniach. Najpierw zapisuję założenia, bo bez nich żadne uzupełnienie nie ma sensu: pusty kanał oznacza, że system go nie zapisał, pusty rabat oznacza zamówienie bez rabatu, a pusta ocena to brak odpowiedzi klienta.
# example: orders
import pandas as pd
orders = pd.DataFrame({
"order_id": ["Z1", "Z2", "Z3", "Z4", "Z5"],
"channel": ["web", None, "store", "web", None],
"discount": [5, None, 10, None, None],
"rating": [5, None, 4, None, 2],
})
print(orders.isna().sum())
order_id 0
channel 2
discount 3
rating 2
dtype: int64
Zaczynam od policzenia braków przez isna, bo chcę wiedzieć, ile wartości zaraz zmienię. Potem uzupełniam tylko dwie kolumny, każdą inną wartością. Słownik w fillna załatwia to jednym wywołaniem, a kolumny spoza słownika zostają nietknięte:
# example: fill-dict
filled = orders.fillna({"channel": "unknown", "discount": 0})
filled["discount_was_missing"] = orders["discount"].isna()
print(filled)
order_id channel discount rating discount_was_missing
0 Z1 web 5.0 5.0 False
1 Z2 unknown 0.0 NaN True
2 Z3 store 10.0 4.0 False
3 Z4 web 0.0 NaN True
4 Z5 unknown 0.0 2.0 True
Kanał dostał etykietę unknown, więc te zamówienia nie znikną z grupowania po kanale. Rabat dostał 0, zgodnie z założeniem. Ocena została pusta, bo zera nikt nie wystawił.
Przed uzupełnieniem zapisuję też w osobnej kolumnie, które rabaty były puste. Po zmianie 0.0 z systemu i 0.0 dopisane przeze mnie wyglądają identycznie. Flaga pozwala je później rozróżnić, policzyć i pokazać w raporcie.
Typ kolumny po uzupełnieniu
Rabaty wpisałem jako liczby całkowite, a mimo to widzę 5.0 i 0.0. Braki w zwykłej kolumnie liczbowej pandas zapisuje jako NaN, który jest liczbą zmiennoprzecinkową, więc cała kolumna dostała typ float64. Samo fillna(0) tego nie cofa:
# example: dtypes
print(filled["channel"].dtype)
print(filled["discount"].dtype)
print(filled["discount"].astype("int64").tolist())
print(orders["discount"].fillna("none").dtype)
discount = pd.Series([5, None, 10], dtype="Int64")
print(discount.fillna(0).dtype)
str
float64
[5, 0, 10, 0, 0]
object
Int64
Gdy braków już nie ma, zmieniam typ jawnie przez astype. Przed uzupełnieniem astype("int64") zgłosiłby błąd, bo NaN nie da się zapisać jako liczby całkowitej.
Uzupełnienie kolumny liczbowej napisem, na przykład "none", przechodzi bez ostrzeżenia, ale zmienia typ kolumny na object. Od tej chwili sumy i średnie sprawiają kłopot. Jeśli potrzebuję opisu braku, trzymam go w osobnej kolumnie, tak jak flagę wyżej.
Int64 z wielkiej litery to całkowity typ pandas, który przechowuje brak jako <NA> bez zamiany na float. Po fillna(0) zostaje całkowity. Kolumny tekstowe mają w pandas 3 domyślnie typ str i uzupełnienie napisem go nie zmienia.
ffill i bfill zamiast method=
ffill wstawia w miejsce braku ostatnią wcześniejszą wartość, czyli kopiuje ją w dół kolumny. bfill bierze najbliższą następną wartość i kopiuje ją w górę. Obie mają sens wtedy, gdy wartość obowiązuje, dopóki się nie zmieni: cena z cennika, status klienta, ostatni znany stan magazynu.
Przykład to syntetyczny dzienny stan magazynu z eksportu, który czasem nie dochodzi. Dwa dni przerwy akceptuję, dłuższa luka ma zostać widoczna:
# example: ffill
stock = pd.Series(
[12.0, None, None, None, 9.0, None],
index=pd.date_range("2026-03-02", periods=6, freq="D"),
)
print(stock.ffill(limit=2))
print(stock.bfill(limit=1))
2026-03-02 12.0
2026-03-03 12.0
2026-03-04 12.0
2026-03-05 NaN
2026-03-06 9.0
2026-03-07 9.0
Freq: D, dtype: float64
2026-03-02 12.0
2026-03-03 NaN
2026-03-04 NaN
2026-03-05 9.0
2026-03-06 9.0
2026-03-07 NaN
Freq: D, dtype: float64

W ffill i bfill parametr limit oznacza największą liczbę kolejnych braków wypełnionych w jednej luce. Trzeci dzień przerwy, 5 marca, został więc pusty i od razu widać, że eksport zawiódł na dłużej. Ostatni dzień po bfill też jest pusty, bo po nim nie ma już wartości do skopiowania.
W fillna ten sam parametr liczy co innego: wszystkie uzupełnione braki wzdłuż osi, bez względu na luki. Nazwa jest wspólna, znaczenie różne, więc przy przepisywaniu starego kodu porównuję wynik przed zmianą i po niej.
Druga pułapka pojawia się przy kilku seriach w jednej tabeli. ffill na całej kolumnie nie wie, gdzie kończy się jeden sklep, a zaczyna drugi:
# example: groupby-ffill
shops = pd.DataFrame({
"store": ["A", "A", "B", "B"],
"stock": [7.0, None, None, 4.0],
})
print(shops["stock"].ffill().tolist())
print(shops.groupby("store")["stock"].ffill().tolist())
[7.0, 7.0, 7.0, 4.0]
[7.0, 7.0, nan, 4.0]
Bez grupowania sklep B dostał stan sklepu A. Z groupby brak w B zostaje brakiem, bo w tym sklepie nie ma wcześniejszej wartości. Przed ffill sortuję dane po sklepie i dacie, bo metoda idzie w kolejności wierszy. W pandas 3 nie ma już groupby(...).fillna(...), więc grupowe kopiowanie robię przez groupby(...).ffill() albo .bfill().
Kiedy wybieram interpolate, a kiedy nic nie uzupełniam
ffill powtarza wartość, która naprawdę wystąpiła. interpolate liczy nowe wartości pomiędzy znanymi punktami:
# example: interpolate
print(stock.interpolate().tolist())
print(stock.interpolate(limit_area="inside").tolist())
[12.0, 11.25, 10.5, 9.75, 9.0, 9.0]
[12.0, 11.25, 10.5, 9.75, 9.0, nan]
Stan 11,25 sztuki nigdy nie wystąpił. Dla temperatury z czujnika liniowa zmiana pomiędzy pomiarami bywa rozsądnym założeniem, dla sztuk w magazynie raczej nie. Domyślnie interpolate wypełnia też koniec serii ostatnią znaną wartością, a limit_area="inside" ogranicza ją do braków pomiędzy znanymi punktami. Interpolacja to osobna decyzja analityczna, dlatego nie traktuję jej jako jeszcze jednej opcji fillna.
Często najlepiej nie uzupełniać niczego. Ocena klienta jest dobrym przykładem:
# example: rating
print(round(orders["rating"].mean(), 2), orders["rating"].fillna(0).mean())
print(orders["rating"].count(), orders["rating"].fillna(0).count())
3.67 2.2
3 5
Średnia z trzech odpowiedzi wynosi 3,67. Po wstawieniu zer spada do 2,2, a liczba ocen rośnie z trzech do pięciu, choć nikt nie odpowiedział częściej. Wstawienie średniej z kolumny nie zmieni średniej, ale dopisze dwie odpowiedzi, których nie było, i zmniejszy rozrzut danych. Pandas i tak pomija NaN w mean, sum czy count, więc w wielu obliczeniach brak w ogóle nie przeszkadza.
Zostawiam brak, gdy nie znam jego przyczyny, gdy pusta wartość sama jest informacją (brak odpowiedzi, brak pomiaru, brak zgody) albo gdy uzupełnienie zmieniłoby raportowany wynik. W raporcie piszę wtedy, ile rekordów weszło do obliczenia. Jeśli rekord bez wartości nie nadaje się do konkretnej analizy, wolę go świadomie odfiltrować przez dropna, niż wypełnić zmyśloną liczbą.
Częste pomyłki: inplace, literówki i puste napisy
Pierwsza pułapka dotyczy inplace. W pandas 3 obowiązuje copy-on-write: kolumna wybrana przez orders["discount"] zachowuje się jak kopia, więc jej zmiana nie dociera do tabeli.
# example: copy-on-write
orders["discount"].fillna(0, inplace=True)
print(orders["discount"].isna().sum())
orders["discount"] = orders["discount"].fillna(0)
print(orders["discount"].isna().sum())
3
0
Pierwsza linia zgłasza ostrzeżenie ChainedAssignmentError i niczego nie zmienia, dlatego wciąż widzę trzy braki. Druga przypisuje wynik do kolumny i działa. Zadziała też orders.fillna({"discount": 0}, inplace=True) na całej tabeli, ale wolę jawne przypisanie, bo od razu widać, co się zmienia. Przy okazji: w pandas 3 część metod wywołanych z inplace=True, w tym fillna, ffill, bfill i replace, zwraca sam obiekt zamiast None. Inne, na przykład dropna i sort_values, nadal zwracają None.
Druga pułapka to klucze słownika. fillna pomija klucze, których nie ma wśród kolumn, i nie zgłasza błędu. Trzecia to wartości, które wyglądają na brak, a pandas widzi w nich zwykły tekst:
# example: silent-misses
print(orders.fillna({"Channel": "unknown"})["channel"].isna().sum())
codes = pd.Series(["PL", "", None, "n/a"])
print(codes.fillna("unknown").tolist())
2
['PL', '', 'unknown', 'n/a']
Przez wielką literę w Channel oba braki w kanale zostały na miejscu. Pusty napis i "n/a" też przetrwały. Takie znaczniki najpierw zamieniam na prawdziwy brak przez replace, a dopiero potem decyduję, co z nimi zrobić. Po każdym fillna liczę braki jeszcze raz i porównuję z liczbą sprzed zmiany.
Ostatnia rzecz ze starszych poradników: df.fillna(df.mean()) na tabeli z kolumną tekstową kończy się w pandas 3 błędem TypeError, bo średniej z tekstu nie da się policzyć. Pomaga df.mean(numeric_only=True), choć o średniej jako wypełnieniu napisałem już wyżej.
Krótkie ćwiczenie
Przepisz tabelę orders i dopisz zamówienie Z6 z pustym napisem w kanale, bez rabatu i z oceną 1. Zamień pusty napis na brak, uzupełnij kanał i rabat słownikiem i dodaj flagę dla rabatu. Sprawdź, ile braków zostało w każdej kolumnie, jaki typ ma rabat i czy średnia ocena zmieniła się tylko o nową odpowiedź. Na koniec posortuj serię stock malejąco po dacie i porównaj wynik ffill(limit=2) z wersją rosnącą.
Najważniejsze w pigułce
Dlaczego fillna(method='ffill') zwraca błąd?
W pandas 3.0 parametr method został usunięty z fillna, więc Python zgłasza TypeError o nieoczekiwanym argumencie. Zamiast niego używam df.ffill() albo df.bfill(), które mają też parametr limit.
Czy fillna zmienia oryginalny DataFrame?
Domyślnie zwraca nowy obiekt, więc wynik przypisuję z powrotem, na przykład df['discount'] = df['discount'].fillna(0). Zapis df['discount'].fillna(0, inplace=True) w pandas 3 nie zmienia tabeli, bo wybrana kolumna zachowuje się jak kopia.
Czym uzupełnić brakujące wartości: zerem, średnią czy poprzednią wartością?
Zależy od znaczenia braku. Zero tylko wtedy, gdy brak naprawdę oznacza zero, poprzednią wartość wtedy, gdy obowiązuje do następnej zmiany, a średnią ostrożnie, bo zmniejsza rozrzut danych. Brak odpowiedzi w ankiecie zwykle zostawiam jako brak.
