wisdom-tooth / app.py
tricc's picture
"Thời gian phẫu thuật dự kiến" -> "Kết quả ước đoán"
85c680d verified
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)