05 - Biblioteka `pandas` cz. 1: `Series`
pandas
to biblioteka służąca analizie i przetwarzaniu danych. Do jej największych zalet należy obecność struktur danych: serii (Series
) i ramek danych (DataFrame
), a także dostępność funkcji ułatwiających przetwarzanie danych, wykonywanie obliczeń statystycznych czy wizualizację danych, przy czym zadania te są dodatkowo ułatwione przez dobrą współpracę z innymi modułami Pythona, które się w nich specjalizują.
Strona domowa pandas
znajduje się pod adresem: https://pandas.pydata.org/. Można tam znaleźć m.in. dokumentację i podręcznik użytkownika.
Struktury danych: serie i ramki danych
Jak wspomniałem, biblioteka pandas
udostępnia struktury danych: serie (Series
) i ramki danych (DataFrame
). Pierwsza z nich umożliwia, podobnie jak ndarrays
z pakietu NumPy
, przechowywanie danych jednego typu, drugą można porównać do tabel znanych np. z arkuszy kalkulacyjnych: przechowuje dane w kolumnach i rzędach, przy czym kolumny zawierają dane jednego typu, ale w różnych kolumnach typy mogą być różne.
Zacznijmy od serii
Serie
# Zaimportowanie biblioteki
import pandas as pd
# Tworzymy serię z listy zawierającej liczby całkowite
seria_1 = pd.Series([7, 3, 2, 4, 4, 5])
print(type(seria_1))
print(f'seria:\n{seria_1}')
<class 'pandas.core.series.Series'>
seria:
0 7
1 3
2 2
3 4
4 4
5 5
dtype: int64
Zauważ, że dane wyświetliły się w inny sposób, niż było to w przypadku list czy tablic ndarray
z pakietu NumPy
. W każdym wierszu pojawiła się pojedyncza wartość, poprzedzona indeksem. Na końcu został wyświetlony typ przechowywanych wartości (int64
).
Typy wartości są podobne do tych znanych z pakietu NumPy
, choć np. łańcuchy znaków są przechowywane jako object
.
Domyślnie indeksem są kolejne liczby całkowite począwszy od zera, ale można nadać kolejnym elementom identyfikujące je etykiety:
seria_1.index = ['A', 'B', 'C', 'D', 'E', 'F']
print(seria_1)
A 7
B 3
C 2
D 4
E 4
F 5
dtype: int64
Można łatwo nadać etykiety wartościom od razu, przy tworzeniu serii:
seria_1 = pd.Series([7, 3, 2, 4, 4, 5], index = ['A', 'B', 'C', 'D', 'E', 'F'])
print(seria_1)
A 7
B 3
C 2
D 4
E 4
F 5
dtype: int64
Przy takim sposobie nadawania etykiet, ich liczba powinna odpowiadać liczbie wartości przechowywanej w serii:
seria_1 = pd.Series([7, 3, 2, 4, 4, 5], index = ['A', 'B', 'C', 'D'])
print(seria)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-4-1b97786ae9b3> in <module>
----> 1 seria_1 = pd.Series([7, 3, 2, 4, 4, 5], index = ['A', 'B', 'C', 'D'])
2 print(seria)
~/anaconda3/lib/python3.8/site-packages/pandas/core/series.py in __init__(self, data, index, dtype, name, copy, fastpath)
311 try:
312 if len(index) != len(data):
--> 313 raise ValueError(
314 f"Length of passed values is {len(data)}, "
315 f"index implies {len(index)}."
ValueError: Length of passed values is 6, index implies 4.
Można też, przy tworzeniu serii skorzystać ze słownika:
chromosomy = pd.Series({'Homo sapiens': 46,
'Ophioglossum reticulatum': 1400,
'Arabidopsis thaliana': 10,
'Myrmecia pilosula': 2,
'Canis familiaris': 36, })
print(chromosomy)
Homo sapiens 46
Ophioglossum reticulatum 1400
Arabidopsis thaliana 10
Myrmecia pilosula 2
Canis familiaris 36
dtype: int64
Etykiety można łatwo pobrać:
print(chromosomy.index)
print(type(chromosomy.index))
Index(['Homo sapiens', 'Ophioglossum reticulatum', 'Arabidopsis thaliana',
'Myrmecia pilosula', 'Canis familiaris'],
dtype='object')
<class 'pandas.core.indexes.base.Index'>
Jak można zauważyć, zwracany jest obiekt typu Index
.
Podobnie uzyskujemy wartości serii, w tym przypadku zwracany jest obiekt typu ndarray
znany nam z pakietu NumPy
:
print(chromosomy.values)
print(type(chromosomy.values))
[ 46 1400 10 2 36]
<class 'numpy.ndarray'>
Kolejność elementów można zmienić, przypisując etykiety w nowej kolejności:
chromosomy_sort = pd.Series(chromosomy, index =
['Myrmecia pilosula',
'Ophioglossum reticulatum',
'Canis familiaris',
'Homo sapiens',
'Arabidopsis thaliana',
])
print(chromosomy_sort)
Myrmecia pilosula 2
Ophioglossum reticulatum 1400
Canis familiaris 36
Homo sapiens 46
Arabidopsis thaliana 10
dtype: int64
Zauważ, że mimo zmiany kolejności etykiet, wartości dalej są do nich przypisane prawidłowo.
Spróbujmy teraz posortować dane wg. etykiet alfabetycznie:
chromosomy_sort_alf = pd.Series(chromosomy,
index = chromosomy.index.sort_values())
print(chromosomy_sort_alf)
Arabidopsis thaliana 10
Canis familiaris 36
Homo sapiens 46
Myrmecia pilosula 2
Ophioglossum reticulatum 1400
dtype: int64
Sprawdźmy teraz, co się stanie, jeśli dodamy jedną nieistniejącą wcześniej etykietę, a trzy istniejące zostaną pominięte:
chromosomy_sort_nowe = pd.Series(chromosomy, index =
['Myrmecia pilosula',
'Ophioglossum reticulatum',
'Zea mays',
])
print(chromosomy_sort_nowe)
Myrmecia pilosula 2.0
Ophioglossum reticulatum 1400.0
Zea mays NaN
dtype: float64
Okazuje się, że dane brakujących etykiet zostały usunięte, natomiast pojawiła się nowa etykieta z wartością NaN
co oznacza brak wartości. Przy okazji, zwróć uwagę, że zmienił się typ przechowywanych danych.
Obiektowi Series
można nadać nazwę indeksów (wyświetla się jak nagłówek nad indeksami) a także całej serii (wyświetla się pod danymi)
chromosomy.index.name = 'Gatunek'
chromosomy.name = 'Liczba 2n chromosomów'
print(chromosomy)
Gatunek
Homo sapiens 46
Ophioglossum reticulatum 1400
Arabidopsis thaliana 10
Myrmecia pilosula 2
Canis familiaris 36
Name: Liczba 2n chromosomów, dtype: int64
pandas
i NumPy
:
Wraz z pandas
, często jest używany pakiet NumPy
, możemy go np. użyć do wypełnienia tworzonej serii wartościami:
import pandas as pd
import numpy as np
seria_1 = pd.Series(np.arange(5))
print(seria_1)
print(type(seria_1))
0 0
1 1
2 2
3 3
4 4
dtype: int64
<class 'pandas.core.series.Series'>
seria_2 = pd.Series(np.ones(5))
print(seria_2)
print(type(seria_2))
0 1.0
1 1.0
2 1.0
3 1.0
4 1.0
dtype: float64
<class 'pandas.core.series.Series'>
Funkcje z pakietu NumPy
, także mogą okazać się przydatne:
print(f'suma: {np.sum(seria_1)}')
print(f'największa: {np.max(seria_1)}')
print(f'średnia: {np.mean(seria_1)}')
print(f'mediana: {np.median(seria_1)}')
suma: 10
największa: 4
średnia: 2.0
mediana: 2.0
Można wywołać je także jako metody na obiekcie Series
:
print(f'suma: {seria_1.sum()}')
print(f'największa: {seria_1.max()}')
print(f'średnia: {seria_1.mean()}')
print(f'mediana: {seria_1.median()}')
suma: 10
największa: 4
średnia: 2.0
mediana: 2.0
Pobieranie danych z serii
Dane z obiektu Series
można pobierać posługując się indeksem, lub etykietą:
import pandas as pd
zasady = pd.Series({'A': 'Adenina',
'C': 'Cytozyna',
'G': 'Guanina',
'T': 'Tymina',})
print(f'Seria:\n{zasady}')
print(f'{zasady[1] = }')
print(f'{zasady["C"] = }')
Seria:
A Adenina
C Cytozyna
G Guanina
T Tymina
dtype: object
zasady[1] = 'Cytozyna'
zasady["C"] = 'Cytozyna'
Jeśli podamy listę etykiet, uzyskujemy obiekt Series
zawierający wskazane dane:
purynowe = zasady[["G", "A"]]
print(f'Zasady purynowe:\n{purynowe}')
print(type(purynowe))
Zasady purynowe:
G Guanina
A Adenina
dtype: object
<class 'pandas.core.series.Series'>
Można tez podać zakres indeksów:
sel = zasady[1:3]
print(f'indeks 1-2:\n{sel}')
indeks 1-2:
C Cytozyna
G Guanina
dtype: object
Wyboru wartości można dokonać przekazując do obiektu odpowiedni test:
liczby = pd.Series(np.random.randint(10, size = 10))
print(liczby)
0 7
1 4
2 7
3 7
4 4
5 9
6 6
7 2
8 2
9 0
dtype: int64
print(f'Liczby < 5:\n{liczby[liczby < 5]}')
Liczby < 5:
1 4
4 4
7 2
8 2
9 0
dtype: int64
print(f'Liczby parzyste:\n{liczby[liczby % 2 == 0]}')
Liczby parzyste:
1 4
4 4
6 6
7 2
8 2
9 0
dtype: int64
Modyfikacje danych w Series
Zmiana wartości może odbywać się poprzez przypisanie nowej wartości do elementu, do którego możemy się odwołać za pomocą indeksu lub etykiety.
import pandas as pd
import numpy as np
seria_1 = pd.Series(np.arange(5), index =
['A', 'B', 'C', 'D', 'E'])
print(seria_1)
A 0
B 1
C 2
D 3
E 4
dtype: int64
seria_1[2] = 88
seria_1['E'] = 77
print(seria_1)
A 0
B 1
C 88
D 3
E 77
dtype: int64
Inną możliwością jest wywołanie metody update()
:
seria_2 = pd.Series(np.arange(5))
print(seria_2)
seria_2.update(pd.Series([22, 99], index = [1, 3]))
print(seria_2)
0 0
1 1
2 2
3 3
4 4
dtype: int64
0 0
1 22
2 2
3 99
4 4
dtype: int64
Dodanie danych do Series
Dodanie nowych wartości może odbywac się poprzez funkcję concat(), która tworzy nowy obiekt typu Series z połączenia dwu,lub więcej obiektów tego typu:
seria_3 = pd.Series([44, 66], index = ['F', 'G'])
seria_4 = pd.concat([seria_1, seria_3])
print(f'seria_1:\n{seria_1}')
print(f'seria_3:\n{seria_3}')
print(f'seria_4:\n{seria_4}')
seria_1:
A 7
B 3
C 2
D 4
E 4
F 5
dtype: int64
seria_3:
F 44
G 66
dtype: int64
seria_4:
A 7
B 3
C 2
D 4
E 4
F 5
F 44
G 66
dtype: int64
Warto zwrócić uwagę na kwestię indeksowania. Zauważ, jak działa ustawienie parametru ignore_index=True
:
dane_1 = pd.Series(np.arange(4))
dane_2 = pd.Series(np.arange(4, 7))
print(f'dane_1:\n{dane_1}')
print(f'dane_2:\n{dane_2}')
dane_1:
0 0
1 1
2 2
3 3
dtype: int64
dane_2:
0 4
1 5
2 6
dtype: int64
dane_3 = pd.concat([dane_1, dane_2])
print(f'dane_3:\n{dane_3}')
dane_3:
0 0
1 1
2 2
3 3
0 4
1 5
2 6
dtype: int64
Okazuje się, że w nowym obiekcie Series
numery indeksów powtarzają się. Zwykle wolelibyśmy tego uniknąć. Rozwiązaniem jest ustawienie parametru ignore_index
na wartość True
.
dane_4 = pd.concat([dane_1, dane_2], ignore_index=True)
print(f'dane_3:\n{dane_4}')
dane_3:
0 0
1 1
2 2
3 3
4 4
5 5
6 6
dtype: int64
Jak widać, ustawienie ignore_index=True
powoduje ,,renumerację’’ indeksów, tak, aby się nie powtarzały.
Usuwanie danych z Series
Dane można usunąć z serii, wywołując metodę drop()
, przy czym można wyznaczyć dane do usunięcia, podając indeksy lub etykiety:
seria_1 = pd.Series([7, 3, 2, 4, 4, 5],
index = ['A', 'B', 'C', 'D', 'E', 'F'])
print(seria_1)
A 7
B 3
C 2
D 4
E 4
F 5
dtype: int64
print('Usuwanie na podstawie indeksów: 2 i 5')
print(seria_1.drop([seria_1.index[2], seria_1.index[5]]))
Usuwanie na podstawie indeksów: 2 i 5
A 7
B 3
D 4
E 4
dtype: int64
print('Usuwanie na podstawie etykiet: "A" i "C"')
print(seria_1.drop(labels=['A', 'C']))
Usuwanie na podstawie etykiet: "A" i "C"
B 3
D 4
E 4
F 5
dtype: int64
Zauważ jednak, że metoda drop()
nie modyfikuje serii:
print(seria_1)
A 7
B 3
C 2
D 4
E 4
F 5
dtype: int64
Jeśli chcemy zmodyfikować serię, można to zrobić tak
seria_1 = seria_1.drop(labels=['A', 'C'])
print(seria_1)
B 3
D 4
E 4
F 5
dtype: int64
Operacje arytmetyczne na Series
Podobnie, jak w przypadku tablic ndarray
, można wykonywać operacje arytmetyczne na elementach serii:
import pandas as pd
import numpy as np
dane_1 = pd.Series(np.arange(5))
print(dane_1)
0 0
1 1
2 2
3 3
4 4
dtype: int64
print(dane_1 + 2)
0 2
1 3
2 4
3 5
4 6
dtype: int64
print(dane_1 * 2)
0 0
1 2
2 4
3 6
4 8
dtype: int64
print(dane_1 / 2)
0 0.0
1 0.5
2 1.0
3 1.5
4 2.0
dtype: float64
print(dane_1 ** 2)
0 0
1 1
2 4
3 9
4 16
dtype: int64