L’obiettivo di questo libro è presentare un software per l’analisi dei dati relazionali. Il software in questione si chiama sna, ed ha alcune particolarità rilevanti:
Essendo R costituito da un insieme di funzioni, ed essendo Free Software, esso ha la possibilità di venire espanso con l’aggiunta di codice nostro; questa possibilità di programmazione ci permette di automatizzare compiti noiosi1.
Dopo una breve introduzione storica e metodologica a R, comprensiva del come muovere i primi passi, la presentazione del pacchetto sna coprirà sia il lato teorico, legato ai concetti basilari della Social Network Analysis, sia il lato applicativo, legato alle necessarie istruzioni tecniche per usare le funzioni del pacchetto.
Introdurremo quindi alcuni indici (e concetti) avanzati dell’approccio strutturale all’analisi delle reti sociali, contestualizzandone l’uso all’interno del dibattito teorico in corso, per poi procedere ad illustrarne l’implementazione in funzioni con gli strumenti di R.
Successivamente, ci occuperemo della visualizzazione, sia con un breve excursus storico legato alla progressiva presa di consapevolezza dell’importanza di questa tipologia di rappresentazione dei dati, che con una precisa disamina delle funzioni che sna ci offre per disegnare sociogrammi.
Infine, ci occuperemo della parte, per così dire, più creativa dell’uso di R, la programmazione. Affronteremo un piccolo caso di studio costituito da circa cinquecento reti ego-centrate; per ogni rete dovremo calcolare alcune misure per poi salvare l’output ottenuto. Ulteriore complicazione, le reti ci sono state fornite in formato Excel.
Sebbene importare una rete, analizzarla e salvarne l’output alla fine del libro non sembrerà più un fatto complicato, il doverne affrontare circa cinquecento potrebbe creare una certa preoccupazione. Sfrutteremo quindi la possibilità di programmare R, riuscendo non solo ad automatizzare l’analisi in meno di 30 righe di codice, ma anche a salvare l’output di ogni singola rete in un’unica tabella riassuntiva dentro un database MySQL, in modo da facilitare ulteriori analisi sulle tendenze presenti nel dataset (sempre nelle stesse 30 righe)2.
Ringraziamenti Questo lavoro è la prosecuzione di un progetto iniziato con la mia seconda tesi di laurea, intitolata “Elaborazione dei dati nella Social Network Analysis attraverso il pacchetto sna: analisi e possibilità di ulteriore innovazione”, supervisionata dal Prof. Andrea Salvini del Dipartimento di Scienze Sociali di Pisa e consultabile online all’indirizzo
http://etd.adm.unipi.it/theses/available/etd-10032005-143922/.
Desidero cogliere l’occasione per ringraziarlo, sia per i consigli ed il supporto ricevuti in sede di realizzazione della tesi, che per quelli dati durante la stesura di questo breve manuale: egli per primo ha creduto e crede fermamente nell’importanza del Free Software e dell’Open Source.
Sebbene le opinioni espresse in questo lavoro siano il frutto dei nostri incontri, durante i quali di volta in volta ho raccolto sia nuove idee che nuove correzioni, in particolare sotto l’aspetto metodologico, la responsabilità per eventuali errori o inesattezze qui contenute è da attribuire solo a me.
Ringrazio inoltre il prof. M.A. Toscano, che ha reso possibile la pubblicazione di questo lavoro, per l’incoraggiamento e il sostegno che riserva alla mia crescita intellettuale.
Infine, un amichevole ringraziamento ai colleghi Dania Cordaz, Alfredo Givigliano e Stefania Milella.
Nella Social Network Analysis il ricercatore dispone di diversi approcci metodologici a differenti livelli di complessità. Tra questi, quello strutturale è senz’altro il più immediato ed il più adottato, nonché il più sviluppato. Esso rappresenta l’origine stessa dell’analisi delle reti sociali, nata dal tentativo di ricercatori come Moreno, Jennings, Lewin, Harary ed altri, di misurare caratteristiche strutturali dei gruppi e di rappresentarle graficamente.
Questa nascita pragmatica ha segnato la disciplina, tanto che nel corso degli anni molti autori l’hanno considerata più un insieme di tecniche che una direzione alternativa di ricerca, a causa proprio della mancanza di una fondazione teorica forte che facesse da contraltare alla proliferazione di sofisticati strumenti di indagine. A questo punto del suo sviluppo però, secondo Salvini, “la Social Network Analysis - sebbene non abbia ancora conseguito uno statuto epistemologico definito - costituisce una prospettiva teorica affidabile e coerente strettamente collegata in un duplice legame logico-concettuale con una metodologia di ricerca pertinente e distinta dalle metodologie di tipo convenzionale” [Salvini, 2005, p. 56].
La scelta del metodo è spesso il frutto più di considerazioni pragmatiche, relative all’oggetto della ricerca, che di una riflessione sulla compatibilità epistemologica con assunti teorici. Il compito della metodologia è quello di riflettere sull’adeguatezza e la coerenza logica degli strumenti in relazione alla cornice epistemologica di riferimento. L’analisi delle reti sociali non fa eccezione: anche la costruzione del dato relazionale deve presupporre “la coerenza con un quadro epistemologico” [Salvini, 2005, p. 56]. L’aspetto teorico è quindi essenziale allo sviluppo di una metodologia coerente. Secondo Randall Collins, la teoria deve servire a spiegare i processi che generano un outcome. Weick aggiunge che, oltre a questo, essa deve metterci in grado di fare previsioni su outcome futuri più o meno probabili. Secondo Patrick Kenis invece, una buona teoria deve rispettare, e rendere conto di, quattro punti fondamentali:
Se questa descrizione vale a rappresentare una buona teoria nelle scienze sociali, quali criteri deve allora rispettare una teoria di network per le scienze sociali? Essa deve forse spiegare l’outcome in base alle caratteristiche strutturali del network? Oppure deve spiegarlo attraverso modelli causali che tengano conto dell’interazione complessa tra gli attori, le loro relazioni e la struttura in una prospettiva di network? In breve, la struttura del network ha una propria ontologia di cui noi quantifichiamo le caratteristiche oppure essa è una metafora che usiamo per catturare la complessità del reale?
Rispondendo affermativamente alla prima domanda, avremo a che fare, e produrremo, network theories (teoria dell’omofilia, teoria del bilanciamento strutturale, teoria della gestione delle risorse, ...), all’interno delle quali le condizioni iniziali vengono pensate come proprietà della rete sociale rilevata, rete che produrrà particolari outcomes proprio perché strutturata in un certo modo.
Al contrario, rispondendo affermativamente alla seconda domanda, spiegheremo l’outcome prodotto dalle condizioni iniziali usando la rete come una metafora per leggere i dati, producendo network theory of ... (una teoria di rete dell’organizzazione, una teoria di rete dell’innovazione, una teoria di rete del potere delle elites, ...), ovvero spiegazioni dell’outcome da prospettive di rete.
Borgatti individua come merito delle prime, le network theories, l’aver spostato l’attenzione del ricercatore dalle spiegazioni atomistiche in termini di attributi dei soggetti alle spiegazioni dei fenomeni in termini di relazioni tra attori interdipendenti. Tuttavia, le network theories non chiariscono lo status ontologico della struttura della rete che individuano.
L’approccio della network theory of , ovvero alla spiegazione dell’outcome da una prospettiva integrata (attore-struttura-relazione), risolve il problema ontologico, ed il modello che il ricercatore costruisce come spiegazione (costruzione in continuo divenire) è il prodotto di un sistema complesso di interazione tra l’azione di attori passati (sedimentata in struttura per gli attori attuali), i dati di attributo degli attori attuali, e le loro azioni che sono determinate e determinano la dimensione dell’interazione. In altre parole, quelle di Kenis [Raab-Kenis, 2007, p. 21], il problema delle network theories è che esse usano la struttura della rete, e solo quella, per spiegare la totalità del processo di produzione dell’outcome: la struttura esiste ed è la sola causa dell’effetto rilevato. Allora, egli osserva:
“noi dovremmo essere in grado di dimostrare che una differenza nelle caratteristiche strutturali della rete, per esempio la presenza o l’assenza di un nodo, hanno un impatto significativo nella produzione di una policy” [Raab-Kenis, 2007, p. 11-12, trad. nostra].
Ma non lo siamo. Riprendiamo allora Salvini:
“Ciò che la Social Network Analysis afferma, anche nella sua versione strutturale, è la circolarità della dimensione dell’agency e della struttura mediante le interdipendenze relazionali: l’idea di circolarità impedisce di assegnare un peso ontologicamente ed epistemologicamente rilevante ad una delle tre dimensioni, che sono mutualmente costitutive. Se così non fosse, si realizzerebbe una qualche forma di determinismo di volta in volta costruito sul primato dell’attore, della relazione o della struttura” [Salvini, 2005, p. 39].
Assumiamo quindi come prospettiva di ricerca quella delle network theories of , un approccio complesso alla dimensione relazionale, di attributo e di struttura degli attori che studia sia il livello del gruppo che quello dell’attore, che le loro continue interazioni.
Il software di cui ci occuperemo in questo manuale è in grado di offrire gli strumenti avanzati indispensabili per questo tipo di approccio, tuttavia dato il carattere introduttivo di questo lavoro, ci limiteremo ad illustrare gli indicatori basilari dell’analisi strutturale.
R è un linguaggio ed un ambiente di sviluppo per l’analisi statistica e la rappresentazione grafica dei dati, conosciuto anche come GNU S, ovvero come versione free di S - quest’ultimo sviluppato negli anni Ottanta presso gli AT&T Bell Laboratories da Rick Becker, John Chambers e Allan Wilks; la comunità degli sviluppatori ne ha poi fatto una versione libera, inizialmente grazie a Robert Gentleman e Ross Ihaka, e grazie anche all’aiuto dello stesso John Chambers.
Il linguaggio di programmazione usato da R è un dialetto di S e, come quest’ultimo, dal punto di vista sintattico ha delle affinità con il C; tuttavia, essendo un linguaggio del tipo FPL (Functional Programming Language), semanticamente viene considerato più vicino al Lisp. Il linguaggio di R permette all’utente di scrivere agevolmente le proprie funzioni, allo scopo sia di automatizzare compiti ripetitivi, sia di sviluppare nuove funzionalità per il framework.
R è un progetto GNU4, il che significa che appartiene alla famiglia del Free Software ed è quindi un prodotto su cui l’utente gode delle seguenti libertà:
Ci troviamo quindi al di là del concetto di Open Source, tipologia legata alla modalità di distribuzione (aperta) di un prodotto nel mercato. Il Free Software è portatore invece di un senso che va oltre il mercato, e che tipicamente viene collegato allo spirito di condivisione proprio delle comunità di hackers. All’interno del mondo Open Source trovano spazio anche “componenti e passaggi closed (comunità ristrette e chiuse, in alcuni casi anche software proprietario)” [Ippolita, 2005, p. 44]; nel mondo della Free Software Foundation tutti i passaggi, i gruppi ed il codice devono essere invece rigorosamente open.
Il framework statistico R è distribuito sotto la GNU General Public Licence (GPL). Questo tipo di licenza fu progettata da Richard Stallman allo scopo di evitare che il software prodotto liberamente “finisse per essere utilizzato in progetti commerciali e proprietari, o ancor peggio, in progetti eticamente discutibili” [Ippolita, 2005, p. 47]. Questo fa della GPL un vero e proprio manifesto etico e filosofico, oltre che giuridico: la libertà di cui si fa portatrice è una libertà “virale”, che rende inalienabili le libertà del software indipendentemente dalle modifiche che l’utente apporterà al medesimo, le quali dovranno necessariamente anch’esse ricadere sotto la medesima licenza d’uso. L’eventualità che possa nascere software commerciale attraverso la manipolazione di software libero viene in questo modo definitivamente scongiurata.
La distinzione tra il movimento del Free Software e quello dell’Open Source, lo ribadiamo, è profonda. Sebbene in entrambi i casi l’obiettivo sia quello di garantire l’accesso ai sorgenti, solo il modello Open Source può essere considerato un modello di business, poiché solo quest’ultimo considera il mercato un obiettivo: dal software Open Source possono nascere progetti commerciali.
“Free Software e Open Source sono entrambe componenti di peso delle comunità hackers, e la seconda è un’evoluzione della prima, ma non necessariamente un miglioramento” [Ippolita, 2005, p. 50].
La GPL è quindi non solo un mezzo, ma anche una riflessione, una presa di posizione etica, come sottolineano gli autori collettivi del wiki “open non è free” su www.ippolita.net.
Raymond, nel suo The Cathedral and the Bazaar, offre una visione spostata a favore del modello Open Source: la sostanziale differenza per lui tra commerciale e libero risiede nella modalità di produzione, una modalità in cui l’aspetto sociale prende decisamente il sopravvento su quello tecnico. La sua metafora esplicativa è costruita sulla cattedrale, dove un gruppo di soggetti decide il progetto e lo realizza, mentre tutti gli altri vedranno soltanto il risultato finale; e sul bazaar, dove l’ideazione è aperta a tutti, e dove tutti possono testare in corso d’opera miglioramenti o eventuali falle progettuali, al fine di contribuire con le proprie idee a migliorare il prodotto. Raymond definisce il modello del Bazaar come modello Open Source - da sottolineare che il pamphlet è datato 1997. Il modello Free Software è da Raymond accomunato al modello della Cattedrale, al pari di quello commerciale, a causa della mancata dimensione sociale in fase di progettazione. Pur non trovandoci d’accordo con questa classificazione del modello Free Software, nella quale secondo noi la dimensione sociale è ben presente, così come il decentramento organizzativo e la possibilità di collaborazione tra ricercatori5, concordiamo con Raymond nell’osservare che:
“una possibile allegoria per il modello [Free Software - nella nostra interpretazione] è [...] rappresentata dall’accademia; [...] gli scienziati mettono a disposizione liberamente il loro lavoro affinché gli altri lo usino, lo testino e lo sviluppino ulteriormente. La loro ricerca è basata sull’idea di un processo aperto e autoregolato. Questa idea di autoregolazione è stata descritta da Robert Merton come una pietra miliare dell’etica scientifica, con importanza uguale a quella rappresentata dal concetto di apertura. L’ha chiamata scetticismo sistematico” [Himanen, 2001, p. 59].
L’etica scientifica, per produrre conoscenza, richiede modelli collettivi di sviluppo delle teorie, così come di revisione critica delle stesse. Il Free Software è la traduzione in ambito informatico di questa latenza platonica all’uso del dialogo critico come strumento di conoscenza, mirato allo sviluppo, in questo caso, di codice sempre più performante.
Continuando con altre analogie, Pekka Himanen paragona il software proprietario al monastero, modello chiuso “che non soltanto blocca le informazioni, ma è in sé anche autoritario” [Himanen, 2001, p. 60]. Un piccolo gruppo decide gli obiettivi, ed affida ad un altro piccolo gruppo il compito di implementare il software. Agli utenti non resta che fruire dei risultati, senza possibilità di contribuire né alla fase di implementazione, né tantomeno nella fase di definizione di nuovi obiettivi: si tratterebbe di comportamenti non autorizzati. In contrasto con questo modo di concepire lo sviluppo del software e quindi la circolazione dell’informazione, Levy sottolinea come “un libero scambio di informazioni, soprattutto quando l’informazione ha l’aspetto di un programma per computer, promuova una maggiore creatività complessiva” [Levy, 2002, p. 35]: cosa ne sarebbe infatti di un computer in cui le varie componenti non potessero scambiare informazioni tra loro? Dal punto di vista hacker, qualsiasi sistema trae beneficio dal libero flusso dell’informazione al suo interno.
Il concetto di bene comune porta con sé il problema di quale senso dare al concetto di proprietà nell’ambito del pensiero scientifico: “il bene comune è una risorsa la cui proprietà appartiene all’intera comunità, e che può essere utilizzata da tutti senza che si renda obbligatorio chiedere un permesso particolare a qualcuno” [Laser, 2005, p. 113]. La ricerca scientifica produce beni collettivi e pubblici, spesso finanziati con denaro pubblico, e dovrebbe per questo evitare vincoli di mercato. Tuttavia, spesso è proprio il mercato ad approvvigionare di fondi la ricerca, specialmente quella diretta alla produzione di risultati brevettabili, ovvero tecnologicamente implementabili.
Il sapere scientifico è un bene comune, ma un bene comune particolare: non è limitato, non rischiamo di esaurirlo. A patto che lo si lasci libero di circolare. Siamo quindi propensi, pur dal punto di vista di un impegno concreto all’interno di una comunità dedita allo sviluppo di software libero, a credere che non sia necessario abbracciare un paradigma cercando di adattare il mondo ad esso, ma che sia possibile, lungo l’asse delle licenze Creative Commons, spostarsi da una posizione Free Software ad una Open Source in relazione all’oggetto con cui ci confrontiamo. Riteniamo che una conoscenza scientifica frutto di ricerca pura debba necessariamente rientrare nei canoni del Free Software (ed è per questo motivo che adottiamo il framework R), e che quindi debba essere liberamente condivisa in quanto bene comune, mentre per ricerche più orientate all’applicazione pratica sia possibile servirsi delle modalità business-oriented dell’Open Source.
Il motivo che ci spinge ad usare Free Software per fare ricerca deriva anche da una considerazione pratica: il ricercatore per definizione ha come obiettivo di investigare la frontiera del conosciuto, possibilmente spostandola un po’ più in avanti, nel tentativo di produrre nuove teorie sul mondo, o di affinare le vecchie. Sempre più spesso la ricerca è computer-aided, tanto da poter dire che il computer fa parte integrante del metodo; proprio per questo motivo il ricercatore deve avere la possibilità di conoscere quello che il computer fa in ogni singolo passaggio, e non semplicemente fidarsi di lui (ovvero dei programmatori che hanno implementato algoritmi blindati). Ciò non vuol dire che un sociologo debba diventare un programmatore: ma semplicemente che, qualora egli volesse investigare il perché di alcuni risultati prodotti dal computer, dovrebbe avere la libertà di farlo controllando gli algoritmi. Vedersi negato questo passaggio risulta a nostro avviso una limitazione stringente. A livello di analisi strutturale, questo problema resta sullo sfondo, si tratta di algoritmi semplici e unanimemente condivisi. Ma quando si tratta di complicati, ed in parte ermetici, modelli esplicativi di Social Network Analysis avanzata ci troviamo di fronte ad una situazione diversa. Entriamo in un ambito in cui i modelli sono spesso problematici nella loro interpretazione, in cui ogni gruppo di ricerca ne sviluppa di propri, ed in cui spesso ad ognuno di essi corrisponde un pacchetto software “blindato” a disposizione della comunità per l’analisi dei dati. In questi contesti il ricercatore si fida dell’implementazione degli ideatori dei vari modelli. Se ottenesse risultati di incerta interpretazione, non avrebbe altra scelta che chiedere chiarimenti ai progettisti. Non avrebbe modo di verificare, in autonomia, il perché di tali risultati, risultati che, non dobbiamo dimenticare, saranno alla base di successive speculazioni teoriche. Crediamo quindi che il software libero, nella ricerca, sia di fondamentale importanza per diffondere la conoscenza.
Il modello di apprendimento legato all’utilizzo di software libero si lega profondamente con quello di apprendimento proprio dell’etica hacker: per questi ultimi infatti il modello generale di riferimento consiste nell’insegnare agli altri quello che si è appreso, attraverso discussioni continue che si sviluppano all’interno di comunità virtuali e che restano per così dire “agli atti”, reperibili, attraverso i motori di ricerca, sui vari server: “la ricompensa per la partecipazione a questa discussione è il riconoscimento dei pari”, come sottolinea Himanen [Himanen, 2001, p. 63].
All’interno di R, i limiti maggiori sono quelli dettati dalla propria creatività. Ognuno è infatti libero di sviluppare il proprio pacchetto a partire da quello che gli altri hanno già scritto. In una battuta, spesso usata, “non è necessario ogni volta reinventare la ruota”. Generalmente un progetto software è formato da un elemento innovativo (l’idea da implementare), e da molti elementi accessori (non innovativi ma necessari alla realizzazione dell’idea); il ricercatore che si muove nel mondo del software libero ha la possibilità di concentrarsi direttamente sullo sviluppo della propria idea, usando le “ruote” costruite dagli altri ricercatori per implementare le parti accessorie. è questa una delle ragioni per cui “il software libero è una formidabile dimostrazione che l’innovazione tecnologica può prosperare anche in assenza di proprietà intellettuale” [Laser, 2005, p. 64].
Il numero di pacchetti per R già sviluppati da ricercatori afferenti alle più svariate discipline è molto ampio, come abbiamo detto. Grazie a questa consistente partecipazione, possiamo accedere ad una estesa rete di supporto attraverso mailing list (R-announce, R-packages, R-help, R-devel), newsletter, tutorials, documentazione ed esempi tutti prodotti dalla comunità scientifica che usa e/o sviluppa il framework. In particolare le mailing lists di R rappresentano luoghi privilegiati, teatro delle operazioni più rappresentative ed interessanti della vita della comunità:
“Queste liste, che durano spesso a lungo, generando dozzine di email al giorno, producono una comunità virtuale critica e ricca di informazioni nella quale e attraverso la quale i lavori vengono realizzati, distribuiti e discussi”[Lovink, 2004, p. 28].
La liberalità e gratuità dell’aiuto è conditio sine qua non della sua stessa esistenza.
R è supportato dalla R Foundation, organizzazione non-profit fondata dall’R Development Core Team al fine di:
Tra gli obiettivi della R Foundation sono da annoverare l’esplorazione di nuove metodologie statistiche, l’organizzazione di corsi di aggiornamento su R e di conferenze legate alla discussione di procedure statistiche computerizzate.
R è un ambiente software integrato all’interno del quale è possibile manipolare dati, eseguire operazioni di calcolo e disegnare i grafici dei risultati così ottenuti. Dentro R troviamo:
Data quest’ultima caratteristica, secondo Wikipedia possiamo considerare R non solo un ambiente, ma un vero e proprio framework di lavoro: “In software development, a framework is a defined support structure in which another software project can be organized and developed. A framework may include support programs, code libraries, a scripting language, or other software to help develop and glue together the different components of a software project [...]”6, visto che al suo interno possiamo sviluppare il nostro personale progetto software (che ricadrà ovviamente anch’esso sotto la licenza GPL).
Poiché si tratta di un framework, e non semplicemente di un programma, una delle caratteristiche che possiede è la flessibilità: spesso è la creatività del ricercatore, non limitata dall’immaginazione del software designer, a decidere la soluzione per un determinato problema, mentre l’ambiente offre molti percorsi alternativi per giungere ad una stessa soluzione, sia in termini di funzioni, che di approccio ai dati, che di analisi degli stessi. Dentro R i percorsi obbligati, tipici di molti programmi, sono davvero ridotti al minimo, ed il ricercatore ha sempre gli strumenti per personalizzare al massimo le proprie routine, dove altri software statistici spesso posseggono solo parti molto specifiche e poco flessibili.
Dal punto di vista della gestione del flusso di lavoro, evidenziamo ancora un approccio alternativo rispetto a blasonati concorrenti come SPSS o SAS: mentre questi ultimi sono prodighi di risultati a video alla fine di una analisi, R (ma anche S) riducono la visualizzazione automatica dei risultati al minimo, salvandoli però in oggetti residenti in memoria, che possono poi essere sia stampati (a video o su carta), sia utilizzati da altre funzioni, automatizzando così molti processi ed evitando reimmissioni di dati. In questo modo è possibile programmare loops, ovvero cicli condizionali, per analizzare in una fase successiva più dataset contemporaneamente, magari combinando insieme diverse funzioni in un’unica routine per produrre analisi più complesse. La gestione dei risultati in oggetti residenti in memoria, caratteristica che ripetiamo distingue R da altri software per l’analisi statistica, è rappresentata in figura 1.1.
Tutte queste features del framework R rappresentano certamente un vantaggio sul lungo periodo, ma si traducono, nella fase di approccio, in una curva di apprendimento decisamente più ripida.
R viene correntemente sviluppato per i sistemi operativi Windows, Linux e Macintosh e la portabilità del codice dei pacchetti è garantita dalla possibilità di distribuire i propri programmi in versione sorgente, lasciando ad R l’onere di compilarli automaticamente in relazione alla macchina su cui è installato. Per ogni tipologia di lavoro è stato creato un pacchetto software idoneo; ogni pacchetto ha un nome, un numero progressivo che ne identifica la versione, ed un set di funzioni che rappresenta quello che il pacchetto è in grado di fare. Essi sono disponibili in repository come il CRAN o BioConductor, per citare i più famosi, e sono veramente moltissimi, a dimostrazione della varietà di compiti che possiamo adempiere usando il framework.
R è quindi un ambiente all’interno del quale possiamo gestire, analizzare dati e scrivere report. Ogni comando che esso ci permette di eseguire è contenuto in un pacchetto: ogni pacchetto ha un nome, delle funzioni con un comando associato per richiamarle, e un numero che ci rende nota la versione.
Insieme ad R vengono di default installati i seguenti pacchetti:
Ad ogni pacchetto corrispondono, abbiamo detto, precise funzioni. La somma di queste funzioni rappresenta quello che possiamo fare con R all’avvio. Tuttavia, possiamo installare un gran numero di pacchetti aggiuntivi semplicemente scaricandoli dal sito CRAN (Comprehensive R Archive Network, raggiungibile da www.r-project.org selezionando il mirror a noi più vicino) ed installandoli sul proprio sistema. Ad ogni pacchetto installato, corrisponderanno nuove funzioni per il framework e nuove possibilità per soddisfare le nostre esigenze di ricerca. Come facilmente si evince dal numero dei pacchetti installabili, ci troviamo ben oltre quello che un normale software statistico può offrire.
La maggior parte delle istruzioni per far funzionare R viene passata manualmente attraverso la console, una sorta di finestra che ricorda il terminale di bash o il prompt del DOS, interfacce a prima vista obsolete e “difficili” (vedi figura 1.2).
“L’interfaccia è il modo in cui si fa qualcosa con uno strumento: le azioni che dobbiamo eseguire e il modo in cui lo strumento risponde” [Raskin, 2003, p. 2]. Così Jef Raskin, già curatore del progetto Macintosh per Apple, definisce questo concetto. In questo senso, egli intende tutto quello che si pone tra la nostra volontà e la macchina che eseguirà i passi necessari per realizzarla: tutto quello che è, appunto, interfaccia. In ambito informatico, il termine si riferisce non solo alle ormai affermatissime GUI, ovvero Graphical User Interface, ma anche alle più datate Command Line Interface, ovvero le interfacce a linea di comando, le cosiddette shell o terminali degli ambienti -nix, così come alle interfacce vocali.
Generalmente, tendiamo a dividere la popolazione degli utenti di software in due classi, i principianti e gli esperti, e tendiamo altresì a ritenere che solo questi ultimi abbiano sufficiente padronanza degli strumenti da essere in grado di usare interfacce spartane come quelle a linea di comando. Raskin contesta questo approccio dicotomico:
“[...] l’utente di un sistema complesso non è nè un principiante nè un esperto, e nemmeno lo si può collocare lungo la scala di valori fra questi due estremi. L’utente conosce o ignora singolarmente ciascuna funzionalità del sistema, o ciascun gruppo di funzionalità che si usano in modo simile. Un utente può conoscere molti comandi e funzionalità di un dato software [...], eppure lo stesso utente può non sapere come usare altri comandi o intere categorie di comandi e funzionalità di quello stesso software, e può addirittura ignorare che esistano” [Raskin, 2003, p. 77-78].
Sebbene sia a nostro avviso opinabile l’uso che in questo contesto Raskin fa della locuzione “sistema complesso” (a nostro avviso “sistema complicato” risulterebbe più adeguato), ci troviamo d’accordo con lui: R è un ambiente talmente vasto, mutevole e dinamico che non è pensabile conoscerlo tutto; possiamo solo impararne i rudimenti e poi approfondire le funzioni che ci interessano di più o che ci sono più utili. In pratica, possiamo diventare esperti di un pacchetto, e restare principianti di tutti gli altri. In questo senso, abbandonando la dicotomia principiante/esperto, proviamo a pensare le interfacce a linea di comando come a delle versioni nude delle più comuni GUI.
RCommander (in sigla RCmdr), uno dei molti pacchetti installabili, facilita l’utilizzo delle funzioni statistiche di R, offrendo una interfaccia grafica che evita all’utente inesperto il ricorso alla linea di comando (sebbene questa sia presente) in favore di comodi pulsanti da cliccare.
John Fox, autore del pacchetto e professore di Statistica presso la McMaster University in Ontario, Canada, precisa che le interfacce a linea di comando sono infinitamente preferibili alle GUI, proprio per la libertà che garantiscono all’utente: le finalità didattiche del suo progetto sono allora evidenti, dato che una volta cliccato il pulsante relativo alla funzione desiderata (esattamente come faremmo con qualsiasi altro software), mentre in una finestra di output comparirà il risultato, in una finestra di script comparirà il comando per esteso che, inserito da linea di comando, avrebbe offerto lo stesso risultato del pulsante. Il pulsante quindi non è altro che un modo sicuro per l’utente per comporre il comando senza commettere errori sintattici; tuttavia la contemporanea visualizzazione del comando permette, con l’uso, di familiarizzare con il linguaggio dell’ambiente.
Questo pacchetto è inoltre stato pensato in modo da essere facilmente estendibile da parte di terzi, in modo che tutti quelli che vogliano aggiungere delle funzionalità al programma siano in grado di farlo con un minimo lavoro di programmazione, senza la necessità di ricompilare il sorgente principale. La nostra estensione7 per comandare il pacchetto sna è in fase di beta-test, e ha riscontrato l’interesse di alcuni appartenenti alla comunità di R, tra cui l’autore dello stesso pacchetto sna, il professor Carter Butts [Butts, 2005].
Veniamo quindi al pacchetto che svolge in R analisi di rete, ovvero sna (versione 1.0), sviluppato da Carter Tribley Butts, Assistant Professor al Dipartimento di Sociologia ed all’Institute for Mathematical Behavioral Sciences dell’Università della California, Irvine. Nella documentazione allegata, il pacchetto viene descritto come:
“Un insieme di strumenti per la Social Network Analysis, che include indici a livello sia di nodo che di grafo, metodi di covarianza e di distanza strutturale, di equivalenza strutturale, di modellazione p*, di generazione di grafi casuali, e di visualizzazione sia 2D che 3D”8.
All’interno di questo pacchetto troviamo funzioni che vanno dalla modellazione gerarchica bayesiana alla regressione logistica di network (QAP e CUG tests), dalle funzioni per la rappresentazione grafica delle matrici (2D e 3D), alle molte misure di centralità per l’analisi strutturale della rete. L’obiettivo di questo pacchetto, nelle parole del suo autore, è:
“generare un utile e accessoriato strumento per l’analisi di network all’interno di un consolidato ambiente statistico. Questa libreria deve essere gratuitamente fruibile attraverso il copyleft, allo scopo di facilitarne l’uso, la critica e ulteriori sviluppi da parte della comunità degli utilizzatori. La speranza è che questo strumento possa facilitare l’uso dei metodi statistici di analisi di rete per i ricercatori, e che possa aiutare l’integrazione dei metodi di network con le analisi statistiche standard”9.
Nell’attuale versione 1.0 di sna, a cui siamo lieti di aver, seppur in piccola parte, contribuito10, sono presenti le funzioni elencate in appendice a pagina §.
La maggior parte di queste funzioni richiede matrici di adiacenza per poter lavorare, come è ovvio quando si vuol conoscere le misure strutturali di un grafo. Tuttavia, alcune necessitano di input diversi, a causa della particolarità della loro funzione; in ogni caso, la documentazione allegata al pacchetto è più che sufficiente a dare, per ogni routine, tutte le informazioni necessarie. Nel prossimo capitolo illustreremo alcune delle funzioni sopra elencate, prediligendo le funzioni più usate della Social Network Analysis strutturale.
Abbiamo detto che dobbiamo scrivere i comandi direttamente nella console. I comandi che R accetta sono tutte le funzioni presenti nei vari pacchetti preinstallati con il software, così come quelle dei pacchetti che noi decideremo, in un secondo momento, di installare. Una funzione di R ha la seguente forma:
nomeFunzione(dati, parametro1=”valore”, parametro2=”valore”, ...)
Una funzione ha molti parametri, come vediamo, e ricordarseli tutti è complicato. Fortunatamente, non abbiamo necessità di farlo dato che vengono passati automaticamente anche se scriviamo semplicemente:
nomefunzione(dati)
Quando è necessario richiamarli allora? Semplicemente quando le impostazioni di default non ci vanno bene e vogliamo cambiarle (ma non accadrà molto spesso).
Una funzione di solito esegue un’azione che produce un output; questo viene scritto automaticamente nella console, proprio sotto quello che abbiamo digitato noi. Ma certo in questo modo l’output non è più manipolabile, viene solo scritto a video. Possiamo costruire un oggetto che verrà invece conservato in memoria tramite l’operatore < -, in questo modo:
Facciamo un esempio per capire meglio, immaginando di chiedere ad R di costruire un oggetto vettore chiamato mioPrimoVettore e di riempirlo con i numeri da 1 a 10:
> mioPrimoVettore <- array(1:10)
Premendo Invio R eseguirà il compito. Sebbene non si veda nulla a video, qualcosa è successo. Proviamo ad usare il comando ls(), il quale chiede ad R di visualizzare tutti gli oggetti in memoria (ovvero tutti gli oggetti presenti nel workspace):
> ls()
[1] mioPrimoVettore
Il nostro primo oggetto è stato dunque creato. Proviamo ora a scriverne in console il nome e a premere Invio; R ne visualizzerà il contenuto:
> mioPrimoVettore
[1] 1 2 3 4 5 6 7 8 9 10
La funzione che abbiamo usato, contenuta nel pacchetto base preinstallato nel sistema, è:
Come precedentemente osservato, non è stato necessario modificare alcun parametro, quindi non abbiamo avuto necessità di riscriverli ed R ha usato le impostazioni di default per questa funzione.
Qualora avessimo dubbi sui parametri di una funzione, sarà sufficiente scrivere in console:
> ?nomeFunzione
oppure
> ?nomePacchetto
In questo modo si aprirà una finestra di aiuto con tutti i dettagli della funzione o con le informazioni sul pacchetto richiesto, come in figura 1.3.
Qualora non si conoscesse il nome della funzione di cui abbiamo bisogno, ma solamente quello che vorremmo che lei facesse, potremo usare il comando:
In questo modo R invierà al sito http://search.r-project.org la nostra richiesta, e presenterà il risultato nel nostro browser elencando tutte volte in cui nella mailing-list si discutono soluzioni al nostro problema. Per sfruttare le capacità di questa interessante funzione dobbiamo, necessariamente, disporre di una connessione internet e formulare la nostra richiesta in inglese, lingua ufficiale di tutte le mailing-list di supporto.
Una matrice può essere pensata come una tabella bidimensionale formata da righe e colonne. Ogni valore contenuto al suo interno è individuato univocamente da una coppia di indici numerici i cui valori sono compresi tra 1 ed il numero totale o di righe o di colonne. L’uso opportuno degli indici ci permette di trattare le singole righe o le singole colonne che formano la matrice come dei vettori.
Riprendendo un esempio da Crivellari [Crivellari, 2006, p. 66], scriviamo:
> mat<-matrix(1:18, ncol=6)
> mat
[, 1] | [, 2] | [, 3] | [, 4] | [, 5] | [, 6] |
|
[1, ] | 1 | 4 | 7 | 10 | 13 | 16 |
[2, ] | 2 | 5 | 8 | 11 | 14 | 17 |
[3, ] | 3 | 6 | 9 | 12 | 15 | 18 |
Abbiamo usato la funzione matrix(), del pacchetto preinstallato base, per costruire una matrice di sei colonne, riempita con i numeri da 1 a 18. Automaticamente R ha prodotto le tre righe necessarie per conservare 18 numeri in 6 colonne. In caso avessimo passato un insieme di numeri diverso da un multiplo di 6 (come i numeri da 1 a 17), R ci avrebbe restituito un errore, in quanto non avrebbe potuto riempire tutte le colonne.
Vediamo come manipolare i dati di una matrice:
> mat[,2] # prende l’intero contenuto della seconda colonna
[1] 4 5 6
> mat[3,] # prende l’intero contenuto della terza riga
[1] 3 6 9 12 15 18
> mat[2,4] # prende il valore all’incrocio tra la 2a riga e la 4a colonna
[1] 11
Altri parametri da usare con questa funzione sono nrow = n e byrow = F. Con il primo possiamo specificare il numero di righe che vogliamo creare nella matrice; con il secondo invece indichiamo il verso del riempimento della matrice, ovvero se per colonna (default) o per riga:
> mat2<-matrix(1:18, ncol=6, byrow=T)
> mat2
[, 1] | [, 2] | [, 3] | [, 4] | [, 5] | [, 6] |
|
[1, ] | 1 | 2 | 3 | 4 | 5 | 6 |
[2, ] | 7 | 8 | 9 | 10 | 11 | 12 |
[3, ] | 13 | 14 | 15 | 16 | 17 | 18 |
Come possiamo osservare, in questa seconda matrice la disposizione dei numeri da 1 a 18 è diversa rispetto alla precedente matrice, avendo questa volta richiesto un ordinamento per riga.
Vediamo brevemente altre funzioni che possono risultare utili quando si lavora con le matrici:
> dim(mat) # vettore con le dimensioni della matrice
[1] 3 6
> length(mat) # numero totale degli elementi in matrice
[1] 18
> nrow(mat) # numero di righe della matrice
[1] 3
> ncol(mat) # numero di colonne della matrice
[1] 6
> diag(mat) # diagonale principale di una matrice
[1] 1 5 9
Una matrice è quindi una tabella (di valori numerici) di dimensioni m×n, formata cioè da m righe e n colonne. Seguendo Crivellari [Crivellari, 2006, p. 76-77], possiamo identificare alcuni casi particolari in relazione alle dimensioni ed ai valori contenuti in una matrice:
La matrice principalmente usata nell’analisi delle reti sociali è la matrice di adiacenza, o sociomatrice. I teorici dei grafi chiamano questa matrice di adiacenza perché, semplicemente, le sue entrate indicano se due nodi sono adiacenti o meno tra loro, ovvero se tra i due nodi esiste una relazione.
Una sociomatrice, rispetto alla classificazione precedente, è una matrice quadrata di dimensione g ×g (g righe e g colonne): c’è quindi una riga ed una colonna per ogni vertice, e le righe e le colonne sono etichettate 1, 2,..., g. Righe e colonne etichettano i nodi del grafo nello stesso ordine; questo significa che la prima riga e la prima colonna della matrice conterranno entrambe lo stesso nodo e così via fino ad esaurire la totalità dei nodi.
Le entrate della sociomatrice, indicate con xij, registrano, come abbiamo detto, se due nodi sono adiacenti. Riportiamo un esempio da Wasserman [Wasserman et al., 1998, p. 151]:
n1 | n2 | n3 | n4 | n5 | n6 |
|
n1 | - | 0 | 0 | 0 | 1 | 1 |
n2 | 0 | - | 1 | 0 | 0 | 0 |
n3 | 0 | 1 | - | 0 | 0 | 0 |
n4 | 0 | 0 | 0 | - | 1 | 1 |
n5 | 1 | 0 | 0 | 1 | - | 1 |
n6 | 1 | 0 | 0 | 1 | 1 | - |
Nella sociomatrice abbiamo un 1 se esiste una relazione tra ni e nj, e uno 0 viceversa.
Nel caso di un grafo completo, la relativa sociomatrice conterrà un 1 in ogni entrata, ad esclusione della diagonale, i cui valori rappresentano la relazione di un nodo con se stesso, (altrimenti detti loop), ma che non vengono mai presi in considerazione per grafi semplici.
Nel caso allora di un grafo semplice, ovvero relativo ad una tipologia di relazione non direzionata, la sociomatrice sarà non solo quadrata, ma anche simmetrica (possiamo confrontare, nell’esempio sopra riportato, il triangolo inferiore e quello superiore della matrice rispetto alla diagonale principale).
Nel caso invece di un grafo direzionato l’entrata (i,j)esima della matrice conterrà un 1 se l’attore rappresentato nella riga ni avrà scelto l’attore rappresentato dalla colonna nj; tuttavia, poiché la scelta del primo attore è sostanzialmente differente da quella del secondo attore, non è detto che anche l’entrata (j,i)esima conterrà un 1. In altre parole, se l’attore i sceglie l’attore j, ma j non reciproca, avremo xij = 1 e xji = 0.
Possiamo pensare allora alla matrice simmetrica come ad un caso particolare di matrice quadrata. Ma non dobbiamo pensare al grafo semplice come ad un caso particolare di grafo direzionato simmetrico.
La differenza è di ordine logico: nel primo caso, il grafo semplice, non ha alcun senso - in relazione alla tipologia relazionale che esso rappresenta - parlare di “verso” o “direzione” della relazione; nel secondo caso, il grafo direzionato, il verso o la direzione sono invece importanti - sempre in relazione alla tipologia relazionale rappresentata - e qualora un grafo direzionato risulti simmetrico, noi ne trarremmo importanti informazioni in merito ai livelli di reciprocità o mutualità presenti.
Il pacchetto sna è dotato di alcune funzioni per produrre matrici di adiacenza secondo determinate condizioni scelte dal ricercatore. rgraph() ci permette di generare matrici di adiacenza casuali secondo una distribuzione del grafo Bernoulliana, ovvero dove ogni casella della matrice è indipendentemente dalle altre una prova Bernoulliana condizionata sul valore dell’argomento tprob [Butts, 2005, sna-manual.1.0-0 p. 147]. Il valore della variabile tprob è interpretato da R in diversi modi:
rguman() genera grafi da una distribuzione casuale uniforme condizionata sul dyad census scelto dal ricercatore [Butts, 2005, sna-manual.1.0-0 p. 149], mentre rgnm() genera grafi da una distribuzione casuale uniforme condizionata sulla densità, scelta ancora una volta dal ricercatore [Butts, 2005, sna-manual.1.0-0 p. 146].
Useremo la prima di queste funzioni per creare la matrice che analizzeremo nel corso di questo breve lavoro di presentazione. Dopo aver installato R e sna, carichiamo il pacchetto in memoria scrivendo in console l’istruzione:
> library(sna)
A questo punto dobbiamo semplicemente chiamare la funzione che ci serve scrivendo, sempre nella console:
> g<-rgraph(4, tprob=0.3, mode=”graph”)
Così facendo otteniamo un grafo semplice rappresentato da una matrice di adiacenza simmetrica secondo le specifiche passate alla funzione rgraph(), conservata nell’oggetto g (nome scelto per semplicità, e può ovviamente essere diverso), che passeremo alle successive funzioni per le prossime fasi di analisi.
Il parametro mode è quello che ci permette di specificare se vogliamo un grafo semplice (graph) o un grafo direzionato (digraph), mentre il parametro tprob si riferisce, come già specificato, alla probabilità bernoulliana di trovare archi nel grafo.
Ecco i dati generati:
> g
[1] | [2] | [3] | [4] |
|
[1] | 0 | 1 | 0 | 1 |
[2] | 1 | 0 | 0 | 0 |
[3] | 0 | 0 | 0 | 0 |
[4] | 1 | 0 | 0 | 0 |
Naturalmente in questo modo sna creerà per noi delle matrici casuali. Ma se noi avessimo bisogno di creare matrici ad esempio digitalizzando i dati di un set di questionari, e quindi avessimo necessità di specificare uno per uno i valori di tutte le entrate?
Facciamo un esempio con un grafo semplice che rappresenta la relazione “si conoscono” e contenente quattro soggetti (A, B, C, D), dove l’unico soggetto che non conosce nessuno (vertice isolato) è D. Dovendo digitalizzare i dati relativi a questo grafo, procederemo per prima cosa creando una sociomatrice delle dimensioni volute, riempiendone le entrate con degli 0:
> h <- rgraph(4, tprob=0)
> h
[, 1] | [, 2] | [, 3] | [, 4] |
|
[1, ] | 0 | 0 | 0 | 0 |
[2, ] | 0 | 0 | 0 | 0 |
[3, ] | 0 | 0 | 0 | 0 |
[4, ] | 0 | 0 | 0 | 0 |
Ovviamente questa è la strada che riteniamo più veloce, ma sarebbe altrettanto possibile usare1:
> x <- 4
> h <- matrix(0, ncol=x, nrow=x)
> h
[, 1] | [, 2] | [, 3] | [, 4] |
|
[1, ] | 0 | 0 | 0 | 0 |
[2, ] | 0 | 0 | 0 | 0 |
[3, ] | 0 | 0 | 0 | 0 |
[4, ] | 0 | 0 | 0 | 0 |
Una volta creato l’oggetto h, scegliamo poi Workspace -> Esplora Workspace. Si aprirà una finestra che elenca tutti gli oggetti disponibili nel nostro workspace, ovvero nella memoria di R. Scegliamo l’oggetto appena creato, h, e apriamolo facendo un click sulla lente di ingrandimento della finestra (vedi figura 2.1).
Si aprirà a questo punto una finestra come in figura 2.2, dalla quale potremo modificare l’oggetto inserendo gli 1 mancanti in modo da rispecchiare i dati dell’esempio. Una volta modificata la sociomatrice, basterà chiudere la finestra per salvare automaticamente le modifiche.
Da questo momento in poi, l’oggetto h risiederà in memoria fino a che ci sarà utile; nel linguaggio di R, diremo che esso risiederà nel workspace.
Nella precedente sezione abbiamo imparato a creare una matrice ex-novo sia usando la funzione matrix() del pacchetto base che usando le funzioni del pacchetto sna esplicitamente dedicate alla creazione di matrici di adiacenza (o sociomatrici che dir si voglia).
Tuttavia, potrebbe capitarci di non avere tanto la necessità di creare delle matrici nuove, quanto di importarne di già pronte, create con altri programmi. R è in grado di leggere, principalmente attraverso il pacchetto base ed il pacchetto foreign, un gran numero di file proprietari di specifiche applicazioni, come SAS, SPSS, Excel, Pajek. Le tipologie più comuni di file con le quali potremmo avere a che fare in ambito di Social Network Analysis sono:
Ognuno di questi formati necessita di regole diverse per essere interpretato; in R troviamo le funzioni in grado di aiutarci. Cominciamo con il caso più semplice, ovvero un file di testo (.txt). Una matrice di adiacenza in questo tipo di file è codificata come:
tab tab c1 tab c2 tab c3 tab c4
r1 tab 0 tab 1 tab 0 tab 1
r2 tab 1 tab 0 tab 0 tab 0
r3 tab 0 tab 0 tab 0 tab 0
r4 tab 1 tab 0 tab 0 tab 0
La funzione read.table() è in grado di interpretare le tabulazioni tab come “confini” tra un elemento e l’altro e di ricostruire un oggetto corrispondente in R:
> a<-read.table(“table.txt", header=T)
> a
1 | 2 | 3 | 4 |
|
1 | 0 | 1 | 0 | 1 |
2 | 1 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 |
4 | 1 | 0 | 0 | 0 |
Abbiamo quindi detto alla funzione di andare a leggere il contenuto del file “table.txt”. Una breve precisazione: possiamo chiamare il file direttamente poiché esso risiede nella stessa cartella di lavoro (working directory) che abbiamo scelto per lavorare2; qualora risiedesse in un altro luogo, dovremmo comunicare ad R il percorso esatto (path) dove andarlo a cercare.
A questo punto però l’oggetto prodotto è un dataframe3, mentre a noi, per poter lavorare con le funzioni di sna, occorre avere un oggetto matrice; procediamo dunque alla conversione:
Un altro tipico formato di file è il cosiddetto comma separated values (.csv) dove righe, colonne e valori sono, appunto, separati da un segno di interpunzione come la virgola, il punto e virgola o altro invece che da un semplice spazio, come accadeva per il precedente tipo di file. In questo caso useremo la funzione read.csv(); essa non è nella sostanza differente dalla precedente (ed è logico che non lo sia, visto che stiamo sempre trattando file testuali, che differiscono per alcuni aspetti ma sono in fondo di identica natura), tuttavia di default incorpora dei parametri utili per questo particolare tipo di file:
> a <- read.csv(“table.csv”, sep=”;”)
> a
1 | 2 | 3 | 4 |
|
1 | 0 | 1 | 0 | 1 |
2 | 1 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 |
4 | 1 | 0 | 0 | 0 |
Ultimo caso che visioniamo è il file di Excel. Va ricordato che è possibile da Excel esportare un file in formato .csv, in modo da utilizzare la precedente funzione per caricare poi i dati in memoria. Esiste tuttavia il modo di leggere direttamente i file .xls senza preventive codifiche. Abbiamo a questo scopo bisogno della funzione read.xls() contenuta nel pacchetto gdata:
> library(gdata)
> a <- read.xls(“table.xls”, sheet=1)
Abbiamo letto i dati contenuti nel primo foglio del file “table.xls”. Anche questa volta però l’oggetto creato è un dataframe, mentre a noi, ormai lo sappiamo, serve una matrice. Procediamo dunque alla conversione:
> a <- as.matrix(a)
> is.matrix(a)
[1] TRUE
Ovviamente il secondo comando viene in questa sede utilizzato per dimostrare l’avvenuta conversione, e non c’è nessuna necessità di replicarlo ogni volta che si chiede di convertire un oggetto in un altro.
Nel libro di Crivellari [Crivellari, 2006, p. 192-3] viene suggerito un ulteriore metodo per leggere dati da un file Excel contenente più fogli al proprio interno, previa connessione ad un database via driver ODBC: rimandiamo alla consultazione del testo per i dettagli tecnici.
Gli oggetti creati fino ad ora risiedono nella memoria assegnata ad R, in quello che viene chiamato il workspace, ovvero lo “spazio di lavoro”. è uno spazio prezioso, sia perché costruire i vari oggetti ha richiesto tempo, sia perché non è detto che in una sola sessione di lavoro si sia in grado di portare a termine tutte le fasi di analisi programmate. Ecco perchè è importante avere la possibilità di salvarlo, sia per sessioni future, che per non perdere il lavoro di inserimento dei dati. Scegliamo quindi dal menu Workspace -> Salva File Workspace in modo da far scrivere il contenuto del workspace su file. Abbiamo a disposizione anche un’altra strada, usare Workspace -> Salva Workspace: il risultato è esattamente lo stesso, tranne che in questo caso il file prodotto viene chiamato automaticamente .RData, sarà invisibile ai nostri occhi (a meno di non accedervi attraverso un terminale) e sarà caricato automaticamente al prossimo avvio di R.
Certo in questo modo il contenuto della memoria di R, tutte le matrici che abbiamo creato, è al sicuro in un file; ma questa soluzione ci penalizza nel caso in cui ci sia la necessità di scambiare i dati così creati con altri ricercatori i quali, a meno di non utilizzare lo stesso programma, non sarebbero in grado di leggere i file .RData.
è opportuno allora cercare una soluzione idonea al problema della condivisione dei dati. Notato qualcosa di stonato? In effetti, parlare di “problema della condivisione” non è un atteggiamento molto Free Software: condividere è conseguenza di collaborare, ed è quindi una cosa positiva, che non dovrebbe preoccuparci in nessun modo. Tuttavia, siamo abituati a restare confinati dentro alcuni programmi per la paura che gli altri non possano “leggere” il nostro lavoro. Ovviamente R ci toglie questo problema, offrendoci una quantità di soluzioni diverse per poter condividere i dati con i nostri collaboratori, senza obbligare questi ultimi a parlare la sua lingua.
Il primo modo, il più immediato, consiste nello scrivere su file ogni singola matrice che abbiamo creato tramite la funzione write.table():
> write.table(g, nomefile=”scriviOggetto.txt”)
L’output prodotto avrà la forma in figura 2.3, e sarà recuperabile tramite la funzione read.table(), precedentemente esaminata.
Altro formato che possiamo usare per salvare su file la nostra matrice è il comma separated value, ovvero il .csv. Nella precedente sezione abbiamo studiato la funzione read.csv(), atta a leggere file di questo tipo; il suo reciproco per l’output si chiama write.csv(), ed anche in questo caso vale il ragionamento fatto in fase di analisi dell’input tra read.table() e read.csv(): non c’è una differenza di fondo tra write.table() e write.csv(), poiché entrambe scrivono file di testo, ma ognuna ha per default il settaggio idoneo per la tipologia di file cui si riferisce (il che esime noi dal conoscerla). Il vantaggio di usare il formato .csv è che esso è immediatamente gestibile dai fogli di calcolo come Excel. Vediamo la sintassi del comando:
> write.csv(g, “scrivoMatrice.csv”)
Il risultato è in figura 2.4. La differenza rispetto alla precedente funzione è immediata: ogni valore della matrice, righe e colonne comprese, è separato dagli altri da un segno di interpunzione (in questo caso, per dafault, è la virgola, ma abbiamo la possibilità di settarne uno a piacere attraverso il parametro sep).
Veniamo adesso all’ultimo formato di file che esaminiamo in questa sede (ma che non esaurisce il numero di formati gestibili da R), ovvero il formato Data Language (.dl). Questo tipo di file, nato per gestire dati di rete, è di conseguenza molto usato dalle applicazioni per l’analisi delle reti sociali, e sia UCINET che Pajek, due blasonati software in questo ambito, sono in grado di interpretarlo. La funzione che useremo si chiama write.dl() e, a dimostrazione di quanto appena detto, non appartiene al pacchetto base, ma al pacchetto sna, ovvero a quello che direttamente si occupa di dati relazionali. Vediamo un esempio di output, salvando sempre la nostra matrice di esempio:
> write.dl(g, “scrivoMatrice.dl”)
Il risultato in figura ci mostra una codifica molto più complessa dei dati, in grado anche di salvare informazioni sia sull’intensità delle relazioni, in caso di valued graph, che sui singoli nodi, come caratteristiche di attributo o altro.
Ricapitolando, abbiamo esaminato quattro modi diversi di salvare i nostri dati:
Nel primo caso produciamo un unico file contenente tutti gli oggetti residenti in memoria: comodo da gestire, ma i dati non saranno condivisibili. Negli altri casi, sebbene in formati diversi, creiamo tanti file quanti sono gli oggetti salvati: scomodo da gestire, ma i dati saranno accessibili a tutti.
Sarà possibile avere un metodo che accomuni facilità di gestione e possibilità di condivisione? Che ci permetta in pratica di avere un unico file da gestire, ma che i dati in questo file siano accessibili anche da altri ricercatori con altri programmi? La risposta è affermativa e si chiama Relational Data Base Management System (RDBMS).
La esamineremo in dettaglio nel Capitolo 5.
“L’analisi di rete è finalizzata alla misurazione delle caratteristiche delle strutture sociali [...]: le analisi possono includere le caratteristiche delle singole unità che compongono la rete (dati di attributo), le caratteristiche relazionali dei legami che collegano queste unità, e le caratteristiche strutturali di interi gruppi di attori” [Salvini, 2005, p. 59].
Una rete è definita come:

con V = {x1, x2, ..., xn} R = {r(x1,x2), r(x1,x3), ..., r(xn,xn-1)} R ⊆ V ×V
dove V rappresenta un insieme n di attori sociali, ed R rappresenta una relazione definita tra coppie di attori appartenenti a V (in questo caso abbiamo escluso i loop, ovvero quelle relazioni che ricadono sul soggetto che le compie, come in rx1,x1).
Le relazioni possono essere sia di tipo binario, nella forma (0, 1) - in questo caso rileviamo solamente la loro presenza o assenza; sia quantificate da un valore - in questo caso rileviamo non solo presenza o assenza della relazione R, ma anche la sua intensità.
La Social Network Analysis contemporanea studia gli attori sociali e le loro relazioni utilizzando gli strumenti della teoria matematica dei grafi. Chiesi dà una giustificazione pragmatica di tale preferenza, ovvero la presenza in tale teoria di un vocabolario ben sviluppato per definire molte delle proprietà di una rete in generale; vocabolario che offre al ricercatore non solo i concetti primitivi per lavorare con le reti, ma anche la matematica di base per poterli quantificare1. L’approccio matematico fornisce inoltre al ricercatore la possibilità di dimostrare teoremi sulla struttura del grafo. è bene precisare che detti teoremi sono validi per il modello costruito dal ricercatore, e che quindi le conseguenze di detti teoremi coinvolgono solo questo, e non necessariamente la realtà sociale. Un modello è infatti una rappresentazione idealtipica che prende in considerazione solo alcuni (quelli ritenuti più utili), e non tutti (impresa impossibile) gli elementi presenti nella realtà sociale.
Quando un grafo viene usato come modello di una struttura sociale esso prende il nome di sociogramma, i suoi vertici rappresentano gli attori sociali, mentre gli archi che li collegano rappresentano le relazioni tra gli attori. Questo tipo di rappresentazione consente talvolta di scoprire connessioni che potrebbero altrimenti passare inosservate, ad esempio usando delle matrici numeriche.
L’uso delle matrici è infatti un mezzo di codifica dei dati idoneo più all’analisi computerizzata che a quella visuale. Questi due tipi di rappresentazione non devono essere pensati come autoescludentisi, ma anzi come a due modi integrati: il primo più adatto alle capacità creative umane, il secondo più adatto alle potenzialità computazionali della macchina.
Definiamo quindi un grafo non orientato come:

i cui vertici sono rappresentati da:

e gli archi da:

Nel caso di un grafo orientato, gli archi tra ogni coppia di vertici, ad esempio b{S,K} e b{K,S} vengono contati separatamente, poiché possono avere valori differenti (vedremo le implicazioni dell’orientamento degli archi nelle misure di centralità dell’attore, specialmente in relazione al concetto di prestigio).
Definiamo adiacenti due vertici di V collegati da una arco di E:

Definiamo inoltre vicinato di un vertice v l’insieme dei vertici a questo adiacenti:

I vertici di un grafo sono quindi collegati da archi, che possono essere orientati o meno. Un grafo può contenere al suo interno uno o più sottografi. Definiamo Gs sottografo di G se:

Lo studio dei sottografi è utile per rilevare alcune proprietà del grafo: il sottografo deve possedere dette proprietà in grado massimo (ad indicare che esse cesserebbero di essere tali qualora venisse aggiunto un nuovo vertice al sottografo - questo tipo di considerazioni sono alla base dello studio delle cliques, dei clan e dei clubs). Particolari tipi di sottografi sono le diadi e le triadi, generati a partire dai vertici e considerando successivamente i legami tra questi.
Una diade rappresenta una coppia di attori ed i possibili legami tra loro. Nei grafi non orientati, la diade assume due stati: connessa o nulla, ovvero i due vertici considerati sono adiacenti oppure no. In un grafo orientato i possibili stati diventano quattro, corrispondenti alle tre classi isomorfe illustate in Figura 3.2.
Le triadi sono sottografi costituiti da tre attori e dai loro legami. In un grafo non orientato la triade può assumere quattro stati differenti, mentre in un grafo orientato gli stati possibili diventano sedici, in relazione al fatto che questa prospettiva direzionata prende in considerazione la reciprocazione delle scelte compiute dagli attori.
Due vertici, o nodi, possono essere collegati non solo mediante link diretti, ma anche mediante collegamenti indiretti, chiamati percorsi: un percorso è formato da tutti gli archi ed i nodi intermedi necessari a collegare due vertici. In un grafo orientato, un percorso è formato da archi orientati tutti nella stessa direzione. Possono esistere molti percorsi per collegare il vertice ni al vertice nj, di cui il più breve, il cosiddetto shortest path, è quello maggiormente usato in letteratura per la costruzione di indici. La lunghezza di un percorso è pari al numero di archi da cui è formato, mentre la distanza di due vertici si stima pari alla lunghezza del percorso più breve che li collega (detta anche geodetica). Da sottolineare che ciascun arco e ciascun vertice vengono contati una sola volta in ogni percorso: esso passa infatti una sola volta da ogni arco ed una sola volta da ogni vertice coinvolto.
Le tipologie di relazione che possono venire rappresentate attraverso gli strumenti dell’analisi delle reti sociali sono le più svariate. Knoke ne propone una tassonomia, ripresa in Salvini [Salvini, 2005, p. 63]:
Questa opera di classificazione è utile al ricercatore non solo per una questione di chiarezza teorica, ma anche metodologica: una volta stabilita la classe di appartenenza della relazione studiata, il ricercatore ha implicitamente individuato anche tutta una serie di misure che, meglio di altre, si adattano concettualmente alla ricerca.
”Certamente non c’è unanimità di consensi intorno a quello che esattamente si debba intendere con centralità, nè chiarezza intorno alla sua concettualizzazione teorica, e non c’è neppure molto accordo su quali siano le procedure più idonee per la sua misurazione”2.
Con queste parole Linton Freeman descriveva il dibattito teorico intorno al concetto di importanza dell’attore nel 1979. Molti progressi sono stati compiuti, da allora, nella specificazione di questo difficile concetto, così come, parallelamente, molte misure si sono consolidate nel metodo. Cos’è allora una misura di centralità oggi? Esaminiamone alcuni sviluppi teorici.
“Uno degli usi principali della teoria dei grafi nella Social Network Analysis consiste nell’identificazione degli attori più importanti di una rete sociale” [Wasserman et al., 1998, p. 169, trad. nostra]. Sono state date molte definizioni diverse del concetto di importanza, tuttavia tutti gli indici nati per misurare questo concetto hanno in comune il tentativo di localizzare la posizione dell’attore in relazione a quella degli altri nella rete. L’idea alla base di questo approccio è molto semplice: l’attore più importante deve essere localizzato strategicamente all’interno del gruppo. Knoke e Burt considerano un attore come importante, o prominente, quando i legami che possiede con gli altri membri del gruppo lo rendono particolarmente visibile ai suddetti membri. Questa definizione è stata accettata e condivisa dal resto della comunità scientifica, e tutt’ora risulta un criterio fondamentale di ricerca.
Per misurare la prominenza di un attore, occorre considerare non solo i legami che un attore origina, o che riceve, ma anche i percorsi indiretti che coinvolgono intermediari. Questo tipo di definizione resta tuttavia ancora operativamente vaga.
Knoke e Burt specificano così ulteriormente il concetto di prominenza, introducendone due dimensioni: centralità e prestigio. Entrambe queste dimensioni del concetto di prominenza sono strutturate sui pattern relazionali espressi dalle entrate della matrice di adiacenza. L’attore centrale è allora quello coinvolto nel maggior numero di relazioni possibili con altri attori. Questo tipo di coinvolgimento lo rende più visibile agli occhi del resto del gruppo. L’attore centrale è maggiormente coinvolto, indipendentemente dal fatto che sia emittente o ricevente di uno scambio relazionale. Un attore centrale è allora un attore prominente in un contesto di relazioni non-direzionate. La centralità viene usata nella Social Network Analysis per effettuare misurazioni in contesti in cui il semplice partecipare alla relazione è più significativo del come vi si partecipa; abbiamo esempi di questi contesti ogni volta che abbiamo a che fare con relazioni legate all’accesso, il controllo o il brokeraggio dell’informazione. A questo livello di analisi dei suddetti contesti, la differenza tra emittente e ricevente è meno importante della semplice partecipazione a molte interazioni. Studiando una relazione come la comunicazione, diremo allora che gli attori che hanno maggior accesso agli scambi sono gli attori più centrali della rete, in virtù della loro estesa partecipazione ad un gran numero di interazioni con altri attori, ma indipendentemente dal loro ruolo.
Per quanto riguarda il prestigio, Wasserman e Faust lo descrivono come la capacità di un attore di attirare su di sé le preferenze di altri attori coinvolti in determinate dinamiche relazionali. In un grafo orientato, allora, un vertice che riceve molti archi viene definito prestigioso. Il prestigio è un concetto più complesso di quello di centralità. Esso non dipende da quanti legami l’attore crea, ma da quanti ne riceve: è perciò impossibile determinare il prestigio di un attore se il grafo è non-orientato, poichè in quel caso non avremo una stima realistica degli indegree. In un certo senso, il prestigio sta alla centralità come gli indegree stanno ai gradi totali. “Prestigio” è comunque una etichetta ambigua, che in determinati casi può risultare inadeguata: se la relazione studiata è una relazione in negativo, allora essere il destinatario di molte preferenze, quindi essere un attore prestigioso, non significa anche essere tenuto in un’alta considerazione da parte del gruppo. “Prestigio” è quindi un termine tecnico della Social Network Analysis, usato in un senso specifico, che non deve essere confuso con il senso attribuitogli dal linguaggio comune.
Il concetto di prestigio è stato etichettato anche con la parola “status” da alcuni studiosi importanti, come Moreno, Katz e Harary. Tuttavia, “status” è un termine che si è legato nello sviluppo della disciplina alle procedure di blockmodeling troppo saldamente per esserne ora scisso, quindi Wasserman delimita il concetto di prestigio a quello di popolarità, riservando il concetto di status per i metodi di analisi dei blocchi e delle posizioni degli attori.
Concludendo, centralità e prestigio rappresentano due modi diversi di essere importanti in un network. La natura sostantiva della relazione oggetto di studio determina la scelta dell’indice più idoneo ad essere usato per la misurazione. Vedremo che relazioni direzionate possono essere studiate sia in termini di centralità che in termini di prestigio, mentre relazioni non-direzionate possono essere studiate solo in termini di centralità.
Ogni indice rileva modi diversi di essere al centro del grafo: il ricercatore deve scegliere accuratamente, in relazione al concetto che sta rilevando (popolarità, status, prestigio, potere...) quello più idoneo.
La sociomatrice è stata uno strumento fondamentale della Sociometria, assieme al sociogramma: con l’introduzione dei computer nelle procedure di analisi, esse hanno guadagnato terreno nei confronti dei sociogrammi, in relazione al fatto che le procedure matematiche e statistiche per la costruzione di indici si basano sui dati forniti dalle prime, relegando il secondo a semplice rappresentazione, per quanto sofisticata, di una situazione analiticamente già studiata.
Come abbiamo detto (vedi Capitolo 2), una sociomatrice è una tabella alle cui righe corrispondono gli attori “emittenti”, ed alle colonne gli attori “riceventi”. In questa matrice quadrata ogni attore occupa nelle colonne la stessa posizione che occupa nelle righe. Le entrate della sociomatrice registrano quali nodi sono adiacenti tra loro (da qui il nome alternativo di matrice di adiacenza): se tra il vertice ni e il vertice nj esiste un arco, allora nella sociomatrice ci sarà un 1 nella cella (i,j); viceversa, avremo nella stessa cella uno 0. Quindi se i nodi ni e nj sono adiacenti, allora xij = 1, se non lo sono allora xij = 0. La sociomatrice per una relazione dicotomica (dove si rileva solo la presenza o l’assenza di una relazione) corrisponde esattamente alla matrice di adiacenza.
Come primo aiuto alla visualizzazione, possiamo rappresentare la sociomatrice precedentemente creata con la funzione plot.sociomatrix() [Butts, 2005, sna-manual.1.0-0 p. 120]:
> plot.sociomatrix(g)
da cui si ricava il grafico in figura 3.1. Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Una importante proprietà per un grafo consiste nell’essere o meno connesso. Per essere connesso esso non deve contenere nodi isolati, quindi tra ogni coppia di vertici deve esistere almeno un percorso. Se un grafo invece contiene anche un solo nodo isolato, si dice disconnesso. I vertici in un grafo disconnesso possono essere a loro volta organizzati in sottografi; ogni sottografo si dice componente del grafo principale. Le componenti non sono mai connesse tra loro, ovvero un vertice può appartenere esclusivamente ad una sola componente. Al loro interno, le componenti sono sempre connesse.
Per verificare l’esistenza di componenti nel grafo, usiamo la funzione booleana is.connected() [Butts, 2005, sna-manual.1.0-0 p. 92]. Essa restituisce semplicemente vero o falso, a seconda che il grafo sia o meno connesso. Chiamiamo la funzione scrivendo in console:
> is.connected(g)
[1] FALSE
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Proviamo a ricercare vertici isolati a conferma del precedente risultato. Usiamo la funzione isolates(), cui è necessario passare un solo parametro, la matrice da analizzare [Butts, 2005, sna-manual.1.0-0 p. 94]. La funzione non individua la somma dei vertici isolati, ma una lista contente le etichette di detti vertici. Nel nostro caso:
> isolates(g)
> 3
Possiamo chiedere a SNA direttamente se il vertice numero 3 sia o meno un isolato con la funzione booleana is.isolate() [Butts, 2005, sna-manual.1.0-0 p. 93]. Nella console di R scriveremo dunque:
> is.isolate(g, 3)
[1] TRUE
Volendo conoscere il numero dei vertici isolati in un grafo, possiamo chiedere ad R di contare quanti elementi siano presenti nella lista restituita da isolates() con la funzione length():
> length(isolates(g))
> 1
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice alle pagine B e B.
Una proprietà molto importante per un vertice consiste nel possedere o meno un percorso che lo colleghi agli altri. Se questo percorso esiste, indipendentemente dalla sua lunghezza (e quindi dagli intermediari che dovranno essere attraversati dal percorso), il vertice è definito raggiungibile. Un vertice isolato, al contrario, è definito come non raggiungibile e la sua distanza dagli altri è ∞. La funzione reachability() [Butts, 2005, sna-manual.1.0-0 p. 142] questa proprietà del grafo producendo una matrice di raggiungibilità, indicando con 0 ed 1 assenza e presenza di percorsi tra i vertici:
> reachability(g)
| [1] | [2] | [3] | [4] | |
| [1] | 1 | 1 | 0 | 1 |
| [2] | 1 | 1 | 0 | 1 |
| [3] | 0 | 0 | 1 | 0 |
| [4] | 1 | 1 | 0 | 1 |
Dobbiamo tenere in considerazione che questa matrice, a parità di densità, tende ad essere sempre meno significativa con l’aumentare delle dimensioni del grafo, poiché grafi molto grandi tendono ad essere connessi, e quindi la matrice di raggiungibilità tende ad essere completa. Avremo quindi un conteggio che ci dirà chi è collegato, ma non ci dirà quanto lunghi sono i percorsi, e se questo può essere comunque informativo per un grafo di piccole dimensioni, non lo è più per uno di grandi. La significatività di questa matrice cresce con i grafi orientati, poiché la direzione della relazione influisce nella costruzione del percorso e quindi nella possibilità che da un vertice se ne possa raggiungere un altro3.
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Consideriamo i percorsi tra due vertici: spesso ne esistono più di uno, con lunghezze differenti. Quelli generalmente usati nei calcoli sono gli shortest path, ovvero i percorsi più brevi, chiamati anche geodetiche. Il concetto di geodetica è alla base di quello di distanza: definiamo infatti la distanza tra due vertici come la lunghezza della geodetica che li unisce. Come abbiamo detto, nel caso di un grafo non connesso, la distanza tra il vertice isolato e gli altri è infinita (vedremo come questo possa causare problemi nel calcolo di alcuni indici). Il concetto di geodetica è inoltre utile a definirne altri due, quello di eccentricità dei vertici e quello di diametro del grafo. L’eccentricità di un vertice è individuata dalla sua geodetica più lunga, mentre il diametro di un grafo consiste nella geodetica in assoluto più lunga in esso presente. Se il grafo è non-orientato, la geodetica tra ni e nj è identica a quella tra nj e ni, così come la loro distanza. Nel caso invece di un grafo orientato, questa equivalenza cessa reggere, in relazione al fatto che un percorso è costituito da archi che puntano tutti nella stessa direzione, quindi la distanza per andare da ni ad nj potrebbe essere più breve di quella necessaria per andare da nj a ni, distanza questa che potrebbe anche semplicemente non esistere. In quest’ultimo caso saremmo in presenza di un grafo connesso weak, debolmente, del quale sarebbe impossibile determinare il diametro. I grafi non orientati sono per definizione connessi strong, per cui è sempre possibile calcolarne il diametro.
La funzione geodist() [Butts, 2005, sna-manual.1.0-0 p. 54] ci fornisce informazioni sulla lunghezza degli shortest paths. Il suo output consiste di due matrici, $counts e $gdist. La prima, analogamente a reachability(), computa semplicemente la presenza o assenza di un percorso tra due vertici. La seconda invece calcola la lunghezza delle geodetiche per ogni coppia di vertici, ovvero la loro distanza.
> geodist(g)
$counts
| [1] | [2] | [3] | [4] | |
| [1] | 1 | 1 | 0 | 1 |
| [2] | 1 | 1 | 0 | 1 |
| [3] | 0 | 0 | 1 | 0 |
| [4] | 1 | 1 | 0 | 1 |
$gdist
| [1] | [2] | [3] | [4] | |
| [1] | 0 | 1 | Inf | 1 |
| [2] | 1 | 0 | Inf | 2 |
| [3] | Inf | Inf | 0 | Inf |
| [4] | 1 | 2 | Inf | 0 |
Dalla seconda matrice, $gdist, apprendiamo che la geodetica che collega i vertici [2] e [4] ha lunghezza 2, mentre le altre geodetiche hanno lunghezza 1. Il punto [3], che non è raggiungibile, non ha alcuna geodetica. Il valore Inf è un valore di default, ma lo si può personalizzare dalla variabile inf.replace.
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Torniamo brevemente al concetto di connessione del grafo. La funzione connectedness() [Butts, 2005, sna-manual.1.0-0 p. 27] calcola il rapporto tra le diadi presenti nel grafo e quelle possibili; in altre parole, calcola la densità della matrice di raggiungibilità. Questo valore varia da 0 (grafo completamente scollegato) a 1 (grafo collegato debolmente). Vediamone l’applicazione:
> connectedness(g)
[1] 0.5
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
La funzione suddetta fa parte di un insieme di quattro misure della struttura gerarchica di un grafo proposte da Krackhardt. Le altre tre sono efficiency(), hierarchy(), e lubness().
“L’efficienza (Krackhardt, 1994) è, essenzialmente, il grado in cui un grafo usa il minor numero possibile di archi per connettere i propri vertici già connessi nel grafo” [Anderson et al., 1999, p. 243].
L’efficienza è quindi una funzione della densità media delle componenti presenti nel grafo e rappresenta una misura della ridondanza della connettività. In sna usiamo la funzione efficiency() [Butts, 2005, sna-manual.1.0-0 p. 36] per misurare questa proprietà. Scriviamo dunque in console:
> efficiency(g)
[1] 0.5
Il grafo in esame ha due componenti, la prima è formata dal punto [3], la seconda dai punti {[1], [2], [4]}. Le componenti non hanno mai legami tra loro, ma solo al loro interno. Nel caso in esame, la componente formata da un unico punto [3] non ha quindi bisogno di legami. I punti [1], [2] e [4] invece sono legati in modo sovrabbondante: sono massimamente connessi tra loro, ovvero connessi con legami simmetrici (sebbene la componente non sia connessa in modo completo). Per tenerli connessi ad un grado minimo basterebbe avere un legame asimmetrico che da [1] vada a [2] ed un altro legame asimmetrico che da [2] vada a [4]. In questo modo avremo:
> efficiency(g)
[1] 1
La funzione efficiency() restituisce un valore che varia tra 1 (all’interno di ogni componente del grafo troviamo soltanto il numero minimo di legami indispensabili per tenere connessi i punti che ne fanno parte) e 0 (presenza di collegamenti ridondanti). Un grafo connesso e completo ha quindi efficiency pari a 0. Come conseguenza della definizione di efficienza, per valori vicini allo 0 ci aspettiamo di trovare legami simmetrici; viceversa, per valori vicini ad 1, ci aspettiamo di trovare legami asimmetrici.
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Il concetto di gerarchia fa pensare a situazioni in cui un attore è in una posizione particolare rispetto ad altri appartenenti al medesimo gruppo. La gerarchizzazione misura quanto gli attori siano collegati da percorsi asimmetrici (ni può arrivare a nj ma nj non può arrivare a ni).
L’asimmetria della relazione può nascere sia da una gerarchia di comando, che da una gerarchia da attribuzione di prestigio. Nel primo caso, l’attore in posizione strategica esercita sugli altri il proprio volere; nel secondo caso lo stesso attore è destinatario delle preferenze di quelli che lo hanno scelto. Entrambe le situazioni possono essere quantificate in sna attraverso la funzione hierarchy() [Butts, 2005, sna-manual.1.0-0 p. 87]. Il calcolo viene effettuato sulla matrice di raggiungibilità, e rappresenta il rapporto tra percorsi direzionati non reciprocati sul totale di quelli presenti nella matrice. L’indice varia tra 1 (nessun persorso è reciprocato, le diadi sono tutte asimmetriche ed il grafo è fortemente gerarchizzato) e 0 (tutti i percorsi sono reciprocati, le diadi sono tutte simmetriche ed il grafo non è strutturato gerarchicamente). Nel nostro caso avremo:
Prendiamo invece in considerazione una versione modificata del nostro grafo. In questo caso modifichiamo la diade [1][2] in modo da renderla asimmetrica: il vertice [2] è collegato al vertice [1] in modo unilaterale. Abbiamo quindi una matrice di raggiungibilità:
> reachability(g)
| [1] | [2] | [3] | [4] | |
| [1] | 1 | 0 | 0 | 1 |
| [2] | 1 | 1 | 0 | 1 |
| [3] | 0 | 0 | 1 | 0 |
| [4] | 1 | 0 | 0 | 1 |
Il vertice [2] è collegato a [1] e [4], ma nè [1] nè [4] sono più collegati a [2]. I due percorsi [2][1] e [2][4] questa volta non vengono quindi reciprocati, aumentando così la gerarchizzazione del grafo:
> hierarchy(g, measure= “krackhardt”)
[1] 0.6666667
Bisogna tenere presente che questo concetto è altra cosa da quello di centralità: esso non si occupa dell’importanza dei nodi in relazione a quanto i nodi sono coinvolti negli scambi, ma in relazione alla natura dei percorsi che generano nel grafo e a quanti, per così dire, “sensi unici” vi troviamo.
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
L’ultima funzione ispirata dal gruppo proposto da Krackhardt è lubness() [Butts, 2005, sna-manual.1.0-0 p. 102]. Essa prende il nome da quello che cerca, ovvero un least upper bound (limite minimo superiore). Un least upper bound per una coppia di vertici è il più vicino vertice esterno alla coppia che può raggiungerne entrambi i membri. Consideriamo una serie di elementi:

Per ogni paio di elementi ni e nj definiamo come loro limite superiore quell’elemento nk tale che ni ≤ nk e nj ≤ nk. Il limite superiore più piccolo è quell’elemento nk che soddisfa la condizione nk ≤ nl per tutti i limiti superiori nl di ni e nj [Wasserman et al., 1998, p. 327]. Di tutti i limiti superiori di ni e nj, quindi, nk è il più piccolo. Un discorso analogo può essere fatto per i limiti inferiori.
In un grafo, allora, possiamo dire che due punti i e j hanno un confine superiore se esiste un punto k tale che i percorsi diretti ki e kj appartengono anch’essi al grafo.
Queste informazioni sul grafo sono utili per analizzare le affiliazioni tra attori ed eventi, sfruttando tipologie di analisi dei sottogruppi largamente usate in ambito matematico, ma solo recentemente introdotte in Social Network Analysis. Se tutte le coppie di vertici posseggono un least upper bound, la funzione restituisce 1; più ci si allontana da questa condizione, più il risultato tende a 0. Per il nostro grafo di esempio, scrivendo in console la richiesta otteniamo:
> lubness(g)
[1] 1
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
La costruzione di un indice importante come quello di densità necessita di una
informazione preliminare: il computo delle linee possibili L nel grafo. A seconda che
un grafo con g vertici sia direzionato o meno, avremo L = g(g - 1) nel primo
caso e L =
nel secondo. Nel caso del grafo direzionato, non dovremo
dividere per 2, poiché come abbiamo già visto il legame xij e quello xji non
rappresentano la stessa cosa, ma due attori diversi che compiono ognuno una scelta
indipendente. In entrambe le formule non consideriamo i loops. In sna questo
calcolo viene effettuato dalla funzione nties() [Butts, 2005, sna-manual.1.0-0 p.
113]. Nel nostro esempio, un grafo non-orientato con 4 vertici, nella console
scriveremo:
> nties(g, mode= “graph")
[1] 6
Se il grafo fosse orientato, sarebbe sufficiente scrivere:
> nties(g, mode= “digraph”)
[1] 12
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
La densità di un grafo è una proprietà strutturale a livello di gruppo che considera il rapporto tra gli archi presenti nel grafo su quelli possibili. Per un grafo non orientato, formalmente avremo:

La densità di un grafo varia tra 0 e 1. Vale 0 se nessuna linea nel grafo è presente, ovvero se tutti i nodi sono isolati; vale 1 se invece le linee presenti sono tutte le linee possibili, ovvero se ogni attore è adiacente a tutti gli altri. In quest’ultimo caso il grafo si dice completo. C’è una relazione diretta tra la densità di un grafo e il grado medio dei nodi di quel grafo. Poiché sappiamo che la somma dei gradi è uguale a 2L (ogni arco viene contato due volte, una per ogni nodo a cui è connesso), usando il grado medio d′ avremo:

Se invece abbiamo, come nel nostro esempio, un grafo orientato, trattiamo coppie ordinate di vertici, e come abbiamo visto precedentemente il numero massimo possibile di archi è g(g - 1), quindi:

Anche questo valore oscilla tra 0 e 1. Nel primo caso, nessun arco è presente, e tutti i nodi sono isolati; nel secondo caso, tutti i legami sono presenti, e tutte le diadi sono mutue, ovvero ogni nodo reciproca tutte le preferenze ricevute dagli altri. A livello di gruppo la densità è l’indice più usato in letteratura come misura della coesione.
La funzione gden() [Butts, 2005, sna-manual.1.0-0 p. 50] si occupa di calcolare la densità direttamente. Possiamo quindi scrivere nella console:
> gden(g, mode= “graph")
[1] 0.3333333
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
L’interpretazione più semplice del concetto di centralità è data dal computo dei gradi: questa misura si concentra sugli archi che collegano un dato vertice al suo vicinato, e semplicemente ne conta il numero. L’attore con il grado più alto rappresenta metaforicamente il luogo nel gruppo dove “le cose accadono”. In contrasto, gli attori con un basso grado rappresentano le posizioni periferiche nella rete: estremizzando, se avessimo un attore isolato (grado 0), e decidessimo di eliminarlo, nulla cambierebbe nella disposizione dei legami tra gli altri attori. Il grado di un vertice varia da 0 a g - 1 (il vertice è collegato con tutti gli altri). Per calcolare il grado di un vertice è quindi sufficiente contare il numero degli archi incidenti ad esso, oppure sommare i valori di riga (o di colonna) a cui il vertice appartiene nella matrice di adiacenza.
Se il grafo è orientato, il concetto di grado si amplia, poiché possiamo prendere in considerazione la direzione della relazione. Tutti gli archi che originano dal vertice ni (ovvero le scelte compiute dall’attore che il vertice rappresenta) costituiscono il suo outdegree, o grado in uscita; tutti gli archi che arrivano al vertice ni (ovvero le preferenze ricevute dall’attore che il vertice rappresenta) costituiscono invece il suo indegree, o grado in entrata.
L’introduzione di questi due concetti ci permette di distinguere quattro tipologie all’interno delle quali i teorici dei grafi classificano i vertici:
Esiste una condizione che distingue un portatore da un ordinario: sebbene entrambi abbiano indegree ed outdegree positivi, nel nodo portatore questi due valori sono entambi uguali ad 1.
La distinzione tra outdegree ed indegree è utile per quantificare il prestigio degli attori: in particolare l’indegree è diventato sinonimo di popolarità.
La funzione in sna per calcolare il grado dei vertici si chiama degree()[Butts, 2005, sna-manual.1.0-0 p. 33]. Scriveremo in console:
> degree(g, gmode="graph", cmode=”freeman”)
| col1 | col2 | col3 | col4 |
| 4 | 2 | 0 | 2 |
L’opzione freeman somma il numero di gradi in entrata con quelli in uscita per ogni vertice. Per calcolare separatamente indegree ed outdegree è sufficiente passare alla variabile cmode i valori, rispettivamente, “indegree” ed “outdegree”. Questo vale sia per i grafi semplici che per quelli orientati - in questo ultimo caso dovremo passare il valore “digraph” alla variabile gmode.
Dunque il risultato ottenuto un po’ ci spiazza: sembra che i gradi abbiano valore doppio rispetto a quello che ci aspetteremmo dalle considerazioni teoriche esposte poco sopra. Carter Butts ci spiega la natura di questo problema:
“The standard graph theoretic notion of degree for a simple graph is the size of the vertex neighborhood, which is equal to either the indegree or outdegree (both being identical in this case). Thus, graph theoretic degree is one half the Freeman degree in this case, which is why degree() returns a vector which sums to 4 rather than 2. Indegree is the cardinality of the in-neighborhood of a given vertex, while outdegree is the cardinality of the respective out-neighborhood. Both are well-defined for undirected graphs... they just happen to be identical”4.
Quindi, quando calcoliamo i gradi usando il valore “freeman”, dobbiamo tenere presente che esso somma indegree ed outdegree per i nodi, e questo, di fatto, nei grafi semplici raddoppia il valore teorico dei gradi.
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
è una misura basata sulla distanza. Intuitivamente, questo indice focalizza la propria attenzione su quanto vicino un attore è agli altri. Un attore è quindi tanto più centrale quanto più è nella posizione di interagire velocemente (avendo meno intermediari) con altri attori. Un attore centrale in questo senso risulta allora molto efficace nel divulgare informazioni, proprio perché è quello che maggiormente nel gruppo ha contatti diretti, o indiretti ma brevi, con tutti gli altri. Questa misura, a differenza dei gradi, non si concentra solo sui legami diretti, ma prende in considerazione anche i percorsi indiretti tra i nodi (limitatamente agli shortest paths).
Wasserman riporta la definizione che Hakimi e Sabidussi hanno dato per quantificare il concetto di vicinanza (close) alla base della definizione di closeness: essere centrali in un grafo significa essere vicini agli altri, ovvero avere un numero minimo di passi che ci separano dagli altri. Praticamente si tratta di verificare, tra tutti i vertici presenti, per quale di loro la somma delle distanze dagli altri nodi è minore. In altre parole ancora, quale attore possiede le geodetiche più brevi. La centralità come vicinanza è quindi inversamente proporzionale alla distanza: meno si è distanti agli altri, più si è centrali e viceversa. Il concetto può essere espresso in maniera formale [Brandes et al., 2005, Wasserman et al., 1998, p. 184] come:

L’indice non è altro che l’inverso della somma delle geodesiche che dall’attore ni
raggiungono tutti gli altri attori. Il massimo di questo indice è (g - 1)-1 e si
ottiene quando un nodo è adiacente a tutti gli altri nodi del grafo (come
accade nei grafi a stella, ad esempio). Il minimo si raggiunge a 0, e si ottiene
quando un nodo non è raggiungibile dall’attore in questione. Un nodo è
raggiungibile quando esiste un percorso che lo collega ad altri nodi. Questo indice
quindi ha senso solamente per grafi connessi: quando esistono nodi isolati,
esso perde significato, poiché nella sommatoria sopra illustrata si insinuano
distanze infinite che portano il rapporto a
= 0 . Un modo per evitare questo
problema consiste nell’escludere i nodi isolati dal grafo prima di computare
l’indice, in virtù della considerazione che un nodo isolato è ugualmente isolato
da ogni altro nodo, e quindi la sua rimozione non comporterà perdite di
informazione.
Per calcolare l’indice chiamiamo la funzione closeness() [Butts, 2005, sna-manual.1.0-0 p. 23]. Nel nostro caso, come abbiamo precedentemente accennato, non possiamo chiamarla sul grafo intero disconnesso. Dovremo specificare la lista di vertici da includere nel calcolo:
> closeness(g, gmode= “graph”, nodes=c(1, 2, 4))
> 1.0000000 0.6666667 0.6666667
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
L’interazione tra due attori non adiacenti è legata alla collaborazione con altri attori. In particolare, con quelli localizzati sui percorsi che collegano i primi due: questi intermediari hanno un ruolo delicato e potrebbero esercitare un potere di controllo sul flusso delle informazioni che veicolano. Il concetto di betweenness prende quindi in considerazione le geodesiche presenti nel grafo, contando quante volte ogni vertice si trova coinvolto in quelle di altri attori. L’idea alla base di questo concetto di centralità è quindi che un attore è centrale se sta in mezzo ad altri attori: se la betweenness di questo attore è alta, allora ciò significa che egli è un intermediario di molte coppie di attori. Molti ricercatori, già dai primi anni ’50, avevano intuito l’importanza insita nell’occupare questo ruolo: ciò equivaleva ad avere la possibilità di influenzare la comunicazione, di gestire l’informazione. Ma questi ricercatori non riuscirono a sviluppare una tecnica per quantificare questo tipo di centralità fino a che Freeman e Pitts non proposero un metodo basato sulle geodetiche; Wasserman riporta una citazione di Pitts:
“Suppose that in order for [actor] i to contact [actor] j, [actor] k must be used as an intermediate station. [Actor] k in such a network has a certain ’responsibility’ to [actor] i and j. If we count all of the minimum paths which pass through [actor] k, then we have a measure of the ’stress’ which [actor] k must undergo during the activity of the network.” [Wasserman et al., 1998, p. 189].
Qui il concetto di stress del vertice si sovrappone a quello di betweenness. L’idea consiste nel contare le geodesiche tra i e j, tutte ovviamente della stessa lunghezza, e quindi verificare in quante di queste sia presente l’attore k.
L’indice proposto da Freeman prevede di considerare esclusivamente gli shortest
paths tra vertici, e prevede altresì di considerali, qualora ce ne fossero più di uno tra
due vertici, tutti equiprobabili per un flusso di informazione: ovvero, qualora
l’informazione dovesse andare da un punto A ad un punto B, e si trovasse di
fronte diverse geodesiche, ognuna di esse avrebbe la stessa probabilità delle
altre di venire scelta. Se quindi gij rappresenta il numero di geodesiche che
collegano i nodi i e j, allora la probabilità che una di esse venga scelta è
. Consideriamo anche la probabilità che l’attore k sia convolto in uno di
questi scambi: se gij(nk) rappresenta il numero di geodesiche tra i e j sulle
quali si trova k, allora Freeman stima che quella probabilità sia data da
gij(nk)∕gij.
La betweenness dell’attore k sarà data dalla sommatoria di tutte le betweenneess parziali calcolate per ogni coppia di vertici:

L’indice è quindi dato da una somma di probabilità, stante l’assunzione dell’equiprobabilità delle geodesiche. Questo indice è massimo quando l’attore appartiene a tutte le geodesiche del grafo e vale (g - 1)(g - 2)∕2 ovvero il numero di coppie di vertici escluso nk. Questo indice può essere computato anche per grafi non connessi.
Come per quelli individuati da stresscent() e da infocent(), questi nodi hanno il ruolo di ponti tra i vertici. In sna chiameremo la funzione usando betweenness()[Butts, 2005, sna-manual.1.0-0 p.11]. Per il nostro esempio scriveremo allora:
> betweenness(g, gmode= “graph”)
[1] 1 0 0 0
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Lo stress di un vertice è dato dal numero di geodetiche che lo attraversano. I vertici considerati sotto stress sono quelli che vengono attraversati da molte geodetiche; possiamo pensarli come ponti da cui tutti passano per comunicare l’uno con l’altro. Il concetto è analogo a quello di betweenness, da cui si distingue per il fatto di considerare anche le geodetiche ridondanti tra coppie di vertici (i.e. se tra due vertici ci sono più geodetiche, stresscent le considera tutte, mentre betweenness ne considera una soltanto: questo computo è motivato dal fatto che lo stress di un nodo viene collegato al numero degli archi che lo attraversano, non al numero di coppie a cui funge da intermediario).
Usiamo la funzione stresscent() per calcolare questo indice [Butts, 2005, sna-manual.1.0-0 p. 162]. Applicata al nostro grafo di riferimento, stresscent produce come output:
> stresscent(g)
[1] 1 0 0 0
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
L’indice di Freeman per la betweenness si basa su assunzioni precise in relazione alla distribuzione di probabilità delle geodesiche: specialmente in contesti di comunicazione, non è detto che una strada valga l’altra per l’attore, anzi, è probabile che ci siano percorsi preferiti e percorsi da evitare, per quanto della stessa lunghezza. Quello che Freeman non considera, sottolinea Wasserman, è che se una delle geodetiche esistenti tra due vertici passa per un terzo attore con un grado elevato, essa risentirà della sua influenza e diventerà per così dire più appetibile per il flusso di informazione. L’equiprobabilità si rivela dunque una limitazione troppo stringente. L’assunto di Freeman è corretto solo se gli attori hanno un degree simile (o con uno scostamento basso dal valore medio); se questo assunto non è sostenibile - se gli attori hanno scostamenti significativi dal valore medio, dobbiamo cambiare strategia e cercare un modello che tenga conto di differenti probabilità per le geodesiche.
Inoltre, Freeman concentra la sua analisi soltanto sugli shortest path, ignorando tutti gli altri possibili percorsi tra due vertici. Ancora una volta (realisticamente) dobbiamo allargare questa assunzione, considerando che spesso nella realtà gli attori scelgono percorsi più lunghi di quelli necessari, per ragionamenti di convenienza:
“It is quite possible that information will take a more circuitois route either by random communication or [by being] channeles through many intermediaries in order to ’hide’ or ’shield’ information.” [Wasserman et al., 1998, p. 193].
Possiamo considerare l’information centrality come una generalizzazione della betweenness centrality a tutti i percorsi tra gli attori, pesati in relazione alla loro lunghezza.
Mackenzie fu il primo a proporre l’uso della teoria dell’informazione per studiare la centralità. L’informazione viene definita come l’inverso della varianza di uno stimatore: quanto più lo stimatore ha una varianza piccola, tanto più porterà informazione. E viceversa.
L’information centrality per un vertice viene calcolato come media armonica dell’ampiezza di banda di tutti i percorsi che originano da detto vertice. Gli attori con una information centrality alta sono attori che generalmente hanno un grande controllo sul flusso di informazioni che circola nella rete: essi tendono ad avere un grande numero di legami corti con gli altri attori, ovvero sono collegati direttamente a molti altri senza intermediari.
Come per la closeness centrality, questo indice non può venire calcolato se nel grafo sono presenti vertici isolati (per un problema matematico relativo al processo di inversione di una matrice).
Questo indice ha valore minimo 0, ma non ha un valore massimo. Conviene quindi relativizzarlo ad un campo di variazione tra 0 e 1; la particolarità di questo indice normalizzato è che i risultati che si ottengono, se sommati, non eccedono l’unità. In questo senso possiamo interpretarne i valori come percentuali di informazione controllati da ogni attore; se otteniamo il valore 1, significa che un solo attore ha il controllo sul 100% dell’informazione circolante nel grafo (possiamo ottenere questa normalizzazione in sna impostando il parametro rescale=TRUE).
Per calcolare questo indice, chiamiamo infocent() [Butts, 2005, sna-manual.1.0-0 p.88]. Nel caso usato come esempio avremo:
> infocent(g, gmode= “graph”, rescale=TRUE)
[1] 0.44 0.28 0.00 0.28
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Questo indice combina insieme il numero delle scelte dirette che riceve un vertice con il prestigio degli attori che compiono le scelte, in modo da ottenere una centralità ”pesata” dallo status degli attori coinvolti. Se quindi il dominio di influenza (o vicinato) di un attore è affollato di attori prestigiosi, la sua centralità risulta accresciuta proporzionalmente da questo fattore; viceversa, la sua centralità risulta diminuita se viene scelto da attori scarsamente prestigiosi. Il problema è che in questo modo inneschiamo una catena ricorsiva da cui si esce solo con l’uso di un intelligente approccio matematico: infatti, per determinare il prestigio di ogni attore, dovremmo prima determinare il prestigio degli attori che lo hanno scelto, ma per determinare questo, dovremmo ancora una volta determinare il prestigio degli attori che hanno scelto questi attori... e così via all’infinito. Il metodo più semplice per uscire dal loop regressivo, proposto da Seeley nel 1949, viene discusso da Wasserman e Faust [Wasserman et al., 1998, p. 206-207].
Katz introduce la propria versione nel 1953 [Katz, 1953], Phillip Bonacich nel 1972 [Bonacich, 1972]. In entrambi i casi sono stati introdotti miglioramenti, tuttavia resta l’ambiguità legata al fatto che un suo alto valore può essere prodotto indifferentemente sia da pochi attori importanti, sia da molti attori poco importanti, e questa imprecisione necessita di aggiustamenti. Wasserman nota tuttavia che i correttivi proposti rendono generalmente innaturalmente complicati i calcoli, recando alla fine poca informazione aggiuntiva.
Il significato di questa misura in ogni caso risiede nel considerare l’importanza (ovvero la qualità) del vicinato. In questo modo, un vertice è tanto più centrale quanto più è collegato a vertici centrali: una misura indubbiamente adatta a valutare la posizione del soggetto per gruppi di importanza nel grafo (è un fatto che i valori più alti di eigenvector vengono raggiunti dai vertici inseriti nelle cliques più ampie, ovvero da quei vertici che sono collegati a molti altri, che a loro volta sono collegati a molti altri...). La funzione si invoca usando evcent() [Butts, 2005, sna-manual.1.0-0 p. 40]. Nel nostro caso scriviamo nella console:
> evcent(g, gmode= “graph”)
> 0.7071068 0.5000000 0.0000000 0.5000000
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Questo indice rileva la centralità secondo i criteri di Bonacich [Bonacich, 1987, Scott, 2003, p. 129]. Il suo significato (misurando la centralità come potere) sta nel definire la centralità di un vertice come definita ricorsivamente dalla somma della centralità dei suoi vicini: rappresenta uno sviluppo teorico di eigenvector, di cui estende l’approccio. In questo caso la natura del processo ricorsivo di definizione è moderata da un parametro in relazione al cui segno, positivo o negativo, si configurano due situazioni speculari: nel primo caso, valore positivo, il potere di un vertice cresce al crescere di quello dei vicini (tipico il caso delle relazioni cooperative); nel secondo caso, valore negativo, il potere di un vertice cresce al diminuire del potere dei vicini (caso questo tipico delle relazioni competitive o antagoniste). In relazione invece alla grandezza del parametro, indipendentemente dal segno, otteniamo la tendenza dell’effetto di accescimento del potere a decadere su lunghi percorsi: più grande è il valore, più lento è il decadimento. In questo modo possiamo avere relazioni di proporzionalità inversa tra importanza dell’attore e importanza del suo vicinato e quindi studiare sia contesti collaborativi, sia contesti competitivi. Chiamiamo l’indice con la funzione bonpow() [Butts, 2005, sna-manual.1.0-0 p. 18]. Nel caso del nostro grafo di esempio, avremo:
> bonpow(g, gmode= “graph”)
> -1.371989 -1.028992 0.000000 -1.028992
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
In presenza di grafi direzionati, abbiamo la possibilità di analizzare separatemente le scelte ricevute da quelle effettuate. Le prime in particolare sono interessanti per determinare l’attore più importante nel grafo dal punto di vista del prestigio.
La misura più semplice di prestigio dell’attore è costruita sull’indegree. L’idea, come abbiamo già osservato, è che un attore prestigioso riceva molte preferenze. Definiamo quindi:

Questa equazione, che fa corrispondere l’indegree al prestigio dell’attore, è dipendente dalla grandezza del grafo, e quindi non può essere usata per confronti prima di essere standardizzata in:

Wasserman chiama questa misura di prestigio “indegree relativo”. Più grande è l’indice, più prestigioso è l’attore. Se l’indice vale 1, l’attore ni è stato scelto da tutti gli altri.
Questo primo concetto di prestigio è però limitato dal fatto di considerare nel calcolo esclusivamente gli attori direttamente collegati (adiacenti) a ni. Generalizzando ed estendendo il concetto anche ad altri attori, è necessario definire un dominio di influenza all’interno del quale misurare il prestigio dell’attore, un dominio che ovviamente si estenda oltre il vicinato. Lin usò questa definizione per primo, costruendo l’indice di prestigio attraverso la matrice di raggiungibilità, che rappresenta il dominio di influenza dell’attore e identifica tutti gli altri attori che possono appunto raggiungerlo.
All’interno del dominio di influenza, consideriamo quanto ni è lontano dagli appartenenti al suo dominio, definendo questa distanza come ”prossimità” (concettualmente vicina all’idea di closeness, ma focalizzata sulla distanza verso, piuttosto che sulla distanza da: nei digraph, queste due distanze non si equivalgono). La prossimità rappresenta il percorso che parte da ni e arriva agli attori del dominio, quindi è una closeness per così dire “in uscita”.
Questo indice può essere calcolato anche per grafi non connessi, poiché sebbene basato sulla closeness, esso lavora a partire dalla matrice di raggiungibilità, escludendo gli attori isolati da ogni dominio di influenza, e quindi non considerandoli nel calcolo.
Sotto questo unico concetto di prestigio cadono una serie di tipi di centralità diverse, tutte legate in modi diversi a quanto un attore viene nominato dagli altri. La costruzione dell’indicatore avviene attraverso vari criteri di valutazione delle preferenze (da passare alla funzione come valori del parametro cmode):
Chiameremo l’indice con la funzione prestige() [Butts, 2005, p. 124]. Creiamo dunque un grafo direzionato e calcoliamo il prestigio dei suoi nodi scrivendo in console:
> h<-rgraph(5, gmode= “digraph”)
[1] h
[, 1] | [, 2] | [, 3] | [, 4] | [, 5] |
|
[1, ] | 0 | 0 | 1 | 1 | 0 |
[2, ] | 0 | 0 | 1 | 1 | 0 |
[3, ] | 0 | 0 | 0 | 0 | 1 |
[4, ] | 1 | 1 | 1 | 0 | 0 |
[5, ] | 0 | 0 | 1 | 1 | 0 |
> prestige(h)
[1] 1 1 4 3 1
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
La maggior parte degli indici di centralità precedentemente esaminati possono venire aggregati in indici di centralizzazione, ovvero indici che misurano la centralità a livello non più dell’attore ma del grafo nel suo complesso:
“Queste misure di centralità sono variabili relazionali che possono essere considerate proprietà degli attori o dei nodi della rete considerata; ad ognuna di esse corrisponde una misura di centralizzazione che si estende a livello della collettività, e che quindi può essere considerata una proprietà della rete. La centralizzazione della rete mostrerà valori elevati quanto più le relazioni sono concentrate su singoli attori” [Salvini, 2005, p. 67].
La centralizzazione di un grafo, insieme alla densità, rappresenta una proprietà a livello di grafo, e si costruisce a partire da diverse misure di centralità; in sna abbiamo a disposizione la funzione centralization() [Butts, 2005, sna-manual.1.0-0 p.21]. Un indice di centralizzazione varia tra 0 ed 1: quanto più si avvicina ad uno, tanto più avremo il grafo avrà un solo attore centrale, e molti attori periferici; viceversa, quanto più sarà vicino a 0, tanto più gli attori saranno ugualmente centrali. Per questa caratteristica l’indice di centralizzazione può essere considerato come una rozza misura di variabilità o dispersione, un modo di misurare quanto diseguali siano i valori dei singoli attori.
Wasserman sottolinea come questa interpretazione degli indici di centralizzazione sia condivisa da molti studiosi, tra i quali Leavitt, Moscovici, Mackenzie, Nieminen e Freeman [Wasserman et al., 1998, p. 176]. A livello di gruppo, potremmo anche costruire degli indici aggregati di prestigio, tuttavia la realizzazione tecnica di questo tipo di indice risulta più complessa, se fatta negli stessi termini degli indici di centralizzazione; vengono per questo motivo usate misure più semplici [Wasserman et al., 1998, p. 177].
Le misure di centralità da passare al parametro FUN tra cui possiamo scegliere sono:
Nel caso della matrice precedentemente creata, h, volendo controllarne la centralizzazione in relazione alla betweenness dei suoi nodi, avremo:
> centralization(h, betweenness)
[1] 0.375
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Consideriamo le proprietà principali di un grafo orientato. In una matrice di adiacenza sono possibili un numero L di archi, loop esclusi, compreso tra 0 e g(g - 1). Ne consegue che, a parità di nodi g, ad L più grandi corrispondano networks più densi e viceversa. I totali di riga della matrice di adiacenza rappresentano l’outdegrees dei nodi; la loro somma è pari a L. I totali di colonna della matrice di adiacenza rappresentano l’indegrees dei nodi; anche la loro somma è pari a L.
Seguendo Wasserman e Faust, definiamo una diade come “una coppia non ordinata di attori e gli archi che esistono tra i due attori della coppia” [Wasserman et al., 1998, p. 510, trad. nostra.].
L’analisi delle diadi nasce dal tentativo di rispondere a domande del tipo “quanto frequentemente osserviamo relazioni mutue (o reciproche) nelle reti sociali?”, oppure “quanto frequentemente osserviamo relazioni nulle?”.
Esistono due classi all’interno delle quali classificare i quattro stati possibili in cui si può presentare una diade: una classe di diadi simmetriche, ed una di diadi asimmetriche.
La classe delle diadi simmetriche contiene tutte quelle diadi per cui vale la relazione:

ovvero nella matrice di adiacenza osserviamo:

La classe delle diadi asimmetriche contiene tutte quelle diadi per cui vale la relazione:

ovvero nella matrice di adiacenza osserviamo:

La classe delle diadi simmetriche contiene quindi diadi nulle e diadi mutue, diadi cioè in cui un attore reciproca il comportamento dell’altro. L’analisi delle diadi nasce proprio dal tentativo di esplorare la simmetria relazionale analizzandone le frequenze. L’interesse dei ricercatori storicamente si è concentrato sull’analisi di questa classe a scapito delle diadi asimmetriche, poiché queste ultime venivano ritenute una sorta di stato intermedio non duraturo nella transizione verso la simmetria; questo ha portato i due stati della classe simmetrica a guadagnare una propria autonomia ed indipendenza, tanto che nella pratica, parlando di diadi, se ne enumerano tre tipi: mutue, nulle o asimmetriche. La novità dell’interesse teorico verso queste ultime come oggetto di studio fine a se stesso, rappresentante una stabile ma diversa disponibilità delle risorse per gli attori, è secondo noi dimostrato dal fatto che, per questa classe, ci si riferisce indistintamente con l’aggettivo “asimmetrica” sia alla diade gi ← gj che alla diade gi → gj. Le due classi all’interno delle quali si collocano i quattro stati possibili (gi ← gj, gi → gj, gi ↔ gj, gi ↮ gj) di una diade si riferiscono, ovviamente, ad una relazione dicotomizzata.
Una diade è, concludendo, un sub-grafo di rango 2, o 2-subgraph, contenente due
nodi e gli archi che li collegano; in ogni grafo direzionato esistono
diadi. Ogni
diade è isomorfa ad uno dei tre stati mutuo, asimmetrico (in due varianti) o nullo
(MAN). Due diadi si dicono inoltre isomorfe tra loro se, a parità di struttura
relazionale, differiscono esclusivamente nell’ordine con cui sono etichettati i
nodi.
L’operazione di censimento delle diadi esamina ogni diade presente nella matrice di adiacenza e la colloca nella corretta classe di appartenenza, secondo uno schema classificatorio così strutturato:
N, il numero di diadi nulle presenti nella matrice, viene quindi ricavato per deduzione sottraendo dal numero totale di diadi presenti il numero di quelle asimmetriche e di quelle mutue. Il numero di diadi asimmetriche viene a sua volta determinato sottraendo al numero di archi effettivamente rilevati nella matrice quelli che sono impegnati in diadi mutue. Il numero di diadi mutue viene invece effettivamente contato. Nel pacchetto sna queste operazioni vengono eseguite dalla funzione dyad.census() [Butts, 2005, sna-manual.1.0-0 p. 35].
Questa funzione, implementazione del metodo proposto da Holland e Leinhardt in un articolo dal titolo “A Method for Detecting Structure in Sociometric Data”, apparso nel 1970 sulla rivista American Journal of Sociology [Holland, 1970], restituisce una matrice contenente il censimento delle diadi contenute nel sociogramma. Nel caso della nostra matrice di adiacenza di riferimento, avremo:
> dyad.census(g)
| Mut | Asym | Null | |
| [1] | 2 | 0 | 4 |
La tabella ci informa che il nostro grafo contiene 2 diadi mutue, 0 diadi asimmetriche e 4 diadi nulle. Il dyad census ci offre gli elementi base, in termini di frequenze assolute, per sviluppare indici che misurino la reciprocità presente nelle scelte osservate nel grafo (a patto di poter ipotizzare la distribuzione di probabilità che sottende alla relazione).
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Il concetto di reciprocità è stato studiato dai ricercatori della Network Analysis sin dagli anni ’30. La domanda che sottende questo concetto è formulata da Wasserman nei termini: quanto è forte la tendenza di un attore a sceglierne un altro, se questi lo ha già scelto per primo?
Da questa domanda nascono e si sviluppano la maggior parte degli indici di mutualità o reciprocità (i due termini sono usati come sinonimi dai ricercatori). sna ci offre una funzione per calcolare una misura di mutualità, grecip() [Butts, 2005, sna-manual.1.0-0 p. 77]. Questa funzione calcola la reciprocità degli elementi presenti nella matrice di adiacenza.
Dalla documentazione apprendiamo che la variabile measure ha tre parametri. Il
primo, dyadic, serve a calcolare “la proporzione di diadi che sono simmetriche” su
quelle totali. Come precedentemente osservato, alla classe delle diadi simmetriche
appartengono sia le diadi mutue che le diadi nulle, quindi la formula seguita nel
calcolo è
:
> grecip(dat, measure = “dyadic")
[1] 1
Il secondo parametro, edgewise, calcola “la proporzione di archi che sono reciprocati”,
quindi usa come formula
:
> grecip(dat, measure = “edgewise")
[1] 1
Il terzo ed ultimo parametro, dyadic.nonnull, calcola la frazione di diadi non nulle che
sono reciprocate secondo la formula
. Questo parametro offre in pratica la
possibilità di calcolare il parametro dyadic escludendo le diadi nulle dal
conteggio.
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Una relazione mutua tra due nodi i e j è tale quando i → j ANDj → i. Indichiamo questa condizione di mutualità con i ↔ j. Questa condizione si traduce in una matrice di adiacenza in cui le celle (i,j) e (j,i) - simmetriche lungo la diagonale, contengono un 1. In un grafo direzionato abbiamo una diade mutua se e solo se ogni attore della coppia sceglie l’altro nella relazione.
Nel pacchetto sna la funzione mutuality() [Butts, 2005, sna-manual.1.0-0 p. 104] che conta questo tipo di diadi, nel nostro caso:
> mutuality(g)
[1] 2
Per ulteriori approfondimenti sui parametri di questa funzione si rimanda in Appendice a pagina B.
Siamo in presenza quindi di una misura relativa al grafo e non confrontabile con altre, a meno che non provengano da grafi della stessa dimensione ( e relativi ovviamente allo stesso contesto relazionale). Le origini di questo concetto di misura si possono rintracciare in un intervento del 1938 di Moreno e Jennings in Sociometry, dal titolo “Statistics of Social Configurations” [Moreno et al., 1938]. In pratica rappresenta un dyad census parziale, a cui dobbiamo cercare di dare un significato: il valore trovato, in assenza di una scala di misura, è alto o basso?
Wasserman e Faust illustrano due indici di mutualità che possono aiutarci a rispondere alla domanda, il primo proposto da Katz e Powell (1955), il secondo da Achuthan, Rao e Rao (1982): entrambi questi indici esprimono il tentativo di staccarsi dalla particolarità di un caso per ottenere una misura confrontabile con valori teorici attesi. Osservando quanto i nostri dati si discostino da questi valori possiamo supporre l’esistenza o meno di particolari dinamiche, diverse dal caso, che influenzano i processi di scelta degli attori.
Il primo di questi due indici è derivato dalle indicazioni di Katz e Powell del 1955. La logica dietro questo indice di reciprocità consiste nel misurare se la tendenza degli attori nel gruppo a reciprocare le scelte sia più o meno frequente di quello che accadrebbe se gli attori scegliessero gli altri attori casualmente (assenza massima di reciprocità). L’indice misura la forza di questa tendenza, e varia tra -infinito < pkp ≤ 1: quando vale 0 la tendenza a reciprocare le scelte è nulla, come se ogni attore avesse scelto a caso gli altri attori. Quando l’indice vale 1, al contrario, la tendenza a reciprocare le scelte è massima. Valori negativi indicano che la tendenza degli attori è più indirizzata a costruire diadi asimmetriche o nulle piuttosto che mutue.
L’importanza di questo indice è sostanziale: non è un semplice valore, ma esprime un confronto tra un valore effettivo calcolato sulla matrice di adiacenza ed un valore teorico di riferimento derivato da una ipotetica distribuzione casuale uniforme (ovvero governata esclusivamente dal caso) delle scelte nel grafo.
C’è quindi la costruzione teorica di un modello, da cui “misurare lo scostamento”, che parte dall’assunzione di un criterio casuale di distribuzione nelle scelte degli attori, criterio tuttavia non ancora verificato da alcun test, ma semplicemente presunto.
L’indice ρkp di Katz e Powell viene calcolato sia a partire da un outdegree fisso per tutti gli attori (calcolato come outdegree medio rilevato nel grafo), sia sulla base degli effettivi outdegree dei singoli attori: nel primo caso abbiamo la possibilità di calcolare il numero E(M) delle diadi mutue attese; nel secondo caso, ovviamente, il calcolo non è possibile a causa del fatto che ogni attore ha un outdegree presumibilmente diverso dagli altri. Questa funzione quindi calcola sia:

usando il grado medio d, sia:

usando i gradi effettivamente rilevati [Wasserman et al., 1998, p. 516]. In Appendice C riportiamo il codice relativo alla nostra implementazione.
Il secondo indice è stato proposto da Achuthan, Rao e Rao nel 19825. Questo indice ha un approccio diverso dal precedente al calcolo della reciprocità, e lungi dal partire da considerazioni sul numero atteso di diadi mutue o sulla probabilità della loro formazione (sulla scia di Lazarsfeld, ma anche di Moreno e Jennings), questi autori propongono un approccio “intervallare” (sebbene sempre incentrato sul concetto di outdegree, da cui dipende ovviamente il numero di scelte effettuate e quindi il numero di diadi mutue che si possono formare): essi si chiedono quale sia, a parità di outdegree, il numero minimo ed il numero massimo di diadi mutue che in un grafo con quell’outdegree (non medio ma relativo ad ogni attore) si possono formare. Partiamo dalla semplice correlazione positiva tra reciprocità e numero di diadi mutue: maggiore la reciprocità, maggiore il numero di diadi mutue che ci aspettiamo di osservare.
Questo secondo indice è uno strumento a nostro avviso interessante per le condizioni di ricerca in cui lo si fa operare:
“If we are not sure of the validity of the probabilistic model, we may find the minimum smin and the maximum smax of s(D’) over all digraph D’ which have the same out-degrees as the given graph D [...] mathematically, the problem can be posed as follows: determine the range of the number of symmetric edges in a digraph with prescribed outdegrees.” [Achuthan ed al., 1982, p. 9].
Con questo indice, nel caso in cui si sia all’oscuro di quale distribuzione di probabilità meglio si adatti alla relazione investigata, siamo comunque in grado di quantificare il numero delle relazioni reciprocate in relazione al numero minimo e massimo possibile di diadi mutue in grafi della stessa dimensione e con lo stesso outdegree.
Condizionando sull’outdegree, calcoliamo il numero minimo Mmin e quello massimo Mmax possibile di diadi mutue in relazione alla struttura del grafo analizzato. Una volta calcolati questi valori, ed appurato che Mmin ≤ M ≤ Mmax[Wasserman et al., 1998, p. 518], avremo che:

L’indice così creato varia tra 0 (nessuna reciprocità) ed 1 (reciprocità massima) ed è svincolato dalla variabilità, essendo relativizzato al campo di variazione, al fine di facilitare valutazioni comparative [Del Vecchio, 1995, p. 133].
Come osserva Wasserman [Wasserman et al., 1998, p. 519], i valori prodotti da questo indice sono in genere più grandi di quelli forniti dall’indice di Katz e Powell. In Appendice C riportiamo il codice relativo alla nostra implementazione dell’indice.
Fino a qui abbiamo esplorato una zona della ricerca scientifica legata alla quantificazione di alcune proprietà delle reti, cioé a quei metodi e a quelle tecniche atte a produrre sintesi numeriche delle proprietà strutturali degli attori e delle loro reti. Un approccio alternativo alla struttura ci viene offerto dalla visualizzazione attraverso il sociogramma.
La visualizzazione dei dati, secondo Freeman, ha giocato un ruolo centrale nella comprensione della struttura delle reti sociali, ma ancor più ha giocato un ruolo centrale nella condivisione di tale comprensione con altri ricercatori. Le tecniche di visualizzazione si sono sviluppate in due distinte forme, una basata sull’uso di vertici ed archi, l’altra basata su matrici di adiacenza. Nella prima forma, i vertici tradizionalmente rappresentano gli attori, mentre gli archi rappresentano le relazioni tra attori; nella seconda forma, le righe e colonne della matrice rappresentano entrambe gli attori e i numeri o i simboli nelle celle rappresentano la connessione tra loro.
In entrambe le forme esistono svariati modi per rappresentare non solo la dicotomia tra presenza o assenza di una relazione, ma anche la sua intensità.
L’importanza di visualizzare i dati fu sottolineata in primis da Jacob Moreno:
“we have first to visualize... [...] A process of charting has been devised by the sociometrists, the sociogram, which is more than merely a method of presentation. It is first of all a method of exploration. It makes possible the exploration of sociometric facts. The proper placement of every individual and of all interrelations of individuals can be shown on a sociogram. It is at present the only available scheme which makes structural analysis of a community possible” [Freeman, 2000, p. 2].
Freeman[Freeman, 2000] individua cinque fasi distinte nello sviluppo di strumenti di rappresentazione dei dati relativi alle reti sociali:
Negli anni ’30 Moreno cominciò a disegnare a mano i primi sociogrammi, usando punti e linee. Freeman sottolinea come questo sociologo sia stato il primo ad usare l’idea della rappresentazione grafica per cercare di scoprire connessioni nei pattern relazionali. Sempre Moreno generalizzò l’approccio della linea come relazione che collega due punti inserendo il concetto di direzione: un arco direzionato rappresentava un attore che comunica con un altro, ma non viceversa. Nascono grazie a questa intuizione i digraph, o grafi direzionati, i quali rivelano molte informazioni strutturali. Nella pratica del disegno, Moreno stabilì una regola tutt’ora valida:
“the fewer the number of lines crossing, the better the sociogram” [Freeman, 2000, p. 3].
Introdusse anche l’idea di usare forme diverse per ogni attore in modo da veicolare informazioni aggiuntive sui dati di attributo di questi all’interno del sociogramma. In ultimo, studiò modalità idonee per il posizionamento dei punti nello spazio. L’uso di una disposizione circolare fu una sua idea: egli cercava, in base alle informazioni ricavate dalla matrice, di posizionare i punti in modo che la posizione scelta accentuasse particolari caratteristiche dell’attore. Quando non aveva informazioni particolari per decidere le posizioni dei punti, semplicemente adottava la distribuzione circolare.
Ricapitolando, dobbiamo a Moreno almeno cinque importanti conquiste in questo campo:
Quest’ultimo punto è stato il centro focale delle prime applicazioni del computer alla analisi di network: una volta creata la matrice di correlazione, il computer calcolava le coordinate dei punti attraverso l’analisi fattoriale; il ricercatore poi aveva il compito di disegnare a mano i punti, usando un sistema di assi di riferimento. Ovviamente il procedimento continuava ad essere manuale, ma un importante passo avanti era stato fatto, poiché adesso i ricercatori disponevano di una procedura standard per trovare le coordinate dei punti, svincolando la leggibilità del grafo dalla perizia del disegnatore e consegnandola ad una tecnica in grado di rendere riproducibile un risultato.
Dal 1960 in poi, aumentata la disponibilità di computer, si fece largo un altro procedimento per determinare il posizionamento dei vertici nel grafo: il multidimensional scaling. Inizialmente sviluppato da Laumann e Guttman (1966), questa procedura si diffuse largamente, anche grazie alla nascita di programmi software dedicati e, dagli anni ’70 in poi, in grado di far stampare automaticamente il grafo da un plotter. Questi software erano capaci di disegnare i punti stabilendone la posizione con lo scaling, e poi, con procedure separate, di individuarne i sottografi.
Da quegli anni in poi, i programmi software sono stati sempre più indirizzati all’analisi dei dati piuttosto che alla loro rappresentazione grafica.
Negli anni ’80 finalmente compaiono i primi software in grado di rappresentare i grafi direttamente sul monitor del computer. Dopo Moreno, si rinnova l’interesse per l’uso del colore, fuori dalle possibilità dei plotter del tempo.
I programmi attualmente disponibili per la Social Network Analysis sono forniti di molti algoritmi in grado di permettere le più svariate esplorazioni dei dati che possono venirci in mente. Freeman rileva che probabilmente il limite di questi programmi è di non essere molto compatibili con alcune piattaforme software: non abbiamo ancora la libertà di avere un file, sottolinea Freeman, scambiabile senza limitazioni alcune di compatibilità:
“anyone who produces an image can email it to others. But the receivers can only inspect it; they cannot themselves manipulate the image in the hope of learning more or of developing a new insight” [Freeman, 2000, p. 14].
La sua speranza consiste nella “lingua franca” del world wide web, attraverso VRML (Virtual Reality Modeling Language) o Java. Noi crediamo che la vera “lingua franca” non sia un linguaggio, ma un protocollo: quale che sia il linguaggio adottato per definirlo, è l’adesione o meno ad un protocollo comune, aperto e condiviso che definisce i limiti di un software. Esiste un protocollo per i grafi, chiamato GraphML1: grazie a questo formato di file possiamo salvare un grafo con tutte le sue proprietà, la forma dei vertici, i tipi di legami, gli attributi degli attori, con la sicurezza che qualunque programma, anche non disponibile per la nostra piattaforma, ma capace di leggere il formato GraphML, sarà in grado di interpretare, importare e poi manipolare come se fosse un oggetto da lui stesso creato. A tutt’oggi, la maggior parte dei software più blasonati in questo settore sono in grado di interpretare questo formato, così come il più diffuso DL.
La visualizzazione dei dati ha quindi costituito una tendenza costante, dedicata al miglioramento della comprensione del mondo sociale. Inizialmente attraverso regole ad hoc caso per caso, con il tempo seguendo regole sempre più standardizzate (in algoritmi), la volontà di usare le rappresentazioni grafiche ha sempre accompagnato il ricercatore.
E con il passare del tempo, il monitor è diventato l’ambiente preferito per la condivisione delle immagini: Freeman osserva come l’uso della computer grafica renda possibile un uso particolare del colore, l’introduzione delle animazioni e dei motori 3D [Freeman, 2000]. La pagina stampata, punto di arrivo degli anni ’70, perde terreno dagli anni ’90 in poi, fino ai giorni nostri in cui possiamo inserire negli articoli digitali direttamente degli oggetti in tre dimensioni con cui il lettore è in grado di interagire. Seguendo una suggestione di Freeman, l’obiettivo sarà allora avere a disposizione un software che ci aiuti nella gestione completa dei dataset di network: dallo storaggio in database relazionali dedicati, all’analisi strutturale ed avanzata, fino alla visualizzazione in due e tre dimensioni2.
Gli strumenti contemporanei di visualizzazione sono quindi mezzi molto potenti per esplorare la struttura di un grafo. La complessità della rappresentazione può essere tuttavia un’arma a doppio taglio: da un lato riesce a veicolare in maniera forte una caratteristica del grafo che vogliamo far emergere; da un altro lato può introdurre nuove informazioni non intenzionate dal ricercatore:
“good visual displays are those that ’tell the truth about the data’ (Tufte, 1983) but often social network data reveal more than one truth” [McGrath et al., 2004, p. 2].
La percezione dell’informazione contenuta in un grafo è influenzata dal layout con cui il grafo viene presentato. Differenti layout sottolineano infatti differenti proprietà del grafo, ed esistono dei fenomeni di salienza che l’utente percepisce guardandolo [McGrath et al., 2004, p. 4]: il vertice posizionato al centro viene ad esempio interpretato come il più importante. Un interessante approccio alla visualizzazione dei dati arriva da un software non commerciale, nato per scopi di ricerca ed insegnamento, chiamato VISONE, sviluppato da Ulrik Brandes e Dorothea Wagner presso l’Università di Konstanz:
“visone is a long-term research project, in which models and algorithms to integrate and advance the analysis and visualization of social networks are being developed. An important part of visone is the design and implementation of a software tool intended for research and teaching in Social Network Analysis. It is specifically designed to allow experts and novices alike to apply innovative and advanced visual methods with ease and accuracy”3.
|
L’idea che ha ispirato questi computer scientists è abbastanza semplice ma allo stesso tempo profonda: i ricercatori sono soliti visualizzare i dati, e poi fare analisi per capire meglio le proprietà strutturali insite in essi, ovvero i dati vengono visualizzati mentre l’informazione che essi contengono computata; perché allora non provare a unire queste due fasi visualizzando direttamente l’informazione contenuta nei dati, rappresentando graficamente i valori degli indicatori strutturali tipici della Social Network Analysis direttamente nel sociogramma?
Attualmente la rappresentazione grafica viene usata per presentare il grafo, mentre i risultati quantitativi dell’analisi vengono inseriti in tabelle riassuntive:
“In network analysis, it is therefore desirable to integrate graphical presentation of the actual network and results from quantitative analyses” [Brandes et al., 2001, p. 2].
Secondo Brandes e Wagner unire l’analisi formale e la presentazione grafica rappresenta un passo in avanti per la ricerca:
“our approach is to contextualize analytic results with the underlying network data by parameterizing the graphical design of visualizations with structural properties. In other words, we want to ‘explain’ derived quantities by showing them simultaneously with the data in a single diagram” [Brandes et al., 2001].
Con l’uso di algoritmi avanzati, da loro stessi progettati, quella che loro chiamano explanatory visualization verrebbe a prodursi automaticamente, portando la rappresentazione grafica dal campo dell’arte a quello della scienza (la rappresentazione che l’algoritmo fa del grafo deve essere, a questo proposito, non solo leggibile, ma possibilmente anche “elegante”).
VISONE visualizza l’informazione contenuta nel grafo in relazione a misure di centralità. Ma questo non è un limite imposto, semplicemente è una fase dello sviluppo del software. Gli sviluppatori hanno deciso di concentrarsi dapprima su quelle caratteristiche che possono risultare più appetibili per un ricercatore di Social Network: avere un grafico che rappresenta direttamente, con una forma diversa, con l’essere al centro di un bersaglio concentrico oppure all’inizio di una scala graduata, i diversi concetti di centralità esposti nel Capitolo 3.
In un certo senso, l’impressione che si trae usando il software è crediamo paragonabile a quella che devono aver suscitato i sociogrammi di Northway paragonati a quelli di Moreno (cfr. figura 10 con figura 12) - quando questi erano comunque già un passo avanti che possedeva “obvious advantages over a verbal or tabular means of presentation” [Bronfenbrenner, 1944, p. 283].
|
L’idea di Northway consisteva nel rendere il sociogramma di Moreno ancora più comprensibile e facile da leggere, anche ad una prima occhiata. L’idea dei progettisti di VISONE non è dissimile nella finalità, e anche, ma questo è detto solo per analogie grafiche, nel modo di proporre i risultati.
Rendere il sociogramma più leggibile significa creare metafore intuitive per l’utente, e adatte a rappresentare il concetto espresso da un tipo di indicatore. Brandes ne ha creata una molto efficace nel rappresentare graficamente lo status:
“Brandes suggested a prescriptive model of layout that uses vertical positioning to convey information about status. Specifically, they suggest to position actors at vertical positions that exactly represent their status score, and by determining horizontal positions algorithmically in such a way that the overall visualization is readable"[McGrath et al., 2004, Brandes et al., 2001].
I benefici della explanatory visualization sono duplici: da un lato, questo tipo di rappresentazione facilità la condivisione dei risultati proprio in virtù della propria leggibilità, e sempre per lo stesso motivo aiuta ad esplorare visivamente i dati.
Il pacchetto sna ha una sola funzione per disegnare sociogrammi, ovvero gplot(). Questa funzione produce una rappresentazione grafica bidimensionale di un grafo g appartenente ad uno stack dat (che potrebbe contenerne più di uno) [Butts, 2005, sna-manual.1.0-0 p.57].
I parametri relativi alla personalizzazione grafica dell’output vengono passati come valori singoli oppure sotto forma di vettori se ogni vertice deve essere, in relazione ad esempio a variabili di attributo, diverso dagli altri.
Il layout del grafo può essere specificato a mano passando direttamente le coordinate per ogni vertice, oppure automatizzato grazie all’uso di diversi algoritmi per il posizionamento dei vertici, riassunti in tabella 4.1.
Coerentemente con lo stile di programmazione del framework, è possibile e previsto l’utilizzo di funzioni di layout personalizzate dall’utente. Per una disamina completa dei vari algoritmi, si rimanda alla documentazione del pacchetto sna, sotto la voce gplot.layouts.
Procediamo adesso ad una semplice visualizzazione:
> g<-rgraph(5)
> g
[, 1] | [, 2] | [, 3] | [, 4] | [, 5] |
|
[1, ] | 0 | 0 | 1 | 0 | 0 |
[2, ] | 0 | 0 | 1 | 0 | 1 |
[3, ] | 0 | 1 | 0 | 0 | 0 |
[4, ] | 1 | 0 | 1 | 0 | 1 |
[5, ] | 0 | 0 | 1 | 1 | 0 |
> gplot(g)
L’immagine prodotta è in figura 4.4.
|
|
Dobbiamo ammettere che il risultato non è molto attraente. Ma ovviamente abbiamo la possibilità di intervenire. Facciamo in modo che compaiano le etichette, e scegliamo una rappresentazione circolare:
> gplot(g, displaylabels=T, mode="circle", xlab="Grafo di Prova", boxed.labels=F, vertex.col=5, vertex.border=4, vertex.cex=1.5)
Ecco il risultato, decisamente più gradevole, in figura 4.5. La customizzazione è ancora molto rudimentale, ma dobbiamo tenere presente che il numero di parametri per questa funzione è davvero elevato, e che quindi le possibilità di intervento sono tantissime. Virtualmente non esiste aspetto del grafo su cui ci sia impossibile intervenire. Per avere un’idea di tutti i parametri a nostra disposizione, rimandiamo a pagina B.
Una ulteriore possibilità di visualizzazione consiste nell’utilizzo della tridimensionalità. Questa ci permette di esplorare in profondità una rete sociale, in una versione per così dire “animata” rispetto a quella bidimensionale precedentemente osservata: possiamo ruotare l’immagine, guardarla da sopra, da sotto, di lato, sfruttando la terza dimensione per evidenziare particolarità nella struttura non realizzabili in un modello bidimensionale.
La funzione si chiama gplot3d() [Butts, 2005, sna-manual.1.0-0 p. 69]. I parametri che ci offre per personalizzare l’immagine sono analoghi a quelli presentati per gplot(); a questi si aggiungono quelli che riguardano la specificazione delle caratteristiche della terza dimensione. Ci soffermeremo qui ad elencare i principali, caratteristici della rappresentazione 3D:
zlab = gestisce etichetta dell’asse Z
vertex.radius = gestisce il raggio dei vertici relativo alla baseline;
absolute.radius = gestisce il raggio dei vertici specificato in termini assoluti
edge.alpha = gestisce il livello di trasparenza per gli archi;
vertex.alpha = gestisce il livello di trasparenza per i vertici.
Creiamo una matrice e rappresentiamola in tre dimensioni:
> g<-rgraph(4)
> g
[, 1] | [, 2] | [, 3] | [, 4] |
|
[1, ] | 0 | 1 | 0 | 1 |
[2, ] | 1 | 0 | 0 | 0 |
[3, ] | 0 | 0 | 0 | 0 |
[4, ] | 1 | 0 | 0 | 0 |
> gplot3d(g, displaylabels=T)
In figura 4.6 possiamo osservare il grafo 3D ottenuto. Dobbiamo ricordare che questa immagine non è altro che, usando una analogia, un’istantanea scattata ad un mondo in 3D: se qualche etichetta di un vertice risulta nascosta, o qualche vertice addirittura poco visibile, abbiamo la possibilità (non riproducibile su carta) di muovere il grafo per adottare prospettive più consone.
Anche la funzione gplot3d() fa uso di algoritmi per decidere la disposizione dei vertici nello spazio; tuttavia le scelte a nostra disposizione questa volta sono in numero minore rispetto a quelle elencate per la precedente funzione (cfr. tabella 4.1).
La principale differenza della rappresentazione 3D rispetto a quella 2D risiede non solo nella tridimensionalità della grafica, ma anche nella capacità di interazione che i motori 3D come OpenGL (ad es. le librerie 3D usate dagli Apple Macintosh) permettono all’utente: rotazioni, traslazioni, zoom e manipolazioni, ottenibili dalla combinazione del movimento del mouse con la pressione di alcuni tasti funzione (tipicamente Shift, Control o Mela, ed Alt).
Da ricordare che questa funzione di sna poggia le sue fondamenta sulla presenza di un altro pacchetto, rgl, che gestisce le rappresentazioni 3D in R. Nato per i sistemi operativi Unix-based, il funzionamento di questo pacchetto implicava fino a poco tempo fa la presenza nel sistema di un X window server, nativo sotto Linux o sotto forma di porting in altri sistemi operativi, come X11 per Apple OS X. Istruzioni in merito all’installazione di rgl si possono trovare all’indirizzo:
http://cran.r-project.org/src/contrib/Descriptions/rgl.html.
Per creare un flusso di lavoro sono necessari pochi elementi: un dataset, un software di analisi ed una minima dimestichezza con un linguaggio di programmazione.
Quando progettiamo un flusso di lavoro, è importante cercare di essere il più ordinati possibile; per questo partiremo da alcune considerazioni sul primo degli elementi citati, ovvero il dataset. Ogni programma dedicato alla Social Network Analysis è dotato di una interfaccia che ci permette di inserire i dati su cui successivamente faremo analisi. Il framework R non fa eccezione (si veda quanto detto nel Capitolo 2).
Nella pratica della ricerca, tuttavia, l’inserimento dei dati è spesso una questione tutt’altro che banale: il numero dei questionari da digitalizzare è quasi sempre elevato e raramente l’operazione viene effettuata direttamente dal ricercatore. è molto più probabile che alla fase di data entry pensino persone scelte ad hoc, spesso meno specializzate del ricercatore, e per questo non familiari con programmi di analisi delle reti sociali (sna, UCINET, Pajek), o statistici in genere (SPSS, SAS, STATA). Non è infrequente quindi assistere all’uso dei fogli di calcolo per la digitalizzazione dei dati. La ragione di questa preferenza è intuitivamente semplice: un programma come Excel, già di per sè molto conosciuto, si presenta all’utente con un’interfaccia talmente immediata che anche un neofita riuscirebbe a inserire i dati dopo un breve colloquio orientativo con il ricercatore.
Ma un foglio elettronico è davvero adatto ai nostri scopi? La risposta è negativa. E non lo è per diversi motivi, tra i principali:
Il fatto che la maggior parte dei software per l’analisi delle reti sociali siano in grado di aprire direttamente o di importare un file di Excel è testimonianza del largo uso che se ne fa. Anche sotto questo aspetto, R non fa eccezione. In R inoltre è possibile importare i file prodotti da SPSS, SAS, STATA, nonché quelli nei formati testo più comuni, come .txt e .csv. Secondo la filosofia di R, dovremo solo caricare il pacchetto giusto per avere le funzioni necessarie ad interpretare i vari formati di file; per i file di testo classici come i .csv, il pacchetto base, preinstallato, ci offre una serie di funzioni idonee, mentre per leggere i file degli altri pacchetti statistici dovremo caricare il pacchetto foreign o il pacchetto gdata (vedi Capitolo 2).
Un metodo certamente più idoneo alla digitalizzazione dei dati ed alla loro conservazione, specialmente nel lungo periodo, consiste nell’uso dei database relazionali.
Che cos’è un database? E che cosa è in particolare un Relational Data Base Management System (RDBMS)? Cominciamo a rispondere alla prima domanda.
“Il termine ’database’, tradotto in italiano con banca dati, base di dati (soprattutto in testi accademici) o anche basedati, indica un insieme di dati riguardanti uno stesso argomento, o più argomenti correlati tra loro, strutturato in modo tale da consentire che i dati possano venire utilizzati per diverse applicazioni e, normalmente, possano evolvere nel tempo.
La base di dati, oltre ai dati veri e propri, deve contenere anche le informazioni sulle loro rappresentazioni e sulle relazioni che li legano.”1
I dati contenuti in un database sono quindi dati in relazione tra loro, ma perchè?
“Un requisito importante di una buona base dati consiste nel non duplicare inutilmente le informazioni in essa contenute: questo è reso possibile dai gestori di database relazionali (teorizzati da Edgar F. Codd), che consentono di salvare i dati in tabelle che possono essere collegate.”2
Il modello relazionale sviluppato nel 1970 da Codd, ingegnere presso la IBM, è:
“... un modello logico di rappresentazione dei dati. Esso si basa sul concetto di relazione e di tabella. Venne proposto per favorire l’indipendenza dei dati e venne reso disponibile come modello logico in DBMS reali nel 1981. È utilizzato per la descrizione delle relazioni esistenti nelle tabelle di un database riutilizzando la nozione di relazione usata nella matematica, in particolare dalla teoria degli insiemi. Il modello relazionale risponde al requisito dell’indipendenza dei dati e prevede una distinzione tra livello fisico (ovvero valori come 5, 8, “bello”, VERO, ...) e livello logico (ovvero natura di tali valori, come numero, stringa, booleano ...).”3
Passiamo a rispondere al secondo dei due quesiti precedenti:
“a partire dalla fine degli anni sessanta, per gestire basi di dati complesse condivise da più applicazioni, si sono utilizzati appositi sistemi software, detti sistemi per la gestione di basi di dati (in inglese "Database Management System" o "DBMS").”4
e ancora:
“Un Database Management System (abbr. DBMS) è un programma informatico (o, più frequentemente, un insieme di programmi) progettato per gestire un database, ovvero un insieme di numerosi dati strutturati. Le operazioni, normalmente, sono richieste da un gran numero di utenti.”5
quindi:
“Il termine Relational Database Management System (RDBMS) indica semplicemente un Database Management System basato sul modello relazionale introdotto da Edgar F. Codd.”6
MySQL non è altro che un RDBMS, composto da un server, in cui risiedono i dati, e da un client usato dall’utente per interrogarlo; è disponibile sia per sistemi Unix-based che per Windows. Per avere un’idea concreta delle possibilità di questo RDBMS, basta considerare il fatto che MediaWiki, il software che gestisce i siti del progetto Wikipedia, è basasto su database MySQL.
Qual è il vantaggio di usare uno software così, almeno ad un primo impatto, imponente? A nostro avviso, la possibilità di gestire con un unico strumento la base dati di un intero gruppo di ricerca; non solo, ma ogni ricercatore appartenente al gruppo avrà accesso ai dati sia in locale che in remoto grazie ad una connessione internet, mentre il server sarà in grado di gestire le richieste di più ricercatori contemporaneamente. Un bel vantaggio, rispetto ad avere, su diverse macchine, diverse versioni di innumerevoli file .sav, .xls. o altro senza mai poter esercitare un controllo completo sulla totalità dei dati disponibili, specialmente a distanza di tempo.
Ultima precisazione: sebbene usando un RDBMS i dati siano accessibili anche in remoto, la procedura di accesso è ristretta ai soli possessori di una autorizzazione preventimente stabilita.
Veniamo a considerazioni più tecniche. Il database MySQL7 è reperibile gratuitamente da internet e si aggira intorno ai 25 MB nella versione standard. Nella pagina dei download sono reperibili i binari per molte architetture. Scelta quella richiesta dalla nostra macchina, procediamo all’installazione seguendo le istruzioni fornite con il software.
Superata questa fase, dobbiamo occuparci di far parlare MySQL ed R. Il pacchetto che si occupa della gestione di questo “dialogo” sui sistemi Unix-Based (Mac OS X e Linux) si chiama RMySQL, a cui si associa, come dipendenza, il pacchetto DBI. L’installazione del pacchetto DBI è automatica e non crea problemi, mentre invece dobbiamo dedicare un attimo di attenzione all’installazione di RMySQL.
Per i sistemi Windows invece il collegamento può essere gestito tramite il pacchetto RODBC8.
Generalmente, quando abbiamo bisogno di installare un pacchetto nuovo, eseguiamo i seguenti passaggi:
A questo punto si apre una finestra nella quale clicchiamo il pulsante Scarica Elenco, in modo da ottenere la lista di tutti i pacchetti disponibili presso il CRAN, come in figura 5.1. Selezioniamo il pacchetto che vogliamo installare e clicchiamo il pulsante Installa Selezionati: R fa tutto da solo, ed alla fine del processo ci ritroviamo con un pacchetto in più e nuove funzioni da usare.
Anche per questo pacchetto l’installazione automatica funziona alla perfezione, ma c’è una condizione: aver fatto una installazione di default di MySQL. In questo caso, il processo di installazione del database avrà installato file nelle cartelle:
/usr/local/lib/mysql
e
/usr/local/include/mysql
R sa già di dover cercare lì quello che gli serve per collegarsi a MySQL e tutto filerà liscio.
Qualora così non fosse, potremmo trovarci di fronte l’errore:
Configuration error:
could not find the MySQL installation include and/or library directories. Manually specify the location of the MySQL libraries and the header files and re-run R CMD INSTALL.
R non è in grado di trovare il nostro database, e ci chiede di passare a mano le informazioni su dove reperire quelle due cartelle.
I motivi per cui scegliere diversi tipi di installazioni sono molti: nel nostro caso, ad esempio, abbiamo usato un interessante pacchetto Free Software chiamato MAMP9 (versione Macintosh dell’acronimo LAMP): in un’unica applicazione abbiamo un server apache, un database MySQL ed uno strumento per l’amministrazione del sistema, il tutto contenuto in un’unica cartella che possiamo liberamente spostare dove vogliamo e che non modifica minimamente l’assetto della nostra macchina. Ovviamente questa comodità la paghiamo con il non avere le due cartelle menzionate sopra all’indirizzo giusto.
Dobbiamo quindi passare ad R i percorsi giusti. Tra R e MySQL deve nascere un ponte, e questo è il modo di costruirlo; in fondo, giunti quasi alla fine del libro, passare parametri ad una funzione non è cosa che ci preoccupi più.
Come abbiamo già detto, quando clicchiamo un pulsante in una interfaccia grafica, in realtà non facciamo che richiamare, in un modo scevro da errori di battitura, un particolare comando. Quando schiacciamo il pulsante Installa Selezionati non facciamo che richiamare il comando R CMD INSTALL. Apriamo allora una finestra di terminale (tipo bash) e scriviamolo noi:
> sudo R CMD INSTALL - -configure-args=’
- -with-mysql-lib=/miopercorso/lib/mysql
- -with-mysql-inc=/miopercorso/include/mysql RMySQL_0.5-7.tar.gz
Premiamo Invio e, quando ci viene chiesta la password, inseriamola premendo di nuovo Invio. Se tutto procede bene, alla fine del processo di compilazione vedremo la scritta:
> done
Nel caso di MAMP, i percorsi corretti sono:
/Applications/MAMP/bin/mysql4/lib/mysql
e
/Applications/MAMP/bin/mysql4/include/mysql
Qualora dovessero esserci problemi, si consiglia di ricorrere ad internet per ricercare soluzioni a quelli che saranno presumibilmente inconvenienti comuni con ogni probabilità già risolti, oppure alle mailing-list di aiuto per ricevere il parere di altri utenti (in questo caso leggiamo attentamente, prima di spedire un messaggio, la Netiquette della lista per assicurarci di rispettare i requisiti richiesti).
Visto che del software abbiamo già abbondantemente discusso, non resta che parlare brevemente della programmazione.
Come abbiamo precedentemente osservato, R non è solo una collezione di pacchetti software, ma anche un ambiente dove possiamo sviluppare idee e soluzioni nuove per risolvere i nostri problemi. Non potremmo propriamente “sviluppare” niente, tuttavia, se non avessimo modo di usare un linguaggio per programmare le azioni che vogliamo che R esegua. Queste azioni possono andare dall’elencare una semplice sequenza di funzioni da computare, alla creazione di cicli, alla verifica di condizioni, fino alla creazione di complicatissime routine.
Il linguaggio contenuto in R è un linguaggio orientato agli oggetti10. Come tutti i linguaggi di questo tipo possiede dei costrutti di controllo che gli permettono di gestire il flusso con cui vengono eseguiti i vari comandi. In pratica, questi costrutti riescono ad alterare “l’andamento tipicamente sequenziale con cui vengono elaborate normalmente le istruzioni” [Crivellari, 2006, p. 128].
Ovviamente, più a fondo conosciamo un linguaggio, più cose siamo in grado di dire. In questo caso, più a fondo conosciamo un linguaggio di programmazione, più cose siamo in grado di far fare alla macchina. Tuttavia, per cominciare ad essere seriamente produttivi con R, non è necessario imparare che poche rudimentali costruzioni logiche, in particolare come fare un ciclo (istruzione for()), come verificare una condizione (istruzione if()). In rete potremo poi trovare numerosi esempi per soddisfare necessità più complesse, senza dimenticare la possibilità di aiuto direttamente dagli altri utenti via mailing-list.
Vediamo quindi di familiarizzare con i due costrutti che utilizzeremo successivamente, cominciando con il costrutto condizionale if():
if(espressione da verificare){istruzioni da eseguire in caso di verità}
Concettualmente è molto semplice, e può essere tradotto con:
se A è vero allora fai B
Quindi, se R verifica che la condizione che abbiamo imposto è vera, eseguirà tutte le istruzioni contenute nel blocco racchiuso in parentesi graffe; in caso contrario, non farà nulla. Sulla costruzione della condizione potremo discutere a lungo, e fare esempi molto complessi con operatori logici di connessione tra più condizioni; tuttavia, limitandoci all’esempio che vedremo, useremo una condizione molto semplice, ovvero diremo ad R “il file x esiste”, a cui lui potrà rispondere VERO o FALSO; come abbiamo ormai capito, se R risponde VERO, eseguirà dei comandi, altrimenti non farà nulla e passerà oltre.
Veniamo ora al costrutto iterativo for():
for(variabile in sequenza){istruzioni da eseguire fino a che siamo dentro alla sequenza}
Questa istruzione fa eseguire ad R una serie di comandi per un certo numero di volte, e può essere tradotta con:
conta da 1 a x, per ogni numero che conti esegui questi comandi, quando hai finito di contare fermati
Noi dobbiamo in questo caso impostare una sequenza di numeri da contare; visto che abbiamo 500 reti da esaminare, il nostro ciclo sarà lungo da 1 a 500. Per ogni numero conteggiato, R eseguirà dei comandi. Alla fine del ciclo, ed eseguita l’ultima serie di comandi (la cinquecentesima), R si fermerà.
Veniamo infine al nostro esempio: abbiamo un dataset composto dagli ego-network di circa 500 anziani, raccolto nel 2003. Per ogni rete, siamo interessati a conoscere alcune misure che ci saranno utili per capire le caratteristiche della dimensione relazionale del campione.
Il dataset è stato digitalizzato in circa 500 file di Excel, uno per ogni matrice di adiacenza. Le matrici in Excel appaiono come in figura 5.2. I file sono ovviamente separati tra loro, quindi non c’è modo di interrogare i dati se non accedendo separatamente ad ogni file, analizzando i dati, salvando l’output e così via. Un compito decisamente noioso e ripetitivo da eseguire.
Allora perché farlo noi? Facciamolo fare ad R.
Il nostro obiettivo è leggere i file, un file dopo l’altro: dobbiamo quindi caricare il pacchetto gdata, il quale contiene una funzione idonea alla lettura dei file .xls di Excel, read.xsl():
read.xls(xls, sheet=1, verbose=FALSE, ..., perl="perl")
Dobbiamo quindi caricare il pacchetto sna per poter avere accesso alla libreria di funzioni per l’analisi di rete; ed infine dobbiamo caricare il pacchetto che si occupa del dialogo tra R ed il database dove vogliamo salvare i risultati, in modo non solo da poterli utilizzare in una seconda fase di analisi, ma anche da archiviarli con sicurezza per usi futuri. Dato che il database che abbiamo scelto è MySQL, caricheremo il pacchetto RMySQL, estensione per questo database del pacchetto DBI, il quale contiene le (poche) funzioni che saranno utili ai nostri scopi, ovvero dbConnect(), dbWriteTable(), mysqlCloseConnection():
dbConnect(drv, ...)
dbWriteTable(conn, name, value, row.names = T, ..., overwrite = F, append = F)
mysqlCloseConnection(con, ...)
Dobbiamo quindi scrivere la funzione che si occuperà del lavoro “sporco” al posto nostro. Ricordiamo che una funzione deve rispettare la seguente sintassi:
nome <- function(argomento) { comandi }
Dal menu File di R clicchiamo su Nuovo documento: si aprirà un file vuoto, nel quale scriveremo il codice della funzione. Cominciamo dandogli un nome, e dichiarando che essa è, appunto, una funzione:
analisiAnziani<-function(){
Poi carichiamo i pacchetti necessari:
analisiAnziani<-function(){
library(gdata);
library(RMySQL);
library(sna);
A questo punto abbiamo bisogno di creare una istruzione condizionale, ovvero dobbiamo dire ad R di fare qualcosa per un certo numero di volte, fino a che una condizione non sarà soddisfatta. Tutto questo lungo discorso nel linguaggio di programmazione di R si riassume con l’istruzione for():
analisiAnziani<-function(){
library(gdata);
library(RMySQL);
library(sna);
for(i in 1:500){
...
}
I nostri file Excel sono numerati nella forma “SOCIOGRAMMA_1.XLS”, “SOCIOGRAMMA_2.XLS”, ecc..., ma nulla ci assicura che la numerazione progressiva sia completa, ovvero che tutti i numeri siano presenti; per eliminare il rischio di possibili errori, assicuriamoci che un file esista prima di dire ad R di andare a leggerne il contenuto:
file=paste("SOCIOGRAMMA_", i, ".XLS", sep="");
if(file.exists(file)){
...
}
A questo punto, possiamo dire ad R di leggere il file di cui ci siamo assicurati la presenza:
z<-read.xls(file)
Le matrici in Excel necessitano di essere simmetrizzate prima di poterle sottoporre ad analisi. sna possiede una funzione per farlo automaticamente:
a<-symmetrize(as.matrix(z),rule="lower");
Adesso possiamo chiedere a sna di calcolare le misure di rete di cui abbiamo bisogno. In questo caso, abbiamo deciso di calcolare densità, ampiezza e alcune misure di centralizzazione delle reti:
densita<-gden(a);
ampiezza<-dim(a);
centDeg<-centralization(a, degree, mode="graph");
centClo<-centralization(a, closeness, mode="graph");
centBet<-centralization(a, betweenness, mode="graph");
centEigen<-centralization(a, evcent, mode="graph");
A questo punto non resta che salvare l’output da qualche parte. Per le ragioni espresse nel precedente paragrafo, scegliamo un database, dove scriveremo non solo i valori delle misure scelte, ma copieremo anche le singole matrici, in modo da eliminare i 500 file Excel e da costruire un dataset facilmente accessibile e condivisibili per usi futuri. Non resta quindi che collegarsi al database:
conn<-dbConnect(drv="MySQL", username="UserName", password="Password", dbname="NomeDB")
Dobbiamo convertire le matrici in dataframe assegnandogli dinamicamente un nome, mentre dobbiamo inserire i valori delle misure di rete calcolate in un altro dataframe in cui ogni riga sia identificativa di ego, il tutto prima di poter scrivere l’output nel database:
EgoNet<-data.frame(a);
nomeTB<-paste("sociogramma_", i, sep="");
analisiEgoNet<-data.frame("densita"=densita, "ampiezza"=ampiezza[1], "centDeg"=centDeg, "centClo"=centClo, "centBet"=centBet, "centEigen"=centEigen);
row.names(analisiEgoNet)<-i;
A questo punto non resta che scrivere nel database:
dbWriteTable(conn, nomeTB, printEgoNet, overwrite=T, append=F, row.names=T);
dbWriteTable(conn, "Tabella_Sintetica", analisiEgoNet, overwrite=F, append=T, row.names=T);
Ricordiamoci di chiudere la connessione precedentemente aperta con il database:
mysqlCloseConnection(conn);
}
}
E di scrivere a monitor un messaggio per avvisarci della fine dell’esecuzione di tutta la routine:
return(“Ho finito di analizzare le matrici.”)
}
Il codice è pronto per l’uso; possiamo salvare il file e chiuderlo. Torniamo quindi al menu File di R e clicchiamo su Elabora File, scegliendo proprio il file che abbiamo appena salvato. Appena R avrà finito di elaborarlo, esso diventerà una vera e propria funzione eseguibile, esattamente come quelle che abbiamo a disposizione grazie ai vari pacchetti. Sarà a questo punto sufficiente digitare nella console di R il comando:
> analisiAnziani()
per avviare l’analisi. Alla fine del lavoro (i tempi sono strettamente legati alle capacità della macchina su cui lavoriamo), avremo nel nostro database tutte le matrici correttamente archiviate ed una tabella riepilogativa con i valori delle misure di rete per i nostri 500 soggetti, tabella su cui potremo fare ulteriori analisi per cercare medie, tendenze e così via - questa volta, però, usando la funzione che permette ad R non di scrivere ma di leggere i dati da un database.
Di seguito il codice completo della nostra prima funzione:
analisiAnziani<-function(){
# carico i pacchetti di cui avrò bisogno
library(gdata)
library(RMySQL)
library(sna)
# creo il loop per far ripetere l’azione ad R per 500 volte
for(i in 1:500){
# creo dinamicamente il nome del file da cercare
file=paste("SOCIOGRAMMA_", i, ".xls", sep="");
# mi assicuro che esista il file
if(file.exists(file)){
# leggo il file Excel e simmetrizzo la matrice
z<-read.xls(file);
a<-symmetrize(as.matrix(z),rule="lower");
# misure di rete da calcolare
densita<-gden(a);
ampiezza<-dim(a);
centDeg<-centralization(a, degree, mode="graph");
centClo<-centralization(a, closeness, mode="graph");
centBet<-centralization(a, betweenness, mode="graph");
centEigen<-centralization(a, evcent, mode="graph");
# connessione al database
conn<-dbConnect(drv="MySQL", username="UserName", password="Password", dbname="NomeDB")
# inserisco i risultati dell’analisi in un dataframe
analisiEgoNet<-data.frame("densita" = round(densita, 2), "ampiezza" = ampiezza[1], "centDeg" = round(centDeg, 2), "centClo" = round(centClo, 2), "centBet" = round(centBet, 2), "centEigen" = round(centEigen, 2));
# converto la variabile a da matrix in dataframe in modo che possa essere scritto sul database
egoNet<-data.frame(a);
# i risultati del soggetto n saranno sulla riga n della tabella riepilogativa
row.names(analisiEgoNet)<-i;
# il nome della tabella n sarà sociogramma_n, ma può essere cambiato a piacere
nomeTB<-paste("sociogramma_", i, sep="");
# scrivo l’i-esimo ego-network nel database
dbWriteTable(conn, nomeTB, egoNet, overwrite=T, append=F, row.names=T);
# scrivo l’i-esima riga nella tabella riepilogativa del database
dbWriteTable(conn, "Tabella_Sintetica", analisiEgoNet, overwrite=F, append=T, row.names=T);
# chiudo la connessione con il database
mysqlCloseConnection(conn);
}
}
# dico ad R di avvisarmi quando ha finito tutto il lavoro
return("done.")
}
La conoscenza di un linguaggio non influenza solo la quantità di cose che siamo in grado di chiedere, ma anche il modo, l’eleganza, la minore prolissità con cui si diventa in grado di riferirci addirittura alle medesime cose.
Non crediamo quindi che il codice sopra presentato sia il miglior codice possibile. Probabilmente, anzi sicuramente, ricercatori più esperti di noi sarebbero in grado di ottenere gli stessi risultati usando un numero minore di istruzioni.
L’uso del pacchetto sna nell’ambito della Social Network Analysis ci sembra molto promettente. Certamente esistono software commerciali molto più avanzati, e forse più semplici da usare, almeno ad un primo approccio. Tuttavia riteniamo che il vantaggio competitivo di sna sia quello di essere un pacchetto aperto all’interno di un framework libero. In questo modo, e grazie alla comunità che lo supporta, l’utente entra a far parte di una rete collaborativa ricca non solo di consigli tecnici, ma anche di discussioni teoriche e metodologiche essenziali per lo sviluppo della conoscenza e del metodo.
All’interno di R non esistono limiti, se non quelli dettati dalla propria creatività. Il potere è nelle mani dell’utente, messo in grado di creare le proprie soluzioni usando gli strumenti del framework, qualora queste non fossero direttamente disponibili.
Il numero di pacchetti, sviluppati dai ricercatori delle più svariate discipline, è veramente ampio; e quelli usati in questo libro sono solo alcuni (pochissimi) dei molti disponibili presso il già citato CRAN. A questo repository potremmo aggiungerne altri, tra cui citiamo BioConductor per la qualità e la vastità dell’assortimento.
Abbiamo imparato a calcolare le principali misure di analisi strutturale con sna. Nonostante l’assenza di una interfaccia grafica, abbiamo visto con che semplicità sia possibile richiamare ogni funzione. Dobbiamo ricordare ancora una volta che l’uso di misure di rete nasconde l’uso di concetti di rete, e come questi debbano essere le vere ragioni delle nostre scelte.
Abbiamo visto l’importanza della visualizzazione, della sua evoluzione, e le possibilità che sna ci offre per disegnare grafi, personalizzandone il layout.
Abbiamo visto inoltre la facilità con cui è possibile automatizzare compiti ripetitivi, semplicemente scrivendo una lista di comandi che R interpreterà come una nuova funzione da eseguire. Il vantaggio è quello di poterci concentrare esclusivamente sulla parte teorica della ricerca, ovvero su quali siano le misure di rete significative per il caso che stiamo analizzando; una volta definita la struttura logica dell’analisi, poche righe di codice saranno sufficienti a che la macchina esegua le routinarie operazioni necessarie alla produzione dell’output, da cui probabilmente ripartiremo con nuove logiche di analisi per noi e nuove routine di elaborazioni per lei.
Abbiamo da poco dato vita ad un nostro progetto, sempre legato all’analisi delle reti sociali, sviluppando alcune funzioni sulla base degli indici di mutualità descritti da Wasserman e Faust nel loro Social Network Analysis [Wasserman et al., 1998]. Non ci troviamo ovviamente all’interno di logiche di mercato, per cui due pacchetti sulla Social Network Analysis potrebbero entrare in conflitto tra loro, così come potrebbero farlo i loro autori. Anzi, la collaborazione è la norma, come dimostrato in questo lavoro dalle preziose parole del Prof. Carter Butts, prodigo di consigli non solo per la creazione di rete, ma anche per lo sviluppo di una interfaccia grafica al suo pacchetto sna tramite RCmdr.
Questo atteggiamento distingue la tipologia di sviluppo della ricerca con software libero. Non è necessario, ogni volta, reinventare la ruota: ognuno è libero di sviluppare il proprio progetto a partire da quello che gli altri hanno fatto fino ad ora. Il nostro pacchetto è costruito completamente sopra sna, e non potrebbe essere altrimenti. A meno di non riscriverne tutte le funzioni. Ecco la ragione della creatività dei progetti liberi: avere la possibilità di concentrarsi direttamente sulla propria idea, usando le “ruote” costruite dagli altri.
Altro ambito di cui vorremmo brevemente discutere riguarda la sicurezza e l’affidabilità di R. Non solo esso è sviluppato liberamente da statistici e programmatori, ma è quotidianamente oggetto delle attenzioni di ricercatori che cercano di portarlo ai limiti delle sue possibilità, stressandolo molto più di quello che potrebbe avvenire in comuni contesti aziendali: un baco in R verrebbe perciò scoperto e risolto in poco tempo. E nessuno avrebbe interesse a nasconderlo.
Ulteriore aspetto, che riteniamo fondamentale in un contesto di ricerca, come quello a cui ci riferiamo, e che direttamente discende dalle considerazioni sopra esposte riguardo alle possibilità di personalizzazione del software, è quello della conoscenza profonda che possiamo avere di quello che R fa. Per ogni funzione di ogni pacchetto che usiamo nel framework abbiamo la possibilità di leggerne il codice: è una considerazione apparentemente banale, di nessuna utilità quando tutto va per il meglio. Ma se ci trovassimo di fronte a risultati inattesi, che persistono nonostante i nostri attenti controlli e l’aver ripetuto più e più volte le corrette procedure, allora la possibilità di esaminare i passi compiuti dalla macchina sarebbe la soluzione ideale.
Ovviamente, vedere cosa fa la macchina e capire cosa fa la macchina non sono la stessa cosa. Il sociologo non è un informatico. Ma questo è un problema minore e facilmente risolvibile con una consulenza; quello che a noi interessa è avere la possibilità, importante e spesso sottovalutata categoria del pensiero e dell’azione, di vedere. Non c’è nessuna libertà senza la possibilità di conoscere ed eventualmente fare scelte diverse.
Al livello di analisi strutturale, questo problema resta sullo sfondo, trattandosi di algoritmi semplici e unanimemente condivisi (se qualche errore dovesse mai verificarsi, sarebbe sicuramente imputabile ad una cattiva implementazione, piuttosto che a problemi teorici). Ma quando aumenta la complessità della ricerca, e si tentano analisi esplicative, allora la situazione cambia. Entriamo in un ambito in cui i modelli sono spesso problematici da interpretare, in cui ogni gruppo di ricerca ne sviluppa di propri, ed in cui spesso ad ogni modello corrisponde un pacchetto software (non libero) per fare analisi.
In questi contesti il ricercatore è costretto a “fidarsi” dell’implementazione dei modelli che ne hanno fatto i loro ideatori. Se ottiene dei risultati su cui è incerto, non ha altra strada che chiedere lumi ai progettisti. Non ha modo di verificare, in autonomia, quello che sta facendo ed i cui risultati saranno alla base delle successive speculazioni teoriche.
Crediamo che il software libero, nella ricerca, sia di fondamentale importanza per diffondere la conoscenza, e per dare al ricercatore uno strumento aperto in cui possa unire teoria e metodo, liberamente.
# BETWEENNESS:
betweenness(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, tmaxdev=FALSE, cmode="directed", geodist.precomp=NULL, rescale=FALSE)
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g=1.
nodes vector indicating which nodes are to be included in the calculation. By default, all nodes are included. gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected.
gmode is set to "digraph" by default.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
cmode string indicating the type of betweenness centrality being computed (directed or undirected geodesics).
geodist.precomp A geodist object precomputed for the graph to be analyzed (optional)
rescale if true, centrality scores are rescaled such that they sum to 1.
# BONPOW:
bonpow(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, tmaxdev=FALSE, exponent=1, rescale=FALSE, tol=1e-07)
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g=1.
nodes vector indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. This is currently ignored.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. Diag is FALSE by default.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev=FALSE.
exponent exponent (decay rate) for the Bonacich power centrality score; can be negative
rescale if true, centrality scores are rescaled such that they sum to 1.
tol tolerance for near-singularities during matrix inversion.
# CENTRALIZATION:
centralization(dat, FUN, g=1, mode="digraph", diag=FALSE, normalize=TRUE, ...)
Argomenti:
dat one or more input graphs.
FUN Function to return nodal centrality scores.
g Integer indicating the index of the graph for which centralization should be computed. By default, g=1.
mode String indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. mode is set to "digraph" by default.
diag Boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
normalize Boolean indicating whether or not the centralization score should be normalized to the theoretical maximum. (Note that this function relies on FUN to return this value when called with tmaxdev==TRUE.) By default, tmaxdev==TRUE.
... Additional arguments to FUN.
# CLOSENESS:
closeness(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, tmaxdev=FALSE, cmode="directed", geodist.precomp=NULL, rescale=FALSE)
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g=1.
nodes list indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. gmode is set to "digraph" by default.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
cmode string indicating the type of closeness centrality being computed (distances on directed or undirected geodesics).
geodist.precomp a geodist object precomputed for the graph to be analyzed (optional)
rescale if true, centrality scores are rescaled such that they sum to 1.
# GRADI:
degree(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, tmaxdev=FALSE, cmode="freeman", rescale=FALSE)
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g=1.
nodes vector indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. gmode is set to "digraph" by default.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
cmode string indicating the type of degree centrality being computed. "indegree", "out- degree", and "freeman" refer to the indegree, outdegree, and total (Freeman) degree measures, respectively. The default for cmode is "freeman".
rescale if true, centrality scores are rescaled such that they sum to 1.
# EVCENT:
evcent(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, tmaxdev=FALSE, rescale=FALSE)
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g=1.
nodes vector indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. This is currently ignored.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
rescale if true, centrality scores are rescaled such that they sum to 1.
# CONECTEDNESS:
connectedness(dat, g=NULL)
Argomenti:
dat one or more graphs.
g index values for the graphs to be utilized; by default, all graphs are selected.
# GEODIST:
geodist(dat, inf.replace=Inf)
Argomenti:
dat one or more input graphs.
inf.replace the value to use for geodesic distances between disconnected nodes; by default, this is equal Inf.
# GPLOT:
gplot(dat, g = 1, gmode = "digraph", diag = FALSE, label = c(1:dim(dat)[2]), coord = NULL, jitter = TRUE, thresh = 0, usearrows = TRUE, mode = "fruchtermanreingold", displayisolates = TRUE, interactive = FALSE, xlab = NULL, ylab = NULL, xlim = NULL, ylim = NULL, pad = 0.2, label.pad = 0.5, displaylabels = FALSE, boxed.labels = TRUE, label.pos = 0, label.bg = "white", vertex.sides = 8, vertex.rot = 0, arrowhead.cex = 1, label.cex = 1, loop.cex = 1, vertex.cex = 1, edge.col = 1, label.col = 1, vertex.col = 2, label.border = 1, vertex.border = 1, edge.lty = 1, label.lty = NULL, vertex.lty = 1, edge.lwd = 0, label.lwd = par("lwd"), edge.len = 0.5, edge.curve = 0.1, edge.steps = 50, loop.steps = 20, object.scale = 0.01, uselen = FALSE, usecurve = FALSE, suppress.axes = TRUE, vertices.last = TRUE, new = TRUE, layout.par = NULL, ...)
Esaminiamo i parametri principali per configurare e personalizzare il proprio grafico:
“dat a graph or set thereof. This data may be valued.
g integer indicating the index of the graph which is to be plotted. By default, g==1.
gmode String indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undi- rected; "twomode" indicates that data should be interpreted as bimodal (i.e., rows and columns are distinct vertex sets). gmode is set to "digraph" by default.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
label a vector of vertex labels, if desired; defaults to the vertex index number.
coord user-specified vertex coordinates, in an NCOL(dat)x2 matrix. Where this is specified, it will override the mode setting.
jitter boolean; should the output be jittered?
thresh real number indicating the lower threshold for tie values. Only ties of value > thresh are displayed. By default, thresh=0.
usearrows boolean; should arrows (rather than line segments) be used to indicate edges?
mode the vertex placement algorithm; this must correspond to a gplot.layout function.58 gplot
displayisolates boolean; should isolates be displayed?
interactive boolean; should interactive adjustment of vertex placement be attempted?
xlab x axis label.
ylab y axis label.
xlim the x limits (min, max) of the plot.
ylim the y limits of the plot.
pad amount to pad the plotting range; useful if labels are being clipped.
label.pad amount to pad label boxes (if boxed.labels==TRUE), in character size units.
displaylabels boolean; should vertex labels be displayed?
boxed.labels boolean; place vertex labels within boxes?
label.pos position at which labels should be placed, relative to vertices. 0 results in labels which are placed away from the center of the plotting region; 1, 2, 3, and 4 result in labels being placed below, to the left of, above, and to the right of vertices (respectively); and label.pos>=5 results in labels which are plotted with no offset (i.e., at the vertex positions).
label.bg background color for label boxes (if boxed.labels==TRUE); may be a vector, if boxes are to be of different colors.
vertex.sides number of polygon sides for vertices; may be given as a vector, if vertices are to be of different types.
vertex.rot angle of rotation for vertices (in degrees); may be given as a vector, if vertices are to be rotated differently.
arrowhead.cex expansion factor for edge arrowheads.
label.cex character expansion factor for label text.
loop.cex expansion factor for loops; may be given as a vector, if loops are to be of differ- ent sizes.
vertex.cex expansion factor for vertices; may be given as a vector, if vertices are to be of different sizes.
edge.col color for edges; may be given as a vector or adjacency matrix, if edges are to be of different colors.
label.col color for vertex labels; may be given as a vector, if labels are to be of different colors.
vertex.col color for vertices; may be given as a vector, if vertices are to be of different colors.
label.border label border colors (if boxed.labels==TRUE); may be given as a vector, if label boxes are to have different colors.
vertex.border border color for vertices; may be given as a vector, if vertex borders are to be of different colors.
edge.lty line type for edge borders; may be given as a vector or adjacency matrix, if edge borders are to have different line types.
label.lty line type for label boxes (if boxed.labels==TRUE); may be given as a vector, if label boxes are to have different line types.gplot 59
vertex.lty line type for vertex borders; may be given as a vector or adjacency matrix, if vertex borders are to have different line types.
edge.lwd line width scale for edges; if set greater than 0, edge widths are scaled by edge.lwd*dat. May be given as a vector or adjacency matrix, if edges are to have different line widths.
label.lwd line width for label boxes (if boxed.labels==TRUE); may be given as a vector, if label boxes are to have different line widths.
edge.len if uselen==TRUE, curved edge lengths are scaled by edge.len.
edge.curve if usecurve==TRUE, the extent of edge curvature is controlled by edge.curv. May be given as a fixed value, vector, or adjacency matrix, if edges are to have different levels of curvature.
edge.steps for curved edges (excluding loops), the number of line segments to use for the curve approximation.
loop.steps for loops, the number of line segments to use for the curve approximation.
object.scale base length for plotting objects, as a fraction of the linear scale of the plotting region. Defaults to 0.01.
uselen boolean; should we use edge.len to rescale edge lengths?
usecurve boolean; should we use edge.curve?
suppress.axes boolean; suppress plotting of axes?
vertices.last boolean; plot vertices after plotting edges?
new boolean; create a new plot? If new==FALSE, vertices and edges will be added to the existing plot.
layout.par parameters to the gplot.layout function specified in mode.
... additional arguments to plot.
# GPLOT3D:
gplot3d(dat, g = 1, gmode = "digraph", diag = FALSE, label = c(1:dim(dat)[2]), coord = NULL, jitter = TRUE, thresh = 0, mode = "fruchtermanreingold", displayisolates = TRUE, displaylabels = FALSE, xlab = NULL, ylab = NULL, zlab = NULL, vertex.radius = NULL, absolute.radius = FALSE, label.col = "gray50", edge.col = "black", vertex.col = "red", edge.alpha = 1, vertex.alpha = 1, edge.lwd = NULL, suppress.axes = TRUE, new = TRUE, bg.col = "white", layout.par = NULL)
# GRECIP:
grecip(dat, g = NULL, measure = c("dyadic", "dyadic.nonnull", "edgewise"))
“dat one or more input graphs.
g a vector indicating which graphs to evaluate (optional).
measure one of ’dyadic’ (default), ’dyadic.nonnull’ or ’edgewise’.
# EFFICIENCY:
efficiency(dat, g=NULL, diag=FALSE)
dat one or more graphs.
g index values for the graphs to be utilized; by default, all graphs are selected.
diag TRUE if the diagonal contains valid data; by default, diag==FALSE.
# GDEN:
gden(dat, g=NULL, diag=FALSE, mode="digraph")
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graphs for which the density is to be calculated (or a vector thereof). If g==NULL (the default), density is calculated for all graphs in dat.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
mode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undi- rected. mode is set to "digraph" by default.
# HIERARCHY:
hierarchy (dat, g=NULL, measure=c("reciprocity", "krackhardt"))
Argomenti:
dat a stack of input graphs.
g index values for the graphs to be utilized; by default, all graphs are selected.
measure one of ’reciprocity’ or ’krackhardt’"
# INFOCENT:
infocent(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, cmode="weak", tmaxdev=FALSE, rescale=FALSE,tol=1e-20)
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g==1.
nodes list indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. This is currently ignored.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
cmode the rule to be used by symmetrize when symmetrizing dichotomous data; must be one of "weak" (for an OR rule), "strong" for an AND rule), "upper" (for a max rule), or "lower" (for a min rule). Set to "weak" by default, this parameter obviously has no effect on symmetric data.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
rescale if true, centrality scores are rescaled such that they sum to 1.
tol tolerance for near-singularities during matrix inversion.
# IS.CONNECTED:
is.connected(g, connected = "strong", comp.dist.precomp = NULL)
Argomenti:
g one or more input graphs.
connected definition of connectedness to use; must be one of "strong", "weak", "unilateral", or "recursive".
comp.dist.precomp a component.dist object precomputed for the graph to be analyzed (optional).
# IS.ISOLATES:
is.isolate(dat, ego, g=1, diag=FALSE)
Argomenti:
dat one or more input graphs.
ego index of the vertex (or a vector of vertices) to check.
g which graph should be examined?
diag boolean indicating whether adjacency matrix diagonals (i.e., loops) contain meaningful data.
# ISOLATES:
isolates(dat, diag=FALSE)
Argomenti:
dat one or more input graphs.
diag boolean indicating whether adjacency matrix diagonals (i.e., loops) contain mean- ingful data.
# LUBNESS:
lubness(dat, g=NULL)
Argomenti:
dat one or more input graphs.
g index values for the graphs to be utilized; by default, all graphs are selected.
# MUTUALITY:
mutuality(dat, g=NULL)
Argomenti:
dat one or more input graphs.
g a vector indicating which elements of dat should be analyzed; by default, all graphs are included.
# DYAD.CENSUS:
dyad.census(dat, g=NULL)
Argomenti:
dat one or more graphs.
g the elements of dat to be included; by default, all graphs are processed. "
# CONSENSUS:
consensus(dat, mode="digraph", diag=FALSE, method="central.graph", tol=1e-06)
Argomenti:
dat a set of input graphs (must have same order).
mode "digraph" for directed data, else "graph".
diag a boolean indicating whether the diagonals (loops) should be treated as data.
method one of "central.graph", "single.reweight", "iterative.reweight" "PCA.reweight", "LAS.intersection", "LAS.union", "OR.row", or "OR.col".
tol tolerance for the iterative reweighting algorithm.
# NTIES:
nties(dat, mode="digraph", diag=FALSE)
Argomenti:
dat a graph or set thereof.
mode one of ’digraph’, ’graph’, and ’hgraph’.
diag a boolean indicating whether or not diagonal entries (loops) should be treated as valid data; ignored for hypergraphic (’hgraph’) data.
# PRESTIGE:
prestige(dat, g=1, nodes=NULL, gmode="digraph", diag=FALSE, cmode="indegree", tmaxdev=FALSE, rescale=FALSE, tol=1e-07)
Argomenti:
dat one or more input graphs.
g integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g==1.
nodes vector indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. gmode is set to "digraph" by default.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
cmode one of "indegree", "indegree.rownorm", "indegree.rowcolnorm", "eigenvector", "eigenvector.rownorm", "eigenvector.colnorm", "eigenvector.rowcolnorm", "domain", or "domain.proximity".
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
rescale if true, centrality scores are rescaled such that they sum to 1.
tol Currently ignored.
# PLOT.SOCIOMATRIX:
plot.sociomatrix(x, labels=NULL, drawlab=TRUE, diaglab=TRUE, drawlines=TRUE, xlab=NULL, ylab=NULL, cex.lab=1, ...)
Argomenti:
x an input graph.
labels a list containing the vectors of row and column labels (respectively); defaults to numerical labels.
drawlab logical; add row/column labels to the plot?
diaglab logical; label the diagonal?
drawlines logical; draw lines to mark cell boundaries?
xlab x axis label.
ylab y axis label.
cex.lab optional expansion factor for labels.
... additional arguments to plot."
# REACHABILITY:
reachability(dat, geodist.precomp=NULL)
Argomenti:
dat one or more graphs (directed or otherwise).
geodist.precomp optionally, a precomputed geodist object.
# RGNM:
rgnm(n, nv, m, mode = "digraph", diag = FALSE)
Argomenti:
n the number of graphs to generate.
nv the size of the vertex set ( |V (G)|) for the random graphs.
m the number of edges on which to condition.
mode "digraph" for directed graphs, or "graph" for undirected graphs. diag boolean; should loops be allowed?
# RGRAPH:
rgraph(n, m=1, tprob=0.5, mode="digraph", diag=FALSE, replace=FALSE, tielist=NULL)
Argomenti:
n The size of the vertex set (|V(G)|) for the random graphs
m The number of graphs to generate
tprob Information regarding tie (edge) probabilities; see below
mode ’digraph’ for directed data, ’graph’ for undirected data
diag Should the diagonal entries (loops) be set to zero?
replace Sample with or without replacement from a tie list (ignored if tielist==NULL)
tielist A vector of edge values, from which the new graphs should be bootstrapped”
# RGUMAN:
rguman(n, nv, mut = 0.25, asym = 0.5, null = 0.25, method = c("probability", "exact"))
Argomenti:
n the number of graphs to generate.
nv the size of the vertex set ( |V (G)|) for the random graphs.
mut if method=="probability", the probability of obtaining a mutual dyad; otherwise, the number of mutual dyads.
asym if method=="probability", the probability of obtaining an asymmetric dyad; otherwise, the number of asymmetric dyads.
null if method=="probability", the probability of obtaining a null dyad; otherwise, the number of null dyads.
method the generation method to use. "probability" results in a multinomial dyad distribution (conditional on the underlying rates), while "exact" results in a uniform draw conditional on the exact dyad distribution.
# STRESSCENT:
stresscent(dat, g=1, nodes=c(1:dim(dat)[2]), gmode="digraph", diag=FALSE, tmaxdev=FALSE, cmode="directed", geodist.precomp=NULL, rescale=FALSE)
Argomenti:
dat one or more input graphs.
g Integer indicating the index of the graph for which centralities are to be calculated (or a vector thereof). By default, g==1.
nodes list indicating which nodes are to be included in the calculation. By default, all nodes are included.
gmode string indicating the type of graph being evaluated. "digraph" indicates that edges should be interpreted as directed; "graph" indicates that edges are undirected. gmode is set to "digraph" by default.
diag boolean indicating whether or not the diagonal should be treated as valid data. Set this true if and only if the data can contain loops. diag is FALSE by default.
tmaxdev boolean indicating whether or not the theoretical maximum absolute deviation from the maximum nodal centrality should be returned. By default, tmaxdev==FALSE.
cmode string indicating the type of betweenness centrality being computed (directed or undirected geodesics).
geodist.precomp a geodist object precomputed for the graph to be analyzed (optional).
rescale if true, centrality scores are rescaled such that they sum to 1.
# Katz and Powell (1955):
kprecip<-function(dat, measure=c("fixed", "free")){
if(measure=="fixed") {
# number of nodes
x<-dim(dat);
# number of mutual dyads in the graph
dc<-dyad.census(dat);
# medium outdegree for the node in dat
medDeg<-round(mean(degree(dat, cmode="outdegree")));
# expected number of mutual dyads if choices are made at random
# and all nodes have outdegree = medDeg
expMut<-as.integer(round((x[1]*(medDeg^2))/(2*(x[1]-1))));
# pkp value:
resultPK = ((2*dc[,1]*(x[1]-1))-(x[1]*(medDeg^2)))/(x[1]*medDeg*(x[1]-1-medDeg));
return(list=c("Expected Dyads:"=expMut,
"Found Dyads:" = dc[,1],
"pKP Fixed:"=resultPK));
} else {
# number of nodes
x<-dim(dat);
# number of arcs
L = sum(dat);
# number of mutual dyads in the graph
dc<-dyad.census(dat);
# medium outdegree for the node in dat
medDeg<-round(mean(degree(dat, cmode="outdegree")));
# expected number of mutual dyads if choices are made at random
# and all nodes have outdegree = medDeg
expMut<-as.integer(round((x[1]*(medDeg^2))/(2*(x[1]-1))));
# sum of squares of the outdegrees
D=0;
deg = degree(dat, cmode="outdegree");
for(i in 1:x[1]){
D=D+(deg[i]^2);
}
# pkp value:
resultPK = (2*((x[1]-1)^2)*dc[,1]-(L^2)+D)/(L*(x[1]-1)-(L^2)+D);
return(list=c("Expected Dyads:"=expMut,
"Found Dyads:" = dc[,1],
"pKP Free:"=resultPK));
}
}
#
# Achuthan, Rao and Rao (1982):
#
arrmut<-function(dat){
# number of nodes in the graph
x<-dim(dat);
# nodal outdegree
deg = degree(dat, cmode="outdegree");
# dyad census
dc<-dyad.census(dat);
y = 0
t = 0
u = 0
funMIN=0
for(t in 1:(x[1]+1)){
funMIN[t] = 0
u = t-1
if(u==0){
y=0
}else{
y = y+deg[u]
}
# array di valori della funzione a gradino per Smin
funMIN[t]<-y-u*(x[1]-u)-(u*(u-1))/2
}
# numero MIN di diadi mutue possibili per un grafo con outdegree fisso
smin<-max(funMIN)
r = 0
j = 0
k = 0
i = 0
e = 0
funMAX = 0
for(j in 1:(x[1]+1)){
k= j-1
if(k==0){
r=0
}else{
r = r + deg[k]
}
s = k*(k-1)
e[j] = 0
for (i in (k+1):x[1]){
if(k == x[1]){
e[j] = 0
} else {
e[j] = e[j] + min(k, deg[i])
}
}
funMAX[j] = r-s-e[j]
}
m = max(funMAX)
smax = floor((1/2)*(sum(deg)-m))
# indice di mutualità relativizzato al campo di variazione
index<-(dc[1]-smin)/(smax-smin)
# output della funzione
return(c("Smin:"=smin,
"Smax:" = smax,
"Found:"=dc[1],
"ARRmut:"=index));
}
[Achuthan ed al., 1982] ACHUTHAN N., RAO S.B., RAO A.R., “The number of symmetric edges in a digraph with prescribed outdegrees”, in Proceedings of the seminar on COmbinatorics and Applications in honour of Prof. S.S.Shrikhande on his 65th birthday, Indian Statistical Institute, december 14-17, 1982, p. 8-20.
[Anderson et al., 1999] ANDERSON B.S, BUTTS C., CARLEY K., “The interaction of size and density wih graph-level indices”, in Social Networks, 21, 1999, p. 239-267.