Манипуляция и обработка данных в Python (Data wrangling and munging)

Любой человек, который хоть немного связан со статистикой, бизнес-аналитикой или непосредственно анализом данных, согласится с тем фактом, что несравненно большая часть времени всегда уходит на то, чтобы собрать данные, привести их в удобную для анализа форму, чем на построение модели и даже, чем на программирование production-решения.

Сбор данных мы оставим за кадром, потому что это отдельная тема, которая включает в себя и парсинг, и сложные запросы к базам данных, сегодня мы поговорим о том, как собранными данными можно манипулировать.

Основная часть информации взята из этого конспекта.

In [1]:
import pandas as pd
import numpy as np

# Set some Pandas options
pd.set_option('display.notebook_repr_html', False)
pd.set_option('display.max_columns', 20)
pd.set_option('display.max_rows', 10)

Манипуляции со временем и датами

Вобще, я не хотел переводить эту секцию, но прочитав внимательно, решил перевести, добавив даже самые базовые вещи, потому что моя практика показывает, что это очень часто пригождается.

In [2]:
from datetime import datetime, date, time
now = datetime.now()
print 'Точное время на момент вызова функции: ',now
print 'Сегодняшняя дата:',now.day
print 'День недели по порядку (внимание, 0 - это воскресенье):',now.weekday()
print 'Время в часах и минутах: ',time(3, 24)
print 'Вот так задаётся дата: ',date(1970, 9, 3)
#С этими объектами можно производить арифметические операции
print 'Вычислим возраст: ',
my_age = now - datetime(1970, 9, 3)
print my_age, ', В годах: ', my_age.days/365
Точное время на момент вызова функции:  2014-02-09 22:02:47.007605
Сегодняшняя дата: 9
День недели по порядку (внимание, 0 - это воскресенье): 6
Время в часах и минутах:  03:24:00
Вот так задаётся дата:  1970-09-03
Вычислим возраст:  15865 days, 22:02:47.007605 , В годах:  43

In [3]:
segments = pd.read_csv("../data/AIS/transit_segments.csv")
segments.head()
Out[3]:
   mmsi               name  transit  segment  seg_length  avg_sog  min_sog  \
0     1        Us Govt Ves        1        1         5.1     13.2      9.2   
1     1  Dredge Capt Frank        1        1        13.5     18.6     10.4   
2     1      Us Gov Vessel        1        1         4.3     16.2     10.3   
3     1      Us Gov Vessel        2        1         9.2     15.4     14.5   
4     1  Dredge Capt Frank        2        1         9.2     15.4     14.6   

   max_sog  pdgt10        st_time       end_time  
0     14.5    96.5  2/10/09 16:03  2/10/09 16:27  
1     20.6   100.0   4/6/09 14:31   4/6/09 15:20  
2     20.5   100.0   4/6/09 14:36   4/6/09 14:55  
3     16.1   100.0  4/10/09 17:58  4/10/09 18:34  
4     16.2   100.0  4/10/09 17:59  4/10/09 18:35  

К примеру, посмотрим на распределение длительности поездки. Построим гистограмму.

In [4]:
segments.seg_length.hist(bins=500)
Out[4]:
<matplotlib.axes.AxesSubplot at 0xc9fe08c>

Сейчас будет первое преобразование данных.

In [5]:
segments.seg_length.apply(np.log).hist(bins=500)
Out[5]:
<matplotlib.axes.AxesSubplot at 0xcff010c>

Данные сохранены в datetime формате. Поэтому в первую очередь мы переведём их к этому типу, используя метод strptime(), который считывает datetime объект, основываясь на формате входных данных.

In [6]:
datetime.strptime(segments.st_time.ix[0], '%m/%d/%y %H:%M')
Out[6]:
datetime.datetime(2009, 2, 10, 16, 3)

В модуле dateutil кстати есть функция для автоматического парсинга.

In [7]:
from dateutil.parser import parse
parse(segments.st_time.ix[0])
Out[7]:
datetime.datetime(2009, 2, 10, 16, 3)
In [8]:
segments.st_time.apply(lambda d: datetime.strptime(d, '%m/%d/%y %H:%M'))
Out[8]:
0   2009-02-10 16:03:00
1   2009-04-06 14:31:00
2   2009-04-06 14:36:00
...
262523   2010-06-17 19:16:00
262524   2010-06-18 02:52:00
262525   2010-06-18 10:19:00
Name: st_time, Length: 262526, dtype: datetime64[ns]

Кстати, в Pandas есть плюшка в виде to_datetime() метода, которая парсит и переводит в нужный формат весь объект Series.

In [9]:
pd.to_datetime(segments.st_time)
Out[9]:
0   2009-02-10 16:03:00
1   2009-04-06 14:31:00
2   2009-04-06 14:36:00
...
262523   2010-06-17 19:16:00
262524   2010-06-18 02:52:00
262525   2010-06-18 10:19:00
Name: st_time, Length: 262526, dtype: datetime64[ns]

Ещё надо упомянуть, что в Pandas есть значение NA, с которым в этом наборе данных нам повезло)

In [10]:
pd.to_datetime([None])
Out[10]:
<class 'pandas.tseries.index.DatetimeIndex'>
[NaT]
Length: 1, Freq: None, Timezone: None

Если бы у нас были проблемы с методом to_datetime(), можно было явно задать формат парсинга параметром format= argument.

Слияние объектов DataFrame

Сейчас у нас есть информация о рейсах судов, попытаемся вытащить информацию о самих судах. Она содержится в соседнем файле.

In [11]:
vessels = pd.read_csv("../data/AIS/vessel_information.csv", index_col='mmsi')
vessels.head()
Out[11]:
      num_names                                              names sov  \
mmsi                                                                     
1             8  Bil Holman Dredge/Dredge Capt Frank/Emo/Offsho...   Y   
9             3                         000000009/Raven/Shearwater   N   
21            1                                      Us Gov Vessel   Y   
74            2                                  Mcfaul/Sarah Bell   N   
103           3           Ron G/Us Navy Warship 103/Us Warship 103   Y   

         flag flag_type  num_loas                                    loa  \
mmsi                                                                       
1     Unknown   Unknown         7  42.0/48.0/57.0/90.0/138.0/154.0/156.0   
9     Unknown   Unknown         2                              50.0/62.0   
21    Unknown   Unknown         1                                  208.0   
74    Unknown   Unknown         1                                  155.0   
103   Unknown   Unknown         2                             26.0/155.0   

      max_loa  num_types                             type  
