Время прочтения: 5 мин.
Всем привет! Сегодня я хочу поделиться решением задачи по анализу жалоб граждан в Московскую мэрию, которую я реализовывал в рамках конкурса DSC.
Портал mos.ru агрегирует жалобы граждан на общественно значимые проблемы Москвы, находящиеся в зоне ответственности местных властей. На данный момент в открытом доступе находятся около 100 тысяч сообщений. Каждому сообщению автором или модератором присваивается одна из немногочисленных категорий, детально проблема излагается в тексте обращения. Как правило, темы сообщения специфичны, и категория не описывает суть проблемы. Требовалось и проанализировать жалобы, опредилив их в соответствующие подкатегории.
Для решения данной задачи я применил gensim для тематического моделирования и визуализировал данные с помощью дендрограмм. Уточню — scikit-leam не поддерживает метод латентного размещения Дирихле. Поэтому я и воспользовался пакетом geпsim, разработанным Радимом Ржехоржеком – исследователем в области машинного обучения и консультантом из Великобритании. Получилось эффектно, понятно и, по итогам конкурса, данное решение было удостоено упоминания как «интересная реализация»
Этапы реализации задачи по большей части стандартные:
- Загрузка данных;
- Нормализация текста;
- Векторизация TF-IDF;
- Классификация.
Для начала необходимо сделать импорт необходимых библиотек.
from itertools import combinations
import pymorphy2
from gensim import models, corpora
from nltk import corpus, download as download_sw
import networkx as nx
download_sw('stopwords') # загружаем стоп-слова
stopwords = corpus.stopwords.words('russian')
Загрузка данных
Код стандартный. Объем датасета – 90176 строк, выглядит так:
theme | text | responsible | post | date | |
0 | ‘Сообщение о проблеме «Несоблюдение требований… | Выше второго этажа | Ларин А.С. | начальник Объединения административно-техниче… | 16 Ноября 2020 в 16:31′ |
1 | ‘Сообщение о проблеме «Неубранная городская те… | По адресу Снежная д24 расположена музыкальная … | Кучма А.А. | глава управы района Свиблово города Москвы | 05 Мая 2020 в 15:45′ |
Далее, немного почистил датасет, удалив дубликаты, и обогатил некоторыми атрибутами, спарсенными с текстового поля самого обращения с помощью метода pd.DataFrame.assign, выделив адрес и район обращения. Результат – ниже:
problem | adress | area | text | |
1 | Неубранная городская территория | Снежная улица д.24Д | Свиблово | По адресу Снежная д24 расположена музыкальная … |
3 | Неубранная городская территория | Снежная улица д.24Д | Свиблово | Очистите опору освещения. Приведите в надлежащ… |
4 | Захламление территории | Снежная улица д.1А | Свиблово | Более двух недель лежит куча грунта в перемешк… |
Морфология и токенизация
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer("russian")
def token_morf_stemm(text):
morph = pymorphy2.MorphAnalyzer()
tokens = (text.str.lower().replace('[^а-я-ё]|-', ' ', regex=True)
.apply(lambda x: [morph.parse(word)[0].normal_form for word in x.split(' ')
if len(word) > 2 and word not in stopwords]))
stemms=[]
for tokk in tokens:
stem = [stemmer.stem(t) for t in tokk]
stemms.append(stem)
return stemms
# готовим данные токениация, морфология
tokens = token_morf_stemm(prepared['text'])
len(tokens)
Векторизация
Для расчета TF-IDF я использовал класс TfidfVectorizer из библиотеки sklearn, чтобы вычислить TF-IDF. Отмечу, что просто подсчет количества слов в каждом документе имеет одну проблему: это дает больший вес более длинным документам, чем более коротким документам. Чтобы избежать этого, мы можем использовать частоту (TF — термин частоты) т.е. # количество (слово) / # общее количество слов в каждом документе. В библиотеке Scikit-Learn есть преобразователь TfidfVectorizer, в модуле feature_extraction.text, предназначенный для векторизации документов с оценками TF–IDF. Технически TfidfVectorizer вызывает преобразователь CountVectorizer, который мы использовали для превращения мешка слов в количества вхождений лексем, а затем TfidfTransformer, нормализующий эти количества обратной частотой документа. На входе TfidfVectorizer принимает последовательность имен файлов, объектов файлов или строк с коллекцией исходных документов (мы будем использовать prepared[‘text’]), подобно преобразователю CountVectorizer. Далее применяются методы по умолчанию лексемизации и предварительной обработки, если не указаны другие функции. Результатом работы преобразователя является разреженная матрица вида ((doc, term), tfidf), где каждый ключ является парой «документ/лексема», а значением служит оценка TF–IDF.
tfidf_vectorizer=TfidfVectorizer(ngram_range=(1,3), max_df=0.8, max_features=10000, min_df=0.01, stop_words=stopwords, use_idf=True,
tokenizer=casual_tokenize)
tfidf_matrix = tfidf_vectorizer.fit_transform(prepared['text']).toarray()
Теперь у нас есть пул из 88283 сообщений и построенный по ним 223-мерный вектор признаков.
KMeans | MiniBatchKMeans | problem | text | tokens | |
0 | 0 | 6 | Несоблюдение требований к размещению информаци… | Выше второго этажа | [выш, втор, этаж] |
1 | 2 | 5 | Неубранная городская территория | По адресу Снежная д24 расположена музыкальная … | [адрес, снежн, располож, музыкальн, школ, задн… |
2 | 0 | 7 | Неубранная городская территория | После проведения работ на кабельной канализаци… | [проведен, работ, кабельн, канализац, мгтс, за… |
3 | 2 | 5 | Неубранная городская территория | Очистите опору освещения. Приведите в надлежащ… | [очист, опор, освещен, привест, надлежа, состоян] |
4 | 0 | 6 | Захламление территории | Более двух недель лежит куча грунта в перемешк… | [два, недел, лежа, куч, грунт, перемешок, мусор] |
Визуализация
Для визуализации сначала решил построить график с помощью IncrementalPCA модуля sklearn.decomposition, однако картинка не дала интерпретируемых результатов:
Тогда я решил использовать dendrogram пакета scipy.cluster.hierarchy. Картинка получилась понятнее.
link_matrix=ward(tfidf_matrix[:1000])
fig, ax=plt.subplots(figsize=(15, 20))
ax=dendrogram(link_matrix, orientation="right", labels=sent_topics[:1000])
plt.tick_params(axis='x', which='both', bottom='off', top='off', labelbottom='off')
plt.tight_layout()
Тематическое моделирование
Далее переходим непосредственно к моделированию — готовим данные (корпус) для LDA модели.
distcorp = corpora.Dictionary(tokens)
corpus = [distcorp.doc2bow(text) for text in tokens]
и собираем ее:
lda_model = models.ldamodel.LdaModel(corpus=corpus,
num_topics=10,
id2word=distcorp,
random_state=5,
update_every=1,
#chunksize=10
passes=10,
#alpha='symmetric',
iterations=10,
per_word_topics=True)
Готово. Посмотрим первые 10 слов по первым темам:
lda_model.show_topics(num_topics=3, num_words=10, formatted=True)
[(0, '0.110*"мусор" + 0.077*"территор" + 0.052*"убра" + 0.025*"строительн" + 0.022*"газон" + 0.017*"куч" + 0.016*"убира" + 0.015*"захламлен" + 0.015*"бетон" + 0.014*"свалк"'),
(1, '0.105*"провер" + 0.102*"закон" + 0.089*"рекламн" + 0.089*"реклам" + 0.087*"прос" + 0.063*"конструкц" + 0.055*"размещен" + 0.032*"столб" + 0.029*"щит" + 0.023*"установк"'),
(2, '0.115*"дом" + 0.062*"улиц" + 0.046*"здан" + 0.042*"адрес" + 0.034*"сторон" + 0.025*"штендер" + 0.024*"фасад" + 0.023*"напрот" + 0.022*"прос" + 0.019*"надп"')]
Также была задача построить облако слов. Я сделал это с помощью wordcloud.
from wordcloud import WordCloud, STOPWORDS
cloud=WordCloud(stopwords=stopwords,
background_color="white",
width=2500,
height=1800,
max_words=10,
colormap='tab10',
color_func=lambda*args, **kwargs: cols[i],
prefer_horizontal=1.0)
topics_p=lda_model_mc.show_topics(formatted=False)
fig, axes = plt.subplots(5, 2, figsize=(10, 10), sharex=True, sharey=True)
for i, ax in enumerate(axes.flatten()):
fig.add_subplot(ax)
topic_words=dict(topics_p[i][1])
cloud.generate_from_frequencies(topic_words, max_font_size=300)
plt.gca().imshow(cloud)
plt.gca().set_title("Topic "+str(i), fontdict=dict(size=12))
plt.gca().axis('off')
Так, благодаря данному алгоритму, удалось оценить полноту проблемы, разделить жалобы на подтемы с помощью методов ML, оценить их значимость, а также визуализировать результат работы.