Анализ данных: кластеризация

Как группировать записи по вычисленному признаку, находить центр группы и независимо проверять два результата.

Задание 27

Теория

Что хранит файл

Представь точки на листе бумаги. У каждой есть два числа: положение по горизонтали x и по вертикали y. Пара (2, 3) означает: вправо на 2, вверх на 3. Точки могут образовывать заметные группы. Прежде чем искать их, нужно понять, какие свойства точки даны в файле и какое именно свойство определяет группу.

Одна строка файла называется записью: её числа относятся к одному объекту и должны оставаться вместе. В старых задачах № 27 строка часто содержала только координаты. В проекте демоверсии 2027 года запись о частице длиннее: координаты, горизонтальная и вертикальная составляющие скорости, массу и обозначение типа. Группу определяют по вычисленной энергии, а координаты понадобятся позже для другого вопроса. Проект демоверсии ещё может измениться; здесь мы учимся читать правило конкретного условия, а не угадывать будущий вариант.

В файле могут быть десятичные запятые. Python ожидает в записи дробного числа точку, поэтому replace(",", ".") заменяет запятую перед float(...). Функция float превращает текст в число с дробной частью. split() разделяет строку по пробелам или табуляции. В отличие от примера с заголовком в уроке о задаче 26, здесь количество записей не стоит в первой строке: формат каждого файла всегда нужно читать заново. Открытие и чтение файлов подробно разобраны в уроке «Файлы».

with open("points.txt", encoding="utf-8") as source:    points = []    for line in source:        parts = line.split()        x = float(parts[0].replace(",", "."))        y = float(parts[1].replace(",", "."))        points.append((x, y)) print(points)  # [(0.0, 0.0), (2.0, 0.0), (8.0, 1.0)]

Для запуска создай рядом с программой файл из трёх строк: 0,0 0,0, 2,0 0,0 и 8,0 1,0. Метод append добавляет пару в список. Каждая пара хранится целиком: если отдельно переставить только x, связь с её y потеряется. Списки и их позиции подробнее разобраны в уроке «Списки».

Разберём на примере
Прочитайте запись целиком
В строке стоят 3,5 −2,0. Что окажется в списке после замены запятых и преобразования?
  1. Разделение даёт два текстовых поля: 3,5 и −2,0. В настоящем файле знак минуса записывают обычным символом -, как в коде Python.
  2. После замены запятой и float получаются числа 3.5 и -2.0.
  3. В список добавляется одна пара (3.5, -2.0), а не две независимые точки.

Группы на плоскости

Кластер — группа точек, объединённых правилом из условия. На рисунке две кучки можно увидеть глазами, но программе нужна точная граница. Например, пусть точки с x < 5 относятся к левой группе, а остальные — к правой. Точка с x = 5 попадёт вправо. Прямоугольники, окружности и другие области требуют других проверок; одного универсального теста «точки рядом» нет.

Две группы точек по координате xТочки с координатами x равными 0 и 2 лежат слева от границы x=5. Точки с x равными 5 и 8 лежат справа; точка на границе принадлежит правой группе.x < 5x ≥ 55
Пунктир — граница x = 5. Закрашенные точки слева, незакрашенные справа; точка на границе относится к правой группе.
points = [(0, 0), (2, 0), (5, 0), (8, 1)]left = []right = []for point in points:    if point[0] < 5:        left.append(point)    else:        right.append(point) print(left)   # [(0, 0), (2, 0)]print(right)  # [(5, 0), (8, 1)]

Иногда задача просит найти «центр» кластера. Нельзя автоматически подставлять среднее координат: условие может требовать выбрать одну из исходных точек. Представим три точки на прямой: (0, 0), (2, 0), (5, 0). Суммы расстояний от них до всей тройки равны 7, 5 и 8. Лучший кандидат — (2, 0), а не среднее координат (7/3, 0), которого вообще нет среди точек.

Расстояние между точками (x₁, y₁) и (x₂, y₂) на плоскости равно квадратному корню из суммы квадратов разностей координат. Такое расстояние называют евклидовым. В коде ** 0.5 берёт квадратный корень. Ниже мы намеренно проверяем каждого кандидата: на трёх точках такой прямой способ легко сверить вручную. В других условиях может использоваться иная мера близости; правило центра нужно читать буквально.

