Dalla chiacchiera nasce il dataframe

Analisi di dominî culturali: creare un network di termini

di

thumbnail

16

mag

2018

L’obiettivo è capire quanto lo stesso termine, usato da due comunità diverse, conservi un identico significato.

[Pubblichiamo una versione italiana dell’articolo Analyzing Cultural Domains with Python, opera dell’autore di Data Science con Python. La prima parte dell’articolo è stata pubblicata lo scorso 18 aprile.]

Nella parte iniziale di questo articolo ho avviato un lavoro con Python e librerie assortite allo scopo di definire un gruppo omogeneo di persone e verificare se, parlando di interessi diversi, un termine dato conservi lo stesso significato per tutti o ne assuma di diversi. Questo processo è noto come analisi del dominî culturali, CDA (Cultural Domain Analysis). Mi sono interrotti mentre lavoravo sull’elenco dei membri della comunità di LiveJournal che ho scelto di adottare.

L’elenco in questione sta in una tabella a due colonne. La prima rappresenta alcuni aspetti sottili del tipo di iscrizione (“P” per i singoli individui, “C” per le comunità, “<” per “friends,” “>” per “friends-of”); la seconda colonna contiene i nomi utente. Una situazione ideale per un DataFrame di Pandas! esclamo, e chiamo pandas.read_table() per convertire la tabella in un DataFrame a due colonne:

members_url = "http://www.livejournal.com/misc/fdata.bml?user=" \ + GROUP +¬
 "&comm=1"
members = pd.read_table(members_url, sep=" ", comment="#", names=("type",¬
 "uid"))

 

Il mio prossimo passo consiste nello scaricare tutti gli elenchi di interessi, convertire gli interessi in termini (no, non sempre coincidono) e unire tutte le liste di termini in una matrice vettoriale di termini. Un elenco di interessi appare molto simile all’elenco dei membri, è uguale persino la chiamata al caching:

# Nota: I data miner educati fanno cache quando finiscono.
#  Quelli maleducati vengono banditi.
# <intid> <intcount> <interest ...>
18576742 1 +5 sexterity
624552 7 a beautiful mess
18576716 1 any more hot chicks?
44870 28 seriously?
1638195 94 shiny!
«…altri interessi…»

 

