- PVSM.RU - https://www.pvsm.ru -

Meduza.io: а как же лайки?

Как-то раз, читая новости на Медузе, я обратил внимание на то, что у разных новостей разное соотношение лайков из Facebook и ВКонтакте. Какие-то новости мегапопулярны [1] на fb, а другими [2] люди делятся только во ВКонтакте. Захотелось присмотреться к этим данным, попытаться найти в них интересные закономерности. Заинтересовавшихся приглашаю под кат!

image

Data Scraping

Первым делом нужно получить данные для анализа. Предвкушая скорое расчехление Python + BeautifulSoup, я начал читать исходный код страниц. Разочарование ждало довольно быстро: эти данные подгружатся не сразу вместе с html'ой, а отложенно. Так как я не умею JavaScript, я начал искать ноги в сетевых соединениях страницы, и довольно быстро наткнулся на замечательную ручку API медузы:

https://meduza.io/api/v3/social?links=["shapito/2016/05/03/poliem-vse-kislotoy-i-votknem-provod-v-rozetku"]

Ручка возвращает приятную глазу json'ку:

image

Ну и конечно, раз links это массив, то сразу хочется попробовать подставить туда сразу несколько записей, и, ура, получаем интересующий нас список.

Даже парсить ничего не пришлось!

Теперь хочется получить данные о самих новостях. Здесь хочется поблагодарить хаброжителя sirekanyan [3] за его статью [4], где он нашел другую ручку.

https://meduza.io/api/v3/search?chrono=news&page=0&per_page=10&locale=ru

Опытным путём удалось установить, что максимальное значение параметра per_page равно 30, а page около 752 на момент написания статьи. Важной проверка того, что ручка social выдержит все 30 документов, пройдена успешно.

Осталось только выгрузить! Я использовал простенький скрипт на питоне

stream = 'https://meduza.io/api/v3/search?chrono=news&page={page}&per_page=30&locale=ru'
social = 'https://meduza.io/api/v3/social'
user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.3411.123 YaBrowser/16.2.0.2314 Safari/537.36'
headers = {'User-Agent' : user_agent }

def get_page_data(page):
    # Достаём страницы
    ans = requests.get(stream.format(page = page), headers=headers).json()
    # отдельно достаёт все социальные
    ans_social = requests.get(social, params = {'links' : json.dumps(ans['collection'])}, headers=headers).json()
    documents = ans['documents']
    for url, data in documents.iteritems():
        try:
            data['social'] = ans_social[url]['stats']
        except KeyError:
            continue
    with open('res_dump/page{pagenum:03d}_{timestamp}.json'.format(
        pagenum = page, timestamp = int(time.time())
    ), 'wb') as f:
        json.dump(documents, f, indent=2)

На всякий случай подставил валидный User-Agent, но и без этого всё работает.

Далее для распараллеливания и визуализации процесса очень помог скрипт моего бывшего коллеги, alexkuku [5]. Подробнее про подход можно почитать в его посте [6], он позволил сделать вот такой мониторинг:

image

Данные выгрузились очень быстро, менее чем за 10 минут, никакой капчи или заметного замедления. Качал в 4 потока с одного айпишника, без каких-либо надстроек.

Data Minining

Итак, на выходе у нас получилась большая json'ка с данными. Теперь загоним её в pandas dataframe, и покрутим в Jupyter.

Загрузим нужные данные:

df = pd.read_json('database.json').T
df = df.join(pd.DataFrame(df.social.to_dict()).T)
df.pub_date = pd.DatetimeIndex(df.pub_date)
df['trust']=df.source.apply(lambda x: x.get('trust', None) if type(x) == dict else None)

Построим boxplot

df[['fb', 'tw','vk']].plot.box(logy = True);

image

