import gradio as gr import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import joblib import os plt.style.use('seaborn-v0_8-muted') sns.set_theme(style="whitegrid") import warnings warnings.filterwarnings('ignore') custom_css = """ #pell-gregory-grid .wrap { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; gap: 15px !important; } """ # Load mô hình model_path = "dental_models_v1.pkl" print(f"📂 Đang kiểm tra file tại: {os.getcwd()}/{model_path}") try: if os.path.exists(model_path): models = joblib.load(model_path) print("✅ Load model thành công.") else: print("⚠️ Lỗi: Không tìm thấy file model.") except Exception as e: print(f"❌ Lỗi khi load model: {e}") def preprocess_input(tuoi, gioi_tinh, ben_pt, kinh_nghiem, pell_gregory, goc, chan_rang, ord_lq, ha_mieng, ma, so_thuoc): # Tách phân loại Pell & Gregory thành Cành đứng và R7 cd = pell_gregory[:-1] # VD: 'IIB' -> 'II' r7 = pell_gregory[-1] # VD: 'IIB' -> 'B' # Tách lấy ID của Hình thái chân răng chan_val = int(str(chan_rang)[0]) raw_data = { 'Tuổi': tuoi, 'Giới tính': gioi_tinh, 'Bên PT': ben_pt, 'Kinh nghiệm PTV': kinh_nghiem, 'Tương quan R7': r7, 'Tương quan cành đứng': cd, 'Góc nghiêng': goc, 'Hình thái chân răng': chan_val, 'Liên quan ORD': ord_lq, 'Độ há miệng': ha_mieng, 'Độ linh động má': ma, 'Số viên thuốc GD': so_thuoc, 'Thời gian PT (phút)': 0 # Placeholder sẽ cập nhật sau } df = pd.DataFrame([raw_data]) # Tính toán các đặc trưng (Feature Engineering) df['Age'] = 2025 - df['Tuổi'] df['R7_Grouped'] = 'Class A' if df['Tương quan R7'].iloc[0] == 'A' else 'Class B/C' canh_dung_val = df['Tương quan cành đứng'].iloc[0] df['Ramus_Grouped'] = 'Class I/II' if canh_dung_val in ['I', 'II'] else 'Class III' if chan_val in [2, 3]: df['Root_Grouped'] = 'Easy (2/3)' elif chan_val in [1, 4]: df['Root_Grouped'] = 'Hard (1/4)' else: df['Root_Grouped'] = 'Other' df['Age_Group'] = pd.cut(df['Age'], bins=[0, 22, 30, 100], labels=['Young (<22)', 'Adult (22-30)', 'Senior (>30)']) return df def predict_with_uncertainty(model, input_df): rf_model = model.named_steps['model'] preprocessor = model.named_steps['preprocessor'] X_transformed = preprocessor.transform(input_df) predictions = [tree.predict(X_transformed)[0] for tree in rf_model.estimators_] mean_pred = np.mean(predictions) std_pred = np.std(predictions) return mean_pred, std_pred def predict_with_error(model, input_df): mean_pred, std_pred = predict_with_uncertainty(model, input_df) error_margin = 1.0 * std_pred # Giữ nguyên hệ số như code cũ của user return mean_pred, error_margin def predict_general_v2(tuoi, gioi, ben, exp, pell_gregory, goc, chan, ord_lq, ha, ma, thuoc): df = preprocess_input(tuoi, gioi, ben, exp, pell_gregory, goc, chan, ord_lq, ha, ma, thuoc) time_mean, time_err = predict_with_error(models['Op_Time'], df) df['Thời gian PT (phút)'] = time_mean p1_m, p1_e = predict_with_error(models['Pain_D1'], df) p3_m, p3_e = predict_with_error(models['Pain_D3'], df) p7_m, p7_e = predict_with_error(models['Pain_D7'], df) end_m, end_e = predict_with_error(models['End_Day'], df) res_text = ( f"⏱️ THỜI GIAN NHỔ RĂNG DỰ KIẾN: {time_mean:.1f} ± {time_err:.1f} phút\n" f"(Dải biến thiên: {max(0, time_mean-time_err):.1f} - {time_mean+time_err:.1f} phút)\n\n" f"📈 DỰ BÁO MỨC ĐỘ ĐAU:\n" f"• Ngày 1: {p1_m:.1f} (±{p1_e:.1f})\n" f"• Ngày 3: {p3_m:.1f} (±{p3_e:.1f})\n" f"• Ngày 7: {p7_m:.1f} (±{p7_e:.1f})\n" f"• Hết đau hoàn toàn: Ngày thứ {end_m:.1f} (±{end_e:.1f})" ) fig, ax = plt.subplots(figsize=(8, 4.5)) days = np.array([1, 3, 7]) means = np.array([p1_m, p3_m, p7_m]) errors = np.array([p1_e, p3_e, p7_e]) ax.fill_between(days, means - errors, means + errors, color='#4A90E2', alpha=0.2, label='Dải sai số dự báo') ax.plot(days, means, 'o-', color='#4A90E2', linewidth=3, markersize=8, label='Mức đau trung bình') for i, m in enumerate(means): ax.text(days[i], m + 0.3, f"{m:.1f}", ha='center', fontweight='bold', color='#2C3E50') ax.set_ylim(0, 5.5) ax.set_xticks([1, 3, 7]) ax.set_xticklabels(['Ngày 1', 'Ngày 3', 'Ngày 7']) ax.set_title("Biểu đồ diễn tiến đau và sai số dự báo", fontsize=14, pad=15) ax.set_ylabel("Mức độ đau (VAS)") ax.legend() plt.tight_layout() plt.close() return res_text, fig def predict_experience(tuoi, gioi, ben, pell_gregory, goc, chan, ord_lq, ha, ma, thuoc): exp_mapping = ["<5 năm", "5-10 năm", ">10 năm"] times = [] for e in exp_mapping: df = preprocess_input(tuoi, gioi, ben, e, pell_gregory, goc, chan, ord_lq, ha, ma, thuoc) times.append(models['Op_Time'].predict(df)[0]) fig, ax = plt.subplots(figsize=(7, 3.5)) colors = ['#FFADAD', '#FFD6A5', '#CAFFBF'] bars = ax.barh(["Dưới 5 năm", "5-10 năm", "Trên 10 năm"], times, color=colors, height=0.6) ax.bar_label(bars, fmt='%.1f phút', padding=5, fontweight='bold') ax.set_title("So sánh thời gian nhổ theo kinh nghiệm bác sĩ", fontsize=12) ax.set_xlim(0, max(times) * 1.3) plt.tight_layout() plt.close() advice = f"💡 Bác sĩ trên 10 năm kinh nghiệm làm nhanh hơn bác sĩ mới khoảng {times[0]-times[2]:.1f} phút." return fig, advice def predict_actual_with_range(tuoi, gioi, ben, exp, pell_gregory, goc, chan, ord_lq, ha, ma, thuoc_da_uong, time_actual): df = preprocess_input(tuoi, gioi, ben, exp, pell_gregory, goc, chan, ord_lq, ha, ma, thuoc_da_uong) df['Thời gian PT (phút)'] = time_actual targets = { 'Ngày 1': models['Pain_D1'], 'Ngày 3': models['Pain_D3'], 'Ngày 7': models['Pain_D7'], 'Hết đau': models['End_Day'] } results_mean = [] results_std = [] labels = ['Ngày 1', 'Ngày 3', 'Ngày 7'] for label in labels: m, s = predict_with_uncertainty(targets[label], df) results_mean.append(m) results_std.append(s) end_mean, end_std = predict_with_uncertainty(targets['Hết đau'], df) fig, ax = plt.subplots(figsize=(8, 4)) x_vals = [1, 3, 7] means = np.array(results_mean) stds = np.array(results_std) ax.plot(x_vals, means, 'o-', color='#d9534f', linewidth=2, label='Dự báo trung bình') ax.fill_between(x_vals, means - 1.96*stds, means + 1.96*stds, color='#d9534f', alpha=0.2, label='Khoảng sai số dự báo') for i, txt in enumerate(means): ax.annotate(f'{txt:.1f}', (x_vals[i], means[i]), xytext=(0, 10), textcoords='offset points', ha='center') ax.set_ylim(0, 5.5) ax.set_xticks([1, 3, 7]) ax.set_xticklabels(['Ngày 1', 'Ngày 3', 'Ngày 7']) ax.set_title(f"Diễn tiến đau thực tế (Thời gian: {time_actual}ph, Thuốc: {thuoc_da_uong} viên)") ax.set_ylabel("Mức độ đau (VAS)") ax.legend() plt.tight_layout() plt.close() res_text = (f"✅ CẬP NHẬT SAU PHẪU THUẬT:\n" f"• Thời gian thực hiện: {time_actual} phút\n" f"• Thuốc giảm đau (Paracetamol 500mg) đã uống: {thuoc_da_uong} viên\n" f"----------------------------------------\n" f"🕒 DỰ KIẾN HỒI PHỤC:\n" f"• Ngày hết đau trung bình: Ngày thứ {end_mean:.1f} (± {end_std*1.96:.1f} ngày)\n" f"• Mức độ đau ngày đầu: {results_mean[0]:.1f} / 5") return res_text, fig with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo: gr.Markdown("# 🦷 HỆ THỐNG DỰ BÁO THỜI GIAN PHẪU THUẬT RĂNG KHÔN VÀ PHÂN TÍCH MỨC ĐỘ ĐAU") with gr.Row(): with gr.Column(): gr.Markdown("### 📋 Thông tin người bệnh") tuoi = gr.Number(label="Tuổi", value=2005) gioi_tinh = gr.Dropdown(["Nam", "Nữ"], label="Giới tính", value="Nữ") ben_pt = gr.Dropdown(["Trái", "Phải"], label="Bên PT", value="Trái") ha_mieng = gr.Number(label="Độ há miệng", value=52) linh_dong_ma = gr.Number(label="Độ linh động má", value=48) so_thuoc = gr.Number(label="Số viên thuốc GD", value=5, visible=False) with gr.Column(): gr.Markdown("### 👨‍⚕️ Kinh nghiệm PTV") kinh_nghiem = gr.Dropdown(["<5 năm", "5-10 năm", ">10 năm"], label="Kinh nghiệm PTV", value="<5 năm") gr.Markdown("### 🔍 Đặc điểm răng (X-quang)") pell_gregory = gr.Radio( ["IA", "IB", "IC", "IIA", "IIB", "IIC", "IIIA", "IIIB", "IIIC"], label="Phân loại Pell & Gregory", value="IIB", elem_id="pell-gregory-grid" ) goc_nghieng = gr.Number(label="Góc nghiêng", value=7) chan_rang = gr.Dropdown([ "1 - Mầm răng/Hình thành <1/3 chân răng", "2 - Hình thành >1/3 và <2/3 chân răng", "3 - Hình thành >2/3 chân và chân chụm", "4 - Hình thành >2/3 chân và chân phân kỳ/cong phía chóp/có >2 chân" ], label="Hình thái chân răng", value="4 - Hình thành >2/3 chân và chân phân kỳ/cong phía chóp/có >2 chân") ord_lq = gr.Dropdown(["Không", "Có"], label="Liên quan ORD", value="Không") base_inputs = [tuoi, gioi_tinh, ben_pt, kinh_nghiem, pell_gregory, goc_nghieng, chan_rang, ord_lq, ha_mieng, linh_dong_ma] inputs_all = base_inputs + [so_thuoc] with gr.Tabs(): with gr.TabItem("🕒 Kết quả ước đoán"): btn1 = gr.Button("🚀 Dự đoán thời gian phẫu thuật", variant="primary") with gr.Row(): out_txt1 = gr.Textbox(label="Kết quả dự báo chi tiết", lines=8) out_plot1 = gr.Plot(label="Diễn tiến đau") btn1.click(predict_general_v2, inputs=inputs_all, outputs=[out_txt1, out_plot1]) # Tab So sánh ẨN trên UI with gr.TabItem("👨‍⚕️ So sánh theo kinh nghiệm bác sĩ", visible=False): btn2 = gr.Button("📊 Dự đoán thời gian phẫu thuật theo kinh nghiệm bác sĩ") with gr.Row(): out_plot2 = gr.Plot(label="Biểu đồ so sánh") out_txt2 = gr.Textbox(label="Nhận xét", lines=2) inputs_for_exp = [tuoi, gioi_tinh, ben_pt, pell_gregory, goc_nghieng, chan_rang, ord_lq, ha_mieng, linh_dong_ma, so_thuoc] btn2.click(predict_experience, inputs=inputs_for_exp, outputs=[out_plot2, out_txt2]) with gr.TabItem("📝 Dự kiến sau phẫu thuật"): gr.Markdown("### Nhập thông số thực tế sau ca mổ để tư vấn cho bệnh nhân") with gr.Row(): time_actual = gr.Number(label="Thời gian mổ thực tế (phút)", value=20) thuoc_post = gr.Number(label="Số viên thuốc giảm đau (Paracetamol 500mg) đã uống", value=10) btn3 = gr.Button("🔄 Dự đoán thời gian hết đau", variant="secondary") with gr.Row(): out_txt3 = gr.Textbox(label="Chi tiết", lines=8) out_plot3 = gr.Plot(label="Biểu đồ diễn tiến đau") btn3.click(predict_actual_with_range, inputs=base_inputs + [thuoc_post, time_actual], outputs=[out_txt3, out_plot3]) demo.launch(theme=gr.themes.Soft(font='sans-serif'), share=True)