Baza wiedzy / Python
Pandas to_numeric: konwersja liczb, błędy i downcast w pandas 3
Jak zamienić tekst na liczby, zachować błędne rekordy do kontroli i uniknąć pułapek downcast, przecinka dziesiętnego oraz braków. Przykłady dla pandas 3.

Masz kolumnę, która wygląda jak liczby, ale suma nie działa albo wynik znika po czyszczeniu? Zaczynam od dwóch pytań: co naprawdę znajduje się w komórkach i które wartości wolno uznać za niepoprawne. pd.to_numeric() rozwiązuje konwersję. Nie podejmie za mnie decyzji, czy tekst brak oznacza brak pomiaru, błąd eksportu, czy zero.
Pokażę Ci konwersję na małych, syntetycznych danych oraz sposób kontroli tego, co nie przeszło. Przykłady wykonałem w Pythonie 3.14.4, pandas 3.0.6 i NumPy 2.5.4, bez PyArrow. Uruchamiaj bloki po kolei w jednym skrypcie lub notebooku. Zachowuję angielskie nazwy zmiennych i identyczny kod w wersji PL oraz EN, żeby wyniki dało się łatwo porównać.
Co konwertuje to_numeric
pd.to_numeric(arg, errors="raise", downcast=None) przyjmuje między innymi pojedynczą wartość, listę albo serię. Dla Series dostajesz serię z zachowanym indeksem, dla listy tablicę. Cały dwuwymiarowy DataFrame nie jest prawidłowym argumentem. Przy tabeli wybieram konkretne kolumny, zamiast prosić funkcję o odgadnięcie ich znaczenia.
Najczęściej interesują mnie dwa ustawienia: errors="raise" zatrzymuje konwersję przy niepoprawnym zapisie, a errors="coerce" wstawia w takim miejscu brak. W przykładach z domyślnymi typami NumPy tym brakiem jest NaN. Wariant z typami nullable może używać pd.NA. To reprezentacja techniczna; powód braku nadal trzeba zachować osobno.
Wynik nie zawsze ma ten sam typ. Seria tekstów z samymi liczbami całkowitymi może dać int64, a ułamki albo braki wymagające NaN często prowadzą do float64. Już numeryczne wejście może zachować własny typ. Dlatego oglądam dtype wyniku, nie wyciągam wniosku z tego, że w podglądzie widzę kilka liczb bez części dziesiętnej. Pełna dokumentacja to_numeric opisuje obsługiwane argumenty.
Konwersja z kontrolą błędów
W danych mamy trzy prawidłowe wartości, niepoprawny tekst oraz jeden brak obecny od początku. Jawny dtype="object" pozwala pokazać oryginalne None obok tekstów bez zależności od automatycznego wyboru typu tekstowego.
# example: basic
import pandas as pd
raw = pd.Series(["12", "20.5", "bad", None, "0"], name="raw", dtype="object")
numeric = pd.to_numeric(raw, errors="coerce")
invalid = raw.notna() & numeric.isna()
audit = pd.DataFrame({"raw": raw, "numeric": numeric, "invalid": invalid})
print(audit.to_string(index=False))
print(numeric.dtype)
print("invalid:", int(invalid.sum()), "missing:", int(raw.isna().sum()))
raw numeric invalid
12 12.0 False
20.5 20.5 False
bad NaN True
None NaN False
0 0.0 False
float64
invalid: 1 missing: 1
Najważniejsza jest maska raw.notna() & numeric.isna(). Wskazuje wiersz, który miał wartość przed konwersją, a stracił ją po konwersji. Dlatego bad jest błędem, a None nie jest nowym błędem. Samo numeric.isna().sum() zwróciłoby dwa i połączyło dwie różne sytuacje.
Nie nadpisuję tu raw. Mogę wrócić do wartości źródłowej, pokazać ją autorowi pliku i sprawdzić, czy problem dotyczy pojedynczego wpisu czy całej kolumny. Przy większych danych wystarczy zachować identyfikator rekordu i próbkę odrzuconych wartości w kontrolowanym miejscu. Nie potrzebuję do diagnozy wypisywać całego eksportu do logów.
Jeżeli źródło ma gwarantować poprawne liczby, wolę zatrzymać przetwarzanie:
# example: raise
try:
pd.to_numeric(raw, errors="raise")
except ValueError:
print("Invalid numeric value; inspect the source column")
Invalid numeric value; inspect the source column
Komunikat w przykładzie jest mój, a nie dosłownym tekstem wyjątku pandas. Łapię konkretny ValueError; nie ukrywam dowolnej awarii programu. W produkcyjnym procesie po takim błędzie trzeba jawnie zdecydować o zatrzymaniu zadania lub odłożeniu danych do naprawy. Sam print nie stanowi kompletnej obsługi błędu.
coerce jest przydatne, gdy świadomie dopuszczam częściowo poprawny plik. Dokładam wtedy licznik i regułę akceptacji, na przykład odrzucenie importu, jeśli błędnych sztuk jest więcej niż uzgodniony próg. Konkretny próg zależy od zastosowania. Nie ma sensownego uniwersalnego procentu, który można wkleić do każdego projektu.

