Fondamenti di Programmazione

Dizionari

In questa lezione cercheremo di risolvrere in seguente problema. Dati vari files su disco, selezionare il file in cui una lista di parole occorre con piu' alta probabilita'. Questa e' una parte fondamentale di ogni search engine (Google, Bing, etc.). Ovviamente, noi proporremo una versione estremamemnte semplificata. Alla stesso tempo questo introdurra' vari concetti utili, sia sui file che su insiemi e dizionari. Per lavorare su questa lezione, useremo i file contenuti in questo pacchetto.

Dizionari

Per risolvere il problema di cui sopra, useremo un nuovo tipo di oggetto. Un dizionario in Python è un oggetto di tipo dict che può essere visto come una collezione di coppie "chiave"-valore: (key, value). In un dizionario, non ci sono due coppie con lo stesso valore della chiave key. In altri termini, un dizionario è una mappa da un insieme di chiavi a un insieme di valori che associa ad ogni chiave un valore. Ad ogni chiave è associato un solo valore ma più chiavi possono avere lo stesso valore. Un dizionario può anche essere visto come una generalizzazione del tipo list in cui le chiavi possono solamente essere un intervallo di interi consecutivi a partire da 0.

Il tipo dict ha molti modi per costruire i suoi oggetti. Si può creare un dizionario vuoto:

>>> d = dict()
>>> d
{}
>>> d2 = {}   # Simile alla costruzione della lista vuota
>>> d2
{}

Oppure si può creare un dizionario con delle associazioni esplicite, similmente alle liste, ma con le parentesi graffe e i : che associano la chiave al corrispondente valore e le virgole che separano le coppie:

>>> rubrica = { 'pippo': '555-123456', 'pluto': '555-654321' }
>>> rubrica
{'pippo': '555-123456', 'pluto': '555-654321'}

Possiamo accedere ai valori dei singoli elementi utilizzando il nome del dizionario seguito dal nome della chiave tra parentesi quadre. Python da' errore se si accede erroneamente ad una chiave che non esiste. As esempio:

>>> rubrica['pippo']
'555-123456'
>>> rubrica['pluto']
'555-654321'
>>> rubrica['paperone']
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    rubrica['paperone']
KeyError: 'paperone'

Una volta che un dizionario è stato costruito possiamo aggiungere, modificare o rimuovere associazioni. L'aggiunta di una nuova chiave e del relativo valore può essere fatto tramite la sintassi delle parentesi quadre:

# aggiounta di elementi
>>> rubrica['topolino'] = '555-112233'
>>> rubrica['topolino']
'555-112233'

# modifica di elementi
>>> rubrica['pippo'] = '555-214365'
>>> rubrica['pippo']
'555-214365'

Si puo' controllare se una chiave esiste nel dizione con l'operatore in. Notate che questo e' diverso dalle liste dove in controllare su un valore e' presente. Nel dizionario controlla se la chiave e' presente. As esempio, possiamo controllare i valori inseriti in precedenza.

>>> 'pippo' in rubrica
True
>>> 'pluto' in rubrica
True
>>> 'paperino' in rubrica
False

Infine, possiamo iterare sulle chiavi di un dizionario usando semplicemente il ciclo for. Possiamo anche iterare sulle coppie chiave, valore usando il ciclo for della lista dizionario.items().

# iterazione sulle chiavi
>>> for chiave in rubrica:
        print chiave, rubrica[chiave]
topolino 555-112233
pippo 555-214365
pluto 555-654321

# iterazine sulle chiavi e i valori
>>> for chiave, valore in rubrica.items():
    print chiave, valore
topolino 555-112233
pippo 555-214365
pluto 555-654321

Ecco un esempio visualizzato.

rubrica = { 'pippo': '555-123456', 'pluto': '555-654321' }

# accesso agli elementi
print rubrica['pippo']
print rubrica['pluto']

# aggiunta di elementi
rubrica['topolino'] = '555-112233'
print rubrica['topolino']

# modifica di elementi
rubrica['pippo'] = '555-214365'
print rubrica['pippo']

# check sugli elementi
pippo_esiste = 'pippo' in rubrica
pluto_esiste = 'pluto' in rubrica
paperino_esiste = 'paperino' in rubrica

# iterazioni sulle chiavi
for chiave in rubrica:
    print chiave, rubrica[chiave]

# iterazione su chiavi e valori
for chiave, valore in rubrica.items():
    print chiave, valore

Insiemi

