"Длинный" и "широкий" форматы данных
Данные являются длинными когда для каждого признака выделяется свой, отдельный столбец. Представим что у нас есть некоторое множество студентов, изучающих три языка программирования, причем студенты разбиты на 5 групп и у каждой группы есть свой рейтинг по каждому языку. Мы можем без труда представить эти данные в "длинной" форме:
# придумываем данные:
group = ['F' + str(i//3) for i in range(3, 18)]
course = ['Pyton', 'C++', 'Java']*5
rating = np.random.randint(10, 50, size=15)
# создаем из этих данных датафрейм:
df_long = pd.DataFrame({'Group': group,
'Course': course,
'Rating': rating})
df_long
Group | Course | Rating | |
---|---|---|---|
0 | F1 | Pyton | 32 |
1 | F1 | C++ | 15 |
2 | F1 | Java | 39 |
3 | F2 | Pyton | 12 |
4 | F2 | C++ | 15 |
5 | F2 | Java | 21 |
6 | F3 | Pyton | 43 |
7 | F3 | C++ | 14 |
8 | F3 | Java | 36 |
9 | F4 | Pyton | 18 |
10 | F4 | C++ | 37 |
11 | F4 | Java | 32 |
12 | F5 | Pyton | 44 |
13 | F5 | C++ | 10 |
14 | F5 | Java | 48 |
В длинной форме все признаки (название группы, язык программирования и рейтинг) сгруппированы по столбцам, а строки представляют собой отдельные "наблюдения" - объекты с указанным набором признаков. Очень часто, именно в таком формате данные "скармливаются" алгоритмам машинного обучения. Очень часто такой формат данных используется для хранения временных рядов. Но есть еще и "широкий" формат данных:
df_wide = df_long.pivot(index='Group',
columns='Course',
values='Rating')
df_wide
Course | C++ | Java | Pyton |
---|---|---|---|
Group | |||
F1 | 15 | 39 | 32 |
F2 | 15 | 21 | 12 |
F3 | 14 | 36 | 43 |
F4 | 37 | 32 | 18 |
F5 | 10 | 48 | 44 |
В "широком" формате данные выглядят более компактными, так как строки представляют собой признаки, а отдельные наблюдения хранятся в ячейках. Такие данные легче анализировать, а воспринимаются они еще легче, если их представить в виде тепловой карты:
import seaborn as sns
sns.set()
sns.heatmap(df_wide, annot=True);
Как вы заметили, метод pivot()
устроен очень просто, первые два аргумента определяют значения каких столбцов станут индексами и заголовками столбцов, а последний задает значения столбца которые будут помещены в результирующую, "широкую" таблицу. Давайте придумаем еще один пример, пускай у нас есть вот такой датафрейм:
# Выдумываем данные:
date_idx = pd.date_range(start='2020-01-01', end='2020-12-31')
date_idx
feature_1 = ['Red', 'Green', 'Blue']*122
feature_2 = list('ABCDEF')*61
value_1 = np.cumsum(np.random.randn(366))
# Создаем датафрейм, с "типа реальными" данными:
df = pd.DataFrame({'Date': date_idx,
'Feature_1': feature_1,
'Feature_2': feature_2,
'Value_1': value_1})
df
Date | Feature_1 | Feature_2 | Value_1 | |
---|---|---|---|---|
0 | 2020-01-01 | Red | A | 1.280320 |
1 | 2020-01-02 | Green | B | 2.309967 |
2 | 2020-01-03 | Blue | C | 3.446230 |
3 | 2020-01-04 | Red | D | 2.758629 |
4 | 2020-01-05 | Green | E | 2.558029 |
... | ... | ... | ... | ... |
361 | 2020-12-27 | Green | B | 14.641240 |
362 | 2020-12-28 | Blue | C | 15.521487 |
363 | 2020-12-29 | Red | D | 13.846629 |
364 | 2020-12-30 | Green | E | 14.492868 |
365 | 2020-12-31 | Blue | F | 15.604031 |
Этот датафрейм очень похож на предыдущий, и кажется что мы можем повернуть эти данные в широкий формат. Но ничего не получится:
df.pivot(index='Feature_1',
columns='Feature_2',
values='Value_1')
ValueError: Index contains duplicate entries, cannot reshape
Дело в том, что индекс результирующего датафрейма не должен содержать повторяющихся значений, а из этого следует, что пары значений из столбцов Feature_1 и Feature_2 должны разбиваться на непересекающиеся подмножества. Заметьте, что для датафрейма из предыдущего примера это условие выполняется. Данный пример пригодился бы гораздо больше для демонстрации приемов агрегации данных.
Что же делать если все-таки очень хочется повернуть такие неповорачиваемые данные. Обратите внимание что датафрейм df
содержит информацию о дате. Очень часто вам придется работать с временными рядами, которые помимо количественных данных ('Value_1') будут содержать еще и категориальные данные ('Feature_1', 'Feature_2'). Однако (!) дата тоже может быть воспринята как комбинация категориальных признаков: признак-год, признак-месяц, признак-день месяца и т.д.
С помощью аксессора dt
мы можем получить доступ к месяцу и дню месяца любой даты, а значит можем поместить эти данные в наш датафрейм:
df['Day'] = df['Date'].dt.day
df['Month'] = df['Date'].dt.month
df
Date | Feature_1 | Feature_2 | Value_1 | Day | Month | |
---|---|---|---|---|---|---|
0 | 2020-01-01 | Red | A | 1.280320 | 1 | 1 |
1 | 2020-01-02 | Green | B | 2.309967 | 2 | 1 |
2 | 2020-01-03 | Blue | C | 3.446230 | 3 | 1 |
3 | 2020-01-04 | Red | D | 2.758629 | 4 | 1 |
4 | 2020-01-05 | Green | E | 2.558029 | 5 | 1 |
... | ... | ... | ... | ... | ... | ... |
361 | 2020-12-27 | Green | B | 14.641240 | 27 | 12 |
362 | 2020-12-28 | Blue | C | 15.521487 | 28 | 12 |
363 | 2020-12-29 | Red | D | 13.846629 | 29 | 12 |
364 | 2020-12-30 | Green | E | 14.492868 | 30 | 12 |
365 | 2020-12-31 | Blue | F | 15.604031 | 31 | 12 |
Что дальше? А дальше можно заметить, что значения этих двух новых столбцов образуют пары месяц-день, которые могут быть разбиты на непересекающиеся подмножества, следовательно, они могут быть повернуты:
df.pivot(index='Day',
columns='Month',
values='Value_1')
Month | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Day | ||||||||||||
1 | 1.280320 | 4.499663 | 4.995611 | 7.809455 | 2.789509 | 3.701714 | 12.796463 | 12.607722 | 22.540160 | 17.215560 | 15.543285 | 15.692926 |
2 | 2.309967 | 4.849108 | 4.893813 | 8.584060 | 3.064709 | 4.062598 | 13.345212 | 12.315211 | 21.292353 | 16.927769 | 16.711957 | 16.248795 |
3 | 3.446230 | 5.446209 | 6.459221 | 8.852954 | 2.652694 | 4.559032 | 13.491173 | 13.515112 | 21.302111 | 16.477393 | 16.935500 | 17.651427 |
4 | 2.758629 | 4.943358 | 6.482055 | 8.284182 | 1.233104 | 5.836615 | 14.024985 | 14.712774 | 20.290859 | 17.467914 | 16.031338 | 20.475901 |
5 | 2.558029 | 4.219435 | 5.910340 | 8.225869 | 0.783635 | 6.089833 | 15.161028 | 15.805101 | 19.335010 | 18.581113 | 15.757614 | 21.179886 |
6 | 2.441635 | 4.125523 | 6.345021 | 8.180521 | 0.824530 | 6.787122 | 13.925873 | 15.290465 | 18.279796 | 18.037706 | 15.475966 | 20.984207 |
7 | 3.832410 | 4.186335 | 7.486341 | 7.361676 | 0.983088 | 6.723896 | 14.906861 | 17.318725 | 18.241858 | 17.858568 | 14.692343 | 20.455460 |
8 | 4.846519 | 4.983882 | 6.355167 | 6.273304 | 0.066668 | 7.028689 | 14.779322 | 18.048511 | 17.421804 | 17.226697 | 14.535815 | 20.730132 |
9 | 3.457392 | 4.938479 | 5.241748 | 6.236852 | 0.164098 | 6.670649 | 16.839607 | 16.565633 | 16.781808 | 16.711535 | 14.797774 | 20.459460 |
10 | 3.513267 | 6.309816 | 4.714442 | 6.189914 | 0.168759 | 8.384687 | 16.071680 | 17.792522 | 17.501697 | 16.703412 | 13.618565 | 20.451517 |
11 | 4.041146 | 6.652065 | 4.307849 | 6.512205 | -0.034089 | 10.080500 | 14.828499 | 19.012577 | 17.069420 | 15.122851 | 13.110402 | 19.134122 |
12 | 4.205785 | 6.306073 | 3.439218 | 7.972237 | 0.071666 | 10.445729 | 15.768343 | 19.812321 | 18.043636 | 15.632000 | 12.666769 | 19.604051 |
13 | 4.116227 | 9.051883 | 3.755990 | 6.819247 | 1.403657 | 11.136731 | 14.703597 | 19.836070 | 16.982851 | 16.769261 | 13.475709 | 21.417337 |
14 | 2.982602 | 9.041637 | 4.660545 | 6.606446 | 1.881773 | 10.995132 | 15.259424 | 21.697963 | 15.986117 | 15.814248 | 15.048174 | 21.507058 |
15 | 2.557564 | 10.092654 | 5.220842 | 5.728087 | 1.921805 | 10.857222 | 15.137296 | 20.833738 | 15.612040 | 15.604509 | 14.690829 | 22.034822 |
16 | 3.267154 | 8.962249 | 5.800329 | 5.700436 | 0.793541 | 10.874454 | 14.805577 | 19.476960 | 16.366093 | 14.460775 | 14.368752 | 21.738550 |
17 | 3.851380 | 8.568515 | 7.907889 | 4.980183 | 0.621525 | 12.641269 | 14.177881 | 21.542676 | 16.950196 | 13.361580 | 12.857784 | 23.826108 |
18 | 3.939723 | 7.480232 | 7.387798 | 3.731267 | -0.023413 | 13.230486 | 13.886881 | 20.373860 | 18.098437 | 13.785337 | 12.307476 | 23.044278 |
19 | 5.390438 | 5.461422 | 8.331284 | 2.411781 | 0.108509 | 11.803238 | 14.632978 | 20.468362 | 18.314649 | 13.929535 | 13.237107 | 22.460037 |
20 | 6.297791 | 3.792500 | 8.038598 | 4.017413 | 0.818674 | 10.539023 | 14.437609 | 19.434992 | 17.895655 | 13.435562 | 13.220168 | 20.868031 |
21 | 6.485818 | 5.203672 | 8.969631 | 2.851895 | 1.708978 | 10.294971 | 15.235624 | 20.189890 | 17.217551 | 13.463076 | 15.112032 | 21.062359 |
22 | 6.515786 | 4.089452 | 9.195306 | 5.159096 | 0.924272 | 9.050102 | 15.486413 | 21.120262 | 16.792765 | 13.931651 | 14.382543 | 21.120787 |
23 | 5.672433 | 2.722785 | 9.416622 | 3.974338 | 2.231564 | 9.231848 | 15.907123 | 20.948861 | 15.553665 | 12.698836 | 16.060535 | 22.222557 |
24 | 5.056586 | 1.997972 | 9.052705 | 4.422946 | 0.997579 | 10.271095 | 14.394013 | 21.079046 | 16.174482 | 11.580446 | 14.715038 | 19.935368 |
25 | 4.589747 | 2.045224 | 7.311904 | 4.290826 | 0.365493 | 10.103482 | 14.060862 | 21.999951 | 17.062645 | 12.208017 | 15.254084 | 17.865460 |
26 | 6.012402 | 1.508136 | 7.176945 | 3.072318 | -1.324701 | 10.241017 | 12.061628 | 22.488478 | 16.620308 | 11.387496 | 14.820201 | 15.460649 |
27 | 5.182279 | 2.458931 | 8.803566 | 4.560745 | 1.420925 | 11.165881 | 12.567365 | 20.858919 | 17.391087 | 10.704322 | 16.334217 | 14.641240 |
28 | 3.549366 | 2.470116 | 8.259062 | 5.001389 | 2.781761 | 10.341632 | 12.044444 | 21.634459 | 17.461757 | 12.390838 | 16.964727 | 15.521487 |
29 | 2.031239 | 4.547036 | 10.108951 | 5.547591 | 2.956593 | 11.649154 | 12.342516 | 20.704986 | 16.925448 | 14.421016 | 16.960148 | 13.846629 |
30 | 2.710428 | NaN | 8.629203 | 5.157218 | 3.772812 | 12.784799 | 11.846687 | 20.979176 | 17.060442 | 14.660711 | 16.679309 | 14.492868 |
31 | 5.153429 | NaN | 9.111987 | NaN | 3.316355 | NaN | 11.112553 | 21.422006 | NaN | 14.576556 | NaN | 15.604031 |
Благодаря тепловой карте обратить внимание на самые интересные места новой таблицы гораздо проще:
sns.heatmap(df.pivot(index='Day',
columns='Month',
values='Value_1'));
Далее, мы можем повернуть эти данные в широкий формат для каждого категориального значения из столбцов Feature_1 и Feature_2, например, вот так:
# Вытаскиваем все строки в которых
# 'Feature_1' имеет значение 'Red':
df_reds = df[df['Feature_1'] == 'Red']
df_reds
Date | Feature_1 | Feature_2 | Value_1 | Day | Month | |
---|---|---|---|---|---|---|
0 | 2020-01-01 | Red | A | 0.919593 | 1 | 1 |
3 | 2020-01-04 | Red | D | -0.074999 | 4 | 1 |
6 | 2020-01-07 | Red | A | 4.355910 | 7 | 1 |
9 | 2020-01-10 | Red | D | 5.437354 | 10 | 1 |
12 | 2020-01-13 | Red | A | 2.504805 | 13 | 1 |
... | ... | ... | ... | ... | ... | ... |
351 | 2020-12-17 | Red | D | -15.226387 | 17 | 12 |
354 | 2020-12-20 | Red | A | -15.711254 | 20 | 12 |
357 | 2020-12-23 | Red | D | -16.550948 | 23 | 12 |
360 | 2020-12-26 | Red | A | -18.844241 | 26 | 12 |
363 | 2020-12-29 | Red | D | -18.495055 | 29 | 12 |
# смотрим на уникальные значения в
# 'Feature_2':
pd.unique(df_reds['Feature_2'])
array(['A', 'D'], dtype=object)
import matplotlib.pyplot as plt
# строим график:
fig = plt.figure()
ax_1 = fig.add_subplot(1, 2, 1)
ax_2 = fig.add_subplot(1, 2, 2)
df_red_a = df_reds[df_reds['Feature_2'] == 'A'].pivot('Day', 'Month', 'Value_1')
sns.heatmap(df_red_a, ax=ax_1)
df_red_d = df_reds[df_reds['Feature_2'] == 'D'].pivot('Day', 'Month', 'Value_1')
sns.heatmap(df_red_d, ax=ax_2)
ax_1.set(title = 'Feature_2 = "A"')
ax_2.set(title = 'Feature_2 = "D"')
fig.suptitle('Feature_1 = "Red"')
fig.set_figwidth(14)
fig.set_figheight(6)
Серые участки на тепловых картах соответствуют NaN-ам, т.е. если при повороте в широкий формат каких-то значений нет, то это не приводит к ошибке.
Кстати, последний аргумент - values
в методе pivot()
может быть опущен, в этом случае мы можем получить "широкие" данные с иерархической индексацией. Давайте вернемся к нашему датафрейму со студентами:
df_long
Group | Course | Rating | |
---|---|---|---|
0 | F1 | Pyton | 37 |
1 | F1 | C++ | 26 |
2 | F1 | Java | 44 |
3 | F2 | Pyton | 20 |
4 | F2 | C++ | 48 |
5 | F2 | Java | 49 |
6 | F3 | Pyton | 13 |
7 | F3 | C++ | 48 |
8 | F3 | Java | 16 |
9 | F4 | Pyton | 18 |
10 | F4 | C++ | 11 |
11 | F4 | Java | 37 |
12 | F5 | Pyton | 32 |
13 | F5 | C++ | 16 |
14 | F5 | Java | 49 |
И добавим к нему несколько столбцов:
df_long['missing_classes '] = np.random.randint(0, 5, 15)
df_long['Progress'] = np.random.randint(70, 100, 15)
df_long.head()
Group | Course | Rating | missing_classes | Progress | |
---|---|---|---|---|---|
0 | F1 | Pyton | 37 | 4 | 99 |
1 | F1 | C++ | 26 | 0 | 76 |
2 | F1 | Java | 44 | 1 | 89 |
3 | F2 | Pyton | 20 | 2 | 88 |
4 | F2 | C++ | 48 | 3 | 78 |
А теперь повернем этот датафрейм без аргумента values
:
df_long.pivot(index='Group', columns='Course')
Rating | missing_classes | Progress | |||||||
---|---|---|---|---|---|---|---|---|---|
Course | C++ | Java | Pyton | C++ | Java | Pyton | C++ | Java | Pyton |
Group | |||||||||
F1 | 26 | 44 | 37 | 0 | 1 | 4 | 76 | 89 | 99 |
F2 | 48 | 49 | 20 | 3 | 4 | 2 | 78 | 78 | 88 |
F3 | 48 | 16 | 13 | 4 | 0 | 3 | 76 | 77 | 82 |
F4 | 11 | 37 | 18 | 0 | 2 | 2 | 92 | 78 | 84 |
F5 | 16 | 49 | 32 | 2 | 2 | 3 | 80 | 78 | 86 |
В итоге получился датафрейм с иерархической индексацией. Аргумент values
так же может принимать списки с перечисленными именами столбцов:
df_long.pivot(index='Group',
columns='Course',
values=['Rating', 'Progress'])
Rating | progress | |||||
---|---|---|---|---|---|---|
Course | C++ | Java | Pyton | C++ | Java | Pyton |
Group | ||||||
F1 | 26 | 44 | 37 | 76 | 89 | 99 |
F2 | 48 | 49 | 20 | 78 | 78 | 88 |
F3 | 48 | 16 | 13 | 76 | 77 | 82 |
F4 | 11 | 37 | 18 | 92 | 78 | 84 |
F5 | 16 | 49 | 32 | 80 | 78 | 86 |
Более того, параметры index
и columns
так же могут принимать списки с именами столбцов, которые будут преобразованы к иерархической индексации строк (или столбцов).
df_long['Gender'] = ['Male']*7 + ['Female']*8
df_long
Group | Course | Rating | missing_classes | progress | Gender | |
---|---|---|---|---|---|---|
0 | F1 | Pyton | 37 | 4 | 99 | Male |
1 | F1 | C++ | 26 | 0 | 76 | Male |
2 | F1 | Java | 44 | 1 | 89 | Male |
3 | F2 | Pyton | 20 | 2 | 88 | Male |
4 | F2 | C++ | 48 | 3 | 78 | Male |
5 | F2 | Java | 49 | 4 | 78 | Male |
6 | F3 | Pyton | 13 | 3 | 82 | Male |
7 | F3 | C++ | 48 | 4 | 76 | Female |
8 | F3 | Java | 16 | 0 | 77 | Female |
9 | F4 | Pyton | 18 | 2 | 84 | Female |
10 | F4 | C++ | 11 | 0 | 92 | Female |
11 | F4 | Java | 37 | 2 | 78 | Female |
12 | F5 | Pyton | 32 | 3 | 86 | Female |
13 | F5 | C++ | 16 | 2 | 80 | Female |
14 | F5 | Java | 49 | 2 | 78 | Female |
df_long.pivot(index=['Group', 'Gender'],
columns='Course',
values=['Rating', 'progress'])
Rating | progress | ||||||
---|---|---|---|---|---|---|---|
Course | C++ | Java | Pyton | C++ | Java | Pyton | |
Group | Gender | ||||||
F1 | Male | 26.0 | 44.0 | 37.0 | 76.0 | 89.0 | 99.0 |
F2 | Male | 48.0 | 49.0 | 20.0 | 78.0 | 78.0 | 88.0 |
F3 | Female | 48.0 | 16.0 | NaN | 76.0 | 77.0 | NaN |
Male | NaN | NaN | 13.0 | NaN | NaN | 82.0 | |
F4 | Female | 11.0 | 37.0 | 18.0 | 92.0 | 78.0 | 84.0 |
F5 | Female | 16.0 | 49.0 | 32.0 | 80.0 | 78.0 | 86.0 |