points = [(0, 0), (2, 0), (5, 0)]for candidate in points:    total = 0    for other in points:        dx = candidate[0] - other[0]        dy = candidate[1] - other[1]        total += (dx * dx + dy * dy) ** 0.5    print(candidate, total)# (0, 0) 7.0# (2, 0) 5.0# (5, 0) 8.0
Разберём на примере
Проверьте границу и центр
Точки (1, 0), (5, 0), (9, 0) делят по правилу x < 5. Куда попадёт средняя точка? Какая из трёх точек имеет наименьшую сумму расстояний до остальных?
  1. Точка (5, 0) не удовлетворяет строгому x < 5, поэтому относится к правой группе.
  2. Для всей тройки суммы расстояний равны 12, 8 и 12.
  3. Если искать центр именно всей тройки по сумме расстояний, это (5, 0). Центр правой группы по собственным точкам был бы отдельным вопросом.

Группы по энергии

В проекте демоверсии 2027 года близость частиц определяется не расстоянием на рисунке. Сначала для каждой частицы вычисляют кинетическую энергию по массе и двум составляющим скорости: E = m × (Vx² + Vy²) / 2. Здесь масса m, а Vx и Vy показывают скорость в двух направлениях. Если m = 2, Vx = 1 и Vy = 0, энергия равна 1. Координаты при этом не участвуют в вычислении энергии.

В каждой строке ниже шесть полей: x, y, Vx, Vy, m и обозначение типа. Текстовый тип оставляем строкой. Числовые поля переводим через float после замены десятичной запятой. Полученную энергию кладём рядом с остальными полями той же частицы; при сортировке запись переместится целиком. Сортировка записей по полю подробно разобрана в уроке «Сортировка и поиск».

lines = [    "0,0 0,0 1,0 0,0 2,0 II",    "1,0 0,0 1,0 0,0 3,0 V",    "8,0 1,0 1,0 0,0 10,0 II",]particles = []for line in lines:    parts = line.split()    x = float(parts[0].replace(",", "."))    y = float(parts[1].replace(",", "."))    vx = float(parts[2].replace(",", "."))    vy = float(parts[3].replace(",", "."))    mass = float(parts[4].replace(",", "."))    kind = parts[5]    energy = mass * (vx * vx + vy * vy) / 2    particles.append((energy, x, y, kind)) energies = []for record in particles:    energies.append(record[0])print(energies)  # [1.0, 1.5, 5.0]

Последний цикл берёт первый элемент каждой готовой записи — её энергию — и добавляет его в отдельный список. Как работают списки и добавление элементов, подробно разобрано в уроке о списках. В реальном файле вместо lines читают строки через open(...), как в первом разделе.

Теперь сформулируем правило группы. Пусть R = 2: разность наибольшей и наименьшей энергий всей группы должна быть не больше 2. После сортировки достаточно сравнивать очередную энергию с первой энергией текущей группы. Только сравнение с соседней не годится: у 1, 2,5 и 4 соседние разности равны 1,5, но между крайними разница 3. Все три в одну группу при R = 2 не входят.

energies = [15.5, 1.5, 10.0, 6.0, 2.0, 16.0,            5.0, 11.0, 1.0, 5.5, 10.5, 15.0]limit = 2.0clusters = []for energy in sorted(energies):    if len(clusters) == 0 or energy - clusters[-1][0] > limit:        clusters.append([energy])    else:        clusters[-1].append(energy) print(clusters)# [[1.0, 1.5, 2.0], [5.0, 5.5, 6.0],#  [10.0, 10.5, 11.0], [15.0, 15.5, 16.0]]

clusters[-1] — последняя созданная группа, а [0] — её первая, то есть наименьшая энергия. Пустой список проверяем отдельно, чтобы не обращаться к несуществующей группе. При разности ровно 2 частица остаётся в группе: новая начинается только при > limit. В данном примере между группами есть явные разрывы, а число групп совпадает с условием. В задаче, где допустимы несколько разбиений, такой короткий проход сам по себе не доказывает, что найдено требуемое: нужно использовать дополнительные правила.

Проход действует по определённому правилу: начинаем с наименьшей ещё не распределённой энергии и добавляем следующие по порядку, пока полный размах не превысит R. Затем начинаем новую группу. Когда условие гарантирует единственное разбиение, как в рассмотренном проекте демоверсии, дополнительно проверяем указанное число кластеров. Если гарантии нет, само ограничение на размах допускает разные разбиения, поэтому задача должна уточнить, какое из них требуется.

