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

Основано на лабораторке 2022 года (мне слишком нравился предмет, так что пора применить знания). Профессиональные аналитики: лучше закройте глаза.
Сбор текстовых данных
Сбор текстовых данных является важным этапом в процессе анализа и обработки естественного языка. Эти данные могут быть получены из различных источников, таких как социальные сети, блоги, новостные статьи, отзывы пользователей и многие другие.
Парсинг
Для парсинга текстов постов из Telegram-канала в Google Colab с использованием Python, использовала библиотеку Telethon
. Она позволяет взаимодействовать с Telegram API как обычный пользователь, что дает возможность читать сообщения из каналов (в том числе приватных, если ваш аккаунт имеет к ним доступ).
Для работы с Telegram API вам потребуются api_id
и api_hash
.
Перейдите на сайт my.telegram.org.
Войдите в свой аккаунт Telegram.
Нажмите "API development tools".
Создайте новое приложение (любое название и описание подойдут).
Вы получите
App api_id
иApp api_hash
. Скопируйте их.
Код для подключения к Google Drive и Telegram
import os
from google.colab import drive
drive.mount('/content/drive')
session_path = '/content/drive/MyDrive/telethon_sessions/'
os.makedirs(session_path, exist_ok=True)
session_name = 'my_telegram_session'
session_file_path = os.path.join(session_path, session_name)
print(f"Файл сессии будет сохранен по пути: {session_file_path}.session")
!pip install telethon
from telethon.sync import TelegramClient
from telethon.tl.types import Channel
# !!! ЗАМЕНИТЕ НА ВАШИ API ID и API Hash !!!
api_id = 0
api_hash = ''
client = TelegramClient(session_file_path, api_id, api_hash)
try:
await client.start()
except Exception as e:
if not client.is_user_authorized():
phone_number = input('Введите ваш номер телефона (с кодом страны, например +79XXXXXXXXX): ')
await client.start(phone=phone_number)
print(f"Авторизация успешна как: {(await client.get_me()).first_name} {(await client.get_me()).last_name}")
# client.disconnect() # Отключение после парсинга
Так как постов много и каждый раз устанавливать сессию затратно, то запишем результаты парсинга в файл и сохраним на диске. Также можно поставить лимит на количество читаемых постов. Таким образом, у нас получится файл с данными для дальнейшего анализа.
import json
async def get_channel_posts_text(channel_identifier):
posts_text = []
try:
entity = await client.get_entity(channel_identifier)
if not isinstance(entity, (Channel)):
return []
async for message in client.iter_messages(entity, limit=None):
if message.text:
posts_text.append(message.text)
print(f"Всего получено {len(posts_text)} текстовых постов из канала '{entity.title}'.")
return posts_text
except Exception as e:
return []
channel_username = '@m0rtymerr_channel'
parsed_posts = await get_channel_posts_text(channel_username)
if parsed_posts:
safe_channel_name = channel_username.replace('@', '')
output_file_name = f"{safe_channel_name}_posts.json"
output_file_path = os.path.join(session_path, output_file_name)
with open(output_file_path, 'w', encoding='utf-8') as f:
json.dump(parsed_posts, f, ensure_ascii=False, indent=4)
Добавлю код для чтение из файла:
Код
!pip install numpy==1.26.4 'pandas<1.8'
# ПЕРЕЗАПУСТИТЬ
import pandas as pd
import re
import numpy as np
from matplotlib import pyplot as plt
channel_username = '@m0rtymerr_channel'
safe_channel_name = channel_username.replace('@', '')
output_file_name = f"{safe_channel_name}_posts.json"
output_file_path = os.path.join(session_path, output_file_name)
df=pd.read_json(output_file_path)
df.rename(columns={0:'text'}, inplace = True )
df.head()
Предобработка собранных данных
Предобработка данных — это ключевой этап, который помогает подготовить текстовую информацию к дальнейшему анализу. Этот процесс включает несколько шагов, таких как:
приведение к нижнему регистру
удаление эмодзи, цифр, знаков препинания
удаление стоп-слов
для анализа русскоязычного сообщества можно удалить англицизмы
Отдельно стоит отметить токенизацию и лемматизацию. Вместе они помогают структурировать и упрощать данные, что является необходимым для успешного выполнения множества задач в области обработки естественного языка, таких как анализ тональности, тематическое моделирование, машинный перевод и многие другие.
Токенизация — это процесс разделения текста на отдельные элементы, называемые токенами. Токены могут быть словами, фразами или символами, в зависимости от задачи анализа. Этот шаг необходим для дальнейшей обработки текста и позволяет работать с отдельными единицами информации.
Лемматизация — это процесс приведения слов к их базовой форме (лемме). Например, слова "бегу", "бежит" и "бегал" будут приведены к лемме "бежать". Лемматизация помогает уменьшить количество уникальных слов в тексте и улучшает качество анализа, так как разные формы одного и того же слова будут рассматриваться как одно и то же слово.

