Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Pandas - notes terenowy, który liczy sam

Wracasz z obchodu, @name, i masz w notesie cztery wiersze. Lew: 120 sztuk, sawanna, gatunek zagrożony. Słoń: 450, las, niezagrożony. Gepard: 85, sawanna, zagrożony. Żyrafa: 200, sawanna, niezagrożony. W jednym wierszu siedzą trzy zupełnie różne rodzaje informacji: nazwa gatunku, czyli tekst, liczebność, czyli liczba, oraz odpowiedź tak albo nie. NumPy z poprzedniej lekcji policzy średnią z liczb szybciej, niż zdążysz mrugnąć, ale zanim to zrobi, musisz podać mu same liczby. A notes terenowy to nie kolumna liczb, tylko tabela.

Pandas to biblioteka, która zamienia Pythona w narzędzie do pracy z takimi tabelami. Nie zastępuje NumPy - jest na nim zbudowana i pod spodem liczy dokładnie tymi samymi tablicami. Dokłada natomiast dwie rzeczy, których w NumPy nie ma: każda kolumna dostaje nazwę i każda kolumna może mieć własny typ danych. To dokładnie ta różnica, która dzieli surowy pomiar od porządnie prowadzonego dziennika ekspedycji.

Dlaczego tablica NumPy tu nie wystarczy

Zanim sięgniemy po nowe narzędzie, warto zobaczyć na własne oczy, gdzie stare przestaje działać. Spróbujmy wcisnąć dwa wiersze notesu do tablicy NumPy, tak jak robiliśmy to z liczbami w poprzedniej lekcji. Funkcja

np.array
przyjmie listę list bez mrugnięcia okiem, bo formalnie nic złego się nie dzieje. Interesuje nas to, co wyjdzie po drugiej stronie: atrybut
dtype
mówi, jakim jednym typem NumPy opisał całą zawartość tablicy, bo w NumPy wszystkie elementy muszą dzielić jeden typ.

1import numpy as np
2
3raw = np.array([["Lion", 120], ["Elephant", 450]])
4
5print(raw)
6# [['Lion' '120']
7#  ['Elephant' '450']]
8
9print(raw.dtype)  # <U21

Zwróć uwagę na apostrofy wokół liczb w wydruku:

'120'
zamiast
120
. Skoro tablica musi mieć jeden typ, a "Lion" nie da się zamienić na liczbę, to NumPy poszedł w drugą stronę i zamienił liczby na tekst. Skrót
<U21
znaczy właśnie "łańcuch znaków o długości do dwudziestu jeden". Twoje pomiary przestały być liczbami: dodawanie ich skończy się sklejeniem napisów, a średnia w ogóle nie przejdzie. Do tego dochodzi drugi kłopot, cichszy, ale równie dotkliwy - kolumny nie mają nazw, więc do końca analizy musisz pamiętać, że populacja siedzi pod numerem jeden. Pandas rozwiązuje oba problemy naraz i po to powstał.

Instalacja i skrót
pd

Pandas nie wchodzi w skład standardowej biblioteki Pythona, więc trzeba go doinstalować. Robi to

pip
, czyli menedżer pakietów Pythona, tym samym poleceniem, którym instalowałeś wcześniej inne biblioteki. Instalacja pociągnie za sobą NumPy, jeśli jeszcze go nie masz, bo Pandas bez NumPy nie ruszy - to zależność, nie sugestia. Polecenie wykonujesz w terminalu, a nie w pliku z kodem, i tylko raz na środowisko. Jeśli pracujesz w Google Colab albo w dystrybucji Anaconda, Pandas jest już na miejscu i możesz ten krok pominąć.

1pip install pandas

Po instalacji nic się jeszcze nie wydarzyło w Twoim programie: pakiet wylądował na dysku, ale żaden plik z kodem o nim nie wie. Do tego służy osobne polecenie w samym Pythonie. Przyjęło się nadawać bibliotece skrót

pd
za pomocą słowa kluczowego
as
, które tworzy alias, czyli drugą, krótszą nazwę na tę samą rzecz. To wyłącznie konwencja, ale konwencja tak powszechna, że każdy przykład w dokumentacji, każda odpowiedź w sieci i każde zadanie w tym module zakładają właśnie
pd
.

1import pandas as pd

Ta jedna linia wciąga bibliotekę do pamięci i wiesza na niej etykietę

pd
. Nic nie zostało wczytane, policzone ani wyświetlone - dane pojawią się dopiero w kolejnych poleceniach. Wariant
import pandas
też jest poprawny, tylko wtedy do każdej funkcji musiałbyś pisać pełne
pandas.DataFrame
, co przy trzydziestu wywołaniach w skrypcie robi się męczące. Nie zadziała za to
import pd
: pakiet nazywa się
pandas
, więc Python zakończy błędem
ModuleNotFoundError: No module named 'pd'
. Skrót nadajesz sam, w tej właśnie linii, i dopiero od niej zaczyna on istnieć.

Series - jedna kolumna z etykietami

Zacznijmy od najmniejszego kawałka notesu: jednej kolumny. Lista

[120, 450, 85, 200]
przechowuje cztery liczebności, ale nie mówi ani słowa o tym, do kogo należy osiemdziesiąt pięć. Pandas ma na to strukturę o nazwie Series. Series jest jednowymiarowa: to pojedynczy ciąg wartości, a nie tabela z wierszami i kolumnami naraz. Do wartości dokłada indeks, czyli zestaw etykiet, po jednej na wartość, oraz opcjonalną nazwę całego ciągu, którą podajesz parametrem
name
. Zapamiętaj tę definicję dokładnie, bo za chwilę odróżnimy Series od struktury, która naprawdę jest tabelą.

1populations = pd.Series([120, 450, 85, 200], name="population")
2
3print(populations)
4# 0    120
5# 1    450
6# 2     85
7# 3    200
8# Name: population, dtype: int64

Po lewej stronie wydruku widzisz indeks, którego nigdzie nie podawałeś: Pandas dorobił go sam jako kolejne liczby od zera. To ważne, bo Series ma indeks zawsze, nawet gdy o niego nie prosisz. Po prawej stoją Twoje wartości, pod spodem nazwa serii i

dtype: int64
, czyli typ wspólny dla wszystkich elementów. Pod tym względem Series zachowuje się jak tablica NumPy i nie ma w tym przypadku: w środku naprawdę siedzi tablica NumPy. Sama lista, którą podałeś, nie została w żaden sposób zmieniona - Pandas skopiował z niej wartości do nowej struktury.

