はじめに
図書館は今夜利用できるゲームを判定できました。次は、人が選べるだけの説明を備えた、透明な類似候補一覧が必要です。二つの公開特徴から距離を求め、原単位の符号付き差を示し、選択境界の同順位をすべて残します。
前のラボの Python 適合判定と MinMax スケーリングを前提としますが、環境は独立しています。全入力と完成済みの補助関数があり、以前のファイルやプロセスは不要です。順位付けを実装し、実際の JSON を生成します。リーダーは結果を補完しません。50 分は著者の見積もりです。内容の類似性は個人の好みを予測せず、大規模カタログでの性能も実証しません。
適合する各候補を測る
このステップでは完全な距離表を作り、各候補が参照ゲームと比べて何を変えるか示します。
Terminal タブを開き、独立した作業場所へ入ります。この入口を開いたり再接続したりしても再インストールや進捗の消去はしません。cd はこのターミナルの作業ディレクトリを設定し、新しいターミナルでは再実行します。
cd /home/labex/project/board-games
カタログは一意な game_id キー、表示専用のタイトル、所要時間の minutes、1~5 の complexity 評価、両端を含む min_players/max_players を使います。別の貸出 CSV には各 ID の真偽値 true/false が異なる順であります。night.json は検索例、今夜の players、希望する k を指定します。参照自体が貸出不可や人数不適合でも構いません。今夜の条件が必要なのは返す候補だけです。同名別 ID や同じ特徴の別 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 の 6 つと、下記の尺度が見えるはずです。これは動く適合結果であって完成した推薦ではありません。順位付けモジュールにはまだ空の実装領域があります。
choose_games は ID で貸出状態を結び、全適合 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、2 つの starter-*.py は維持します。
値 x の MinMax は (x - minimum) / range です。一定の特徴は範囲ゼロを保存し、除数を 1 にします。検索例がカタログの一員なので、その特徴の差はすべてゼロです。スケーリングした参照 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、候補ループ、ソート、最後の return が周囲の構造を提供します。3 つのコメントが完成させる領域を示しますが、不足する距離や選択は計算しません。
cat ranking.py
ソースをバックアップします。cp は現在のファイルをコピーし、このバックアップは未完成の土台であって完成解答ではありません。
cp ranking.py ranking.py.bak
np.where は範囲ゼロなら 1、それ以外では本当の範囲を選びます。np.linalg.norm は 2 要素の差ベクトルのユークリッド長を返します。float は NumPy 値を JSON 対応の数値に変えます。提供済みのソートキーは、まず未丸めの距離、正確に等しければ game_id という組です。短い lambda が各行のキーを定義します。ソート前に丸めないでください。同等の式でも微小な演算差でほぼ等しい値の順が変わり得ます。表示する小数桁は保存値の順位規則ではありません。
nano でモジュールを開きます。最初のラボと同じく LC_ALL=C.UTF-8 はこのエディター起動だけに適用されます。Python は字下げで文をまとめるため、インデント 1 段に 4 空白を保ちます。
LC_ALL=C.UTF-8 nano ranking.py
Ctrl+W で ## Metric parameters を検索して Enter を押します。次の pass 行へ下がり、Home、Ctrl+K 1 回で仮置きの行を除きます。先頭 4 空白を持つ次の 5 行を入れます。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 1 回で除きます。次のブロックを入れます。既存ループ内なので文は 8 空白、rows.append 内のフィールドは 12 空白から始まります。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
候補は 6 行です。zero は距離ゼロ、near と same は約 0.1、left/right は 0.3 です。raw は 1 分長いだけでも約 0.5001 で、複雑さが 2 ポイント違うためです。Minutes +/- と Complexity +/- は原単位の符号付き差を表示します。この初版は意図的に Cutoff: None と選択 ID なしになります。次のステップで選択規則を実装します。
SyntaxError や IndentationError が出たらファイルと行番号を読み、ranking.py を開き直してその行や領域を修正します。1 行を直すためにモジュール全体を上書きしないでください。ファイル欠落エラーは作業ディレクトリが違うことが多く、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 境界(Python の None)を返します。貸出不可や人数不適合のゲームへ代替してはいけません。全カタログから適合判定より先に近い k 個を取ると、後の正当な候補を失い得ます。適合条件を先に扱う補助関数がそれを防ぎます。
通常の要求は k=2 です。zero が最も近く、near と same は同順位規則で境界を共有するため、3 つとも選ぶ必要があります。順は実際の未丸めソート済み行に従い、距離が正確に等しいときは game_id で決めます。何を検討したか見えるよう、6 行の候補表全体を残します。
選択を加える前に動く距離表をバックアップします。このコピーは試したばかりの距離パラメーターと候補記録を保ちます。
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 2 回でその行と selected = [] だけを除きます。次を入れ、if と else は 4 空白、分岐内は 8 空白にします。最後の return はブロック後の既存の字下げのまま残します。
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 を表示できます。
追加の準備済み要求 2 つは夜の条件だけを変え、カタログや貸出状態の入力は変えません。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
8 人が使えるのは right だけで、k=5 でも正直な 1 候補が正解です。尺度は全カタログのまま、距離も 0.3 のままです。
cat empty-night.json
python3 planner.py --mode recommend --request empty-night.json --output empty.json
python3 show_results.py empty.json
20 人に合うゲームはありません。表の行なし、Cutoff: None、適合ゲームなしの明示メッセージを期待します。Terminal で cat empty.json を使うと保存した境界が null と分かります。リーダーは空結果を表示しており、代用を選んでいません。
recommendations.json、sparse.json、empty.json の 3 ファイルがすべて必要です。各要求に対応し、変更していない入力から再実行できる必要があります。誤ったコード編集から回復するには nano を開き直し、他の完成作業を保って対象領域を直し、その後 3 コマンドをすべて再実行します。cp starter-ranking.py ranking.py は、再練習したい場合に未完成の開始コードだけを戻します。ターミナルを開き直してもデータのリセットや setup の再実行はしません。
まとめ
同じカタログ尺度ですべての適合候補を測り、実際の原単位の符号付き差で説明し、事前に丸めず境界の同順位をすべて選びました。三つのファイルは通常、候補不足、候補ゼロを扱います。これは透明な内容類似性であり、誰かが楽しむかどうかの予測ではありません。