mmsi                                                       
1         156          4  Dredging/MilOps/Reserved/Towing  
9          62          2                     Pleasure/Tug  
21        208          1                          Unknown  
74        155          1                          Unknown  
103       155          2                   Tanker/Unknown  
In [12]:
[v for v in vessels.type.unique() if v.find('/')==-1]
Out[12]:
['Unknown',
 'Other',
 'Tug',
 'Towing',
 'Pleasure',
 'Cargo',
 'WIG',
 'Fishing',
 'BigTow',
 'MilOps',
 'Tanker',
 'Passenger',
 'SAR',
 'Sailing',
 'Reserved',
 'Law',
 'Dredging',
 'AntiPol',
 'Pilot',
 'HSC',
 'Diving',
 'Resol-18',
 'Tender',
 'Spare',
 'Medical']
In [13]:
vessels.type.value_counts()
Out[13]:
Cargo       5622
Tanker      2440
Pleasure     601
...
BigTow/Towing/WIG           1
Towing/Unknown/WIG          1
AntiPol/Fishing/Pleasure    1
Length: 206, dtype: int64

Трудность заключается в том, что некоторые корабли проходили несколько участков пути, поэтому нет прямой связи между двумя таблицами. В Pandas для этого случая предусмотрена возможность создания индекса.

In [14]:
df1 = pd.DataFrame(dict(id=range(4), age=np.random.randint(18, 31, size=4)))
df2 = pd.DataFrame(dict(id=range(3)+range(3), score=np.random.random(size=6)))

df1, df2
Out[14]:
(   age  id
0   18   0
1   28   1
2   26   2
3   27   3,
    id     score
0   0  0.590668
1   1  0.027045
2   2  0.047198
3   0  0.770978
4   1  0.686227
5   2  0.532288)
In [15]:
pd.merge(df1, df2)
Out[15]:
   age  id     score
0   18   0  0.590668
1   18   0  0.770978
2   28   1  0.027045
3   28   1  0.686227
4   26   2  0.047198
5   26   2  0.532288

Стоит заметь, что без информации о каждом столбце, фреймворк сам использовал айдишники из нужного столбца. Если не задано явно, так и будет слияние так и будет происходить на основе столбцов с одинаковыми именами.

Помимо этого, id=3 из df1 был исключен из объединённой таблицы. Это произошло потому, что по умолчанию выполняется inner join, что означает, что результатом будет пересечение множеств.

In [16]:
pd.merge(df1, df2, how='outer')
Out[16]:
   age  id     score
0   18   0  0.590668
1   18   0  0.770978
2   28   1  0.027045
3   28   1  0.686227
4   26   2  0.047198
5   26   2  0.532288
6   27   3       NaN

Outer join, напротив, выдаёт объединение множеств, так что в результирующее множество попадают все строки, недостающие значения которых заполняются NaN-ом. Также можно использовать right join и left join, для того чтобы получить все строки из первой или второй таблицы.

In [17]:
segments.head(1)
Out[17]:
   mmsi         name  transit  segment  seg_length  avg_sog  min_sog  max_sog  \
0     1  Us Govt Ves        1        1         5.1     13.2      9.2     14.5   

   pdgt10        st_time       end_time  
0    96.5  2/10/09 16:03  2/10/09 16:27  
In [18]:
vessels.head(1)
Out[18]:
      num_names                                              names sov  \
mmsi                                                                     
1             8  Bil Holman Dredge/Dredge Capt Frank/Emo/Offsho...   Y   

         flag flag_type  num_loas                                    loa  \
mmsi                                                                       
1     Unknown   Unknown         7  42.0/48.0/57.0/90.0/138.0/154.0/156.0   

      max_loa  num_types                             type  
mmsi                                                       
1         156          4  Dredging/MilOps/Reserved/Towing  

значение mmsi (идентификатор судна) присутствует в каждой таблицы. В таблицы кораблей он используется как индекс. В этом случае мы явно укажем, что хотим его использовать как индекс результирующей таблицы.

In [19]:
segments_merged = pd.merge(vessels, segments, left_index=True, right_on='mmsi')
segments_merged.head()
Out[19]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5 entries, 0 to 4
Data columns (total 21 columns):
num_names     5  non-null values
names         5  non-null values
sov           5  non-null values
flag          5  non-null values
flag_type     5  non-null values
num_loas      5  non-null values
loa           5  non-null values
max_loa       5  non-null values
num_types     5  non-null values
type          5  non-null values
mmsi          5  non-null values
name          5  non-null values
transit       5  non-null values
segment       5  non-null values
seg_length    5  non-null values
avg_sog       5  non-null values
min_sog       5  non-null values
max_sog       5  non-null values
pdgt10        5  non-null values
st_time       5  non-null values
end_time      5  non-null values
dtypes: float64(6), int64(6), object(9)

В этом случае, нам подошёл применяемый по умолчанию inner join. Нам не нужны объекты, для которых отсутствует соотвествие во второй таблице.

Особое внимание стоит обратить на то, что mmsi был индексом для кораблей, но больше не является индексом для объединённой таблицы.

In [20]:
vessels.merge(segments, left_index=True, right_on='mmsi').head()
Out[20]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5 entries, 0 to 4
Data columns (total 21 columns):
num_names     5  non-null values
names         5  non-null values
sov           5  non-null values
flag          5  non-null values
flag_type     5  non-null values
num_loas      5  non-null values
loa           5  non-null values
max_loa       5  non-null values
num_types     5  non-null values
type          5  non-null values
mmsi          5  non-null values
name          5  non-null values
transit       5  non-null values
segment       5  non-null values
seg_length    5  non-null values
avg_sog       5  non-null values
min_sog       5  non-null values
max_sog       5  non-null values
pdgt10        5  non-null values
st_time       5  non-null values
end_time      5  non-null values
dtypes: float64(6), int64(6), object(9)

Иногда бывает так, что данные дуплицируются, или содержат разную информацию для столбцов с одинаковыми названиями. В этом случае им будут присвоены суффиксы _x и _y, чтобы названия были уникальными.

In [21]:
segments['type'] = 'foo'
pd.merge(vessels, segments, left_index=True, right_on='mmsi').head()
Out[21]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5 entries, 0 to 4
Data columns (total 22 columns):
num_names     5  non-null values
names         5  non-null values
sov           5  non-null values
flag          5  non-null values
flag_type     5  non-null values
num_loas      5  non-null values
loa           5  non-null values
max_loa       5  non-null values
num_types     5  non-null values
type_x        5  non-null values
mmsi          5  non-null values
name          5  non-null values
transit       5  non-null values
segment       5  non-null values
seg_length    5  non-null values
avg_sog       5  non-null values
min_sog       5  non-null values
max_sog       5  non-null values
pdgt10        5  non-null values
st_time       5  non-null values
end_time      5  non-null values
type_y        5  non-null values
dtypes: float64(6), int64(6), object(10)

