From 4ad79a9e29f897d6a99f122e113b4f7cf575a9da Mon Sep 17 00:00:00 2001 From: Aniello Anzevino Date: Mon, 1 Jun 2026 18:48:17 +0200 Subject: [PATCH] commenti funzioni --- functions/get_annualproduction.py | 65 +++++++++++++++++++++++--- functions/get_averagecitations.py | 49 +++++++++++++++++--- functions/get_bradfordlaw.py | 48 ++++++++++++++++--- functions/get_citeddocuments.py | 76 +++++++++++++++++++++++++++---- functions/get_lotkalaw.py | 49 ++++++++++++++++++-- functions/get_relevantauthors.py | 69 ++++++++++++++++++++++++---- functions/get_relevantsources.py | 61 +++++++++++++++++++++---- 7 files changed, 368 insertions(+), 49 deletions(-) diff --git a/functions/get_annualproduction.py b/functions/get_annualproduction.py index 4b114b9f1..ec99d3116 100644 --- a/functions/get_annualproduction.py +++ b/functions/get_annualproduction.py @@ -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"): @@ -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.") @@ -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}) @@ -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) diff --git a/functions/get_averagecitations.py b/functions/get_averagecitations.py index f9e67bbee..ae5e2fe16 100644 --- a/functions/get_averagecitations.py +++ b/functions/get_averagecitations.py @@ -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"): @@ -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) @@ -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() diff --git a/functions/get_bradfordlaw.py b/functions/get_bradfordlaw.py index 5d27d8576..e96cbcccb 100644 --- a/functions/get_bradfordlaw.py +++ b/functions/get_bradfordlaw.py @@ -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"): @@ -16,18 +35,29 @@ 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) @@ -35,6 +65,8 @@ def get_bradford_law(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: @@ -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, @@ -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="Core
Sources
", diff --git a/functions/get_citeddocuments.py b/functions/get_citeddocuments.py index ecf789a44..fa1f8c665 100644 --- a/functions/get_citeddocuments.py +++ b/functions/get_citeddocuments.py @@ -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"): @@ -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() @@ -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) @@ -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( diff --git a/functions/get_lotkalaw.py b/functions/get_lotkalaw.py index f57581946..e1953d9ba 100644 --- a/functions/get_lotkalaw.py +++ b/functions/get_lotkalaw.py @@ -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` consente di usare la funzione sia in Biblioshiny sia +# nei controlli diretti sul DataFrame standardizzato dalla pipeline ETL; +# - aggiunti controlli su `AU`, perche' Lotka richiede una lista di autori per +# documento e la funzione originale assumeva che il formato fosse sempre valido. 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"): @@ -16,19 +35,32 @@ def _resolve_dataframe(df): raise ValueError("get_lotka_law requires a non-empty DataFrame.") if not isinstance(data, pd.DataFrame): raise TypeError("get_lotka_law expects a pandas DataFrame or an object with .get().") + # Lavoriamo su una copia per non normalizzare/filtrare `AU` nel DataFrame + # condiviso dalla dashboard. return data.copy() def get_lotka_law(df): """ - Calculates Lotka's Law for a given dataset and generates a line plot comparing observed and theoretical author productivity distributions. + Calcola la legge di Lotka sulla produttivita' degli autori. + + La funzione usa la colonna standardizzata `AU`, attesa come lista di autori + per documento. Conta quanti articoli ha scritto ogni autore, aggrega gli + autori per numero di articoli e confronta la distribuzione osservata con una + distribuzione teorica di Lotka. Args: - df (pd.DataFrame): Dataset containing at least the "AU" (authors) column as lists of author names. + df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che + contiene un DataFrame. Returns: - fig: Plotly figure showing the observed and theoretical Lotka's Law distributions. - author_prod (pd.DataFrame): Table summarizing the number of articles per author and their frequencies. + tuple: `(fig, author_prod)`, dove `fig` e' un `go.FigureWidget` Plotly e + `author_prod` contiene `N.Articles`, `N.Authors`, `Freq` e + `Theoretical`. + + Raises: + ValueError: Se manca `AU` o se non contiene autori validi. + TypeError: Se l'input non puo' essere risolto in un DataFrame pandas. """ # Calculate Lotka's Law @@ -37,6 +69,10 @@ def get_lotka_law(df): if "AU" not in data.columns: raise ValueError("Missing required column: AU.") + # La versione fornita faceva direttamente: + # for sublist in data["AU"] for author in sublist + # Questo funziona solo se ogni cella e' gia' una lista. Dopo ETL reali e' + # possibile trovare NaN/stringhe: vengono filtrati per evitare risultati falsi. data = data.dropna(subset=["AU"]).copy() data["AU"] = data["AU"].apply(lambda x: x if isinstance(x, list) else []) data = data[data["AU"].apply(len) > 0].copy() @@ -59,6 +95,9 @@ def get_lotka_law(df): author_prod['Theoretical'] = 10**(lotka_law[1] - 2 * np.log10(author_prod['N.Articles'])) author_prod['Theoretical'] = author_prod['Theoretical'] / author_prod['Theoretical'].sum() else: + # `np.polyfit` richiede almeno due punti. Con dataset piccoli o molto + # uniformi esiste un solo livello di produttivita', quindi usiamo 1.0 + # invece di far fallire il plot. author_prod['Theoretical'] = 1.0 # Create the plot with improved hover diff --git a/functions/get_relevantauthors.py b/functions/get_relevantauthors.py index 1806895bb..004b6fd89 100644 --- a/functions/get_relevantauthors.py +++ b/functions/get_relevantauthors.py @@ -2,6 +2,13 @@ import plotly.graph_objects as go +# Patch rispetto al file fornito: +# - import espliciti invece di `from www.services import *`; +# - aggiunto `_resolve_dataframe`, per usare la funzione sia con reactive.Value +# di Biblioshiny sia con DataFrame pandas prodotti direttamente dalla ETL; +# - aggiunta normalizzazione dei nomi delle metriche, perche' la dashboard passa +# valori tecnici (`n_docs`, `percentage`, `freq_measure`) mentre la tabella usa +# etichette leggibili. FREQUENCY_LABELS = { "n_docs": "N. of Documents", "percentage": "Percentage", @@ -10,7 +17,20 @@ 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"): @@ -22,24 +42,50 @@ def _resolve_dataframe(df): raise ValueError("get_relevant_authors requires a non-empty DataFrame.") if not isinstance(data, pd.DataFrame): raise TypeError("get_relevant_authors expects a pandas DataFrame or an object with .get().") + # Le trasformazioni successive toccano `AU`; la copia evita side effect sul + # DataFrame globale della dashboard. return data.copy() def _normalize_frequency(frequency): + """ + Converte il valore tecnico della dashboard nel nome colonna leggibile. + + Args: + frequency (str): Valore selezionato nella UI o nome gia' leggibile. + + Returns: + str: Etichetta da usare come colonna e titolo dell'asse. + """ + # La versione fornita confrontava direttamente stringhe come "percentage" e + # usava poi quella stessa stringa come nome colonna. Questo rendeva output e + # grafico meno coerenti con la dashboard/table. return FREQUENCY_LABELS.get(frequency, frequency) def get_relevant_authors(df, num_of_authors, frequency="N. of Documents"): """ - Generate a plot and table of the most relevant authors with frequency options. - + Individua e visualizza gli autori piu' rilevanti della collezione. + + Usa la colonna standardizzata `AU`, attesa come lista di autori per + documento. La metrica puo' essere numero di documenti, percentuale o + frequenza frazionata. + Args: - df: A DataFrame object containing the data. - num_of_authors: The number of top authors to display. - frequency: Type of frequency calculation. Options: "N. of Documents", "Percentage", "Fractionalized". - + df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che + contiene un DataFrame. + num_of_authors (int): Numero massimo di autori da mostrare nel grafico. + frequency (str): Metrica da usare. Accetta valori UI come `n_docs`, + `percentage`, `freq_measure` oppure etichette leggibili. + Returns: - A Plotly figure object and a DataFrame of the most relevant authors. + tuple: `(fig, table_relevant_authors)`, dove `fig` e' un + `go.FigureWidget` Plotly e la tabella contiene gli autori ordinati per + la metrica selezionata. + + Raises: + ValueError: Se manca `AU` o se non contiene liste di autori valide. + TypeError: Se l'input non puo' essere risolto in un DataFrame pandas. """ data = _resolve_dataframe(df) frequency = _normalize_frequency(frequency) @@ -47,7 +93,9 @@ def get_relevant_authors(df, num_of_authors, frequency="N. of Documents"): if "AU" not in data.columns: raise ValueError("Missing required column: AU.") - # Drop rows with missing values + # `AU` deve essere una lista di autori prodotta dallo standardizer. I valori + # non-lista vengono ignorati per evitare di iterare stringhe carattere per + # carattere o di rompere il calcolo fractional. data = data.dropna(subset=["AU"]).copy() # Ensure all values in the "AU" column are lists @@ -66,6 +114,9 @@ def get_relevant_authors(df, num_of_authors, frequency="N. of Documents"): elif frequency == "Fractionalized Frequency": # Calculate fractional counts fractional_counts = data["AU"].apply(lambda authors: 1 / len(authors) if authors else 0) + # Dopo i filtri l'indice del DataFrame puo' non essere 0..n. La versione + # fornita usava `fractional_counts[i]` con enumerate e poteva associare + # pesi errati o generare KeyError. `zip` mantiene allineate righe e pesi. fractional_authors = [ (author, weight) for authors, weight in zip(data["AU"], fractional_counts) diff --git a/functions/get_relevantsources.py b/functions/get_relevantsources.py index c1ba26f1d..8c1d2199f 100644 --- a/functions/get_relevantsources.py +++ b/functions/get_relevantsources.py @@ -2,8 +2,28 @@ import plotly.graph_objects as go +# Patch rispetto al file fornito: +# - import espliciti al posto di `from www.services import *`, per rendere chiaro +# che la funzione dipende solo da pandas/plotly; +# - `_resolve_dataframe` permette di usare sia `df.get()` di Biblioshiny sia un +# DataFrame pandas ottenuto direttamente dalla pipeline ETL; +# - sono stati aggiunti controlli sulla colonna `SO`, perche' "Relevant Sources" +# ha senso solo se la standardizzazione ha prodotto i nomi delle riviste/fonti. 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"): @@ -15,26 +35,39 @@ def _resolve_dataframe(df): raise ValueError("get_relevant_sources requires a non-empty DataFrame.") if not isinstance(data, pd.DataFrame): raise TypeError("get_relevant_sources expects a pandas DataFrame or an object with .get().") + # La copia protegge il DataFrame caricato in app da eventuali filtri locali. return data.copy() def get_relevant_sources(df, num_of_sources): """ - Generate a plot and table of the most relevant sources. - + Individua e visualizza le fonti piu' rilevanti della collezione. + + Conta i documenti per valore della colonna standardizzata `SO` (source, + journal o venue) e restituisce sia la tabella completa sia il grafico delle + prime `num_of_sources` fonti. + Args: - df: A DataFrame object containing the data. - num_of_sources: The number of top sources to display. - + df: `pd.DataFrame` standardizzato oppure reactive.Value di Shiny che + contiene un DataFrame. + num_of_sources (int): Numero massimo di fonti da mostrare nel grafico. + Returns: - A Plotly figure object and a DataFrame of the most relevant sources. + tuple: `(fig, table_relevant_sources)`, dove `fig` e' un + `go.FigureWidget` Plotly e `table_relevant_sources` contiene tutte le + fonti ordinate per `N. of Documents`. + + Raises: + ValueError: Se manca `SO` o se non contiene nomi di fonti validi. + TypeError: Se l'input non puo' essere risolto in un DataFrame pandas. """ data = _resolve_dataframe(df) if "SO" not in data.columns: raise ValueError("Missing required column: SO.") - # Drop rows with missing values + # La versione fornita eliminava solo i NaN. Qui scartiamo anche stringhe + # vuote/spazi: altrimenti una fonte vuota puo' comparire tra le piu' rilevanti. data = data.dropna(subset=["SO"]) data = data[data["SO"].astype(str).str.strip() != ""] if data.empty: @@ -50,10 +83,22 @@ def get_relevant_sources(df, num_of_sources): # Limit the number of sources to display if num_of_sources > len(source_counts): num_of_sources = len(source_counts) + # `.copy()` e' necessario perche' `head()` restituisce una slice: subito dopo + # aggiungiamo `Sources_wrapped` e vogliamo evitare SettingWithCopyWarning/errori. source_counts = source_counts.head(num_of_sources).copy() # Truncate long source names and add line breaks every 50 characters def wrap_label(label, width=50): + """ + Spezza una label lunga in piu' righe HTML per renderla leggibile. + + Args: + label (str): Testo della fonte da visualizzare sull'asse. + width (int): Numero massimo di caratteri per riga. + + Returns: + str: Label con tag `
` inseriti a intervalli regolari. + """ return '
'.join([label[i:i+width] for i in range(0, len(label), width)]) source_counts["Sources_wrapped"] = source_counts["Sources"].apply(wrap_label)