Indeks nie musi być ciągiem liczb i właśnie w tym tkwi jego siła. Możesz podać własne etykiety parametrem

index
, przekazując listę tak samo długą jak lista wartości. Wtedy zamiast pamiętać, że osiemdziesiąt pięć to trzeci pomiar, sięgasz po wartość nazwą gatunku, w nawiasach kwadratowych, dokładnie tak jak po elemencie słownika.

1species = pd.Series(
2    [120, 450, 85, 200],
3    index=["Lion", "Elephant", "Cheetah", "Giraffe"],
4    name="population",
5)
6
7print(species["Lion"])     # 120
8print(species["Cheetah"])  # 85

Etykiety zastąpiły numery od zera, ale liczby zostały te same i w tej samej kolejności - indeks opisuje wartości, a nie miesza nimi. Zwróć uwagę, że nazwy gatunków nie są drugą kolumną. To wciąż jedna kolumna liczb, tyle że każda ma przypiętą karteczkę z nazwą. Gdybyś chciał trzymać gatunek i liczebność jako dwie równorzędne kolumny, potrzebujesz innej struktury i poznasz ją w następnej sekcji.

Skoro w Series siedzi tablica NumPy, to działają na niej te same sztuczki co w poprzedniej lekcji. Porównanie

species > 100
nie zwraca jednego
True
, tylko wykonuje porównanie osobno dla każdej wartości i oddaje Series wypełnioną wartościami logicznymi. Taką serię nazywamy maską logiczną. Włożona z powrotem w nawiasy kwadratowe wybiera tylko te elementy, dla których wypadło
True
.

1print(species > 100)
2# Lion         True
3# Elephant     True
4# Cheetah     False
5# Giraffe      True
6# Name: population, dtype: bool
7
8print(species[species > 100])
9# Lion        120
10# Elephant    450
11# Giraffe     200
12# Name: population, dtype: int64

Gepard zniknął z wyniku, bo osiemdziesiąt pięć nie przekracza setki, ale nie zniknął z

species
- filtrowanie zwraca nową serię, a oryginał zostaje nietknięty. Zauważ też, że etykiety pojechały razem z wartościami: w wyniku dalej stoi "Elephant" obok czterystu pięćdziesięciu, choć w środku został tylko co trzeci wiersz. To zachowanie przeniesiemy za moment na całe tabele i będzie ono podstawą filtrowania danych w Pandas.

DataFrame - cała tabela naraz

Jedna Series to jedna kolumna, a Twój notes ma ich cztery. Potrzebujesz struktury, która trzyma wiersze i kolumny naraz, i taką strukturą w Pandas jest DataFrame. DataFrame jest dwuwymiarowy: ma tyle wierszy, ile obserwacji, i tyle kolumn, ile cech opisujesz. Każda pojedyncza kolumna DataFrame jest Series, więc obie struktury nie konkurują ze sobą, tylko składają się jedna z drugiej. Najprostszy sposób zbudowania tabeli to podanie słownika: klucz zostaje nazwą kolumny, a wartość, czyli lista, wypełnia tę kolumnę od góry do dołu. Wszystkie listy muszą mieć tę samą długość, bo inaczej wiersze by się nie domknęły.

1safari_data = pd.DataFrame({
2    "species": ["Lion", "Elephant", "Cheetah", "Giraffe"],
3    "population": [120, 450, 85, 200],
4    "habitat": ["Savanna", "Forest", "Savanna", "Savanna"],
5    "endangered": [True, False, True, False],
6})
7
8print(safari_data)
9#     species  population  habitat  endangered
10# 0      Lion         120  Savanna        True
11# 1  Elephant         450   Forest       False
12# 2   Cheetah          85  Savanna        True
13# 3   Giraffe         200  Savanna       False

Cztery klucze słownika stały się nagłówkami kolumn, w takiej kolejności, w jakiej je wpisałeś. Po lewej stronie znów pojawił się indeks od zera, dorobiony automatycznie tak samo jak w Series. Najważniejsze jest jednak to, czego tu nie widać: liczby nie zamieniły się w tekst. Kolumna

population
trzyma liczby całkowite,
species
i
habitat
tekst, a
endangered
wartości logiczne, i wszystkie trzy typy żyją obok siebie w jednej tabeli. Dokładnie tego nie potrafiła tablica NumPy z początku lekcji. Nic ponadto się nie stało - Pandas niczego nie posortował, nie policzył i nie poprawił.

Trzy nazwy, które nie oznaczają tabeli

Wokół DataFrame krąży kilka nazw, które łatwo pomylić, a które oznaczają coś zupełnie innego. Series to, jak już wiesz, struktura jednowymiarowa - jedna kolumna z indeksem, nie tabela. Panel był kiedyś w Pandas strukturą trójwymiarową, czyli stosem kilku tabel ułożonych jedna na drugiej, ale został wycofany i od wersji 1.0 po prostu nie istnieje; próba użycia

pd.Panel
kończy się błędem
AttributeError
. Table z kolei nigdy w Pandas nie było - taka klasa występuje w innych bibliotekach, na przykład do obsługi plików astronomicznych, i do naszej tabeli nie ma żadnego stosunku. Nie jest nią też lista zagnieżdżonych słowników: to zwykła struktura Pythona, którą owszem, możesz przekazać do
pd.DataFrame
, ale sama z siebie nie jest ani Series, ani żadną strukturą Pandas. Możesz to sprawdzić bez wiary na słowo, funkcją
hasattr
, która odpowiada na pytanie "czy ten obiekt ma taką nazwę w środku".

1print(hasattr(pd, "DataFrame"))  # True
2print(hasattr(pd, "Series"))     # True
3print(hasattr(pd, "Panel"))      # False
4print(hasattr(pd, "Table"))      # False

Dwie prawdy i dwa fałsze, czarno na białym. W bibliotece Pandas mieszkają dokładnie dwie podstawowe struktury danych: jednowymiarowa

Series
i dwuwymiarowa
DataFrame
. Kiedy ktoś mówi o tabeli z wierszami i kolumnami w Pandas, mówi o DataFrame - i to jedyna poprawna odpowiedź na to pytanie. Samo wywołanie
hasattr
niczego przy okazji nie utworzyło ani nie zmieniło; sprawdziło tylko, co jest, a czego nie ma.

Dane z terenu przychodzą w pliku CSV

