Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 59 additions & 6 deletions functions/get_annualproduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,29 @@
import plotly.graph_objects as go


# Patch rispetto al file fornito:
# - gli import sono espliciti invece di `from www.services import *`, perche' la
# funzione usa solo pandas/plotly e non deve dipendere da oggetti globali;
# - e' stato aggiunto `_resolve_dataframe` per poter usare la stessa funzione sia
# in Biblioshiny, dove arriva un reactive.Value con `.get()`, sia nei controlli
# diretti sulla pipeline ETL, dove arriva un normale pd.DataFrame;
# - il calcolo della tabella annuale e' stato isolato in un helper per validare
# `PY` prima del plot: la versione originale assumeva anni gia' numerici.
def _resolve_dataframe(df):
"""Accept both a Shiny reactive value and a plain pandas DataFrame."""
"""
Risolve l'input dati accettando sia Biblioshiny sia test diretti.

Args:
df: Un `pd.DataFrame` gia' standardizzato oppure un oggetto reattivo
Shiny che espone il metodo `.get()`.

Returns:
pd.DataFrame: Copia del DataFrame da usare nei calcoli.

Raises:
ValueError: Se il valore reattivo non contiene dati.
TypeError: Se l'input risolto non e' un DataFrame pandas.
"""
if isinstance(df, pd.DataFrame):
data = df
elif hasattr(df, "get"):
Expand All @@ -16,13 +37,31 @@ def _resolve_dataframe(df):
raise ValueError("get_annual_production requires a non-empty DataFrame.")
if not isinstance(data, pd.DataFrame):
raise TypeError("get_annual_production expects a pandas DataFrame or an object with .get().")
# La copia evita che conversioni/filtri interni modifichino il DataFrame
# condiviso dalla dashboard.
return data.copy()


def _annual_publications_table(data):
"""
Costruisce la tabella della produzione scientifica per anno.

Args:
data (pd.DataFrame): DataFrame standardizzato contenente la colonna
`PY` con l'anno di pubblicazione.

Returns:
pd.DataFrame: Tabella con colonne `Year` e `Freq`, includendo anche
gli anni senza pubblicazioni con frequenza pari a 0.

Raises:
ValueError: Se `PY` manca o non contiene anni validi.
"""
if "PY" not in data.columns:
raise ValueError("Missing required column: PY.")

# Dopo la standardizzazione `PY` puo' arrivare come stringa; senza questa
# conversione `range(min_year, max_year + 1)` puo' fallire o ordinare male.
years = pd.to_numeric(data["PY"], errors="coerce").dropna().astype(int)
if years.empty:
raise ValueError("Column PY does not contain valid publication years.")
Expand All @@ -34,6 +73,8 @@ def _annual_publications_table(data):
max_year = int(publications_per_year["Year"].max())
all_years = pd.DataFrame({"Year": range(min_year, max_year + 1)})

# Come in bibliometrix, gli anni senza pubblicazioni vengono mantenuti a 0:
# cosi' il grafico mostra anche i buchi temporali della collezione.
publications_per_year = all_years.merge(
publications_per_year, on="Year", how="left"
).fillna({"Freq": 0})
Expand All @@ -43,13 +84,25 @@ def _annual_publications_table(data):

def get_annual_production(df):
"""
Generate a plot of annual scientific production.

Calcola e visualizza la produzione scientifica annuale.

Usa la colonna standardizzata `PY` per contare quanti documenti sono stati
pubblicati in ciascun anno. E' pensata per funzionare sia dalla dashboard
Biblioshiny sia passando direttamente il DataFrame prodotto dalla pipeline
ETL.

Args:
df: A pandas DataFrame or a Shiny reactive value containing the data.

df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che
contiene un DataFrame.

Returns:
A Plotly figure object and a DataFrame with annual publication counts.
tuple: `(fig, publications_per_year)`, dove `fig` e' un
`go.FigureWidget` Plotly e `publications_per_year` e' una tabella con
colonne `Year` e `Freq`.

Raises:
ValueError: Se il DataFrame e' vuoto o non contiene anni validi in `PY`.
TypeError: Se l'input non puo' essere risolto in un DataFrame pandas.
"""
data = _resolve_dataframe(df)
publications_per_year = _annual_publications_table(data)
Expand Down
49 changes: 43 additions & 6 deletions functions/get_averagecitations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,29 @@
import plotly.graph_objects as go


