Трохи про кіно або як робити інтерактивні візуалізації в python


Введення
У цій замітці я хочу розповісти про те, як можна досить легко будувати інтерактивні графіки в Jupyter Notebook'e з допомогою бібліотеки
plotly
. Більше того, для їх побудови не потрібно піднімати свій сервер і писати код на javascript. Ще один великий плюс пропонованого підходу — візуалізації будуть працювати і в NBViewer'e, тобто можна буде легко поділитися своїми результатами з колегами. Ось, наприклад, мій код для цієї замітки.
Для прикладів я взяла скачали в квітні дані про фільми (рік випуску, оцінки на Кинопоиск і IMDb, жанри тощо). Я вивантажила дані по всім фільмам, в яких було хоча б 100 оцінок — всього 36417 фільмів. Про те, як завантажити і розпарсити дані Кинопоиск, я розповідала в попередньому пості.

Візуалізація в python і plotly
В python є багато бібліотек для візуалізації:
matplotlib
,
seaborn
, портований з R
ggplot
та інші (докладніше про інструменти можна почитати тут або тут). Є серед них і ті, які дозволяють будувати інтерактивні графіки, наприклад,
bokeh
,
pygal
та
plotly
, про який власне і піде мова.
Plotly
позицинируется як онлайн-платформа, де можна створювати та публікувати свої графіки. Однак, цю бібліотеку можна використовувати і просто в
Jupyter Notebook'e
. До того ж у бібліотеки є offline-mode, який дозволяє використовувати її без реєстрації і публікації даних і графіків на сервер
plotly
(документація).
В цілому, мені бібліотека дуже сподобалася: є докладна документація з прикладами, підтримані різні типи графіків (scatter plots, box plots, 3D графіки, bar charts, heatmaps, дендрограми і т. д.) і графіки виходять досить симпатичними.
Приклади
Тепер настав час перейти безпосередньо до прикладів. Як я вже говорила вище, весь код і інтерактивні графіки доступні в NBViewer'e.
Бібліотеки легко встановити за допомогою команди:
pip install plotly
.
Перш за все, необхідно зробити import'и, викликати команду
init_notebook_mode
для ініціалізації
plot.ly
завантажити
pandas.DataFrame
дані, з якими будемо працювати.
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly.graph_objs as go

init_notebook_mode(connected=True)

df = pd.read_csv('kp_all_movies.csv') #завантажуємо підготовлені дані
df.head()


Скільки фільмів виходило в різні роки?
Для початку побудуємо простий bar chart, що показує розподіл фільмів по року випуску.
count_year_df = df.groupby('movie_year', as_index = False).movie_id.count()

trace = go.Bar(
x = count_year_df.movie_year,
y = count_year_df.movie_id
)
layout = go.Layout(
title='Фільми на Кинопоиск',
)

fig = go.Figure(data = [trace], layout = layout)
iplot(fig)

В результаті отримаємо інтерактивний графік, який показує значення при наведенні на рік і досить очікуваний висновок про те, що з роками фільмів стало більше.

Сталі чи з роками знімати більш гарне кіно?
Для відповіді на це питання побудуємо графік залежності середньої оцінки на Кинопоиск і IMDb від року випуску.
rating_year_df = df.groupby('movie_year', as_index = False)[['kp_rating', 'imdb_rating']].mean()

trace_kp = go.Scatter(
x = rating_year_df.movie_year,
y = rating_year_df.kp_rating,
mode = 'lines',
name = u 'КиноПоиск'
)
trace_imdb = go.Scatter(
x = rating_year_df.movie_year,
y = rating_year_df.imdb_rating,
mode = 'lines',
name = 'IMDb'
)

layout = go.Layout(
title='Оцінки фільмів',
) 

fig = go.Figure(data = [trace_kp, trace_imdb], layout = layout)
iplot(fig)

На оцінках за КиноПоиску та IMDb видно тренд на зниження середньої оцінки залежно від року випуску. Але, насправді, цього не можна зробити однозначний висновок про те, що раніше знімали більш якісні фільми. Справа в тому, що якщо вже люди дивляться старі фільми і оцінюють їх на Кинопоиск, то вибирають культове кіно з свідомо більш високими оцінками (думаю, мало хто дивиться прохідні фільми, що вийшли в 1940го році, принаймні, я не дивлюся).