Un oggetto di tipo set rappresenta una collezione di valori (o oggetti) senza duplicati. L'insieme e' quindi simile al dizionario, ma senza valori. In generale, gli insiemi sono usati meno di frequesnte rispetto ai dizionari, ma rimangono utili. Qui introduciamo i set, ma ci focalizzeremo sui dizionari. Un nuovo insieme puù essere creato vuoto:

>>> insieme = set()
>>> insieme
set([])

E poi possiamo aggiungere elemnti tramite il metodo add():

>>> insieme.add(5)
>>> insieme
set([5])
>>> insieme.add('five')
>>> insieme
set(['five', 5])
>>> insieme.add(5)   # Non ha effetto perchè 5 è già presente
>>> insieme
set(['five', 5])

Un insieme può anche essere creato a partire da un oggetto sequenza o insieme:

>>> lettere = set('abcdefgabcdefg')
>>> lettere
set(['a', 'c', 'b', 'e', 'd', 'g', 'f'])

Può essere iterato come una sequenza, anche se l'ordine con cui sono iterati gli elementi è arbitrario:

>>> for x in insieme:
        print x

five
5

Il numero di elementi di un insieme può essere ottenuto come al solito con la funzione len(). Si può controllare se un valore è nell'insieme tramite l'operatore in, e si può rimuovere un elemento con i metodi remove() o pop(). Inoltre, gli insiemi supportano le operazioni di base che sono unione |, intersezione &, differenza - e differenza simmetrica ^:

>>> lettere2 = set('gioco')
>>> lettere | lettere2
set(['a', 'c', 'b', 'e', 'd', 'g', 'f', 'i', 'o'])
>>> lettere - lettere2
set(['a', 'b', 'e', 'd', 'f'])
>>> lettere & lettere2
set(['c', 'g'])
>>> lettere ^ lettere2
set(['a', 'b', 'e', 'd', 'f', 'i', 'o'])

Ricerca di Documenti

Applichiamo adesso l'uso dei file e dei dizionari per risolvere un problema molto comune. dato una lista di documenti, trovare il documento che e' piu' attinente ad una serie di parole. Questo e' uno dei componenti fondamentali dei search engines (come Google). L'idea di base e' quella di calcolare la frequenza con cui le parole occorrono in ogni documento, per poi scegliere in documento in cui le parole richeste occorronno piu' di frequente. Schematicamente, procederemo su queste linee:

  1. dato un testo, creiamo una lista di parole rimuovendo la punteggiatura con str.replace(), convertendo le parole in minnuscolo con str.lower(), ed infine creando la lista con str.split();
  2. creaiamo un dizionario di coppie (parola, conteggio) scorrendo le parole nella lista e inserendone nel dizionario se non gia' presenti, o encrementandone il conteggio su gia' inserite;
  3. per tenere conto che testi diversi possono vaere lunghezza diversa, creaiamo un dizionario di frequenze normalizzando i conteggi per il numero totale di parole;
  4. dato una o piu' parole in ricerca, calcoliamo poi la somma delle frequenze di queste parole nel dizionario precedente; piu' alto e' questo valore, maggiore e' la probabilita' che il testo sia un buon match per la ricerca.
def text_score(text, search):
    # 1. grab words list
    for c in ',!?':
        text = text.replace(c,'')
    words = text.lower().split()
    # 2. count words coccurrances
    counts = {}
    for word in words:
        if word in counts:
            counts[word] += 1
        else:
            counts[word] = 1
    # 3. normalize counts
    nwords = len(words)
    frequencies = {}
    for word, count in counts.items():
        frequencies[word] = float(count) / float(nwords)
    # 4. compute scores
    score = 0
    for word in search.lower().split():
        if word in frequencies:
            score += frequencies[word]
    # done
    return score

def texts_scores(texts, search):
    # compute scores for each text and return as dictionary
    scores = {}
    for name, text in texts.items():
        scores[name] = text_score(text, search)
    return scores

texts_dict = {
    'ciao': 'Ciao ciao, come va?',
    'addio': 'Addio! O forse dovevo dire ciao?'
}

print texts_scores(texts_dict, 'ciao')
print texts_scores(texts_dict, 'addio')

In codice precente implementa l'algoritmo descritto sopra. Per gli esempi indicati, l'output e'

{'ciao': 0.5, 'addio': 0.16666666666666666}
{'ciao': 0, 'addio': 0.16666666666666666}

Possiamo estendere questo approccio a fare ricerca su piu' semplicemente caricando i file in memoria ed eseguendo le funzioni precedenti. Nel codice sequente abbiamo fatto proprio questo, come due piccoli migliramenti. Primo, usiamo l'elenco di punteggiatura che Python definisce in string.punctuation. Secondo, aggiungiamo una piccola funzione per rimuovere parole spurie (in questo caso escludiamo le parole che iniziano per un numero).