Questa è ancora una tabella (che mostra l’identificativo dell’interesse, il numero di volte nel quale Internet viene dichiarata a livello di sistema e l’interesse vero e proprio. I frame di Pandas faticano con testo irregolare e dovrò occuparmene essenzialmente a mano, usando strumenti Python di basso livello. Se non c’è nome utente o se l’utente non ha dichiarato interessi, il contenuto è diverso:

! invalid user, or no interests

 

Certamente voglio leggere la prima riga prima di addentrarmi in qualsivoglia analisi intelligente.

Nel prossimo frammento di codice, preparo un WNL (WordNetLemmatizer) e un elenco di stop word (ne riparleremo), e imposto un accumulatore DFS (Depth-first search) vuoto (a ❶); ottengo un elenco di interessi per ciascun membro unico (a ❷–❺); lo appendo in coda all’accumulatore (a ❻); e fondo tutti gli elenchi in un solo DataFrame (a ❼).

❶ wnl = nltk.WordNetLemmatizer()
stop = set(nltk.corpus.stopwords.words("english")) | set(("&"))
dfs = []
for uid in members.uid.unique():
	print(uid)
	uid_url = "http://www.livejournal.com/misc/interestdata.bml?user=" + uid
	❷ try:
		with urllib.request.urlopen(uid_url) as source:
		raw_interests = [line.decode().strip() for line in source.readlines()]
	except:
		print("Could not open", uid_url)
		continue
	❸ if raw_interests[0] == '! invalid user, or no interests':
	continue
	❹ interests = [" ".join(wnl.lemmatize(w)
		for w in nltk.wordpunct_tokenize(line)[2:] if w not in stop)
		for line in raw_interests if line and line[0] != "#"]
	❺ interests = set(interest for interest in interests if interest)
	❻ dfs.append(pd.Series(index=interests, name=uid).fillna(1))
❼ domain = pd.DataFrame().join(dfs, how="outer").fillna(0)

 

Il blocco try-except a ❷ apre l’URL di un elenco di interessi e lo scarica come una lista Python di stringhe. Ogni stringa viene decodificata e ripulita dagli spazi bianchi in eccesso a fine riga. Su LJ gli elenchi di interessi sono sempre in minuscolo ma, ove non fosse così, una chiamata di lower() assicurerebbe l’adeguatezza dei confronti per il resto dello script. Se l’URL non si apre per qualsiasi ragione fuori dal mio controllo, lo script mi informa del problema e, invece di bloccarsi, passa all’utente successivo. Lo stesso accade quando l’utente non ha interessi o neanche esiste (a ❸).

Verso il modello vettoriale

Un interesse può essere composto da più parole e alcune di queste, o tutte, possono essere forme di altre parole (l’esempio più banale è quello dei plurali, cavallo-cavalli). I ricercatori hanno opinioni eterogenee su come trattare queste situazioni. Alcuni suggeriscono di trattare ciascuna forma come una parola a sé. Altri, me compreso, propugnano la lemmatizzazione o lo stemming: ridurre la forma al suo lemma (la presentazione standard della parola) o al suo stem (gambo; la parte significativa minima della parola cui possono essere attaccate le terminazioni). Un lemmatizzatore ridurrebbe più programmatori a un programmatore e un paladino dello stemming, in funzione del proprio zelo, genererebbe un programm se non un program. Quasi tutti concordano che certe parole (articoli, congiunzioni) non dovrebbero mai essere conteggiate. Il blocco di codice a ❹ fa uso intelligente di NLTK, Natural Language Toolkit. Il toolkit contiene strumenti per la tokenizzazione delle parole (wordpunct_tokenize(), scompone una stringa in una serie di parole), lo stemming e la lemmatizzazione (wnl.lemmatize(); ho creato il lemmatizzatore a ❶), più molti altri. Contiene anche un elenco di stop word, che ho ampliato per comprendere anche & (sempre a ❶). Si noti che ho convertito l’elenco standard di stop word in un insieme, per un lookup più veloce.

Come risultato di lemmatizzazione, stemming ed eliminazione delle stop word, un elenco di termini può trovarsi ad avere duplicati (un sushi e il sushi possono ridursi entrambi a sushi). Converto ciascun elenco di termini in un insieme (a ❺). Di sicuro, gli insieme non contengono duplicati.

Il mio obiettivo è produrre un modello vettoriale di termini (Term Vector Model, TVM), una tabella che contiene nelle righe i termini e nelle colonne i membri della comunità. Nel linguaggio di Pandas questa tabella si chiama DataFrame e e le sue colonne sono Series. Il codice a ❻ converte un elenco di termini in una Series. I termini individuali formano l’indice delle Series, nel quale i valori sono tutti 1, come questo:

shiny!           1
+5 sexterity 1
big damn hero 1
«…altri interessi…»
Name: twentyplanes, dtype: float64

 

Infine, a ❼ riunisco tutte le Series in un DataFrame. Questa operazione evoca il mistero dell’allineamento dei dati, nel quale tutte le Serie partecipanti vengono stirate verticalmente per allineare le rispettive etichette di riga (termini!) aligned. Questo produce inevitabilmente caselle vuote nel frame. Le riempio con zeri: una casella vuota all’intersezione di riga A e colonna B segnala che il termine A è assente dalla lista B. Il dominio variable rappresenta pienamente il mio dominio culturale: gli utenti di LiveJournal interessati alla serie The Good Wife di CBS.

Manca ancora un passaggio per arrivare al risultato finale: costruire il network dei termini. La prossima volta!

Data Science con Python

La scelta di molti data scientist, per versatilità.

 




Dmitry Zinoviev (@aqsaqal) vanta un dottorato di ricerca in Informatica e un master in Fisica. I suoi ambiti di ricerca includono la creazione di modelli simulati al computer, l’analisi delle reti sociali e l’informatica umanistica. Insegna alla Suffolk University di Boston dal 2001.

In Rete: https://github.com/dzinoviev

Letto 1.012 volte | Tag: , , , , , ,

Lascia il tuo commento