# Patch rispetto al file fornito:
# - sostituito `from www.services import *` con import espliciti, perche' qui
# servono solo pandas e plotly;
# - aggiunto `_resolve_dataframe`, perche' la versione originale usava sempre
# `df.get()` e quindi funzionava solo dentro Shiny, non con il DataFrame gia'
# restituito dalla pipeline ETL;
# - aggiunti controlli e conversioni su `PY` e `TC`, perche' file standardizzati
# da sorgenti diverse possono avere anni/citazioni come stringhe o valori NaN.
def _resolve_dataframe(df):
"""Accept both a Shiny reactive value and a plain pandas DataFrame."""
"""
Risolve l'input dati accettando sia Biblioshiny sia test diretti.

Args:
df: Un `pd.DataFrame` gia' standardizzato oppure un oggetto reattivo
Shiny che espone il metodo `.get()`.

Returns:
pd.DataFrame: Copia del DataFrame da usare nei calcoli.

Raises:
ValueError: Se il valore reattivo non contiene dati.
TypeError: Se l'input risolto non e' un DataFrame pandas.
"""
if isinstance(df, pd.DataFrame):
data = df
elif hasattr(df, "get"):
Expand All @@ -16,18 +37,31 @@ def _resolve_dataframe(df):
raise ValueError("get_average_citations requires a non-empty DataFrame.")
if not isinstance(data, pd.DataFrame):
raise TypeError("get_average_citations expects a pandas DataFrame or an object with .get().")
# Lavoriamo su una copia per non cambiare i tipi del DataFrame condiviso
# dalla dashboard mentre calcoliamo la metrica.
return data.copy()


def get_average_citations(df):
"""
Generate a plot of average citations per year.

Calcola le citazioni medie annue dei documenti.

La funzione usa `PY` come anno di pubblicazione e `TC` come totale delle
citazioni globali. Il risultato permette di osservare se gli articoli di un
certo anno ricevono, in media, piu' o meno citazioni per anno citabile.

Args:
df: A DataFrame object containing the data.

df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che
contiene un DataFrame.

Returns:
A Plotly figure object representing the average citations per year.
tuple: `(fig, table)`, dove `fig` e' un `go.FigureWidget` Plotly e
`table` contiene `Year`, `MeanTCperArt`, `N`, `MeanTCperYear` e
`CitableYears`.

Raises:
ValueError: Se mancano `PY`/`TC` o se `PY` non contiene anni validi.
TypeError: Se l'input non puo' essere risolto in un DataFrame pandas.
"""
data = _resolve_dataframe(df)

Expand All @@ -36,6 +70,9 @@ def get_average_citations(df):
if missing_columns:
raise ValueError(f"Missing required columns: {', '.join(sorted(missing_columns))}.")

# `PY` e `TC` sono colonne standard della pipeline, ma non sempre arrivano
# gia' numeriche. Le citazioni mancanti vengono trattate come 0: e' il caso
# tipico di sorgenti come PubMed, che spesso non esportano citazioni.
data["PY"] = pd.to_numeric(data["PY"], errors="coerce")
data["TC"] = pd.to_numeric(data["TC"], errors="coerce").fillna(0)
data = data.dropna(subset=["PY"]).copy()
Expand Down
48 changes: 42 additions & 6 deletions functions/get_bradfordlaw.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,27 @@
import plotly.graph_objects as go


# Patch rispetto al file fornito:
# - import espliciti invece di `from www.services import *`;
# - `_resolve_dataframe` rende la funzione usabile sia dalla dashboard Shiny sia
# da un DataFrame pandas gia' prodotto dalla ETL;
# - sono stati aggiunti controlli su `SO`, perche' la legge di Bradford si basa
# sulla frequenza delle fonti e fallisce se la standardizzazione non la produce.
def _resolve_dataframe(df):
"""Accept both a Shiny reactive value and a plain pandas DataFrame."""
"""
Risolve l'input dati accettando sia Biblioshiny sia test diretti.

Args:
df: Un `pd.DataFrame` gia' standardizzato oppure un oggetto reattivo
Shiny che espone il metodo `.get()`.

Returns:
pd.DataFrame: Copia del DataFrame da usare nei calcoli.

Raises:
ValueError: Se il valore reattivo non contiene dati.
TypeError: Se l'input risolto non e' un DataFrame pandas.
"""
if isinstance(df, pd.DataFrame):
data = df
elif hasattr(df, "get"):
Expand All @@ -16,25 +35,38 @@ def _resolve_dataframe(df):
raise ValueError("get_bradford_law requires a non-empty DataFrame.")
if not isinstance(data, pd.DataFrame):
raise TypeError("get_bradford_law expects a pandas DataFrame or an object with .get().")
# Usiamo una copia per non filtrare/modificare il DataFrame condiviso in app.
return data.copy()