Код для предобработки данных
Нижний регистр
df["tokens"] = df.text.apply(str.lower)
Удаление эмоджи
!pip install emoji
import emoji
df['tokens'] = df['tokens'].apply(lambda s: emoji.replace_emoji(s, ''))
Удаление цифр
df['tokens'] = df['tokens'].apply(lambda s: re.sub(r"\d+", "", s, flags=re.UNICODE))
Токенизация
!pip install razdel
from razdel import tokenize
def get_tokens(sentence):
return [_.text.strip() for _ in tokenize(sentence)]
df["tokens"] = df.tokens.apply(get_tokens)
Удаление пунктуации
import string
df["tokens"] = df.tokens.apply(lambda row: [token for token in row if token not in string.punctuation + string.digits + '...'+'—'+'»'+'«'+"–" + '**' + '⁃' + '=)'+'•'+'-'])
df["tokens"] = df["tokens"].apply(lambda row: [token for token in row if not re.match(r'^-+$', token)])
Удаление английских слов
df['tokens'] = df['tokens'].apply( lambda row: [ token for token in row if token == "it" or not re.search(r"[a-zA-Z]+", token)])
Лемматизация
!pip install pymorphy3 pymorphy3-dicts-ru pymorphy2
import pymorphy2
from tqdm.notebook import tqdm
# ну... бывает
def pymorphy2_311_hotfix():
from inspect import getfullargspec
from pymorphy2.units.base import BaseAnalyzerUnit
def _get_param_names_311(klass):
if klass.__init__ is object.__init__:
return []
args = getfullargspec(klass.__init__).args
return sorted(args[1:])
setattr(BaseAnalyzerUnit, '_get_param_names', _get_param_names_311)
pymorphy2_311_hotfix()
analyzer = pymorphy2.MorphAnalyzer()
df["tokens"] = tqdm(df.tokens.apply(lambda row: [analyzer.parse(token)[0].normal_form for token in row if token]))
Удаление стоп слов
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
nltk.download('punkt')
additional_stops=["это", "который", "наш", "мочь", "год","такой", "мы", "свой", "один", "другой", "человек", "всё", "все", "весь", "очень", "каждый", "день", "её", "ваш", "ваше", "день", "самый", "ещё","также", "нужно","например", "вещь", "хороший", 'новый', "спасибо", 'твой', 'любой','что-то','че','какой-то','какой-то', 'привет', 'час', 'месяц','неделя','сегодня']
om_stops=['сообщество', 'ом', 'антон','назаров']
stops = list(string.ascii_lowercase) + list('абвгдеёжзийклмнопрстуфхцчшщъыьэюя') + stopwords.words("russian") + om_stops + additional_stops
df["tokens"] = df.tokens.apply(lambda row: [token for token in row if token not in stops])
Для дальнейшего использования
all_words = []
for doc in df.tokens.tolist():
all_words.extend(doc)
Тематическое моделирование
Тематическое моделирование — это важный подход в NLP, который позволяет выявлять скрытые структуры в больших объемах текстовых данных. С его помощью можно автоматически идентифицировать темы, которые присутствуют в текстах, что особенно полезно для анализа больших коллекций документов, таких как статьи, блоги, отзывы и другие источники информации.
Одним из наиболее распространенных методов тематического моделирования является латентное размещение Дирихле (LDA). Этот алгоритм основывается на предположении, что каждый документ может быть представлен как смесь нескольких тем, а каждая тема характеризуется распределением слов. LDA позволяет исследователям и аналитикам не только находить основные темы в текстах, но и оценивать их значимость и взаимосвязи.
Также можно использовать метрики согласованности для оценки качества обнаруженных тем. Будем варьировать количество тем и анализировать, как это влияет на согласованность модели.

