Pandas: аналіз даних на Python

Pandas це високорівнева бібліотека Python для аналізу даних. Чому високорівнева, тому що побудована вона поверх нижчого рівня бібліотеки NumPy (написана на С), що є великим плюсом у продуктивності. В екосистемі Python, pandas є найбільш просунутою бібліотекою, що швидко розвивається, для обробки та аналізу даних.

DataFrame та Series

Щоб ефективно працювати з pandas, необхідно освоїти найголовніші структури даних бібліотеки: DataFrame та Series. Без розуміння що вони собою представляють, неможливо надалі проводити якісний аналіз.

Series

Структура/объект Series є об’єкт, схожий на одномірний масив (пітонівський список, наприклад), але відмінною його рисою є наявність асоційованих міток, т.зв. індексів, вздовж кожного елемента зі списку. Така особливість перетворює його на асоціативний масив або словник на Python.

>>> import pandas as pd
>>> my_series = pd.Series([5, 6, 7, 8, 9, 10])
>>> my_series
0     5
1     6
2     7
3     8
4     9
5    10
dtype: int64
>>> 

У рядковому поданні об’єкта Series індекс знаходиться ліворуч, а сам елемент праворуч. Якщо індекс явно не заданий, Pandas автоматично створює RangeIndex від 0 до N-1, де N загальна кількість елементів. Також варто звернути, що у Series є тип елементів, що зберігаються, в нашому випадку це int64, т.к. ми передали цілі значення. Об’єкт Series має атрибути, через які можна отримати список елементів та індекси, це values і index відповідно.

>>> my_series.index
RangeIndex(start=0, stop=6, step=1)
>>> my_series.values
array([ 5,  6,  7,  8,  9, 10], dtype=int64) 

Доступ до елементів об’єкта Series можливі за їх індексом (згадується аналогія зі словником та доступом по ключу).

>>> my_series[4]
9

Індекси можна ставити явно:

>>> my_series2 = pd.Series([5, 6, 7, 8, 9, 10], index=['a', 'b', 'c', 'd', 'e', 'f'])
>>> my_series2['f']
10

Робити вибірку за декількома індексами та здійснювати групове привласнення:

>>> my_series2[['a', 'b', 'f']]
a     5
b     6
f    10
dtype: int64
>>> my_series2[['a', 'b', 'f']] = 0
>>> my_series2
a    0
b    0
c    7
d    8
e    9
f    0
dtype: int64

Фільтрувати Series як душі заманеться, а також застосовувати математичні операції та багато іншого:

>>> my_series2[my_series2 > 0]
c    7
d    8
e    9
dtype: int64

>>> my_series2[my_series2 > 0] * 2
c    14
d    16
e    18
dtype: int64

Якщо Series нагадує нам словник, де ключем є індекс, а значенням сам елемент, можна зробити так:

>>> my_series3 = pd.Series({'a': 5, 'b': 6, 'c': 7, 'd': 8})
>>> my_series3
a    5
b    6
c    7
d    8
dtype: int64
>>> 'd' in my_series3
True

Об’єкт Series та його індекс має атрибут name, що задає ім’я об’єкту та індексу відповідно.

>>> my_series3.name = 'numbers'
>>> my_series3.index.name = 'letters'
>>> my_series3
letters
a    5
b    6
c    7
d    8
Name: numbers, dtype: int64

Індекс можна поміняти “на льоту”, надавши список атрибуту index об’єкта Series

>>> my_series3.index = ['A', 'B', 'C', 'D']
>>> my_series3
A    5
B    6
C    7
D    8
Name: numbers, dtype: int64

Майте на увазі, що список з індексами по довжині повинен співпадати з кількістю елементів у Series.

DataFrame

Об’єкт DataFrame найкраще уявляти у вигляді звичайної таблиці і це правильно, адже DataFrame є табличною структурою даних. У будь-якій таблиці завжди присутні рядки та стовпці. Стовпцями в об’єкті DataFrame виступають об’єкти Series, рядки яких є безпосередніми елементами. DataFrame найпростіше сконструювати на прикладі пітонівського словника:

>>> df = pd.DataFrame({
...     'country': ['Kazakhstan', 'Russia', 'Belarus', 'Ukraine'],
...     'population': [17.04, 143.5, 9.5, 45.5],
...     'square': [2724902, 17125191, 207600, 603628]
... })
>>> df
   country  population    square