Разберём на примере
Сравните с началом группы
Энергии 1,0; 2,5; 4,0 уже отсортированы. Порог R равен 2. Можно ли поместить все три в один кластер?
  1. Первая и вторая отличаются на 1,5 — это допустимо.
  2. Третья отличается от второй тоже на 1,5, но от первой — на 3,0.
  3. Три вместе недопустимы. Соседние различия не заменяют проверку крайних энергий всей группы.
Неверно
Раз отсортированные соседние энергии отличаются не более чем на R, вся цепочка образует один кластер.
Как правильно
Разности могут накапливаться. Для 1,0; 2,5; 4,0 обе соседние разности равны 1,5, но полный размах равен 3,0 и нарушает R = 2.

Выбор центра

После выделения групп нужно ещё раз прочитать определение центра. Для частиц, сгруппированных по энергии, центр — одна из частиц группы, у которой сумма расстояний по энергии до остальных минимальна. Расстояние по энергии здесь означает abs(E1 - E2): функция abs убирает знак разности. Это не то же расстояние, что между точками на плоскости.

Возьмём энергии 1,0; 1,5; 2,0. Для первой сумма разностей 0 + 0,5 + 1,0 = 1,5. Для средней — 0,5 + 0 + 0,5 = 1,0. Для последней — снова 1,5. Центр — частица с энергией 1,5. Если упорядоченных значений пять, выгоднее всего третье: при движении кандидата к нему число точек справа больше числа точек слева, поэтому общая сумма убывает; после него — растёт. Это среднее по положению в упорядоченном списке значение называют медианой. Оно не обязано равняться арифметическому среднему.

Индексы списка начинаются с нуля. Для трёх энергий середина имеет индекс 3 // 2 = 1; знак // берёт целую часть результата деления. В проекте демоверсии гарантирован единственный центр каждого кластера. Без такой гарантии у группы из чётного числа частиц две средние частицы могут давать одинаковую минимальную сумму. Равенство бывает и при нечётном числе частиц: для энергий 1, 1, 2 суммы разностей равны 1, 1 и 2. Обе частицы с энергией 1 делят минимум. Если условие не обещает единственный центр, нельзя произвольно выбрать одну частицу: нужно искать правило разрешения равенства.

