Кластеризация
В файле тысячи звёзд, собранных в кучки. Делим их на кучки, находим центр каждой и отвечаем на вопрос. Способ всегда один и состоит из пяти шагов.
- 1Что просят сделать
- 2Шаг 1. Читаем файл
- 3Шаг 2. Делим на кластеры
- 4Шаг 3. Подбираем порог
- 5Шаг 4. Находим центры
- 6Шаг 5. Отвечаем на вопрос
- 7Весь порядок на экзамене
- 8Ловушки
- 9Домашка
Что просят сделать
Это самый длинный номер, зато он всегда решается одинаково. Если разобраться один раз, дальше меняются только последние строки кода.
Простыми словами
Представь ночное небо, где звёзды собрались в несколько отдельных кучек. Кучка и есть кластер. Глазом кучки видно на графике, но компьютеру надо дать правило. Правило такое: если две звезды стоят ближе некоторого расстояния t, они в одной кучке. Если до звезды можно дойти по цепочке близких соседей, она тоже в этой кучке.
Что дано
Два файла. В файле А обычно 2 кластера, в файле Б обычно 3. В каждой строке три значения: координата x, координата y и класс звезды.
4,924550 4,721530 O7V 3,111640 6,900950 G1V
В ответ идут 4 числа: два для файла А и два для файла Б. Часто просят умножить на 10000 и взять целую часть.
План из пяти шагов
- Прочитать файл в список.
- Разделить звёзды на кластеры.
- Проверить, что кластеров столько, сколько в условии. Если нет, поменять порог.
- Найти центр каждого кластера.
- Посчитать то, что спрашивают.
Весь код в уроке запускается на маленьком учебном файле 27-demo.txt, в нём 36 звёзд и 3 кластера. Можешь менять код и смотреть, что произойдёт.
Шаг 1. Читаем файл
Читаем файл построчно и превращаем каждую строку в удобную запись.
from math import dist data = [] for line in open('27-demo.txt'): x, y, h = line.replace(',', '.').split() data.append(([float(x), float(y)], h)) print(len(data)) print(data[0])
Разберём строку за строкой:
line.replace(',', '.'): в файле дробные числа записаны через запятую, а Python понимает только точку..split()разрезает строку по пробелам на три части:x,y,h.float(x)превращает текст в число.data.append(([x, y], h))кладёт в список одну звезду: сначала координаты, потом класс.
Значит, у звезды p координаты лежат в p[0], а класс в p[1]. Это пригодится на всех следующих шагах.
Расстояние между звёздами считает dist из модуля math: dist(p[0], q[0]). Формулу писать самому не нужно.
Шаг 2. Делим на кластеры
Это главная часть. Идея такая: берём любую звезду и начинаем новую кучку. Находим всех её близких соседей и добавляем в кучку. Потом ищем соседей у соседей, пока новые не закончатся. Кучка готова, берём следующую свободную звезду.
from math import dist data = [] for line in open('27-demo.txt'): x, y, h = line.replace(',', '.').split() data.append(([float(x), float(y)], h)) t = 1.5 clusters = [] while data: clusters.append([data.pop(0)]) for p in clusters[-1]: sosedi = [p1 for p1 in data if dist(p[0], p1[0]) < t] clusters[-1].extend(sosedi) for p1 in sosedi: data.remove(p1) for cl in clusters: print(len(cl))
Что делает каждая строка:
while data: пока остались звёзды без кучки, продолжаем.clusters.append([data.pop(0)]): берём первую свободную звезду и начинаем с неё новую кучку.popубирает её изdata.for p in clusters[-1]: идём по текущей кучке. Важный момент: кучка растёт прямо во время цикла, поэтому до новых соседей цикл тоже дойдёт. Так и работает цепочка.sosedi = [...]: все свободные звёзды ближе порогаtк звездеp.extendдобавляет соседей в кучку, аremoveубирает их из свободных, чтобы никого не посчитать дважды.
Запусти код. Он напечатает размер каждого кластера.
Шаг 3. Подбираем порог
Порог t в условии не дан, его подбираем сами. Вот что происходит с учебным файлом при разных порогах:
| порог t | сколько кластеров | размеры |
|---|---|---|
| 0,2 | 33 | 1, 1, 1, 1, 1, 1, 1, 1 … |
| 0,5 | 16 | 9, 1, 1, 2, 2, 1, 8, 3 … |
| 1 | 5 | 14, 12, 2, 7, 1 |
| 1,5 | 3 | 14, 12, 10 |
| 4 | 3 | 14, 12, 10 |
| 6 | 1 | 36 |
Маленький порог рвёт кучки на части, большой склеивает разные кучки в одну. Нам нужен порог посередине.
Как подбирать
- Начни с
t = 1и посмотри на напечатанные размеры. - Кластеров больше, чем в условии, увеличь
t. - Кластеров меньше, чем в условии, уменьшай
t. - Число совпало и размеры выглядят как настоящие кучки, можно идти дальше.
На настоящих файлах это важно. В одном файле экзамена при t = 1 получается лишний кластер из одной точки, и нужно поднять порог. В другом при t = 1 два кластера склеиваются, и нужно опустить порог примерно до 0,6.
Если число кластеров совпало с условием и не меняется при небольшом изменении порога, ответ будет один и тот же. Кластер из одной-двух точек обычно значит, что порог мал.
Прямоугольник H×W в условии описывает, какого размера кучки. Для нашего кода он нужен только как подсказка: порог обычно гораздо меньше этих размеров.
Точки: [(0, 0), (0.5, 0.5), (1, 0.2), (5, 5), (5.4, 5.3), (9, 1), (9.3, 1.5), (9.8, 1.1), (4, 9)]. Две точки считаем связанными, если расстояние меньше 1. Сколько получится кластеров и сколько точек в самом большом? Запиши через пробел.
# пиши решение здесь
Подсказка
Возьми цикл из шага 2 с t = 1, только вместо чтения файла запиши точки списком.
Шаг 4. Находим центры
Центр это настоящая звезда из кучки, а не точка посередине. Представь, что все звёзды кучки ходят в гости к одной. Центр это та, к которой всем вместе идти меньше всего, то есть у неё наименьшая сумма расстояний до остальных.
Посчитаем на маленьком примере
Кластер из четырёх точек. Для каждой складываем расстояния до трёх других:
| точка | сумма расстояний |
|---|---|
| (1, 1) | 3,92 |
| (2, 1) | 3,62 |
| (1,5; 2) | 2,94 |
| (2; 2,5) | 4,01 |
Наименьшая сумма у точки (1,5; 2), она и есть центр.
То же в коде
from math import dist data = [] for line in open('27-demo.txt'): x, y, h = line.replace(',', '.').split() data.append(([float(x), float(y)], h)) t = 1.5 clusters = [] while data: clusters.append([data.pop(0)]) for p in clusters[-1]: sosedi = [p1 for p1 in data if dist(p[0], p1[0]) < t] clusters[-1].extend(sosedi) for p1 in sosedi: data.remove(p1) def centroids(cl): m = [] for p in cl: sm = sum(dist(p[0], p1[0]) for p1 in cl) m.append([sm, p]) return min(m)[1] centroid = [centroids(cl) for cl in clusters] for c in centroid: print(c)
- Для каждой звезды
pсчитаем суммуsmрасстояний до всех звёзд кучки. - В список
mкладём пары «сумма, звезда». min(m)находит пару с наименьшей суммой, а[1]достаёт из неё звезду.
В итоге centroid[0] это центр кластера clusters[0], и так далее. Номера совпадают, и это удобно на следующем шаге.
Кластер 1: [(1, 1), (2, 1), (1.5, 2), (2, 2.5)]. Кластер 2: [(8, 7), (9, 7), (8, 8), (9.5, 8.5), (8.5, 7.5)]. Найди центр каждого кластера и расстояние между центрами. В ответ запиши целую часть расстояния, умноженного на 1000.
# пиши решение здесь
Подсказка
Запиши каждый кластер списком и посчитай для точек сумму расстояний, как в таблице выше. Здесь точка это просто пара чисел, поэтому пиши dist(p, q).
Для кластера 2 из прошлого задания: сколько точек, кроме центра, лежат от центра на расстоянии не больше 1?
# пиши решение здесь
Шаг 5. Отвечаем на вопрос
Самый творческий шаг, но вопросы повторяются. Сначала пойми, про кого спрашивают.
Как читать класс звезды
Класс записан так: буква цвета, цифра, потом светимость. Например, K4IV значит оранжевый субгигант.
| буква | цвет | светимость | название |
|---|---|---|---|
| O | голубой | I | сверхгигант |
| B | бело-голубой | II | яркий гигант |
| A | белый | III | гигант |
| F | жёлто-белый | IV | субгигант |
| G | жёлтый | V | карлик |
| K | оранжевый | VI | субкарлик |
| M | красный |
Белые карлики записаны просто VII, без цвета и цифры.
Проверяем отдельно цвет и светимость: цвет это h[0], светимость это h[2:].
h[0] == 'K' and h[2:] == 'III' # оранжевый гигант h[0] == 'O' and h[2:] == 'VI' # голубой субкарлик
h.endswith('V') примет за карлика и субгиганта K4IV, ведь он тоже оканчивается на V. А h[2:] == 'V' сравнивает всю светимость целиком.
Частые вопросы и готовый код
| что спрашивают | код |
|---|---|
| расстояние между центрами | dist(centroid[0][0], centroid[1][0]) |
| расстояние от центра до точки | dist(centroid[0][0], (-1, -2)) |
| кластер с наименьшим числом точек | min(clusters, key=len) |
| сколько нужных звёзд в кластере | sum(1 for p in cl if условие) |
| сколько звёзд не дальше R от центра c | sum(1 for p in cl if dist(p[0], c[0]) <= R) |
| ближайшая нужная звезда к центру c | min(dist(p[0], c[0]) for p in cl if условие) |
| умножить на 10000, целая часть | int(x * 10000) |
Полный разбор вопроса
Вопрос: найди центр кластера, в котором меньше всего карликов, и выведи модули его координат, умноженные на 10000. Здесь обычный шаблон не подойдёт: нужно считать по каждому кластеру и запоминать, какой номер лучший.
from math import dist data = [] for line in open('27-demo.txt'): x, y, h = line.replace(',', '.').split() data.append(([float(x), float(y)], h)) t = 1.5 clusters = [] while data: clusters.append([data.pop(0)]) for p in clusters[-1]: sosedi = [p1 for p1 in data if dist(p[0], p1[0]) < t] clusters[-1].extend(sosedi) for p1 in sosedi: data.remove(p1) def centroids(cl): m = [] for p in cl: sm = sum(dist(p[0], p1[0]) for p1 in cl) m.append([sm, p]) return min(m)[1] centroid = [centroids(cl) for cl in clusters] def is_dwarf(h): return h[0] in 'OBAFGKM' and h[2:] == 'V' counts = [sum(1 for p in cl if is_dwarf(p[1])) for cl in clusters] print(counts) i = counts.index(min(counts)) c = centroid[i] print(int(abs(c[0][0]) * 10000), int(abs(c[0][1]) * 10000))
counts хранит число карликов по кластерам, а counts.index(min(counts)) находит номер кластера с наименьшим числом. Номер i подходит и для clusters, и для centroid.
В учебном файле 27-demo.txt три кластера. Найди наибольшее расстояние между центрами кластеров. В ответ запиши целую часть расстояния, умноженного на 10000.
from math import dist
data = []
for line in open('27-demo.txt'):
x, y, h = line.replace(',', '.').split()
data.append(([float(x), float(y)], h))
# дальше твой код
Подсказка
Перебери все пары: 0 и 1, 0 и 2, 1 и 2. Шаги 1–4 скопируй из кода выше.
В том же файле возьми кластер с наименьшим числом звёзд. Сколько в нём звёзд? Затем возьми кластер, центр которого дальше всего от начала координат: сколько в нём звёзд, кроме центра, не дальше 1 от центра? Запиши два числа через пробел.
from math import dist
data = []
for line in open('27-demo.txt'):
x, y, h = line.replace(',', '.').split()
data.append(([float(x), float(y)], h))
# дальше твой код
Подсказка
min(clusters, key=len). Для второго числа найди номер через dist(c[0], (0, 0)) и не считай сам центр.
Весь порядок на экзамене
Собери всё вместе. Удобно держать один файл с кодом и менять в нём только имя файла, порог и последние строки.
- Копируешь шаблон из шага 4 и пишешь имя файла А.
- Запускаешь и смотришь размеры. Подбираешь порог, пока число кластеров не совпадёт с условием.
- Читаешь вопрос для А и дописываешь последние строки, сверяясь с таблицей из шага 5.
- Записываешь два числа.
- Меняешь имя файла на Б и снова проверяешь порог: для него он может быть другим.
- Дописываешь вопрос для Б. Он обычно другой, поэтому код для А не подойдёт.
Файл Б большой, код может считаться несколько минут. Это нормально: не прерывай его, пока печатаются размеры кластеров.
Если кластеризуют не звёзды, а числа
Иногда кучки строятся не по расстоянию на плоскости, а по одному числу, например по энергии E = m(Vx² + Vy²) / 2. Код почти тот же: положи в начало записи список из одного числа, [E], а координаты и признак отдельно. dist прекрасно работает и со списками из одного числа. Порог снова подбираем по числу кластеров.
Ловушки
Запятые в числах: replace(',', '.'), иначе float падает.
Порог для файлов А и Б бывает разный. Проверяй размеры кластеров каждый раз.
Лишний кластер из одной-двух точек значит, что порог мал. Если таких точек не убрать, номера кластеров и ответ будут неверными.
Центр это звезда из кластера, а не среднее арифметическое координат.
Целая часть: int(x * 10000), без округления. Если просят модуль координаты, не забудь abs.
При подсчёте «не дальше R от центра» сам центр тоже попадает в выборку, ведь расстояние до себя равно 0. Прочитай условие: считать его или нет.
Светимость сравнивай целиком: h[2:] == 'V'. По концу строки субгигант IV примется за карлика.
Домашка
Задачи с настоящих экзаменов и из банка ФИПИ. Открой задачу и нажми «Решать здесь». Файлы задачи уже лежат рядом с кодом, поэтому в шаблоне поменяй только имя файла: оно написано в строке «Скачать файл». В ответ записывай сначала числа для файла А, затем для файла Б.
Задачи «Попробуй сам» учебные и считаются по учебному файлу, в домашке задачи с настоящих экзаменов и из банка ФИПИ. Код в блоках можно менять и запускать, он выполняется прямо в браузере.