Сразу несколько выводов:

  1. Twitter отключил возможность смотреть количество твитнувших новость. :-( Придется обойтись без него
  2. Распределение, как и ожидалось, крайне ненормально: есть очень сильные выбросы, которые заметны даже на лог-шкале (сотни тысяч репостов).
  3. При этом, среднее число репостов оказалось довольно близким: медиана 24 и 17 (здесь и далее, facebook и вконтакте, соответственно), распределение vk несколько более "размазано".

Так кто же те самые супер-репостнутые новости медузы? Угадаете?

image

Ответ:

image

Ну конечно же, первое это FB: там же иностранные языки, советские газеты, Серов. А во второй 5nizza, "Моя ориентация", политика. Не знаю, как по мне, так всё очевидно!

Единственное, в чем схожи предпочтения двух соц.сетей: это Ирина Яровая, да Цветаева с Гуфом.

Теперь, хочется посмотреть на scatter plot двух величин: ожидается, что данные будут хорошо коррелировать друг с другом.

df['logvk'] = np.log10(df.vk)
df['logfb'] = np.log10(df.fb)
# Без логарифмов совсем непонятная картинка
sns.regplot('logfb', 'logvk', data = df )

image

sns.set(style="ticks")
sns.jointplot('logfb', 'logvk', 
              data = df.replace([np.inf, -np.inf], np.nan).dropna(subset = ['logfb', 'logvk']),
              kind="hex")

image

Кажется, видно два кластера: один с центром в (2.3, 2.4), и второй размазанный около нуля. В целом нет цели провести анализ даже для низкочастотных новостей (тех, которые оказались неинтересными в соц.сетях), так что давайте ограничимся только записями с более 10 лайков в обеих сетях. Не забудем проверить, что мы избавились от незначительного числа наблюдений.

stripped = df[(df.logfb > 1) & (df.logvk > 1)]
print "Working with {0:.0%} of news, {1:.0%} of social network activity".format(
    float(len(stripped)) / len(df), float(stripped[['vk', 'fb']].sum().sum()) / df[['vk', 'fb']].sum().sum()
)
# Working with 47% of news, 95% of social network activity

Плотность:

sns.jointplot('logfb', 'logvk', data = stripped, kind="kde", size=7, space=0)

image

Выводы

  • Нашли плотный кластер соотношения комментирования: 220 в facebook, 240 во ВКонтакте.
  • Кластер вытянут больше в facebook: в этой соц.сети люди репостят более диапазонно, по сравнению с ВК, где пик достаточно "узкий"
  • Есть мини-кластер фейсбучной активности в 150 fb и около 70 vk, достаточно необычный

Теперь хочется посмотреть на это соотношений в динамике: возможно, оно менялось.

by_month = stripped.set_index('pub_date').groupby(pd.TimeGrouper(freq = 'MS')).agg({'fb':sum, 'vk':sum})
by_month.plot( kind = 'area')

image

Интересно, что при общем росте объема активности в соц.сетях, фейсбук растёт быстрее. Кроме того здесь не видно какого-то взрывного роста, который я ожидал бы увидеть в Медузе. Первые месяцы активность была довольно низкой, но уже к декабрю 2014 уровень стабилизировался, новый рост начался лишь через год.

Посмотрим на динамику плотности распределения комментариев из двух социальных сетей:

image

Довольно занятно, что второй кластер уменьшается со временем, и скорее является артефактом прошлого.

Наконец, хочется проверить, что соотношение социальных сетей не меняется от типа документа: у Медузы кроме новостей есть карточки, истории, шапито, галереи, а также полигон.

def hexbin(x, y, color, **kwargs):
    cmap = sns.light_palette(color, as_cmap=True)
    plt.hexbin(x, y, gridsize=20, cmap=cmap, **kwargs)

g = sns.FacetGrid(stripped.loc[::-1], col="document_type", margin_titles=True, size=5, col_wrap = 3)
g.map(hexbin, "logfb", "logvk", extent=[1, 4, 1, 4]);

image

В целом видно, что данные вполне себе однородны по классам, нет заметных перекосов. Я ожидал бы от "шапито" большей социальной активности, но этого эффекта не наблюдается.

Зато, если посмотреть на разбивку по уровню доверия к источнику [7], то приятно видеть, что ненадежный источник менее популярен в соц.сетях, особенно в фейсбуке:

image

Что дальше?

На этом мой вечер завершился, и я пошел спать.

  • Я попробовал обучить простенькую Ridle регрессию на word2vec данных из заголовков статей. Можно посмотреть на гитхабе [8], никакой особенной предсказательной силы там нет. Кажется, чтобы хорошо предсказывать количество лайков, стоит хотя бы обучить модель на полных текстах новостей.
  • На основе этих данных очень хорошо можно ловить "яркие" события, сильно всколыхнувшие общественность. При этом соотношение fb/vk может быть хорошим предиктором для типа новости.
  • Активность в соц.сетях, кажется, сейчас может быть таким же важным KPI для новостника, как и посещаемость. Можно посмотреть на авторов / источники популярных постов, и на этой базе давать оценку работе. В пользу этой идеи говорит контрастность по достоверности источника: в facebook меньше постят недостоверные новости. Думаю, в том или ином виде это уже применяется в журналистике.

Код на Github [9]

Автор: feriat

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/120128

Ссылки в тексте:

[1] мегапопулярны: https://meduza.io/news/2016/04/11/umer-akter-albert-filozov

[2] другими: https://meduza.io/news/2016/04/09/demonstranty-v-londone-potrebovali-otstavki-premier-ministra-iz-za-ofshorov

[3] sirekanyan: https://habrahabr.ru/users/sirekanyan/

[4] статью: https://habrahabr.ru/post/259471/

[5] alexkuku: https://habrahabr.ru/users/alexkuku/

[6] посте: https://habrahabr.ru/post/277919/

[7] уровню доверия к источнику: https://www.facebook.com/AlShaburov/posts/10206190006647888

[8] на гитхабе: https://github.com/apetrin/meduza.io-parse-likes/blob/master/processing.ipynb

[9] Код на Github: https://github.com/apetrin/meduza.io-parse-likes

[10] Источник: https://habrahabr.ru/post/283058/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best