clusters = [[1.0, 1.5, 2.0], [5.0, 5.5, 6.0]]for cluster in clusters:    centre = cluster[len(cluster) // 2]    total = 0    for energy in cluster:        total += abs(centre - energy)    print(centre, total)# 1.5 1.0# 5.5 1.0
Разберём на примере
Не путайте два средних
Энергии трёх частиц равны 1, 2 и 9. Какую из них выбирает правило минимальной суммы модулей разностей?
  1. Для 1 сумма 0 + 1 + 8 = 9; для 2 — 1 + 0 + 7 = 8; для 9 — 8 + 7 + 0 = 15.
  2. Выбирается частица с энергией 2 — средняя в отсортированном списке.
  3. Арифметическое среднее равно 4; такой частицы вообще нет. Подменять им центр нельзя.

Второй расчёт

Вторая часть задачи может спрашивать уже не об энергии. Например, найдём наибольшее расстояние между частицами типа II внутри одного кластера. Здесь одновременно действуют два фильтра: тип обеих частиц равен II и кластер у них общий. Координаты нужны только после группировки по энергии. Пара точек из разных кластеров не подходит, даже если расстояние между ними самое большое во всём файле.

Если файл уже указывает имя кластера в каждой строке, записи удобно собрать в словарь: имя служит ключом, а связанное с ним значение — список точек этой группы. Метод values() позволяет перебрать именно эти списки, не обращаясь к именам. Как устроены ключи и значения, подробно объясняет урок «Словари». В нашем примере группы уже собраны в списке, поэтому словарь не требуется.

Для точек с координатами (x₁, y₁) и (x₂, y₂) сначала найдём квадрат расстояния: (x1 - x2)² + (y1 - y2)². Чем больше этот квадрат, тем больше и само расстояние: квадратный корень не меняет порядок неотрицательных чисел. Поэтому внутри циклов достаточно сравнивать квадраты, а корень взять один раз в конце.

clusters = [    [(0, 0, "II"), (2, 3, "II"), (20, 20, "V")],    [(0, 0, "II"), (1, 4, "II"), (30, 30, "III")],]best_square = 0for cluster in clusters:    selected = []    for point in cluster:        if point[2] == "II":            selected.append(point)    for left in range(len(selected)):        for right in range(left + 1, len(selected)):            dx = selected[left][0] - selected[right][0]            dy = selected[left][1] - selected[right][1]            square = dx * dx + dy * dy            if square > best_square:                best_square = square print(best_square)  # 17print(int(best_square ** 0.5 * 10000))  # 41231

Внутренний цикл начинается с left + 1, чтобы не сравнивать точку с собой и не считать одну пару дважды. В первой группе квадрат расстояния равен 13, во второй — 17. Далёкие точки другого типа намеренно пропущены. int оставляет целую часть положительного результата после умножения на 10 000: для корня из 17 получается 41231. Это не округление до ближайшего целого.

Если в одной группе после отбора осталось m подходящих точек, цикл сравнит m × (m - 1) / 2 пар. Для 100 точек это 4 950, а для 10 000 — уже 49 995 000. Поэтому сначала отбирай нужный тип и оценивай число сравнений. Однократный корень убирает лишнюю дорогую операцию, но не сокращает число пар. Если после фильтрации их всё ещё слишком много, нужен отдельный более быстрый геометрический алгоритм; нельзя выдавать этот прямой перебор за быстрое решение любого файла.

Разберём на примере
Отделите условие пары от её расстояния
В одном кластере есть II в (0, 0), II в (3, 4) и V в (100, 100). Какую пару следует проверить?
  1. Частица V не проходит фильтр по типу, каким бы далёким ни было её положение.
  2. Остаётся одна пара частиц II: (0, 0) и (3, 4).
  3. Квадрат расстояния 3² + 4² = 25, само расстояние 5.

Два результата

Соединим шаги на собственном коротком наборе. В каждой записи ниже лежат x, y, Vx, Vy, m и тип. Для наглядности скорость всегда равна (1, 0), поэтому энергия равна половине массы; в файле с иной скоростью программа применит ту же полную формулу. Четыре группы разделены по энергиям при R = 2. В каждой три частицы с разными энергиями, поэтому средняя по энергии частица — единственный центр.

Первое число Q1 — наибольшее расстояние между двумя частицами II одного кластера. Второе Q2 — наибольшая энергия среди четырёх центров. Это независимые расчёты: центр не обязан быть типа II и не участвует автоматически в паре для Q1.

Чтобы получить второе число в программе, сохраним энергии центров в список centre_energies. Вызов max(centre_energies) выбирает из этого списка наибольшее число.

particles = [    (0, 0, 1, 0, 2, "II"), (0, 2, 1, 0, 3, "V"),    (3, 0, 1, 0, 4, "II"),    (0, 0, 1, 0, 10, "II"), (5, 5, 1, 0, 11, "V"),    (2, 3, 1, 0, 12, "II"),    (0, 0, 1, 0, 20, "II"), (20, 20, 1, 0, 21, "III"),    (1, 4, 1, 0, 22, "II"),    (0, 0, 1, 0, 30, "II"), (50, 50, 1, 0, 31, "V"),    (2, 2, 1, 0, 32, "II"),]records = []for x, y, vx, vy, mass, kind in particles:    energy = mass * (vx * vx + vy * vy) / 2    records.append((energy, x, y, kind))records = sorted(records, key=lambda record: record[0]) clusters = []for record in records:    if len(clusters) == 0 or record[0] - clusters[-1][0][0] > 2:        clusters.append([record])    else:        clusters[-1].append(record) centre_energies = []best_square = 0for cluster in clusters:    centre = cluster[len(cluster) // 2]    centre_energies.append(centre[0])    selected = []    for record in cluster:        if record[3] == "II":            selected.append(record)    for left in range(len(selected)):        for right in range(left + 1, len(selected)):            dx = selected[left][1] - selected[right][1]            dy = selected[left][2] - selected[right][2]            square = dx * dx + dy * dy            if square > best_square:                best_square = square q1 = best_square ** 0.5q2 = max(centre_energies)print(int(q1 * 10000), int(q2 * 10000))  # 41231 155000

Строка for x, y, vx, vy, mass, kind in particles раздаёт шесть полей записи шести именам слева по порядку. После добавления энергии запись становится четвёркой (энергия, x, y, тип), поэтому record[0] — энергия, а record[1] и record[2] — координаты. lambda record: record[0] сообщает sorted, по какому полю расположить целые записи; это тот же приём из урока о сортировке.

Разберём на примере
Проверьте обе части без программы
Почему программа печатает именно 41231 155000?
  1. Энергии центров четырёх групп: 1,5; 5,5; 10,5; 15,5. Наибольшая — 15,5, поэтому второй результат 155000.
  2. Среди разрешённых пар II самый большой квадрат расстояния равен 17: это точки (0, 0) и (1, 4) в третьем кластере.
  3. Корень из 17 после умножения на 10 000 имеет целую часть 41231. Сначала записываем Q1, затем Q2.

Проверка и границы

На маленьком файле выпиши для каждой частицы энергию, номер группы и тип. Затем отдельно пересчитай сумму разностей для возможных центров и расстояния только между частицами II внутри одной группы. Прямой перебор на таком наборе — независимая проверка более короткого способа с сортировкой и медианой. После этого можно запускать программу на полном файле.

Проверьте путь от строки до ответа
  1. Разбери поля. Проверь число строк, десятичные запятые, тип частицы и то, что координаты, скорость и масса остаются у одной записи.
  2. Проверь группы. Вычисли энергию, отсортируй записи целиком, сравни максимальную энергию каждой группы с минимальной и сверь число групп с условием.
  3. Проверь центр и пары. Центр выбирай среди частиц по сумме разностей энергий; пространственные расстояния считай только для разрешённых пар II одного кластера.
  4. Проверь форму ответа. Сверь два показателя независимо, порядок, множитель 10 000 и взятие целой части вместо округления.

Разные варианты № 27 меняют определение кластера и центра. В предоставленной подборке встречаются и прежние задачи на суммы или произведения пар с ограничением по делимости. Там тоже важно не перебирать огромный файл вслепую, но группировка точек по энергии к ним не применяется. Этот урок подробно учит одному современному семейству и способу проверять условие; для другого семейства потребуется свой вывод алгоритма.

Дробные числа в Python хранятся приближённо. Поэтому при значении вплотную к порогу разности или к целому после умножения нужно дополнительно сверить вычисление. В задачах этого урока данные подобраны так, чтобы погрешность хранения не меняла ответ. Условие с произвольными точными десятичными границами потребует отдельного способа точного счёта, а не произвольной добавки к сравнению.

На экзамене

В актуальных материалах ФИПИ задание 27 связано с анализом данных и кластеризацией. Проект демоверсии 2027 года пока не окончательный: конкретные поля, порог и показатели могут измениться. На экзамене начни с определения группы и центра в данном условии, затем назови данные, которые участвуют в каждом числе ответа.

Если файл большой, оцени число пар после фильтрации по типу, а не по общему числу строк. На небольшом наборе отдельно проверь строку на границе кластера, среднюю частицу, пару из разных групп и правило взятия целой части.

Практика

Практика временно недоступна. Можно продолжить читать теорию.

Итог

Что получилось

Теперь ты можешь прочитать запись целиком, вычислить признак группировки, проверить ограничение на всю группу и выбрать центр по точному правилу условия. Для второго результата ты отбираешь только разрешённые пары и сравниваешь их расстояния отдельно от поиска центра.

Решай задачи по порядку. Если ответ не сошёлся, вернись к малому набору: пересчитай энергии, крайние значения каждого кластера, среднюю частицу и пары типа II. Повтори задачу без подсказки лишь после того, как сможешь объяснить, почему программа оставляет именно эти записи и печатает именно эти два числа.

Проверьте себя
Можно ли после чтения отсортировать только энергии, забыв координаты и тип?
Нет. Переставлять нужно целые записи: координаты и тип ещё потребуются для второго результата.
Энергии 1, 2,5 и 4 имеют соседние разности 1,5. Почему при R = 2 они не составляют один кластер?
Размах всей тройки равен 4 − 1 = 3. Он превышает R, хотя каждая соседняя разность мала.
Чем медианная частица отличается от среднего значения энергий?
Центр выбирают среди самих частиц по минимальной сумме модулей разностей; арифметическое среднее может не принадлежать файлу.
Можно ли сравнить две частицы II из разных кластеров при поиске Q1?
Нет. Обе должны иметь тип II и принадлежать одному кластеру. Энергия нужна для группировки, координаты — для расстояния.