Słownik nadaje się do czterech wierszy. Przy trzech tysiącach odczytów z obroży GPS nikt nie będzie przepisywał danych ręcznie - dostajesz je jako plik CSV, czyli comma-separated values, wartości rozdzielone przecinkami. To zwykły plik tekstowy o bardzo prostej budowie: pierwszy wiersz to nagłówek z nazwami kolumn, a każdy następny to jedna obserwacja. Otwórz taki plik w dowolnym edytorze, a zobaczysz coś takiego.

1species,population,habitat,endangered
2Lion,120,Savanna,True
3Elephant,450,Forest,False
4Cheetah,85,Savanna,True
5Giraffe,200,Savanna,False

Pięć wierszy tekstu: jeden nagłówek i cztery obserwacje, dokładnie te same dane, które przed chwilą wpisywaliśmy słownikiem. Żadnych typów, żadnych nawiasów, żadnej struktury poza przecinkami. Zamianą tego pliku na gotową tabelę zajmuje się funkcja

read_csv
. Podajesz jej nazwę pliku jako tekst, a dostajesz z powrotem DataFrame. Wynik przypisujemy do zmiennej
df
- to tradycyjny skrót od DataFrame i taką nazwę zobaczysz w niemal każdym przykładzie w sieci.

1df = pd.read_csv("safari_species.csv")
2
3print(df.shape)                 # (4, 4)
4print(df["population"].sum())   # 855

Cztery wiersze i cztery kolumny, a suma liczebności wyszła jako liczba, nie jako sklejony napis - to znaczy, że

read_csv
sam rozpoznał typy kolumn. Nagłówek z pliku stał się nazwami kolumn, wiersze tekstu stały się wierszami tabeli, a Ty nie napisałeś ani jednej linii konwersji. Zapamiętaj to wywołanie znak po znaku, bo będziesz je pisał codziennie:
pd
,
.
,
read_csv
,
(
,
'data.csv'
,
)
. Kolejność jest sztywna - najpierw skrót biblioteki, potem kropka, nazwa funkcji, nawias otwierający, nazwa pliku w cudzysłowie i nawias zamykający. Funkcja należy do modułu
pd
, a nie do tabeli, więc
df.read_csv(...)
nie ma sensu: w chwili wczytywania nie masz jeszcze żadnego
df
. Bliźniacze funkcje
pd.read_excel
i
pd.read_json
działają tak samo dla arkuszy i plików JSON, tyle że pierwsza wymaga doinstalowania dodatkowego pakietu do obsługi Excela.

Pierwsze spojrzenie:
head
i
tail

Wczytałeś plik i pierwsze, co chcesz zrobić, to zajrzeć do środka. Wypisanie całego DataFrame przy trzech tysiącach wierszy zaleje Ci terminal i niczego nie wyjaśni. Od zerknięcia na początek tabeli jest metoda

head
, która zwraca pierwsze wiersze, a od zerknięcia na koniec metoda
tail
. Obie przyjmują opcjonalną liczbę wierszy, a jeśli jej nie podasz, biorą pięć. Nasza tabela ma tylko cztery wiersze, więc samo
df.head()
pokazałoby ją w całości - żeby różnica była widoczna, poprosimy wprost o dwa.

1print(df.head(2))
2#     species  population  habitat  endangered
3# 0      Lion         120  Savanna        True
4# 1  Elephant         450   Forest       False
5
6print(df.tail(2))
7#    species  population  habitat  endangered
8# 2  Cheetah          85  Savanna        True
9# 3  Giraffe         200  Savanna       False

Dwa wiersze z góry i dwa z dołu, każdy w komplecie, ze wszystkimi czterema kolumnami. I tu uwaga na trzy nieporozumienia, które biorą się z angielskiej nazwy "head". Ta metoda nie zwraca nagłówków kolumn, mimo że słowo "header" brzmi podobnie - od nazw kolumn jest

df.columns
. Nie zwraca też pierwszego elementu każdej kolumny; pierwszy wiersz jako taki wyciąga
df.iloc[0]
, o czym za moment. I na pewno niczego nie sortuje: kolejność wierszy w wyniku jest dokładnie taka, jak w pliku, a od sortowania jest osobna metoda
sort_values
z końcowej części lekcji.
head
zwraca nowy DataFrame z wycinkiem danych, a
df
zostaje bez zmian.

Metryczka zbioru:
info
i
describe

Podgląd kilku wierszy mówi, jak dane wyglądają, ale nie mówi, ile ich jest ani czy gdzieś nie brakuje wartości. Od takiej metryczki jest metoda

info
. Wypisuje ona klasę obiektu, zakres indeksu, a potem tabelkę z jednym wierszem na kolumnę: numer, nazwę, liczbę wartości niepustych w kolumnie
Non-Null Count
i typ w kolumnie
Dtype
. Na końcu dokłada podsumowanie typów i zużycie pamięci. To polecenie warte uruchomienia przy każdym nowym zbiorze, jaki bierzesz do ręki.

1df.info()          # wypisuje metryczke tabeli
2
3print(df.info())   # ta sama metryczka, a pod nia dodatkowe None

Te dwie linie robią prawie to samo i właśnie dlatego stoją obok siebie.

info
należy do nielicznych metod, które same drukują i nie zwracają żadnego wyniku, a dokładniej zwracają
None
. Owinięcie jej w
print
niczego nie poprawia, tylko dokłada na końcu samotne słowo
None
, które co roku myli kilkoro uczniów. Zapamiętaj więc:
info
wołasz gołą, bez
print
. Metoda niczego w tabeli nie zmienia - to czysty odczyt, tak samo jak
head
.

Kiedy już wiesz, ile masz wierszy i jakich typów, przychodzi pora na liczby. Metoda

describe
liczy dla każdej kolumny liczbowej komplet statystyk opisowych: liczbę wartości
count
, średnią
mean
, odchylenie standardowe
std
, minimum, trzy kwartyle oznaczone
25%
,
50%
i
75%
oraz maksimum. Wywołanie składa się z pięciu części i warto je znać na pamięć:
df
,
.
,
describe
,
(
,
)
.

1print(df.describe())
2#        population
3# count    4.000000
4# mean   213.750000
5# std    164.690366
6# min     85.000000
7# 25%    111.250000
8# 50%    160.000000
9# 75%    262.500000
10# max    450.000000

Osiem wierszy statystyk, ale tylko jedna kolumna w wyniku. To nie błąd:

describe
domyślnie bierze pod uwagę wyłącznie kolumny liczbowe, a w naszej tabeli taka jest tylko
population
. Tekstowe
species
i
habitat
oraz logiczna
endangered
zostały pominięte i pozostały w tabeli nietknięte. Gdyby w zbiorze nie było ani jednej kolumny liczbowej,
describe
przełączyłby się na statystyki tekstowe: liczbę wartości, liczbę unikatów i wartość najczęstszą. Średnia dwieście trzynaście i trzy czwarte przy maksimum czterysta pięćdziesiąt od razu podpowiada, że jeden gatunek mocno wystaje ponad resztę.

Kształt tabeli:
shape
,
columns
i
dtypes

Czasem nie potrzebujesz całej metryczki, tylko jednej konkretnej liczby - na przykład tego, ile wierszy przetrwało filtrowanie. Służą do tego trzy atrybuty, czyli właściwości obiektu, a nie metody. Atrybut

shape
zwraca krotkę w kolejności wiersze, kolumny. Atrybut
columns
oddaje listę nazw kolumn, a
dtypes
typ każdej kolumny z osobna. Ponieważ to atrybuty, a nie metody, piszesz je bez nawiasów:
df.shape
, nie
df.shape()
. Ta druga wersja kończy się błędem
TypeError: 'tuple' object is not callable
, bo próbujesz wywołać gotową krotkę jak funkcję.

1print(df.shape)    # (4, 4)
2print(df.columns)
3# Index(['species', 'population', 'habitat', 'endangered'], dtype='str')
4print(df.dtypes)
5# species         str
6# population    int64
7# habitat         str
8# endangered     bool
9# dtype: object

Krotka

(4, 4)
czyta się zawsze tak samo: cztery wiersze na cztery kolumny, nigdy odwrotnie, więc
df.shape[0]
to liczba obserwacji. Nazwy kolumn wracają opakowane w obiekt
Index
- to ta sama klasa, która obsługuje etykiety wierszy, bo w Pandas nagłówki kolumn też są indeksem. Jeśli wolisz zwykłą listę Pythona, dopisz
.tolist()
. Jedno ostrzeżenie co do wydruku typów: końcówka
str
przy kolumnach tekstowych pojawia się w Pandas od wersji 3.0, a we wcześniejszych zobaczysz w tym miejscu
object
. To ta sama rzecz nazwana po staremu i nie zmienia niczego w Twoim kodzie.

Wybieranie kolumn: jedna klamra czy dwie

Analiza rzadko dotyczy całej tabeli naraz - najczęściej wyciągasz z niej wycinek. Kolumnę wskazujesz jej nazwą w nawiasach kwadratowych i tu czeka pierwsza pułapka, bo od liczby nawiasów zależy typ wyniku. Jedna para nawiasów z pojedynczą nazwą zwraca Series, czyli tę jednowymiarową kolumnę z początku lekcji. Dwie pary nawiasów zwracają DataFrame, bo w środku podajesz zwykłą listę Pythona z nazwami kolumn - stąd druga para nawiasów, to nie żadna magia. Różnicę widać najlepiej w atrybucie

shape
, który dla struktury jednowymiarowej ma jedną liczbę, a dla dwuwymiarowej dwie.

1populations = df["population"]
2subset = df[["species", "population"]]
3
4print(populations.shape)  # (4,)
5print(subset.shape)       # (4, 2)

Ten sam zbiór liczb, dwa różne kształty. Krotka

(4,)
z pojedynczą wartością to podpis Series: cztery wartości ustawione w jednym szeregu. Krotka
(4, 2)
to podpis DataFrame: cztery wiersze na dwie kolumny. Kolejność nazw na liście decyduje o kolejności kolumn w wyniku, więc
df[["population", "species"]]
dałoby tę samą treść w odwrotnym ustawieniu. Obie zmienne są nowymi obiektami -
df
dalej ma komplet czterech kolumn i żadna z tych linii go nie okroiła.

Wybieranie wierszy:
iloc
i
loc

Kolumny wybiera się nazwą, a wiersze? Do wierszy służą dwa narzędzia i mylenie ich to klasyczny błąd początkującego.

iloc
to skrót od integer location, czyli pozycja liczona od zera, zupełnie jak w liście Pythona.
loc
sięga po etykietę z indeksu, czyli po to, co widzisz po lewej stronie wydruku. W naszej tabeli indeks to liczby od zera do trzech, więc oba zapisy wyglądają podobnie, ale zachowują się inaczej przy zakresach:
iloc
pomija koniec zakresu, tak jak wycinki list, a
loc
koniec włącza, bo etykiety nie są pozycjami. Obu narzędziom możesz podać dwa argumenty po przecinku - najpierw wiersze, potem kolumny.

1print(df.iloc[0])
2# species          Lion
3# population        120
4# habitat       Savanna
5# endangered       True
6# Name: 0, dtype: object
7
8print(df.iloc[0, 1])                    # 120
9print(df.iloc[0:3].shape)               # (3, 4)
10print(df.loc[0:2, "species"].tolist())  # ['Lion', 'Elephant', 'Cheetah']

Pierwsza linia pokazuje, czym naprawdę jest wiersz w Pandas: to Series, w której indeksem są nazwy kolumn, a wartości mają różne typy. Druga sięga po wiersz zero i kolumnę jeden, licząc od zera, więc trafia w liczebność lwa. Trzecia i czwarta to obiecana różnica:

iloc[0:3]
daje trzy wiersze, bo trójka wypada poza zakresem, a
loc[0:2]
też daje trzy wiersze, bo dwójka mieści się w środku. Zapis
df.iloc[0]
przyda Ci się jeszcze jako kontrprzykład - to on zwraca pierwszy element każdej kolumny, czego czasem błędnie oczekuje się po
head
.

Filtrowanie: maska logiczna na całej tabeli

Teraz najważniejsza umiejętność tej lekcji. Pytanie z terenu brzmi rzadko "pokaż mi wiersz numer dwa", a prawie zawsze "pokaż mi gatunki liczniejsze niż sto sztuk". Odpowiedź składa się z dwóch kroków, które poznałeś już na pojedynczej Series. Najpierw budujesz maskę logiczną: porównanie

df["population"] > 100
sprawdza warunek osobno dla każdego wiersza i zwraca Series wartości
True
i
False
o dokładnie takiej samej długości jak tabela.

1mask = df["population"] > 100
2
3print(mask)
4# 0     True
5# 1     True
6# 2    False
7# 3     True
8# Name: population, dtype: bool

Cztery wiersze tabeli dały cztery odpowiedzi logiczne, a indeks pozostał ten sam - to on wiąże maskę z wierszami. Zwróć uwagę, że w wyniku nie ma ani jednej liczebności: maska nie przechowuje danych, tylko decyzję o każdym wierszu. Sama w sobie niczego jeszcze nie odfiltrowała i

