Начальный анализ данных (Exploratory Data Analysis)

Итак, поскольку мы уже рассмотрели какие графики можно построить для того, чтобы посмотреть, с какими данными мы имеем дело, сейчас мы посмотрим, какие ещё инструменты есть в python для начального анализа. По сути дела, большая часть материала является перевода замечательной лекции A Rubric for Data Wrangling and Exploration из Гарвардского курса CS109: Data Science.

Поэтому, если вы знаете английский, то можете прочитать её в оригинале.

In [1]:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import brewer2mpl
from matplotlib import rcParams

#colorbrewer2 Dark2 qualitative color table
dark2_colors = brewer2mpl.get_map('Dark2', 'Qualitative', 7).mpl_colors

rcParams['figure.figsize'] = (10, 6)
rcParams['figure.dpi'] = 150
rcParams['axes.color_cycle'] = dark2_colors
rcParams['lines.linewidth'] = 2
rcParams['axes.facecolor'] = 'white'
rcParams['font.size'] = 14
rcParams['patch.edgecolor'] = 'white'
rcParams['patch.facecolor'] = dark2_colors[0]
rcParams['font.family'] = 'StixGeneral'

#tell pandas to display wide tables as pretty HTML tables
pd.set_option('display.width', 500)
pd.set_option('display.max_columns', 100)

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

Общий для большинства случаев подход заключается в следующем:
  1. Загрузить все данные в один DataFrame
  2. Очистить данные. В результате обработки они должны быть:
    • Каждая строка должна представлять один-единственный объект
    • Каждый столбец описывает одно свойство объекта
    • Каждый столбец содержит неделимые данные, среди которых нельзя выделить подмножества
  3. Проанализировать весь набор данных. Для этого можно воспользоваться гистограммой, scatter plot-ом и встроенными фнукциями, чтобы сделать выводы
  4. Проанализируйте свойства имеющихся кластеров или подмножеств данных. Для этого, например, можно использовать функцию groupby().

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

В этом примере мы скачаем список подготовленный список фильмов с imdb http://bit.ly/cs109_imdb.

1. Инициализируем DataFrame

В текстовом файле был использован для отделения характеристик друг от друга и не было никакого заголовка, описывающего свойства. Поэтому:

In [2]:
names = ['imdbID', 'title', 'year', 'score', 'votes', 'runtime', 'genres']
data = pd.read_csv('../data/imdb_top_10000.txt', delimiter='\t', names=names).dropna()
print "Number of rows: %i" % data.shape[0]
data.head()  # Это выведет 5 первых строчек
Number of rows: 9999

Out[2]:
imdbID title year score votes runtime genres
0 tt0111161 The Shawshank Redemption (1994) 1994 9.2 619479 142 mins. Crime|Drama
1 tt0110912 Pulp Fiction (1994) 1994 9.0 490065 154 mins. Crime|Thriller
2 tt0137523 Fight Club (1999) 1999 8.8 458173 139 mins. Drama|Mystery|Thriller
3 tt0133093 The Matrix (1999) 1999 8.7 448114 136 mins. Action|Adventure|Sci-Fi
4 tt1375666 Inception (2010) 2010 8.9 385149 148 mins. Action|Adventure|Sci-Fi|Thriller

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

Что мы будем с этим делать:

Выделяем длительность фильма

In [3]:
clean_runtime = [float(r.split(' ')[0]) for r in data.runtime]
data['runtime'] = clean_runtime
data.head()
Out[3]:
imdbID title year score votes runtime genres
0 tt0111161 The Shawshank Redemption (1994) 1994 9.2 619479 142 Crime|Drama
1 tt0110912 Pulp Fiction (1994) 1994 9.0 490065 154 Crime|Thriller
2 tt0137523 Fight Club (1999) 1999 8.8 458173 139 Drama|Mystery|Thriller
3 tt0133093 The Matrix (1999) 1999 8.7 448114 136 Action|Adventure|Sci-Fi
4 tt1375666 Inception (2010) 2010 8.9 385149 148 Action|Adventure|Sci-Fi|Thriller