Dlaczego errors ignore już nie działa
Starsze poradniki pokazują errors="ignore". W pandas 3 ten wariant został usunięty, co odnotowują informacje o wydaniu 3.0. Nie używaj go jako trzeciego działającego trybu.
# example: ignore-removed
try:
pd.to_numeric(raw, errors="ignore")
except ValueError:
print("errors='ignore' is not supported in pandas 3")
errors='ignore' is not supported in pandas 3
Jeśli naprawdę chcesz zachować pierwotną kolumnę przy nieudanej konwersji, wykonaj raise we własnym try/except i zdecyduj, co zrobić w gałęzi błędu. Zwrócenie raz liczb, a raz tekstu pod tą samą nazwą utrudnia dalszą analizę. Preferuję osobną kolumnę wynikową i jawny status walidacji. Wtedy użytkownik raportu nie musi zgadywać, czy operacja się udała.
Downcast i typy z brakami
downcast to próba użycia mniejszego typu, gdy wartości na to pozwalają. Dla małych liczb całkowitych efekt widać od razu:
# example: downcast-integer
whole = pd.to_numeric(pd.Series(["12", "20", "0"]), downcast="integer")
print(whole.tolist())
print(whole.dtype)
[12, 20, 0]
int8
int8 wystarcza do tych trzech wartości. Gdyby pojawiła się większa liczba, funkcja musiałaby dobrać szerszy typ. Nie traktuję tego wyniku jako stałej umowy dla wszystkich przyszłych plików.
A teraz te same ustawienia dla ułamka i braków:
# example: downcast-fraction
fractional = pd.to_numeric(raw, errors="coerce", downcast="integer")
print(fractional.tolist())
print(fractional.dtype)
[12.0, 20.5, nan, nan, 0.0]
float64
Wynik pozostaje float64. downcast="integer" nie oznacza obcięcia 20.5 do 20. Nie usuwam ułamka, żeby wymusić mniejszy rozmiar kolumny. Najpierw ustalam, czy w tej dziedzinie ułamki są dozwolone, a dopiero później rozważam pamięć i typ docelowy.
Dla całkowitych wartości z brakami mogę świadomie wybrać backend nullable:
# example: nullable
counts = pd.to_numeric(
pd.Series(["12", None, "0"]),
errors="raise", downcast="integer", dtype_backend="numpy_nullable",
)
print(counts.tolist())
print(counts.dtype)
[12, <NA>, 0]
Int8
Int8 z wielką literą to typ rozszerzony pandas z obsługą pd.NA; int8 to inny typ i nie pomieści tego braku. Zapis [12, <NA>, 0] zachowuje różnicę między nieznaną liczbą a rzeczywistym zerem. Szczegóły opisuje przewodnik po nullable integers.
Istnieje też dtype_backend="pyarrow", ale wymaga osobnej biblioteki PyArrow. Nie wykonywałem tego wariantu w opisanym środowisku i nie przypisuję mu wyników powyższych testów. Przy wyborze backendu sprawdź również zgodność dalszych operacji i eksportu, nie tylko nazwę typu. Dokumentacja integracji PyArrow pokazuje odrębne wymagania.
Przecinek i separator tysięcy
to_numeric nie jest parserem wszystkich lokalnych zapisów liczb. 12,75 nie stanie się automatycznie 12.75. Najpierw ustalam format źródła. W poniższym przykładzie kontrakt dopuszcza przecinek dziesiętny, zwykłe spacje między pełnymi grupami tysięcy, opcjonalny znak oraz całkowite liczby bez separatorów. Nie dopuszcza kropek, symboli walut ani pustego tekstu.
# example: locale
local_raw = pd.Series(
["1 234,50", "12,75", None, "1,234.50", "-2 000,00", "1 2,00", ""],
dtype="string",
)
text = local_raw.str.strip()
valid_format = text.str.fullmatch(
r"[+-]?(?:\d{1,3}(?: \d{3})+|\d+)(?:,\d+)?"
).fillna(False)
normalised = text.where(valid_format).str.replace(" ", "", regex=False).str.replace(
",", ".", regex=False
)
local_numeric = pd.to_numeric(normalised, errors="raise", dtype_backend="numpy_nullable")
local_invalid = local_raw.notna() & ~valid_format
print(local_numeric.tolist())
print(local_invalid.tolist())
[1234.5, 12.75, <NA>, <NA>, -2000.0, <NA>, <NA>]
[False, False, False, True, False, True, True]
Wzorzec odrzuca 1,234.50, bo miesza inny format, oraz 1 2,00, bo grupa tysięcy ma złą długość. Ostatni pusty tekst także jest błędem według przyjętej umowy. Źródłowe None pozostaje brakiem, nie błędem formatu. Jeśli w Twoim źródle pusty tekst oznacza brak, dodaj tę regułę świadomie przed walidacją i zachowaj informację o oryginale.
Nie usuwam globalnie wszystkich przecinków i kropek. Zapis 1,234 może znaczyć różne liczby zależnie od konwencji. Tutaj oznacza 1.234, bo przecinek jest dziesiętny. Przy mieszanych konwencjach trzeba najpierw ustalić pochodzenie wierszy; pojedyncza zamiana znaków nie rozwiązuje niejednoznaczności.
Ten wzorzec nie obejmuje spacji nierozdzielającej ani wąskiej spacji nierozdzielającej. Jeśli występują w eksporcie, rozpoznaj je i dodaj konkretną normalizację do kontraktu. Nie opisuję obecnego przykładu jako uniwersalnego importera europejskich liczb.
Przy spójnym CSV można rozwiązać część problemu już podczas wczytania. read_csv ma parametry decimal i thousands, opisane w dokumentacji importu CSV. Ich ustawienie nie zwalnia z kontroli, czy kolumna rzeczywiście dostała oczekiwany typ i czy nie zawiera wyjątków.
Kilka kolumn i identyfikatory
Nie wszystko, co składa się z cyfr, jest miarą. Identyfikator 0012 ma zachować zera z przodu. Wybieram więc tylko dwie kolumny do konwersji:
# example: columns
orders = pd.DataFrame({
"id": ["0012", "0013"],
"quantity": ["2", "bad"],
"amount": ["12.5", "8"],
})
columns = ["quantity", "amount"]
converted = orders.copy()
converted[columns] = orders[columns].apply(pd.to_numeric, errors="coerce")
print(converted.to_string(index=False))
print((orders[columns].notna() & converted[columns].isna()).sum().to_dict())
id quantity amount
0012 2.0 12.5
0013 NaN 8.0
{'quantity': 1, 'amount': 0}
apply przekazuje do to_numeric każdą wybraną serię. Zachowuję kopię oryginalnej tabeli oraz licznik nowych braków dla każdej kolumny. Dzięki temu widzę, że problem dotyczy quantity, a amount przeszło konwersję.
Nie zastępuję wszystkich braków zerem na końcu łańcucha metod. W naszym przykładzie byłoby to twierdzenie, że niepoprawnie zapisana liczba sztuk wynosi zero. Tego nie wiemy. Podobnie nie konwertuję numerów telefonu, kodów pocztowych ani długich identyfikatorów tylko dlatego, że wyglądają numerycznie. Jeśli nie zamierzasz ich dodawać lub porównywać jako wielkości, tekst często jest właściwą reprezentacją.
Nieskończoność i precyzja
Poprawne parsowanie nie oznacza jeszcze poprawnej wartości biznesowej:
# example: finite
import numpy as np
special = pd.to_numeric(pd.Series(["3", "inf", "-inf", "bad"]), errors="coerce")
print(special.tolist())
print(np.isfinite(special).tolist())
[3.0, inf, -inf, nan]
[True, False, False, False]
inf przechodzi jako nieskończoność. Jeśli kolumna ma zawierać skończony pomiar, potrzebuję dodatkowej kontroli np.isfinite, a często też zakresu, jednostki i dopuszczalnego znaku. errors="coerce" nie wykonuje tych kontroli za mnie.
Zwróć też uwagę na duże liczby. Ten przykład celowo pokazuje utratę dokładności po przejściu do typu zmiennoprzecinkowego:
# example: precision
identifier = "9007199254740993"
exact_integer = pd.to_numeric(pd.Series([identifier]))
approximate_float = pd.to_numeric(pd.Series([identifier, "0.5"]))
print(int(exact_integer.iloc[0]))
print(int(approximate_float.iloc[0]))
9007199254740993
9007199254740992
Pierwsza seria mieści dokładną liczbę w typie całkowitym. Druga musi pomieścić również ułamek, a reprezentacja float64 nie zachowuje dokładnie wskazanej wartości całkowitej. To kolejny powód, żeby identyfikatorów nie przerabiać na miary. Przy wymaganiu dokładnej arytmetyki dziesiętnej trzeba wybrać odpowiedni model danych; samo downcast nie daje takiej gwarancji.
Puste dane i ćwiczenie
Po udanej konwersji nadal sprawdzam liczbę obserwacji:
# example: empty
empty = pd.to_numeric(pd.Series([], dtype="object"), errors="coerce")
all_missing = pd.to_numeric(pd.Series([None, None], dtype="object"), errors="coerce")
print(len(empty), all_missing.isna().tolist())
print(all_missing.sum(), all_missing.sum(min_count=1))
0 [True, True]
0.0 nan
Pusta seria ma zero elementów. Seria zawierająca wyłącznie braki nie daje żadnego pomiaru, choć domyślne sum() pokazuje 0.0. min_count=1 wymaga przynajmniej jednej niepustej wartości i tutaj zwraca brak. Nie myl tego z sumą poprawnych zer.
Na koniec małe ćwiczenie. Dla wejścia "7", "oops", None, "0", "2.5" przewidź liczbę nowych braków oraz typ po downcast="integer". Dopiero potem uruchom kod:
# example: exercise
practice = pd.Series(["7", "oops", None, "0", "2.5"], dtype="object")
answer = pd.to_numeric(practice, errors="coerce", downcast="integer")
print(answer.tolist())
print(int((practice.notna() & answer.isna()).sum()))
print(answer.dtype)
[7.0, nan, nan, 0.0, 2.5]
1
float64
Nowy błąd jest jeden, źródłowy brak pozostaje osobną sytuacją, a ułamek utrzymuje typ zmiennoprzecinkowy. Gdy potrafisz wyjaśnić te trzy rzeczy, możesz dołożyć konwersję do większego procesu bez zgadywania, co zniknęło po drodze. Następnym krokiem może być świadoma obsługa fillna, ale dopiero po ustaleniu znaczenia braków.
Najważniejsze w pigułce
Czy to_numeric przyjmuje cały DataFrame?
Nie jako dwuwymiarową tabelę. Wybierz serię lub zastosuj funkcję do jawnie wybranych kolumn przez apply. Zostaw identyfikatory jako tekst, jeśli cyfry nie oznaczają wielkości do obliczeń.
Dlaczego po downcast integer nadal widzę float64?
Dane mogą zawierać ułamki albo braki reprezentowane przez NaN. Downcast nie jest poleceniem obcinania części dziesiętnej. Dla całkowitych danych z brakami rozważ nullable i sprawdź rzeczywisty typ wyniku.
Czy coerce wystarczy do czyszczenia danych?
Nie. Wykrywa nieudane parsowanie, ale nie zna dopuszczalnego zakresu, znaczenia jednostek ani lokalnej konwencji zapisu. Zachowaj oryginał, policz nowe braki i zweryfikuj wartości po konwersji.
