Обновлено: 10.04.2026
Как data-driven подход превращает «чутьё» в измеримую инженерию знаний.
Введение: почему ключевые слова больше не главное
Классическое SEO опирается на частотность запросов. Но что делать, если ваша ниша — узкий B2B, медицинские устройства или промышленная автоматизация? Вордстат показывает ноль, конкурентов не видно, а клиенты есть и они находят сайт по «странным» фразам.
В этой статье мы разберём метод, который позволяет увидеть семантическую структуру вашего сайта глазами машинного обучения — диаграммы рассеяния (scatter plots) на основе векторных представлений (эмбеддингов) текста.
Вы научитесь:
- превращать страницы сайта в точки на плоскости,
- отличать здоровую семантическую структуру от патологической,
- находить пробелы в контенте до того, как их заметят конкуренты,
- тематическая карта будет проектироваться на основе реальных данных, а не интуиции.
Для кого: SEO-специалисты, владеющие Python на уровне базовых скриптов и знакомые с понятиями «вектор», «кластеризация», «расстояние».
Часть 1. От текста к точке: эмбеддинги и снижение размерности
1.1 Почему слова — плохие координаты
Представьте, что каждая страница — это набор слов. Можно измерить близость страниц по пересечению слов (TF-IDF, Jaccard). Но такой подход не видит синонимов и смысловых связей: «насос» и «помпа» будут разнесены далеко, хотя означают одно и то же.
Эмбеддинги (векторные представления) решают эту проблему. Модель (например, sentence-transformers/all-MiniLM-L6-v2 или intfloat/e5-large) превращает любой текст в плотный вектор фиксированной длины — обычно 384 или 768 чисел. Эти числа кодируют семантику: близкие по смыслу тексты получают близкие векторы (по косинусному расстоянию).
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
pages = ["Насос КМ 80-50-200 для кислых сред",
"Уплотнение торцевое для агрессивных жидкостей",
"Правила утилизации бытовых отходов"]
embeddings = model.encode(pages) # форма (3, 384)
1.2 Проклятие размерности: зачем нужны UMAP и t-SNE
Вектор размерности 384 нельзя нарисовать на плоскости — у нас нет 384 глаз. Но для анализа нам нужна визуализация. Снижение размерности (UMAP, t-SNE) проецирует 384-мерные точки на 2D-плоскость, максимально сохраняя расстояния между ними.
Почему не PCA? PCA ищет линейные проекции с максимальной дисперсией, но семантические данные часто имеют нелинейную структуру (например, «рукава» кластеров). UMAP и t-SNE справляются лучше.
import umap reducer = umap.UMAP(n_components=2, random_state=42) embeddings_2d = reducer.fit_transform(embeddings) # форма (n, 2)
Важное предупреждение: UMAP и t-SNE не сохраняют глобальные расстояния. Две далёкие точки на графике действительно далеки в исходном пространстве, но обратное может быть неверным (близкие на графике могут быть не очень близки в оригинале). Для анализа используйте график как качественный инструмент, а для точных расчётов — метрики в исходном пространстве (косинусное расстояние).
1.3 Диаграмма рассеяния: что мы видим?
После проецирования мы получаем набор точек на плоскости. Каждая точка — одна страница (или фрагмент текста). Координаты абстрактны, но относительное расположение имеет смысл:
- Точки рядом → семантически похожие страницы.
- Точки далеко → разные темы.
- Сгущения → кластеры (группы страниц, посвящённых одной узкой теме).
- Выбросы → страницы, не вписывающиеся в общую структуру (возможно, требующие переработки или удаления).
Вот пример того, как может выглядеть сайт на диаграмме рассеивания:

Часть 2. Строим диаграмму для реального сайта
2.1 Подготовка данных
Что мы векторизуем? Целевые страницы:
- Категории товаров
- Карточки продуктов
- Услуги
- Статьи блога
- Посадочные страницы
Важно: Исключите служебные блоки (меню, футер, контакты) — они создают шум. Для каждой страницы возьмите:
- заголовок H1,
- содержимое (основной текст, характеристики),
- мета-описание (опционально).
Если страница слишком длинная (>2000 символов), разбейте её на смысловые фрагменты (по абзацам или заголовкам H2). Тогда одна точка = один фрагмент. Это позволит увидеть внутреннюю структуру длинной статьи.
import pandas as pd
from bs4 import BeautifulSoup
def extract_text_from_html(html, selector='body'):
soup = BeautifulSoup(html, 'html.parser')
# удаляем скрипты, стили, навигацию
for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
tag.decompose()
return soup.select_one(selector).get_text(separator=' ', strip=True)
2.2 Векторизация и снижение размерности (полный код)
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
import umap
import plotly.express as px
# 1. Загрузка модели
model = SentenceTransformer('intfloat/e5-large-v2') # лучше для технических ниш
# 2. Получаем тексты и метаданные (пример)
df = pd.read_csv('pages.csv') # колонки: url, title, content
texts = [f"title: {row['title']} content: {row['content']}"
for _, row in df.iterrows()]
# 3. Эмбеддинги
embeddings = model.encode(texts, show_progress_bar=True)
# 4. UMAP
reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, metric='cosine', random_state=42)
embeddings_2d = reducer.fit_transform(embeddings)
# 5. Визуализация с Plotly (интерактивная)
df['x'] = embeddings_2d[:, 0]
df['y'] = embeddings_2d[:, 1]
fig = px.scatter(df, x='x', y='y', hover_data=['url', 'title'],
title='Semantic map of the site')
fig.show()
Настройки UMAP, важные для SEO:
n_neighbors=15— сколько соседей учитывать при построении локальной структуры. Меньше (5–10) → больше локальных деталей, но сильнее разрывает кластеры. Больше (30–50) → глобальная структура, кластеры сливаются.min_dist=0.1— минимальное расстояние между точками на плоскости. Чем меньше, тем плотнее упаковка (хорошо для поиска подкластеров).metric='cosine'— обязательно, так как эмбеддинги нормализованы.
2.3 Добавляем кластеризацию на график
Одних глаз недостаточно — добавим цветовую маркировку алгоритмических кластеров (HDBSCAN). Это позволит увидеть, сколько естественных групп выделяет модель.
import hdbscan
clusterer = hdbscan.HDBSCAN(min_cluster_size=5, min_samples=2, metric='euclidean')
df['cluster'] = clusterer.fit_predict(embeddings_2d) # -1 = шум/выбросы
fig = px.scatter(df, x='x', y='y', color='cluster',
hover_data=['url', 'title'],
title='Semantic clusters (HDBSCAN)')
fig.show()
Что означают кластеры:
- Кластеры с номерами 0,1,2… — тематические группы.
- Кластер -1 — выбросы, страницы, которые не похожи ни на одну группу. Это либо уникальные важные страницы (например, «О компании»), либо мусор.
Часть 3. Диагностика семантического здоровья сайта
3.1 Паттерны дисперсии: что видит аналитик
А) Низкая дисперсия — «чёрная дыра»

Признаки: все точки в радиусе 20% от размаха, нет видимых подгрупп.
Диагноз: контент слишком однороден (шаблонные короткие описания) или модель не различает вашу специфику.
Лечение: увеличить длину и уникальность текстов, сменить эмбеддер на специализированный (например, deepvk/USER-bge-m3 для русского технического).
Б) Нормальная иерархическая дисперсия — «здоровый сад»

Признаки: несколько компактных групп, между ними заметные зазоры, внутри групп есть разброс.
Диагноз: структура сайта логична, эмбеддинги работают. Можно искать пробелы.
В) Высокая дисперсия — «рассыпавшееся ожерелье»