Выделяем жанры

Для того, чтобы принадлежность жанру находилась в этой же таблицы, мы воспользуемся индикаторными переменными. Теперь в наборе данных у нас добавится по одному столбцу для каждого жанра и при помощи True/False значений мы будем задавать его наличие.

In [4]:
#determine the unique genres
genres = set()
for m in data.genres:
    genres.update(g for g in m.split('|'))
genres = sorted(genres)

#make a column for each genre
for genre in genres:
    data[genre] = [genre in movie.split('|') for movie in data.genres]
         
data.head()
Out[4]:
imdbID title year score votes runtime genres Action Adult Adventure Animation Biography Comedy Crime Drama Family Fantasy Film-Noir History Horror Music Musical Mystery News Reality-TV Romance Sci-Fi Sport Thriller War Western
0 tt0111161 The Shawshank Redemption (1994) 1994 9.2 619479 142 Crime|Drama False False False False False False True True False False False False False False False False False False False False False False False False
1 tt0110912 Pulp Fiction (1994) 1994 9.0 490065 154 Crime|Thriller False False False False False False True False False False False False False False False False False False False False False True False False
2 tt0137523 Fight Club (1999) 1999 8.8 458173 139 Drama|Mystery|Thriller False False False False False False False True False False False False False False False True False False False False False True False False
3 tt0133093 The Matrix (1999) 1999 8.7 448114 136 Action|Adventure|Sci-Fi True False True False False False False False False False False False False False False False False False False True False False False False
4 tt1375666 Inception (2010) 2010 8.9 385149 148 Action|Adventure|Sci-Fi|Thriller True False True False False False False False False False False False False False False False False False False True False True False False

Выделяем год выпуска фильма в численную переменную

In [5]:
data['title'] = [t[0:-7] for t in data.title]
data.head()
Out[5]:
imdbID title year score votes runtime genres Action Adult Adventure Animation Biography Comedy Crime Drama Family Fantasy Film-Noir History Horror Music Musical Mystery News Reality-TV Romance Sci-Fi Sport Thriller War Western
0 tt0111161 The Shawshank Redemption 1994 9.2 619479 142 Crime|Drama False False False False False False True True False False False False False False False False False False False False False False False False
1 tt0110912 Pulp Fiction 1994 9.0 490065 154 Crime|Thriller False False False False False False True False False False False False False False False False False False False False False True False False
2 tt0137523 Fight Club 1999 8.8 458173 139 Drama|Mystery|Thriller False False False False False False False True False False False False False False False True False False False False False True False False
3 tt0133093 The Matrix 1999 8.7 448114 136 Action|Adventure|Sci-Fi True False True False False False False False False False False False False False False False False False False True False False False False
4 tt1375666 Inception 2010 8.9 385149 148 Action|Adventure|Sci-Fi|Thriller True False True False False False False False False False False False False False False False False False False True False True False False

2. Анализируем все данные

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

In [6]:
data[['score', 'runtime', 'year', 'votes']].describe()
Out[6]:
score runtime year votes
count 9999.000000 9999.000000 9999.000000 9999.000000
mean 6.385989 103.580358 1993.471447 16605.462946
std 1.189965 26.629310 14.830049 34564.883945
min 1.500000 0.000000 1950.000000 1356.000000
25% 5.700000 93.000000 1986.000000 2334.500000
50% 6.600000 102.000000 1998.000000 4981.000000
75% 7.200000 115.000000 2005.000000 15278.500000
max 9.200000 450.000000 2011.000000 619479.000000

Сразу обращаем внимание, что минимальная длительность фильма составляет 0 минут. Интересно, для скольких фильмов нет информации о длительности?

In [7]:
print len(data[data.runtime == 0])

#Сразу пометим длительность этих фильмов как плохие данные.
data.runtime[data.runtime==0] = np.nan
282

