Какие статьи по науке о данных привлекают больше внимания читателей (часть 2)

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

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

Сводка данных

Напомним, как выглядят данные. Это комбинация статей, полученных из трех источников данных [поле: Источник] - Аналитика Видхья [‘avd’], TDS [‘tds’] и Towards AI [‘tai’].

Мы собрали заголовки, подзаголовки, аплодисменты и отзывы отдельных статей в архивах публикаций.

import pandas as pd
# Reading the data obtained using code here.
avd = pd.read_csv('analytics_vidhya_data.csv')
tds = pd.read_csv('medium_articles.csv')
tai = pd.read_csv('towards_ai_data.csv')
avd['source'] = 'avd'
tds['source'] = 'tds'
tai['source'] = 'tai'
# Create single data set, join title and subtitle
single_matrix = pd.concat([avd, tds, tai])
single_matrix['title_subtitle'] = [' '.join([str(i),str(j)]) for i, j in zip(single_matrix['Title'].fillna(''), single_matrix['Subtitle'].fillna(''))]

Мы добавили в набор данных дополнительный столбец под названием «title_subtitle», который является объединением столбцов «Title» и «Subtitle», мы будем использовать этот столбец в основном для того, чтобы иметь лучшее представление о теме статьи. Интересно, что 39% статей не имеют субтитров, и очень небольшая часть (0,13%) не имеет заголовков.

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

# We will use seaborn to create all plots
import seaborn as sns
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(8, 5))
# Claps
sns.boxplot(ax=axes[0], x="source", y="Claps", data=single_matrix)
# Responses
sns.boxplot(ax=axes[1], x="source", y="Responses", data=single_matrix)

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

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

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

# Code to create distribution subplots
fig, axes = plt.subplots(2, 1, figsize=(8, 8))
# Claps
sns.distplot(avd['Claps'][avd['Claps']<10000], hist=True, rug=False, ax=axes[0])
sns.distplot(tds['Claps'][tds['Claps']<10000], hist=True, rug=False, ax=axes[0])
sns.distplot(tai['Claps'][tai['Claps']<10000], hist=True, rug=False, ax=axes[0])
# Responses
sns.distplot(avd['Responses'], hist=True, rug=False, ax=axes[1])
sns.distplot(tds['Responses'], hist=True, rug=False, ax=axes[1])
sns.distplot(tai['Responses'], hist=True, rug=False, ax=axes[1])

Мы видим, что оба дистрибутива смещены влево, а это означает, что большинство статей получают очень мало аплодисментов и еще меньше откликов. И снова это неудивительно, поскольку успех статей зависит от многих факторов, таких как хорошее качество написания, актуальные темы и многое другое. Достижение хорошего баланса - непростая задача!

Как почистить текстовые данные?

Очистка данных - важный этап (если не самый важный) при работе с текстом. Существуют стандартные практики, которым следует следовать при решении таких задач. Мы предпримем следующие шаги для обработки заголовков и субтитров:

  • удалить знаки препинания и другие символы
  • удалить стоп-слова и цифры
  • лемматизировать слова

Мы будем использовать смесь регулярных выражений и библиотеки nltk для удаления знаков препинания, символов, стоп-слов и цифр.

import re
single_matrix['title_subtitle'] = [re.findall(r'\w+', i.lower()) for i in single_matrix['title_subtitle'].fillna('NONE')]

Приведенный выше код соответствует одному или нескольким символам слова, на самом деле r ’\ w +’ совпадает с r ’[a-zA-Z0–9 _] +’. Кроме того, при применении команд re.findall () и i.lower () они удобно разбивают предложения на слова и преобразуют их в нижний регистр. Это будет очень полезно в следующих шагах. Итак, предложение «Отчетность в Qlikview | Специальная отчетность » становится [отчет, в, qlikview, ad, hoc, отчет].

Затем мы будем использовать библиотеку nltk для загрузки словаря стоп-слов, чтобы мы могли удалить их из предложений. Кроме того, мы добавляем в список слова «использование» и «часть», поскольку они чрезмерно используются в наборе данных. Чтобы удалить стоп-слова, мы используем цикл for для перебора каждого предложения, при этом мы также убираем цифры из предложений.

# The code to upload list of stop words and remove them from sentences
import nltk
nltk.download('stopwords') 
from nltk.corpus import stopwords
stopwords_eng = stopwords.words('english')  
stopwords_eng += ['use', 'using', 'used', 'part']
new_titles_sub = []
for title_sub in single_matrix['new_title_subtitle']:
    new_title_sub = []
    for w_title in title_sub:
        if w_title not in stopwords_eng and not w_title.isdigit():
            new_title_sub.append(w_title)
    
    new_titles_sub.append(new_title_sub) 
    
