Files changed (6) hide show
  1. .gitattributes +0 -1
  2. DejaVuSans.ttf +0 -3
  3. Dockerfile +0 -32
  4. README.md +1 -2
  5. app.py +136 -215
  6. requirements.txt +11 -10
.gitattributes CHANGED
@@ -33,4 +33,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- DejaVuSans.ttf filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
DejaVuSans.ttf DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:08ca98e69d9d8fa1065584b4f9ab7d49b6205abea6572b90e171b254845bb990
3
- size 741536
 
 
 
 
Dockerfile DELETED
@@ -1,32 +0,0 @@
1
- # Используем Python 3.12 для стабильной работы numba и umap
2
- FROM python:3.12-slim
3
-
4
- # Устанавливаем системные зависимости для OpenCV, FFmpeg и сборки моделей
5
- RUN apt-get update && apt-get install -y \
6
- git \
7
- git-lfs \
8
- ffmpeg \
9
- libsm6 \
10
- libxext6 \
11
- cmake \
12
- rsync \
13
- libgl1 \
14
- && rm -rf /var/lib/apt/lists/* \
15
- && git lfs install
16
-
17
- # Создаем рабочую директорию
18
- WORKDIR /app
19
-
20
- # Сначала копируем только требования, чтобы использовать кэш Docker
21
- COPY requirements.txt .
22
- RUN pip install --no-cache-dir -U pip && \
23
- pip install --no-cache-dir -r requirements.txt
24
-
25
- # Копируем остальные файлы (app.py и модели .joblib)
26
- COPY . .
27
-
28
- # Открываем порт для Gradio (Hugging Face использует 7860)
29
- EXPOSE 7860
30
-
31
- # Запускаем приложение
32
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -5,10 +5,9 @@ colorFrom: indigo
5
  colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.3.0
8
- python_version: 3.12
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
5
  colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.3.0
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
12
 
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -6,228 +6,149 @@ import numpy as np
6
  import io
7
  import joblib
8
  import os
9
- from PIL import Image
10
- from fpdf import FPDF
11
- from fpdf.enums import XPos, YPos
12
- import umap
13
-
14
- # Безопасный импорт HDBSCAN
15
- try:
16
- import hdbscan
17
- except ImportError:
18
- try:
19
- from sklearn.cluster import HDBSCAN as hdbscan
20
- except ImportError:
21
- hdbscan = None
22
-
23
- # 1. Настройки среды и загрузка моделей
24
  plt.switch_backend('Agg')
25
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
26
-
27
- def load_model(file_name):
28
- path = os.path.join(BASE_DIR, file_name)
29
- if not os.path.exists(path):
30
- path = os.path.join(BASE_DIR, 'models', file_name)
31
- try:
32
- return joblib.load(path)
33
- except:
34
- return None
35
-
36
- # Загрузка компонентов (названия файлов из вашего репозитория)
37
- xgb_reg = load_model('xgb_reg_model.joblib')
38
- xgb_clf = load_model('xgb_clf_model.joblib')
39
- umap_reducer = load_model('umap_reducer.joblib')
40
- hdbscan_model = load_model('hdbscan_clusterer.joblib')
41
- train_coords = load_model('train_umap_coords.joblib')
42
- train_labels = load_model('train_clusters.joblib')
43
- # Если файла с признаками нет, мы создадим его структуру из модели
44
- train_features = load_model('train_data_for_umap.joblib')
45
-
46
- # Словарь для перевода признаков в SHAP
47
- rename_dict = {
48
- 'RIDAGEYR': 'Возраст', 'RIAGENDR': 'Пол', 'BMXBMI': 'ИМТ',
49
- 'BMXWAIST': 'Талия', 'BPXSY1': 'Сист. АД', 'BPXDI1': 'Диаст. АД',
50
- 'LBXGH': 'Гликированный гемоглобин', 'LBDTCSI': 'Общий холестерин',
51
- 'LBDHDD': 'ЛПВП', 'LBXTR': 'Триглицериды', 'LBXCRP': 'СРБ',
52
- 'PHQ9_score': 'Депрессия (PHQ-9)', 'LBXBPB': 'Свинец',
53
- 'LBXBCD': 'Кадмий', 'LBXTHG': 'Ртуть', 'URXPHL': 'Фталаты',
54
- 'LBXIN': 'Инсулин', 'URXMEP': 'Моноэтилфталат'
55
  }
 
56
 
57
- # 2. Логика PDF (Исправленная работа со шрифтами)
58
- class PDF(FPDF):
59
- def __init__(self):
60
- super().__init__()
61
- self.font_name = "DejaVu"
62
- font_path = os.path.join(BASE_DIR, "DejaVuSans.ttf")
63
- if os.path.exists(font_path):
64
- self.add_font(self.font_name, "", font_path)
65
- self.unicode_ready = True
66
- else:
67
- self.unicode_ready = False
68
-
69
- def header(self):
70
- use_font = self.font_name if self.unicode_ready else "Helvetica"
71
- self.set_font(use_font, size=14)
72
- self.cell(0, 10, text="Отчет PolySen Plus: Мониторинг здоровья",
73
- new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
74
- self.ln(5)
75
-
76
- def create_report(al_score, stage, recs, shap_img, umap_img):
77
- pdf = PDF()
78
- pdf.add_page()
79
- use_font = pdf.font_name if pdf.unicode_ready else "Helvetica"
80
 
81
- pdf.set_font(use_font, size=11)
82
- pdf.cell(0, 10, text=f"Индекс нагрузки (AL Score): {al_score}", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
83
- pdf.cell(0, 10, text=f"Клиническая стадия: {stage}", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
84
- pdf.ln(5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- pdf.set_font(use_font, size=10)
87
- clean_recs = "ПЕРСОНАЛЬНЫЕ РЕКОМЕНДАЦИИ:\n" + recs.replace('###', '').replace('**', '').replace('-', '•')
88
- pdf.multi_cell(0, 6, text=clean_recs)
89
 
90
- if shap_img:
91
- shap_img.save("temp_sh.png")
92
- pdf.image("temp_sh.png", x=10, w=180)
93
- pdf.ln(5)
94
 
95
- if umap_img:
96
- umap_img.save("temp_um.png")
97
- pdf.image("temp_um.png", x=40, w=120)
98
-
99
- path = "PolySen_Health_Report.pdf"
100
- pdf.output(path)
101
- return path
102
-
103
- # 3. Функция рекомендаций
104
- def get_recommendations(al_score, stage, data):
105
- recs = []
106
- if al_score > 1.1:
107
- recs.append("- Внимание: Повышенный биологический износ систем организма.")
108
- if data['BMXBMI'] > 27:
109
- recs.append("- Рекомендуется снижение ИМТ и контроль калорийности.")
110
- if data['BPXSY1'] > 135:
111
- recs.append("- Высокое давление: ограничьте натрий и проверьте почки.")
112
- if data['PHQ9_score'] > 10:
113
- recs.append("- Ментальное здоровье: высокий балл PHQ-9, обратитесь к специалисту.")
114
- if not recs:
115
- recs.append("- Все биомаркеры в норме. Рекомендуется плановое наблюдение.")
116
- return "\n".join(recs)
117
-
118
- # 4. Основной процесс
119
- def main_process(age, gender_txt, bmi, waist, sbp, dbp, gh, tc, hdl, tg, crp, phq, lead, cadmium, mercury, phthalates):
120
- try:
121
- gender = 1 if gender_txt == "Мужской" else 2
122
- input_data = {
123
- 'RIDAGEYR': age, 'RIAGENDR': gender, 'BMXBMI': bmi, 'BMXWAIST': waist,
124
- 'BPXSY1': sbp, 'BPXDI1': dbp, 'LBXGH': gh, 'LBDTCSI': tc,
125
- 'LBDHDD': hdl, 'LBXTR': tg, 'LBXCRP': crp, 'PHQ9_score': phq,
126
- 'LBXBPB': lead, 'LBXBCD': cadmium, 'LBXTHG': mercury, 'URXPHL': phthalates
127
- }
128
-
129
- # Регрессия AL Score
130
- reg_features = xgb_reg.get_booster().feature_names
131
- df_reg = pd.DataFrame(0.0, index=[0], columns=reg_features)
132
- for c in reg_features:
133
- if c in input_data: df_reg.at[0, c] = input_data[c]
134
- al_val = float(xgb_reg.predict(df_reg)[0])
135
-
136
- # Классификация Стадии
137
- clf_features = xgb_clf.get_booster().feature_names
138
- df_clf = pd.DataFrame(0.0, index=[0], columns=clf_features)
139
- input_data['AL_score'] = al_val
140
- for c in clf_features:
141
- if c in input_data: df_clf.at[0, c] = input_data[c]
142
-
143
- st_idx = int(xgb_clf.predict(df_clf)[0])
144
- st_map = {0: "Норма", 1: "Начальная", 2: "Выраженная", 3: "Критическая"}
145
- res_stage = st_map.get(st_idx, f"Стадия {st_idx}")
146
-
147
- # SHAP
148
- df_shap = df_reg.rename(columns=rename_dict)
149
- explainer = shap.TreeExplainer(xgb_reg)
150
- shap_vals = explainer.shap_values(df_reg)
151
- plt.figure(figsize=(10, 3))
152
- shap.force_plot(explainer.expected_value, shap_vals[0], df_shap.iloc[0], matplotlib=True, show=False)
153
- buf_s = io.BytesIO()
154
- plt.savefig(buf_s, format='png', bbox_inches='tight', dpi=120)
155
- img_shap = Image.open(buf_s)
156
- plt.close()
157
-
158
- # UMAP (Исправленный блок)
159
- img_umap = None
160
- phenotype = "Фенотип не определен"
161
-
162
- if umap_reducer is not None:
163
- try:
164
- # Автоматически определяем нужные колонки из модели, если train_features не загружен
165
- expected_cols = getattr(umap_reducer, 'feature_names_in_', reg_features)
166
- u_in = pd.DataFrame(0.0, index=[0], columns=expected_cols)
167
- for col in u_in.columns:
168
- if col in input_data: u_in.at[0, col] = input_data[col]
169
-
170
- # Координаты
171
- coords = umap_reducer.transform(u_in)[0]
172
-
173
- # Отрисовка
174
- plt.figure(figsize=(6, 4))
175
- if train_coords is not None:
176
- plt.scatter(train_coords[:, 0], train_coords[:, 1], c=train_labels, cmap='viridis', s=2, alpha=0.1)
177
- plt.scatter(coords[0], coords[1], color='red', marker='X', s=150, label='Вы здесь')
178
- plt.axis('off')
179
- plt.title("Позиция в популяции")
180
-
181
- buf_u = io.BytesIO()
182
- plt.savefig(buf_u, format='png', bbox_inches='tight')
183
- img_umap = Image.open(buf_u)
184
- plt.close()
185
-
186
- if hdbscan_model:
187
- pid = hdbscan_model.predict([coords])[0]
188
- phenotype = f"Фенотип здоровья №{pid}"
189
- except Exception as e:
190
- phenotype = f"Ошибка визуализации: {str(e)[:50]}"
191
-
192
- recs = get_recommendations(al_val, res_stage, input_data)
193
- pdf_path = create_report(f"{al_val:.2f}", res_stage, recs, img_shap, img_umap)
194
-
195
- return f"{al_val:.2f}", res_stage, recs, phenotype, img_shap, img_umap, pdf_path
196
-
197
- except Exception as e:
198
- return f"Ошибка: {str(e)}", "---", "---", "---", None, None, None
199
-
200
- # 5. Интерфейс
201
- with gr.Blocks(title="PolySen Plus") as demo:
202
- gr.Markdown("# 🏥 PolySen Plus: Мониторинг здоровья")
203
  with gr.Row():
204
  with gr.Column():
205
- age = gr.Slider(18, 100, 45, label="Возраст")
206
- gender = gr.Radio(["Мужской", "Женский"], value="Мужской", label="Пол")
207
- with gr.Row():
208
- bmi = gr.Number(value=25.0, label="ИМТ")
209
- waist = gr.Number(value=90.0, label="Талия (см)")
210
- with gr.Row():
211
- sbp = gr.Slider(80, 200, 120, label="Сист. АД")
212
- dbp = gr.Slider(40, 120, 80, label="Диаст. АД")
213
- with gr.Accordion("Дополнительные биомаркеры", open=False):
214
- 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, "СРБ")]]
215
- phq = gr.Slider(0, 27, 5, label="PHQ-9 (Депрессия)")
216
- l, c, m, p = [gr.Number(v, label=n) for v, n in [(1.0, "Свинец"), (0.3, "Кадмий"), (0.5, "Ртуть"), (5.0, "Фталаты")]]
217
- btn = gr.Button("🚀 Провести анализ", variant="primary")
218
-
219
  with gr.Column():
220
- with gr.Row():
221
- o1 = gr.Label(label="AL Score")
222
- o2 = gr.Label(label="Стадия")
223
- o3 = gr.Markdown(label=екомендации")
224
- o4 = gr.Label(label="Ваш фенотип")
225
- o5 = gr.Image(label="Влияние факторов (SHAP)")
226
- o6 = gr.Image(label="Позиция в популяции (UMAP)")
227
- o7 = gr.File(label="📄 Скачать отчет")
228
-
229
- btn.click(main_process,
230
- inputs=[age, gender, bmi, waist, sbp, dbp, gh, tc, hdl, tg, crp, phq, l, c, m, p],
231
- outputs=[o1, o2, o3, o4, o5, o6, o7])
232
-
233
- demo.launch(theme=gr.themes.Soft())
 
 
 
 
6
  import io
7
  import joblib
8
  import os
9
+
10
+ # Установка бэкенда matplotlib
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  plt.switch_backend('Agg')
12
+
13
+ # Загрузка моделей
14
+ model_dir = './models'
15
+ xgb_reg_model = joblib.load(os.path.join(model_dir, 'xgb_reg_model.joblib'))
16
+ xgb_clf_model = joblib.load(os.path.join(model_dir, 'xgb_clf_model.joblib'))
17
+ umap_reducer = joblib.load(os.path.join(model_dir, 'umap_reducer.joblib'))
18
+ hdbscan_clusterer = joblib.load(os.path.join(model_dir, 'hdbscan_clusterer.joblib'))
19
+
20
+ # Определение колонок (ВАЖНО: должны совпадать с обучением)
21
+ feature_columns = [
22
+ 'RIDAGEYR', 'RIAGENDR', 'BMXBMI', 'BMXWAIST', 'BPXSY1', 'BPXDI1',
23
+ 'LBXGH', 'LBXHSCRP', 'LBDTCSI', 'LBDHDD', 'LBXIN',
24
+ 'SLQ050', 'HSQ510', 'PHQ9_score',
25
+ 'LBXBPB', 'LBXBCD', 'LBXTHG', 'URXMHH'
26
+ ]
27
+
28
+ # Списки признаков для конкретных моделей
29
+ X_reg_features_cols = feature_columns
30
+ X_clf_features_cols_without_AL_score = feature_columns
31
+
32
+ # Медианы для заполнения пропусков
33
+ x_train_medians_values = {
34
+ 'RIDAGEYR': 31.0, 'RIAGENDR': 2.0, 'BMXBMI': 25.8, 'BMXWAIST': 91.2, 'BPXSY1': 118.0,
35
+ 'BPXDI1': 70.0, 'LBXGH': 5.5, 'LBXHSCRP': 1.35, 'LBDTCSI': 4.55, 'LBDHDD': 51.0,
36
+ 'SLQ050': 2.0, 'HSQ510': 2.0, 'LBXIN': 10.04, 'PHQ9_score': 0.0,
37
+ 'LBXBPB': 0.76, 'LBXBCD': 0.22, 'LBXTHG': 0.51, 'URXMHH': 5.5
 
 
 
 
38
  }
39
+ X_train_medians = pd.Series(x_train_medians_values)
40
 
41
+ thresholds_recommendation = {
42
+ 'LBXHSCRP': 2.71, 'SLQ050': 2.0, 'PHQ9_score': 2.0, 'HSQ510': 2.0,
43
+ 'LBXBPB': 1.07, 'LBXBCD': 0.35, 'LBXTHG': 0.91, 'URXMHH': 5.5
44
+ }
45
+
46
+ def generate_recommendations(participant_data):
47
+ recommendations = []
48
+ stage = participant_data.get('stage_label', 0)
49
+
50
+ if stage == 3.0:
51
+ recommendations.append("🔴 Уровень адаптационной нагрузки крайне высокий. Необходима немедленная консультация врача.")
52
+ elif stage == 2.0:
53
+ recommendations.append("🟠 Уровень адаптационной нагрузки высокий. Рекомендует��я изменение образа жизни.")
54
+ elif stage == 1.0:
55
+ recommendations.append("🟡 Наблюдается повышенная адаптационная нагрузка. Следите за здоровьем.")
56
+
57
+ if participant_data.get('LBXHSCRP', 0) > thresholds_recommendation['LBXHSCRP']:
58
+ recommendations.append(f"⚠️ Повышен уровень C-реактивного белка ({participant_data['LBXHSCRP']:.2f}).")
59
+
60
+ # Сбор симптомов
61
+ symptoms = []
62
+ if participant_data.get('SLQ050', 0) > thresholds_recommendation['SLQ050']: symptoms.append("сонливость")
63
+ if participant_data.get('PHQ9_score', 0) > thresholds_recommendation['PHQ9_score']: symptoms.append("высокий стресс (PHQ-9)")
64
 
65
+ if symptoms:
66
+ recommendations.append(f"💡 Обратите внимание на: {', '.join(symptoms)}.")
67
+
68
+ if not recommendations:
69
+ recommendations.append("✅ Ваши показатели в норме.")
70
+
71
+ return recommendations
72
+
73
+ def predict_and_recommend(*args):
74
+ # Создание DataFrame
75
+ input_df = pd.DataFrame([args], columns=feature_columns)
76
+
77
+ # Заполнение пропусков
78
+ for col in feature_columns:
79
+ if pd.isna(input_df[col]).any():
80
+ input_df[col] = input_df[col].fillna(X_train_medians.get(col, 0))
81
+
82
+ # Предсказание AL Score
83
+ predicted_al_score = xgb_reg_model.predict(input_df[X_reg_features_cols])[0]
84
+
85
+ # Подготовка для классификатора и UMAP
86
+ input_df_clf = input_df[X_clf_features_cols_without_AL_score].copy()
87
+ input_df_clf.insert(0, 'AL_score', predicted_al_score)
88
+
89
+ predicted_stage_label = xgb_clf_model.predict(input_df_clf)[0]
90
 
91
+ # UMAP и Кластеризация
92
+ umap_emb = umap_reducer.transform(input_df_clf)
93
+ cluster = hdbscan_clusterer.predict(umap_emb)[0]
94
 
95
+ # SHAP
96
+ explainer = shap.TreeExplainer(xgb_reg_model)
97
+ shap_vals = explainer.shap_values(input_df[X_reg_features_cols])
 
98
 
99
+ plt.figure(figsize=(10, 3))
100
+ shap.force_plot(explainer.expected_value, shap_vals[0], input_df.iloc[0], matplotlib=True, show=False)
101
+ buf = io.BytesIO()
102
+ plt.savefig(buf, format='png', bbox_inches='tight')
103
+ plt.close()
104
+
105
+ # Рекомендации
106
+ diag_data = input_df.iloc[0].to_dict()
107
+ diag_data['stage_label'] = predicted_stage_label
108
+ recs = generate_recommendations(diag_data)
109
+
110
+ return (
111
+ round(float(predicted_al_score), 2),
112
+ int(predicted_stage_label),
113
+ f"{umap_emb[0,0]:.2f}, {umap_emb[0,1]:.2f}",
114
+ str(cluster),
115
+ buf.getvalue(),
116
+ "\n".join(recs)
117
+ )
118
+
119
+ # Описание интерфейса Gradio
120
+ feature_labels = {
121
+ 'RIDAGEYR': 'Возраст', 'RIAGENDR': 'Пол (1=М, 2=Ж)', 'BMXBMI': 'ИМТ',
122
+ 'BMXWAIST': 'Талия', 'BPXSY1': 'Сист. АД', 'BPXDI1': 'Диаст. АД',
123
+ 'LBXGH': 'HbA1c', 'LBXHSCRP': 'hsCRP', 'LBDTCSI': 'Холестерин',
124
+ 'LBDHDD': 'ЛПВП', 'LBXIN': 'Инсулин', 'SLQ050': 'Сонливость',
125
+ 'HSQ510': 'Боль', 'PHQ9_score': 'PHQ-9', 'LBXBPB': 'Свинец',
126
+ 'LBXBCD': 'Кадмий', 'LBXTHG': 'Ртуть', 'URXMHH': 'Фталаты'
127
+ }
128
+
129
+ with gr.Blocks() as demo:
130
+ gr.Markdown("# Система оценки адаптационной нагрузки")
131
+
132
+ inputs = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  with gr.Row():
134
  with gr.Column():
135
+ for col in feature_columns[:9]:
136
+ inputs.append(gr.Number(label=feature_labels[col], value=X_train_medians[col]))
 
 
 
 
 
 
 
 
 
 
 
 
137
  with gr.Column():
138
+ for col in feature_columns[9:]:
139
+ inputs.append(gr.Number(label=feature_labels[col], value=X_train_medians[col]))
140
+
141
+ btn = gr.Button("Рассчитать")
142
+
143
+ with gr.Row():
144
+ al_out = gr.Textbox(label="AL Score")
145
+ stage_out = gr.Textbox(label="Стадия")
146
+
147
+ coords_out = gr.Textbox(label="UMAP Координаты")
148
+ cluster_out = gr.Textbox(label="Кластер")
149
+ shap_out = gr.Image(label="Влияние признаков (SHAP)")
150
+ recs_out = gr.Textbox(label="Рекомендации", lines=5)
151
+
152
+ btn.click(predict_and_recommend, inputs=inputs, outputs=[al_out, stage_out, coords_out, cluster_out, shap_out, recs_out])
153
+
154
+ demo.launch()
requirements.txt CHANGED
@@ -1,10 +1,11 @@
1
- numpy==1.26.4
2
- pandas==2.1.4
3
- scikit-learn==1.3.2
4
- xgboost==2.0.3
5
- umap-learn==0.5.5
6
- shap==0.44.0
7
- matplotlib==3.8.2
8
- joblib==1.3.2
9
- Pillow==10.2.0
10
- fpdf2
 
 
1
+ pandas>=2.2.3
2
+ numpy>=2.1.0
3
+ scikit-learn==1.6.1
4
+ xgboost==3.1.2
5
+ umap-learn==0.5.9.post2
6
+ hdbscan==0.8.41
7
+ shap==0.50.0
8
+ gradio==5.50.0
9
+ matplotlib==3.10.0
10
+ seaborn==0.13.2
11
+ joblib==1.5.3