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





