1.2 MiB
Бизнес-цель: кластеризация пациентов для выявления групп с схожими характеристиками здоровья и рисками инсульта. Что, к примеру, может использоваться для следующего:¶
- определение, люди каких групп могут иметь бОльшую предрасположенность к возникновению инсульта
- помощь в медицине на основе полученных данных в разработке медицинских показаний людям с повышенным риском возникновения инсульта
import pandas as pd
import numpy as np
from sklearn import cluster
from scipy.cluster import hierarchy
df1 = pd.read_csv("./csv/option4.csv", index_col='id')
df1.info
df = df1.head(2500)
df
уберем пустые значения, подготовим данные:
print(df.isnull().sum())
print()
print(df.isnull().any())
print()
df['bmi'] = df['bmi'].fillna(df['bmi'].median())
print("\nНаличие пропущенных значений:")
print(df.isnull().sum())
df.describe()
import matplotlib.pyplot as plt
import seaborn as sns
from typing import List, Any
def draw_data_2d(
df: pd.DataFrame,
col1: int,
col2: int,
y: List | None = None,
classes: List | None = None,
subplot: Any | None = None,
):
ax = None
if subplot is None:
_, ax = plt.subplots()
else:
ax = subplot
scatter = ax.scatter(df[df.columns[col1]], df[df.columns[col2]], c=y)
ax.set(xlabel=df.columns[col1], ylabel=df.columns[col2])
if classes is not None:
ax.legend(
scatter.legend_elements()[0], classes, loc="lower right", title="Classes"
)
columns = ['age', 'avg_glucose_level', 'bmi', 'hypertension']
df_temp = df[columns]
sns.set_theme(style="whitegrid")
print("ЗАВИСИМОСТЬ ЗНАЧЕНИЙ ДРУГ ОТ ДРУГА")
plt.figure(figsize=(16,12))
draw_data_2d(df_temp, 0, 1, subplot=plt.subplot(2, 2, 1)) # age vs avg_glucose_level
plt.figure(figsize=(16,12))
draw_data_2d(df_temp, 0, 2, subplot=plt.subplot(2, 2, 2)) # age vs bmi
plt.figure(figsize=(16,12))
draw_data_2d(df_temp, 0, 3, subplot=plt.subplot(2, 2, 3)) # age vs hypertension
plt.figure(figsize=(16,12))
draw_data_2d(df_temp, 1, 2, subplot=plt.subplot(2, 2, 4)) # avg_glucose_level vs bmi
видно, что индекс массы тела в зависимости от возраста в основном держится поменьше в раннем возрасте (до полового созревания, грубо говоря), а потом уже распределяется от адекватного до 40+ (в общем, вплоть до ожирения, с выбросами-то)
потом гипертония встречается все таки после 20 лет чаще
ну и чем ниже индекс массы тела, тем ниже и адекватнее уровень глюкозы (ну тут ясно, почему. люди с избыточным весом и болеют диабетом чаще)
продолжим приводить данные к нормальному виду, и теперь их стандартизуем:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
columns_to_scale = df_temp.drop(columns=["hypertension"]).columns
columns_to_keep = ["hypertension"]
data_scaled = scaler.fit_transform(df_temp[columns_to_scale])
df_scaled = pd.DataFrame(data_scaled, columns=columns_to_scale, index=df_temp.index)
df_scaled[columns_to_keep] = df_temp[columns_to_keep]
df_scaled
самое время применить иерархический алгоритм кластеризации (когда мы создаем дерево кластеров, где каждый уровень - это объединение более мелких кластеров)
# linkage_matrix = linkage(data_scaled, method='ward')
# plt.figure(figsize=(10,10))
# dendrogram(linkage_matrix)
# plt.title('Дендрограмма')
# plt.ylabel('')
# plt.xlabel('')
# plt.show()
import numpy as np
from sklearn import cluster
from scipy.cluster import hierarchy
def run_agglomerative(
df: pd.DataFrame, num_clusters: int | None = 2
) -> cluster.AgglomerativeClustering:
agglomerative = cluster.AgglomerativeClustering(
n_clusters=num_clusters,
compute_distances=True,
)
return agglomerative.fit(df)
def get_linkage_matrix(model: cluster.AgglomerativeClustering) -> np.ndarray:
counts = np.zeros(model.children_.shape[0]) # type: ignore
n_samples = len(model.labels_)
for i, merge in enumerate(model.children_): # type: ignore
current_count = 0
for child_idx in merge:
if child_idx < n_samples:
current_count += 1
else:
current_count += counts[child_idx - n_samples]
counts[i] = current_count
return np.column_stack([model.children_, model.distances_, counts]).astype(float)
def draw_dendrogram(linkage_matrix: np.ndarray):
hierarchy.dendrogram(linkage_matrix, truncate_mode="level", p=3)
plt.xticks(fontsize=10, rotation=45)
plt.tight_layout()
tree = run_agglomerative(df_scaled)
linkage_matrix = get_linkage_matrix(tree)
draw_dendrogram(linkage_matrix)
result = hierarchy.fcluster(linkage_matrix, 40, criterion="distance")
y_names = ['0', '1', '2']
plt.figure(figsize=(12, 10))
draw_data_2d(df_temp, 0, 1, result, y_names, subplot=plt.subplot(2, 2, 1))
draw_data_2d(df_temp, 0, 2, result, y_names, subplot=plt.subplot(2, 2, 2))
draw_data_2d(df_temp, 0, 3, result, y_names, subplot=plt.subplot(2, 2, 3))
draw_data_2d(df_temp, 1, 2, result, y_names, subplot=plt.subplot(2, 2, 4))
емае теперь переходим к НЕиерархической кластеризации будем использовать метод К-средних (K-means), мы выбираем количество кластеров и флгоритм пытается распределить данные так, чтобы минимизировать расстояние между объектами и центром их кластера
from typing import Tuple
def print_cluster_result(
df: pd.DataFrame, clusters_num: int, labels: np.ndarray, separator: str = ", "
):
for cluster_id in range(clusters_num):
cluster_indices = np.where(labels == cluster_id)[0]
print(f"Cluster {cluster_id + 1} ({len(cluster_indices)}):")
rules = [str(df.index[idx]) for idx in cluster_indices]
print(separator.join(rules))
print("")
print("--------")
def run_kmeans(
df: pd.DataFrame, num_clusters: int, random_state: int
) -> Tuple[np.ndarray, np.ndarray]:
kmeans = cluster.KMeans(n_clusters=num_clusters, random_state=random_state)
labels = kmeans.fit_predict(df)
return labels, kmeans.cluster_centers_
random_state = 9
labels, centers = run_kmeans(df_scaled, 3, random_state)
print_cluster_result(df_scaled, 3, labels)
display(centers)
def draw_cluster_results(
df: pd.DataFrame,
col1: int,
col2: int,
labels: np.ndarray,
cluster_centers: np.ndarray,
subplot: Any | None = None,
):
ax = None
if subplot is None:
ax = plt
else:
ax = subplot
centroids = cluster_centers
u_labels = np.unique(labels)
for i in u_labels:
ax.scatter(
df[labels == i][df.columns[col1]],
df[labels == i][df.columns[col2]],
label=i,
)
ax.scatter(centroids[:, col1], centroids[:, col2], s=80, color="k")
plt.title('Точка - это кластер, грубо говоря (центр кластера) :)')
plt.figure(figsize=(16, 12))
draw_cluster_results(df_scaled, 0, 1, labels, centers, plt.subplot(2, 2, 1)) # age vs avg_glucose_level
draw_cluster_results(df_scaled, 0, 2, labels, centers, plt.subplot(2, 2, 2)) # age vs bmi
draw_cluster_results(df_scaled, 0, 3, labels, centers, plt.subplot(2, 2, 3)) # age vs hypertension
draw_cluster_results(df_scaled, 1, 2, labels, centers, plt.subplot(2, 2, 4)) # avg_glucose_level vs bmi
from sklearn.decomposition import PCA
#до двухмерного пространсва :) вжух
pca_data = PCA(n_components=2).fit_transform(df_scaled)
pca_data
# Визуализация сокращенных данных
plt.figure(figsize=(16, 6))
# Визуализация для KMeans кластеризации
plt.subplot(1, 2, 1)
sns.scatterplot(x=pca_data[:, 0], y=pca_data[:, 1], hue=labels, palette='Set1', alpha=0.6)
plt.title('PCA Reduced Data: KMeans Clustering')
plt.tight_layout()
plt.show()
теперь интересная штука... с помощью так называемого метода локтя посмотрим на лучшее (оптимальное) количество кластеров на основе инерции (расстояния = сумме квадратов расстояния от объектов до центра кластера)
from sklearn.cluster import KMeans
inertias = []
clusters_range = range(1, 23)
for i in clusters_range:
kmeans = KMeans(n_clusters=i, random_state=random_state)
kmeans.fit(data_scaled)
inertias.append(kmeans.inertia_)
plt.figure(figsize=(10, 6))
plt.plot(clusters_range, inertias, marker='*')
plt.title('Метод локтя для оптимального k')
plt.xlabel('Количество кластеров')
plt.ylabel('Инерция')
plt.grid(True)
plt.show()
отсюда можем видеть, что кривая, в принципе, прекращает резко падать после 15 кластера. а в принципе, САМЫЙ резкий спад закончился на количесте кластеров,равному 3-5
ТЕПЕРЬ ЧАВО ой капс
теперь берем и считаем, насколько хорошо будут данные разделены на некое количество кластеров
делаем это с помощью коэффициента силуета - чем ближе он к 1, тем лучше сгруппирован объект и тем дальше он от соседних кластеров. чем ближе к нулю, тем он ближе к соседям.
чем ближе к -1, тем больше вероятность, что он неправильно сгруппирован в кластер
from sklearn.metrics import silhouette_score
silhouette_scores = []
for i in clusters_range[1:]:
kmeans = KMeans(n_clusters=i, random_state=random_state)
labels = kmeans.fit_predict(data_scaled)
score = silhouette_score(data_scaled, labels)
silhouette_scores.append(score)
# Построение диаграммы значений силуэта
plt.figure(figsize=(10, 6))
plt.plot(clusters_range[1:], silhouette_scores, marker='4')
plt.title('Коэффициенты силуэта для разных k')
plt.xlabel('Количество кластеров')
plt.ylabel('Коэффициент силуэта')
plt.grid(True)
plt.show()
видим, что при количестве кластеров, равном трем, самое лучшее разбиение получается. вот и отличненько) ведь я как раз разбивала данные на 3 кластера