import tkinter as tk from tkinter import ttk, messagebox import pandas as pd import numpy as np from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler import json import os # --- 設定 --- DATA_FILE = "sample.cvd" class MatchingApp: def __init__(self, root): self.root = root self.root.title("AI Compatibility Matcher") self.root.geometry("600x700") # 1. データの読み込みと前処理 self.load_and_process_data() # 2. UIの構築 self.create_widgets() # 3. 初期表示 self.update_student_list() def load_and_process_data(self): # --- データ読み込み (main.pyと同様のロジック) --- if os.path.exists(DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: raw_data = json.load(f) except Exception: raw_data = self.get_default_data() else: raw_data = self.get_default_data() self.df = pd.DataFrame(raw_data) # --- PCA計算 (6次元) --- features = [ 'Score_Math_Logic', 'Score_English', 'Score_Science', 'Score_Humanities', 'Extraversion', 'Sensitivity', 'Independence', 'Gender_Code' ] # データが少なすぎる場合のガード if len(self.df) < 6: messagebox.showerror("Error", "データが少なすぎて分析できません(最低6人必要です)") self.root.destroy() return # 標準化 scaler = StandardScaler() df_scaled = scaler.fit_transform(self.df[features]) # PCA (6成分) # 1-3: 位置 (x, y, z), 4-6: ベクトル (u, v, w) pca = PCA(n_components=6) components = pca.fit_transform(df_scaled) self.df[['x', 'y', 'z', 'u', 'v', 'w']] = components # 生徒と講師に分離して保持 self.students = self.df[self.df['Type'] == 'Student'].copy() self.teachers = self.df[self.df['Type'] == 'Teacher'].copy() def get_default_data(self): # デフォルトデータ(ファイルがない場合用) return { 'Name': ['講師A', '生徒1'], 'Type': ['Teacher', 'Student'], 'Score_Math_Logic': [0.9, 0.1], 'Score_English': [0.9, 0.1], 'Score_Science': [0.9, 0.1], 'Score_Humanities': [0.9, 0.1], 'Extraversion': [0.5, 0.5], 'Sensitivity': [0.5, 0.5], 'Independence': [0.5, 0.5], 'Gender_Code': [1.0, 1.0] } def create_widgets(self): # --- 上部: コントロールエリア --- control_frame = tk.LabelFrame(self.root, text="検索条件", padx=10, pady=10) control_frame.pack(fill=tk.X, padx=10, pady=10) # 生徒選択 tk.Label(control_frame, text="対象の生徒を選択:").grid(row=0, column=0, sticky="w") self.student_var = tk.StringVar() self.student_combo = ttk.Combobox(control_frame, textvariable=self.student_var, state="readonly", width=30) self.student_combo.grid(row=0, column=1, padx=10, sticky="w") self.student_combo.bind("<>", self.calculate_match) # アルファ値 (隠れた特性の重み) tk.Label(control_frame, text="隠れた特性(ベクトル)の重視度 α:").grid(row=1, column=0, sticky="w", pady=10) self.alpha_var = tk.DoubleVar(value=1.0) self.alpha_scale = tk.Scale(control_frame, variable=self.alpha_var, from_=0.0, to=5.0, resolution=0.1, orient=tk.HORIZONTAL, length=200, command=lambda x: self.calculate_match()) self.alpha_scale.grid(row=1, column=1, padx=10, sticky="w") # --- 下部: 結果エリア --- result_frame = tk.LabelFrame(self.root, text="AI推奨講師ベスト3", padx=10, pady=10) result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 結果表示用ツリービュー columns = ('Rank', 'Name', 'Total_Dist', 'Pos_Dist', 'Vec_Dist', 'Major_Subj') self.tree = ttk.Treeview(result_frame, columns=columns, show='headings', height=10) self.tree.heading('Rank', text='順位') self.tree.heading('Name', text='講師名') self.tree.heading('Total_Dist', text='総合距離(相性)') self.tree.heading('Pos_Dist', text='位置差') self.tree.heading('Vec_Dist', text='ベクトル差') self.tree.heading('Major_Subj', text='得意科目') self.tree.column('Rank', width=50, anchor='center') self.tree.column('Name', width=150) self.tree.column('Total_Dist', width=100, anchor='center') self.tree.column('Pos_Dist', width=80, anchor='center') self.tree.column('Vec_Dist', width=80, anchor='center') self.tree.column('Major_Subj', width=100) self.tree.pack(fill=tk.BOTH, expand=True) # 解説ラベル note = "※ 総合距離が 0 に近いほど「AI判定による相性」が良いことを示します。\n" \ "※ 位置差: 大まかなキャラの一致度\n" \ "※ ベクトル差: こだわりや細部の感性の一致度" tk.Label(result_frame, text=note, justify="left", fg="gray").pack(anchor="w", pady=5) def update_student_list(self): student_names = self.students['Name'].tolist() self.student_combo['values'] = student_names if student_names: self.student_combo.current(0) self.calculate_match() def calculate_match(self, event=None): student_name = self.student_var.get() alpha = self.alpha_var.get() if not student_name: return # 選択された生徒のデータ取得 target_s = self.students[self.students['Name'] == student_name].iloc[0] results = [] # 全講師との距離を計算 for _, teacher in self.teachers.iterrows(): # 1. 空間距離 (位置の差) - Position Distance # (x, y, z) のユークリッド距離 pos_dist = np.sqrt( (target_s['x'] - teacher['x'])**2 + (target_s['y'] - teacher['y'])**2 + (target_s['z'] - teacher['z'])**2 ) # 2. ベクトル距離 (向きと強さの差) - Vector Difference # (u, v, w) のユークリッド距離 vec_dist = np.sqrt( (target_s['u'] - teacher['u'])**2 + (target_s['v'] - teacher['v'])**2 + (target_s['w'] - teacher['w'])**2 ) # 3. 総合距離 (Total Distance) # あなたの提案ロジック: Distance + alpha * VectorDiff total_dist = pos_dist + (alpha * vec_dist) # 講師の得意科目(スコア最大の科目)を取得(表示用) scores = { '数': teacher['Score_Math_Logic'], '英': teacher['Score_English'], '理': teacher['Score_Science'], '社国': teacher['Score_Humanities'] } best_subj = max(scores, key=scores.get) results.append({ 'Name': teacher['Name'], 'Total_Dist': total_dist, 'Pos_Dist': pos_dist, 'Vec_Dist': vec_dist, 'Major_Subj': best_subj }) # 距離が小さい順(相性が良い順)にソート results.sort(key=lambda x: x['Total_Dist']) # ツリービュー更新 for item in self.tree.get_children(): self.tree.delete(item) for i, res in enumerate(results[:5]): # トップ5まで表示 self.tree.insert('', 'end', values=( f"{i+1}位", res['Name'], f"{res['Total_Dist']:.4f}", f"{res['Pos_Dist']:.4f}", f"{res['Vec_Dist']:.4f}", res['Major_Subj'] )) if __name__ == "__main__": root = tk.Tk() app = MatchingApp(root) root.mainloop()