df
po tej linii ma wciąż cztery wiersze. Dopiero włożenie maski do nawiasów kwadratowych tabeli zostawia te wiersze, przy których stoi
True
.

1print(df[df["population"] > 100])
2#     species  population  habitat  endangered
3# 0      Lion         120  Savanna        True
4# 1  Elephant         450   Forest       False
5# 3   Giraffe         200  Savanna       False

Trzy wiersze zamiast czterech, a gepard wypadł, bo osiemdziesiąt pięć nie spełnia warunku. Spójrz na indeks po lewej: zostały numery 0, 1 i 3, z dziurą po dwójce. Pandas nie przenumerowuje wierszy po filtrowaniu, dzięki czemu zawsze wiesz, skąd wiersz pochodzi w oryginale. Całą konstrukcję układa się w czterech krokach i warto zapamiętać ich kolejność: najpierw

df[
, potem
df['population']
, potem
> 100
, a na końcu
]
. Innymi słowy: otwierasz nawias na tabeli, w środku budujesz warunek na kolumnie i zamykasz nawias. I znów najważniejsze zdanie: oryginalny
df
ma po tej operacji nadal cztery wiersze, bo filtrowanie zwraca nową tabelę.

Warunki można łączyć, tylko trzeba pamiętać o dwóch szczegółach składni. Zamiast słów

and
i
or
używa się znaków
&
oraz
|
, bo Python porównuje całe serie, a nie pojedyncze wartości. Każdy warunek musi też stać we własnych nawiasach okrągłych, bo
&
wiąże mocniej niż
>
i bez nawiasów Python policzyłby to w złej kolejności.

1print(df[(df["population"] > 100) & (df["endangered"] == False)])
2#     species  population  habitat  endangered
3# 1  Elephant         450   Forest       False
4# 3   Giraffe         200  Savanna       False

Zostały dwa gatunki: liczne i niezagrożone. Znak

&
znaczy "oba warunki naraz", a
|
znaczyłby "przynajmniej jeden". Gdybyś napisał w tym miejscu
and
, dostałbyś
ValueError: The truth value of a Series is ambiguous
- Python nie wie, czy seria czterech wartości logicznych to prawda, czy fałsz, i słusznie odmawia zgadywania. Wynik jest, jak zawsze, nową tabelą;
df
po tych trzech blokach jest dokładnie taki sam jak po wczytaniu z pliku.

Nowe kolumny i metoda
apply

Zwiad terenowy to nie tylko wybieranie tego, co już masz, ale też dokładanie nowych informacji. Nową kolumnę tworzysz przez zwykłe przypisanie do nazwy, której w tabeli jeszcze nie ma. Po prawej stronie możesz napisać działanie na istniejącej kolumnie, a Pandas wykona je dla każdego wiersza z osobna, bez żadnej pętli - to ta sama wektoryzacja, którą poznałeś przy tablicach NumPy.

1df["population_thousands"] = df["population"] / 1000
2
3print(df["population_thousands"].tolist())
4# [0.12, 0.45, 0.085, 0.2]

Tabela ma od tej chwili pięć kolumn zamiast czterech i to jedna z nielicznych operacji, które zmieniają

df
na miejscu, bez przypisywania wyniku z powrotem. Warto to sobie ułożyć w głowie: przypisanie do
df["cos"]
modyfikuje tabelę, natomiast filtrowanie,
head
czy
describe
tylko zwracają nowe obiekty. Kolumna
population
nie ucierpiała - dalej trzyma liczby całkowite, a nowe wartości ułamkowe wylądowały obok, w osobnej kolumnie.

Nie każde przekształcenie da się zapisać jednym działaniem arytmetycznym. Gdy dla każdej wartości chcesz wykonać własną logikę, sięgasz po metodę

apply
, która przyjmuje funkcję i uruchamia ją po kolei dla każdego elementu kolumny. Zwykle podaje się jej funkcję lambda, czyli funkcję bez nazwy, zapisaną w jednej linii: po słowie
lambda
idzie nazwa argumentu, dwukropek, a potem wyrażenie, którego wynik zostanie zwrócony.

1df["status"] = df["population"].apply(
2    lambda x: "endangered" if x < 100 else "stable"
3)
4
5print(df["status"].tolist())
6# ['stable', 'stable', 'endangered', 'stable']

Funkcja dostała po kolei sto dwadzieścia, czterysta pięćdziesiąt, osiemdziesiąt pięć i dwieście, a zwróciła cztery napisy, z których Pandas zbudował nową kolumnę. Argument

x
to pojedyncza wartość, a nie cała kolumna, bo
apply
wywołane na Series pracuje element po elemencie. Sama kolumna
population
znów pozostała nietknięta -
apply
niczego w niej nie nadpisuje, tylko czyta. Zwróć uwagę, że gepard z liczebnością osiemdziesiąt pięć dostał status
endangered
, choć w kolumnie
endangered
i tak stało już
True
; to dwa niezależne źródła tej samej informacji i w prawdziwym zbiorze warto sprawdzić, czy się zgadzają.

Kolumnę można też nadpisać, podając po lewej stronie nazwę, która już istnieje. Robię to niechętnie i Tobie też odradzam, bo po takim przypisaniu oryginalnych pomiarów już nie odzyskasz - w pamięci zostaje tylko wynik. Kiedy potrzebuję przeliczonej wersji, wolę zrobić kopię tabeli metodą

copy
i przeliczać kopię. Kosztuje to jedną linię, a ratuje przed powtarzaniem całego wczytywania od zera.

1projection = df.copy()
2projection["population"] = projection["population"] * 1.1
3
4print(projection["population"].round(1).tolist())  # [132.0, 495.0, 93.5, 220.0]
5print(df["population"].tolist())                   # [120, 450, 85, 200]

Prognoza dziesięcioprocentowego wzrostu wylądowała w

projection
, a
df
trzyma nadal liczby zmierzone w terenie - i o to właśnie chodziło. Zauważ przy okazji, że po pomnożeniu przez liczbę ułamkową wartości przestały być całkowite: dziewięćdziesiąt trzy i pół zwierzęcia to oczywiście bezsens biologiczny, ale matematycznie tak działa mnożenie i Pandas zmienił typ kolumny na zmiennoprzecinkowy. Wywołanie
round
w wydruku też nie stoi tam bez powodu: bez niego zobaczyłbyś
495.00000000000006
, bo komputer trzyma ułamki w postaci dwójkowej, a jedna dziesiąta nie ma w niej dokładnego zapisu. Gdybyś pominął
copy
i przypisał wynik wprost do
df
, kolumna
population
w oryginale też zmieniłaby typ i wartości.