Если согласованность повышается или понижается с увеличением количества тем, это может свидетельствовать о наличии корреляции между этими двумя переменными. Здесь же корреляция не сильно заметна. Так что возьмём 10 тем: при таком количестве нужное значение максимально.
Полный код с построением графика
import gensim.corpora as corpora
from gensim.models import LdaMulticore, CoherenceModel
from tqdm import tqdm
import warnings
import matplotlib.pyplot as plt
id2word = corpora.Dictionary(df.tokens.tolist())
texts = df.tokens.tolist()
corpus = [id2word.doc2bow(text) for text in texts]
warnings.filterwarnings("ignore")
def compute_coherence_values(dictionary, corpus, texts, limit, start=2, step=3):
coherence_values = []
model_list = []
for num_topics in tqdm(range(start, limit, step)):
model=LdaMulticore(corpus=corpus,id2word=dictionary, num_topics=num_topics)
model_list.append(model)
coherencemodel = CoherenceModel(model=model, texts=texts, dictionary=dictionary, coherence='c_v')
coherence_values.append(coherencemodel.get_coherence())
return model_list, coherence_values
model_list, coherence_values = compute_coherence_values(dictionary=id2word, corpus=corpus, texts=texts, start=2, limit=20, step=1)
limit=20; start=2; step=1;
x = range(start, limit, step)
plt.figure(facecolor='black')
ax = plt.gca()
ax.set_facecolor('black')
plt.plot(x, coherence_values, color='#DEB040')
plt.xlabel("Количество тем", color='white')
plt.ylabel("Согласованность", color='white')
plt.tick_params(labelcolor='white')
plt.grid(color='white', linestyle='--', linewidth=0.5)
plt.title("График согласованности", color='white')
plt.show()
Когда определились с количеством тем, обучаем модель LDA на нашем корпусе текстов и затем используем pyLDAvis для подготовки интерактивной визуализации тем. Эта визуализация позволяет исследовать темы, их ключевые слова и взаимосвязи между ними, что значительно упрощает интерпретацию результатов тематического моделирования.
lda_model = LdaMulticore(corpus=corpus, id2word=id2word, num_topics=n_topics)
pyLDAvis.enable_notebook()
LDAvis_prepared = pyLDAvis.gensim.prepare(lda_model, corpus, id2word)
LDAvis_prepared