0  Kazakhstan       17.04   2724902
1      Russia      143.50  17125191
2     Belarus        9.50    207600
3     Ukraine       45.50    603628

Щоб переконатися, що стовпець у DataFrame це Series, виймаємо будь-який:

>>> df['country']
0    Kazakhstan
1        Russia
2       Belarus
3       Ukraine
Name: country, dtype: object
>>> type(df['country'])
<class 'pandas.core.series.Series'>

Об’єкт DataFrame має 2 індекси: за рядками та стовпцями. Якщо індекс рядків явно не заданий (наприклад, колонка по якій потрібно їх будувати), то pandas задає цілий індекс RangeIndex від 0 до N-1, де N це кількість рядків у таблиці.

>>> df.columns
Index([u'country', u'population', u'square'], dtype='object')
>>> df.index
RangeIndex(start=0, stop=4, step=1)

У таблиці ми маємо 4 елементи від 0 до 3. Доступ за індексом в DataFrame Індекс рядків можна задати різними способами, наприклад, при формуванні самого об’єкта DataFrame або “на льоту”:

>>> df = pd.DataFrame({
...     'country': ['Kazakhstan', 'Russia', 'Belarus', 'Ukraine'],
...     'population': [17.04, 143.5, 9.5, 45.5],
...     'square': [2724902, 17125191, 207600, 603628]
... }, index=['KZ', 'RU', 'BY', 'UA'])
>>> df
       country  population    square
KZ  Kazakhstan       17.04   2724902
RU      Russia      143.50  17125191
BY     Belarus        9.50    207600
UA     Ukraine       45.50    603628
>>> df.index = ['KZ', 'RU', 'BY', 'UA']
>>> df.index.name = 'Country Code'
>>> df
                 country  population    square
Country Code                                  
KZ            Kazakhstan       17.04   2724902
RU                Russia      143.50  17125191
BY               Belarus        9.50    207600
UA               Ukraine       45.50    603628

Як видно, індексу було встановлено ім’я - Country Code. Зазначу, що об’єкти Series із DataFrame матимуть ті ж індекси, що й об’єкт DataFrame:

>>> df['country']
Country Code
KZ    Kazakhstan
RU        Russia
BY       Belarus
UA       Ukraine
Name: country, dtype: object

Доступ до рядків за індексом можливий декількома способами:

* .loc - використовується для доступу за рядковою міткою
* .iloc - використовується для доступу за числовим значенням (починаючи від 0)
>>> df.loc['KZ']
country       Kazakhstan
population         17.04
square           2724902
Name: KZ, dtype: object

>>> df.iloc[0]
country       Kazakhstan
population         17.04
square           2724902
Name: KZ, dtype: object

Можна робити вибірку за індексом і колонкам, що цікавлять:

>>> df.loc[['KZ', 'RU'], 'population']
Country Code
KZ     17.04
RU    143.50
Name: population, dtype: float64

Як можна помітити, .loc у квадратних дужках приймає 2 аргументи: індекс, що цікавить, у тому числі підтримується слайсинг і колонки.

>>> df.loc['KZ':'BY', :]
                 country  population    square
Country Code                                  
KZ            Kazakhstan       17.04   2724902
RU                Russia      143.50  17125191
BY               Belarus        9.50    207600

Фільтрувати DataFrame за допомогою т.зв. булевих масивів:

>>> df[df.population > 10][['country', 'square']]
                 country    square
Country Code                      
KZ            Kazakhstan   2724902
RU                Russia  17125191
UA               Ukraine    603628

До речі, до шпальт можна звертатися, використовуючи атрибут або нотацію словників Python, тобто. df.population і df[‘population’] це те саме. Скинути індекси можна так:

>>> df.reset_index()
  Country Code     country  population    square
0           KZ  Kazakhstan       17.04   2724902
1           RU      Russia      143.50  17125191
2           BY     Belarus        9.50    207600
3           UA     Ukraine       45.50    603628

pandas під час операцій над DataFrame, повертає новий об’єкт DataFrame. Додамо новий стовпець, у якому населення (у мільйонах) поділимо на площу країни, отримавши тим самим густину:

>>> df['density'] = df['population'] / df['square'] * 1000000
>>> df
                 country  population    square    density
Country Code                                             
KZ            Kazakhstan       17.04   2724902   6.253436
RU                Russia      143.50  17125191   8.379469
BY               Belarus        9.50    207600  45.761079
UA               Ukraine       45.50    603628  75.377550

Чи не подобається новий стовпець? Не проблема, видалимо його:

>>> df.drop(['density'], axis='columns')
                 country  population    square
Country Code                                  
KZ            Kazakhstan       17.04   2724902
RU                Russia      143.50  17125191
BY               Belarus        9.50    207600
UA               Ukraine       45.50    603628

Особливо ліниві можуть просто написати del df[‘density’]. Перейменовувати стовпці потрібно через метод rename:

>>> df = df.rename(columns={'Country Code': 'country_code'})
>>> df
  country_code     country  population    square
0           KZ  Kazakhstan       17.04   2724902
1           RU      Russia      143.50  17125191
2           BY     Belarus        9.50    207600
3           UA     Ukraine       45.50    603628

У цьому прикладі перед тим, як перейменувати стовпець Country Code, переконайтеся, що з нього скинутий індекс, інакше не буде жодного ефекту.

Читання та запис даних

pandas підтримує всі найпопулярніші формати зберігання даних: csv, excel, sql, буфер обміну, html та багато іншого. Найчастіше доводиться працювати з csv-файлами. Наприклад, щоб зберегти наш DataFrame з країнами, достатньо написати:

>>> df.to_csv('filename.csv')

Функції to_csv ще передаються різні аргументи (наприклад, символ роздільника між колонками) про які докладніше можна дізнатися в офіційній документації. Вважати дані з csv-файлу та перетворити на DataFrame можна функцією read_csv.

>>> df = pd.read_csv('filename.csv', sep=',')

Аргумент sep вказує на розділення стовпців. Існує ще безліч способів сформувати DataFrame з різних джерел, але найчастіше використовують CSV, Excel і SQL. Наприклад, за допомогою функції read_sql pandas може виконати SQL запит і на основі відповіді від бази даних сформувати необхідний DataFrame. За більш детальною інформацією варто звернутись до офіційної документації.

Угруповання та агрегування в pandas

Угруповання даних один із найчастіше використовуваних методів під час аналізу даних. Pandas за угруповання відповідає метод .groupby. Я довго думав, який приклад буде найбільш наочним, щоб продемонструвати угруповання, вирішив взяти стандартний набір даних (dataset), що використовується у всіх курсах про аналіз даних - дані про пасажирів Титаніка. Завантажити файл CSV можна тут.

>>> titanic_df = pd.read_csv('titanic.csv')
>>> print(titanic_df.head())
   PassengerID                                           Name PClass    Age  \
0            1                   Allen, Miss Elisabeth Walton    1st  29.00   
1            2                    Allison, Miss Helen Loraine    1st   2.00   
2            3            Allison, Mr Hudson Joshua Creighton    1st  30.00   
3            4  Allison, Mrs Hudson JC (Bessie Waldo Daniels)    1st  25.00   
4            5                  Allison, Master Hudson Trevor    1st   0.92   
      Sex  Survived  SexCode  
0  female         1        1  
1  female         0        1  
2    male         0        0  
3  female         0        1  
4    male         1        0 

Необхідно підрахувати, скільки жінок та чоловіків вижило, а скільки ні. У цьому допоможе метод .groupby.

>>> print(titanic_df.groupby(['Sex', 'Survived'])['PassengerID'].count())
Sex     Survived
female  0           154
        1           308
male    0           709
        1           142
Name: PassengerID, dtype: int64

А тепер проаналізуємо у розрізі класу кабіни:

>>> print(titanic_df.groupby(['PClass', 'Survived'])['PassengerID'].count())
PClass  Survived
*       0             1
1st     0           129
        1           193
2nd     0           160
        1           119
3rd     0           573
        1           138
Name: PassengerID, dtype: int64

Зведені таблиці у pandas

Термін “зведена таблиця” добре відомий тим, хто не з чуток знайомий з інструментом Microsoft Excel або будь-яким іншим, призначеним для обробки та аналізу даних. У пандах зведені таблиці будуються через метод .pivot_table. За основу візьмемо той самий приклад з Титаніком. Наприклад, маємо завдання порахувати скільки всього жінок і чоловіків було у конкретному класі корабля:

>>> titanic_df = pd.read_csv('titanic.csv')
>>> pvt = titanic_df.pivot_table(index=['Sex'], columns=['PClass'], values='Name', aggfunc='count')

Як індекс тепер у нас буде стать людини, колонками стануть значення з PClass, функцією агрегування буде count (підрахунок кількості записів) по колонці Name.

>>> print(pvt.loc['female', ['1st', '2nd', '3rd']])
PClass
1st    143.0
2nd    107.0
3rd    212.0
Name: female, dtype: float64

Все дуже просто.

Аналіз тимчасових рядів

У pandas дуже зручно аналізувати часові ряди. Як показовий приклад я використовуватиму ціну на акції корпорації Apple за 5 років по днях.

>>> import pandas as pd
>>> df = pd.read_csv('apple.csv', index_col='Date', parse_dates=True)
>>> df = df.sort_index()
>>> print(df.info())
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1258 entries, 2017-02-22 to 2012-02-23
Data columns (total 6 columns):
Open         1258 non-null float64
High         1258 non-null float64
Low          1258 non-null float64
Close        1258 non-null float64
Volume       1258 non-null int64
Adj Close    1258 non-null float64
dtypes: float64(5), int64(1)
memory usage: 68.8 KB

Тут ми формуємо DataFrame з DatetimeIndex по колонці Date та сортуємо новий індекс у правильному порядку для роботи з вибірками. Якщо колонка має формат дати і часу, відмінний від ISO8601, то для правильного переведення рядка в потрібний тип можна використовувати метод pandas.to_datetime. Давайте тепер дізнаємось середню ціну акції (mean) на закритті (Close):

>>> df.loc['2012-Feb', 'Close'].mean()
528.4820021999999

А якщо взяти проміжок з лютого 2012 по лютий 2015 та порахувати середнє:

>>> df.loc['2012-Feb':'2015-Feb', 'Close'].mean()
430.43968317018414

А якщо нам потрібно дізнатися середню ціну закриття по тижнях?!

>>> df.resample('W')['Close'].mean()
Date
2012-02-26    519.399979
2012-03-04    538.652008
2012-03-11    536.254004
2012-03-18    576.161993
2012-03-25    600.990001
2012-04-01    609.698003
2012-04-08    626.484993
2012-04-15    623.773999
2012-04-22    591.718002
2012-04-29    590.536005
2012-05-06    579.831995
2012-05-13    568.814001
2012-05-20    543.593996
2012-05-27    563.283995
2012-06-03    572.539994
2012-06-10    570.124002
2012-06-17    573.029991
2012-06-24    583.739993
2012-07-01    574.070004
2012-07-08    601.937489
2012-07-15    606.080008
2012-07-22    607.746011
2012-07-29    587.951999
2012-08-05    607.217999
2012-08-12    621.150003
2012-08-19    635.394003
2012-08-26    663.185999
2012-09-02    670.611995
2012-09-09    675.477503
2012-09-16    673.476007
                 ...    
2016-08-07    105.934003
2016-08-14    108.258000
2016-08-21    109.304001
2016-08-28    107.980000
2016-09-04    106.676001
2016-09-11    106.177498
2016-09-18    111.129999
2016-09-25    113.606001
2016-10-02    113.029999
2016-10-09    113.303999
2016-10-16    116.860000
2016-10-23    117.160001
2016-10-30    115.938000
2016-11-06    111.057999
2016-11-13    109.714000
2016-11-20    108.563999
2016-11-27    111.637503
2016-12-04    110.587999
2016-12-11    111.231999
2016-12-18    115.094002
2016-12-25    116.691998
2017-01-01    116.642502
2017-01-08    116.672501
2017-01-15    119.228000
2017-01-22    119.942499
2017-01-29    121.164000
2017-02-05    125.867999
2017-02-12    131.679996
2017-02-19    134.978000
2017-02-26    136.904999
Freq: W-SUN, Name: Close, dtype: float64

Resampling потужний інструмент при роботі з тимчасовими рядами (time series), що допомагає переформувати вибірку так, як вам зручно. Метод resample першим аргументом приймає рядок rule. Усі доступні значення можна знайти у документації