Usuwanie kolumn:
drop
i oś

Prędzej czy później w tabeli wyląduje kolumna, która była potrzebna tylko na chwilę. Do wyrzucania służy metoda

drop
, ale ma ona pewien haczyk: tym samym poleceniem usuwa się wiersze i kolumny, więc musisz powiedzieć, o który wymiar chodzi. Robi to parametr
axis
, czyli . Zapamiętaj raz na zawsze:
axis=0
to wiersze,
axis=1
to kolumny. Ta numeracja nie jest przypadkowa - wraca w niej krotka
shape
, w której na pozycji zerowej stoją wiersze, a na pierwszej kolumny.

1df["temp"] = 1
2clean = df.drop("temp", axis=1)
3
4print("temp" in clean.columns)  # False
5print("temp" in df.columns)     # True
6
7df = df.drop("temp", axis=1)
8print("temp" in df.columns)     # False

Najciekawszy jest tu drugi wydruk. Kolumna

temp
zniknęła z
clean
, ale w
df
została, bo
drop
nie usuwa niczego z oryginału - zwraca nową tabelę bez wskazanej kolumny. Dopiero przypisanie wyniku z powrotem do
df
, jak w piątej linii, utrwala zmianę. Istnieje wprawdzie parametr
inplace=True
, który modyfikuje tabelę na miejscu, ale polecam Ci przypisanie: widać wtedy gołym okiem, że zmienna dostała nową zawartość, a wywołania da się łączyć jedno po drugim.

Trzy warianty tego polecenia nie zadziałają i wszystkie trzy widuję w pracach uczniów. Zapis

df.drop('temp', axis=0)
przechodzi przez składnię, ale kończy się błędem
KeyError: "['temp'] not found in axis"
, bo z osią zero Pandas szuka wiersza o etykiecie
temp
, a takiego wiersza nie ma. Zapis
df.delete(column='temp')
daje
AttributeError
, bo DataFrame nie ma metody
delete
- myli się ona z funkcją
np.delete
z NumPy, która działa na tablicach. Zapis
df.remove('temp')
również kończy się
AttributeError
, bo
remove
to metoda list Pythona, nie tabel Pandas. Poprawna jest jedna forma:
df.drop('temp', axis=1)
. Dla porządku dodam, że istnieje też równoważny zapis
df.drop(columns='temp')
, czytelniejszy w długich skryptach, ale wersję z
axis=1
musisz znać, bo spotkasz ją w każdej dokumentacji. Podobnie działa
df.rename(columns={"population": "count_2024"})
, które zmienia nazwy kolumn i tak samo zwraca kopię zamiast poprawiać oryginał.

Dziury w danych

Dane z terenu bywają dziurawe: obroża padła, obserwator nie dojechał, kartka zamokła. Pandas zapisuje brakującą wartość jako NaN, czyli not a number. Do zbudowania takiej dziurawej tabeli w Pythonie używa się

None
, które Pandas sam zamienia na NaN. Miejsca braków znajdziesz metodą
isnull
, zwracającą tabelę wartości logicznych o tym samym kształcie co dane, gdzie
True
znaczy "tu jest dziura". Dopisanie
.sum()
sumuje te wartości kolumnami, a ponieważ
True
liczy się jako jeden, dostajesz liczbę braków w każdej kolumnie.

1notes = pd.DataFrame({
2    "species": ["Lion", "Elephant", "Cheetah", "Giraffe"],
3    "population": [120, None, 85, 200],
4    "habitat": ["Savanna", "Forest", None, "Savanna"],
5})
6
7print(notes.isnull().sum())
8# species       0
9# population    1
10# habitat       1
11# dtype: int64
12
13print(notes["population"].tolist())  # [120.0, nan, 85.0, 200.0]

Jedna dziura w liczebnościach, jedna w siedliskach, komplet w nazwach gatunków - i to wszystko z jednej linii kodu. Ostatni wydruk pokazuje skutek uboczny, o którym łatwo zapomnieć: kolumna

population
przestała być całkowita i sto dwadzieścia wyświetla się jako
120.0
. Dzieje się tak, bo NaN jest liczbą zmiennoprzecinkową, więc cała kolumna musiała przejść na ten typ. Same dane poza tym nie ruszyły się o milimetr -
isnull
niczego nie naprawia, tylko wskazuje palcem.

Najprostsza reakcja na braki to wyrzucenie niepełnych obserwacji metodą

dropna
. Domyślnie usuwa ona cały wiersz, w którym brakuje choćby jednej wartości, i tak samo jak
drop
zwraca nową tabelę zamiast poprawiać starą.

1print(notes.dropna().shape)  # (2, 3)
2print(notes.shape)           # (4, 3)

Z czterech obserwacji zostały dwie, bo słoń stracił liczebność, a gepard siedlisko. Zwróć uwagę, jak drogo kosztuje ta czystość: usunąłeś połowę zbioru, żeby załatać dwie komórki, a razem z nimi wyleciały poprawne nazwy gatunków i poprawna liczebność geparda. Przy dużym zbiorze i pojedynczych dziurach

dropna
jest w porządku, przy małym potrafi wyciąć Ci dane spod nóg. Oryginalne
notes
ma dalej cztery wiersze, więc nic nie przepadło na zawsze.

Drugie wyjście to wypełnienie dziur metodą

fillna
, której podajesz wartość zastępczą. Wybór tej wartości to decyzja merytoryczna, nie techniczna. Wstawienie zera do kolumny z liczebnością oznacza "tego gatunku tu nie ma", co jest zwyczajnie nieprawdą i zaniży każdą późniejszą średnią. Dlatego przy pomiarach polecam wypełnianie średnią z pozostałych obserwacji: nie zmyśla wtedy nowej informacji, tylko wstawia wartość typową.

1average = notes["population"].mean()
2print(average)  # 135.0
3
4print(notes["population"].fillna(0).tolist())        # [120.0, 0.0, 85.0, 200.0]
5print(notes["population"].fillna(average).tolist())  # [120.0, 135.0, 85.0, 200.0]

Średnia wyszła sto trzydzieści pięć, czyli suma czterystu pięciu podzielona przez trzy, a nie przez cztery - metody statystyczne w Pandas pomijają NaN przy liczeniu. Porównaj oba wyniki: wersja z zerem wciska w środek zbioru wartość niższą od wszystkich pozostałych, wersja ze średnią wstawia liczbę, która nie zaburza obrazu. Żadne z tych wywołań nie zmieniło