На первый взгляд очень много одинаковых слов в темах, но на интерактивном графике (тут только картинка), можно посмотреть отдельно на слова и узнать в каких темах они чаще используются. Можно исследовать, как темы связаны друг с другом. Есть ли темы, которые часто появляются вместе?
Потыкав на графике я видела темы собеседований, менторов, накрутку, выход роликов, интервью, всякое странное о жизни. Но в самом большом кружке очень много пересечений. Точный анализ не совсем получился.
Полный код с построением графика
!pip install matplotlib pyLDAvis==2.1.2
from gensim.models import LdaMulticore, CoherenceModel
import pyLDAvis.gensim
import pickle
import pyLDAvis
import os
n_topics= 10
lda_model = LdaMulticore(corpus=corpus, id2word=id2word, num_topics=n_topics)
pyLDAvis.enable_notebook()
LDAvis_prepared = pyLDAvis.gensim.prepare(lda_model, corpus, id2word)
LDAvis_prepared
Анализ тональности текста
Анализ тональности — это процесс определения эмоциональной окраски текста, который позволяет классифицировать его как положительный, отрицательный или нейтральный. В последние годы этот метод стал особенно актуален в условиях быстрого роста объемов текстовых данных, генерируемых пользователями в социальных сетях, отзывах, новостях и других источниках.
С помощью анализа тональности компании и исследователи могут извлекать ценные инсайты о чувствах потребителей, что способствует более эффективному принятию решений. Например, бренды могут отслеживать общественное мнение о своих продуктах, выявлять проблемы на ранних стадиях и адаптировать свои маркетинговые стратегии.
В 2022 году мы в универе использовали библиотеку dostoevsky. Однако в связи с изменениями в поддержке и доступности библиотеки, я решила попробовать библиотеку Transformers
от Hugging Face.
from transformers import pipeline
sentiment_pipeline = pipeline("sentiment-analysis", model="blanchefort/rubert-base-cased-sentiment", tokenizer="blanchefort/rubert-base-cased-sentiment")
sentiment_pipeline('Эта статья прекрасна!')

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

Тут я решила проследить динамику изменения количества негативных\позитивных сообщений. Суммировала по 100 штук. Видно, что есть периоды, где наблюдаются скачки. Но сейчас агрессивный маркетинг количество отрицательных постов уменьшается.
Полный код с построением графика
from transformers import pipeline
sentiment_pipeline = pipeline("sentiment-analysis", model="blanchefort/rubert-base-cased-sentiment", tokenizer="blanchefort/rubert-base-cased-sentiment")
tokens = df.tokens.tolist()
messages = [" ".join(inner_list) for inner_list in tokens]
sentiment = [item['label'] for item in sentiment_pipeline(messages)]
vis = pd.DataFrame(pd.Series(sentiment).value_counts()).reset_index()
vis.columns = ['sentiment', 'counts']
vis=vis.sort_values(by="counts")
vis_res=vis.drop([0]) # убираем нейтральные сообщения
colors = ["#DEB040", "yellow"]
plt.figure(facecolor='black')
ax = plt.gca()
ax.set_facecolor('black')
plt.tick_params(labelcolor='white')
plt.grid(color='white', linestyle='--', linewidth=0.5)
plt.title("Анализ тональности (кол-во сообщений)", color='white')
plt.barh(y=vis_res.sentiment, width=vis_res.counts, color=colors)
plt.show()
Для второго графика
from matplotlib.ticker import FuncFormatter
positive_counts = []
negative_counts = []
step = 100
for i in range(0, len(sent), step):
inner_list = sent[i:i + step]
positive_count = inner_list.count('POSITIVE')/len(inner_list)*100
negative_count = inner_list.count('NEGATIVE')/len(inner_list)*100
positive_counts.append(positive_count)
negative_counts.append(negative_count)
indices = np.arange(len(sent) / step)
plt.figure(facecolor='black')
ax = plt.gca()
ax.set_facecolor('black')
plt.tick_params(labelcolor='white')
plt.title("Анализ тональности (подсчет по 100 сообщений)", color='white')
ax.set_xticklabels([])
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f'{int(x)}%'))
plt.plot(indices, positive_counts, label='Positive', color='#DEB040')
plt.plot(indices, negative_counts, label='Negative', color='yellow')
plt.xticks(indices)
plt.legend(facecolor='black', labelcolor='white')
plt.show()
Частотный анализ
По своей сути, частотный анализ — это процесс подсчета количества вхождений (частоты) отдельных слов, фраз или символов в заданном текстовом корпусе. Этот, казалось бы, простой метод открывает двери для глубокого понимания структуры и содержания текста. Он позволяет быстро выявить ключевые слова, доминирующие темы, а также необычные или повторяющиеся паттерны, которые могут быть неочевидны при поверхностном чтении.
Частотный анализ служит отправной точкой для множества прикладных задач: от определения наиболее релевантных терминов для SEO-оптимизации до лингвистических исследований.
Для начала можно просто посмотреть самые популярные слова:
res = pd.DataFrame(all_words, columns=["terms"])
vis = pd.DataFrame(res.terms.value_counts()).reset_index()
vis.columns = ['terms', 'counts']
vis=vis.sort_values(by="counts")[-10:]
colors = ["#DEB040" for _ in range(9)]+["yellow"]
plt.figure(facecolor='black')
ax = plt.gca()
ax.set_facecolor('black')
plt.tick_params(labelcolor='white')
plt.barh(y=vis.terms, width=vis.counts, color=colors)
plt.grid(color='white', linestyle='--', linewidth=0.5)
plt.title("Частотный анализ", color='white')
plt.show()