Поскольку мы пометили длительность специальой переменной Nan (отсутствие значения), теперь для этой колонки будет рассчитано настоящая минимальная длительность фильма.

In [8]:
data.runtime.describe()
Out[8]:
count    9717.000000
mean      106.586395
std        20.230330
min        45.000000
25%        93.000000
50%       103.000000
75%       115.000000
max       450.000000
dtype: float64

В общем случае отсутсвующие значения можно каким-то образом прогнозировать. На хабре про это есть отличная статья.

Начальные графики

В данном случае мы ограничимся гистограммами и scatter-plot'ом. Подробнее про виды графиков можно прочитать в предыдущей статье.

In [9]:
plt.hist(data.year, bins=np.arange(1950, 2013), color='#cccccc');
plt.xlabel("Release Year");
In [10]:
plt.hist(data.score, bins=20, color='#cccccc')
plt.xlabel("IMDB rating");
In [11]:
plt.hist(data.runtime.dropna(), bins=50, color='#cccccc');
plt.xlabel("Runtime distribution");

Получается, что большинство фильмов вышло совсем недавно и имеет довольно низкую среднюю оценку. Посмотрим подробнее.

In [12]:
plt.scatter(data.year, data.score, lw=0, alpha=.08, color='k');
plt.xlabel("Year");
plt.ylabel("IMDB Rating");

plt.scatter(data.votes, data.score, lw=0, alpha=.2, color='k') plt.xlabel("Number of Votes"); plt.ylabel("IMDB Rating"); plt.xscale('log');

Посмотрим на некоторые числовые характеристики

Действительно плохие фильмы с большим количеством голосов.

In [13]:
data[(data.votes > 9e4) & (data.score < 5)][['title', 'year', 'score', 'votes', 'genres']]
Out[13]:
title year score votes genres
317 New Moon 2009 4.5 90457 Adventure|Drama|Fantasy|Romance
334 Batman & Robin 1997 3.5 91875 Action|Crime|Fantasy|Sci-Fi

Фильмы с минимальным рейтингом

In [14]:
data[data.score == data.score.min()][['title', 'year', 'score', 'votes', 'genres']]
Out[14]:
title year score votes genres
1982 Manos: The Hands of Fate 1966 1.5 20927 Horror
2793 Superbabies: Baby Geniuses 2 2004 1.5 13196 Comedy|Family
3746 Daniel the Wizard 2004 1.5 8271 Comedy|Crime|Family|Fantasy|Horror
5158 Ben & Arthur 2002 1.5 4675 Drama|Romance
5993 Night Train to Mundo Fine 1966 1.5 3542 Action|Adventure|Crime|War
6257 Monster a-Go Go 1965 1.5 3255 Sci-Fi|Horror
6726 Dream Well 2009 1.5 2848 Comedy|Romance|Sport

Фильмы с лучшим рейтингом

In [15]:
data[data.score == data.score.max()][['title', 'year', 'score', 'votes', 'genres']]
Out[15]:
title year score votes genres
0 The Shawshank Redemption 1994 9.2 619479 Crime|Drama
26 The Godfather 1972 9.2 474189 Crime|Drama

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

In [16]:
genre_count = np.sort(data[genres].sum())[::-1]
pd.DataFrame({'Genre Count': genre_count})
Out[16]:
Genre Count
Drama 5697
Comedy 3922
Thriller 2832
Romance 2441
Action 1891
Crime 1867
Adventure 1313
Horror 1215
Mystery 1009
Fantasy 916
Sci-Fi 897
Family 754
War 512
Biography 394
Music 371
History 358
Animation 314
Sport 288
Musical 260
Western 235
Film-Noir 40
Adult 9
News 1
Reality-TV 1

Посмотрим, сколько в среднем жанров указано для каждого фильма. Параметр axid=1 позволяет вычислять сумму по столбцу.

In [17]:
genre_count = data[genres].sum(axis=1) 
print "Average movie has %0.2f genres" % genre_count.mean()
genre_count.describe()
Average movie has 2.75 genres

