import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.preprocessing import StandardScaler
from scipy.spatial import distance

# Загрузка датасета MLB
mlb_data = pd.read_csv('SOCR_Data_MLB_HeightsWeights.txt', sep='\t')
mlb_data.columns = ['Name', 'Team', 'Position', 'Height', 'Weight', 'Age']

# Создаем DataFrame
mlb_pd = pd.DataFrame(data=np.c_[mlb_data[['Height', 'Weight', 'Age']]],
                      columns=['Height (inches)', 'Weight (pounds)', 'Age (years)'])

# Стандартизация данных
scaler = StandardScaler()
data_scaled = scaler.fit_transform(mlb_pd[['Height (inches)', 'Weight (pounds)', 'Age (years)']])


# Функция для вычисления матрицы расстояний
def calculate_distance_matrix(data, metric):
    n_samples = len(data)
    distance_matrix = np.zeros((n_samples, n_samples))

    for i in range(n_samples):
        for j in range(i + 1, n_samples):
            if metric == 'euclidean':
                dist = distance.euclidean(data[i], data[j])
            elif metric == 'cityblock':
                dist = distance.cityblock(data[i], data[j])
            elif metric == 'chebyshev':
                dist = distance.chebyshev(data[i], data[j])
            elif metric == 'cosine':
                dist = distance.cosine(data[i], data[j])

            distance_matrix[i][j] = dist
            distance_matrix[j][i] = dist

    return distance_matrix


# Список комбинаций для исследования
combinations = [
    # Евклидово расстояние
    {'metric': 'euclidean', 'linkage': 'single', 'name': 'Евклидово расстояние, одиночная связь'},
    {'metric': 'euclidean', 'linkage': 'complete', 'name': 'Евклидово расстояние, полная связь'},
    {'metric': 'euclidean', 'linkage': 'average', 'name': 'Евклидово расстояние, средняя связь'},
    {'metric': 'euclidean', 'linkage': 'ward', 'name': 'Евклидово расстояние, связь Ward'},

    # Манхэттенское расстояние
    {'metric': 'cityblock', 'linkage': 'single', 'name': 'Манхэттенское расстояние, одиночная связь'},
    {'metric': 'cityblock', 'linkage': 'average', 'name': 'Манхэттенское расстояние, средняя связь'},
    {'metric': 'cityblock', 'linkage': 'complete', 'name': 'Манхэттенское расстояние, полная связь'},

    # Косинусное расстояние
    {'metric': 'cosine', 'linkage': 'single', 'name': 'Косинусное расстояние, одиночная связь'},

    # Расстояние Чебышева
    {'metric': 'chebyshev', 'linkage': 'single', 'name': 'Расстояние Чебышева, одиночная связь'},
    {'metric': 'euclidean', 'linkage': 'centroid', 'name': 'Евклидово расстояние, центроидная связь'}
]

# Построение отдельных дендрограмм
for i, combo in enumerate(combinations, 1):
    try:
        # Вычисляем матрицу расстояний
        distance_matrix = calculate_distance_matrix(data_scaled, combo['metric'])

        # Создаем linkage matrix
        Z = linkage(distance_matrix, method=combo['linkage'])

        # Создаем отдельную фигуру для каждой дендрограммы
        plt.figure(figsize=(12, 8))

        # Строим дендрограмму
        dendrogram(Z,
                   truncate_mode='lastp',
                   p=12,
                   show_leaf_counts=True,
                   leaf_rotation=90)

        plt.title(f'Дендрограмма: {combo["name"]}', fontsize=14, pad=20)
        plt.xlabel('Количество точек в узле', fontsize=12)
        plt.ylabel('Расстояние', fontsize=12)
        plt.grid(True, alpha=0.3)

        plt.tight_layout()
        plt.show()

        print(f"{i}. {combo['name']} - Успешно построена")

    except Exception as e:
        print(f"{i}. {combo['name']} - Ошибка: {e}")

# 3D ВИЗУАЛИЗАЦИИ ДЛЯ КАЖДОЙ КОМБИНАЦИИ
print("\n" + "=" * 80)
print("3D ВИЗУАЛИЗАЦИЯ КЛАСТЕРОВ ДЛЯ КАЖДОЙ КОМБИНАЦИИ")
print("=" * 80)