import string

def text_score(text, search):
    # 1. grab words list
    for c in string.punctuation:
        text = text.replace(c,'')
    words = text.lower().split()
    # 2. remove spurious words
    nwords = []
    for word in words:
        if word[0].isdigit(): continue
        nwords.append(word)
    words = nwords
    # 3. count normalized words coccurrances
    counts = {}
    for word in words:
        if word in counts:
            counts[word] += 1
        else:
            counts[word] = 1
    # 4. normalize counts
    nwords = len(words)
    frequencies = {}
    for word, count in counts.items():
        frequencies[word] = float(count) / float(nwords)
    # 5. compute scores
    score = 0
    for word in search.lower().split():
        if word in frequencies:
            score += frequencies[word]
    # done
    return score

def texts_scores(texts, search):
    # compute scores for each text and return as dictionary
    scores = {}
    for name, text in texts.items():
        scores[name] = text_score(text, search)
    return scores

Possiamo eseguire la funzione precedente su un set of file che risiedono su disco. Nel nostro caso, useremo quattro libri scaricati da Project Gutemberg. Per semplicita' abbiamo collezionato tutti i file nel [pacchetto][pacchetto] di questa lezione. Per fare girare l'esempio, basta eseguire il file program_docretrieval.py. Ecco un test per un po' di ricerche diverse.

# book list
books = {
    'alice': 'text_alice.txt',
    'frankenstein': 'text_frankenstein.txt',
    'holmes': 'text_holmes.txt',
    'prince': 'text_prince.txt'
}

# search list
searches = ['monster','horror','rabbit','crime']

# load books
texts_dict = {}
for name, filename in books.items():
    with open(filename,'rU') as f:
        texts_dict[name] = f.read()

# print matches
for search in searches:
    print search, ':', texts_scores(texts_dict,search)
print ''

Il programma stampa il seguente output. Notate come per le ricerca monster o horror il libro con lo score maggiore e' frankeinstein. Mentre per la ricerca rabbit il libro piu' pertinente e' alice (Alice in Winderland).

monster : {'frankenstein': 0.00039823747800059095, 'holmes': 0, 'alice': 0, 'prince': 0}
horror : {'frankenstein': 0.0005652402913556775, 'holmes': 0.00010249911478037235, 'alice': 0, 'prince': 0}
rabbit : {'frankenstein': 0, 'holmes': 1.8636202687340428e-05, 'alice': 0.001466975982532751, 'prince': 0}
crime : {'frankenstein': 0.00020554192412933727, 'holmes': 0.00029817924299744685, 'alice': 0, 'prince': 1.91255785487511e-05}

Formati di Testo

La stampa dei risultati precedenti rimane proprio poco chiara. Per renderla piu' leggibile si potrebbe stamparla in una tabella. La prima riga della tabella conterra' i nomi dei libri iniziando con l'etichetta results per indicare il contenuto di tutta la tabella. Le altre righe conterranno i risultati iniziando con i termini cercati. In ogni riga, i valori sono separati da un carattere tab (\t). Possiamo creare una stringa separata da una lista usando il metodo str.join(lista). Ecco un esempio:

# better printing
booknames = books.keys()
formatted =  '\t'.join(['results']+booknames) + '\n'
for search in searches:
    results = texts_scores(texts_dict,search)
    formatted_results = []
    for result in results.values():
        formatted_results += [ str(result) ]
    formatted += '\t'.join([search]+formatted_results) + '\n'
print formatted
print ''

che stampa

results frankenstein    holmes  alice   prince
monster 0.000398237478001   0   0   0
horror  0.000565240291356   0.00010249911478    0   0
rabbit  0   1.86362026873e-05   0.00146697598253    0
magic   2.56927405162e-05   0   3.4115720524e-05    0
crime   0.000205541924129   0.000298179242997   0   1.91255785488e-05

Possiamo salvare questa stringa su file (vedere lezione precedente).

# saving results
with open('results.txt','w') as f:
    f.write(formatted)

Ancora meglio, possiamo salvarne una versione in cui il carattere separatore e' una virgola facendo

# saving csv results
with open('results.csv','w') as f:
    f.write(formatted.replace('\t',','))

Questo semplice formato e' utilissimo dato che possiamo caricare i risultati su Excel o Google Docs direttamente per analizzzarli meglio! E con pochissime righe di codice. Nel pacchetto in dotazione, i files results.txt e results.csv sono i risultati in questi due formati.