Признаки: точки разбросаны равномерно, нет явных кластеров.
Диагноз: либо страницы слишком разные и не объединены общей темой (сайт-каталог всего подряд), либо параметры UMAP/HDBSCAN выбраны неправильно (слишком высокое n_neighbors).
Лечение: проверить выборку страниц, возможно, вы включили несвязанные разделы.
3.2 Поиск семантических пробелов в контенте
Это ключевой метод для ниш без статистики.
Идея: внутри одного кластера (по мнению HDBSCAN) две точки могут быть довольно далеко друг от друга. Если между ними нет других точек — значит, между соответствующими темами нет промежуточного контента. Это и есть пробел.
from sklearn.metrics.pairwise import cosine_distances
# Для каждого кластера (кроме -1)
for cluster_id in df[df['cluster'] != -1]['cluster'].unique():
cluster_points = df[df['cluster'] == cluster_id][['x', 'y']].values
if len(cluster_points) < 3:
continue
# Вычисляем попарные расстояния в 2D (или лучше в исходных эмбеддингах)
dist_matrix = cosine_distances(cluster_points)
# Находим пару с максимальным расстоянием
i, j = np.unravel_index(np.argmax(dist_matrix), dist_matrix.shape)
gap_distance = dist_matrix[i, j]
if gap_distance > 0.7: # порог подбирается экспериментально
page_a = df[df['cluster'] == cluster_id].iloc[i]
page_b = df[df['cluster'] == cluster_id].iloc[j]
print(f"Gap in cluster {cluster_id}: {page_a['title']} <-> {page_b['title']}")
print(f"Distance: {gap_distance:.2f}")
Что делать с найденными парами: создайте страницу, которая связывает эти две темы. Например, если одна страница — «Насос КМ для воды», другая — «Уплотнения для кислот», пробелом будет «Уплотнения для насоса КМ в кислых средах».
3.3 Оценка полноты покрытия темы
Постройте диаграмму рассеяния для вашего сайта и для «идеальной» тематической карты (если бы у вас был контент по всем подтемам). Где пустоты? Сравните с данными конкурентов (соберите их страницы, векторизуйте и сопоставьте с вашим графиком). Там, где у конкурентов есть точки, а у вас нет — потенциальные темы для новых страниц.
Часть 4. Проектирование тематических карт на основе диаграмм рассеивания
4.1 От кластеров к рубрикатору
Каждый кластер на графике — это кандидат в категорию верхнего уровня или в столповую страницу (хаб). Внутри кластера видны подгруппы — это подкатегории.
Практический алгоритм:
- Выделите кластеры HDBSCAN (параметры подбирайте так, чтобы получилось 5–15 кластеров).
- Для каждого кластера выберите «центроидную» страницу (ближайшую к геометрическому центру) — она станет хабом (pillar page, портальной страницей).
- Для каждой точки внутри кластера определите, является ли она уже существующей страницей или это пробел.
- Спроектируйте структуру URL: /topic/cluster/subtopic.
4.2 Динамические тематические карты с перелинковкой
Зная координаты страниц, можно рекомендовать автоматические связи. Правило: если две страницы находятся на расстоянии меньше порога (например, 0.3 в косинусной метрике исходных эмбеддингов), но между ними нет ссылки — добавьте взаимную перелинковку.
from sklearn.neighbors import NearestNeighbors
# В исходном пространстве эмбеддингов
nn = NearestNeighbors(n_neighbors=6, metric='cosine')
nn.fit(embeddings)
distances, indices = nn.kneighbors(embeddings)
for i, neighbors in enumerate(indices):
for j in neighbors[1:]: # пропускаем саму страницу
if distances[i, j] < 0.3:
print(f"Link {df.iloc[i]['url']} -> {df.iloc[j]['url']}")
4.3 Визуализация как артефакт для клиента
Клиенты, которые «не могут объяснить, что продают», отлично понимают картинки. Постройте интерактивный scatter plot с цветовыми кластерами и подписями. Покажите пустые области — это «семантический долг». Предложите план наполнения: каждая новая точка должна заполнять конкретную пустоту.
Часть 5. Ограничения и подводные камни
5.1 Ложные разрывы из-за бедных эмбеддингов
Если страницы слишком короткие (<300 символов) или шаблонные, эмбеддинги будут почти одинаковыми — низкая дисперсия. Вы получите много ложноположительных разрывов (или, наоборот, ни одного). Решение: предварительно обогатить контент (добавить описания, характеристики, контекст).
5.2 Влияние стоп-слов и шумовых блоков
Если на всех страницах повторяется один и тот же блок («Заказать звонок», «Мы работаем с 2000 года»), эмбеддинги «схлопнутся» к этому общему знаменателю. Решение: перед векторизацией удалять повторяющиеся блоки с помощью алгоритмов типа difflib или просто вырезать меню/футер.
5.3 Выбор модели эмбеддингов
Универсальные модели (all-MiniLM-L6-v2) хорошо работают для новостей и общих текстов. Для технических, юридических или медицинских ниш используйте специализированные:
- Русский: intfloat/e5-large (англ., но с поддержкой русских субтитров), deepvk/USER-bge-m3.
- Английский: BAAI/bge-large-en-v1.5, intfloat/e5-large-v2.
- Мультиязычный: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2.
5.4 Стабильность UMAP
UMAP стохастичен. Запустите его 5 раз с разными random_state, чтобы убедиться, что основные паттерны повторяются. Для production-анализа используйте фиксированный seed.
Заключение: от визуализации к инженерии знаний
Диаграммы рассеяния на основе эмбеддингов превращают SEO из «угадай ключевое слово» в измеримую инженерию семантического пространства. Вы перестаёте гадать, какие страницы нужны — вы видите дыры в вашей карте знаний.
Чек-лист внедрения:
- Собрать все значимые страницы сайта (минимум 50–100).
- Очистить текст от шаблонных блоков.
- Векторизовать (выбрать модель под нишу).
- Снизить размерность (UMAP, n_neighbors=15, min_dist=0.1).
- Кластеризовать (HDBSCAN, min_cluster_size=3-5).
- Построить scatter plot с цветными кластерами.
- Найти пары с максимальным внутрикластерным расстоянием → гипотезы о пробелах.
- Спроектировать новые страницы и перелинковку.
- Через 2–3 месяца повторить анализ.
Не пытайтесь автоматизировать всё. Визуализация — инструмент для генерации гипотез, а не истина в последней инстанции. Самые ценные инсайты рождаются, когда вы смотрите на график и вдруг замечаете: «Почему эти две точки так далеко, ведь они оба про уплотнения? Ах, в одном тексте сказано “сальник”, а в другом “манжета” — нужно добавить синоним в контент». Это и есть data-driven SEO в действии.