Out[17]:
count    9999.000000
mean        2.753975
std         1.168910
min         1.000000
25%         2.000000
50%         3.000000
75%         3.000000
max         8.000000
dtype: float64

Детальный анализ групп данных

Разобьём фильмы по декадам.

In [18]:
decade =  (data.year // 10) * 10

tyd = data[['title', 'year']]
tyd['decade'] = decade

tyd.head()
Out[18]:
title year decade
0 The Shawshank Redemption 1994 1990
1 Pulp Fiction 1994 1990
2 Fight Club 1999 1990
3 The Matrix 1999 1990
4 Inception 2010 2010

Теперь мы воспользуемся функцией GroupBy(), для того чтобы сгруппировать их по декадам и рассчитать среднее значение рейтинга. Это значение мы отобразим на графике.

In [19]:
decade_mean = data.groupby(decade).score.mean()
decade_mean.name = 'Decade Mean'
print decade_mean

plt.plot(decade_mean.index, decade_mean.values, 'o-',
        color='r', lw=3, label='Decade Average');
plt.scatter(data.year, data.score, alpha=.04, lw=0, color='k');
plt.xlabel("Year");
plt.ylabel("Score");
plt.legend(frameon=False);
year
1950    7.244522
1960    7.062367
1970    6.842297
1980    6.248693
1990    6.199316
2000    6.277858
2010    6.344552
Name: Decade Mean, dtype: float64

Посмотрим, насколько рассеиваются оценки для каждого года. Для этого мы вычислим дисперсию для каждого года.

In [20]:
grouped_scores = data.groupby(decade).score

mean = grouped_scores.mean()
std = grouped_scores.std()

plt.plot(decade_mean.index, decade_mean.values, 'o-',
        color='r', lw=3, label='Decade Average');
plt.fill_between(decade_mean.index, (decade_mean + std).values,
                 (decade_mean - std).values, color='r', alpha=.2);
plt.scatter(data.year, data.score, alpha=.04, lw=0, color='k');
plt.xlabel("Year");
plt.ylabel("Score");
plt.legend(frameon=False);

Также можно пройтись по списку, используя объект GroupBy. Итератор будет возвращать две переменные: одну со значением ключа группу, а вторую - со списком объектов в эту группу попадающих.

In [21]:
for year, subset in data.groupby('year'):
    print year, subset[subset.score == subset.score.max()].title.values
1950 ['Sunset Blvd.']
1951 ['Strangers on a Train']
1952 ["Singin' in the Rain"]
1953 ['The Wages of Fear' 'Tokyo Story']
1954 ['Seven Samurai']
1955 ['Diabolique']
1956 ['The Killing']
1957 ['12 Angry Men']
1958 ['Vertigo']
1959 ['North by Northwest']
1960 ['Psycho']
1961 ['Yojimbo']
1962 ['To Kill a Mockingbird' 'Lawrence of Arabia']
1963 ['The Great Escape' 'High and Low']
1964 ['Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb']
1965 ['For a Few Dollars More']
1966 ['The Good, the Bad and the Ugly']
1967 ['Cool Hand Luke']
1968 ['Once Upon a Time in the West']
1969 ['Butch Cassidy and the Sundance Kid' 'Army of Shadows']
1970 ['Patton' 'The Conformist' 'Le Cercle Rouge']
1971 ['A Clockwork Orange']
1972 ['The Godfather']
1973 ['The Sting' 'Scenes from a Marriage']
1974 ['The Godfather: Part II']
1975 ['Outrageous Class']
1976 ['Tosun Pasa']
1977 ['Star Wars: Episode IV - A New Hope']
1978 ['The Girl with the Red Scarf']
1979 ['Apocalypse Now']
1980 ['Star Wars: Episode V - The Empire Strikes Back']
1981 ['Raiders of the Lost Ark']
1982 ['The Marathon Family']
1983 ['Star Wars: Episode VI - Return of the Jedi']
1984 ['Balkan Spy']
1985 ['The Broken Landlord']
1986 ['Aliens']
1987 ['Mr. Muhsin']
1988 ['Cinema Paradiso']
1989 ['Indiana Jones and the Last Crusade' "Don't Let Them Shoot the Kite"]
1990 ['Goodfellas']
1991 ['The Silence of the Lambs']
1992 ['Reservoir Dogs']
1993 ["Schindler's List"]
1994 ['The Shawshank Redemption']
1995 ['The Usual Suspects' 'Se7en']
1996 ['Fargo' 'The Bandit']
1997 ['Life Is Beautiful']
1998 ['American History X']
1999 ['Fight Club']
2000 ['Memento']
2001 ['The Lord of the Rings: The Fellowship of the Ring']
2002 ['City of God']
2003 ['The Lord of the Rings: The Return of the King']
2004 ['Eternal Sunshine of the Spotless Mind']
2005 ['My Father and My Son']
2006 ['The Departed' 'The Lives of Others']
2007 ['Like Stars on Earth']
2008 ['The Dark Knight']
2009 ['Inglourious Basterds']
2010 ['Inception']
2011 ['A Separation']

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

In [22]:
fig, axes = plt.subplots(nrows=4, ncols=6, figsize=(12, 8))
plt.tight_layout()

bins = np.arange(1950, 2013, 3)
for ax, genre in zip(axes.ravel(), genres):
    ax.hist(data[data[genre] == 1].year, 
            bins=bins, normed=True, color='r', alpha=.3, ec='none')
    ax.hist(data.year, bins=bins, ec='None', normed=True, zorder=0, color='#cccccc')
    
    ax.annotate(genre, xy=(1955, 3e-2), fontsize=14)
    ax.xaxis.set_ticks(np.arange(1950, 2013, 30))
    ax.set_yticks([])
    ax.set_xlabel('Year')
Выводы:
  1. Вестерны и мьюзиклы распределены более-менее равномерно.
  2. Фильмы в жанре "Нуар" уже не так популярны.
In [23]:
fig, axes = plt.subplots(nrows=4, ncols=6, figsize=(12, 8))
plt.tight_layout()

bins = np.arange(30, 240, 10)

for ax, genre in zip(axes.ravel(), genres):
    ax.hist(data[data[genre] == 1].runtime, 
            bins=bins, color='r', ec='none', alpha=.3, normed=True)
               
    ax.hist(data.runtime, bins=bins, normed=True, ec='none', color='#cccccc',
            zorder=0)
    
    ax.set_xticks(np.arange(30, 240, 60))
    ax.set_yticks([])
    ax.set_xlabel("Runtime [min]")
    ax.annotate(genre, xy=(230, .02), ha='right', fontsize=12)
Выводы:
  1. Биографические и исторические фильмы длятся дольше других
  2. Мультфильмы, как правило, короче
  3. Фильмы в жанре "Нуар", как правило, длятся 100 минут
  4. Мьюзиклы очень вариативны относительно длительности, несмотря на то, что большая часть длится примерно одинаково со всеми
In [24]:
fig, axes = plt.subplots(nrows=4, ncols=6, figsize=(12, 8))
plt.tight_layout()

bins = np.arange(0, 10, .5)

for ax, genre in zip(axes.ravel(), genres):
    ax.hist(data[data[genre] == 1].score, 
            bins=bins,color='r', ec='none', alpha=.3, normed=True);
               
    ax.hist(data.score, bins=bins, normed=True, ec='none', color='#cccccc',
            zorder=0);
    
    ax.set_yticks([]);
    ax.set_xlabel("Score");
    ax.set_ylim(0, .4);
    ax.annotate(genre, xy=(0, .2), ha='left', fontsize=12);
Выводы:
  1. Исторические и биографические фильмы получают более высокий рейтинг. Возможно, это обусловлено тем, что за них не голосует широкая масса.
  2. Ужасы и фильмы для взрослых (чтобы не скрывалось за этой категорией), оценивают хуже.