Какие статьи по науке о данных привлекают больше внимания читателей (часть 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» и «обнаружение мошенничества» вызвали наименьший интерес у читателей.
Переходя к кластерам с наибольшей активностью, выделенные темы, вызывающие больший интерес у читателей, - это обработка естественного языка, нейронные сети, функции активации и вспомогательные векторные машины.
Подведение итогов
Хотя нам удалось определить общие темы в группах с низкой и высокой читательской активностью, мы все же наблюдали статьи, у которых не было много аплодисментов и ответов, а также статьи с высокой активностью в каждой группе. Анализ может быть полезен при попытке установить общие закономерности того, что интересует читателей, а также тем, которые имеют более высокую насыщенность статьями. Однако выбор соответствующей темы не гарантирует успеха статьи, так как многие другие важные факторы способствуют привлечению внимания читателя.