適合するゲームと似たゲームを分ける

PythonBeginner
オンラインで実践に進む

はじめに

小さなボードゲーム図書館で、今夜遊ぶための代替候補を探します。好きなゲームに似ていても、貸出中だったり人数に合わなかったりすることがあります。このラボでは、利用できるゲームの判定と数値的な特徴の比較を分けて考えます。

再実行できる適合判定プログラムと、実際の JSON 結果を作ります。Python の関数と辞書、NumPy の基本配列、表の読み方が前提です。k近傍法による分類を知っていると役立ちますが、ここには予測するラベルはありません。Python と NumPy は準備済みで、scikit-learn は不要です。45 分は、読む時間と通常の修正を含む著者の見積もりです。

今夜利用できるゲームを判定する

このステップでは実際の図書館カタログを調べ、準備済み関数の 1 領域に今夜の適合条件を実装します。

Terminal タブを開きます。保存ファイルを読むリーダーを含め、全コマンドはここで実行します。Terminal を開いたり再接続したりしても作業領域を再インストール・リセットしません。cd はこのターミナルの現在のディレクトリを変え、別のターミナルは独自のディレクトリを持ちます。

cd /home/labex/project/board-games

実際の入力から始めます。CSV はヘッダーの後にカンマ区切りの行が続きます。cat はファイルを変えずに表示します。これは完全で未絞り込みのカタログで、すでに今夜の条件を満たしたゲームの一覧ではありません。

cat catalog.csv

各行は異なるゲームの版を表します。一意で空ではない game_id が、その版を探して対応付けるキーです。title は表示テキストで、Harbor の複数の版は ID が異なり、別々に保つ必要があります。minutes は正の所要時間の見積もり、complexity は大きいほど複雑な合成の 1~5 評価です。整数の min_players と max_players は両端を含みます。合成入力は完全で ID ごとに 1 行あり、不正データの修正は演習の対象外です。

cat availability.csv

別の貸出表には、すべてのカタログ ID に対して別順で 1 行ずつあります。CSV テキストの true/false は準備済みローダーで Python 真偽値の True/False に変わります。キーによる結合とは同じ game_id を探すことで、1 行目同士を付けることではありません。一意なキーと ID ごとに 1 行の貸出状態によって、曖昧な対応や欠落を防ぎます。この検索を実装します。

JSON は名前付きの値、リスト、オブジェクトを保存します。今夜の要求を読みます。

cat night.json

reference_id は似せたい例、players は人数、k は希望する近い代替の数で次のラボで使います。参照は検索例なので、それ自体が貸出不可や人数不適合でも構いません。今夜の適合条件が必要なのは返す代替だけです。正確な参照 ID だけを除き、同名や同じ特徴を持つ全ゲームを除外しないでください。生の入力の概要は、まだこの規則を適用したり適合結果を作ったりしていません。

貸出可能で 4 人に対応する 3 ゲームを考えます。Harbor は 60 分・複雑さ 3、Quay は 61 分・5、Bridge は 90 分・3 です。原単位の差は候補から参照を引いた値です。Quay は 1 分長く複雑さ 2 ポイント増、Bridge は 30 分長く複雑さは同じです。Python の -c は引用された計算を 1 引数として実行し、セミコロンは 2 つの Python 文を区切ります。

python3 -c "print('Quay minutes:', 61 - 60); print('Bridge minutes:', 90 - 60)"

1 と 30 を期待します。生のユークリッド距離は特徴差を二乗して足し、平方根を取ります。単位をそのまま混ぜると 30 分が複雑さ 2 ポイントを圧倒し得ます。次に共通尺度を定義しますが、先に比較に参加できるゲームを決めます。

編集できるのは suitability.py、ranking.py、結果ファイル、自分のスクリプトのバックアップです。提供済みの全 CSV/JSON 入力、planner.py、coursekit.py、show_results.py、両方の starter-*.py 復元用コピーは維持します。planner.py は公開関数を選び、ファイル読み取り、型変換、JSON 保存を coursekit.py に委ねます。どちらも業務上の判断はしません。このラボでは ranking.py を未完成のままにします。

準備済みモジュールを読みます。

cat suitability.py

choose_games(catalog, availability, request) は読み込まれた辞書のリストと要求辞書を受け取ります。ラベル付きの 2 領域は eligible = [] と scale = {} で始まります。これは未完成の仮置きで、適合ゲームがないという宣言ではありません。最後の return が ID と尺度を実行役に渡します。今は適合領域を置き換え、尺度の仮置きはステップ 2 用に残します。setup や入力の概要は採点対象の結果を作りません。

