Подтема 2 из 6
Разделение на кластеры
Диаграмма рассеяния, координатные границы, аномалии, наклонные прямые и треугольники между ними, кольца и секторы, деление частиц по энергии.
Диаграмма рассеяния
Как понять, на какие группы делятся точки и где проходят границы?
Построй диаграмму рассеяния — каждая запись файла становится точкой на плоскости — и посмотри на неё. Функцию read_points из подтемы 1 считаем определённой выше в программе.
В табличном редакторе выдели два столбца координат и построй точечную диаграмму. Не линейный график: он соединит точки в порядке строк файла, а порядок строк ничего не говорит о том, какие точки рядом.
В Python можно использовать matplotlib, если эта библиотека доступна в твоей среде:
import matplotlib.pyplot as plt xs = []ys = [] for x, y in points: xs.append(x) ys.append(y) plt.scatter(xs, ys, s=3)plt.axis("equal")plt.grid()plt.show()Здесь scatter рисует отдельные точки, s=3 задаёт небольшой размер маркеров, axis("equal") устанавливает одинаковый масштаб по обеим осям, grid() добавляет сетку, show() показывает рисунок. Одинаковый масштаб важен: иначе круг может выглядеть вытянутым эллипсом, а расстояния по горизонтали и вертикали будут восприниматься неправильно.
Строй рисунок по самому файлу. Картинка в условии может быть только иллюстрацией и не соответствовать данным задания — такое предупреждение прямо встречается в условиях подборки.
После построения рисунка спроси себя:
- Сколько отчётливых групп видно?
- Есть ли между ними пустые полосы?
- Можно ли провести разделяющую прямую?
- Есть ли отдельно стоящие точки?
- Похожи ли группы на компактные облака, дуги, кольца или вытянутые цепочки?
От ответов зависит, чем делить: границами по координатам, углом, вычисленной величиной или цепочками соседей.
Координатные границы
Как записать правило, если на рисунке группы разделяет пустая полоса?
Провести по полосе границу и сравнивать с ней координату точки. В файле example27.txt все точки одной группы левее x = 5, а другой — правее:
points = read_points("example27.txt", has_header=True) clusters = [[], []] for point in points: x, y = point if x < 5: clusters[0].append(point) else: clusters[1].append(point) print(len(clusters[0]), len(clusters[1]))print(clusters[1])На диаграмме рассеяния граница видна сразу:
Почему это работает? Мы нашли правило, которое однозначно определяет принадлежность каждой точки: x < 5 — первый кластер, x ≥ 5 — второй.
Число 5 здесь — граница для конкретных данных, а не универсальная константа задания 27. Если группы разделены горизонтально, используется координата y: if y < 8:.
Три группы
Как разделить три группы, если одной границы мало?
Условия комбинируют цепочкой if / elif / else: выполняется первая истинная проверка, поэтому порядок проверок важен. означает «иначе, если». Например:
- Сначала выделяется нижняя группа:
y < 3. - Среди оставшихся точек выделяется левая:
x < 5. - Всё остальное попадает в третью.
Запусти цепочку по шагам на трёх точках и посмотри, какую ветвь проходит каждая:
clusters = [[], [], []] for x, y in [(2, 1), (3, 6), (8, 7)]: if y < 3: clusters[0].append((x, y)) elif x < 5: clusters[1].append((x, y)) else: clusters[2].append((x, y)) print(clusters)Для (2, 1) проверка в строке 4 истинна, и до строки 6 дело не доходит. Числа 3 и 5 — границы для этих точек: в другом файле их снова находят по рисунку.
Почему часто удобнее if / elif / else, а не несколько независимых if? При независимых проверках точка может удовлетворить сразу двум условиям:
if x < 5: clusters[0].append(point) if y < 3: clusters[1].append(point)Если у точки одновременно x < 5 и y < 3, она попадёт в оба списка. Цепочка if / elif / else выполняет только одну ветвь. Правильные границы она за тебя не выберет, но гарантирует, что каждая точка попадёт ровно в одну группу.
Аномалии
Что делать с точками, которые не входят ни в один кластер?
Задать области кластеров явно, а всё, что в них не попало, отложить в отдельный список аномалий. Аномалия — запись, которая не относится ни к одному из кластеров. Например, в демоверсии 2026 года сказано, что в файле Б три лишние точки и их нужно исключить.
Привычное последнее else здесь опасно: оно отправит в последний кластер всё, что не подошло раньше, — и аномалии тоже. Поэтому каждую область задают прямоугольником из двух условий через , а в else остаются только аномалии. Так устроен наш файл 27-2.txt: три кластера и три аномалии.
points = read_points("27-2.txt") clusters = [[], [], []]noise = [] for point in points: x, y = point if -8 < x < 0 and 2 < y < 10: clusters[0].append(point) elif 1 < x < 10 and 2 < y < 10: clusters[1].append(point) elif -2 < x < 5 and -7 < y < 1: clusters[2].append(point) else: noise.append(point) for cluster in clusters: print("Размер:", len(cluster)) print("Аномалий:", len(noise))print(sum(len(c) for c in clusters) + len(noise), len(points))И здесь все границы — только учебный пример для этого файла. Последние две строки — проверка: число аномалий совпало с условием, а все прочитанные точки распределены.
Совпадение количества — необходимая проверка, но не доказательство: одну точку можно потерять, а другую случайно учесть дважды. Поэтому стоит ещё раз взглянуть на рисунок: каждая граница должна проходить по пустому месту.
Наклонная граница
Как разделить группы, если пустая полоса между ними идёт наискосок?
Провести наклонную прямую и сравнить y точки с высотой прямой при том же x. Пусть группы разделяет прямая y = 2x + 1. Точки выше неё удовлетворяют условию y > 2x + 1, а ниже — y < 2x + 1:
if y < 2 * x + 1: clusters[0].append(point)else: clusters[1].append(point)Чтобы понять проверку, возьми конкретное x. Выражение 2*x + 1 вычисляет высоту прямой при этом x. Затем программа сравнивает с ней высоту точки.
Квадраты из условия — не способ разделения
В некоторых задачах сказано, что кластеры помещаются в прямоугольники или квадраты заданных размеров. Это ограничение на расположение точек, а не готовая инструкция, как их разделить. Из него нельзя автоматически заключить:
- что границы проходят через начало координат;
- что точки нужно группировать по
x // W; - что стороны прямоугольников параллельны осям;
- что ширину прямоугольника можно использовать как порог соседства.
В подборке есть условия, прямо разрешающие поворот квадратов относительно осей. Если прямоугольник заведомо расположен параллельно осям, для его точек необходимо x_max − x_min ≤ W и y_max − y_min ≤ H.
Но для повёрнутого прямоугольника его проекции на оси могут быть шире соответствующих сторон. Поэтому такая проверка уже не является универсальной.
Между прямыми
Как разделить точки, если кластеры лежат в треугольниках, которые образуют две наклонные прямые и оси координат?
Треугольник между прямыми — это несколько условий «по какую сторону прямой лежит точка», соединённых через and. В файле 27-8.txt прямые y = x и y = 10 − x вместе с осями ограничивают два треугольника — это кластеры. Точки вне треугольников — аномалии:
Обе области лежат правее оси y, выше оси x и ниже прямой y = 10 − x. Внутри этого большого треугольника их делит прямая y = x:
points = read_points("27-8.txt")clusters = [[], []]noise = [] for x, y in points: if x > 0 and y > 0 and y < 10 - x: if y < x: clusters[0].append((x, y)) else: clusters[1].append((x, y)) else: noise.append((x, y)) print(len(clusters[0]), len(clusters[1]), len(noise))Проверим на точке (6; 2): 6 > 0, 2 > 0 и 2 < 10 − 6 = 4 — она внутри большого треугольника, а 2 < 6 — значит, в нижнем. Точка (7; 5) не проходит третью проверку: 5 < 3 — ложь, это аномалия.
Расстояние и угол
Как разделить группы, которые не отделить прямой: кольца вокруг точки или узкие секторы?
Вычислить по координатам другую величину — расстояние до точки или угол направления — и делить по ней.
Например, группы могут располагаться в разных кольцевых областях вокруг известной точки C = (x₀; y₀). Тогда вычисляем расстояние до неё, r = √((x − x₀)² + (y − y₀)²), и разделяем точки по диапазонам r:
r = dist(point, (x0, y0)) if r < 3: ...elif r < 7: ...else: ...Границы опять выбираются по конкретным данным. Здесь точка C, относительно которой расположены кольца, не обязана быть тем «центром кластера», который затем требуется найти по сумме расстояний. У этих точек разное назначение.
Если группы лежат в разных секторах, разделяющим признаком может быть направление от известной точки C. Сектор можно представить как кусок круга между двумя лучами. Для точки, не совпадающей с C, угол направления вычисляется так:
from math import atan2, degrees angle = degrees(atan2(y - y0, x - x0))angle = angle % 360Здесь atan2 определяет угол направления с учётом обеих координат; degrees переводит его из радиан в градусы; % 360 приводит угол к диапазону от 0 включительно до 360 не включительно. Направление вправо соответствует 0°, вверх — 90°, влево — 180°.
Почему лучше не использовать обычное деление (y-y0)/(x-x0)? У противоположных направлений оно одинаковое: точки (1; 1) и (−1; −1) от начала координат дают одно и то же отношение 1. А на вертикальном луче получается деление на ноль.
Есть тонкость: направления 359° и 1° очень близки. Их нельзя считать далёкими только потому, что одно число почти 360, а другое почти 0. Чтобы проверить сектор, начинающийся с угла start и имеющий ширину width, удобно написать:
relative_angle = (angle - start) % 360 if relative_angle <= width: ...Так выглядит сектор, который переходит через направление 0°:
Не дели угол на ширину сектора автоматически. Из того, что ширина равна 20°, не следует, что границы проходят именно по 0°, 20°, 40° и так далее. Положение секторов тоже нужно установить. В файле 27-5.txt три сектора по 20° вокруг C = (2; −1):
По рисунку и углам крайних точек начала секторов — 350°, 110° и 230°:
from math import atan2, degrees points = read_points("27-5.txt")x0, y0 = 2, -1starts = [350, 110, 230]clusters = [[], [], []] for x, y in points: angle = degrees(atan2(y - y0, x - x0)) % 360 for k in range(3): if (angle - starts[k]) % 360 <= 20: clusters[k].append((x, y)) print([len(c) for c in clusters], len(points))Сумма размеров совпала с числом точек: каждая точка попала ровно в один сектор.
Энергия частиц
Как разделить частицы из задачи темы, если их координаты тут ни при чём?
Делить по величине, вычисленной по нескольким столбцам строки, — по кинетической энергии. По условию модуль разности энергий любых двух частиц одного кластера не больше R = 2. Посчитаем энергию каждой частицы файла 27-4.txt функцией energy из подтемы 1 и посмотрим на неё так же, как на точки, — только на одной оси:
Без рисунка большие промежутки находит программа: отсортируем энергии и выведем соседние значения, между которыми больше 0,5. Затем делим по границам из этих промежутков. Номер группы частицы — сколько границ её энергия уже превысила:
def energy_groups(particles, bounds): groups = [[] for _ in range(len(bounds) + 1)] for p in particles: k = 0 while k < len(bounds) and energy(p) > bounds[k]: k += 1 groups[k].append(p) return groupsПроверим и условие задачи: в каждой группе наибольшая и наименьшая энергии должны отличаться не больше чем на 2.
particles = read_particles("27-4.txt")es = sorted(energy(p) for p in particles) for a, b in zip(es, es[1:]): if b - a > 0.5: print(round(a, 3), round(b, 3)) groups = energy_groups(particles, [3.5, 7, 10.5]) for g in groups: e = [energy(p) for p in g] print(len(g), round(max(e) - min(e), 3))zip(es, es[1:]) даёт пары соседних значений отсортированного списка, а [[] for _ in …] создаёт нужное число пустых групп (имя _ пишут для переменной, которая не нужна). Ширина каждой группы меньше 2, значит, разбиение подходит под условие.
Итого
- Рисунокпо самому файлу, одинаковый масштаб по осям
- Границыцепочка
if / elif / else: каждая точка — ровно в одну группу - Аномалииявные области, остальное — в
noise; сверь количество с условием - Наклонная прямаяподставь x в уравнение и сравни с y
- Угол и расстояние
atan2и(angle − start) % 360;distдо точки - Энергиясортировка и большие промежутки; ширина группы ≤ R
Проверьте себя
Ответь сначала сам, потом открой ответ.
(4, 2), (6, 1), (3, 5), (7, 8) распределяются цепочкой: y < 3 — группа 1, иначе x < 5 — группа 2, иначе — группа 3. Куда попадёт каждая точка?ОтветСкрыть ответ
(4, 2) и (6, 1) — группа 1 (y < 3); (3, 5) — группа 2; (7, 8) — группа 3.x < 5 — в группу 1, y < 3 — в группу 2. Какие точки окажутся сразу в двух группах, а какие — ни в одной?ОтветСкрыть ответ
(4, 2) — сразу в двух группах; (7, 8) — ни в одной; (6, 1) — только в группе 2, (3, 5) — только в группе 1.(6, 2), (2, 6), (7, 5) и (-1, 3)?ОтветСкрыть ответ
(6, 2) — нижний треугольник: 2 < 4 и 2 < 6; (2, 6) — левый: 6 < 8, но 6 > 2; (7, 5) — аномалия: 5 > 3; (-1, 3) — аномалия: x < 0.ОтветСкрыть ответ
ОтветСкрыть ответ
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Два кластера по границе x = 6
В файле 27-1.txt записаны точки двух кластеров. Построй диаграмму рассеяния (по рисунку в Excel или своей программой), найди разделяющую границу и определи размер каждого кластера.
Введите два числа через пробел: размер левого кластера, затем правого.
Подсказка
По диаграмме рассеяния кластеры разделяет вертикальная прямая.
Решение
Граница x = 6: слева 118 точек, справа 87.
Решение программой
from math import dist def read_points(filename): points = [] with open(filename, encoding='utf-8-sig') as f: for line in f: parts = line.replace(',', '.').split() points.append((float(parts[0]), float(parts[1]))) return points def center_sum(cluster): best_point = cluster[0] best_sum = float('inf') for candidate in cluster: total = 0.0 for point in cluster: total += dist(candidate, point) if total < best_sum: best_sum = total best_point = candidate return best_point left, right = [], []for p in read_points('27-1.txt'): if p[0] < 6: left.append(p) else: right.append(p) print(len(left), len(right))Ответ: 118 87
Три кластера звёзд
В файле 27-3.txt записаны звёзды трёх кластеров: координаты и характеристика. Найди границы кластеров по диаграмме рассеяния и определи количество звёзд в каждом кластере. В ответе запиши три числа в порядке возрастания.
Введите три числа через пробел в том порядке, в каком их спрашивают.
Подсказка
Сначала горизонтальная граница отделяет нижний кластер, затем вертикальная делит верхние.
Решение
172, 198, 231: границы y = 5 (нижний кластер, 231 звезда) и x = 10 среди верхних (198 слева и 172 справа).
Решение программой
from math import dist def center_sum(cluster): best_point = cluster[0] best_sum = float('inf') for candidate in cluster: total = 0.0 for point in cluster: total += dist(candidate, point) if total < best_sum: best_sum = total best_point = candidate return best_point stars = []with open('27-3.txt', encoding='utf-8-sig') as f: for line in f: sx, sy, code = line.replace(',', '.').split() stars.append((float(sx), float(sy), code)) clusters = [[], [], []]for s in stars: if s[1] < 5: clusters[0].append(s) elif s[0] < 10: clusters[1].append(s) else: clusters[2].append(s) def is_kind(code, cls, lum): return code[0] == cls and code[2:] == lum print(*sorted(len(c) for c in clusters))Ответ: 172 198 231
Кластеры между прямыми
В файле 27-8.txt записаны точки двух кластеров и аномалии. Кластеры лежат в двух треугольниках, которые образуют прямые y = x, y = 10 − x и оси координат: нижний — под обеими прямыми, левый — над y = x и под y = 10 − x. Точки вне треугольников — аномалии. Сколько в файле аномалий и сколько точек в большем кластере?
Введите два числа через пробел в том порядке, в каком их спрашивают.
Подсказка
Внутри большого треугольника x > 0, y > 0 и y < 10 − x; кластеры делит прямая y = x.
Решение
Проверка сторон прямых: x > 0, y > 0, y < 10 − x, затем y < x или y > x.
Решение программой
def read_points(filename): points = [] with open(filename, encoding='utf-8-sig') as f: for line in f: parts = line.replace(',', '.').split() points.append((float(parts[0]), float(parts[1]))) return points counts = [0, 0]noise = 0for x, y in read_points('27-8.txt'): if x > 0 and y > 0 and y < 10 - x: counts[0 if y < x else 1] += 1 else: noise += 1print(noise, max(counts))Ответ: 4 170
Угол направления
Под каким углом из начала координат видна точка (-3, 3)? Чему равно degrees(atan2(3, -3)) % 360?
Введите число.
Подсказка
atan2(y, x) — угол точки; % 360 убирает отрицательные углы.
Решение
135°: точка смотрит влево-вверх, ровно между 90° и 180°.
Решение программой
from math import atan2, degrees print(f'{degrees(atan2(3, -3)) % 360:g}')Ответ: 135