Введение
Игротека уже определяет подходящие игры. Теперь нужен прозрачный список похожих альтернатив с объяснениями для человека. Вы вычислите расстояния по двум опубликованным признакам, покажете разности со знаком в исходных единицах и сохраните все совпадения на границе отбора.
Предполагаются понятия пригодности и MinMax из предыдущей работы, но среда независима: входы и готовая функция отбора уже есть, прежние файлы и процессы не нужны. Вы реализуете ранжирование и настоящие JSON, которые просмотрщик не дополняет. 50 минут — оценка автора. Сходство содержания не предсказывает предпочтения и не доказывает производительность на больших каталогах.
Измерьте каждую подходящую альтернативу
На этом шаге вы создадите полную таблицу расстояний и покажете изменения каждого кандидата относительно образца.
Откройте вкладку Terminal и перейдите в независимый каталог. Открытие или переподключение этого входа не переустанавливает рабочую папку и не стирает прогресс. cd задаёт рабочий каталог данного терминала; повторяйте в новых терминалах.
cd /home/labex/project/board-games
В каталоге есть уникальные game_id, отображаемые названия, время minutes, оценка complexity 1–5 и включительные границы min_players/max_players. Независимый CSV содержит по одному true/false для ID в другом порядке. night.json указывает образец, players и желаемое k. Даже недоступный или неподходящий образец допускается: условия вечера обязательны только для кандидатов. Другие ID с тем же названием или признаками законны, включая нулевую дистанцию. Исключается только точный reference_id.
cat показывает файл без изменений. Прочитайте запрос и вспомогательную функцию, реализующую предыдущую работу.
cat night.json
cat suitability.py
Перед добавлением вычисления расстояний запустите существующий интерфейс. --mode suitable вызывает choose_games; обязательный --output задаёт JSON для записи. Без других параметров входа запускатель читает catalog.csv, availability.csv и night.json из текущего каталога. Просмотрщик выводит лишь сохранённый вами файл.
python3 planner.py --mode suitable --output suitable.json
python3 show_results.py suitable.json
Вы должны увидеть шесть подходящих ID по алфавиту: left, near, raw, right, same и zero, с масштабом ниже. Это работающий результат пригодности, а не готовая рекомендация: в модуле ранжирования всё ещё пустые участки реализации.
choose_games связывает состояния по ID и возвращает всех подходящих кандидатов плюс масштаб, найденный по всему каталогу до фильтра. Порядок признаков: minutes, затем complexity. scale.min равен [10, 1], scale.range — [100, 4]. Применяйте одни параметры к образцу и кандидатам: выдача игры не двигает линейку. Целевых меток и разделения для обучения с учителем нет.
planner.py обрабатывает параметры и форматы через coursekit.py, но сам не выбирает и не ранжирует. Можно менять suitability.py, ranking.py и результаты; сохраните все CSV/JSON, planner.py, coursekit.py, show_results.py, оба starter-*.py.
MinMax превращает значение x в (x - minimum) / range. Постоянный признак сохраняет нулевой диапазон, но использует делитель один; образец входит в каталог, поэтому все соответствующие разности нулевые. Для масштабированных образца q и кандидата c евклидово расстояние равно sqrt((c_minutes-q_minutes)^2 + (c_complexity-q_complexity)^2). Оба признака после преобразования имеют одинаковый вес. Меньше расстояние — больше сходство по объявленному правилу, не больше удовольствия.
Исходное объяснение считается отдельно: minutes_delta и complexity_delta — кандидат минус образец до масштабирования. Отрицательная разность минут означает более короткую игру. Значения должны относиться к настоящему кандидату этого ID, а не другой строке и не выдумываться из расстояния.
Перед редактированием изучите заготовку ранжирования. from suitability import choose_games импортирует локальную вспомогательную функцию. Предоставленные строки prepared, by_id и reference связывают её вывод и запрос с записями каталога по ID. Пустой список rows, цикл кандидатов, сортировка и последний возврат создают окружающую структуру. Три комментария помечают участки, которые вы завершите; они не вычисляют недостающие расстояния или отбор.
cat ranking.py
Создайте резервную копию исходника. cp копирует текущий файл; эта копия — незавершённая основа, а не готовый ответ.
cp ranking.py ranking.py.bak
np.where выбирает один при нулевом диапазоне и настоящий диапазон в остальных случаях. np.linalg.norm даёт евклидову длину двухэлементного вектора разностей. float превращает значения NumPy в числа, совместимые с JSON. Предоставленный ключ сортировки — пара: сначала неокруглённое расстояние, затем game_id при точном равенстве расстояний. Короткая lambda определяет этот ключ для каждой строки. Не округляйте до сортировки. Малые арифметические различия между эквивалентными формулами могут менять порядок почти равных значений; отображаемое число десятичных знаков не является правилом сохранённого порядка.
Откройте модуль в nano. Как и в первой работе, LC_ALL=C.UTF-8 действует только на этот вызов редактора. Сохраняйте четыре пробела на уровень отступа; Python группирует инструкции с помощью отступов.
LC_ALL=C.UTF-8 nano ranking.py
Нажмите Ctrl+W, найдите ## Metric parameters и нажмите Enter. Перейдите ниже к следующему pass, нажмите Home, затем один раз Ctrl+K, чтобы удалить эту строку-заглушку. Вставьте пять строк с четырьмя ведущими пробелами. Сохраните rows = [] и окружающую функцию.
minimum = np.array(prepared['scale']['min'])
ranges = np.array(prepared['scale']['range'])
divisor = np.where(ranges == 0, 1, ranges)
query = np.array([reference['minutes'], reference['complexity']])
query_scaled = (query - minimum) / divisor
Затем нажмите Ctrl+W, найдите ## Candidate record и нажмите Enter. Перейдите ниже к pass этого участка, нажмите Home и один раз Ctrl+K. Вставьте следующий блок. Он находится внутри существующего цикла, поэтому инструкции начинаются с восьми пробелов; поля внутри rows.append используют двенадцать. Оставьте строку for, game = by_id[game_id] и последующую rows.sort без изменений.
values = np.array([game['minutes'], game['complexity']])
candidate_scaled = (values - minimum) / divisor
distance = float(np.linalg.norm(candidate_scaled - query_scaled))
difference = values - query
rows.append({
'game_id': game_id, 'title': game['title'], 'distance': distance,
'minutes_delta': float(difference[0]),
'complexity_delta': float(difference[1]),
})
Пока оставьте в участке ## Cutoff selection строки cutoff = None и selected = []. Это позволяет прочитать промежуточную таблицу, честно оставляя отбор незавершённым. Нажмите Ctrl+O, Enter, затем Ctrl+X, чтобы сохранить файл и выйти. Если заметите нежелательную правку до сохранения, Ctrl+X, затем N отбрасывает эту сессию редактора; откройте файл заново и исправьте только нужный участок.
Запустите planner.py в режиме recommend. --output задаёт реальный файл. Без других параметров читаются catalog.csv, availability.csv, night.json текущего каталога; --catalog, --availability, --request задают явные пути.
python3 planner.py --mode recommend --output recommendations.json
Покажите сохранённый JSON готовым просмотрщиком. Он открывает только этот файл, не пересчитывая каталог и не добавляя строки.
python3 show_results.py recommendations.json
Должно быть шесть строк. zero на расстоянии ноль; near и same около 0.1; left/right — 0.3. raw около 0.5001, хотя длиннее лишь на минуту: сложность выше на два пункта. Minutes +/- и Complexity +/- отображают исходные разности со знаком. Первая версия намеренно оставляет Cutoff: None и пустую выборку; правило добавим далее.
Если появляется SyntaxError или IndentationError, прочитайте имя файла и номер строки, снова откройте ranking.py и исправьте эту строку или участок. Не перезаписывайте весь модуль ради исправления одной строки. Ошибка отсутствующего файла обычно означает неверный каталог терминала: pwd показывает его, а начальная команда cd возвращает нужный, как в первой работе. cp ranking.py.bak ranking.py восстанавливает только незавершённую основу, если вы намеренно хотите отказаться от работы этого шага. Сохраняйте входные файлы, а не редактируйте их ради прохождения проверки.
Сохраните весь набор на границе
На этом шаге вы завершите отбор и запустите ту же программу для малого и пустого набора кандидатов.
Продолжайте во вкладке Terminal с таким рабочим каталогом.
cd /home/labex/project/board-games
k — желаемый минимум, когда подходящих игр достаточно, не обещание ровно k строк. Сначала вычислите и отсортируйте все допустимые расстояния. При N кандидатах граница — дистанция на позиции min(k, N). Индексы Python начинаются с нуля: индекс равен min(k, N) - 1. Сохраните всех кандидатов с неокруглённым расстоянием не больше cutoff + 1e-9. Абсолютный допуск 1e-9 определяет граничное равенство, не группы сортировки и не право заранее округлять.
При количестве меньше k возвращаются настоящие кандидаты. Без кандидатов верните пустые candidates, selected_ids и JSON-порог null (None в Python). Не подставляйте недоступную или неподходящую игру. Взять ближайшие k по всему каталогу и фильтровать после — значит рисковать потерять последующие законные альтернативы; предварительный отбор предотвращает ошибку.
Обычный запрос имеет k=2. Ближе всего zero; near и same делят границу согласно правилу. Нужны все три. Их порядок следует фактическим неокруглённым строкам, game_id разрешает точное равенство. Полная таблица из шести строк сохраняет информацию обо всех рассмотренных играх.
Создайте резервную копию работающей таблицы расстояний перед добавлением отбора. Эта копия сохраняет параметры метрики и записи кандидатов, которые вы только что проверили.
cp ranking.py ranking-distances.py.bak
Откройте тот же модуль, не заменяя предыдущую работу.
LC_ALL=C.UTF-8 nano ranking.py
Нажмите Ctrl+W, найдите ## Cutoff selection и нажмите Enter. Перейдите ниже к cutoff = None, нажмите Home, затем дважды Ctrl+K, чтобы удалить только эту строку и selected = []. Вставьте этот блок: четыре пробела перед if и else, восемь внутри каждой ветки. Последний возврат остаётся после блока с существующим отступом.
if rows:
cutoff = rows[min(request['k'], len(rows)) - 1]['distance']
selected = [row['game_id'] for row in rows
if row['distance'] <= cutoff + 1e-9]
else:
cutoff = None
selected = []
Проверка if rows предотвращает индексирование пустого списка. Списковое включение сохраняет подходящие ID в существующем отсортированном порядке. Сохраните параметры метрики, цикл кандидатов и сортировку из шага 1. Сохраните файл с помощью Ctrl+O, Enter и выйдите через Ctrl+X. Если нужно отказаться от этого шага, cp ranking-distances.py.bak ranking.py восстанавливает проверенную версию таблицы расстояний с ещё незавершённым отбором; повторите правку границы, а не весь файл.
JSON требует scale.min, scale.range в порядке признаков; для каждого кандидата — game_id, title, числовой distance, знаковые minutes_delta, complexity_delta; также cutoff, selected_ids. Храните конечные числа вычисленной точности, не округлённые строки. Безвредные дополнительные объяснения разрешены. Значения по ID сравниваются с абсолютным и относительным допусками 1e-8, отдельно от правила 1e-9. Эквивалентные формулы и обычные способы реализации допустимы; вкусы не оцениваются. Оба интерфейса должны работать: choose_games возвращает алфавитные eligible_ids и полный масштаб, rank_games — рекомендацию. Ранжирование может быть независимым и не вызывать помощника. Дополнительный файл пригодности в рабочем каталоге не нужен.
Пересоздайте и прочитайте обычный результат.
python3 planner.py --mode recommend --output recommendations.json
python3 show_results.py recommendations.json
Теперь выбраны zero, near, same, остальные строки остаются видны. cat recommendations.json показывает сохранённый JSON и неокруглённые расстояния.
Два других запроса меняют только вечер, не каталог или выдачи. Сначала прочитайте их через cat. --request выбирает запрос для одной команды, не меняя night.json или переменные терминала. Разные пути выхода сохраняют обычный результат.
cat sparse-night.json
python3 planner.py --mode recommend --request sparse-night.json --output sparse.json
python3 show_results.py sparse.json
Для восьми человек подходит только right, даже при k=5. Один честный кандидат — правильный ответ. Полный масштаб прежний, расстояние остаётся 0.3.
cat empty-night.json
python3 planner.py --mode recommend --request empty-night.json --output empty.json
python3 show_results.py empty.json
Двадцати участникам не подходит ни одна игра. Ожидайте пустую таблицу, Cutoff: None и явное сообщение. В Terminal команда cat empty.json показывает сохранённый порог null. Просмотрщик показывает ваш пустой результат, а не выбирает замену.
Нужны все три файла: recommendations.json, sparse.json, empty.json, соответствующие запросам и воспроизводимые из неизменённых входов. После случайной правки кода снова откройте nano и исправьте затронутый участок, сохраняя остальную завершённую работу; затем повторите все три команды. cp starter-ranking.py ranking.py возвращает только незавершённый starter для повторной практики. Открытие терминала не сбрасывает данные и не запускает setup.
Резюме
Вы измерили всех подходящих кандидатов в едином масштабе каталога, объяснили расстояния настоящими разностями со знаком и сохранили все граничные совпадения без предварительного округления. Три файла представляют обычный вечер, нехватку кандидатов и их отсутствие. Это прозрачное сходство содержания, а не предсказание того, что понравится человеку.