single_matrix['new_title_subtitle'] = new_titles_sub

Наконец, мы собираемся лемматизировать слова в предложениях. Лемматизация преобразует слово в его значимую корневую форму с учетом контекста. Часто стемминг используется как более быстрая в вычислительном отношении альтернатива, но менее точная. Мы снова используем nltk для лемматизации слов.

nltk.download('wordnet')
nltk.download('words')
from nltk.stem import WordNetLemmatizer
wordnet_lemmatizer = WordNetLemmatizer()
new_titles_sub = []
for title_sub in single_matrix['title_subtitle']:
    new_title_sub = []
    for w_title in title_sub:
        new_title_sub.append(wordnet_lemmatizer.lemmatize(w_title, pos="v"))
    new_titles_sub.append(new_title_sub) 
    
single_matrix['new_title_subtitle'] = new_titles_sub
single_matrix['new_title_subtitle'] = [' '.join(i) for i in single_matrix['new_title_subtitle']]

Давайте посмотрим, как выглядят предложения после всех преобразований.

Как векторизовать текстовые данные с помощью TF-IDF?

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

Как следует из названия, мера состоит из двух частей: одна определяет частоту появления слова в документе (TF), а другая - степень уникальности слова в корпусе (IDF). Давайте посмотрим на упрощенную версию формулы и ее компоненты:

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

Самый простой способ понять вычисления - это на примере: в нашем наборе данных один заголовок - это документ, а все заголовки образуют корпус (набор документов). Обратите внимание на слово 'create' в заголовке 'используйте переменные qlikview для создания мощных историй данных', в документе 7 слов и появляется 'create' только один раз, поэтому TF (create) = 1/7. Общее количество статей в одном из источников данных составляет 12963, а слово 'create' появляется в 268 заголовках, поэтому IDF (create ) = журнал (12963/268) = 3,88. Таким образом, TF-IDF = 0,14 * 3,88 = 0,55 - это оценка за слово «создать».

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

Примечание: TfidfVectorize r использует формулу, немного отличающуюся от указанной выше, она добавляет 1 к IDF. Это сделано для того, чтобы не пропустить слова, которые встречаются в каждом документе.

from sklearn.feature_extraction.text import TfidfVectorizer
tf = TfidfVectorizer(analyzer='word', ngram_range=(1, 3), min_df=0)
tfidf_matrices = []
data_sets = []
for source in ['avd', 'tai', 'tds']:
    source_data = single_matrix[single_matrix['source'] == source].drop_duplicates()
    data_sets.append(source_data['new_title_subtitle'])
    tfidf_matrices.append(tf.fit_transform(
source_data['new_title_subtitle']))

Мы ввели цикл for, который выполняет итерацию по источникам данных, это сделано для сокращения времени вычислений. Это также понадобится при просмотре расстояний между всеми парами предложений и их разбиении на группы. Результатом является разреженная матрица, где строки - это документы, а столбцы - все уникальные слова в корпусе.

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

Мы использовали библиотеку sklearn для вычисления попарного косинусного сходства, опять же с разделением по источнику.

from sklearn.metrics.pairwise import linear_kernel
matrix_with_cos_sim = []
for m in tfidf_matrices:
    matrix_with_cos_sim.append(linear_kernel(m, m))

Результатом для каждого источника данных является массив numpy (NxN) с попарным сходством между всеми предложениями, где N - количество заголовков / субтитров для одного источника данных.

Как сгруппировать похожие предложения с помощью сетевых графов?

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

Мы будем использовать библиотеки Python networkx и community для построения и разбиения графа. Прежде чем приступить к построению графика, мы выберем только топ-15 похожих заголовков для каждого документа в корпусе, число было выбрано на основе показателя, называемого модульностью, который показывает, насколько хорошее разделение. Такой подход не только делает график более четким, но и помогает увеличить скорость вычислений.

import numpy as np
from tqdm import tnrange
top_n_sentences = []
for cs, t in zip(matrix_with_cos_sim, data_sets):
    no_dups = np.array(t)
    i = 0
    top_frame = []
    for c, z in zip(cs, tnrange(len(cs))):
        # Create vector of titles
        start_name = pd.Series([no_dups[i]]*15) 
        # Index of top 15 similar titles
        ix_top_n = np.argsort(-c)[0:15]
        cos_sim = pd.Series(c[ix_top_n])
        names = pd.Series(no_dups[ix_top_n])
        i +=1
        top_frame.append(pd.DataFrame([start_name, names, cos_sim]).transpose())
    
    top_frame = pd.concat(top_frame)
    top_frame.columns = ['title1', 'title2', 'cos_sim']
    # Remove the similarities for the same sentences
    top_frame['is_same'] = [bool(i==j) for i, j in zip(top_frame['title1'], top_frame['title2'])]
    top_frame = top_frame[top_frame['is_same'] != True]
        
    top_n_sentences.append(top_frame)

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