for i, combo in enumerate(combinations, 1):
    try:
        # Вычисляем матрицу расстояний
        distance_matrix = calculate_distance_matrix(data_scaled, combo['metric'])

        # Создаем linkage matrix
        Z = linkage(distance_matrix, method=combo['linkage'])

        # Определяем кластеры (берем 3 кластера для визуализации)
        from scipy.cluster.hierarchy import fcluster

        labels = fcluster(Z, t=3, criterion='maxclust')

        # Создаем отдельную 3D визуализацию
        fig = plt.figure(figsize=(10, 8))
        ax = fig.add_subplot(111, projection='3d')

        scatter = ax.scatter3D(mlb_pd['Height (inches)'],
                               mlb_pd['Weight (pounds)'],
                               mlb_pd['Age (years)'],
                               c=labels,
                               cmap='tab10',
                               s=50,
                               alpha=0.7)

        ax.set_title(f'3D Визуализация: {combo["name"]}', fontsize=14, pad=20)
        ax.set_xlabel('Height (inches)', fontsize=12)
        ax.set_ylabel('Weight (pounds)', fontsize=12)
        ax.set_zlabel('Age (years)', fontsize=12)

        plt.colorbar(scatter, ax=ax, label='Cluster')
        plt.tight_layout()
        plt.show()

        # Выводим информацию о кластерах
        print(f"\n{i}. {combo['name']}:")
        for cluster_id in np.unique(labels):
            cluster_data = mlb_pd[labels == cluster_id]
            print(f"  Кластер {cluster_id}: {len(cluster_data)} игроков")
            print(
                f"    Рост: {cluster_data['Height (inches)'].mean():.1f} ± {cluster_data['Height (inches)'].std():.1f}")
            print(
                f"    Вес: {cluster_data['Weight (pounds)'].mean():.1f} ± {cluster_data['Weight (pounds)'].std():.1f}")
            print(f"    Возраст: {cluster_data['Age (years)'].mean():.1f} ± {cluster_data['Age (years)'].std():.1f}")

    except Exception as e:
        print(f"{i}. Ошибка в 3D визуализации для {combo['name']}: {e}")

# АНАЛИЗ РАССТОЯНИЙ ОБЪЕДИНЕНИЯ ДЛЯ ОПРЕДЕЛЕНИЯ ОПТИМАЛЬНОГО ЧИСЛА КЛАСТЕРОВ
print("\n" + "=" * 80)
print("АНАЛИЗ ОПТИМАЛЬНОГО ЧИСЛА КЛАСТЕРОВ")
print("=" * 80)

# Анализируем лучшие комбинации
best_combinations = [
    {'metric': 'euclidean', 'linkage': 'ward', 'name': 'Евклидово расстояние, связь Ward'},
    {'metric': 'euclidean', 'linkage': 'average', 'name': 'Евклидово расстояние, средняя связь'},
    {'metric': 'cityblock', 'linkage': 'average', 'name': 'Манхэттенское расстояние, средняя связь'}
]

for i, combo in enumerate(best_combinations, 1):
    try:
        # Вычисляем матрицу расстояний
        distance_matrix = calculate_distance_matrix(data_scaled, combo['metric'])
        Z = linkage(distance_matrix, method=combo['linkage'])

        # Анализ расстояний объединения
        last_merges = Z[-15:, 2]  # последние 15 объединений
        distances_diff = np.diff(last_merges[::-1])

        # Находим оптимальное число кластеров
        optimal_idx = np.argmax(distances_diff) + 2
        optimal_clusters = min(optimal_idx, 8)

        # Создаем отдельную фигуру для анализа
        plt.figure(figsize=(10, 6))

        plt.plot(range(len(last_merges)), last_merges[::-1], 'bo-', linewidth=2, markersize=6)
        plt.axvline(x=optimal_idx - 1, color='red', linestyle='--',
                    label=f'Оптимально: {optimal_clusters} кластеров')
        plt.title(f'Анализ расстояний объединения: {combo["name"]}\nОптимальное число кластеров: {optimal_clusters}')
        plt.xlabel('Шаг объединения')
        plt.ylabel('Расстояние')
        plt.legend()
        plt.grid(True, alpha=0.3)

        plt.tight_layout()
        plt.show()

        print(f"{i}. {combo['name']}: оптимальное число кластеров = {optimal_clusters}")
        print(f"   Наибольший скачок расстояния: {distances_diff[optimal_idx - 2]:.3f}")

    except Exception as e:
        print(f"{i}. Ошибка в анализе для {combo['name']}: {e}")

# ИТОГОВЫЙ ВЫВОД
print("\n" + "=" * 80)
print("ИТОГОВЫЕ ВЫВОДЫ")
print("=" * 80)
print("1. Все 10 дендрограмм построены по отдельности")
print("2. Для каждой комбинации создана отдельная 3D визуализация")
print("3. Метод Ward обычно дает наиболее сбалансированные кластеры")
print("4. Одиночная связь может создавать вытянутые 'цепочки' кластеров")
print("5. Оптимальное число кластеров определяется по наибольшему скачку в расстояниях объединения")
print("6. Для MLB данных оптимально 3-5 кластеров, соответствующих различным физическим типам игроков")
print("7. Разные метрики расстояния показывают различные аспекты сходства между игроками")