import gradio as gr import shap import matplotlib.pyplot as plt import pandas as pd import numpy as np import io import joblib import os from PIL import Image from fpdf import FPDF from fpdf.enums import XPos, YPos import umap # Безопасный импорт HDBSCAN try: import hdbscan except ImportError: try: from sklearn.cluster import HDBSCAN as hdbscan except ImportError: hdbscan = None # 1. Настройки среды и загрузка моделей plt.switch_backend('Agg') BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def load_model(file_name): path = os.path.join(BASE_DIR, file_name) if not os.path.exists(path): path = os.path.join(BASE_DIR, 'models', file_name) try: return joblib.load(path) except: return None # Загрузка компонентов (названия файлов из вашего репозитория) xgb_reg = load_model('xgb_reg_model.joblib') xgb_clf = load_model('xgb_clf_model.joblib') umap_reducer = load_model('umap_reducer.joblib') hdbscan_model = load_model('hdbscan_clusterer.joblib') train_coords = load_model('train_umap_coords.joblib') train_labels = load_model('train_clusters.joblib') # Если файла с признаками нет, мы создадим его структуру из модели train_features = load_model('train_data_for_umap.joblib') # Словарь для перевода признаков в SHAP rename_dict = { 'RIDAGEYR': 'Возраст', 'RIAGENDR': 'Пол', 'BMXBMI': 'ИМТ', 'BMXWAIST': 'Талия', 'BPXSY1': 'Сист. АД', 'BPXDI1': 'Диаст. АД', 'LBXGH': 'Гликированный гемоглобин', 'LBDTCSI': 'Общий холестерин', 'LBDHDD': 'ЛПВП', 'LBXTR': 'Триглицериды', 'LBXCRP': 'СРБ', 'PHQ9_score': 'Депрессия (PHQ-9)', 'LBXBPB': 'Свинец', 'LBXBCD': 'Кадмий', 'LBXTHG': 'Ртуть', 'URXPHL': 'Фталаты', 'LBXIN': 'Инсулин', 'URXMEP': 'Моноэтилфталат' } # 2. Логика PDF (Исправленная работа со шрифтами) class PDF(FPDF): def __init__(self): super().__init__() self.font_name = "DejaVu" font_path = os.path.join(BASE_DIR, "DejaVuSans.ttf") if os.path.exists(font_path): self.add_font(self.font_name, "", font_path) self.unicode_ready = True else: self.unicode_ready = False def header(self): use_font = self.font_name if self.unicode_ready else "Helvetica" self.set_font(use_font, size=14) self.cell(0, 10, text="Отчет PolySen Plus: Мониторинг здоровья", new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C') self.ln(5) def create_report(al_score, stage, recs, shap_img, umap_img): pdf = PDF() pdf.add_page() use_font = pdf.font_name if pdf.unicode_ready else "Helvetica" pdf.set_font(use_font, size=11) pdf.cell(0, 10, text=f"Индекс нагрузки (AL Score): {al_score}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) pdf.cell(0, 10, text=f"Клиническая стадия: {stage}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) pdf.ln(5) pdf.set_font(use_font, size=10) clean_recs = "ПЕРСОНАЛЬНЫЕ РЕКОМЕНДАЦИИ:\n" + recs.replace('###', '').replace('**', '').replace('-', '•') pdf.multi_cell(0, 6, text=clean_recs) if shap_img: shap_img.save("temp_sh.png") pdf.image("temp_sh.png", x=10, w=180) pdf.ln(5) if umap_img: umap_img.save("temp_um.png") pdf.image("temp_um.png", x=40, w=120) path = "PolySen_Health_Report.pdf" pdf.output(path) return path # 3. Функция рекомендаций def get_recommendations(al_score, stage, data): recs = [] if al_score > 1.1: recs.append("- Внимание: Повышенный биологический износ систем организма.") if data['BMXBMI'] > 27: recs.append("- Рекомендуется снижение ИМТ и контроль калорийности.") if data['BPXSY1'] > 135: recs.append("- Высокое давление: ограничьте натрий и проверьте почки.") if data['PHQ9_score'] > 10: recs.append("- Ментальное здоровье: высокий балл PHQ-9, обратитесь к специалисту.") if not recs: recs.append("- Все биомаркеры в норме. Рекомендуется плановое наблюдение.") return "\n".join(recs) # 4. Основной процесс def main_process(age, gender_txt, bmi, waist, sbp, dbp, gh, tc, hdl, tg, crp, phq, lead, cadmium, mercury, phthalates): try: gender = 1 if gender_txt == "Мужской" else 2 input_data = { 'RIDAGEYR': age, 'RIAGENDR': gender, 'BMXBMI': bmi, 'BMXWAIST': waist, 'BPXSY1': sbp, 'BPXDI1': dbp, 'LBXGH': gh, 'LBDTCSI': tc, 'LBDHDD': hdl, 'LBXTR': tg, 'LBXCRP': crp, 'PHQ9_score': phq, 'LBXBPB': lead, 'LBXBCD': cadmium, 'LBXTHG': mercury, 'URXPHL': phthalates } # Регрессия AL Score reg_features = xgb_reg.get_booster().feature_names df_reg = pd.DataFrame(0.0, index=[0], columns=reg_features) for c in reg_features: if c in input_data: df_reg.at[0, c] = input_data[c] al_val = float(xgb_reg.predict(df_reg)[0]) # Классификация Стадии clf_features = xgb_clf.get_booster().feature_names df_clf = pd.DataFrame(0.0, index=[0], columns=clf_features) input_data['AL_score'] = al_val for c in clf_features: if c in input_data: df_clf.at[0, c] = input_data[c] st_idx = int(xgb_clf.predict(df_clf)[0]) st_map = {0: "Норма", 1: "Начальная", 2: "Выраженная", 3: "Критическая"} res_stage = st_map.get(st_idx, f"Стадия {st_idx}") # SHAP df_shap = df_reg.rename(columns=rename_dict) explainer = shap.TreeExplainer(xgb_reg) shap_vals = explainer.shap_values(df_reg) plt.figure(figsize=(10, 3)) shap.force_plot(explainer.expected_value, shap_vals[0], df_shap.iloc[0], matplotlib=True, show=False) buf_s = io.BytesIO() plt.savefig(buf_s, format='png', bbox_inches='tight', dpi=120) img_shap = Image.open(buf_s) plt.close() # UMAP (Исправленный блок) img_umap = None phenotype = "Фенотип не определен" if umap_reducer is not None: try: # Автоматически определяем нужные колонки из модели, если train_features не загружен expected_cols = getattr(umap_reducer, 'feature_names_in_', reg_features) u_in = pd.DataFrame(0.0, index=[0], columns=expected_cols) for col in u_in.columns: if col in input_data: u_in.at[0, col] = input_data[col] # Координаты coords = umap_reducer.transform(u_in)[0] # Отрисовка plt.figure(figsize=(6, 4)) if train_coords is not None: plt.scatter(train_coords[:, 0], train_coords[:, 1], c=train_labels, cmap='viridis', s=2, alpha=0.1) plt.scatter(coords[0], coords[1], color='red', marker='X', s=150, label='Вы здесь') plt.axis('off') plt.title("Позиция в популяции") buf_u = io.BytesIO() plt.savefig(buf_u, format='png', bbox_inches='tight') img_umap = Image.open(buf_u) plt.close() if hdbscan_model: pid = hdbscan_model.predict([coords])[0] phenotype = f"Фенотип здоровья №{pid}" except Exception as e: phenotype = f"Ошибка визуализации: {str(e)[:50]}" recs = get_recommendations(al_val, res_stage, input_data) pdf_path = create_report(f"{al_val:.2f}", res_stage, recs, img_shap, img_umap) return f"{al_val:.2f}", res_stage, recs, phenotype, img_shap, img_umap, pdf_path except Exception as e: return f"Ошибка: {str(e)}", "---", "---", "---", None, None, None # 5. Интерфейс with gr.Blocks(title="PolySen Plus") as demo: gr.Markdown("# 🏥 PolySen Plus: Мониторинг здоровья") with gr.Row(): with gr.Column(): age = gr.Slider(18, 100, 45, label="Возраст") gender = gr.Radio(["Мужской", "Женский"], value="Мужской", label="Пол") with gr.Row(): bmi = gr.Number(value=25.0, label="ИМТ") waist = gr.Number(value=90.0, label="Талия (см)") with gr.Row(): sbp = gr.Slider(80, 200, 120, label="Сист. АД") dbp = gr.Slider(40, 120, 80, label="Диаст. АД") with gr.Accordion("Дополнительные биомаркеры", open=False): gh, tc, hdl, tg, crp = [gr.Number(v, label=l) for v, l in [(5.5, "HbA1c"), (5.0, "Общий Холест."), (1.2, "ЛПВП"), (1.5, "ТГ"), (1.0, "СРБ")]] phq = gr.Slider(0, 27, 5, label="PHQ-9 (Депрессия)") l, c, m, p = [gr.Number(v, label=n) for v, n in [(1.0, "Свинец"), (0.3, "Кадмий"), (0.5, "Ртуть"), (5.0, "Фталаты")]] btn = gr.Button("🚀 Провести анализ", variant="primary") with gr.Column(): with gr.Row(): o1 = gr.Label(label="AL Score") o2 = gr.Label(label="Стадия") o3 = gr.Markdown(label="Рекомендации") o4 = gr.Label(label="Ваш фенотип") o5 = gr.Image(label="Влияние факторов (SHAP)") o6 = gr.Image(label="Позиция в популяции (UMAP)") o7 = gr.File(label="📄 Скачать отчет") btn.click(main_process, inputs=[age, gender, bmi, waist, sbp, dbp, gh, tc, hdl, tg, crp, phq, l, c, m, p], outputs=[o1, o2, o3, o4, o5, o6, o7]) demo.launch(theme=gr.themes.Soft())