Update app.py
Browse files
app.py
CHANGED
|
@@ -2,9 +2,8 @@ import os
|
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
import uuid
|
| 5 |
-
import re
|
| 6 |
-
from typing import Optional, Tuple, List, Dict, Any
|
| 7 |
from datetime import datetime, timezone
|
|
|
|
| 8 |
|
| 9 |
import numpy as np
|
| 10 |
import pandas as pd
|
|
@@ -30,7 +29,7 @@ HF_REQUESTS_REPO = os.getenv("HF_REQUESTS_REPO", "PlanetExpress2125/PostDocReque
|
|
| 30 |
HF_REQUESTS_REPO_TYPE = os.getenv("HF_REQUESTS_REPO_TYPE", "dataset") # dataset | model
|
| 31 |
HF_WRITE_TOKEN = os.getenv("HF_WRITE_TOKEN") # write token (рекомендуется)
|
| 32 |
|
| 33 |
-
# Компактное OA-обогащение (
|
| 34 |
OA_ENRICH_REPO = os.getenv("OA_ENRICH_REPO", "yogl/postdoc_oa_enrichment")
|
| 35 |
|
| 36 |
SLIDER_MIN_YEAR = 2005 # нижняя граница диапазона лет
|
|
@@ -84,6 +83,22 @@ except Exception:
|
|
| 84 |
pass
|
| 85 |
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
# ==========================
|
| 88 |
# СПРАВОЧНИКИ ФИЛЬТРОВ (UI)
|
| 89 |
# ==========================
|
|
@@ -112,12 +127,7 @@ SCIENCE_LABELS = [
|
|
| 112 |
]
|
| 113 |
SCIENCE_LABELS = sorted(list(dict.fromkeys(SCIENCE_LABELS)), key=lambda s: s.casefold())
|
| 114 |
|
| 115 |
-
DEFAULT_SCIENCES = {
|
| 116 |
-
"Технические",
|
| 117 |
-
"Физико-математические",
|
| 118 |
-
"Химические",
|
| 119 |
-
"Биологические",
|
| 120 |
-
}
|
| 121 |
|
| 122 |
SCIENCE_PATTERNS = {
|
| 123 |
"Архитектура": ["архитектур"],
|
|
@@ -143,10 +153,6 @@ SCIENCE_PATTERNS = {
|
|
| 143 |
}
|
| 144 |
|
| 145 |
|
| 146 |
-
def _keyify(label: str) -> str:
|
| 147 |
-
return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
|
| 148 |
-
|
| 149 |
-
|
| 150 |
# ==========================
|
| 151 |
# ЗАПИСЬ ЗАЯВОК В HF REPO
|
| 152 |
# ==========================
|
|
@@ -186,10 +192,24 @@ def load_oa_enrichment() -> pd.DataFrame:
|
|
| 186 |
"""
|
| 187 |
ds = load_dataset(OA_ENRICH_REPO, split="train")
|
| 188 |
df = ds.to_pandas()
|
|
|
|
| 189 |
if "registration_number" not in df.columns:
|
| 190 |
-
return pd.DataFrame().set_index(pd.Index([], name="
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
|
| 195 |
oa_enrich = load_oa_enrichment()
|
|
@@ -203,12 +223,12 @@ oa_enrich = load_oa_enrichment()
|
|
| 203 |
def load_data():
|
| 204 |
ds_meta = load_dataset(HF_MERGED_REPO, split="train")
|
| 205 |
df_meta = ds_meta.to_pandas()
|
| 206 |
-
df_meta["registration_number"] = df_meta["registration_number"].astype(str)
|
| 207 |
df_meta = df_meta.set_index("registration_number", drop=False)
|
| 208 |
|
| 209 |
ds_emb = load_dataset(HF_EMB_REPO, split="train")
|
| 210 |
df_emb = ds_emb.to_pandas()
|
| 211 |
-
df_emb["registration_number"] = df_emb["registration_number"].astype(str)
|
| 212 |
|
| 213 |
reg_nums = df_emb["registration_number"].tolist()
|
| 214 |
|
|
@@ -340,7 +360,7 @@ def extract_year_int(value) -> Optional[int]:
|
|
| 340 |
def build_result_df(results):
|
| 341 |
rows = []
|
| 342 |
for r in results:
|
| 343 |
-
reg = r["registration_number"]
|
| 344 |
score = r["score"]
|
| 345 |
|
| 346 |
if reg in df_all.index:
|
|
@@ -384,27 +404,34 @@ def run_search(
|
|
| 384 |
results = search_core(query, top_k, mask=mask)
|
| 385 |
df_raw = build_result_df(results)
|
| 386 |
|
| 387 |
-
# OA enrichment join (по
|
| 388 |
if not df_raw.empty and not oa_enrich.empty:
|
| 389 |
-
|
| 390 |
-
en = oa_enrich.reindex(
|
| 391 |
-
|
| 392 |
-
def
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
df_raw["
|
| 398 |
-
df_raw["
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
else:
|
| 402 |
df_raw["openalex_url"] = ""
|
| 403 |
df_raw["orcid_url"] = ""
|
| 404 |
-
df_raw["h_index"] =
|
| 405 |
-
df_raw["i10_index"] =
|
| 406 |
-
df_raw["works_count"] =
|
| 407 |
-
df_raw["cited_by_count"] =
|
| 408 |
|
| 409 |
# UI: скрываем №, registration_number, dissertation_type
|
| 410 |
df_ui = df_raw.copy().reset_index(drop=True)
|
|
@@ -457,8 +484,6 @@ st.markdown(
|
|
| 457 |
)
|
| 458 |
|
| 459 |
# session_state — чтобы результаты не исчезали на rerun
|
| 460 |
-
if "last_df_ui" not in st.session_state:
|
| 461 |
-
st.session_state.last_df_ui = None
|
| 462 |
if "last_df_raw" not in st.session_state:
|
| 463 |
st.session_state.last_df_raw = None
|
| 464 |
if "last_excel" not in st.session_state:
|
|
@@ -533,11 +558,9 @@ if do_search:
|
|
| 533 |
year_range=year_range,
|
| 534 |
)
|
| 535 |
|
| 536 |
-
st.session_state.last_df_ui = df_ui
|
| 537 |
st.session_state.last_df_raw = df_raw
|
| 538 |
st.session_state.last_excel = excel_bytes
|
| 539 |
|
| 540 |
-
# подготовим таблицу с чекбоксом
|
| 541 |
if not df_ui.empty:
|
| 542 |
df_table = df_ui[
|
| 543 |
["Сходство", "ORCID", "ФИО", "Название диссертации", "Организация", "Год",
|
|
@@ -548,7 +571,7 @@ if do_search:
|
|
| 548 |
else:
|
| 549 |
st.session_state.table_df = None
|
| 550 |
|
| 551 |
-
st.session_state.search_id += 1 #
|
| 552 |
|
| 553 |
# Отображение результатов
|
| 554 |
df_table_saved = st.session_state.table_df
|
|
@@ -558,7 +581,6 @@ excel_saved = st.session_state.last_excel
|
|
| 558 |
if isinstance(df_table_saved, pd.DataFrame) and not df_table_saved.empty:
|
| 559 |
st.success(f"Найдено записей: {len(df_table_saved)}")
|
| 560 |
|
| 561 |
-
# data_editor: сортировка по клику на заголовок + чекбокс в строке
|
| 562 |
editor_key = f"results_editor_{st.session_state.search_id}"
|
| 563 |
|
| 564 |
edited = st.data_editor(
|
|
@@ -574,6 +596,7 @@ if isinstance(df_table_saved, pd.DataFrame) and not df_table_saved.empty:
|
|
| 574 |
"ORCID",
|
| 575 |
display_text=r"https://orcid\.org/([0-9X\-]+)",
|
| 576 |
width="small",
|
|
|
|
| 577 |
),
|
| 578 |
"ФИО": st.column_config.TextColumn("ФИО", width="medium"),
|
| 579 |
"Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
|
|
@@ -584,6 +607,7 @@ if isinstance(df_table_saved, pd.DataFrame) and not df_table_saved.empty:
|
|
| 584 |
"OpenAlex",
|
| 585 |
display_text=r"https://openalex\.org/(A\d+)",
|
| 586 |
width="small",
|
|
|
|
| 587 |
),
|
| 588 |
"h-index": st.column_config.NumberColumn("h-index", width="small"),
|
| 589 |
"i10-index": st.column_config.NumberColumn("i10-index", width="small"),
|
|
@@ -595,6 +619,15 @@ if isinstance(df_table_saved, pd.DataFrame) and not df_table_saved.empty:
|
|
| 595 |
# сохраняем чекбоксы
|
| 596 |
st.session_state.table_df = edited
|
| 597 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
if excel_saved is not None:
|
| 599 |
st.download_button(
|
| 600 |
label="💾 Скачать результаты в Excel",
|
|
|
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
import uuid
|
|
|
|
|
|
|
| 5 |
from datetime import datetime, timezone
|
| 6 |
+
from typing import Optional, Tuple, List, Any
|
| 7 |
|
| 8 |
import numpy as np
|
| 9 |
import pandas as pd
|
|
|
|
| 29 |
HF_REQUESTS_REPO_TYPE = os.getenv("HF_REQUESTS_REPO_TYPE", "dataset") # dataset | model
|
| 30 |
HF_WRITE_TOKEN = os.getenv("HF_WRITE_TOKEN") # write token (рекомендуется)
|
| 31 |
|
| 32 |
+
# Компактное OA-обогащение (собрано вами)
|
| 33 |
OA_ENRICH_REPO = os.getenv("OA_ENRICH_REPO", "yogl/postdoc_oa_enrichment")
|
| 34 |
|
| 35 |
SLIDER_MIN_YEAR = 2005 # нижняя граница диапазона лет
|
|
|
|
| 83 |
pass
|
| 84 |
|
| 85 |
|
| 86 |
+
# ==========================
|
| 87 |
+
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
| 88 |
+
# ==========================
|
| 89 |
+
|
| 90 |
+
def _norm_regnum(x: Any) -> str:
|
| 91 |
+
s = "" if x is None else str(x).strip()
|
| 92 |
+
# иногда номера могут попасть как float "123.0"
|
| 93 |
+
if s.endswith(".0") and s[:-2].isdigit():
|
| 94 |
+
s = s[:-2]
|
| 95 |
+
return s
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _keyify(label: str) -> str:
|
| 99 |
+
return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
# ==========================
|
| 103 |
# СПРАВОЧНИКИ ФИЛЬТРОВ (UI)
|
| 104 |
# ==========================
|
|
|
|
| 127 |
]
|
| 128 |
SCIENCE_LABELS = sorted(list(dict.fromkeys(SCIENCE_LABELS)), key=lambda s: s.casefold())
|
| 129 |
|
| 130 |
+
DEFAULT_SCIENCES = {"Технические", "Физико-математические", "Химические", "Биологические"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
SCIENCE_PATTERNS = {
|
| 133 |
"Архитектура": ["архитектур"],
|
|
|
|
| 153 |
}
|
| 154 |
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
# ==========================
|
| 157 |
# ЗАПИСЬ ЗАЯВОК В HF REPO
|
| 158 |
# ==========================
|
|
|
|
| 192 |
"""
|
| 193 |
ds = load_dataset(OA_ENRICH_REPO, split="train")
|
| 194 |
df = ds.to_pandas()
|
| 195 |
+
|
| 196 |
if "registration_number" not in df.columns:
|
| 197 |
+
return pd.DataFrame().set_index(pd.Index([], name="reg_norm"))
|
| 198 |
+
|
| 199 |
+
df["reg_norm"] = df["registration_number"].apply(_norm_regnum)
|
| 200 |
+
|
| 201 |
+
# совместимость, если в датасете использовались другие имена колонок
|
| 202 |
+
if "openalex_url" not in df.columns and "openalex" in df.columns:
|
| 203 |
+
df["openalex_url"] = df["openalex"]
|
| 204 |
+
if "orcid_url" not in df.columns and "orcid" in df.columns:
|
| 205 |
+
df["orcid_url"] = df["orcid"]
|
| 206 |
+
|
| 207 |
+
keep = ["reg_norm", "openalex_url", "orcid_url", "h_index", "i10_index", "works_count", "cited_by_count"]
|
| 208 |
+
keep = [c for c in keep if c in df.columns]
|
| 209 |
+
df = df[keep].copy()
|
| 210 |
+
|
| 211 |
+
df = df.drop_duplicates(subset=["reg_norm"]).set_index("reg_norm", drop=True)
|
| 212 |
+
return df
|
| 213 |
|
| 214 |
|
| 215 |
oa_enrich = load_oa_enrichment()
|
|
|
|
| 223 |
def load_data():
|
| 224 |
ds_meta = load_dataset(HF_MERGED_REPO, split="train")
|
| 225 |
df_meta = ds_meta.to_pandas()
|
| 226 |
+
df_meta["registration_number"] = df_meta["registration_number"].astype(str).map(_norm_regnum)
|
| 227 |
df_meta = df_meta.set_index("registration_number", drop=False)
|
| 228 |
|
| 229 |
ds_emb = load_dataset(HF_EMB_REPO, split="train")
|
| 230 |
df_emb = ds_emb.to_pandas()
|
| 231 |
+
df_emb["registration_number"] = df_emb["registration_number"].astype(str).map(_norm_regnum)
|
| 232 |
|
| 233 |
reg_nums = df_emb["registration_number"].tolist()
|
| 234 |
|
|
|
|
| 360 |
def build_result_df(results):
|
| 361 |
rows = []
|
| 362 |
for r in results:
|
| 363 |
+
reg = _norm_regnum(r["registration_number"])
|
| 364 |
score = r["score"]
|
| 365 |
|
| 366 |
if reg in df_all.index:
|
|
|
|
| 404 |
results = search_core(query, top_k, mask=mask)
|
| 405 |
df_raw = build_result_df(results)
|
| 406 |
|
| 407 |
+
# OA enrichment join (выровнять по позициям, а не по индексу!)
|
| 408 |
if not df_raw.empty and not oa_enrich.empty:
|
| 409 |
+
regs_norm = df_raw["registration_number"].apply(_norm_regnum)
|
| 410 |
+
en = oa_enrich.reindex(regs_norm).reset_index(drop=True) # <-- КЛЮЧЕВО
|
| 411 |
+
|
| 412 |
+
def _get(col: str, default):
|
| 413 |
+
if col in en.columns:
|
| 414 |
+
return en[col]
|
| 415 |
+
return pd.Series([default] * len(df_raw))
|
| 416 |
+
|
| 417 |
+
df_raw["openalex_url"] = _get("openalex_url", "").fillna("")
|
| 418 |
+
df_raw["orcid_url"] = _get("orcid_url", "").fillna("")
|
| 419 |
+
|
| 420 |
+
for src, dst in [
|
| 421 |
+
("h_index", "h_index"),
|
| 422 |
+
("i10_index", "i10_index"),
|
| 423 |
+
("works_count", "works_count"),
|
| 424 |
+
("cited_by_count", "cited_by_count"),
|
| 425 |
+
]:
|
| 426 |
+
s = _get(src, np.nan)
|
| 427 |
+
df_raw[dst] = pd.to_numeric(s, errors="coerce").astype("Int64")
|
| 428 |
else:
|
| 429 |
df_raw["openalex_url"] = ""
|
| 430 |
df_raw["orcid_url"] = ""
|
| 431 |
+
df_raw["h_index"] = pd.Series([pd.NA] * len(df_raw), dtype="Int64")
|
| 432 |
+
df_raw["i10_index"] = pd.Series([pd.NA] * len(df_raw), dtype="Int64")
|
| 433 |
+
df_raw["works_count"] = pd.Series([pd.NA] * len(df_raw), dtype="Int64")
|
| 434 |
+
df_raw["cited_by_count"] = pd.Series([pd.NA] * len(df_raw), dtype="Int64")
|
| 435 |
|
| 436 |
# UI: скрываем №, registration_number, dissertation_type
|
| 437 |
df_ui = df_raw.copy().reset_index(drop=True)
|
|
|
|
| 484 |
)
|
| 485 |
|
| 486 |
# session_state — чтобы результаты не исчезали на rerun
|
|
|
|
|
|
|
| 487 |
if "last_df_raw" not in st.session_state:
|
| 488 |
st.session_state.last_df_raw = None
|
| 489 |
if "last_excel" not in st.session_state:
|
|
|
|
| 558 |
year_range=year_range,
|
| 559 |
)
|
| 560 |
|
|
|
|
| 561 |
st.session_state.last_df_raw = df_raw
|
| 562 |
st.session_state.last_excel = excel_bytes
|
| 563 |
|
|
|
|
| 564 |
if not df_ui.empty:
|
| 565 |
df_table = df_ui[
|
| 566 |
["Сходство", "ORCID", "ФИО", "Название диссертации", "Организация", "Год",
|
|
|
|
| 571 |
else:
|
| 572 |
st.session_state.table_df = None
|
| 573 |
|
| 574 |
+
st.session_state.search_id += 1 # сброс state редактора на новый поиск
|
| 575 |
|
| 576 |
# Отображение результатов
|
| 577 |
df_table_saved = st.session_state.table_df
|
|
|
|
| 581 |
if isinstance(df_table_saved, pd.DataFrame) and not df_table_saved.empty:
|
| 582 |
st.success(f"Найдено записей: {len(df_table_saved)}")
|
| 583 |
|
|
|
|
| 584 |
editor_key = f"results_editor_{st.session_state.search_id}"
|
| 585 |
|
| 586 |
edited = st.data_editor(
|
|
|
|
| 596 |
"ORCID",
|
| 597 |
display_text=r"https://orcid\.org/([0-9X\-]+)",
|
| 598 |
width="small",
|
| 599 |
+
help="Профиль автора в ORCID (если найден)",
|
| 600 |
),
|
| 601 |
"ФИО": st.column_config.TextColumn("ФИО", width="medium"),
|
| 602 |
"Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
|
|
|
|
| 607 |
"OpenAlex",
|
| 608 |
display_text=r"https://openalex\.org/(A\d+)",
|
| 609 |
width="small",
|
| 610 |
+
help="Профиль автора в OpenAlex (если найден)",
|
| 611 |
),
|
| 612 |
"h-index": st.column_config.NumberColumn("h-index", width="small"),
|
| 613 |
"i10-index": st.column_config.NumberColumn("i10-index", width="small"),
|
|
|
|
| 619 |
# сохраняем чекбоксы
|
| 620 |
st.session_state.table_df = edited
|
| 621 |
|
| 622 |
+
# Диагностика соответствия reg -> OA (можно убрать позже)
|
| 623 |
+
with st.expander("Диагностика OpenAlex", expanded=False):
|
| 624 |
+
regs_norm = df_raw_saved["registration_number"].apply(_norm_regnum)
|
| 625 |
+
matched = regs_norm.isin(oa_enrich.index).sum() if not oa_enrich.empty else 0
|
| 626 |
+
st.write(f"Совпало registration_number → OA: {matched} из {len(df_raw_saved)}")
|
| 627 |
+
if matched == 0:
|
| 628 |
+
st.write("Пример registration_number из выдачи:", regs_norm.head(10).tolist())
|
| 629 |
+
st.write("Пример registration_number из enrichment:", list(oa_enrich.index[:10]) if not oa_enrich.empty else [])
|
| 630 |
+
|
| 631 |
if excel_saved is not None:
|
| 632 |
st.download_button(
|
| 633 |
label="💾 Скачать результаты в Excel",
|