Это поведение может быть определно явно заданием соотвествующих аргументов в параметрах функции.

Конкатенация

В большинстве случаев все просто добавляют строки или столбцы в набор данных, если они совпадают по размером. В NumPy для этого используется конкатенция, реализованная в функциях c_ и r_.

In [22]:
np.concatenate([np.random.random(5), np.random.random(5)])
Out[22]:
array([ 0.98179923,  0.61371717,  0.77858056,  0.20064324,  0.59358792,
        0.40778113,  0.45379343,  0.3290517 ,  0.29434308,  0.72379453])
In [23]:
np.r_[np.random.random(5), np.random.random(5)]
Out[23]:
array([ 0.31988763,  0.55793215,  0.18486309,  0.25072852,  0.01753147,
        0.78049506,  0.64616581,  0.56494967,  0.09987545,  0.19448227])
In [24]:
np.c_[np.random.random(5), np.random.random(5)]
Out[24]:
array([[ 0.40398302,  0.7743685 ],
       [ 0.77238525,  0.35256306],
       [ 0.75905584,  0.32804947],
       [ 0.36221409,  0.75150748],
       [ 0.51589434,  0.90081062]])

В англоязычной литературе эта операция также называется binding или stacking. Благодаря тому, что в Pandas все данные проиндексированы, мы получаем дополнительные ограничения на значения индексов двух конкатенируемых структур.

Загрузим два набора данных о микробиоме, каждый из которых содержит количество микроорганизмов определённого пациента. Первая колонка будет использоваться в качестве индекса.

In [25]:
mb1 = pd.read_excel('../data/microbiome/MID1.xls', 'Sheet 1', index_col=0, header=None)
mb2 = pd.read_excel('../data/microbiome/MID2.xls', 'Sheet 1', index_col=0, header=None)
mb1.shape, mb2.shape
Out[25]:
((272, 1), (288, 1))
In [26]:
mb1.head()
Out[26]:
                                                                                         1
0                                                                                         
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera    7
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus          2
Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus              3
Archaea "Crenarchaeota" Thermoprotei Thermoproteales Thermofilaceae Thermofilum          3
Archaea "Euryarchaeota" "Methanomicrobia" Methanocellales Methanocellaceae Methanocella  7

Зададим имена индексу и столбцам.

In [27]:
mb1.columns = mb2.columns = ['Count']

Индекс для этих данные будет уникальным биологическим классификатором каждого организма, начинающийся с царства и так далее до вида.

In [28]:
mb1.index.name = mb2.index.name = 'Taxon'
In [29]:
mb1.head()
Out[29]:
                                                                                         Count
Taxon                                                                                         
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera        7
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus              2
Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus                  3
Archaea "Crenarchaeota" Thermoprotei Thermoproteales Thermofilaceae Thermofilum              3
Archaea "Euryarchaeota" "Methanomicrobia" Methanocellales Methanocellaceae Methanocella      7
In [30]:
mb1.index[:3]
Out[30]:
Index([u'Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera', u'Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus', u'Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus'], dtype=object)
In [31]:
mb1.index.is_unique
Out[31]:
True

Если конкатенация происходит со значением axis=0 (значение по умолчанию), то мы получим новый набор данных с конкатенированными строками.

In [32]:
pd.concat([mb1, mb2], axis=0).shape
Out[32]:
(560, 1)

Естественно, индекс перестаёт быть уникальным, из-за пересечения между двумя наборами данных.

In [33]:
pd.concat([mb1, mb2], axis=0).index.is_unique
Out[33]:
False

Конкатенация с axis=1 будет проведена для столбцов, учитывая индексы каждого из двух DataFrame'ов

In [34]:
pd.concat([mb1, mb2], axis=1).shape
Out[34]:
(438, 2)
In [35]:
pd.concat([mb1, mb2], axis=1).head()
Out[35]:
                                                                                            Count  \
Archaea "Crenarchaeota" Thermoprotei Acidilobales Acidilobaceae Acidilobus                    NaN   
Archaea "Crenarchaeota" Thermoprotei Acidilobales Caldisphaeraceae Caldisphaera               NaN   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera           7   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Sulfophobococcus    NaN   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Thermosphaera       NaN   

                                                                                            Count  
Archaea "Crenarchaeota" Thermoprotei Acidilobales Acidilobaceae Acidilobus                      2  
Archaea "Crenarchaeota" Thermoprotei Acidilobales Caldisphaeraceae Caldisphaera                14  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera          23  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Sulfophobococcus      1  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Thermosphaera         2  
In [36]:
pd.concat([mb1, mb2], axis=1).values[:5]
Out[36]:
array([[ nan,   2.],
       [ nan,  14.],
       [  7.,  23.],
       [ nan,   1.],
       [ nan,   2.]])

Если нам интересны только таксоны, которые заключены в сразу в обоих наборах данных, то мы явно задаем аргумент join=inner

In [37]:
pd.concat([mb1, mb2], axis=1, join='inner').head()
Out[37]:
                                                                                         Count  \
Taxon                                                                                            
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera        7   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus              2   
Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus                  3   
Archaea "Crenarchaeota" Thermoprotei Thermoproteales Thermofilaceae Thermofilum              3   
Archaea "Euryarchaeota" "Methanomicrobia" Methanocellales Methanocellaceae Methanocella      7   

                                                                                         Count  
Taxon                                                                                           
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera       23  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus              2  
Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus                 10  
Archaea "Crenarchaeota" Thermoprotei Thermoproteales Thermofilaceae Thermofilum              9  
Archaea "Euryarchaeota" "Methanomicrobia" Methanocellales Methanocellaceae Methanocella      9  

Если мы хотим, чтобы пробелы в первой таблице были заполнены данными из второй, то надо задавать combine_first.

In [38]:
mb1.combine_first(mb2).head()
Out[38]:
                                                                                            Count
Taxon                                                                                            
Archaea "Crenarchaeota" Thermoprotei Acidilobales Acidilobaceae Acidilobus                      2
Archaea "Crenarchaeota" Thermoprotei Acidilobales Caldisphaeraceae Caldisphaera                14
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera           7
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Sulfophobococcus      1
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Thermosphaera         2

Мы можем задать иерархический индекс, построенный на ключах, заданных для исходных таблиц

In [39]:
pd.concat([mb1, mb2], keys=['patient1', 'patient2']).head()
Out[39]:
                                                                                                  Count
         Taxon                                                                                         