Мы продолжим построение и разбиение графа, мы сделаем это для источника, у которого второе по величине количества статей, - это Analytics Vidhya. Фрагменты кода можно применять ко всем источникам, описанным в этой статье.

# We start by defining the structure of the graph
top_frame = top_n_sentences[2] #TDS articles
edges = list(zip(top_frame['title1'], top_frame['title2']))
weighted_edges = list(zip(top_frame['title1'], top_frame['title2'], top_frame['cos_sim']))
nodes = list(set(top_frame['title1']).union(set(top_frame['title2'])))

Теперь мы можем использовать networkx для построения графа с использованием структуры, определенной выше.

import networkx as nx
G = nx.Graph()
G.add_nodes_from(nodes)
G.add_edges_from(edges)
G.add_weighted_edges_from(weighted_edges)

Затем мы разбиваем граф, используя библиотеку community, перед импортом модуля убедитесь, что вы установили библиотеку python-louvain, чтобы избежать ошибок.

# !pip install python-louvain
import community
partition = community.best_partition(G)
modularity = community.modularity(partition, G)

Ранее мы упоминали модульность, показатель качества раздела, в данном случае значение 0,7. Обычно значения выше 0,6 считаются достаточно приличным разделением.

# Takes some time for larger graphs
import matplotlib.pyplot as plt
pos = nx.spring_layout(G, dim=2)
community_id = [partition[node] for node in G.nodes()]
fig = plt.figure(figsize=(10,10))
nx.draw(G, pos, edge_color = ['silver']*len(G.edges()), cmap=plt.cm.tab20,
        node_color=community_id, node_size=150)

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

Прежде чем мы рассмотрим кластеры более подробно, мы собираемся преобразовать переменные partition, которые мы создали ранее, в более читаемый формат.

title, cluster = [], []
for i in partition.items():
    title.append(i[0])
    cluster.append(i[1])
    
frame_clust = pd.DataFrame([pd.Series(title), pd.Series(cluster)]).transpose()
frame_clust.columns = ['Title', 'Cluster']

Результатом приведенного выше кода является фрейм данных со всеми заголовками и субтитрами и сообщество, к которому они принадлежат, с 45 кластерами, идентифицированными путем разбиения графа.

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

avd = single_matrix[single_matrix['source'] ==           'avd'].drop_duplicates()
frame_clust = frame_clust.merge(tds[['Title', 'new_title_subtitle', 'Claps', 'Responses']], how='left', left_on='Title', right_on='new_title_subtitle')
grouped_mat = frame_clust.groupby('Cluster').agg(
{'Claps': ['max', 'mean', 'sum', 'median'],
 'Responses': ['max', 'mean', 'sum', 'median'], 
 'Title_x': 'count'}).reset_index()
grouped_mat.columns = ['cluster', 'claps_max', 'claps_mean',  'claps_sum', 'claps_median','responses_max', 'responses_mean', 'responses_sum', 'responses_median', 'title_count']
grouped_mat = grouped_mat.sort_values(by = ['claps_median', 'title_count'])

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

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

from wordcloud import WordCloud
fig, ax = plt.subplots(1, 3, figsize=(12.5,6.5))
clusters = [19, 39, 38] #lowest activity groups
# clusters = [43, 28, 7] #highest activity groups
for cluster, col in zip(clusters, [0, 1, 2]):
    corpus = ' '.join(frame_clust['new_title_subtitle'].  [frame_clust['Cluster'] == cluster])
    ax[col].imshow(WordCloud(width = 800,
                             height = 800,
                             background_color ='white', 
                             min_font_size = 10).generate(corpus))
    ax[col].axis("off")
plt.show()

Сначала мы смотрим на сообщества с самой низкой активностью. Кажется, что в кластере 19 в основном есть статьи, принадлежащие одному автору, которые объясняют более низкую активность. Две другие группы состоят из статей, написанных несколькими авторами. Интересно отметить, что такие темы, как «объектно-ориентированное программирование на Python» и «обнаружение мошенничества» вызвали наименьший интерес у читателей.

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

Подведение итогов

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