Для анализа существующей модели предоставления разрешений в информационной системе (далее ИС) предлагается провести кластеризацию данных, используемых для предоставления разрешений. В кластеризации может принять участие около 100 числовых и текстовых параметров, представляющих собой категориальные признаки с количеством категорий от 2 до нескольких тысяч. Далее представлена основа выполнения кластеризации данных с большим количеством текстовых и числовых показателей на Python.
Кластерный анализ предлагается проводить методом DBSCAN. В целях этого кластерного анализа совместно с DeepSeek был разработан код генерации ролевой модели и модели предоставления разрешений в Tessa.
Кластерный анализ модели предоставления разрешений в Tessa¶
Для анализа существующей модели предоставления разрешений в информационной системе (далее ИС) предлагается провести кластеризацию данных, используемых для предоставления разрешений. В кластеризации может принять участие около 100 числовых и текстовых параметров, представляющих собой категориальные признаки с количеством категорий от 2 до нескольких тысяч. Далее представлена основа выполнения кластеризации данных с большим количеством текстовых и числовых показателей на Python.
Кластерный анализ предлагается проводить методом DBSCAN. В целях этого кластерного анализа совместно с DeepSeek был разработан код генерации ролевой модели и модели предоставления разрешений в Tessa.
Кластерный анализ тестовой модели предоставления разрешений¶
Загрузка библиотек¶
# Загрузка библиотек
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.neighbors import NearestNeighbors
from sklearn.cluster import DBSCAN
from sklearn.metrics import silhouette_score, silhouette_samples
import matplotlib.pyplot as plt
Загрузка и предобработка данных¶
# Загрузка и подготовка данных
data = pd.read_csv('df_permission.csv', encoding='utf-8-sig', sep=';')
print('Анализ данных:')
print(f'Размер: {data.shape}')
data.head(3)
Анализ данных: Размер: (756, 28)
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Права супермодератора | Подписка на уведомления | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 1 | Смирнов Иван | Сотрудник | 0 | Входящий документ | На согласовании | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Утвержден | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
3 rows × 28 columns
# Преобразуем все в строки и заполняем пропуски
data = data.astype(str).fillna('Unknown')
# Убираем логически не нужные поля
#data = data.drop(['rule_id', 'rule_name', 'role_id', 'document_type_id', 'condition'], axis=1)
# Удаляем полные дубликаты объектов
data = data.drop_duplicates()
print(data.info())
print()
display(data.describe())
print()
data.head(5)
<class 'pandas.core.frame.DataFrame'> Index: 624 entries, 0 to 755 Data columns (total 28 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Название роли 624 non-null object 1 Тип роли 624 non-null object 2 Скрытая роль 624 non-null object 3 Тип карточки 624 non-null object 4 Состояние карточки 624 non-null object 5 Создание карточки 624 non-null object 6 Чтение карточки 624 non-null object 7 Редактирование карточки 624 non-null object 8 Удаление карточки 624 non-null object 9 Добавление файлов 624 non-null object 10 Редактирование всех файлов 624 non-null object 11 Удаление всех файлов 624 non-null object 12 Редактирование собственных файлов 624 non-null object 13 Удаление собственных файлов 624 non-null object 14 Инициация типового процесса отправки задач 624 non-null object 15 Ручное редактирование номера 624 non-null object 16 Редактирование маршрута 624 non-null object 17 Полный пересчет маршрута 624 non-null object 18 Права супермодератора 624 non-null object 19 Подписка на уведомления 624 non-null object 20 Расширенные настройки прав доступа 624 non-null object 21 Создание шаблона и копирование 624 non-null object 22 Пропуск этапов 624 non-null object 23 Редактирование своих сообщений 624 non-null object 24 Редактирование всех сообщений 624 non-null object 25 Добавление обсуждений 624 non-null object 26 Чтение обсуждений 624 non-null object 27 Чтение и отправка сообщений 624 non-null object dtypes: object(28) memory usage: 141.4+ KB None
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Права супермодератора | Подписка на уведомления | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 624 | 624 | 624 | 624 | 624 | 624 | 624 | 624 | 624 | 624 | … | 624 | 624 | 624 | 624 | 624 | 624 | 624 | 624 | 624 | 624 |
| unique | 37 | 3 | 1 | 5 | 4 | 2 | 1 | 2 | 2 | 2 | … | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
| top | Смирнов Иван | Сотрудник | 0 | Договор | Проект | 1 | 1 | 1 | 0 | 1 | … | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 |
| freq | 56 | 500 | 624 | 152 | 156 | 395 | 624 | 401 | 370 | 476 | … | 424 | 554 | 424 | 312 | 424 | 481 | 424 | 470 | 567 | 566 |
4 rows × 28 columns
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Права супермодератора | Подписка на уведомления | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 1 | Смирнов Иван | Сотрудник | 0 | Входящий документ | На согласовании | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Утвержден | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 3 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Архив | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 4 | Смирнов Иван | Сотрудник | 0 | Договор | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
5 rows × 28 columns
# Анализ уникальных значений
print("Уникальные значения по полям:")
for col in data.columns:
unique_count = data[col].nunique()
print(f"{col}: {unique_count} уникальных значений")
Уникальные значения по полям: Название роли: 37 уникальных значений Тип роли: 3 уникальных значений Скрытая роль: 1 уникальных значений Тип карточки: 5 уникальных значений Состояние карточки: 4 уникальных значений Создание карточки: 2 уникальных значений Чтение карточки: 1 уникальных значений Редактирование карточки: 2 уникальных значений Удаление карточки: 2 уникальных значений Добавление файлов: 2 уникальных значений Редактирование всех файлов: 2 уникальных значений Удаление всех файлов: 2 уникальных значений Редактирование собственных файлов: 2 уникальных значений Удаление собственных файлов: 2 уникальных значений Инициация типового процесса отправки задач: 2 уникальных значений Ручное редактирование номера: 2 уникальных значений Редактирование маршрута: 2 уникальных значений Полный пересчет маршрута: 2 уникальных значений Права супермодератора: 2 уникальных значений Подписка на уведомления: 2 уникальных значений Расширенные настройки прав доступа: 2 уникальных значений Создание шаблона и копирование: 2 уникальных значений Пропуск этапов: 2 уникальных значений Редактирование своих сообщений: 2 уникальных значений Редактирование всех сообщений: 2 уникальных значений Добавление обсуждений: 2 уникальных значений Чтение обсуждений: 2 уникальных значений Чтение и отправка сообщений: 2 уникальных значений
# Упрощенное кодирование категориальных признаков
def simple_category_encoding(data):
"""Упрощенное кодирование категориальных признаков"""
encoded_data = pd.DataFrame()
for col in data.columns:
unique_count = data[col].nunique()
if unique_count <= 20:
# One-Hot для малого количества категорий
dummies = pd.get_dummies(data[col], prefix=col)
encoded_data = pd.concat([encoded_data, dummies], axis=1)
else:
# Frequency encoding для большого количества категорий
freq_map = data[col].value_counts().to_dict()
encoded_data[col] = data[col].map(freq_map)
return encoded_data
print("Кодирование признаков...")
encoded_data = simple_category_encoding(data)
print(f"После кодирования: {encoded_data.shape} признаков")
encoded_data.head(5)
Кодирование признаков... После кодирования: (624, 59) признаков
| Название роли | Тип роли_Подразделение | Тип роли_Сотрудник | Тип роли_Статическая роль | Скрытая роль_0 | Тип карточки_Входящий документ | Тип карточки_Договор | Тип карточки_Приказ | Тип карточки_Служебная записка | Тип карточки_Счет | … | Редактирование своих сообщений_0 | Редактирование своих сообщений_1 | Редактирование всех сообщений_0 | Редактирование всех сообщений_1 | Добавление обсуждений_0 | Добавление обсуждений_1 | Чтение обсуждений_0 | Чтение обсуждений_1 | Чтение и отправка сообщений_0 | Чтение и отправка сообщений_1 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 56 | False | True | False | True | True | False | False | False | False | … | False | True | False | True | False | True | False | True | False | True |
| 1 | 56 | False | True | False | True | True | False | False | False | False | … | False | True | False | True | False | True | False | True | False | True |
| 2 | 56 | False | True | False | True | True | False | False | False | False | … | False | True | False | True | False | True | False | True | False | True |
| 3 | 56 | False | True | False | True | True | False | False | False | False | … | False | True | False | True | False | True | False | True | False | True |
| 4 | 56 | False | True | False | True | False | True | False | False | False | … | False | True | False | True | False | True | False | True | False | True |
5 rows × 59 columns
# Стандартизация
scaler = StandardScaler()
scaled_data = scaler.fit_transform(encoded_data)
print('Размерность:', scaled_data.shape)
scaled_data
Размерность: (624, 59)
array([[ 2.41478203, -0.41585133, 0.49799598, ..., 0.31706324,
-0.32011482, 0.32011482],
[ 2.41478203, -0.41585133, 0.49799598, ..., 0.31706324,
-0.32011482, 0.32011482],
[ 2.41478203, -0.41585133, 0.49799598, ..., 0.31706324,
-0.32011482, 0.32011482],
...,
[-1.15589092, -0.41585133, -2.00804832, ..., 0.31706324,
3.12387911, -3.12387911],
[-1.15589092, -0.41585133, -2.00804832, ..., 0.31706324,
3.12387911, -3.12387911],
[-1.15589092, -0.41585133, -2.00804832, ..., 0.31706324,
3.12387911, -3.12387911]])
# Уменьшение размерности с проверкой
if scaled_data.shape[1] > 2:
# Используем PCA только если много признаков
pca = PCA(n_components=0.9) # 90% дисперсии
reduced_data = pca.fit_transform(scaled_data)
print(f"После PCA: {reduced_data.shape[1]} компонент")
else:
reduced_data = scaled_data
print("PCA не применен - мало признаков")
print('Размерность:', reduced_data.shape)
reduced_data
После PCA: 14 компонент Размерность: (624, 14)
array([[-6.66944381e+00, 1.73699452e+00, -2.77415616e-01, ...,
-7.89983622e-01, -1.46725876e-03, 4.07035509e-01],
[-6.64921634e+00, 1.72833260e+00, -2.88282044e-01, ...,
-1.01157229e+00, -2.84718758e-01, -1.52178472e-01],
[-6.64777272e+00, 1.71797606e+00, -2.61027343e-01, ...,
-1.11808482e+00, -1.07316438e-01, -3.07728761e-01],
...,
[ 5.91365519e+00, 1.56469945e+00, 3.63010672e+00, ...,
-1.74072581e-02, -2.03900835e+00, 1.50558501e+00],
[ 4.04313778e+00, -2.28800299e+00, 4.25269136e+00, ...,
8.32828951e-01, -1.21114122e+00, 2.38354682e+00],
[ 4.79466646e+00, 1.02528981e-01, 1.70128985e+00, ...,
-1.10297823e+00, 4.14896794e-01, -7.86012767e-02]])
Подбор параметров модели DBSCAN¶
# Аналитический подбор параметров DBSCAN на основе статистики расстояний
def analytical_dbscan_params(X, auto_min_samples=True):
"""
Аналитический подбор параметров DBSCAN на основе статистики расстояний
"""
n_samples, n_features = X.shape
# Автоматический подбор min_samples
# Эвристика: min_samples = 2 * размерность данных
min_samples = max(2, 2 * n_features)
# Корректировка для больших наборов данных
if auto_min_samples & n_samples > 1000:
min_samples = min(min_samples, 20)
# Анализ распределения расстояний до k-го соседа
k = min_samples
nbrs = NearestNeighbors(n_neighbors=k)
nbrs.fit(X)
distances, indices = nbrs.kneighbors(X)
# Берем расстояние до k-го соседа (последний в массиве)
k_distances = distances[:, k-1]
# Статистический анализ для определения eps
# Метод 1: Анализ квантилей и выбросов
# (наиболее устойчив к выбросам и подходит к любому распределению)
Q1 = np.percentile(k_distances, 25)
Q3 = np.percentile(k_distances, 75)
IQR = Q3 - Q1
standard_method = Q3 + 1.5 * IQR
# Метод 2: Анализ стандартного отклонения от среднего
# (наименее устойчив к выбросам, подходит только к стандартному распределению)
std_near_mean = np.mean(k_distances) + 1.5 * np.std(k_distances)
# Метод 3: Поиск точки перегиба в распределении
# (колено в распределении)
sorted_distances = np.sort(k_distances)
gradients = np.gradient(sorted_distances)
# Находим точку максимального изменения градиента
# (вторая производная)
inflection_idx = np.argmax(np.abs(np.gradient(gradients)))
eps_inflection = sorted_distances[inflection_idx]
# Метод 4: Адаптивный порог на основе статистики
eps_adaptive = np.percentile(k_distances, 85) # 85-й перцентиль
# Комбинированный подход
eps_candidates = [
standard_method, # Стандартный метод выбросов через квантили
std_near_mean, # Метод с использованием стандартного отклонения от среднего
eps_inflection, #
eps_adaptive
]
# Выбираем наиболее стабильное значение
#optimal_eps = np.median(eps_candidates) # Рискованный вариант
optimal_eps = np.min(eps_candidates) # Консервативный вариант (учитывает все подходы)
# Визуализация для валидации
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.hist(k_distances, bins=50, alpha=0.7, edgecolor='black')
plt.axvline(optimal_eps, color='red', linestyle='--',
label=f'Optimal eps: {optimal_eps:.3f}')
plt.xlabel(f'Distance to {k}-th neighbor')
plt.ylabel('Frequency')
plt.legend()
plt.title('Distribution of k-distances')
plt.subplot(1, 2, 2)
plt.plot(sorted_distances, 'b-', alpha=0.7)
plt.axvline(inflection_idx, color='red', linestyle='--',
label=f'Inflection point')
plt.axhline(optimal_eps, color='green', linestyle='--',
label=f'Optimal eps')
plt.xlabel('Points (sorted)')
plt.ylabel(f'Distance to {k}-th neighbor')
plt.legend()
plt.title('Sorted k-distances with inflection point')
plt.tight_layout()
plt.show()
print(f"Рекомендуемые параметры метода DBSCAN:")
print(f" eps: {optimal_eps:.4f}")
print(f" min_samples: {min_samples}")
print(f" Обоснование: статистический анализ {k}-расстояний")
return optimal_eps, min_samples
# Использование
optimal_eps, min_samples = analytical_dbscan_params(reduced_data)
Рекомендуемые параметры метода DBSCAN: eps: 5.3630 min_samples: 28 Обоснование: статистический анализ 28-расстояний
Кластеризация методом DBSCAN¶
# Кластеризация методом DBSCAN
dbscan = DBSCAN(eps=optimal_eps, min_samples=min_samples)
clusters = dbscan.fit_predict(reduced_data)
# Присваивание объектам номеров кластеров
data_with_clusters = data.copy()
data_with_clusters['cluster'] = clusters
# Дополнение данных объектов индивидуальными значениями метода силуэта
silhouette_vals = silhouette_samples(reduced_data, clusters)
data_with_clusters['silhouette_vals'] = silhouette_vals
# Сохранение результатов кластеризации
data_with_clusters.to_excel('data_with_clusters.xlsx')
# Предварительный просмотр результатов кластерного анализа
print('Предварительный просмотр результатов кластерного анализа:')
#pd.DataFrame({'clusters': clusters, 'silhouette_vals': silhouette_vals})
data_with_clusters.head(5)
Предварительный просмотр результатов кластерного анализа:
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | cluster | silhouette_vals | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.595508 |
| 1 | Смирнов Иван | Сотрудник | 0 | Входящий документ | На согласовании | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.588349 |
| 2 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Утвержден | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.582772 |
| 3 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Архив | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.586075 |
| 4 | Смирнов Иван | Сотрудник | 0 | Договор | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.577844 |
5 rows × 30 columns
Анализ кластеров¶
# Анализ кластеров
# Количество кластеров (+1, т.к. нумерация с нуля)
clusters_num = max(clusters) + 1
emissions_share = data_with_clusters[data_with_clusters['cluster'] < 0]['cluster'].count() / data_with_clusters['cluster'].count()
print(f'Количество кластеров: {clusters_num}')
print(f'Доля выбросов: {emissions_share:.2%}\n')
# Визуализация результатов
plt.figure(figsize=(15, 5))
# Визуализация кластеров (первые 2 компоненты)
plt.subplot(1, 3, 1)
if reduced_data.shape[1] >= 2:
plt.scatter(reduced_data[:, 0], reduced_data[:, 1], c=clusters, cmap='tab10', alpha=0.6)
plt.xlabel('Компонента 1')
plt.ylabel('Компонента 2')
else:
# Если одна компонента - гистограмма
for cluster in range(clusters_nums):
cluster_data = reduced_data[clusters == cluster, 0]
plt.hist(cluster_data, alpha=0.6, label=f'Cluster {cluster}')
plt.xlabel('Значение компоненты')
plt.ylabel('Частота')
plt.legend()
plt.title('Визуализация кластеров')
# Размеры кластеров
plt.subplot(1, 3, 2)
cluster_sizes = pd.Series(clusters).value_counts().sort_index()
plt.bar(cluster_sizes.index, cluster_sizes.values)
plt.xlabel('Кластер')
plt.ylabel('Количество объектов')
plt.title('Размеры кластеров')
for i, size in enumerate(cluster_sizes.values):
plt.text(i, size, str(size), ha='center', va='bottom')
# Качество кластеризации по кластерам
plt.subplot(1, 3, 3)
y_lower = 10
for i in range(clusters_num):
cluster_silhouette_vals = silhouette_vals[clusters == i]
cluster_silhouette_vals.sort()
plt.barh(range(y_lower, y_lower + len(cluster_silhouette_vals)),
cluster_silhouette_vals, height=1)
plt.text(-0.05, y_lower + len(cluster_silhouette_vals) // 2, str(i))
y_lower += len(cluster_silhouette_vals) + 10
plt.xlabel('Silhouette Score')
plt.ylabel('Кластер')
plt.title('Silhouette Analysis')
plt.tight_layout()
plt.show()
Количество кластеров: 3 Доля выбросов: 13.14%
# Анализ параметров кластеров
def analyzing_cluster_parameters(data_with_clusters, clusters_nums, minimum_cluster_share):
''' Анализ параметров кластеров '''
# data_with_clusters - датафрейм с указанием принадлежности к кластерам
# cluster_nums - функция типа range(clusters_num)
# для задания номеров кластеров для их перебора циклом
# minimum_cluster_share - минимальная доля категории внутри кластера для ее учета
# Анализируем распределение по самым информативным признакам
# Выбираем признаки с разумным количеством категорий
analysis_cols = []
for col in data_with_clusters.columns:
if 2 <= data_with_clusters[col].nunique() <= 50: # признаки с разумным количеством категорий
analysis_cols.append(col)
if len(analysis_cols) >= 30: # максимальное количество информативных признаков
break
if not analysis_cols:
# Если не нашли подходящие, берем первые 3 с наименьшим количеством категорий
col_stats = [(col, data_with_clusters[col].nunique()) for col in data_with_clusters.columns]
col_stats.sort(key=lambda x: x[1])
analysis_cols = [col for col, count in col_stats[:3]]
for cluster_num in clusters_nums:
cluster_data = data_with_clusters[data_with_clusters['cluster'] == cluster_num]
print(f"\n\n--- Кластер {cluster_num} ({len(cluster_data)} объектов, {len(cluster_data)/len(data_with_clusters):.1%}) ---")
print('\nСилуэтная оценка качества объектов кластера:')
display(pd.DataFrame({'Min':[data_with_clusters.loc[data_with_clusters['cluster'] == cluster_num, 'silhouette_vals'].min()],
'Mean':[data_with_clusters.loc[data_with_clusters['cluster'] == cluster_num, 'silhouette_vals'].mean()],
'Median':[data_with_clusters.loc[data_with_clusters['cluster'] == cluster_num, 'silhouette_vals'].median()],
'Max':[data_with_clusters.loc[data_with_clusters['cluster'] == cluster_num, 'silhouette_vals'].max()]}).round(3))
col = []
value = []
count = []
percentage = []
for c in analysis_cols:
top_value = cluster_data[c].value_counts().head(1)
if len(top_value) > 0:
col.append(c)
value.append(top_value.index[0])
count.append(top_value.values[0])
percentage.append(count[-1] / len(cluster_data))
print('\nКлючевые параметры, характеризующие кластер:')
display_claster = pd.DataFrame({'Параметр':col, 'Модальное значение':value, 'Количество':count, 'Доля в кластере':percentage})
display(display_claster[display_claster['Доля в кластере'] > minimum_cluster_share])
print('\nПримеры объектов кластера:')
display(data_with_clusters[data_with_clusters['cluster'] == cluster_num].head(5))
analyzing_cluster_parameters(data_with_clusters, range(clusters_num), .75)
--- Кластер 0 (200 объектов, 32.1%) --- Силуэтная оценка качества объектов кластера:
| Min | Mean | Median | Max | |
|---|---|---|---|---|
| 0 | 0.563 | 0.586 | 0.584 | 0.608 |
Ключевые параметры, характеризующие кластер:
| Параметр | Модальное значение | Количество | Доля в кластере | |
|---|---|---|---|---|
| 1 | Тип роли | Сотрудник | 200 | 1.0 |
| 4 | Создание карточки | 1 | 200 | 1.0 |
| 5 | Редактирование карточки | 1 | 200 | 1.0 |
| 6 | Удаление карточки | 1 | 200 | 1.0 |
| 7 | Добавление файлов | 1 | 200 | 1.0 |
| 8 | Редактирование всех файлов | 1 | 200 | 1.0 |
| 9 | Удаление всех файлов | 1 | 200 | 1.0 |
| 10 | Редактирование собственных файлов | 1 | 200 | 1.0 |
| 11 | Удаление собственных файлов | 1 | 200 | 1.0 |
| 12 | Инициация типового процесса отправки задач | 1 | 200 | 1.0 |
| 13 | Ручное редактирование номера | 1 | 200 | 1.0 |
| 14 | Редактирование маршрута | 1 | 200 | 1.0 |
| 15 | Полный пересчет маршрута | 1 | 200 | 1.0 |
| 16 | Права супермодератора | 1 | 200 | 1.0 |
| 17 | Подписка на уведомления | 1 | 200 | 1.0 |
| 18 | Расширенные настройки прав доступа | 1 | 200 | 1.0 |
| 19 | Создание шаблона и копирование | 1 | 200 | 1.0 |
| 20 | Пропуск этапов | 1 | 200 | 1.0 |
| 21 | Редактирование своих сообщений | 1 | 200 | 1.0 |
| 22 | Редактирование всех сообщений | 1 | 200 | 1.0 |
| 23 | Добавление обсуждений | 1 | 200 | 1.0 |
| 24 | Чтение обсуждений | 1 | 200 | 1.0 |
| 25 | Чтение и отправка сообщений | 1 | 200 | 1.0 |
| 26 | cluster | 0 | 200 | 1.0 |
Примеры объектов кластера:
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | cluster | silhouette_vals | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.595508 |
| 1 | Смирнов Иван | Сотрудник | 0 | Входящий документ | На согласовании | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.588349 |
| 2 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Утвержден | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.582772 |
| 3 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Архив | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.586075 |
| 4 | Смирнов Иван | Сотрудник | 0 | Договор | Проект | 1 | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0.577844 |
5 rows × 30 columns
--- Кластер 1 (230 объектов, 36.9%) --- Силуэтная оценка качества объектов кластера:
| Min | Mean | Median | Max | |
|---|---|---|---|---|
| 0 | -0.184 | 0.121 | 0.123 | 0.309 |
Ключевые параметры, характеризующие кластер:
| Параметр | Модальное значение | Количество | Доля в кластере | |
|---|---|---|---|---|
| 1 | Тип роли | Сотрудник | 183 | 0.795652 |
| 4 | Создание карточки | 0 | 182 | 0.791304 |
| 6 | Удаление карточки | 0 | 204 | 0.886957 |
| 8 | Редактирование всех файлов | 0 | 230 | 1.000000 |
| 9 | Удаление всех файлов | 0 | 230 | 1.000000 |
| 13 | Ручное редактирование номера | 0 | 230 | 1.000000 |
| 14 | Редактирование маршрута | 0 | 230 | 1.000000 |
| 15 | Полный пересчет маршрута | 0 | 230 | 1.000000 |
| 16 | Права супермодератора | 0 | 230 | 1.000000 |
| 17 | Подписка на уведомления | 1 | 207 | 0.900000 |
| 18 | Расширенные настройки прав доступа | 0 | 230 | 1.000000 |
| 19 | Создание шаблона и копирование | 0 | 230 | 1.000000 |
| 20 | Пропуск этапов | 0 | 230 | 1.000000 |
| 22 | Редактирование всех сообщений | 0 | 230 | 1.000000 |
| 24 | Чтение обсуждений | 1 | 229 | 0.995652 |
| 25 | Чтение и отправка сообщений | 1 | 221 | 0.960870 |
| 26 | cluster | 1 | 230 | 1.000000 |
Примеры объектов кластера:
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | cluster | silhouette_vals | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 180 | Обработчик входящих документов | Статическая роль | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.072081 |
| 181 | Обработчик входящих документов | Статическая роль | 0 | Входящий документ | На согласовании | 0 | 1 | 1 | 0 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0.028767 |
| 182 | Обработчик входящих документов | Статическая роль | 0 | Входящий документ | Утвержден | 0 | 1 | 0 | 0 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0.072624 |
| 183 | Обработчик входящих документов | Статическая роль | 0 | Входящий документ | Архив | 0 | 1 | 0 | 0 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0.070186 |
| 184 | Обработчик входящих документов | Статическая роль | 0 | Служебная записка | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.070408 |
5 rows × 30 columns
--- Кластер 2 (112 объектов, 17.9%) --- Силуэтная оценка качества объектов кластера:
| Min | Mean | Median | Max | |
|---|---|---|---|---|
| 0 | 0.343 | 0.518 | 0.511 | 0.563 |
Ключевые параметры, характеризующие кластер:
| Параметр | Модальное значение | Количество | Доля в кластере | |
|---|---|---|---|---|
| 1 | Тип роли | Сотрудник | 104 | 0.928571 |
| 4 | Создание карточки | 1 | 112 | 1.000000 |
| 7 | Добавление файлов | 1 | 112 | 1.000000 |
| 8 | Редактирование всех файлов | 1 | 112 | 1.000000 |
| 9 | Удаление всех файлов | 0 | 112 | 1.000000 |
| 10 | Редактирование собственных файлов | 1 | 112 | 1.000000 |
| 11 | Удаление собственных файлов | 1 | 112 | 1.000000 |
| 12 | Инициация типового процесса отправки задач | 1 | 112 | 1.000000 |
| 13 | Ручное редактирование номера | 0 | 112 | 1.000000 |
| 14 | Редактирование маршрута | 1 | 112 | 1.000000 |
| 15 | Полный пересчет маршрута | 0 | 112 | 1.000000 |
| 16 | Права супермодератора | 0 | 112 | 1.000000 |
| 17 | Подписка на уведомления | 1 | 112 | 1.000000 |
| 18 | Расширенные настройки прав доступа | 0 | 112 | 1.000000 |
| 19 | Создание шаблона и копирование | 1 | 112 | 1.000000 |
| 20 | Пропуск этапов | 0 | 112 | 1.000000 |
| 21 | Редактирование своих сообщений | 1 | 112 | 1.000000 |
| 22 | Редактирование всех сообщений | 0 | 112 | 1.000000 |
| 23 | Добавление обсуждений | 1 | 112 | 1.000000 |
| 24 | Чтение обсуждений | 1 | 112 | 1.000000 |
| 25 | Чтение и отправка сообщений | 1 | 112 | 1.000000 |
| 26 | cluster | 2 | 112 | 1.000000 |
Примеры объектов кластера:
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | cluster | silhouette_vals | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 308 | Ответственный за договоры | Статическая роль | 0 | Договор | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 2 | 0.360071 |
| 309 | Ответственный за договоры | Статическая роль | 0 | Договор | На согласовании | 1 | 1 | 1 | 0 | 1 | … | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 2 | 0.381258 |
| 310 | Ответственный за договоры | Статическая роль | 0 | Договор | Утвержден | 1 | 1 | 1 | 0 | 1 | … | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 2 | 0.382574 |
| 311 | Ответственный за договоры | Статическая роль | 0 | Договор | Архив | 1 | 1 | 0 | 0 | 1 | … | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 2 | 0.343410 |
| 312 | Ответственный за договоры | Статическая роль | 0 | Счет | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 2 | 0.360925 |
5 rows × 30 columns
# Табличное представление параметров кластеров
cluster_parameters = data_with_clusters[data_with_clusters['cluster'] >= 0].groupby(['cluster']).agg(lambda x: x.mode().iloc[0]).reset_index()
cluster_parameters['cluster_size'] = data_with_clusters.groupby(['cluster']).size()
cluster_parameters['percentage_cluster_size'] = cluster_parameters['cluster_size'] / len(data_with_clusters)
# Сохранение полученного табличного представления ключевых параметров кластеров
cluster_parameters.to_excel('cluster_parameters.xlsx')
print('Табличное представление медианных значений ключевых параметров кластеров:')
cluster_parameters
Табличное представление медианных значений ключевых параметров кластеров:
| cluster | Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | … | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | silhouette_vals | cluster_size | percentage_cluster_size | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | Кузнецов Елена | Сотрудник | 0 | Входящий документ | Архив | 1 | 1 | 1 | 1 | … | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0.566934 | 200 | 0.320513 |
| 1 | 1 | Смирнов Иван | Сотрудник | 0 | Служебная записка | Проект | 0 | 1 | 0 | 0 | … | 0 | 0 | 1 | 0 | 1 | 1 | 1 | -0.086274 | 230 | 0.368590 |
| 2 | 2 | Кузнецов Иван | Сотрудник | 0 | Договор | Архив | 1 | 1 | 1 | 0 | … | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 0.501731 | 112 | 0.179487 |
3 rows × 32 columns
# Итоговая информация
silhouette_avg = data_with_clusters['silhouette_vals'].mean() #silhouette_score(reduced_data, clusters)
print(f"\n=== ИТОГИ ===")
print(f"Исходные данные: {data.shape[1]} категориальных признаков")
print(f"После кодирования: {encoded_data.shape[1]} признаков")
print(f"Для кластеризации: {reduced_data.shape[1]} компонент")
print(f"Число кластеров: {clusters_num}")
print(f"Распределение по кластерам:")
for cluster_num in range(clusters_num):
count = (clusters == clusters_num).sum()
print(f" Кластер {clusters_num}: {count} объектов ({count/len(clusters):.1%})")
=== ИТОГИ === Исходные данные: 28 категориальных признаков После кодирования: 59 признаков Для кластеризации: 14 компонент Число кластеров: 3 Распределение по кластерам: Кластер 3: 0 объектов (0.0%) Кластер 3: 0 объектов (0.0%) Кластер 3: 0 объектов (0.0%)
Анализ выбросов¶
# Анализ выбросов, полученных на основе силуэтной оценки объектов сущствующих кластеров
print('Анализ выбросов, полученных на основе силуэтной оценки объектов сущствующих кластеров:')
analyzing_cluster_parameters(data_with_clusters[data_with_clusters['silhouette_vals'] < 0],
data_with_clusters[(data_with_clusters['silhouette_vals'] < 0) & (data_with_clusters['cluster'] >= 0)]['cluster'].unique(),
.75)
Анализ выбросов, полученных на основе силуэтной оценки объектов сущствующих кластеров: --- Кластер 1 (42 объектов, 84.0%) --- Силуэтная оценка качества объектов кластера:
| Min | Mean | Median | Max | |
|---|---|---|---|---|
| 0 | -0.184 | -0.085 | -0.083 | -0.0 |
Ключевые параметры, характеризующие кластер:
| Параметр | Модальное значение | Количество | Доля в кластере | |
|---|---|---|---|---|
| 4 | Создание карточки | 1 | 37 | 0.880952 |
| 5 | Редактирование карточки | 1 | 34 | 0.809524 |
| 7 | Добавление файлов | 1 | 32 | 0.761905 |
| 8 | Редактирование собственных файлов | 1 | 37 | 0.880952 |
| 9 | Удаление собственных файлов | 1 | 33 | 0.785714 |
| 14 | Чтение обсуждений | 1 | 41 | 0.976190 |
| 15 | Чтение и отправка сообщений | 1 | 34 | 0.809524 |
| 16 | cluster | 1 | 42 | 1.000000 |
Примеры объектов кластера:
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | cluster | silhouette_vals | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 180 | Обработчик входящих документов | Статическая роль | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.072081 |
| 184 | Обработчик входящих документов | Статическая роль | 0 | Служебная записка | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.070408 |
| 188 | Смирнов Иван | Сотрудник | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.083878 |
| 192 | Смирнов Иван | Сотрудник | 0 | Служебная записка | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.081775 |
| 196 | Петров Мария | Сотрудник | 0 | Входящий документ | Проект | 1 | 1 | 1 | 1 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | -0.084201 |
5 rows × 30 columns
# Анализ выбросов, полученных в результате кластеризации методом DBSCAN
print('Анализ выбросов, полученных в результате кластеризации методом DBSCAN:')
cluster_data = data_with_clusters[data_with_clusters['cluster'] < 0]
print(f"\n\n--- Кластер выбросов ({len(cluster_data)} объектов, {len(cluster_data)/len(data_with_clusters):.1%}) ---")
print('\nСилуэтная оценка качества объектов кластера выбросов:')
display(pd.DataFrame({'Min':[cluster_data['silhouette_vals'].min()],
'Mean':[cluster_data['silhouette_vals'].mean()],
'Median':[cluster_data['silhouette_vals'].median()],
'Max':[cluster_data['silhouette_vals'].max()]}).round(3))
analysis_cols = []
for col in cluster_data.columns:
if 2 <= cluster_data[col].nunique() <= 50:
analysis_cols.append(col)
if len(analysis_cols) >= 30:
break
if not analysis_cols:
# Если не нашли подходящие, берем первые 3 с наименьшим количеством категорий
col_stats = [(col, cluster_data[col].nunique()) for col in data.columns]
col_stats.sort(key=lambda x: x[1])
analysis_cols = [col for col, count in col_stats[:3]]
col = []
value = []
count = []
percentage = []
for c in analysis_cols:
top_value = cluster_data[c].value_counts().head(1)
if len(top_value) > 0:
col.append(c)
value.append(top_value.index[0])
count.append(top_value.values[0])
percentage.append(count[-1] / len(cluster_data))
print('\nКлючевые параметры, характеризующие кластер:')
display_claster = pd.DataFrame({'Параметр':col, 'Модальное значение':value, 'Количество':count, 'Доля в кластере':percentage})
display(display_claster[display_claster['Доля в кластере'] > .75])
print('\nПримеры объектов кластера:')
display(cluster_data.head(5))
Анализ выбросов, полученных в результате кластеризации методом DBSCAN: --- Кластер выбросов (82 объектов, 13.1%) --- Силуэтная оценка качества объектов кластера выбросов:
| Min | Mean | Median | Max | |
|---|---|---|---|---|
| 0 | -0.127 | 0.157 | 0.188 | 0.315 |
Ключевые параметры, характеризующие кластер:
| Параметр | Модальное значение | Количество | Доля в кластере |
|---|
Примеры объектов кластера:
| Название роли | Тип роли | Скрытая роль | Тип карточки | Состояние карточки | Создание карточки | Чтение карточки | Редактирование карточки | Удаление карточки | Добавление файлов | … | Расширенные настройки прав доступа | Создание шаблона и копирование | Пропуск этапов | Редактирование своих сообщений | Редактирование всех сообщений | Добавление обсуждений | Чтение обсуждений | Чтение и отправка сообщений | cluster | silhouette_vals | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 628 | Руководство | Подразделение | 0 | Договор | Проект | 0 | 1 | 0 | 0 | 1 | … | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | -1 | 0.062176 |
| 629 | Руководство | Подразделение | 0 | Договор | На согласовании | 0 | 1 | 1 | 0 | 0 | … | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | -1 | 0.250915 |
| 630 | Руководство | Подразделение | 0 | Договор | Утвержден | 0 | 1 | 1 | 0 | 1 | … | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | -1 | 0.170804 |
| 634 | Руководство | Подразделение | 0 | Служебная записка | Утвержден | 1 | 1 | 1 | 0 | 1 | … | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | -1 | 0.191462 |
| 635 | Руководство | Подразделение | 0 | Служебная записка | Архив | 0 | 1 | 0 | 0 | 1 | … | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | -1 | 0.255555 |
5 rows × 30 columns
Альтернативный вариант OPTICS для больших датафреймов с кластерами сложной формы
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import OPTICS
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_blobs
# 1. ЗАГРУЗКА ДАННЫХ
X = pd.read_csv('df_permission.csv', encoding='utf-8-sig', sep=';')
# 2. МАСШТАБИРОВАНИЕ ПРИЗНАКОВ (критично для метрик расстояния)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 3. ПОДБОР ПАРАМЕТРОВ НА ПОДВЫБОРКЕ (для экономии времени)
sample_size = min(20000, len(X_scaled))
sample_indices = np.random.choice(len(X_scaled), sample_size, replace=False)
X_sample = X_scaled[sample_indices]
# Визуализируем первые 2 признака для понимания структуры
plt.figure(figsize=(10, 6))
plt.scatter(X_sample[:, 0], X_sample[:, 1], s=1, alpha=0.6, c='gray')
plt.title("Структура данных (первые 2 признака)")
plt.show()
# 4. СОЗДАНИЕ И ОБУЧЕНИЕ МОДЕЛИ OPTICS
# Ключевые параметры:
# - min_samples: минимальный размер кластера (~1% от данных или от 50-100)
# - max_eps: максимальный радиус поиска (None для автоматического)
# - metric: метрика расстояния (евклидова для масштабированных данных)
# - algorithm: эффективная структура данных для больших наборов
# - n_jobs: распараллеливание
min_samples = max(50, int(0.01 * sample_size)) # 1% от подвыборки, но не менее 50
optics_model = OPTICS(
min_samples=min_samples,
max_eps=np.inf, # Не ограничиваем заранее
metric='euclidean',
cluster_method='xi', # Алгоритм выделения кластеров
xi=0.05, # Параметр для определения перепадов плотности
min_cluster_size=0.05, # Минимальная доля точек в кластере
algorithm='ball_tree', # Эффективная структура для многомерных данных
n_jobs=-1, # Использовать все ядра
leaf_size=30 # Размер листа для ball_tree
)
# Обучаем на подвыборке для быстрой настройки
optics_model.fit(X_sample)
# 5. ПОЛУЧЕНИЕ И АНАЛИЗ РЕЗУЛЬТАТОВ
labels = optics_model.labels_
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
print(f"\nРЕЗУЛЬТАТЫ НА ПОДВЫБОРКЕ ({sample_size} объектов):")
print(f"Найдено кластеров: {n_clusters}")
print(f"Точек шума: {np.sum(labels == -1)}")
print(f"Размеры кластеров: {np.bincount(labels[labels >= 0])}")
# 6. ВИЗУАЛИЗАЦИЯ КЛАСТЕРОВ (по первым 2 признакам)
plt.figure(figsize=(12, 8))
unique_labels = set(labels)
colors = plt.cm.Spectral(np.linspace(0, 1, len(unique_labels)))
for k, col in zip(unique_labels, colors):
if k == -1:
col = 'k' # Черный для шума
alpha = 0.1
else:
alpha = 0.6
class_members = labels == k
plt.scatter(X_sample[class_members, 0],
X_sample[class_members, 1],
s=2, c=[col], alpha=alpha, label=f'Cluster {k}' if k != -1 else 'Noise')
plt.title(f'OPTICS кластеризация: {n_clusters} кластеров')
plt.legend()
plt.show()
# 7. ЗАПУСК НА ПОЛНОМ НАБОРЕ ДАННЫХ (если результаты на подвыборке хорошие)
if n_clusters > 1 and np.sum(labels == -1) < 0.5 * sample_size: # Проверка качества
print("\nЗапуск OPTICS на полном наборе данных...")
# Увеличиваем min_samples пропорционально
full_min_samples = max(100, int(0.01 * len(X_scaled)))
optics_full = OPTICS(
min_samples=full_min_samples,
max_eps=np.inf,
metric='euclidean',
cluster_method='xi',
xi=0.05,
min_cluster_size=0.02, # Можно уменьшить для большего числа кластеров
algorithm='ball_tree',
n_jobs=-1,
leaf_size=40
)
# Обучаем на полных данных (может занять время)
optics_full.fit(X_scaled)
full_labels = optics_full.labels_
print(f"\nРЕЗУЛЬТАТЫ НА ПОЛНЫХ ДАННЫХ ({len(X_scaled)} объектов):")
print(f"Найдено кластеров: {len(set(full_labels)) - (1 if -1 in full_labels else 0)}")
print(f"Точек шума: {np.sum(full_labels == -1)} ({np.sum(full_labels == -1)/len(full_labels)*100:.1f}%)")
# Сохранение результатов в датафрейм
df_result = pd.DataFrame(X, columns=[f'feature_{i}' for i in range(n_features)])
df_result['cluster_label'] = full_labels
df_result['is_noise'] = (full_labels == -1)
print(f"\nПервые 5 строк результатов:")
print(df_result[['feature_0', 'feature_1', 'cluster_label', 'is_noise']].head())
else:
print("\nРезультаты на подвыборке неудовлетворительные. Настройте параметры.")