モジュールをバックアップします。cp SOURCE DESTINATION は指定ファイルだけをコピーし、.bak はバックアップの接尾辞です。ミスの後にこのバックアップを上書きしないでください。

cp suitability.py suitability.py.bak

nano は Terminal 内で既存ファイルを編集します。コマンドに付けた LC_ALL=C.UTF-8 はエディターだけを UTF-8 にし、全体のシェル設定を変えません。nano のショートカット欄で ^ は Ctrl を押す意味です。矢印でカーソルを動かし、入力すると文字を挿入します。Ctrl+W、検索語、Enter で文字を探せます。Home は行頭へ移動し、Ctrl+K は行全体を切り取ります。Python はインデントを使います。以下の行は関数内で 4 空白から始まり、ループや条件内ではさらに字下げします。

LC_ALL=C.UTF-8 nano suitability.py

まず不要な編集を破棄する練習です。Ctrl+W と Enter で ## Candidate identities を探し、End を押してコメントに temporary を加えます。Ctrl+X、保存の質問に N を押します。ファイルを変えずに終了します。開き直します。

LC_ALL=C.UTF-8 nano suitability.py

eligible = [] を探し、Home、Ctrl+K 1 回でその仮置きの 1 行だけを除きます。関数のヘッダー、尺度の領域、最後の return を残します。そこに次の字下げした行を入れます。辞書内包表記は ID から貸出可否への検索表を作ります。ループは独立した 3 条件を and で結び、連鎖する <= は人数の両端を含みます。append は表示名ではなく一致した ID を加えます。

    status = {row['game_id']: row['available'] for row in availability}
    eligible = []
    for game in catalog:
        different_id = game['game_id'] != request['reference_id']
        fits_group = game['min_players'] <= request['players'] <= game['max_players']
        if different_id and fits_group and status[game['game_id']]:
            eligible.append(game['game_id'])

Ctrl+O、Enter で変わっていないファイル名を確認し、Ctrl+X で保存して終了します。保存しても関数は実行しません。既存の return は sorted(eligible) で入力行順とは独立したアルファベット順の ID 一覧を作り、scale は空オブジェクトに残します。

実行役の --mode suitable は choose_games を選び、必須の --output suitable.json が保存先です。既定はこのディレクトリの catalog.csv、availability.csv、night.json で、--catalog、--availability、--request で別パスを選べます。入力は読み取り専用のままです。

python3 planner.py --mode suitable --output suitable.json

提供済みリーダーは保存した JSON だけを開きます。欠落ファイルを作ったり、カタログから何かを計算したりしません。

python3 show_results.py suitable.json

Scale: {} と ID left, near, raw, right, same, zero を期待します。same と zero はどちらも別の Harbor 版で、zero は数値特徴まで参照と同一です。blocked は貸出不可、small は 4 人不可、ref は正確な参照です。2 つの入力表と ID を比較してください。適合判定は完成しましたが、尺度は意図的に未実装です。

次のディレクトリ誤りによる失敗は意図的です。1 階層上へ移ります。

cd /home/labex/project
python3 planner.py --mode suitable --output suitable.json

Python は can't open file と No such file or directory を報告し、ここでは実行役を見つけられません。既存のプログラムと結果はリセットされていません。pwd でこのターミナルのディレクトリを確認し、作業場所へ戻って実際のコマンドを再実行します。

pwd
cd /home/labex/project/board-games
python3 planner.py --mode suitable --output suitable.json
python3 show_results.py suitable.json

編集エラーでは Python のファイル名と行番号を読み、nano でモジュールを開き直して対象領域だけを直します。SyntaxError はコロンや引用符の不足、IndentationError は先頭空白への注意が必要なことをよく示します。保存して終了し、再実行します。Ctrl+X、N は未保存の変更を放棄します。必要なら cp suitability.py.bak suitability.py でこの未完成の土台だけを戻せるので、その後は適合領域を再実装します。バックアップも保護された starter-suitability.py も完成解答ではありません。回復のために setup を再実行したり入力を変えたりしないでください。

カタログ全体で同じ尺度を使う

このステップでは、完成した適合ロジックを保ち、尺度の領域を実装します。

