yogl commited on
Commit
c56387d
·
verified ·
1 Parent(s): 817a60a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -129
app.py CHANGED
@@ -20,7 +20,9 @@ HF_MERGED_REPO = os.getenv("HF_MERGED_REPO")
20
  HF_EMB_REPO = os.getenv("HF_EMB_REPO")
21
  MODEL_NAME = os.getenv("MODEL_NAME")
22
 
23
- # Для Excel (полный набор + "Ссылка ВАК")
 
 
24
  COLUMN_LABELS_RU_EXCEL = {
25
  "№": "№",
26
  "score": "Сходство",
@@ -33,16 +35,6 @@ COLUMN_LABELS_RU_EXCEL = {
33
  "vak_link": "Ссылка ВАК",
34
  }
35
 
36
- # Для отображения (скрываем №, рег.номер, тип)
37
- COLUMN_LABELS_RU_UI = {
38
- "score": "Сходство",
39
- "fio": "ФИО",
40
- "title": "Название диссертации",
41
- "author_org_short": "Организация",
42
- "protection_year": "Год",
43
- "vak": "ВАК",
44
- }
45
-
46
  DISPLAY_COLUMNS_ALL = [
47
  "№",
48
  "score",
@@ -54,6 +46,10 @@ DISPLAY_COLUMNS_ALL = [
54
  "registration_number",
55
  ]
56
 
 
 
 
 
57
  if HF_TOKEN is None:
58
  st.error(
59
  "Не найден секрет `HF_TOKEN`. "
@@ -101,6 +97,7 @@ DEFAULT_SCIENCES = {
101
  "Биологические",
102
  }
103
 
 
104
  SCIENCE_PATTERNS = {
105
  "Архитектура": ["архитектур"],
106
  "Биологические": ["биолог"],
@@ -124,45 +121,48 @@ SCIENCE_PATTERNS = {
124
  "Юридические науки": ["юридич"],
125
  }
126
 
 
 
 
 
 
 
127
  # ==========================
128
  # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
129
  # ==========================
130
 
131
  @st.cache_data(show_spinner="Загрузка данных...")
132
  def load_data():
 
133
  ds_meta = load_dataset(HF_MERGED_REPO, split="train")
134
  df_meta = ds_meta.to_pandas()
135
  df_meta["registration_number"] = df_meta["registration_number"].astype(str)
136
  df_meta = df_meta.set_index("registration_number", drop=False)
137
 
 
138
  ds_emb = load_dataset(HF_EMB_REPO, split="train")
139
  df_emb = ds_emb.to_pandas()
140
  df_emb["registration_number"] = df_emb["registration_number"].astype(str)
 
141
  reg_nums = df_emb["registration_number"].tolist()
142
 
143
  emb_matrix = np.vstack(df_emb["embedding"].to_list()).astype("float32")
144
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
145
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
146
 
 
147
  meta_aligned = df_meta.reindex(reg_nums)
148
 
149
- # dissertation_type
150
- if "dissertation_type" in meta_aligned.columns:
151
- type_s = meta_aligned["dissertation_type"].fillna("").astype(str)
152
- else:
153
- type_s = pd.Series([""] * len(reg_nums), index=meta_aligned.index)
154
-
155
  is_candidate = type_s.str.contains("кандид", case=False, na=False).to_numpy()
156
  is_doctor = type_s.str.contains("доктор", case=False, na=False).to_numpy()
157
 
158
- # degree_pursued
159
- if "degree_pursued" in meta_aligned.columns:
160
- degree_arr = meta_aligned["degree_pursued"].fillna("").astype(str).to_numpy()
161
- else:
162
- degree_arr = np.array([""] * len(reg_nums), dtype=object)
163
- degree_lower = np.char.lower(degree_arr.astype(str))
164
 
165
- # years
166
  if "protection_date" in meta_aligned.columns:
167
  dt = pd.to_datetime(meta_aligned["protection_date"], errors="coerce")
168
  year_arr = dt.dt.year.astype("float").to_numpy()
@@ -208,15 +208,15 @@ def build_filter_mask(
208
  ) -> np.ndarray:
209
  mask = np.ones(len(reg_nums), dtype=bool)
210
 
211
- # Тип диссертации
212
  type_mask = np.zeros(len(reg_nums), dtype=bool)
213
  if candidate_selected:
214
  type_mask |= is_candidate
215
  if doctor_selected:
216
  type_mask |= is_doctor
217
- mask &= type_mask
218
 
219
- # Науки
220
  if science_selected:
221
  sci_mask = np.zeros(len(reg_nums), dtype=bool)
222
  for label in science_selected:
@@ -224,12 +224,11 @@ def build_filter_mask(
224
  sci_mask |= _contains_any(degree_lower, patterns)
225
  mask &= sci_mask
226
 
227
- # Годы защиты (не выкидываем NaN)
228
  if year_range is not None:
229
  y0, y1 = int(year_range[0]), int(year_range[1])
230
  yr = year_arr
231
- year_mask = np.isnan(yr) | ((yr >= y0) & (yr <= y1))
232
- mask &= year_mask
233
 
234
  return mask
235
 
@@ -258,20 +257,23 @@ def search_core(query: str, top_k: int = 10, mask=None):
258
  ]
259
 
260
 
261
- def extract_year(value):
262
  if value is None:
263
  return None
264
  try:
265
  if not isinstance(value, str):
266
  if pd.isna(value):
267
  return None
268
- dt = pd.to_datetime(value)
269
- return str(dt.year)
 
 
270
  except Exception:
271
  pass
 
272
  s = str(value).strip()
273
  if len(s) >= 4 and s[:4].isdigit():
274
- return s[:4]
275
  return None
276
 
277
 
@@ -288,7 +290,7 @@ def build_result_df(results):
288
  else:
289
  meta = pd.Series({}, index=df_all.columns)
290
 
291
- protection_year = extract_year(meta.get("protection_date", None))
292
 
293
  rows.append(
294
  {
@@ -321,7 +323,7 @@ def run_search(
321
  ):
322
  query = query.strip()
323
  if not query:
324
- return pd.DataFrame(), None, None # ui_df, excel_bytes, raw_df
325
 
326
  mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
327
  results = search_core(query, top_k, mask=mask)
@@ -330,23 +332,24 @@ def run_search(
330
  if df_raw.empty:
331
  return pd.DataFrame(), None, df_raw
332
 
333
- # --- UI dataframe: скрываем №, рег.номер, тип; добавляем кликабельный ВАК ---
334
  df_ui = df_raw.copy()
 
 
 
 
 
 
 
 
 
 
335
 
336
- def make_vak_cell(url: str):
337
- url = (url or "").strip()
338
- if not url:
339
- return ""
340
- return f'<a href="{url}" target="_blank">открыть</a>'
341
-
342
- df_ui["vak"] = df_ui["vak_link"].astype(str).map(make_vak_cell)
343
-
344
- df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type", "vak_link"])
345
- df_ui = df_ui.rename(columns=COLUMN_LABELS_RU_UI)
346
 
347
  # --- Excel: полный набор ---
348
  df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
349
-
350
  output = io.BytesIO()
351
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
352
  df_excel_ru.to_excel(writer, index=False)
@@ -355,29 +358,8 @@ def run_search(
355
  return df_ui, output, df_raw
356
 
357
 
358
- def sort_ui_df(df_ui: pd.DataFrame, sort_key_ru: str, ascending: bool) -> pd.DataFrame:
359
- if df_ui.empty:
360
- return df_ui
361
-
362
- df = df_ui.copy()
363
-
364
- # Для корректной сортировки по числам
365
- if sort_key_ru == "Сходство":
366
- df["_sort"] = pd.to_numeric(df["Сходство"], errors="coerce")
367
- df = df.sort_values("_sort", ascending=ascending, kind="mergesort").drop(columns=["_sort"])
368
- return df
369
-
370
- if sort_key_ru == "Год":
371
- df["_sort"] = pd.to_numeric(df["Год"], errors="coerce")
372
- # NaN в конец
373
- df = df.sort_values("_sort", ascending=ascending, kind="mergesort", na_position="last").drop(columns=["_sort"])
374
- return df
375
-
376
- return df.sort_values(sort_key_ru, ascending=ascending, kind="mergesort")
377
-
378
-
379
  # ==========================
380
- # UI НА STREAMLIT
381
  # ==========================
382
 
383
  st.markdown(
@@ -385,8 +367,6 @@ st.markdown(
385
  unsafe_allow_html=True,
386
  )
387
 
388
- # Границы лет: нижняя граница фиксирована 2005
389
- SLIDER_MIN_YEAR = 2005
390
  data_has_years = np.isfinite(year_arr).any()
391
  year_max = int(np.nanmax(year_arr)) if data_has_years else None
392
 
@@ -414,7 +394,7 @@ with st.form("search_form"):
414
  for i, label in enumerate(SCIENCE_LABELS):
415
  default_val = label in DEFAULT_SCIENCES
416
  with cols[i % 3]:
417
- if st.checkbox(label, value=default_val, key=f"sci_{label}"):
418
  science_selected.append(label)
419
 
420
  st.markdown("**Годы защиты:**")
@@ -443,7 +423,7 @@ if do_search:
443
  st.stop()
444
 
445
  with st.spinner("Идёт поиск по базе диссертаций..."):
446
- df_ui, excel_bytes, _df_raw = run_search(
447
  query=query,
448
  top_k=top_k,
449
  candidate_selected=candidate_selected,
@@ -457,63 +437,43 @@ if do_search:
457
  else:
458
  st.success(f"Найдено записей: {len(df_ui)}")
459
 
460
- # ---- СОРТИРОВКА (без горизонтального скролла, таблица HTML с переносом) ----
461
- sort_cols = ["Сходство", "ФИО", "Название диссертации", "Организация", од"]
462
- s1, s2, s3 = st.columns([2, 1, 2])
463
- with s1:
464
- sort_by = st.selectbox("Сортировать по", options=sort_cols, index=0)
465
- with s2:
466
- order = st.selectbox("Порядок", options=["убывание", "возрастание"], index=0)
467
- with s3:
468
- st.write("")
469
-
470
- ascending = (order == "возрастание")
471
- df_sorted = sort_ui_df(df_ui, sort_by, ascending=ascending)
472
-
473
- # HTML-таблица: перенос текста, table-layout fixed, ширина 100% без горизонтальных скроллов
474
- table_html = df_sorted.to_html(
475
- escape=False,
476
- index=False,
477
- classes="result-table",
478
- )
479
-
480
- st.markdown(
481
- """
482
- <style>
483
- table.result-table {
484
- width: 100%;
485
- table-layout: fixed; /* ключевое: фиксирует ширину таблицы под контейнер */
486
- border-collapse: collapse;
487
- }
488
- table.result-table th, table.result-table td {
489
- border: 1px solid rgba(0,0,0,0.08);
490
- padding: 8px 10px;
491
- vertical-align: top;
492
- white-space: normal; /* перенос строк */
493
- word-break: break-word; /* перенос длинных слов */
494
- overflow-wrap: anywhere;
495
- }
496
- table.result-table th {
497
- text-align: center !important;
498
- vertical-align: middle;
499
- }
500
-
501
- /* Примерные ширины колонок (подгоните при желании) */
502
- table.result-table th:nth-child(1), table.result-table td:nth-child(1) { width: 8%; text-align: center; } /* Сходство */
503
- table.result-table th:nth-child(2), table.result-table td:nth-child(2) { width: 16%; } /* ФИО */
504
- table.result-table th:nth-child(3), table.result-table td:nth-child(3) { width: 44%; } /* Название */
505
- table.result-table th:nth-child(4), table.result-table td:nth-child(4) { width: 20%; } /* Организация */
506
- table.result-table th:nth-child(5), table.result-table td:nth-child(5) { width: 6%; text-align: center; } /* Год */
507
- table.result-table th:nth-child(6), table.result-table td:nth-child(6) { width: 6%; text-align: center; } /* ВАК */
508
-
509
- /* чтобы таблица не вылезала за контейнер */
510
- div[data-testid="stMarkdownContainer"] { overflow-x: hidden; }
511
- </style>
512
- """,
513
- unsafe_allow_html=True,
514
  )
515
 
516
- st.markdown(table_html, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
  if excel_bytes is not None:
519
  st.download_button(
 
20
  HF_EMB_REPO = os.getenv("HF_EMB_REPO")
21
  MODEL_NAME = os.getenv("MODEL_NAME")
22
 
23
+ SLIDER_MIN_YEAR = 2005 # фиксированный минимум
24
+
25
+ # Для Excel (полный набор)
26
  COLUMN_LABELS_RU_EXCEL = {
27
  "№": "№",
28
  "score": "Сходство",
 
35
  "vak_link": "Ссылка ВАК",
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
38
  DISPLAY_COLUMNS_ALL = [
39
  "№",
40
  "score",
 
46
  "registration_number",
47
  ]
48
 
49
+ # ==========================
50
+ # ПРОВЕРКА СЕКРЕТОВ
51
+ # ==========================
52
+
53
  if HF_TOKEN is None:
54
  st.error(
55
  "Не найден секрет `HF_TOKEN`. "
 
97
  "Биологические",
98
  }
99
 
100
+ # Паттерны для сопоставления с degree_pursued (на случай разных формулировок)
101
  SCIENCE_PATTERNS = {
102
  "Архитектура": ["архитектур"],
103
  "Биологические": ["биолог"],
 
121
  "Юридические науки": ["юридич"],
122
  }
123
 
124
+
125
+ def _keyify(label: str) -> str:
126
+ # стабильный ключ для streamlit
127
+ return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
128
+
129
+
130
  # ==========================
131
  # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
132
  # ==========================
133
 
134
  @st.cache_data(show_spinner="Загрузка данных...")
135
  def load_data():
136
+ # 1) META
137
  ds_meta = load_dataset(HF_MERGED_REPO, split="train")
138
  df_meta = ds_meta.to_pandas()
139
  df_meta["registration_number"] = df_meta["registration_number"].astype(str)
140
  df_meta = df_meta.set_index("registration_number", drop=False)
141
 
142
+ # 2) EMB
143
  ds_emb = load_dataset(HF_EMB_REPO, split="train")
144
  df_emb = ds_emb.to_pandas()
145
  df_emb["registration_number"] = df_emb["registration_number"].astype(str)
146
+
147
  reg_nums = df_emb["registration_number"].tolist()
148
 
149
  emb_matrix = np.vstack(df_emb["embedding"].to_list()).astype("float32")
150
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
151
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
152
 
153
+ # выравниваем meta под reg_nums
154
  meta_aligned = df_meta.reindex(reg_nums)
155
 
156
+ # dissertation_type masks
157
+ type_s = meta_aligned.get("dissertation_type", pd.Series([""] * len(reg_nums))).fillna("").astype(str)
 
 
 
 
158
  is_candidate = type_s.str.contains("кандид", case=False, na=False).to_numpy()
159
  is_doctor = type_s.str.contains("доктор", case=False, na=False).to_numpy()
160
 
161
+ # degree_pursued lower
162
+ deg_s = meta_aligned.get("degree_pursued", pd.Series([""] * len(reg_nums))).fillna("").astype(str)
163
+ degree_lower = np.char.lower(deg_s.to_numpy().astype(str))
 
 
 
164
 
165
+ # year array
166
  if "protection_date" in meta_aligned.columns:
167
  dt = pd.to_datetime(meta_aligned["protection_date"], errors="coerce")
168
  year_arr = dt.dt.year.astype("float").to_numpy()
 
208
  ) -> np.ndarray:
209
  mask = np.ones(len(reg_nums), dtype=bool)
210
 
211
+ # 1) Тип диссертации
212
  type_mask = np.zeros(len(reg_nums), dtype=bool)
213
  if candidate_selected:
214
  type_mask |= is_candidate
215
  if doctor_selected:
216
  type_mask |= is_doctor
217
+ mask &= type_mask # если оба выключены -> всё False
218
 
219
+ # 2) Науки
220
  if science_selected:
221
  sci_mask = np.zeros(len(reg_nums), dtype=bool)
222
  for label in science_selected:
 
224
  sci_mask |= _contains_any(degree_lower, patterns)
225
  mask &= sci_mask
226
 
227
+ # 3) Годы (NaN пропускаем)
228
  if year_range is not None:
229
  y0, y1 = int(year_range[0]), int(year_range[1])
230
  yr = year_arr
231
+ mask &= (np.isnan(yr) | ((yr >= y0) & (yr <= y1)))
 
232
 
233
  return mask
234
 
 
257
  ]
258
 
259
 
260
+ def extract_year_int(value) -> Optional[int]:
261
  if value is None:
262
  return None
263
  try:
264
  if not isinstance(value, str):
265
  if pd.isna(value):
266
  return None
267
+ dt = pd.to_datetime(value, errors="coerce")
268
+ if pd.isna(dt):
269
+ return None
270
+ return int(dt.year)
271
  except Exception:
272
  pass
273
+
274
  s = str(value).strip()
275
  if len(s) >= 4 and s[:4].isdigit():
276
+ return int(s[:4])
277
  return None
278
 
279
 
 
290
  else:
291
  meta = pd.Series({}, index=df_all.columns)
292
 
293
+ protection_year = extract_year_int(meta.get("protection_date", None))
294
 
295
  rows.append(
296
  {
 
323
  ):
324
  query = query.strip()
325
  if not query:
326
+ return pd.DataFrame(), None, pd.DataFrame()
327
 
328
  mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
329
  results = search_core(query, top_k, mask=mask)
 
332
  if df_raw.empty:
333
  return pd.DataFrame(), None, df_raw
334
 
335
+ # --- UI dataframe (скрываем №, рег.номер, тип) ---
336
  df_ui = df_raw.copy()
337
+ df_ui = df_ui.rename(
338
+ columns={
339
+ "score": "Сходство",
340
+ "fio": "ФИО",
341
+ "title": "Название диссертации",
342
+ "author_org_short": "Организация",
343
+ "protection_year": "Год",
344
+ "vak_link": "ВАК",
345
+ }
346
+ )
347
 
348
+ # Убираем не нужные колонки (как вы просили)
349
+ df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
 
 
 
 
 
 
 
 
350
 
351
  # --- Excel: полный набор ---
352
  df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
 
353
  output = io.BytesIO()
354
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
355
  df_excel_ru.to_excel(writer, index=False)
 
358
  return df_ui, output, df_raw
359
 
360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  # ==========================
362
+ # UI
363
  # ==========================
364
 
365
  st.markdown(
 
367
  unsafe_allow_html=True,
368
  )
369
 
 
 
370
  data_has_years = np.isfinite(year_arr).any()
371
  year_max = int(np.nanmax(year_arr)) if data_has_years else None
372
 
 
394
  for i, label in enumerate(SCIENCE_LABELS):
395
  default_val = label in DEFAULT_SCIENCES
396
  with cols[i % 3]:
397
+ if st.checkbox(label, value=default_val, key=_keyify("sci_" + label)):
398
  science_selected.append(label)
399
 
400
  st.markdown("**Годы защиты:**")
 
423
  st.stop()
424
 
425
  with st.spinner("Идёт поиск по базе диссертаций..."):
426
+ df_ui, excel_bytes, df_raw = run_search(
427
  query=query,
428
  top_k=top_k,
429
  candidate_selected=candidate_selected,
 
437
  else:
438
  st.success(f"Найдено записей: {len(df_ui)}")
439
 
440
+ # ВАЖНО:
441
+ # st.dataframe даёт сортировку по клику, но перенос текста в ячейках не поддерживает.
442
+ # Чтобы читать длинные названия без горизонтального скролла — ниже блок "Детали по выбранной строке".
443
+ df_ui_show = df_ui[["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"]].copy()
444
+
445
+ event = st.dataframe(
446
+ df_ui_show,
447
+ use_container_width=True,
448
+ hide_index=True,
449
+ column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"],
450
+ column_config={
451
+ "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
452
+ "ФИО": st.column_config.TextColumn("ФИО", width="medium"),
453
+ азвание диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
454
+ "Организация": st.column_config.TextColumn("Организация", width="medium"),
455
+ "Год": st.column_config.NumberColumn("Год", width="small"),
456
+ "ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
457
+ },
458
+ on_select="rerun",
459
+ selection_mode="single-row",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  )
461
 
462
+ # Детали с переносом строк (без горизонтального скролла)
463
+ sel = getattr(event, "selection", None)
464
+ sel_rows = sel.rows if sel is not None else []
465
+ if sel_rows:
466
+ i = sel_rows[0]
467
+ row = df_ui_show.iloc[i].to_dict()
468
+ st.markdown("### Детали по выбранной записи")
469
+ st.markdown(f"**ФИО:** {row.get('ФИО', '')}")
470
+ st.markdown(f"**Организация:** {row.get('Организация', '')}")
471
+ st.markdown(f"**Год:** {row.get('Год', '')}")
472
+ st.markdown(f"**Сходство:** {row.get('Сходство', '')}")
473
+ st.markdown("**Название диссертации:**")
474
+ st.write(row.get("Название диссертации", "")) # переносится естественно
475
+ if row.get("ВАК"):
476
+ st.markdown(f"**ВАК:** {row.get('ВАК')}")
477
 
478
  if excel_bytes is not None:
479
  st.download_button(