Работа, вопрос, опыт, it, стая - то, о чем говорят в сообществе. Также нас учили делать акценты на графиках, так что можно выделить первое слово особенно ярко.
Также можно построить облака слов — это визуальное представление текстовых данных, где частота слов в тексте отображается их размером: чем чаще слово встречается, тем крупнее оно отображается. Несмотря на кажущуюся простоту, облака слов делают по нескольким причинам:
быстрый обзор и понимание основной темы
визуализация и привлекательность
доступность и простота для неспециалистов
сравнение текстов
инструмент для предварительного анализа
вовлечение аудитории

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

Код для облаков слов
!pip install wordcloud
from collections import Counter
from wordcloud import WordCloud
from PIL import Image
import requests
from io import BytesIO
import numpy as np
import matplotlib.pyplot as plt
words = dict(Counter(all_words))
url='/content/wolf.png'
if os.path.exists(url):
image_pil = Image.open(url)
cloud_mask = np.array(image_pil)
wc = WordCloud(background_color="black", max_words=200, mask=cloud_mask, colormap="Wistia_r")
wc.generate_from_frequencies(words)
plt.figure(figsize=(10, 10))
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.show()
Облако слов по частям речи
def get_words(functors_pos):
morph=pymorphy2.MorphAnalyzer()
words=[]
for word in all_words:
if morph.parse(word)[0].tag.POS in functors_pos:
words.append(word)
words = dict(Counter(words))
return words
noun = get_words({'NOUN'})
adjf = get_words({'ADJF', 'ADJS'})
verb = get_words({'VERB', 'INFN'})
url = "https://www.pinclipart.com/picdir/middle/560-5604427_money-bag-clip-art-png-download.png"
response = requests.get(url)
cloud_mask = np.array(Image.open(BytesIO(response.content)))
wc = WordCloud(background_color="black", max_words=200, mask=cloud_mask, colormap="Wistia_r")
wc.generate_from_frequencies(words)
wc.generate_from_frequencies(verb)
fig, axs = plt.subplots(nrows= 1 , ncols= 3, figsize=(20, 20))
axs[0].imshow(wc, interpolation="bilinear")
axs[0].axis("off")
wc.generate_from_frequencies(adjf)
axs[1].imshow(wc, interpolation="bilinear")
axs[1].axis("off")
wc.generate_from_frequencies(noun)
axs[2].imshow(wc, interpolation="bilinear")
axs[2].axis("off")
Граф
Использование графов для анализа текстовых данных позволяет не только визуализировать связи между словами, но и выявлять скрытые паттерны в текстах. Такой подход может быть полезен в различных областях, включая лингвистику, маркетинг и социальные исследования.
В данном примере использовала библиотеку NetworkX
для создания графа, где узлы представляют собой слова, а рёбра — связи между ними. Будем считать, что между двумя словами есть связь, если они встретились в одном тексте. Будем использовать все части речи, но опять же, можно построить граф только с существительными, например.