patient1 Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera        7
         Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus              2
         Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus                  3
         Archaea "Crenarchaeota" Thermoprotei Thermoproteales Thermofilaceae Thermofilum              3
         Archaea "Euryarchaeota" "Methanomicrobia" Methanocellales Methanocellaceae Methanocella      7
In [40]:
pd.concat([mb1, mb2], keys=['patient1', 'patient2']).index.is_unique
Out[40]:
True

Ну или мы можем передать ключи для конкатенации, задавая DataFrame (или Series) объект как словарь.

In [41]:
pd.concat(dict(patient1=mb1, patient2=mb2), axis=1).head()
Out[41]:
                                                                                            patient1  \
                                                                                               Count   
Archaea "Crenarchaeota" Thermoprotei Acidilobales Acidilobaceae Acidilobus                       NaN   
Archaea "Crenarchaeota" Thermoprotei Acidilobales Caldisphaeraceae Caldisphaera                  NaN   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera              7   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Sulfophobococcus       NaN   
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Thermosphaera          NaN   

                                                                                            patient2  
                                                                                               Count  
Archaea "Crenarchaeota" Thermoprotei Acidilobales Acidilobaceae Acidilobus                         2  
Archaea "Crenarchaeota" Thermoprotei Acidilobales Caldisphaeraceae Caldisphaera                   14  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera             23  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Sulfophobococcus         1  
Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Thermosphaera            2  

Трансформируем DataFrame (Reshaping DataFrame)

Работая с DataFrame'ом, нам часто бывает нужно каким-то образом трансформировать данные. Сейчас мы загрузим набор данных из книги Statistical Methods for the Analysis of Repeated Measurements by Charles S. Davis, pp. 161-163 (Springer, 2002). Это данные случайного испытания токсина на пациентах с определённой болезнью из 9 штатов США, разделённые на группу с Плацево (36), 5000 миллиграмм токсина (36), 10000 единиц (37). Отклик представляет собой значение Toronto Western Spasmodic Torticollis Rating Scale (TWSTRS), которые измеряет уровень боли и лечнеия от болезни. Измерялся этот уровень на неделях 0, 2, 4, 8, 12 и 16 после начала лечения.

In [42]:
cdystonia = pd.read_csv("../data/cdystonia.csv", index_col=None)
cdystonia.head()
Out[42]:
   patient  obs  week  site  id  treat  age sex  twstrs
0        1    1     0     1   1  5000U   65   F      32
1        1    2     2     1   1  5000U   65   F      30
2        1    3     4     1   1  5000U   65   F      24
3        1    4     8     1   1  5000U   65   F      37
4        1    5    12     1   1  5000U   65   F      39

Этот набор данных включает в себя repeated measurements одних и тех же людей. Информацию можно представить как минмиум двумя путями: для каждого измерения задавать 1 строку, или группировать измерения для одного пациента. Метод stack() используется для того, чтобы представить столбцы как строки:

In [43]:
stacked = cdystonia.stack()
stacked
Out[43]:
0  patient    1
   obs        1
   week       0
...
630  age       57
     sex        M
     twstrs    51
Length: 5679, dtype: object

Дополняя вышесказанное

In [44]:
stacked.unstack().head()
Out[44]:
  patient obs week site id  treat age sex twstrs
0       1   1    0    1  1  5000U  65   F     32
1       1   2    2    1  1  5000U  65   F     30
2       1   3    4    1  1  5000U  65   F     24
3       1   4    8    1  1  5000U  65   F     37
4       1   5   12    1  1  5000U  65   F     39

Для этого набора данных имеет смысл создание иерархического индекса, основываясь на пациенте и замере уровня TWSTRS

In [45]:
cdystonia2 = cdystonia.set_index(['patient','obs'])
cdystonia2.head()
Out[45]:
             week  site  id  treat  age sex  twstrs
patient obs                                        
1       1       0     1   1  5000U   65   F      32
        2       2     1   1  5000U   65   F      30
        3       4     1   1  5000U   65   F      24
        4       8     1   1  5000U   65   F      37
        5      12     1   1  5000U   65   F      39
In [46]:
cdystonia2.index.is_unique
Out[46]:
True

Если мы будем трансформировать данные так что repeated measurements будут содержатьсяв с толбцах, то мы можем отвезать twstrs отталкиваясь от наблюдений.

In [47]:
twstrs_wide = cdystonia2['twstrs'].unstack('obs')
twstrs_wide.head()
Out[47]:
obs       1   2   3   4   5   6
patient                        
1        32  30  24  37  39  36
2        60  26  27  41  65  67
3        44  20  23  26  35  35
4        53  61  64  62 NaN NaN
5        53  35  48  49  41  51
In [48]:
cdystonia_long = cdystonia[['patient','site','id','treat','age','sex']].drop_duplicates().merge(
                    twstrs_wide, right_index=True, left_on='patient', how='inner').head()
cdystonia_long
Out[48]:
    patient  site  id    treat  age sex   1   2   3   4   5   6
0         1     1   1    5000U   65   F  32  30  24  37  39  36
6         2     1   2   10000U   70   F  60  26  27  41  65  67
12        3     1   3    5000U   64   F  44  20  23  26  35  35
18        4     1   4  Placebo   59   F  53  61  64  62 NaN NaN
22        5     1   5   10000U   76   F  53  35  48  49  41  51

Более правильный способ сделать это задать уровень для каждого пациента в качестве индекса прежде чем разгруппировывать даные:

In [49]:
cdystonia.set_index(['patient','site','id','treat','age','sex','week'])['twstrs'].unstack('week').head()
Out[49]:
week                             0   2   4   8   12  16
patient site id treat   age sex                        
1       1    1  5000U   65  F    32  30  24  37  39  36
2       1    2  10000U  70  F    60  26  27  41  65  67
3       1    3  5000U   64  F    44  20  23  26  35  35
4       1    4  Placebo 59  F    53  61  64  62 NaN NaN
5       1    5  10000U  76  F    53  35  48  49  41  51

Чтобы перевисть наш формат назад к исходному виду мы используе функцию melt() со следующими параметрами:

In [50]:
pd.melt(cdystonia_long, id_vars=['patient','site','id','treat','age','sex'], 
        var_name='obs', value_name='twsters').head()
Out[50]:
   patient  site  id    treat  age sex  obs  twsters
0        1     1   1    5000U   65   F    1       32
1        2     1   2   10000U   70   F    1       60
2        3     1   3    5000U   64   F    1       44
3        4     1   4  Placebo   59   F    1       53
4        5     1   5   10000U   76   F    1       53