notes
, bo
fillna
, tak jak
dropna
, zwraca kopię - żeby zachować poprawkę, musisz przypisać wynik do kolumny.

Grupowanie:
groupby

Do tej pory patrzyliśmy na wiersze pojedynczo. Prawdziwe pytania z rezerwatu brzmią jednak inaczej: nie "ile jest lwów", tylko "która strefa utrzymuje większe populacje - sawanna czy las". To pytanie o grupy, a odpowiada na nie metoda

groupby
, której podajesz nazwę kolumny do podziału. Wywołanie składa się z sześciu części:
df
,
.
,
groupby
,
(
,
'habitat'
,
)
. Samo w sobie nie zwraca jeszcze żadnej tabeli, tylko obiekt grupujący, czyli plan podziału. Wynik pojawi się dopiero, gdy dopiszesz do niego działanie - na przykład
mean
, czyli średnią.

1by_habitat = df.groupby("habitat")
2
3print(by_habitat["population"].mean())
4# habitat
5# Forest     450.0
6# Savanna    135.0
7# Name: population, dtype: float64

Dwa siedliska, dwie średnie. Najważniejsze jest to, co stało się z indeksem: nazwy siedlisk przeniosły się z kolumny do etykiet wierszy wyniku, bo po grupowaniu to one identyfikują wiersz. Zapis

by_habitat["population"]
mówi "policz tylko na kolumnie liczebności" i warto go stosować, bo próba policzenia średniej z kolumny tekstowej nie ma sensu. Sam
df
po tej operacji jest bez zmian: grupowanie niczego nie przenosi ani nie usuwa z oryginału, tylko czyta go i buduje podsumowanie obok.

Obiekt grupujący da się odpytać na wiele sposobów i nie musisz go budować od nowa za każdym razem. Metoda

sum
sumuje wartości w grupie, a
size
zlicza wiersze, czyli odpowiada na pytanie "ile gatunków wpadło do każdej strefy".

1print(by_habitat["population"].sum())
2# habitat
3# Forest     450
4# Savanna    405
5# Name: population, dtype: int64
6
7print(by_habitat.size())
8# habitat
9# Forest     1
10# Savanna    3
11# dtype: int64

Suma w lesie wypadła wyżej niż w sawannie, choć średnia mówiła to samo dużo dobitniej - i właśnie dlatego warto patrzeć na obie liczby naraz. Wydruk

size
tłumaczy, skąd ta różnica: w lesie żyje jeden gatunek, a w sawannie trzy, więc czterysta pięćdziesiąt słoni bije trzy mniejsze populacje na sumie, ale różnica w średniej jest ponaddwukrotna. Zauważ, że
size
nie potrzebuje wskazania kolumny, bo liczy wiersze, a nie wartości.

Kiedy chcesz kilka statystyk naraz, zamiast wywoływać metody jedna po drugiej, podajesz metodzie

agg
słownik: klucz to nazwa kolumny, a wartość to lista nazw działań zapisanych jako teksty.

1stats = df.groupby("habitat").agg({
2    "population": ["mean", "sum", "count"],
3    "endangered": "sum",
4})
5
6print(stats)
7#         population            endangered
8#               mean  sum count        sum
9# habitat
10# Forest       450.0  450     1          0
11# Savanna      135.0  405     3          2

Cztery statystyki w jednej tabelce, a nagłówek zrobił się dwupoziomowy: górny wiersz mówi, z której kolumny liczono, dolny - jakie działanie zastosowano. Ostatnia kolumna kryje drobną sztuczkę wartą zapamiętania: sumowanie wartości logicznych daje liczbę wystąpień

True
, więc
endangered
z sumą dwa oznacza dwa zagrożone gatunki w sawannie. Cała ta operacja znów tylko czytała dane -
df
ma dalej te same wiersze i kolumny co przed grupowaniem.

Sortowanie:
sort_values

Podsumowania odpowiadają na pytania o grupy, ale często wystarczy zwykły ranking: który gatunek jest najliczniejszy. Do porządkowania wierszy służy metoda

sort_values
, której podajesz nazwę kolumny. Domyślnie sortuje rosnąco, a parametr
ascending=False
odwraca kolejność na malejącą. Możesz też podać listę kolumn i wtedy Pandas sortuje najpierw po pierwszej, a przy remisach po drugiej.

1ranking = df.sort_values("population", ascending=False)
2
3print(ranking["species"].tolist())  # ['Elephant', 'Giraffe', 'Lion', 'Cheetah']
4print(ranking.index.tolist())       # [1, 3, 0, 2]
5print(df["species"].tolist())       # ['Lion', 'Elephant', 'Cheetah', 'Giraffe']

Ranking od słonia do geparda ustawił się w jednej linii. Drugi wydruk pokazuje rzecz, która zaskakuje wszystkich za pierwszym razem: indeks pojechał razem z wierszami, więc na górze tabeli stoi teraz wiersz o etykiecie 1, a nie 0. To celowe, bo dzięki temu wiadomo, gdzie dany wiersz leżał w oryginale. Trzeci wydruk to najważniejsze zdanie tej sekcji: kolejność w

df
nie zmieniła się ani trochę, bo
sort_values
zwraca posortowaną kopię. Istnieje jeszcze
sort_index
, który porządkuje tabelę po etykietach wierszy i przydaje się właśnie po to, żeby po sortowaniu wrócić do stanu wyjściowego.

Łączenie dwóch notesów:
merge
i
concat

W ekspedycji rzadko wszystko mieści się w jednym zeszycie. Załóżmy, że obok tabeli gatunków masz drugą, opisującą siedliska: ich nazwy i powierzchnię. Chcesz je zestawić, dopisując do każdego zwierzęcia powierzchnię jego strefy. Do łączenia tabel po wspólnej kolumnie służy funkcja

pd.merge
, znana każdemu, kto widział polecenie JOIN w SQL. Podajesz jej dwie tabele, w parametrze
on
nazwę wspólnej kolumny, a w
how
sposób łączenia. Wartość
"left"
znaczy "zachowaj wszystkie wiersze z lewej tabeli", czyli w naszym przypadku wszystkie zwierzęta.

