Introduzione¶
Questo progetto consiste nella creazione di un sistema di gestione elementi (SGE) applicato al settore vinicolo, sviluppato utilizzando il linguaggio di programmazione Python.
Ho sviluppato un programma che consente di esplorare e analizzare un database di vini attraverso diverse funzionalità di ricerca e visualizzazione. Nel database, ogni vino è caratterizzato da attributi specifici: nome, produttore, paese di origine ed etichetta, con particolare enfasi sulle etichette vegan friendly/not vegan friendly.
Ho utilizzato un database ottenuto dallo scraping del sito del progetto Barnivore, considerando esclusivamente i dati relativi ai vini. Nello specifico ho utilizzato questo questo database disponibile su Kaggle. L'ultimo aggiornamento risale al 2020, ma mi è sembrato ottimo per lo scopo che mi ero prefissato.
Ho scelto di focalizzare l'attenzione proprio sui vini vegan friendly poichè, anche nel settore vinicolo, vi è una sempre crescente attenzione verso i processi produttivi e le scelte etiche.
Questo aspetto, apparentemente contro-intuitivo per un prodotto derivato dall'uva, è strettamente legato alle tecnologie di produzione vinicola, in particolare ai processi di chiarificazione e stabilizzazione del vino. Mi scuso se mi dilungherò su questo aspetto, ma, essendo laureato in scienze e tecnologie alimentari e non avendo propriamente il dono della sintesi, ritengo importante comprendere i motivi per cui, dal punto di vista tecnologico-produttivo, anche un prodotto di trasformazione apparentemente semplice come il vino possa essere sottoposto a una serie di lavorazioni che ne possono determinare una specifica classificazione.
Durante il processo di vinificazione, infatti, è necessario rimuovere le particelle in sospensione per garantire la stabilità e la limpidezza del prodotto finale. Questo processo, chiamato chiarificazione, tradizionalmente utilizza agenti di origine animale:
albumina, derivata dalle uova, è utilizzata principalmente per ammorbidire i tannini nei vini rossi, eliminando la tipica astringenza e il gusto amaro;
caseina, proteina del latte, è impiegata per rimuovere composti fenolici ossidati nei vini bianchi;
colla di pesce, ottenuta in larga parte dalla cotenna del maiale e da ossa e cartilagini anche bovine, è particolarmente efficace per la chiarificazione dei vini bianchi e rosati;
gelatina animale, derivata da ossa e tessuti animali, è usata per rimuovere tannini in eccesso.
Questi agenti chiarificanti, pur non rimanendo nel prodotto finale, rendono il vino non compatibile con una dieta vegana per questioni etiche legate al loro utilizzo nel processo produttivo. Tuttavia, l'industria vinicola ha sviluppato alternative vegetali egualmente efficaci:
bentonite, un minerale argilloso, eccellente per la stabilizzazione proteica;
proteine vegetali, principalmente derivate dai piselli, efficaci quanto l'albumina nella gestione dei tannini e quindi nella riduzione dell'astringenza e del gusto amaro;
gel di silice, particolarmente utile per la chiarificazione dei vini bianchi e per evitare intorbidamenti;
caolino, un minerale silicatico delle argille che favorisce la rimozione delle proteine instabili.
L'implementazione soddisfa i requisiti base di un SGE offrendo:
una visualizzazione completa degli elementi del database;
diverse modalità di ricerca basate su attributi singoli (nome, produttore, paese);
funzionalità di analisi e statistiche più dettagliate
Le funzionalità sono state poi estese implementando:
un sistema di statistiche approfondite sulla distribuzione dei vini vegani per paese;
un meccanismo di filtraggio avanzato che permette ricerche incrociate combinando più criteri (nome, paese, vegan frinendly/not vegan friendly);
analisi specifiche sui produttori che si dedicano esclusivamente alla produzione di vini vegan friendly.
Nelle sezioni successive verranno presentate nel dettaglio le funzionalità del programma e la loro implementazione tecnica.
1. Importazione¶
Con import os
importo il modulo os, che permette di interagire con file e directory del sistema operativo (SO).
from pathlib import Path
importa la classe Path
, che permette di gestire in modo orientato agli oggetti file e directory del SO.
Con from tkinter import filedialog
importo il modulo filedialog
dalla libreria tkinter
. Ho utilizzato questo modulo per creare una finestra di dialogo grafica che consentirà all'utente di selezionare il file CSV contenente tutti i dati.
Con import tkinter as tk
importo la libreria tkinter
, il toolkit GUI integrato in Python. Importandola come tk
, il codice può utilizzare questo alias più breve per far riferimento agli elementi ad essa collegata. In questo modo il codice sarà leggermente più conciso.
import os
from pathlib import Path
from tkinter import filedialog
import tkinter as tk
2. Funzione di selezione file¶
Con def seleziona_file()
definisco la funzione seleziona_file
.
Il codice tenta anzitutto di capire se è in esecuzione su Google Colab (che ho utilizzato per scrivere ed eseguire il codice) usando in_colab = 'google.colab' in str(get_ipython())
.
Se il codice viene eseguito in Colab importa il modulo files
da google.colab
e, successivamente, mostra un messaggio all'utente, utilizzando files.upload()
per aprire il widget di caricamento file di Colab.
Infine, viene restituito il nome del file caricato.
In caso contrario è utilizzato tkinter
per mostrare all'utente una finestra di dialogo per selezionare il file CSV dal proprio dispositivo.
filedialog.askopenfilename()
viene usato per permettere all'utente di scegliere un file.
Infine, viene restituito il percorso del file selezionato se ne viene scelto uno, altrimenti viene stampato un messaggio e restituisce None
.
Il blocco
try
...
except Exception as e:
print(f"Errore durante la selezione del file: {e}")
return None
serve per gestire eventuali errori.
Se si verifica un errore durante il processo di selezione del file, è stampato un messaggio di errore che restituisce None
.
def seleziona_file():
try:
in_colab = 'google.colab' in str(get_ipython())
if in_colab:
from google.colab import files
print("Ambiente Google Colab rilevato. Caricamento file tramite uploader...")
uploaded = files.upload()
return list(uploaded.keys())[0]
else:
root = tk.Tk()
root.withdraw()
print("Seleziona il file CSV tramite la finestra di dialogo...")
file_path = filedialog.askopenfilename(
title='Seleziona il file CSV',
filetypes=[('CSV Files', '*.csv'), ('All Files', '*.*')]
)
if file_path:
return file_path
else:
print("Nessun file selezionato.")
return None
except Exception as e:
print(f"Errore durante la selezione del file: {e}")
return None
3. Classe SistemaGestioneVini¶
Ho iniziato progettando questa classe, il cui scopo è quello di contenere e manipolare i dati presenti nel dataset barnivore_new.csv.
__init__(self, nome_file)
Questo è il costruttore della classe, con il quale viene creata un'istanza di SistemaGestioneVini
.
L'input è nome_file
, che deve essere il percorso del file CSV contenente tutti i dati relativi ai vini.
Se nome_file
non viene fornito si crea un ValueError
.
Il costruttore ha anche lo scopo di inizializzare due importanti attributi, ovvero self.vini
e self.paesi_validi
.
Il primo è una lista vuota che memorizzerà come dizionari i dati sui vini.
Il secondo, invece, è un insieme che contiene i paesi di origine validi per i vini (in base chiaramente aile nazioni presenti nel dataset).
La decisione di convertire tutti i paesi in lowercase nel set paesi_validi
è stata dettata dalla necessità di standardizzare i dati e previenire errori di case-sensitivity nelle ricerche successive.
Il seguente blocco
try:
with open(nome_file, 'r', encoding='utf-8') as file:
intestazione = file.readline().strip().split(',')
for linea in file:
valori = linea.strip().split(',')
if len(valori) >= 4: # Verifica che ci siano abbastanza valori
vino = {
'name': valori[0],
'producer': valori[1],
'origin': valori[2],
'label': valori[3]
}
self.vini.append(vino)
except FileNotFoundError:
print(f"Errore: il file {nome_file} non è stato trovato.")
raise
except Exception as e:
print(f"Errore durante la lettura del file: {e}")
raise
print(f"Caricati {len(self.vini)} vini dal database.")
viene utilizzato dal costruttore per gestire eventuali errori durante la lettura del file CSV. La scelta di utilizzare un blocco try-catch è fondamentale perché la lettura di file esterni può facilmente fallire. Il codice, infatti, gestisce separatamente l'errore di file non trovato (FileNotFoundError) da altri possibili errori, permettendo una diagnosi più precisa dei problemi.
L'encoding='utf-8'
gestisce diversi set di caratteri.
La prima riga del file CSV viene letta usando file.readline()
e viene quindi divisa usando ,
come delimitatore. Viene eseguito un controlo per verificare se la lunghezza di valori
sia maggiore o uguale a 4. Ciò garantisce che ogni riga abbia i campi minimi richiesti, ovvero nome, produttore, origine ed etichetta.
Se il controllo viene superato, crea un dizionario chiamato vino
con le chiavi: 'name'
, 'producer'
, 'origin'
e 'label'
, prendendo i valori dagli elementi corrispondenti nella lista valori
.
Questo dizionario vino
viene quindi aggiunto alla lista self.vini
.
cerca_per_nome(self, nome_vino)
Questo metodo cerca i vini in base al loro nome o tipologia.
La funzione cerca_per_nome
implementa un sistema di deduplicazione usando un set (vini_visti
), che consente di evitare la restituzione di risultati ridondanti.
La funzione inizializza una lista vuota risultati
che memorizzerà i vini trovati. Converte poi l'input nome_vino
in minuscolo affinchè la ricerca sia case-insensitive.
Inizializza poi un insieme vuoto chiamato vini_visti
. Esso è utilizzato per tenere traccia dei nomi dei vini che sono già stati aggiunti ai risultati per evitare, appunto,risultati ridondanti.
Quindi, scorre ogni vino nella lista self.vini
. All'interno del ciclo, controlla se nome_vino
è presente nella versione minuscola del nome del vino utilizzando l'operatore in
. Controlla anche se il nome del vino non è già presente in vini_visti
.
Se entrambe le condizioni sono soddisfatte e sono quindi True
, aggiunge il nome del vino a vini_visti
e aggiunge il dizionario vino
alla lista risultati
.
Infine, restituisce la lista risultati
con i risultati della ricerca.
ricerca_incrociata(self, nome_vino=None, stato=None, solo_vegani=False)
Questo metodo esegue una ricerca incrociata, consentendo di combinare più criteri.
L'uso di parametri opzionali (con default None
e False
) permette di chiamare la funzione con qualsiasi combinazione di criteri. Inoltre, una struttura con flag booleani (nome_match
, stato_match
, vegan_match
) rende il codice facilmente estensibile se si volessero aggiungere nuovi criteri di ricerca.
Nello specifico, inizializza una lista vuota risultati
per memorizzare i vini corrispondenti.
Per ogni vino, inizializza tre variabili booleane: nome_match
, stato_match
e vegan_match
a True
. Queste variabili servono a verificare se il vino corrisponde ai criteri di ricerca. Ad esempio, se viene fornito nome_vino
, controlla se è presente nel nome del vino. La variabile nome_match
è impostata su True
se nome_vino
è presente in vino['name']
, altrimenti su False
.
Analogamente, se viene fornito stato
, controlla se corrisponde all'origine del vino. La variabile stato_match
è quindi impostata su True
se stato
corrisponde a vino['origin']
, altrimenti va su False
.
Se solo_vegani
è impostato su True
, controlla se il vino è vegano usando il metodo self.is_vegan
.
Il codice aggiunge vino
a risultati
se tutte e tre le variabili di corrispondenza sono True
.
Quindi crea una stringa titolo
per descrivere i risultati della ricerca, aggiungendo condizioni per ogni filtro.
Infine, chiama il metodo self.visualizza_risultati_completi()
per visualizzare i risultati della ricerca con il titolo generato e restituisce la lista risultati
.
cerca_per_produttore(self, nome_produttore)
Questo metodo cerca i vini in base al nome del produttore e mantiene una struttura lineare, senza deduplicazione, riflettendo la naturale presenza di più vini per singolo produttore nel database.
Inizializza una lista vuota risultati
, convertendo poi l'input nome_produttore
in minuscolo e scorrendo ogni vino
in self.vini
. Controlla poi se nome_produttore
è presente in versione minuscola usando l'operatore in
. Se la condizione è True
, allora l'attuale dizionario vino viene aggiunto alla lista risultati
.
Infine, chiama il metodo self.visualizza_risultati_completi
per visualizzare i risultati e restituisce la lista risultati
.
is_vegan(self, etichetta)
Questo metodo controlla se un vino è vegano in base alla sua etichetta.
Anzitutto l'input etichetta
viene convertito in minuscolo.
Viene poi verificato se il termine 'not vegan'
è presente in etichetta. Se lo è, restituisce un valore False
.
In caso contrario, se presente il termine 'vegan friendly'
nell'etichetta, restituisce True
.
Quindi, prima viene controllato se present 'not vegan'
, poi 'vegan friendly'
. Questa sequenza previene falsi positivi in caso di etichette contenenti entrambe le diciture.
visualizza_risultati_completi(self, risultati, titolo="Risultati della ricerca")
Questo metodo visualizza i risultati della ricerca in modo formattato. La formattazione fissa dei campi (esempio: vino['name'][:25]:25
) garantisce una presentazione ordinata indipendentemente dalla lunghezza dei dati.
Come input accetta la lista risultati
e una stringa titolo
opzionale.
Stampa quindi il titolo e il numero di vini trovati e, se ci sono risultati, stampa una riga di intestazione per la tabella.
Quindi, scorre ogni vino nella lista risultati
, stampando nome, produttore, origine ed etichetta di ogni vino.
cerca_per_stato(self, stato, solo_vegani=False)
Questo metodo integra la validazione contro paesi_validi
e include il parametro opzionale solo_vegani
, unificando due funzionalità correlate in un'unica interfaccia.
Converte l'input stato in minuscolo e lo memorizza in stato_lower
. Come input, oltre a stato
, accetta anche il flag booleano solo_vegani
, con valore predefinito False
.
Controlla se stato_lower
è presente nell'insieme dei paesi validi (self.paesi_validi
). Se lo stato non è valido, stampa un messaggio di errore e restituisce una lista vuota.
Viene poi inizializzata una lista vuota risultati per memorizzare i vini corrispondenti.
Ogni vino presente in self.vini
viene analizzato.
Anzitutto, controlla se stato_lower
è uguale alla versione minuscola relativa all'origine del vino (vino['origin'].lower()
).
Se solo_vegani
è True
, viene eseguito un ulteriore controllo per verificare se il vino in questione è vegan friendly utilizzando self.isvegan()
. Se entrambe le condizioni sono soddisfatte, il vino viene aggiunto a risultati
.
Se, invece, solo_vegani
è False
, tutti i vini dello stato preso in considerazione sono aggiunti a risultati, indipendentemente dal fatto che siano vegani o meno.
Infine, viene richiamato self.visualizza_risultati_completi()
per visualizzare i risultati della ricerca, aggiungendo un titolo che specifica anche se l'utente ha filtrato per vini vegani o meno.
Viene quindi restituita la lista risultati
.
cerca_produttori_solo_vegani(self)
L'obiettivo principale di questa funzione è identificare quei produttori che si dedicano esclusivamente alla produzione di vini vegani.
Anzitutto, all'inizio della funzione viene importato il modulo unicodedata
e definita una funzione interna normalize_name()
. Questo permette di normalizzare i nomi dei produttori: rimuove gli accenti utilizzando una normalizzazione Unicode (NFKD, nello specifico) che separa i caratteri accentati in carattere base più accento e standardizza gli apostrofi.
Ciò garantisce un ordinamento alfabetico corretto, specialmente con nomi che contengono caratteri speciali o accenti.
Ho strutturato l'intera funzione attorno a un dizionario principale chiamato produttori_vegani
. Il dizionario interno contiene tre elementi: un flag booleano tutti_vegani
(inizialmente True
), una lista vini per memorizzare tutti i vini del produttore, e origin
per memorizzare il paese di origine.
Successivamente la funzione itera attraverso tutti i vini nel database (self.vini
).
Per ogni vino, viene estratto il produttore e, se è la prima volta che lo incontriamo, viene inizializzata la sua voce nel dizionario produttori_vegani
.
Ogni vino viene aggiunto alla lista dei vini del rispettivo produttore. Qui avviene anche il controllo: se un vino non è vegano (verificato attraverso il metodo is_vegan()
), il flag tutti_vegani
del produttore viene impostato su False. Questo flag, una volta impostato in tal modo, non potrà più tornare True
, garantendo quindi la corretta identificazione dei produttori che si dedicano ad una produzione esclusivamente vegan friendly.
Viene poi creata la lista produttori
che conterrà solo i produttori che soddisfano i criteri.
Il ciclo itera attraverso le voci di produttori_vegani
usando il metodo items()
.
Un produttore viene incluso nella lista finale solo se soddisfa due condizioni: deve avere il flag tutti_vegani
impostato su True
(indicando che non produce vini non vegani) e deve avere almeno due vini (len(info['vini']) >= 2
).
Per ogni produttore che soddisfa questi criteri, viene creato un nuovo dizionario contenente il nome, l'origine e il numero totale di vini prodotti.
L'ordinamento della lista viene effettuato usando il metodo sort()
con una funzione lambda come chiave. La lambda lambda x: normalize_name(x['name']
) applica la normalizzazione a ogni nome prima del confronto, garantendo un ordinamento alfabetico corretto indipendentemente da accenti e caratteri speciali.
Infine, la funzione si occupa della visualizzazione dei risultati in un formato tabellare chiaro. Utilizza l'operatore di formattazione delle stringhe (f-strings
) per creare una tabella con colonne allineate.
La notazione [:n] indica proprio lo slicing delle stringhe in Python, che estrae i primi n caratteri della stringa. Questo previene che nomi troppo lunghi disturbino l'allineamento della tabella. Nello specifico:
prod['nome_produttore'][:30]
tronca il nome del produttore a 30 caratteri se più lungo;prod['nazione'][:15]
tronca il nome della nazione a 15 caratteri;prod['numero_vini']
viene formattato in uno spazio di 12 caratteri.
La funzione termina restituendo la lista produttori
.
analisi_regioni(self)
Questo metodo analizza la distribuzione dei vini vegani per paese.
Per implementare questa funzione, ho creato una struttura dati principale chiamata statistiche_regioni
. È un dizionario dove ogni chiave rappresenta un paese e il valore associato è a sua volta un dizionario contenente tutte le statistiche relative a quel paese.
La funzione inizia analizzando ogni vino nel database. Per ciascun vino, converte il paese di origine in lowercase.
Verifica poi se il paese è presente nel set self.paesi_validi
. Se incontra un nuovo paese valido, inizializza una nuova voce nel dizionario statistiche_regioni con i contatori a zero e le liste vuote.
Per ogni vino analizzato, incrementa il contatore totale del paese e, se il vino è vegano (verificato attraverso il metodo is_vegan()
), incrementa anche il contatore dei vini vegani e lo aggiunge alla lista appropriata.
Nello specifico, se il vino è vegano incrementa il conteggio vegani e aggiunge il vino alla lista vini_vegani
per quel paese.
Altrimenti, aggiunge il vino alla lista vini_non_vegani
per quel paese.
Ho poi utilizzato una funzione nidificata percentuale_vegani(item)
per ordinare i paesi in base alla percentuale di vini vegani.
Questa funzione calcola la percentuale di vini vegani per ogni paese, ma solo se il paese ha almeno 5 vini nel database - una soglia che ho scelto per evitare che percentuali basate su campioni troppo piccoli distorcessero l'analisi.
Se un paese ha meno di 5 vini, gli viene assegnato un valore di -1, posizionandolo effettivamente in fondo alla lista ordinata.
I paesi vengono ordinati in base alla percentuale di vini vegan friendly richiamando sorted()
e usando la funzione percentuale_vegani
come chiave. I paesi sono ordinati in ordine decrescente e il risultato viene salvato in regioni_ordinate
.
La visualizzazione dei risultati è strutturata in modo da fornire una panoramica immediata. Per ogni paese vengono mostrati il numero totale di vini, il numero di vini vegani, la percentuale di vini vegani e i principali produttori di vini vegani. La parte più interessante della funzione è il menu di analisi, che offre all'utente quattro opzioni di esplorazione dei dati:
- visualizzare i dettagli completi di un paese specifico, vedendo tutti i suoi vini vegani e non vegani;
- visualizzare solo i paesi con un'alta percentuale (>70%) di vini vegani;
- inserire una soglia personalizzata per vedere i paesi con una percentuale di vini vegani inferiore a tale soglia;
- tornare al menu principale.
Per la visualizzazione dei paesi con alta percentuale di vini vegani e per la ricerca con soglia personalizzata, la funzione riutilizza i dati già ordinati in regioni_ordinate
, evitando così calcoli ridondanti.
Nel caso della soglia personalizzata, ho implementato anche una gestione degli errori per assicurarmi che l'input dell'utente sia un numero valido.
La classe SistemaGestioneVini
rappresenta quindi un sistema abbastanza completo per la gestione e l'analisi di un database di vini, con particolare focus sui vini vegan friendly.
Attraverso il suo costruttore, legge e organizza i dati da un file CSV e offre una serie di metodi per la ricerca e l'analisi dei vini: ricerca per nome, per produttore, per stato, ricerca incrociata e analisi dettagliata delle regioni.
La classe è stata progettata prestando particolare attenzione all'efficienza delle ricerche, alla gestione delle duplicazioni e alla standardizzazione dei dati, come, ad esempio, la conversione dei nomi dei paesi in lowercase.
class SistemaGestioneVini:
def __init__(self, nome_file):
"""Inizializza il sistema caricando i dati dal file CSV."""
if nome_file is None:
raise ValueError("Nessun file valido fornito")
self.vini = []
self.paesi_validi = {
'usa', 'australia', 'france', 'italy', 'canada', 'spain', 'england',
'portugal', 'new zealand', 'south africa', 'chile', 'argentina',
'israel', 'germany', 'austria', 'malta', 'luxembourg', 'greece',
'slovenia', 'wales', 'the netherlands', 'belgium', 'scotland', 'poland'
}
try:
with open(nome_file, 'r', encoding='utf-8') as file:
intestazione = file.readline().strip().split(',')
for linea in file:
valori = linea.strip().split(',')
if len(valori) >= 4:
vino = {
'name': valori[0],
'producer': valori[1],
'origin': valori[2],
'label': valori[3]
}
self.vini.append(vino)
except FileNotFoundError:
print(f"Errore: il file {nome_file} non è stato trovato.")
raise
except Exception as e:
print(f"Errore durante la lettura del file: {e}")
raise
print(f"Caricati {len(self.vini)} vini dal database.")
def cerca_per_nome(self, nome_vino):
"""Cerca vini in base al nome o alla tipologia."""
risultati = []
nome_vino = nome_vino.lower()
vini_visti = set()
for vino in self.vini:
if nome_vino in vino['name'].lower() and vino['name'] not in vini_visti:
vini_visti.add(vino['name'])
risultati.append(vino)
return risultati
def ricerca_incrociata(self, nome_vino=None, stato=None, solo_vegani=False):
"""
Ricerca incrociata che permette di combinare più criteri:
- nome del vino (opzionale)
- stato di origine (opzionale)
- filtro per vini vegani (opzionale)
"""
risultati = []
for vino in self.vini:
nome_match = True
stato_match = True
vegan_match = True
if nome_vino:
nome_match = nome_vino.lower() in vino['name'].lower()
if stato:
stato_match = vino['origin'].lower() == stato.lower()
if solo_vegani:
vegan_match = self.is_vegan(vino['label'])
if nome_match and stato_match and vegan_match:
risultati.append(vino)
titolo = f"Risultati ricerca incrociata"
if nome_vino:
titolo += f" per vini contenenti '{nome_vino}'"
if stato:
titolo += f" in {stato}"
if solo_vegani:
titolo += " (solo vegani)"
self.visualizza_risultati_completi(risultati, titolo)
return risultati
def cerca_per_produttore(self, nome_produttore):
"""Cerca vini in base al nome del produttore."""
risultati = []
nome_produttore = nome_produttore.lower()
for vino in self.vini:
if nome_produttore in vino['producer'].lower():
risultati.append(vino)
self.visualizza_risultati_completi(risultati,
f"Vini prodotti da aziende che contengono '{nome_produttore}' nel nome")
return risultati
def is_vegan(self, etichetta):
"""Verifica se un vino è vegano basandosi sull'etichetta."""
etichetta = etichetta.lower()
if 'not vegan' in etichetta:
return False
return 'vegan friendly' in etichetta
def visualizza_risultati_completi(self, risultati, titolo="Risultati della ricerca"):
"""Visualizza tutti i risultati della ricerca in modo formattato."""
print(f"\n{titolo}:")
print(f"Trovati {len(risultati)} vini")
if len(risultati) > 0:
print("\nNome | Produttore | Origine | Etichetta")
print("-" * 80)
for vino in risultati:
print(f"{vino['name'][:25]:25} | {vino['producer'][:20]:20} | "
f"{vino['origin'][:15]:15} | {vino['label']}")
def cerca_per_stato(self, stato, solo_vegani=False):
"""Cerca vini per stato, con opzione per filtrare solo quelli vegani."""
stato_lower = stato.lower()
if stato_lower not in self.paesi_validi:
print(f"Errore: {stato} non è un paese valido nel database.")
return []
risultati = []
for vino in self.vini:
if stato_lower == vino['origin'].lower():
if solo_vegani:
if self.is_vegan(vino['label']):
risultati.append(vino)
else:
risultati.append(vino)
self.visualizza_risultati_completi(risultati,
f"Vini {'vegani ' if solo_vegani else ''}da {stato}")
return risultati
def cerca_produttori_solo_vegani(self):
"""Trova produttori che producono esclusivamente vini vegani."""
import unicodedata
def normalize_name(name):
"""Normalizza il nome rimuovendo accenti e standardizzando l'apostrofo."""
normalized = ''.join(
c for c in unicodedata.normalize('NFKD', name)
if not unicodedata.combining(c)
)
normalized = normalized.replace("'", "'").replace("`", "'")
return normalized.lower()
produttori_vegani = {}
for vino in self.vini:
produttore = vino['producer']
if produttore not in produttori_vegani:
produttori_vegani[produttore] = {
'tutti_vegani': True,
'vini': [],
'nazione': vino['origin']
}
produttori_vegani[produttore]['vini'].append(vino)
if not self.is_vegan(vino['label']):
produttori_vegani[produttore]['tutti_vegani'] = False
produttori = []
for produttore, info in produttori_vegani.items():
if info['tutti_vegani'] and len(info['vini']) >= 2:
produttori.append({
'nome_produttore': produttore,
'nazione': info['nazione'],
'numero_vini': len(info['vini'])
})
produttori.sort(key=lambda x: normalize_name(x['nome_produttore']))
print("\nProduttori che producono solo vini vegani:")
print("-" * 80)
print(f"{'Nome produttore':30} | {'Nazione':15} | {'Numero vini':12}")
print("-" * 80)
for prod in produttori:
print(f"{prod['nome_produttore'][:30]:30} | {prod['nazione'][:15]:15} | {prod['numero_vini']:12}")
return produttori
def analisi_regioni(self):
"""Analizza in dettaglio la distribuzione di vini vegani per paese."""
statistiche_regioni = {}
for vino in self.vini:
origine = vino['origin'].lower()
if origine in self.paesi_validi:
if origine not in statistiche_regioni:
statistiche_regioni[origine] = {
'totale': 0,
'vegani': 0,
'vini_vegani': [],
'vini_non_vegani': []
}
statistiche_regioni[origine]['totale'] += 1
if self.is_vegan(vino['label']):
statistiche_regioni[origine]['vegani'] += 1
statistiche_regioni[origine]['vini_vegani'].append(vino)
else:
statistiche_regioni[origine]['vini_non_vegani'].append(vino)
def percentuale_vegani(item):
stats = item[1]
return (stats['vegani'] / stats['totale']) * 100 if stats['totale'] >= 5 else -1
regioni_ordinate = sorted(
statistiche_regioni.items(),
key=percentuale_vegani,
reverse=True
)
print("\nAnalisi dettagliata dei paesi (ordinati per % di vini vegani):")
print("-" * 80)
print(f"{'Paese':20} | {'Totale':8} | {'Vegani':8} | {'%Vegani':8} | {'Principali Produttori'}")
print("-" * 80)
for paese, stats in regioni_ordinate:
if stats['totale'] >= 5:
perc_vegani = (stats['vegani'] / stats['totale']) * 100
produttori = set()
for vino in stats['vini_vegani']:
produttori.add(vino['producer'])
top_produttori = list(produttori)[:3]
print(f"{paese[:20]:20} | {stats['totale']:8d} | {stats['vegani']:8d} | "
f"{perc_vegani:7.1f}% | {', '.join(top_produttori[:2]) if top_produttori else 'N/A'}")
while True:
print("\nOpzioni di analisi per paese:")
print("1. Visualizza dettagli di un paese specifico")
print("2. Mostra solo paesi con alta percentuale di vini vegani (>70%)")
print("3. Mostra paesi con percentuale di vini vegani sotto una soglia specifica")
print("4. Torna al menu principale")
scelta = input("\nScegli un'opzione (1-4): ")
if scelta == "1":
paese = input("Inserisci il nome del paese: ").lower()
if paese in statistiche_regioni:
stats = statistiche_regioni[paese]
print(f"\nDettagli per {paese}:")
print(f"Totale vini: {stats['totale']}")
print(f"Vini vegani: {stats['vegani']} ({(stats['vegani']/stats['totale'])*100:.1f}%)")
print("\nVini vegani del paese:")
self.visualizza_risultati_completi(stats['vini_vegani'])
print("\nVini non vegani del paese:")
self.visualizza_risultati_completi(stats['vini_non_vegani'])
else:
print("Paese non trovato o non valido.")
elif scelta == "2":
print("\nPaesi con >70% vini vegani:")
for paese, stats in regioni_ordinate:
if stats['totale'] >= 5:
perc_vegani = (stats['vegani'] / stats['totale']) * 100
if perc_vegani > 70:
print(f"{paese}: {perc_vegani:.1f}% vegani ({stats['vegani']}/{stats['totale']} vini)")
elif scelta == "3":
try:
soglia = float(input("Inserisci la soglia percentuale (es. 60 per vedere i paesi con meno del 60% di vini vegani): "))
print(f"\nPaesi con <{soglia}% vini vegani:")
found = False
for paese, stats in regioni_ordinate:
if stats['totale'] >= 5:
perc_vegani = (stats['vegani'] / stats['totale']) * 100
if perc_vegani < soglia:
print(f"{paese}: {perc_vegani:.1f}% vegani ({stats['vegani']}/{stats['totale']} vini)")
found = True
if not found:
print(f"Nessun paese trovato con meno del {soglia}% di vini vegani.")
except ValueError:
print("Errore: inserisci un numero valido per la soglia.")
elif scelta == "4":
break
4. Menu e funzione principale¶
La funzione menu_ricerca()
rappresenta l'interfaccia utente dell'applicativo e ha lo scopo di guidare l'utente attraverso le varie funzionalità disponibili.
La funzione inizia chiedendo all'utente di selezionare il file CSV contenente il database dei vini attraverso la funzione seleziona_file()
. Ho implementato dei controlli di sicurezza in questa fase: se nessun file viene selezionato (ovvero se nome_file
è None
), la funzione termina con un messaggio appropriato. Similmente, se si verifica un errore durante l'inizializzazione del sistema (gestito attraverso un blocco try-except), l'utente viene informato e la funzione termina utilizzando l'istruzione return
.
Il cuore della funzione è un ciclo while True
che presenta all'utente un menu con otto opzioni di ricerca. Questa struttura ciclica permette all'utente di effettuare multiple ricerche senza dover riavviare il programma. Le opzioni includono ricerche per stato, per produttore, per nome o tipologia del vino, oltre a funzionalità più avanzate come l'analisi dettagliata dei paesi e la ricerca incrociata. Praticamente l'utilizzo di un ciclo while
garantisce che il menu rimanga attivo finché l'utente non sceglie di uscire.
L'input dell'utente viene memorizzato nella variabile scelta
.
Per ogni opzione di ricerca, ho implementato la logica necessaria per raccogliere gli input dell'utente e chiamare il metodo appropriato della classe SistemaGestioneVini
.
Ad esempio, se l'utente seleziona l'opzione 1, il programma gli chiede di inserire il nome di uno stato, quindi chiama il metodo cerca_per_stato()
dell'oggetto sistema
(che è un'istanza di SistemaGestioneVini
) per eseguire la ricerca.
La ricerca incrociata avanzata (opzione 7) permette all'utente di combinare diversi criteri di ricerca, con la possibilità di lasciare vuoti alcuni campi per ottenere risultati più ampi.
L'input viene preprocessato utilizzando strip()
per rimuovere spazi superflui e la scelta tra vegan friendly e non vegan friendly viene gestita attraverso una semplice conversione dell'input s/n in un booleano.
Il ciclo continua finché l'utente non sceglie l'opzione 8 (Esci), che termina il programma con un messaggio di saluto. Per gestire input non validi, ho implementato un else finale che informa l'utente dell'errore e permette di riprovare. Quindi se l'utente immette un numero diverso da 1-8, viene stampato un messaggio di errore.
def menu_ricerca():
"""Menu interattivo per le ricerche."""
print("\nSeleziona il file CSV dei vini...")
nome_file = seleziona_file()
if nome_file is None:
print("Impossibile procedere senza un file valido.")
return
try:
sistema = SistemaGestioneVini(nome_file)
except Exception as e:
print(f"Errore nell'inizializzazione del sistema: {e}")
return
while True:
print("\nMenu di ricerca:")
print("1. Cerca per stato")
print("2. Cerca vini vegani per stato")
print("3. Trova produttori esclusivamente vegani")
print("4. Analisi dettagliata dei paesi")
print("5. Cerca per nome produttore")
print("6. Cerca per nome vino")
print("7. Ricerca incrociata avanzata")
print("8. Esci")
scelta = input("\nScegli un'opzione (1-8): ")
if scelta == "1":
stato = input("Inserisci il nome del paese: ")
sistema.cerca_per_stato(stato)
elif scelta == "2":
stato = input("Inserisci il nome del paese: ")
sistema.cerca_per_stato(stato, solo_vegani=True)
elif scelta == "3":
sistema.cerca_produttori_solo_vegani()
elif scelta == "4":
sistema.analisi_regioni()
elif scelta == "5":
produttore = input("Inserisci il nome del produttore: ")
sistema.cerca_per_produttore(produttore)
elif scelta == "6":
nome_vino = input("Inserisci il nome o la tipologia del vino: ")
risultati = sistema.cerca_per_nome(nome_vino)
if risultati:
print(f"\nTrovati {len(risultati)} vini che contengono '{nome_vino}':")
for vino in risultati:
print(f"\nNome: {vino['name']}")
print(f"Produttore: {vino['producer']}")
print(f"Origine: {vino['origin']}")
print(f"Etichetta: {vino['label']}")
else:
print(f"Nessun vino trovato con '{nome_vino}' nel nome.")
elif scelta == "7":
print("\nRicerca incrociata avanzata")
nome_vino = input("Inserisci nome vino (lascia vuoto per tutti i vini): ").strip()
stato = input("Inserisci stato (lascia vuoto per tutti gli stati): ").strip()
solo_vegani = input("Mostrare solo vini vegani? (s/n): ").lower() == 's'
sistema.ricerca_incrociata(
nome_vino=nome_vino if nome_vino else None,
stato=stato if stato else None,
solo_vegani=solo_vegani
)
elif scelta == "8":
print("Arrivederci!")
break
else:
print("Opzione non valida. Riprova.")
5. Esecuzione¶
Questa funzione avvia il programma attivando l'interfaccia utente definita all'interno della funzione menu_ricerca
. Senza questa riga, il programma non eseguirebbe nessuna delle sue logiche fondamentali e terminerebbe semplicemente dopo le definizioni di classi e funzioni.
Quando questa linea viene eseguita, il sistema:
- richiede all'utente di selezionare il file CSV;
- inizializza il sistema di gestione;
- presenta il menu interattivo;
- rimane in esecuzione finché l'utente non sceglie di uscire.
menu_ricerca()
Extra: web app¶
Al fine di rendere questo SGE ancora più accessibile e fruibile, ho deciso di estendere il progetto sviluppando una versione web basata interamente sul medesimo database e sulla stessa logica di ricerca implementata in Python. Ho realizzato un'applicazione React, che riproduce fedelmente le funzionalità del progetto originale.
L'intero codice sorgente è inoltre liberamente consultabile sul repository GitHub.