Этот пример иллюстрирует два подхода к содержанию таких данных: "длинный" и "широкий". В общем случае лучше хранить данные в "длинном" формате, потому что овые наблюдения легко добавляются как строки, тогда как "широкий" формат предполагает изменение схема базы данных при каждом добавлении наблюения. Лучший для анализа данных формат зависит исключительно от того, какие процедуры планируется делать с данными, так что очень важно легко переключаться между данными.

Транспонирование (Pivoting)

Метод pivot() позволяет переводить DataFrame из "длинного" в "широкий" формат. Он использует три аргумента: индекс, строки и столбцы, которые соответствуют идексу (заголовки рядов), колонок и значения данных. К примеру, можно адать значения TWSTRS в широком формате в зависимости от пациента

In [51]:
cdystonia.pivot(index='patient', columns='obs', values='twstrs').head()
Out[51]:
obs       1   2   3   4   5   6
patient                        
1        32  30  24  37  39  36
2        60  26  27  41  65  67
3        44  20  23  26  35  35
4        53  61  64  62 NaN NaN
5        53  35  48  49  41  51

Если мы опустим аргумент со значениями, мы получим DataFrame с иерархически упорядоченными колонками, как будто мы применили unstack() к иерархически-индексированной таблице.

In [52]:
cdystonia.pivot('patient', 'obs')
Out[52]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 109 entries, 1 to 109
Data columns (total 42 columns):
(week, 1)      109  non-null values
(week, 2)      103  non-null values
(week, 3)      106  non-null values
(week, 4)      104  non-null values
(week, 5)      104  non-null values
(week, 6)      105  non-null values
(site, 1)      109  non-null values
(site, 2)      103  non-null values
(site, 3)      106  non-null values
(site, 4)      104  non-null values
(site, 5)      104  non-null values
(site, 6)      105  non-null values
(id, 1)        109  non-null values
(id, 2)        103  non-null values
(id, 3)        106  non-null values
(id, 4)        104  non-null values
(id, 5)        104  non-null values
(id, 6)        105  non-null values
(treat, 1)     109  non-null values
(treat, 2)     103  non-null values
(treat, 3)     106  non-null values
(treat, 4)     104  non-null values
(treat, 5)     104  non-null values
(treat, 6)     105  non-null values
(age, 1)       109  non-null values
(age, 2)       103  non-null values
(age, 3)       106  non-null values
(age, 4)       104  non-null values
(age, 5)       104  non-null values
(age, 6)       105  non-null values
(sex, 1)       109  non-null values
(sex, 2)       103  non-null values
(sex, 3)       106  non-null values
(sex, 4)       104  non-null values
(sex, 5)       104  non-null values
(sex, 6)       105  non-null values
(twstrs, 1)    109  non-null values
(twstrs, 2)    103  non-null values
(twstrs, 3)    106  non-null values
(twstrs, 4)    104  non-null values
(twstrs, 5)    104  non-null values
(twstrs, 6)    105  non-null values
dtypes: float64(30), object(12)

Схожий метод, pivot_table позволяет создать набор данных очень близкий к экселевской таблице с иерархическим индексом и позвоялет значениям таблици быть выбранными используя следующие функции

In [53]:
cdystonia.pivot_table(rows=['site', 'treat'], cols='week', values='twstrs', aggfunc=max).head(20)
Out[53]:
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 20 entries, (1, 10000U) to (7, 5000U)
Data columns (total 6 columns):
0     20  non-null values
2     20  non-null values
4     20  non-null values
8     20  non-null values
12    20  non-null values
16    20  non-null values
dtypes: int64(6)

Для того, чтобы рассчитать кросс-таб групповых частот, используется функция crosstab (но не метод!), которые аггрегирует количество данных, основываясь на фактораных значениях столбцов и строк. Значения фактора могут иметь иерархию.

In [54]:
pd.crosstab(cdystonia.sex, cdystonia.site)
Out[54]:
site   1   2   3   4   5   6   7   8   9
sex                                     
F     52  53  42  30  22  54  66  48  28
M     18  29  30  18  11  33   6  58  33

Трансорфмация данных (Data transformation)

Есть ещё несколько методов, которые используются при обработке данных, такие как удаление повторов, замена переменных и их группировка.

Убираем повторы

Можно достаточно легко убрать все повторы значения из объекта DataFrame. К примеру, мы хотим убрать корабли из предыдущего набора данных, которые имеют одинаковые имена:

In [55]:
vessels.duplicated(cols='names')
Out[55]:
mmsi
1       False
9       False
21      False
...
975318642     True
987654321    False
999999999     True
Length: 10771, dtype: bool
In [56]:
vessels.drop_duplicates(['names'])
Out[56]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 10253 entries, 1 to 987654321
Data columns (total 10 columns):
num_names    10253  non-null values
names        10253  non-null values
sov          10253  non-null values
flag         10253  non-null values
flag_type    10253  non-null values
num_loas     10253  non-null values
loa          10253  non-null values
max_loa      10253  non-null values
num_types    10253  non-null values
type         10253  non-null values
dtypes: float64(1), int64(3), object(6)

Замена значений

Довольно часто значения столбцов бывают представлены как строки, когда нам для анализа нужно, чтобы они были числами, особенно для количественного анализа. К примеру, посмотрим на биологический набор данных:

In [57]:
cdystonia.treat.value_counts()
Out[57]:
10000U     213
5000U      211
Placebo    207
dtype: int64

Логично будет перевести эти данные к числовому виду, использую Плацебо как 0ое значеие. Если мы зададим словарь с оргинальными значениям как ключами и соотвествующими заменнами, то мы сможем использовать прекрасную питоновскую функцию map()

In [58]:
treatment_map = {'Placebo': 0, '5000U': 1, '10000U': 2}
cdystonia['treatment'] = cdystonia.treat.map(treatment_map)
cdystonia.treatment
Out[58]:
0    1
1    1
2    1
...
628    1
629    1
630    1
Name: treatment, Length: 631, dtype: int64

Другой вариант это напрямую заменить значения, используя replace()

Этот пример прекрасно работет при замене нулевых значений, к примеру, если нам нужно взять логарифм от ряда:

In [59]:
vals = pd.Series([float(i)**10 for i in range(10)])
vals
Out[59]:
0             0
1             1
2          1024
3         59049
4       1048576
5       9765625
6      60466176
7     282475249
8    1073741824
9    3486784401
dtype: float64
In [60]:
np.log(vals)
Out[60]:
0         -inf
1     0.000000
2     6.931472
3    10.986123
4    13.862944
5    16.094379
6    17.917595
7    19.459101
8    20.794415
9    21.972246
dtype: float64

