Matplotlib и Seaborn — вполне приличные Python-библиотеки для создания превосходных графиков. Но такие графики получаются статичными, и крайне трудно подобрать для них красивое представление данных или отследить динамику изменений. Вам бы понравилось, если бы в своей следующей презентации/видео/посте в соцсетях вы бы смогли показать динамику изменений в виде короткого видеоролика? И даже больше: такие графики можно было бы создать в Matplotlib, Seaborn или любой другой библиотеке!
Не так давно я создал несколько динамических графиков для короткой документалки об опиоидном кризисе в США, так что данные для статьи будут браться оттуда. Эта информация размещена в открытом доступе на сайтах Национального института по изучению злоупотребления наркотиками и Центра по контролю и профилактике заболеваний. Скачать можно по ссылке: https://www.drugabuse.gov/sites/default/files/overdose_data_1999-2015.xls.
Строить графики я буду в Matplotlib и Seaborn, а для обработки данных воспользуюсь Numpy и Pandas. Matplotlib предлагает несколько функций для анимации. Так что давайте начнем и импортируем все зависимости.
import numpy as np import pandas as pd import seaborn as sns import matplotlib import matplotlib.pyplot as plt import matplotlib.animation as animation
Теперь для подготовки к созданию анимации нужно загрузить данные и добавить их в Pandas DataFrame. При создании нескольких графиков, о передозировке различными опиатами, лучше написать отдельную функцию, которая будет подгружать данные из нужной строки.
overdoses = pd.read_excel('overdose_data_1999-2015.xls',sheetname='Online',skiprows =6) def get_data(table,rownum,title): data = pd.DataFrame(table.loc[rownum][2:]).astype(float) data.columns = {title} return data
Давайте приступим к делу и перейдем к созданию анимации!
Во-первых, если вы, как и я, пользуетесь Jupiter Notebook, то начните ячейку с %matplotlib notebook
— так вы увидите анимацию сразу, а не только после сохранения.
Лично я извлекал статистику по передозировке героином из таблицы с помощью функции get_data
, а затем переносил данные в Pandas DataFrame в две колонки. Первая — год, вторая — количество передозировок.
%matplotlib notebook title = 'Heroin Overdoses' d = get_data(overdoses,18,title) x = np.array(d.index) y = np.array(d['Heroin Overdoses']) overdose = pd.DataFrame(y,x) #XN,YN = augment(x,y,10) #augmented = pd.DataFrame(YN,XN) overdose.columns = {title}
Далее инициируемwriter
, который использует ffmpeg и пишет 20 кадров в секунду с битрейтом 1800. Конечно же, эти значения вы можете настроить сами.
Writer = animation.writers['ffmpeg'] writer = Writer(fps=20, metadata=dict(artist='Me'), bitrate=1800)
Теперь создадим график с обозначениями. Обязательно задавайте пределы для осей Х и У — так анимация не будет «скакать» по диапазону отображаемых данных.
fig = plt.figure(figsize=(10,6)) plt.xlim(1999, 2016) plt.ylim(np.min(overdose)[0], np.max(overdose)[0]) plt.xlabel('Year',fontsize=20) plt.ylabel(title,fontsize=20) plt.title('Heroin Overdoses per Year',fontsize=20)
Основной компонент графика — функция анимации, которой вы задаете, что должно происходить в каждом кадре видео. Здесь i
— индекс кадра анимации. Данным индексом вы выделяете диапазон данных, которые должны показываться в кадре. Затем выстраиваете эти данные в seaborn lineplot. Две последние строчки нужны для красоты.
def animate(i): data = overdose.iloc[:int(i+1)] #select data range p = sns.lineplot(x=data.index, y=data[title], data=data, color="r") p.tick_params(labelsize=17) plt.setp(p.lines,linewidth=7)
Запуск анимации делается через matplotlib.animation.FuncAnimation
— там вы привязываете функцию анимации и задаете общее количество кадров. frames
определяет частоту, с которой будет вызыватьсяanimate(i)
.
ani = matplotlib.animation.FuncAnimation(fig, animate, frames=17, repeat=True)
Для сохранения анимации в mp4 смело вызывайте ani.save()
. Если вам нужен предпросмотр перед сохранением, то лучше воспользуйтесь plt.show()
.
ani.save('HeroinOverdosesJumpy.mp4', writer=writer)
Теперь анимация выглядит вот так:
Функция заработала, но переходы между данными слишком резкие. Для сглаживания прыгающих значений необходимо добавить несколько дополнительных точек между уже существующими. Для этого воспользуемся другой функцией, которую я назвал augment
.
def augment(xold,yold,numsteps): xnew = [] ynew = [] for i in range(len(xold)-1): difX = xold[i+1]-xold[i] stepsX = difX/numsteps difY = yold[i+1]-yold[i] stepsY = difY/numsteps for s in range(numsteps): xnew = np.append(xnew,xold[i]+s*stepsX) ynew = np.append(ynew,yold[i]+s*stepsY) return xnew,ynew
Теперь осталось применить эту функцию к данным и увеличить количество кадров в matplotlib.animation.FuncAnimation
. Здесь я задаю augment с numsteps=10
. Это означает, что я увеличиваю набор данных до 160 точек и указываю frames=160
. Результат получается более сглаженным, однако на графике до сих пор присутствует несколько острых пиков.
Чтобы убрать эту «остроту» можно добавить сглаживающую функцию отсюда: https://www.swharden.com/wp/2008-11-17-linear-data-smoothing-in-python/
def smoothListGaussian(listin,strippedXs=False,degree=5): window=degree*2-1 weight=np.array([1.0]*window) weightGauss=[] for i in range(window): i=i-degree+1 frac=i/float(window) gauss=1/(np.exp((4*(frac))**2)) weightGauss.append(gauss) weight=np.array(weightGauss)*weight smoothed=[0.0]*(len(listin)-window) for i in range(len(smoothed)): smoothed[i]=sum(np.array(listin[i:i+window])*weight)/sum(weight) return smoothed
Еще можно добавить параметры цвета и стиля, чтобы сделать график более оригинальным.
sns.set(rc={'axes.facecolor':'lightgrey', 'figure.facecolor':'lightgrey','figure.edgecolor':'black','axes.grid':False})
Так мы получаем окончательный вариант, показанный выше.
В данной статье мы разобрали функцию анимации в matplotlib на одном примере. Конечно же, с ее помощью можно анимировать любые типы графиков. Просто настройте параметры и тип графика в функции animate()
и вы получите бескрайние возможности!
Перевод статьи Viviane: How to Create Animated Graphs in Python