Update app.py
Browse files
app.py
CHANGED
|
@@ -82,13 +82,12 @@ except Exception:
|
|
| 82 |
|
| 83 |
|
| 84 |
# ==========================
|
| 85 |
-
# CSS
|
| 86 |
# ==========================
|
| 87 |
|
| 88 |
st.markdown(
|
| 89 |
"""
|
| 90 |
<style>
|
| 91 |
-
/* уменьшение шрифта вокруг dataframes */
|
| 92 |
div[data-testid="stDataFrame"] { font-size: 80% !important; }
|
| 93 |
div[data-testid="stDataFrame"] * { font-size: 80% !important; }
|
| 94 |
|
|
@@ -123,11 +122,6 @@ def _safe_text(x: Any) -> str:
|
|
| 123 |
return s
|
| 124 |
|
| 125 |
|
| 126 |
-
def _safe_fragment(s: Any) -> str:
|
| 127 |
-
t = _safe_text(s).replace("#", " ").replace("\n", " ").strip()
|
| 128 |
-
return t
|
| 129 |
-
|
| 130 |
-
|
| 131 |
def _norm_openalex_url(x: Any) -> str:
|
| 132 |
s = _safe_text(x)
|
| 133 |
if not s:
|
|
@@ -152,6 +146,12 @@ def _keyify(label: str) -> str:
|
|
| 152 |
return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
|
| 153 |
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
# ==========================
|
| 156 |
# ФИЛЬТРЫ (UI)
|
| 157 |
# ==========================
|
|
@@ -256,10 +256,9 @@ def load_oa_enrichment() -> pd.DataFrame:
|
|
| 256 |
if "orcid_url" in df.columns:
|
| 257 |
df["orcid_url"] = df["orcid_url"].map(_norm_orcid_url)
|
| 258 |
|
| 259 |
-
# метрики сразу в numeric (иначе получите object + None)
|
| 260 |
for c in ["h_index", "i10_index", "works_count", "cited_by_count"]:
|
| 261 |
if c in df.columns:
|
| 262 |
-
df[c] = pd.to_numeric(df[c], errors="coerce")
|
| 263 |
|
| 264 |
keep = ["reg_norm", "openalex_url", "orcid_url", "h_index", "i10_index", "works_count", "cited_by_count"]
|
| 265 |
keep = [c for c in keep if c in df.columns]
|
|
@@ -461,7 +460,6 @@ def run_search(
|
|
| 461 |
results = search_core(query, top_k, mask=mask)
|
| 462 |
df_raw = build_result_df(results)
|
| 463 |
|
| 464 |
-
# OA join: критично держать числовые типы (иначе i10 сортируется как строка)
|
| 465 |
if not df_raw.empty and not oa_enrich.empty:
|
| 466 |
regs_norm = df_raw["registration_number"].map(_norm_regnum)
|
| 467 |
en = oa_enrich.reindex(regs_norm).reset_index(drop=True)
|
|
@@ -474,11 +472,10 @@ def run_search(
|
|
| 474 |
df_raw["openalex_url"] = _get("openalex_url", "").map(_norm_openalex_url)
|
| 475 |
df_raw["orcid_url"] = _get("orcid_url", "").map(_norm_orcid_url)
|
| 476 |
|
| 477 |
-
|
| 478 |
-
df_raw["
|
| 479 |
-
df_raw["
|
| 480 |
-
df_raw["
|
| 481 |
-
df_raw["cited_by_count"] = pd.to_numeric(_get("cited_by_count", np.nan), errors="coerce").astype("float64")
|
| 482 |
else:
|
| 483 |
df_raw["openalex_url"] = ""
|
| 484 |
df_raw["orcid_url"] = ""
|
|
@@ -487,31 +484,35 @@ def run_search(
|
|
| 487 |
df_raw["works_count"] = np.nan
|
| 488 |
df_raw["cited_by_count"] = np.nan
|
| 489 |
|
| 490 |
-
# UI: ФИО
|
| 491 |
fio_txt = df_raw["fio"].map(_safe_text)
|
| 492 |
-
|
| 493 |
-
fio_cell = np.where(orcid_url != "", orcid_url + "#" + fio_txt.map(_safe_fragment), fio_txt)
|
| 494 |
|
| 495 |
title_txt = df_raw["title"].map(_safe_text)
|
| 496 |
vak_link = df_raw["vak_link"].map(_safe_text)
|
| 497 |
-
title_cell = np.where(vak_link != "", vak_link + "#" + title_txt.map(_safe_fragment), title_txt)
|
| 498 |
|
| 499 |
df_ui = pd.DataFrame(
|
| 500 |
{
|
| 501 |
"_rid": np.arange(len(df_raw)),
|
| 502 |
-
"Сходство":
|
| 503 |
-
"
|
| 504 |
-
"
|
|
|
|
|
|
|
| 505 |
"Организация": df_raw["author_org_short"].map(_safe_text),
|
| 506 |
-
"Год":
|
| 507 |
"OpenAlex": df_raw["openalex_url"].map(_safe_text),
|
| 508 |
-
"h-index": df_raw["h_index"],
|
| 509 |
-
"i10-index": df_raw["i10_index"],
|
| 510 |
-
"Работ": df_raw["works_count"],
|
| 511 |
-
"Цитат": df_raw["cited_by_count"],
|
| 512 |
}
|
| 513 |
).set_index("_rid", drop=True)
|
| 514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
# Excel
|
| 516 |
df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
|
| 517 |
output = io.BytesIO()
|
|
@@ -638,7 +639,9 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 638 |
selection_mode="multi-row",
|
| 639 |
column_order=[
|
| 640 |
"Сходство",
|
|
|
|
| 641 |
"ФИО",
|
|
|
|
| 642 |
"Название диссертации",
|
| 643 |
"Организация",
|
| 644 |
"Год",
|
|
@@ -650,27 +653,34 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 650 |
],
|
| 651 |
column_config={
|
| 652 |
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
"Организация": st.column_config.TextColumn("Организация", width="large"),
|
|
|
|
| 666 |
"Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
|
|
|
|
| 667 |
"OpenAlex": st.column_config.LinkColumn(
|
| 668 |
"OpenAlex",
|
| 669 |
display_text=r"https://openalex\.org/(A\d+)",
|
| 670 |
width="small",
|
| 671 |
help="Профиль автора в OpenAlex (если найден).",
|
| 672 |
),
|
| 673 |
-
|
|
|
|
| 674 |
"h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
|
| 675 |
"i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
|
| 676 |
"Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
|
|
@@ -682,7 +692,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 682 |
if sel is not None and sel.rows is not None:
|
| 683 |
st.session_state.selected_rids = list(sel.rows)
|
| 684 |
|
| 685 |
-
#
|
| 686 |
if st.session_state.selected_rids:
|
| 687 |
with st.expander("Полный текст (для выбранных строк)", expanded=False):
|
| 688 |
for rid in st.session_state.selected_rids[:50]:
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
# ==========================
|
| 85 |
+
# CSS (шрифт таблицы 80%)
|
| 86 |
# ==========================
|
| 87 |
|
| 88 |
st.markdown(
|
| 89 |
"""
|
| 90 |
<style>
|
|
|
|
| 91 |
div[data-testid="stDataFrame"] { font-size: 80% !important; }
|
| 92 |
div[data-testid="stDataFrame"] * { font-size: 80% !important; }
|
| 93 |
|
|
|
|
| 122 |
return s
|
| 123 |
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
def _norm_openalex_url(x: Any) -> str:
|
| 126 |
s = _safe_text(x)
|
| 127 |
if not s:
|
|
|
|
| 146 |
return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
|
| 147 |
|
| 148 |
|
| 149 |
+
def _as_float_series(s: Any) -> pd.Series:
|
| 150 |
+
"""Гарантирует float64 + NaN (никаких None / object)."""
|
| 151 |
+
out = pd.to_numeric(s, errors="coerce")
|
| 152 |
+
return out.astype("float64")
|
| 153 |
+
|
| 154 |
+
|
| 155 |
# ==========================
|
| 156 |
# ФИЛЬТРЫ (UI)
|
| 157 |
# ==========================
|
|
|
|
| 256 |
if "orcid_url" in df.columns:
|
| 257 |
df["orcid_url"] = df["orcid_url"].map(_norm_orcid_url)
|
| 258 |
|
|
|
|
| 259 |
for c in ["h_index", "i10_index", "works_count", "cited_by_count"]:
|
| 260 |
if c in df.columns:
|
| 261 |
+
df[c] = pd.to_numeric(df[c], errors="coerce").astype("float64")
|
| 262 |
|
| 263 |
keep = ["reg_norm", "openalex_url", "orcid_url", "h_index", "i10_index", "works_count", "cited_by_count"]
|
| 264 |
keep = [c for c in keep if c in df.columns]
|
|
|
|
| 460 |
results = search_core(query, top_k, mask=mask)
|
| 461 |
df_raw = build_result_df(results)
|
| 462 |
|
|
|
|
| 463 |
if not df_raw.empty and not oa_enrich.empty:
|
| 464 |
regs_norm = df_raw["registration_number"].map(_norm_regnum)
|
| 465 |
en = oa_enrich.reindex(regs_norm).reset_index(drop=True)
|
|
|
|
| 472 |
df_raw["openalex_url"] = _get("openalex_url", "").map(_norm_openalex_url)
|
| 473 |
df_raw["orcid_url"] = _get("orcid_url", "").map(_norm_orcid_url)
|
| 474 |
|
| 475 |
+
df_raw["h_index"] = _as_float_series(_get("h_index", np.nan))
|
| 476 |
+
df_raw["i10_index"] = _as_float_series(_get("i10_index", np.nan))
|
| 477 |
+
df_raw["works_count"] = _as_float_series(_get("works_count", np.nan))
|
| 478 |
+
df_raw["cited_by_count"] = _as_float_series(_get("cited_by_count", np.nan))
|
|
|
|
| 479 |
else:
|
| 480 |
df_raw["openalex_url"] = ""
|
| 481 |
df_raw["orcid_url"] = ""
|
|
|
|
| 484 |
df_raw["works_count"] = np.nan
|
| 485 |
df_raw["cited_by_count"] = np.nan
|
| 486 |
|
| 487 |
+
# UI: ФИО — текст; ORCID — отдельная безымянная ссылка (пусто если нет)
|
| 488 |
fio_txt = df_raw["fio"].map(_safe_text)
|
| 489 |
+
orcid_link = df_raw["orcid_url"].map(_safe_text)
|
|
|
|
| 490 |
|
| 491 |
title_txt = df_raw["title"].map(_safe_text)
|
| 492 |
vak_link = df_raw["vak_link"].map(_safe_text)
|
|
|
|
| 493 |
|
| 494 |
df_ui = pd.DataFrame(
|
| 495 |
{
|
| 496 |
"_rid": np.arange(len(df_raw)),
|
| 497 |
+
"Сходство": _as_float_series(df_raw["score"]),
|
| 498 |
+
" ": orcid_link, # безымянная колонка-ссылка (ORCID)
|
| 499 |
+
"ФИО": fio_txt, # всегда текст (не ссылка)
|
| 500 |
+
" ": vak_link, # ещё одна безымянная колонка-ссылка (ВАК)
|
| 501 |
+
"Название диссертации": title_txt, # всегда текст (не ссылка)
|
| 502 |
"Организация": df_raw["author_org_short"].map(_safe_text),
|
| 503 |
+
"Год": _as_float_series(df_raw["protection_year"]),
|
| 504 |
"OpenAlex": df_raw["openalex_url"].map(_safe_text),
|
| 505 |
+
"h-index": _as_float_series(df_raw["h_index"]),
|
| 506 |
+
"i10-index": _as_float_series(df_raw["i10_index"]),
|
| 507 |
+
"Работ": _as_float_series(df_raw["works_count"]),
|
| 508 |
+
"Цитат": _as_float_series(df_raw["cited_by_count"]),
|
| 509 |
}
|
| 510 |
).set_index("_rid", drop=True)
|
| 511 |
|
| 512 |
+
# гарантированно убрать "None" в текстовых колонках (если вдруг прилетело строкой)
|
| 513 |
+
for c in df_ui.select_dtypes(include=["object"]).columns:
|
| 514 |
+
df_ui[c] = df_ui[c].map(_safe_text)
|
| 515 |
+
|
| 516 |
# Excel
|
| 517 |
df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
|
| 518 |
output = io.BytesIO()
|
|
|
|
| 639 |
selection_mode="multi-row",
|
| 640 |
column_order=[
|
| 641 |
"Сходство",
|
| 642 |
+
" ",
|
| 643 |
"ФИО",
|
| 644 |
+
" ",
|
| 645 |
"Название диссертации",
|
| 646 |
"Организация",
|
| 647 |
"Год",
|
|
|
|
| 653 |
],
|
| 654 |
column_config={
|
| 655 |
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 656 |
+
|
| 657 |
+
# ORCID: ссылка только если URL не пустой
|
| 658 |
+
" ": st.column_config.LinkColumn("",
|
| 659 |
+
display_text="ORCID",
|
| 660 |
+
width="small",
|
| 661 |
+
help="Профиль ORCID (если найден)."),
|
| 662 |
+
|
| 663 |
+
"ФИО": st.column_config.TextColumn("ФИО", width="medium"),
|
| 664 |
+
|
| 665 |
+
# ВАК: ссылка только если URL не пустой
|
| 666 |
+
" ": st.column_config.LinkColumn("",
|
| 667 |
+
display_text="ВАК",
|
| 668 |
+
width="small",
|
| 669 |
+
help="Открыть запись на сайте ВАК (если есть ссылка)."),
|
| 670 |
+
|
| 671 |
+
"Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
|
| 672 |
"Организация": st.column_config.TextColumn("Организация", width="large"),
|
| 673 |
+
|
| 674 |
"Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
|
| 675 |
+
|
| 676 |
"OpenAlex": st.column_config.LinkColumn(
|
| 677 |
"OpenAlex",
|
| 678 |
display_text=r"https://openalex\.org/(A\d+)",
|
| 679 |
width="small",
|
| 680 |
help="Профиль автора в OpenAlex (если найден).",
|
| 681 |
),
|
| 682 |
+
|
| 683 |
+
# Форматирование + NaN => пустая ячейка; сортировка numeric
|
| 684 |
"h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
|
| 685 |
"i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
|
| 686 |
"Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
|
|
|
|
| 692 |
if sel is not None and sel.rows is not None:
|
| 693 |
st.session_state.selected_rids = list(sel.rows)
|
| 694 |
|
| 695 |
+
# Компенсация отсутствия word-wrap: показываем полный текст выбранных строк
|
| 696 |
if st.session_state.selected_rids:
|
| 697 |
with st.expander("Полный текст (для выбранных строк)", expanded=False):
|
| 698 |
for rid in st.session_state.selected_rids[:50]:
|