Python - wprowadzenie

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 dwie 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 odbywać się poprzez metodę append():

seria_3 = pd.Series([44, 66], index = ['F', 'G'])
seria_4 = seria_1.append(seria_3)
print(seria_3)
print(seria_4)
F    44
G    66
dtype: int64
A     0
B     1
C    88
D     3
E    77
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 = dane_1.append(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
dane_4 = dane_1.append(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

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
Last updated on 24 Mar 2021
Published on 24 Mar 2021