Baza wiedzy / Python

pandas nunique: liczba różnych wartości, axis i braki

Rozdzielam liczbę wierszy, znanych wartości i różnych klientów. Pokazuję poprawne wyniki axis=1 oraz dwie niezależne decyzje o brakach w grupowaniu.

Kajo RudzińskiOpublikowano Aktualizacja 4 min czytania

Pixelartowy stół z tacami powtarzających się kształtów i licznikami ich różnorodności

Ile różnych identyfikatorów klientów występuje w tabeli? To inne pytanie niż „ile jest wierszy”. Jeden klient może złożyć kilka zamówień, a w części rekordów identyfikatora może brakować. nunique pozwala policzyć różne wartości, ale trzeba świadomie ustalić, co zrobić z brakami i w jakim kierunku liczyć.

Pokażę metody Series.nunique() i DataFrame.nunique() na syntetycznych danych. Nie istnieje funkcja najwyższego poziomu pd.nunique(). Przykłady wykonałem w Pythonie 3.14.4, pandas 3.0.6 i 3.1.0, z NumPy 2.5.4, bez PyArrow. Bloki uruchamiaj kolejno w jednym skrypcie albo notebooku.

Seria: liczba wartości i decyzja o brakach

Zacznę od jednej kolumny. nunique() zwraca liczbę różnych wartości, domyślnie pomijając braki. Parametr dropna=False pozwala uwzględnić brak jako odrębny element w pokazanej serii o typie Int64. Wielka litera w nazwie typu oznacza typ pandas obsługujący pd.NA.

# example: series
import pandas as pd

customers = pd.Series([10, 10, 20, pd.NA, 20, pd.NA], dtype="Int64")
print(len(customers))
print(customers.count())
print(customers.nunique())
print(customers.nunique(dropna=False))
6
4
2
3

Mamy sześć pozycji, cztery znane wartości, dwóch różnych znanych klientów i trzy różne elementy po uwzględnieniu braku. Żadna z tych liczb nie zastępuje pozostałych. Gdy opisujesz raport, nazwij dokładnie, co policzyłeś. „Liczba klientów” bez reguły dotyczącej pustych identyfikatorów pozostawia pole do błędnej interpretacji.

W szczególności dwa puste identyfikatory nie oznaczają dwóch znanych osób ani jednej ustalonej osoby. Uwzględnienie braku w liczniku jest operacją techniczną. Jeśli chcesz policzyć rzeczywistych klientów, potrzebujesz informacji o tożsamości, której te rekordy nie dostarczają. Dokumentacja Series.nunique opisuje samą regułę liczenia.

DataFrame: oddzielny wynik dla każdej kolumny

Domyślne axis=0 daje liczbę różnych wartości w każdej kolumnie. Wynik jest serią indeksowaną nazwami kolumn. Poniżej zamieniam go na słownik, żeby wydruk był krótki i łatwy do porównania. Kolumna C zawiera liczby oraz braki, więc pandas reprezentuje ją jako zmiennoprzecinkową.

# example: columns
frame = pd.DataFrame({
    "A": [1, 2, 2, 3, 3, 3],
    "B": ["a", "b", "a", "b", "b", "b"],
    "C": [None, 1, 2, 2, 2, None],
})
print(frame.nunique().to_dict())
print(frame.nunique(dropna=False).to_dict())
{'A': 3, 'B': 2, 'C': 2}
{'A': 3, 'B': 2, 'C': 3}

A ma wartości 1, 2 i 3. B ma dwie etykiety. C ma dwie znane liczby, a po uwzględnieniu braku licznik rośnie do trzech. Nie sumuję tych wyników, żeby otrzymać liczbę różnych wierszy. Unikalność poszczególnych kolumn i unikalność ich kombinacji opisują różne cechy tabeli.

Axis 1 porównuje wartości wewnątrz wiersza

Przy axis=1 każda pozycja wyniku opisuje jeden wiersz. W naszym przykładzie porównujemy wartości z A, B i C. Zwróć uwagę na wiersz o indeksie 2: liczby 2 i 2.0 są równe, a tekst a jest drugim różnym elementem. Wynik wynosi dwa, a nie trzy.

# example: rows
print(frame.nunique(axis=1).tolist())
print(frame.nunique(axis=1, dropna=False).tolist())
[2, 3, 2, 3, 3, 2]
[3, 3, 2, 3, 3, 3]

To poprawione wyniki dla dokładnie podanych danych. Dokumentacja DataFrame.nunique rozróżnia oba kierunki liczenia. Operacja wierszowa ma sens, gdy pola są porównywalne, na przykład trzy odpowiedzi na to samo pytanie. Porównywanie wieku, kodu produktu i kwoty tylko dlatego, że stoją obok siebie, zwykle nie tworzy użytecznej miary.