引き続き Terminal で作業します。ディレクトリを明示すれば、新しいターミナルでも安全にコマンドを実行できます。

cd /home/labex/project/board-games

特徴は比較に使う 1 つの数値属性です。順番は常に minutes、次に complexity です。人数境界と貸出可否は適合条件で、距離の特徴ではありません。参照、貸出不可、人数不適合も含む全カタログで各特徴の最小値・最大値を求めます。範囲は最大値から最小値を引いたものです。MinMax スケーリングは x を (x - minimum) / range に変え、各特徴をカタログ内の範囲に対する割合で表します。

このカタログの最小値は [10, 1]、範囲は [100, 4] です。Quay の差は [1/100, 2/4]、Bridge の差は [30/100, 0/4] になり、スケーリング後のユークリッド距離は約 0.5001 と 0.3 です。宣言した距離規則によって順位は変わりますが、どちらの単位系も個人の好みは予測しません。スケーリング後の特徴に同じ重みを付けるのは、この演習で明示した設計上の選択です。

カタログは距離規則を定義するもので、教師あり訓練集合ではありません。目的ラベルも訓練/テスト分割もありません。貸出状態だけを変えて尺度や残る候補の距離が変わってはいけません。適合条件で絞った後に最小値と範囲を再計算すると、ゲームが借りられるたびに物差しが動いてしまいます。

全カタログで一定の特徴には、本当の範囲 0 を保存します。後の距離計算ではその特徴の除数を 1 とします。参照自体がカタログの一員なので、全候補との差はその特徴ではゼロです。情報を作り出さずにゼロ除算を避けられます。

完成した適合段階の新しいバックアップを作り、元の土台のバックアップは残します。

cp suitability.py suitability-ids.py.bak
LC_ALL=C.UTF-8 nano suitability.py

Ctrl+W と Enter で scale = {} を探し、Home、Ctrl+K でその仮置きの 1 行だけを切り取ります。上の完成した ID 検索とループは変えません。次の字下げした 4 行を入れます。np.array は公開した特徴順の 2 列の数値表を作り、axis=0 は各列を下方向に見て最小・最大を求めます。tolist() は配列を JSON に保存できる通常のリストへ変換します。

    features = np.array([[game['minutes'], game['complexity']] for game in catalog])
    minimum = features.min(axis=0)
    ranges = features.max(axis=0) - minimum
    scale = {'min': minimum.tolist(), 'range': ranges.tolist()}

既存の return に、この計算した scale と同じソート済み ID が含まれるようになりました。Ctrl+O、Enter、Ctrl+X で保存して終了します。前の作業を置き換えたり別プログラムを追記したりせず、領域を追加できました。

結果には、各適合 ID をアルファベット順に 1 回ずつと、scale.min、scale.range の各リストに数値 2 つが必要です。整形した文字列ではなく数値を保存します。無害な追加説明フィールドは許可します。数値比較の絶対・相対許容差は 1e-8 で、通常の浮動小数点計算を許すものであり、異なる物差しを許すものではありません。

変更していない元の入力から結果ファイルだけを再生成します。

python3 planner.py --mode suitable --output suitable.json
python3 show_results.py suitable.json

最小値 10 と 1、範囲 100 と 4、同じ 6 つの適合 ID を期待します。リーダーが 10 でなく 10.0 を表示しても、どちらも数値の 10 です。保存構造を直接調べます。

cat suitable.json

編集で適合領域を壊したら、cp suitability-ids.py.bak suitability.py は動作していたその段階だけを戻します。nano を開き直し、尺度の領域を再実装します。一方 cp starter-suitability.py suitability.py は未完成の土台に戻して両領域を失うので、意図的に最初からやり直す場合だけ使います。普通の修正では該当行を直し、保存、終了、再実行してください。提供済みの両 starter と全入力を維持します。Terminal の再接続で setup が実行されたりバックアップが戻されたりすることはありません。

自分で確認してください。今夜の適合条件は、この全カタログの物差しを変えずに変わり得ます。次の独立したラボでは完全な適合関数が提供され、距離と選択に集中できます。

まとめ

今夜の利用条件と内容の類似性を分離し、安定した ID で独立した記録を対応付けました。同名や数値差ゼロの正当な別ゲームも残し、カタログ全体から固定の MinMax 尺度を求めました。suitable.json は実際に再生成できる成果物です。次のラボではこの知識を使い、独立した環境で候補の距離と理由を示します。