1habitats = pd.DataFrame({
2    "habitat": ["Savanna", "Forest", "Wetland"],
3    "area_km2": [1200, 800, 300],
4})
5
6merged = pd.merge(df, habitats, on="habitat", how="left")
7
8print(merged[["species", "habitat", "area_km2"]])
9#     species  habitat  area_km2
10# 0      Lion  Savanna      1200
11# 1  Elephant   Forest       800
12# 2   Cheetah  Savanna      1200
13# 3   Giraffe  Savanna      1200

Każde zwierzę dostało powierzchnię swojej strefy, a wartość tysiąc dwieście powtórzyła się trzy razy, bo trzy gatunki mieszkają w sawannie - łączenie kopiuje dane z mniejszej tabeli tam, gdzie pasują. Zwróć uwagę na to, czego w wyniku nie ma: mokradła z tabeli

habitats
nie pojawiły się nigdzie, bo w
df
nie mieszka tam żaden gatunek, a łączenie
"left"
patrzy tylko na lewą tabelę. Gdybyś dał
how="outer"
, doszedłby piąty wiersz z mokradłami i pustymi wartościami NaN w kolumnach o zwierzętach. Wariant
"inner"
zostawiłby wyłącznie siedliska obecne w obu tabelach, a
"right"
byłby lustrzanym odbiciem
"left"
.

Zupełnie inny problem pojawia się, gdy dwie tabele mają te same kolumny, a różne wiersze - na przykład dwa obozy przysłały notesy o identycznej budowie. Wtedy nie łączysz ich po kluczu, tylko dostawiasz jeden pod drugim funkcją

pd.concat
, której podajesz listę tabel.

1north = pd.DataFrame({"species": ["Lion", "Zebra"], "population": [120, 300]})
2south = pd.DataFrame({"species": ["Hyena", "Impala"], "population": [60, 540]})
3
4both = pd.concat([north, south], ignore_index=True)
5
6print(both["species"].tolist())  # ['Lion', 'Zebra', 'Hyena', 'Impala']
7print(both.index.tolist())       # [0, 1, 2, 3]

Cztery wiersze z dwóch obozów w jednej tabeli, w kolejności podanej na liście. Parametr

ignore_index=True
jest tu ważniejszy, niż wygląda: bez niego indeks wyniku wyglądałby jak 0, 1, 0, 1, bo każda tabela przyniosłaby swoje własne etykiety i powstałyby duplikaty. Domyślnie
concat
dokłada wiersze, czyli działa wzdłuż osi zero; z parametrem
axis=1
dostawiałby kolumny bok w bok. Obie tabele źródłowe zostają bez zmian -
north
ma dalej swoje dwa wiersze.

Zapis wyników

Analiza, która ginie razem z zamknięciem terminala, nie jest jeszcze wynikiem ekspedycji. Gotową tabelę zapisujesz metodą

to_csv
, podając nazwę pliku. Jeden parametr jest przy tym praktycznie obowiązkowy:
index=False
. Bez niego Pandas zapisze do pliku również etykiety wierszy, jako pierwszą kolumnę bez nagłówka, a przy ponownym wczytaniu
read_csv
potraktuje ją jako zwykłe dane i nazwie
Unnamed: 0
. Po kilku takich rundach plik obrasta w kolumny, których nikt nie zamawiał.

1report = df[["species", "population", "status"]]
2report.to_csv("safari_report.csv", index=False)
3
4with open("safari_report.csv") as f:
5    print(f.read())
6# species,population,status
7# Lion,120,stable
8# Elephant,450,stable
9# Cheetah,85,endangered
10# Giraffe,200,stable

Plik wygląda dokładnie tak jak ten, od którego zaczynaliśmy wczytywanie: nagłówek i cztery wiersze rozdzielone przecinkami. Zapisały się tylko trzy wybrane kolumny, bo

to_csv
woła się na tym, co mu podasz, a podaliśmy wycinek - reszta kolumn siedzi dalej w
df
i nie ucierpiała. Metoda niczego nie zwraca i nie zmienia tabeli, po prostu odkłada jej kopię na dysk. Bliźniacze
to_excel
i
to_json
zapisują ten sam DataFrame w innych formatach, przy czym pierwsza, tak jak przy czytaniu, wymaga doinstalowania pakietu do obsługi arkuszy.

Podsumowanie

  • DataFrame to tabela z wierszami i kolumnami - jedyna taka struktura w Pandas. Series jest jednowymiarowa: to jedna kolumna z indeksem.
    Panel
    został usunięty w wersji 1.0, a klasy
    Table
    w Pandas nigdy nie było.
  • Tabelę ze słownika budujesz przez
    pd.DataFrame({...})
    , gdzie klucz to nazwa kolumny, a wartość to lista.
  • Plik wczytujesz sześcioma znakami:
    pd
    ,
    .
    ,
    read_csv
    ,
    (
    ,
    'data.csv'
    ,
    )
    .
  • df.head()
    zwraca pierwsze pięć wierszy tabeli. Nie zwraca nagłówków (to
    df.columns
    ), nie zwraca pierwszego elementu każdej kolumny (to
    df.iloc[0]
    ) i niczego nie sortuje (to
    sort_values
    ).
  • df.info()
    wołasz bez
    print
    , bo sam drukuje i zwraca
    None
    .
    df.describe()
    liczy statystyki wyłącznie dla kolumn liczbowych.
  • Filtrujesz w czterech krokach:
    df[
    ,
    df['population']
    ,
    > 100
    ,
    ]
    . Warunki łączysz znakami
    &
    i
    |
    , każdy w osobnych nawiasach.
  • Kolumnę usuwasz przez
    df.drop('temp', axis=1)
    , bo
    axis=1
    to kolumny, a
    axis=0
    to wiersze. Metody
    delete
    i
    remove
    w DataFrame nie istnieją.
  • Grupujesz sześcioma częściami:
    df
    ,
    .
    ,
    groupby
    ,
    (
    ,
    'habitat'
    ,
    )
    , a wynik dostajesz dopiero po dopisaniu działania, na przykład
    mean()
    .
  • Prawie każda metoda Pandas zwraca nową tabelę i zostawia oryginał w spokoju. Wyjątkiem jest przypisanie do
    df["kolumna"]
    , które zmienia tabelę na miejscu.

W następnej lekcji te same kolumny zamienimy na wykresy i zobaczysz, jak jedno spojrzenie na rysunek zastępuje przeglądanie stu wierszy. Na razie zapamiętaj jedno, @name: Pandas nie zbiera danych za Ciebie i nie poprawia błędów obserwatora - daje Ci tylko notes, w którym każda kolumna ma nazwę, własny typ i całą tabelę pod ręką, gdy zadajesz pytanie.

Przejdź do CodeWorlds