Nie myl tej operacji z usuwaniem powtarzających się wierszy. axis=1 nie sprawdza, czy wiersz 2 jest kopią wiersza 3. Sprawdza różnorodność wartości wewnątrz każdej pojedynczej obserwacji. Do pracy z duplikatami całych rekordów służą inne metody.

Pixelartowa macierz z osobnymi wynikami liczenia wzdłuż kolumn i wierszy

Grupowanie ma osobną regułę dla brakującego klucza

Chcę policzyć klientów w regionach. dropna przekazane do groupby decyduje, czy zachować grupę z brakującym regionem. dropna przekazane do nunique decyduje, czy policzyć brakujący identyfikator wewnątrz każdej grupy. To dwa różne miejsca podejmowania decyzji, mimo tej samej nazwy parametru.

# example: groups
orders = pd.DataFrame({
    "region": pd.Series(["north", "north", "south", "south", pd.NA, pd.NA], dtype="string"),
    "customer_id": pd.Series([1, 2, 2, pd.NA, 3, pd.NA], dtype="Int64"),
})
grouped = orders.groupby("region", dropna=False)["customer_id"]
print(grouped.nunique().index.tolist())
print(grouped.nunique().tolist())
print(grouped.nunique(dropna=False).tolist())
['north', 'south', <NA>]
[2, 1, 1]
[2, 2, 2]

W południowym regionie znamy jednego klienta, a druga obserwacja nie ma identyfikatora. W grupie bez regionu sytuacja jest podobna. Gdyby zostawić domyślne pomijanie brakujących kluczy grupowania, ostatnia grupa nie trafiłaby do wyniku. Przy kontroli jakości chcę ją zobaczyć, nawet jeśli późniejszy raport biznesowy będzie prezentował ją osobno.

Licznik nie pokazuje częstotliwości ani wszystkich błędów

nunique odpowiada „ile różnych”, a unique pokazuje „jakie”. value_counts pozwala sprawdzić, jak często występują poszczególne wartości. Dwie kolumny mogą mieć po dwie różne etykiety, chociaż w jednej proporcja wynosi pół na pół, a w drugiej jedna etykieta pojawia się prawie zawsze. Sam licznik tego nie ujawni.

Jeśli sprawdzam klucz tabeli, kontroluję także braki i powtórzenia. Równość liczby różnych znanych wartości z liczbą wierszy jest przydatnym sygnałem dla pojedynczej kolumny, ale nie potwierdza sensu identyfikatora w biznesie. Nie dowodzi również, że dane z dwóch systemów oznaczają te same osoby. Do porównania kluczy złożonych trzeba uwzględnić kombinację pól.

Ćwiczenie: same braki i pusta seria

Zanim użyję licznika w automatycznym raporcie, sprawdzam brak znanych obserwacji. Seria zawierająca dwa braki i seria bez elementów mają ten sam domyślny wynik zero. Po uwzględnieniu braków ich wyniki się różnią. To pomaga świadomie opisać stan „nie znam wartości” oraz „nie otrzymałem żadnych rekordów”.

# example: boundaries
missing = pd.Series([pd.NA, pd.NA], dtype="Int64")
empty = pd.Series([], dtype="Int64")
print(missing.nunique(), missing.nunique(dropna=False))
print(empty.nunique(), empty.nunique(dropna=False))
0 1
0 0

Dopisz teraz znaną wartość do każdej serii i przewidź oba liczniki. Zastanów się też, czy pusty napis w tekstowej kolumnie powinien być uznany za brak. Pandas nie podejmie tej decyzji za Ciebie. Listę etykiet sprawdzisz przez unique, a wybór wierszy omówiłem przy drop_duplicates.

Najważniejsze w pigułce

Czy istnieje pandas.nunique()?

Nie ma funkcji najwyższego poziomu pd.nunique. Użyj metody Series.nunique albo DataFrame.nunique; nunique jest także dostępne na odpowiednich obiektach grupowania.

Co robi axis=1 w nunique?

Liczy różne wartości wewnątrz każdego wiersza. Nie liczy różnych wierszy tabeli i nie wykrywa, czy jeden rekord jest kopią innego.

Czy nunique liczy braki?

Domyślnie je pomija. dropna=False uwzględnia je w pokazanych jednorodnych seriach. Parametr dropna w groupby osobno decyduje o zachowaniu brakującego klucza grupy.

Kajo Rudziński

Newsletter KajoData

Zostańmy w kontakcie.

Piszę o analizie danych, uczeniu się i zmianie pracy. Dzielę się tym, co pomaga mi ogarniać technologię i zawodowe decyzje. Jeśli lubisz ten sposób myślenia, zapraszam do mojego newslettera.