В таких ситуациях мы можем заменить 0 очень близким к нему значением, что не повлияет на результат анаиза. Для этого используем replace()

In [61]:
vals = vals.replace(0, 1e-6)
np.log(vals)
Out[61]:
0   -13.815511
1     0.000000
2     6.931472
3    10.986123
4    13.862944
5    16.094379
6    17.917595
7    19.459101
8    20.794415
9    21.972246
dtype: float64

Также можно сделать тоже самое при помощи map():

In [62]:
cdystonia2.treat.replace({'Placebo': 0, '5000U': 1, '10000U': 2})
Out[62]:
patient  obs
1        1      1
         2      1
         3      1
...
109      4      1
         5      1
         6      1
Name: treat, Length: 631, dtype: object

Индикаторные переменные

В некоторых случаях (при построение регрессионной модели и при анализе дисперсии), категориальные или групповые переменные необходимо веревести в столбцы с индикаторными переменными, чтобы сделать то, что называется design matrix. Для этого есть встроенная в Pandas функция get_dummies()

В наборе данных кораблей переменные типа определяет класс судна. Мы можем создать матрицу для этого случая. К примеру, выберем 5 наиболее частых видов корабля:

In [63]:
top5 = vessels.type.apply(lambda s: s in vessels.type.value_counts().index[:5])
vessels5 = vessels[top5]
In [64]:
pd.get_dummies(vessels5.type).head(10)
Out[64]:
         Cargo  Pleasure  Sailing  Tanker  Tug
mmsi                                          
15151        0         0        0       0    1
80404        0         1        0       0    0
366235       1         0        0       0    0
587370       0         0        0       0    1
693559       0         0        0       0    1
1233916      0         1        0       0    0
3041300      1         0        0       0    0
3663760      1         0        0       0    0
3688360      1         0        0       0    0
7718175      1         0        0       0    0

Дискретезация (Discretization)

Функция cut() может быть использована для разбиения на группы количественной переменной. В общем случае дискретезация это плохая идея, поэтому надо быть аккуратным при её использовании! Допустим, мы хотим разбить на возрастные группы пациентов из второго набора данных.

Преобразуем данные в декады, начиная с 20летней группы пациентов:

In [65]:
cdystonia.age.describe()
Out[65]:
count    631.000000
mean      55.616482
std       12.123910
min       26.000000
25%       46.000000
50%       56.000000
75%       65.000000
max       83.000000
dtype: float64
In [66]:
pd.cut(cdystonia.age, [20,30,40,50,60,70,80,90])[:30]
Out[66]:
Categorical: 
[(60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (60, 70], (50, 60], (50, 60], (50, 60], (50, 60], (70, 80], (70, 80], (70, 80], (70, 80], (70, 80], (70, 80], (50, 60], (50, 60]]
Levels (7): Index(['(20, 30]', '(30, 40]', '(40, 50]', '(50, 60]',
                   '(60, 70]', '(70, 80]', '(80, 90]'], dtype=object)

Скобки означают открытый интервал, невключающий границу, в то время как квадратные скобки соотвествуют закрытому (если не ошибаюсь в термине) множеству, совсем как в классическом матане. Этим можно управлять, задавая значения параметров:

In [67]:
pd.cut(cdystonia.age, [20,30,40,50,60,70,80,90], right=False)[:30]
Out[67]:
Categorical: 
[[60, 70), [60, 70), [60, 70), [60, 70), [60, 70), [60, 70), [70, 80), [70, 80), [70, 80), [70, 80), [70, 80), [70, 80), [60, 70), [60, 70), [60, 70), [60, 70), [60, 70), [60, 70), [50, 60), [50, 60), [50, 60), [50, 60), [70, 80), [70, 80), [70, 80), [70, 80), [70, 80), [70, 80), [50, 60), [50, 60)]
Levels (7): Index(['[20, 30)', '[30, 40)', '[40, 50)', '[50, 60)',
                   '[60, 70)', '[70, 80)', '[80, 90)'], dtype=object)

Поскольку данные теперь соответсвуют классам, мы зададим им названия:

In [68]:
pd.cut(cdystonia.age, [20,40,60,80,90], labels=['young','middle-aged','old','ancient'])[:30]
Out[68]:
Categorical: 
[old, old, old, old, old, old, old, old, old, old, old, old, old, old, old, old, old, old, middle-aged, middle-aged, middle-aged, middle-aged, old, old, old, old, old, old, middle-aged, middle-aged]
Levels (4): Index(['young', 'middle-aged', 'old', 'ancient'], dtype=object)

Функция qcut() использует эмпирические квантили для разделения данных. К примеру, нам нужны следующие квантили: (0-25%], (25-50%], (50-70%], (75-100%]. Зададим 4 интервала:

In [69]:
pd.qcut(cdystonia.age, 4)[:30]
Out[69]:
Categorical: 
[(56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (65, 83], (65, 83], (65, 83], (65, 83], (65, 83], (65, 83], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (56, 65], (65, 83], (65, 83], (65, 83], (65, 83], (65, 83], (65, 83], (56, 65], (56, 65]]
Levels (4): Index(['[26, 46]', '(46, 56]', '(56, 65]', '(65, 83]'], dtype=object)

Моно также задать собственные значения квантилей, чтобы дублировать поведение функции cut(). При этом дискритезацию можно совмещать с генерацией индикаторных переменных:

In [70]:
quantiles = pd.qcut(segments.seg_length, [0, 0.01, 0.05, 0.95, 0.99, 1])
quantiles[:30]
Out[70]:
Categorical: 
[(1.8, 7.8], (7.8, 45.4], (1.8, 7.8], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (45.4, 89.7], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (1.8, 7.8], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (45.4, 89.7], (45.4, 89.7], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (1.8, 7.8], (1.8, 7.8], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4], (7.8, 45.4]]
Levels (5): Index(['[1, 1.8]', '(1.8, 7.8]', '(7.8, 45.4]',
                   '(45.4, 89.7]', '(89.7, 1882]'], dtype=object)
In [71]:
pd.get_dummies(quantiles).head(10)
Out[71]:
   (1.8, 7.8]  (45.4, 89.7]  (7.8, 45.4]  (89.7, 1882]  [1, 1.8]
0           1             0            0             0         0
1           0             0            1             0         0
2           1             0            0             0         0
3           0             0            1             0         0
4           0             0            1             0         0
5           0             0            1             0         0
6           0             1            0             0         0
7           0             0            1             0         0
8           0             0            1             0         0
9           0             0            1             0         0

Выборка данных (Permutation and sampling)

При проведении анализа с симуляцией данных, бывает нужно выбрать случайное подмножество. Вызов функции NumPy permutation() позволяет сделать это, задав нужную длину последовательности.

In [72]:
new_order = np.random.permutation(len(segments))
new_order[:30]
Out[72]:
array([233291, 174223, 158363, 201518,  39164,  31182, 111557, 140949,
       231180, 158266, 200939, 166985,  94903, 182303, 134475, 224992,
        87419, 142770,   3968, 160500, 173314, 196554, 231583,  99247,
       257406, 134185, 198516, 105549, 220238,  79198])

Испльзуя эту поседовательность как аргумент для метода .take() получим переупорядоченный массив данных:

In [73]:
segments.take(new_order).head()
Out[73]:
             mmsi               name  transit  segment  seg_length  avg_sog  \
233291  564280000         Pac Alnath       25        1        21.0     12.3   
174223  367416750           Blue Fin       15        1        18.4     11.4   
158363  367174870        Osg Freedom       23        1        26.1     10.3   
201518  372676000  Hanjin Chittagong        1        1        24.4      9.5   
39164   250000848   Asphalt Seminole       21        1        28.5      9.5   

        min_sog  max_sog  pdgt10         st_time       end_time type  
233291      8.7     13.6    95.1  10/16/12 22:19  10/17/12 0:01  foo  
174223     10.7     12.2   100.0    7/13/11 9:21  7/13/11 10:58  foo  
158363      9.3     11.0    82.3    3/6/10 21:25   3/6/10 23:56  foo  
201518      7.3     10.6    16.8   12/30/08 5:11  12/30/08 7:35  foo  
39164       8.4     12.8    11.0   2/28/10 21:01  2/28/10 23:59  foo  

Сравним этот порядок с исходным:

In [74]:
segments.head()
Out[74]:
   mmsi               name  transit  segment  seg_length  avg_sog  min_sog  \
0     1        Us Govt Ves        1        1         5.1     13.2      9.2   
1     1  Dredge Capt Frank        1        1        13.5     18.6     10.4   
2     1      Us Gov Vessel        1        1         4.3     16.2     10.3   
3     1      Us Gov Vessel        2        1         9.2     15.4     14.5   
4     1  Dredge Capt Frank        2        1         9.2     15.4     14.6   

   max_sog  pdgt10        st_time       end_time type  
0     14.5    96.5  2/10/09 16:03  2/10/09 16:27  foo  
1     20.6   100.0   4/6/09 14:31   4/6/09 15:20  foo  
2     20.5   100.0   4/6/09 14:36   4/6/09 14:55  foo  
3     16.1   100.0  4/10/09 17:58  4/10/09 18:34  foo  
4     16.2   100.0  4/10/09 17:59  4/10/09 18:35  foo  

Аггрегация данных и GroupBy()

Одним из самых главных достоинств Pandas является GroupBy(). Он очень помогает в тех ситуациях, когда мы хотим манипулировать подвыборками в данных. К примеру, если надо вычислить сумму мат ожиданий для каждой группы (класса) в массиве данных, мы используем apply() и затем, можем, например, построить графики, нормализовать или сделать что-нибудь ещё.

In [75]:
cdystonia_grouped = cdystonia.groupby(cdystonia.patient)

Эту выборку сложно отрисовать

In [76]:
cdystonia_grouped
Out[76]:
<pandas.core.groupby.DataFrameGroupBy object at 0xe1529cc>

Обычно, группирование является промежуточным шагом. К примеру, иногда мы можем захотеть пройтись по какой-нибудь группе пациентов:

In [78]:
for patient, group in cdystonia_grouped:
    print patient
    print group
    printfor patient, group in cdystonia_grouped:
    print patient
    print group
    print
  File "<ipython-input-78-9dc855fe9755>", line 4
    printfor patient, group in cdystonia_grouped:
                   ^
SyntaxError: invalid syntax

Если это нужно, мы легко можем аггрегировать значения по нескольким ключам:

In [79]:
cdystonia_grouped.agg(mean).head()
Out[79]:
         patient  obs  week  site  id  age     twstrs  treatment
patient                                                         
1              1  3.5   7.0     1   1   65  33.000000          1
2              2  3.5   7.0     1   2   70  47.666667          2
3              3  3.5   7.0     1   3   64  30.500000          1
4              4  2.5   3.5     1   4   59  60.000000          0
5              5  3.5   7.0     1   5   76  46.166667          2
In [80]:
cdystonia_grouped.mean().head()
Out[80]:
         patient  obs  week  site  id  age     twstrs  treatment
patient                                                         
1              1  3.5   7.0     1   1   65  33.000000          1
2              2  3.5   7.0     1   2   70  47.666667          2
3              3  3.5   7.0     1   3   64  30.500000          1
4              4  2.5   3.5     1   4   59  60.000000          0
5              5  3.5   7.0     1   5   76  46.166667          2

Следующие функции используются для задания имён новым столбцам

In [81]:
cdystonia_grouped.mean().add_suffix('_mean').head()
Out[81]:
         patient_mean  obs_mean  week_mean  site_mean  id_mean  age_mean  \
patient                                                                    
1                   1       3.5        7.0          1        1        65   
2                   2       3.5        7.0          1        2        70   
3                   3       3.5        7.0          1        3        64   
4                   4       2.5        3.5          1        4        59   
5                   5       3.5        7.0          1        5        76   

         twstrs_mean  treatment_mean  
patient                               
1          33.000000               1  
2          47.666667               2  
3          30.500000               1  
4          60.000000               0  
5          46.166667               2  
In [82]:
# The median of the `twstrs` variable
cdystonia_grouped['twstrs'].quantile(0.5)
Out[82]:
patient
1          34.0
2          50.5
3          30.5
...
107        44.0
108        50.5
109        38.0
Length: 109, dtype: float64
In [83]:
cdystonia.groupby(['week','site']).mean().head()
Out[83]:
           patient  obs   id        age     twstrs  treatment
week site                                                    
0    1         6.5    1  6.5  59.000000  43.083333   1.000000
     2        19.5    1  7.5  53.928571  51.857143   0.928571
     3        32.5    1  6.5  51.500000  38.750000   1.000000
     4        42.5    1  4.5  59.250000  48.125000   1.000000
     5        49.5    1  3.5  51.833333  49.333333   1.000000

Также можно трансформировать данные, используя нужную функцию и метод transform()

In [84]:
normalize = lambda x: (x - x.mean())/x.std()

cdystonia_grouped.transform(normalize).head()
Out[84]:
   patient       obs      week  site  id  age    twstrs  treatment
0      NaN -1.336306 -1.135550   NaN NaN  NaN -0.181369        NaN
1      NaN -0.801784 -0.811107   NaN NaN  NaN -0.544107        NaN
2      NaN -0.267261 -0.486664   NaN NaN  NaN -1.632322        NaN
3      NaN  0.267261  0.162221   NaN NaN  NaN  0.725476        NaN
4      NaN  0.801784  0.811107   NaN NaN  NaN  1.088214        NaN

При помощи groupby() можно легко выбирать данные из столбцов, если нам надо сделать split-apply-combine на некотором подмножестве столбцов

In [85]:
cdystonia_grouped['twstrs'].mean().head()
Out[85]:
patient
1          33.000000
2          47.666667
3          30.500000
4          60.000000
5          46.166667
Name: twstrs, dtype: float64
In [86]:
# This gives the same result as a DataFrame
cdystonia_grouped[['twstrs']].mean().head()
Out[86]:
            twstrs
patient           
1        33.000000
2        47.666667
3        30.500000
4        60.000000
5        46.166667
Если мы банально хотим разбить массив на несколько кусков, то можно перевести его в словарь:
In [87]:
chunks = dict(list(cdystonia_grouped))
chunks[4]
Out[87]:
    patient  obs  week  site  id    treat  age sex  twstrs  treatment
18        4    1     0     1   4  Placebo   59   F      53          0
19        4    2     2     1   4  Placebo   59   F      61          0
20        4    3     4     1   4  Placebo   59   F      64          0
21        4    4     8     1   4  Placebo   59   F      62          0

Стандартный вызов groupby() работает со строками, но можно задать аргумент axis, чтобы заставить его работать со столбцами:

In [88]:
dict(list(cdystonia.groupby(cdystonia.dtypes, axis=1)))
Out[88]:
{dtype('int64'): <class 'pandas.core.frame.DataFrame'>
Int64Index: 631 entries, 0 to 630
Data columns (total 8 columns):
patient      631  non-null values
obs          631  non-null values
week         631  non-null values
site         631  non-null values
id           631  non-null values
age          631  non-null values
twstrs       631  non-null values
treatment    631  non-null values
dtypes: int64(8),
 dtype('O'): <class 'pandas.core.frame.DataFrame'>
Int64Index: 631 entries, 0 to 630
Data columns (total 2 columns):
treat    631  non-null values
sex      631  non-null values
dtypes: object(2)}

Также можно группировать данные на основе одного или нескольких уровнях иерархического индекса, к примеру:

In [89]:
cdystonia2.head(10)
Out[89]:
             week  site  id   treat  age sex  twstrs
patient obs                                         
1       1       0     1   1   5000U   65   F      32
        2       2     1   1   5000U   65   F      30
        3       4     1   1   5000U   65   F      24
        4       8     1   1   5000U   65   F      37
        5      12     1   1   5000U   65   F      39
        6      16     1   1   5000U   65   F      36
2       1       0     1   2  10000U   70   F      60
        2       2     1   2  10000U   70   F      26
        3       4     1   2  10000U   70   F      27
        4       8     1   2  10000U   70   F      41
In [90]:
cdystonia2.groupby(level='obs', axis=0)['twstrs'].mean()
Out[90]:
obs
1      45.651376
2      37.611650
3      37.066038
4      39.807692
5      42.913462
6      45.628571
Name: twstrs, dtype: float64

Apply

Зададим уникальный индекс, складывая все строки одного класса, используя groupbyt() и объединим их в одном массиве. В примере мы берём массив и название столбца, сортируем по столбцу и выбираем N наибольших значений. Мы также можем использовать apply() чтобы выерунть наибольшие значения для каждой группы в массиве:

In [91]:
def top(df, column, n=5):
    return df.sort_index(by=column, ascending=False)[:n]

Воспользуемся набором данных кораблей, который мы смерджили до этого. Допустим, нам нужно 3 самых длинных участка пути, по которым проходил каждый корабль.

In [92]:
top3segments = segments_merged.groupby('mmsi').apply(top, column='seg_length', n=3)[['names', 'seg_length']]
top3segments
Out[92]:
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 29464 entries, (1, 6) to (999999999, 262525)
Data columns (total 2 columns):
names         29464  non-null values
seg_length    29464  non-null values
dtypes: float64(1), object(1)

Дополнительный аргумент применённой функции может быть передам в apply() после названия функции. Подразумевается, что массив данных всегда идёт первым.

In [93]:
top3segments.head(20)
Out[93]:
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 20 entries, (1, 6) to (3011, 77)
Data columns (total 2 columns):
names         20  non-null values
seg_length    20  non-null values
dtypes: float64(1), object(1)

Вспомним набор данных из примера с конкатенацией. Предположим, что мы хотим собрать данные для более высокого уроня таксономической единицы. К примеру, мы можем определять объекты основываясь на биологическом классе.

In [94]:
mb1.index[:3]
Out[94]:
Index([u'Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Desulfurococcaceae Ignisphaera', u'Archaea "Crenarchaeota" Thermoprotei Desulfurococcales Pyrodictiaceae Pyrolobus', u'Archaea "Crenarchaeota" Thermoprotei Sulfolobales Sulfolobaceae Stygiolobus'], dtype=object)

Используя строковые методы split() и join() мы задаём индекс, который учитывает царство, Phylum и класс.

In [95]:
class_index = mb1.index.map(lambda x: ' '.join(x.split(' ')[:3]))
mb_class = mb1.copy()
mb_class.index = class_index

Поскольку в нашем наборе есть разные животные, этот индекс больше не будет уникальным.

In [96]:
mb_class.head()
Out[96]:
                                           Count
Archaea "Crenarchaeota" Thermoprotei           7
Archaea "Crenarchaeota" Thermoprotei           2
Archaea "Crenarchaeota" Thermoprotei           3
Archaea "Crenarchaeota" Thermoprotei           3
Archaea "Euryarchaeota" "Methanomicrobia"      7

Мы можем восстновить исходный уникальный индекс используя groupby()

In [97]:
mb_class.groupby(level=0).sum().head(10)
Out[97]:
                                           Count
Archaea "Crenarchaeota" Thermoprotei          15
Archaea "Euryarchaeota" "Methanomicrobia"      9
Archaea "Euryarchaeota" Archaeoglobi           2
Archaea "Euryarchaeota" Halobacteria          12
Archaea "Euryarchaeota" Methanococci           1
Archaea "Euryarchaeota" Methanopyri           12
Archaea "Euryarchaeota" Thermoplasmata         2
Bacteria "Actinobacteria" Actinobacteria    1740
Bacteria "Aquificae" Aquificae                11
Bacteria "Bacteroidetes" "Bacteroidia"         1