def get_bradford_law(df):
"""
Generate a plot and table based on Bradford's Law.

Calcola e visualizza la distribuzione delle fonti secondo Bradford.

Usa la colonna standardizzata `SO` per ordinare le fonti per frequenza,
calcolare la frequenza cumulata e assegnare le zone di Bradford. Il grafico
evidenzia il nucleo di fonti piu' produttive della collezione.

Args:
df: A DataFrame object containing the data.

df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che
contiene un DataFrame.

Returns:
A Plotly figure object and a DataFrame of the Bradford's Law zones.
tuple: `(fig, df_bradford)`, dove `fig` e' un grafico Plotly e
`df_bradford` contiene `SO`, `Rank`, `Freq`, `cumFreq` e `Zone`.

Raises:
ValueError: Se manca `SO` o se non contiene fonti valide.
TypeError: Se l'input non puo' essere risolto in un DataFrame pandas.
"""
# Sort data by frequency of occurrence (equivalent to R's sort(table(M$SO), decreasing = TRUE))
data = _resolve_dataframe(df)

if "SO" not in data.columns:
raise ValueError("Missing required column: SO.")

# La versione iniziale assumeva fonti sempre presenti. Con file reali/ETL e'
# meglio rimuovere NaN e stringhe vuote prima di calcolare le zone Bradford.
data = data.dropna(subset=["SO"]).copy()
data = data[data["SO"].astype(str).str.strip() != ""]
if data.empty:
Expand Down Expand Up @@ -93,6 +125,8 @@ def get_bradford_law(df):
# Add the "Core Sources" area with the rectangle
fig.add_shape(
type="rect",
# Nelle collezioni piccole `a` puo' indicare oltre l'ultimo indice.
# Il `min(...)` evita IndexError mantenendo il core sull'ultima fonte valida.
x0=0,
x1=np.log(df_bradford["Rank"].iloc[min(a - 1, len(df_bradford) - 1)]),
y0=0,
Expand All @@ -105,6 +139,8 @@ def get_bradford_law(df):

# Add the "Core Sources" annotation with smaller font
fig.add_annotation(
# Stessa protezione dell'area: la versione fornita usava Rank[a] e
# poteva andare fuori indice quando il dataset aveva poche fonti.
x=np.log(df_bradford["Rank"].iloc[min(a - 1, len(df_bradford) - 1)]) / 2,
y=df_bradford["Freq"].max() * 0.85,
text="<b>Core<br>Sources</b>",
Expand Down
76 changes: 67 additions & 9 deletions functions/get_citeddocuments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@
import plotly.graph_objects as go


# Patch rispetto al file fornito:
# - import espliciti invece di `from www.services import *`;
# - `_resolve_dataframe` consente di usare la funzione sia in Biblioshiny sia
# direttamente sul DataFrame restituito dalla pipeline ETL;
# - rimossa la dipendenza implicita da `metaTagExtraction(df, "SR")`: la traccia
# richiede di verificare che la standardizzazione produca gia' colonne come
# `SR`, quindi qui validiamo quel contratto invece di ricostruirlo dentro.
def _resolve_dataframe(df):
"""Accept both a Shiny reactive value and a plain pandas DataFrame."""
"""
Risolve l'input dati accettando sia Biblioshiny sia test diretti.

Args:
df: Un `pd.DataFrame` gia' standardizzato oppure un oggetto reattivo
Shiny che espone il metodo `.get()`.

Returns:
pd.DataFrame: Copia del DataFrame da usare nei calcoli.

Raises:
ValueError: Se il valore reattivo non contiene dati.
TypeError: Se l'input risolto non e' un DataFrame pandas.
"""
if isinstance(df, pd.DataFrame):
data = df
elif hasattr(df, "get"):
Expand All @@ -15,35 +35,57 @@ def _resolve_dataframe(df):
raise ValueError("get_cited_documents requires a non-empty DataFrame.")
if not isinstance(data, pd.DataFrame):
raise TypeError("get_cited_documents expects a pandas DataFrame or an object with .get().")
# I calcoli aggiungono colonne temporanee (`TCperYear`, `NormalizedTC`), quindi
# la copia evita di sporcare il DataFrame usato dal resto della dashboard.
return data.copy()


def get_cited_documents(df, num_of_cited_docs, cited_docs_measure):
"""
Generate a plot and table of the most cited documents.

Individua e visualizza i documenti piu' citati globalmente.

Usa le colonne standardizzate `SR`, `DI`, `TC` e `PY` per costruire una
classifica dei documenti per citazioni totali o citazioni per anno. Calcola
anche `NormalizedTC`, cioe' le citazioni normalizzate rispetto alla media
degli articoli pubblicati nello stesso anno.

Args:
df: A DataFrame object containing the data.
num_of_cited_docs: The number of top cited documents to display.
cited_docs_measure: Ranking measure from the dashboard, either
"total_cit" or "total_cit_per_year".

df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che
contiene un DataFrame.
num_of_cited_docs (int): Numero massimo di documenti da mostrare nel
grafico.
cited_docs_measure (str): Metrica di ranking, `total_cit` oppure
`total_cit_per_year`.

Returns:
A Plotly figure object and a DataFrame of the most cited documents.
tuple: `(fig, table)`, dove `fig` e' un `go.FigureWidget` Plotly e
`table` contiene la classifica completa con DOI, citazioni totali,
citazioni annue e citazioni normalizzate.

Raises:
ValueError: Se il numero richiesto non e' positivo, la metrica non e'
valida, mancano colonne richieste o non ci sono documenti validi.
TypeError: Se l'input non puo' essere risolto in un DataFrame pandas.
"""
df = _resolve_dataframe(df)

num_of_cited_docs = int(num_of_cited_docs)
if num_of_cited_docs <= 0:
raise ValueError("num_of_cited_docs must be greater than zero.")
# La versione fornita trattava qualunque valore diverso da "total_cit" come
# citazioni per anno. Qui validiamo il parametro per intercettare errori UI.
if cited_docs_measure not in {"total_cit", "total_cit_per_year"}:
raise ValueError("cited_docs_measure must be 'total_cit' or 'total_cit_per_year'.")

# Queste colonne sono il contratto minimo tra standardizer e funzione:
# SR identifica il documento, DI il DOI, TC le citazioni, PY l'anno.
required_columns = {"SR", "DI", "TC", "PY"}
missing_columns = required_columns.difference(df.columns)
if missing_columns:
raise ValueError(f"Missing required columns: {', '.join(sorted(missing_columns))}.")

# I file importati possono portare anni/citazioni come stringhe; PubMed puo'
# non fornire citazioni reali, quindi i TC non numerici vengono portati a 0.
df["PY"] = pd.to_numeric(df["PY"], errors="coerce")
df["TC"] = pd.to_numeric(df["TC"], errors="coerce").fillna(0)
df = df.dropna(subset=["SR", "PY"]).copy()
Expand All @@ -58,8 +100,22 @@ def get_cited_documents(df, num_of_cited_docs, cited_docs_measure):

# Normalize within each publication year; years with zero mean citations stay at 0.
def normalize_year_citations(citations):
"""
Normalizza le citazioni rispetto alla media dell'anno di pubblicazione.

Args:
citations (pd.Series): Citazioni `TC` dei documenti dello stesso
anno di pubblicazione.

Returns:
pd.Series: Valori normalizzati; se la media e' 0 o NaN, restituisce
zeri per evitare divisioni non interpretabili.
"""
mean_citations = citations.mean()
if pd.isna(mean_citations) or mean_citations == 0:
# Se tutte le citazioni di un anno sono 0, la versione originale
# produceva divisione per zero/NaN. Restituire 0 mantiene la metrica
# interpretabile per collezioni senza citazioni, ad esempio PubMed.
return pd.Series(0.0, index=citations.index)
return (citations / mean_citations).round(2)

Expand Down Expand Up @@ -117,6 +173,8 @@ def normalize_year_citations(citations):
if pd.isna(max_metric) or max_metric <= 0
else 18 + 6 * (metric_values / max_metric)
)
# Se tutte le citazioni sono 0, dimensione marker e griglia devono comunque
# essere disegnabili: la versione fornita divideva direttamente per max().

# Add scatter markers and text
fig.add_trace(
Expand Down
Loading