Є відмінності в оцінках залежно від жанру фільму?
Для порівняння оцінок в залежності від жанру побудуємо box plot. Варто пам'ятати, що кожен фільм може належати до кількох жанрів, тому фільми будуть враховуватися в декількох групах.
Код
# насамперед распарсим поле genres і data frame з розмноженими рядками для кожного жанру
def parse_list(lst_str):
return filter(lambda y: y != ", 
map(lambda x: x.strip(), 
re.sub(r'[\[\]]', ", lst_str).split(',')))

df['genres'] = df['genres'].fillna('[]')
genres_data = []
for record in df.to_dict(orient = 'records'):
genres_lst = parse_list(record['genres'])
for genre in genres_lst:
copy = record.copy()
copy['genre'] = genre
copy['weight'] = 1./len(genres_lst)
genres_data.append(copy)

genres_df = pd.DataFrame.from_dict(genres_data)

# сформуємо топ-10 жанрів
top_genres = genres_df.groupby('genre')[['movie_id']].count()\
.sort_values('movie_id', ascending = False)\
.head(10).index.values.tolist()

N = float(len(top_genres))

# згенеруємо кольору для візуалізації
c = ['hsl('+str(h)+',50%'+',50%)' for h in np.linspace(0, 360, N)]

data = [{
'y': genres_df[genres_df.genre == top_genres[i]].kp_rating, 
'type':'box',
'marker':{'color': c[i]},
'name': top_genres[i]
} for i in range(len(top_genres))]

layout = go.Layout(
title='Оцінки фільмів',
yaxis = {'title': 'Оцінка Кинопоиск'}
) 

fig = go.Figure(data = data, layout = layout)
iplot(fig)

По графіку видно, що найбільше виділяються низькими оцінками фільми-жахи

Які жанри найчастіше сусідять?
Як я говорила вище, один фільм найчастіше відноситься до кількох жанрів відразу. Для того, щоб подивитися на те, які жанри частіше зустрічаються разом, побудуємо heatmap.
Код
genres_coincidents = {}

for item in df.genres:
parsed_genres = parse_list(item)
for genre1 in parsed_genres:
if genre1 not in genres_coincidents:
genres_coincidents[genre1] = defaultdict(int)
for genre2 in parsed_genres:
genres_coincidents[genre1][genre2] += 1

genres_coincidents_df = pd.DataFrame.from_dict(genres_coincidents).fillna(0)

# отнормируем таблицю на кількість фільмів кожного жанру
genres_coincidents_df_norm = genres_coincidents_df\
.apply(lambda x: x/genres_df.groupby('genre').movie_id.count(), axis = 1)

heatmap = go.Heatmap(
z = genres_coincidents_df_norm.values,
x = genres_coincidents_df_norm.index.values,
y = genres_coincidents_df_norm.columns
)
layout = go.Layout(
title = 'Пов'язані жанри'
)

fig = go.Figure(data = [heatmap], layout = layout)
iplot(fig)

Читати графік потрібно наступним чином: 74,7% історичних фільмів також мають тег драма.

Як змінювалися оцінки фільмів в залежності від жанру?
Повернемося ще раз до прикладу, в якому ми дивилися на залежність середньої оцінки від року випуску і побудуємо такі графіки для різних жанрів. Паралельно познайомимося з ще однією фішкою
plotly
: можна сконфігурувати drop-down меню і змінювати графік в залежності від обраної опції.
Код
genre_rating_year_df = genres_df.groupby(['movie_year', 'genre'], as_index = False)[['kp_rating', 'imdb_rating']].mean()

N = len(top_genres)

data = []
drop_menus = []

# конструюємо все, що нас цікавлять лінії
for i in range(N):
genre = top_genres[i]
genre_df = genre_rating_year_df[genre_rating_year_df.genre == genre]

trace_kp = go.Scatter(
x = genre_df.movie_year,
y = genre_df.kp_rating,
mode = 'lines',
name = genre + 'Керівник',
visible = (i == 0)
)
trace_imdb = go.Scatter(
x = genre_df.movie_year,
y = genre_df.imdb_rating,
mode = 'lines',
name = genre + 'IMDb',
visible = (i == 0)
)
data.append(trace_kp)
data.append(trace_imdb)

# створюємо випадають меню
for i in range(N):
drop_menus.append(
dict(
args=['visible', [False]*2*i + [True]*2 + [False]*2*(N-1-i)],
label= top_genres[i],
method='restyle'
)
)

layout = go.Layout(
title='Фільми за жанрами',
updatemenus=list([
dict(
x = -0.1,
y = 1,
yanchor = 'top',
buttons = drop_menus
)
]),
)

fig = go.Figure(data = data, layout = layout)
iplot(fig)


висновки
В цій замітці ми познайомилися з використанням бібліотеки
plotly
для побудови різних інтерактивних графіків на python.
Мені здається, це дуже корисний інструмент для аналітичної роботи, оскільки він дозволяє робити інтерактивні візуалізації і легко ділитися ними з колегами.
Зацікавився раджу подивитися і інші приклади використання plot.ly.
Весь код і дані живуть на github
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.