Задание 27: Анализ данных. Кластеризация
1. Общая информация о задании
- Уровень сложности: высокий (Часть 2)
- Максимальный балл: 2
- Примерное время выполнения: около 40 минут
- Формат файла:
.txt, каждая строка — два числаx yчерез пробел (первая строка — заголовок) - Формат ответа: четыре числа в двух строках (для файла A и файла B)
Типичная структура ответа:
int(Px_A × 10000) int(Py_A × 10000)
int(Px_B × 10000) int(Py_B × 10000)
где Px, Py среднее арифметическое координат медоид всех кластеров.
Задания: Задания N27. Анализ данных. уровень 1 | N27. Банк заданий
2. Ключевые понятия
Медоида vs Центроид
Медоида — реальная точка из набора данных, для которой сумма расстояний до всех остальных точек кластера минимальна.
Центроид — геометрический центр кластера (среднее арифметическое координат). Может не совпадать ни с одной точкой из файла.
В задании 27 нужен именно медоида которая является реальной точкой из данных
Варианты методов решения
В 27 заданиях можно применять один из трёх алгоритмов для кластеризации:
Кластеризация через уравнения прямых Кластеризация методом k-средних Кластеризация методом DBSCAN
также см. примеры применения: Часть 1| Часть 2 | Часть 3
Пошаговый алгоритм решения
- Прочитать условие, т.е сколько кластеров N, какая формула расстояния, что именно вывести
- Открыть файл в LibreOffice Calc → выделить данные → вставить точечную диаграмму → определить форму кластеров и границы
- Выбрать метод линейная граница → метод прямых; выпуклые кластеры → k-средних; серповидные / с выбросами → DBSCAN
- Считать данные, запустить кластеризацию
- Проверить количество и размеры кластеров должны совпадать с ожидаемым из диаграммы
- Найти медоиды через функцию
get_medoid() - Вычислить и вывести ответ по условию
3. Формула расстояния
Евклидово расстояние (основное в 2025–2026)
from math import dist
d = dist((x1, y1), (x2, y2)) # sqrt((x2-x1)^2 + (y2-y1)^2)
Расстояние Чебышева (встречается в отдельных вариантах)
d = max(abs(x2 - x1), abs(y2 - y1))
Внимательно читать условие, там явно указана формула расстояния.
4. Считывание данных из файла
with open('27A.txt') as f:
next(f) # пропустить заголовок, также можно вручную удалить
data = [list(map(float, s.split())) for s in f]
Если заголовка нет убрать next(f).
Если разделитель запятая то добавить:
data = [list(map(float, s.replace(',', '.').split())) for s in f]
5. Функция поиска медоиды
from math import dist
def get_medoid(cluster):
return min(cluster, key=lambda p: sum(dist(p, q) for q in cluster))
Или без lambda явный перебор:
def get_medoid(cluster):
best, best_sum = None, 10**18
for p in cluster:
s = sum(dist(p, q) for q in cluster)
if s < best_sum:
best_sum = s
best = p
return best
6. Три метода кластеризации
Метод 1: Ручное разбиение по уравнению прямой (самый быстрый)
Когда применять: кластеры разделяются прямой линией (x < C или y < C), где C число
Визуализируем в LibreOffice Calc: данные → точечная диаграмма → визуально находим границу разреза
Пример: 2 кластера, разделяются прямой x = 1:
from math import dist
def get_medoid(cluster):
return min(cluster, key=lambda p: sum(dist(p, q) for q in cluster))
cluster_1, cluster_2 = [], []
with open('27A.txt') as f:
next(f)
for line in f:
x, y = map(float, line.split())
if x < 1:
cluster_1.append((x, y))
else:
cluster_2.append((x, y))
medoids = [get_medoid(cluster_1), get_medoid(cluster_2)]
px = sum(x for x, y in medoids) / len(medoids)
py = sum(y for x, y in medoids) / len(medoids)
print(abs(int(px * 10_000)), abs(int(py * 10_000)))
Пример: 3 кластера, два разреза:
clusters = [[], [], []]
with open('27B.txt') as f:
next(f)
for line in f:
x, y = map(float, line.split())
if y < 3:
clusters[0].append((x, y))
elif y > 7:
clusters[1].append((x, y))
else:
clusters[2].append((x, y))
medoids = [get_medoid(cl) for cl in clusters]
px = sum(x for x, y in medoids) / len(medoids)
py = sum(y for x, y in medoids) / len(medoids)
print(abs(int(px * 10_000)), abs(int(py * 10_000)))
Метод 2: k-средних с медоидами (универсальный для выпуклых кластеров)
Когда применять: кластеры нельзя разделить одной прямой, но они примерно круглые / выпуклые.
Отличие от классического k-средних: вместо геометрического центра используется медоида (реальная точка).
Начальные точки выбираются вручную по диаграмме — по одной из каждого видимого кластера. Это важно: случайная инициализация может дать неверный результат.
from math import dist
def assign_clusters(data, centers):
"""Каждой точке — индекс ближайшего центра."""
labels = []
for p in data:
distances = [dist(p, center) for center in centers]
labels.append(distances.index(min(distances)))
return labels
def update_centers(data, labels, k):
"""Пересчёт центров: медоида каждого кластера."""
new_centers = []
for i in range(k):
cluster = [p for p, l in zip(data, labels) if l == i]
medoid = min(cluster, key=lambda p: sum(dist(p, q) for q in cluster))
new_centers.append(medoid)
return new_centers
def k_means(data, k, start_centers):
centers = start_centers
while True:
labels = assign_clusters(data, centers)
new_centers = update_centers(data, labels, k)
if new_centers == centers:
break
centers = new_centers
return labels, centers
Полное решение:
from math import dist
def get_medoid(cluster):
return min(cluster, key=lambda p: sum(dist(p, q) for q in cluster))
def assign_clusters(data, centers):
return [min(range(len(centers)), key=lambda i: dist(p, centers[i])) for p in data]
def update_centers(data, labels, k):
return [get_medoid([p for p, l in zip(data, labels) if l == i]) for i in range(k)]
def k_means(data, k, start_centers):
centers = start_centers
while True:
labels = assign_clusters(data, centers)
new_centers = update_centers(data, labels, k)
if new_centers == centers:
break
centers = new_centers
return labels, centers
with open('27A.txt') as f:
next(f)
data = [list(map(float, line.split())) for line in f]
# Начальные точки выбираем по диаграмме вручную!
labels, centers = k_means(data, 2, [[-9, -2], [-1, 15]])
px = sum(x for x, y in centers) / len(centers)
py = sum(y for x, y in centers) / len(centers)
print(abs(int(px * 10_000)), abs(int(py * 10_000)))
Метод 3: DBSCAN (для сложных форм и выбросов)
Когда применять:
- Кластеры серповидной или другой нестандартной формы
- В данных есть выбросы (шумовые точки)
- Нет линейной разделяющей границы
Единственный параметр eps (радиус соседства). Подбирается по диаграмме: должен быть достаточно большим, чтобы объединить точки внутри кластера, но достаточно малым, чтобы не «слить» два кластера.
Принцип: берём точку → находим всех соседей в радиусе eps → добавляем в кластер → для каждого нового соседа снова ищем соседей → повторяем пока кластер растет
from math import dist
def dbscan(data, eps):
data = list(data)
clusters = []
while data:
point = data.pop()
cluster = [point]
queue = [point]
while queue:
current = queue.pop(0)
neighbors = [p for p in data if dist(current, p) < eps]
cluster.extend(neighbors)
queue.extend(neighbors)
data[:] = [p for p in data if p not in neighbors]
clusters.append(cluster)
return clusters
Полное решение:
from math import dist
def get_medoid(cluster):
return min(cluster, key=lambda p: sum(dist(p, q) for q in cluster))
def dbscan(data, eps):
data = list(data)
clusters = []
while data:
point = data.pop()
cluster = [point]
queue = [point]
while queue:
current = queue.pop(0)
neighbors = [p for p in data if dist(current, p) < eps]
cluster.extend(neighbors)
queue.extend(neighbors)
data[:] = [p for p in data if p not in neighbors]
clusters.append(cluster)
return clusters
with open('27A.txt') as f:
next(f)
data = [tuple(map(float, line.split())) for line in f]
clusters = dbscan(data, eps=2)
print([len(cl) for cl in clusters]) # проверка числа и размеров кластеров
# Фильтрация выбросов (одиночных точек)
clusters = [cl for cl in clusters if len(cl) > 5]
medoids = [get_medoid(cl) for cl in clusters]
px = sum(x for x, y in medoids) / len(medoids)
py = sum(y for x, y in medoids) / len(medoids)
print(abs(int(px * 10_000)), abs(int(py * 10_000)))
Подбор eps: начать с 1–2, смотреть на print([len(cl) for cl in clusters]). Нужно получить N кластеров с примерно равным числом точек. Если кластеров слишком много — увеличить eps. Если один большой — уменьшить.
8. Типовые вычисления по медоидам
Среднее арифметическое координат (Px, Py), самый частый вариант
px = sum(x for x, y in medoids) / len(medoids)
py = sum(y for x, y in medoids) / len(medoids)
print(abs(int(px * 10000)), abs(int(py * 10000)))
Расстояние между двумя медоидами
d = dist(medoids[0], medoids[1])
print(int(d * 10000))
Количество точек с x ≤ x_медоиды в кластере
cx, cy = medoids[i]
count = sum(1 for x, y in clusters[i] if x <= cx)
Количество точек внутри квадрата (сторона 2·r) вокруг медоиды
cx, cy = medoids[i]
r = 1.0
count = sum(1 for x, y in clusters[i] if abs(x - cx) <= r and abs(y - cy) <= r)
Расстояние по оси Y между медоидами двух кластеров
dy = abs(medoids[i][1] - medoids[j][1])
print(int(dy * 10000))
9. Визуализация для проверки (модуль turtle)
Полезно при отладке, видно, правильно ли разбились кластеры:
import turtle
def draw_clusters(data, labels):
turtle.screensize(2000, 2000)
turtle.tracer(0)
scale = 50
colors = ["#F76D47", "#A7D380", "#79CFD4", "#8A6FDF"]
for point, label in zip(data, labels):
turtle.teleport(point[0] * scale, point[1] * scale)
turtle.dot(16, colors[label % len(colors)])
turtle.update()
turtle.done()
Вызов: draw_clusters(data, labels), где labels это список индексов кластера для каждой точки.
10. Полный шаблон решения (DBSCAN)
from math import dist
def get_medoid(cluster):
return min(cluster, key=lambda p: sum(dist(p, q) for q in cluster))
def dbscan(data, eps):
data = list(data)
clusters = []
while data:
point = data.pop()
cluster = [point]
queue = [point]
while queue:
current = queue.pop(0)
neighbors = [p for p in data if dist(current, p) < eps]
cluster.extend(neighbors)
queue.extend(neighbors)
data[:] = [p for p in data if p not in neighbors]
clusters.append(cluster)
return clusters
def solve(filename, eps, min_size=5):
with open(filename) as f:
next(f)
data = [tuple(map(float, s.split())) for s in f]
clusters = dbscan(data, eps)
clusters = [cl for cl in clusters if len(cl) >= min_size]
print(f'{filename}: кластеры {[len(cl) for cl in clusters]}') # проверка
medoids = [get_medoid(cl) for cl in clusters]
px = sum(x for x, y in medoids) / len(medoids)
py = sum(y for x, y in medoids) / len(medoids)
print(abs(int(px * 10_000)), abs(int(py * 10_000)))
solve('27A.txt', eps=2)
solve('27B.txt', eps=2)
12. Типичные ошибки
| Ошибка | Как избежать |
|---|---|
| Медоида = среднее координат (геометрический центр) | Медоида реальная точка с минимальной суммой расстояний |
Неправильный eps в DBSCAN → неверное число кластеров |
Проверить print([len(cl) for cl in clusters]) перед подсчётом |
| Забыть убрать шум (одиночные точки-выбросы) | Фильтровать: [cl for cl in clusters if len(cl) >= 5] |
| Заголовок первой строки файла попал в данные | Добавить next(f) при считывании |
int() vs round() |
В условии «целую часть» → int(), не round() |
| Начальные точки k-средних выбраны случайно | Выбирать вручную по диаграмме — по одной из каждого кластера |
| Для серповидных кластеров использован k-средних | Серповидные / нестандартные формы → только DBSCAN |
abs() вокруг int() не применён |
Условие задачи: обычно нужно abs(int(...)) — читать внимательно |
13. Файлы решений
| Файл | Метод | Описание |
|---|---|---|
27010.py |
прямые | Ручной разрез по диаграмме (y > 15 для A; x, y + отсечение выбросов для B) |
27011.py |
k-средних | k_means с медоидами; стартовые точки вручную; файл B — отбор 3 выбросов |
27012.py |
DBSCAN | Кластеризация по eps; фильтрация шума; медоида + ответ по условию |
Все три файла одна и та же задача (2701_A.txt, 2701_B.txt), три способа разбиения на кластеры (см.р.6).