В центре видим слова, которые уже видели в частотном анализе. Очень много связей у слов собес - собеседование, работать - работуа. Также есть ветки посвященные качеству видео, подписке. Много мелочи около слова вопрос. Отдельные темы выделенные уходящими ветками: истории успеха, зарплатный рост, комментарии, уровни подписок.
Код для построения графа
!pip install networkx">=2.5"
from collections import Counter
import networkx as nx
import sys
import matplotlib.pyplot as plt
import matplotlib
pairs = []
for doc in df.tokens.tolist():
if doc:
b = list((nltk.bigrams(doc)))
if b:
pairs.extend(b)
pairs = [tuple(sorted(pair)) for pair in pairs]
word_pairs = dict(Counter(pairs))
word_pairs = [(pair[0], pair[1], val) for pair, val in word_pairs.items() if val > 5]
G = nx.Graph()
edges = word_pairs
G.add_weighted_edges_from(edges)
# Удаляем узлы с степенью менее 2
remove = [node for node, degree in dict(G.degree()).items() if degree < 2]
G.remove_nodes_from(remove)
# Удаляем рёбра, которые соединяют удаленные узлы
remove_edge = [pair for pair in G.edges() if pair[0] in remove and pair[1] in remove]
G.remove_edges_from(remove_edge)
# Удаляем петли
remove = [pair for pair in G.edges() if pair[0] == pair[1]]
G.remove_edges_from(remove)
node_sizes = [deg*50 for node, deg in dict(G.degree()).items()]
# Строим график
plt.figure(figsize=(40,40))
pos = nx.layout.spring_layout(G)
edges, weights = zip(*nx.get_edge_attributes(G,'weight').items())
for key in pos:
x, y = pos[key]
plt.text(x,y,key, ha="center", va="center", fontsize=6)
nx.draw(G, pos, node_color='#DEB040', edgelist=list(G.edges()), edge_color=range(len(G.edges())), width=1.0, with_labels=False, edge_cmap=plt.cm.Blues, node_size=node_sizes)
plt.show()
Граф с узлами цвета тем
def get_color(node):
color_map = {
9: '#526DC0',
8: '#FFFACD',
7: '#FFB6C1',
6: '#98FF98',
5: '#FFDAB9',
4: '#E6E6FA',
3: '#FFE5B4',
2: '#F5F5DC',
1: '#D3D3D3',
0: '#ADD8E6'
}
for topic_index, color in color_map.items():
if node in topics[topic_index][1]:
return color
return '#FFFFF0'
topics=lda_model.print_topics(num_words=100)
color_map = [get_color(node) for node in G]
plt.figure(figsize=(30,30))
edges, weights = zip(*nx.get_edge_attributes(G,'weight').items())
nx.draw(G, pos, edgelist=list(G.edges()), edge_color=range(len(G.edges())), width=1.0, with_labels=False, edge_cmap=plt.cm.Blues, node_size=node_sizes, node_color=color_map)
for key in pos:
x, y = pos[key]
plt.text(x,y,key, ha="center", va="center", fontsize=6)
plt.show()
Конец
P.S.
Я не упоминала ОМ всуе
Комментарии (9)
edta_ff
10.06.2025 16:17Вот бы еще рассказать, что за волки подразумеваются, чтобы не пришлось гуглить.
DuckyMomo
10.06.2025 16:17На этом самом свободном ресурсе в мире имя сие называть нельзя, а так же упоминать о его деятельности и компаниях. :D
dyadyaSerezha
10.06.2025 16:17Аналогично. В названии и в тегах волки, а в тексте и примерах графиков вообще ни одного. Прям нехорошо как-то. Так серьёзные статьи не пишут.
dan_sw
10.06.2025 16:17Так серьёзные статьи не пишут.
По моему, автор и не претендовал на "серьёзную статью". Она вышла довольно интересной и захватывающей, но при этом она не претендует на "серьёзность". Хабр это всё-таки не про научные исследования и искать здесь "академическую серьёзность" (если Вы об этом) я бы не стал (т.к. таких публикаций здесь очень мало). Лучше почитать профильные журналы по интересующим темам.
RustamKuramshin
С телефона открыл. Сначала подумал статья про анализ резюме разработчиков. А потом так почти так и оказалось