Zaczynamy od załadowania pakietów R, które będą nam potrzebne do wygenerowania wykresów na Twitterze. Będziemy używać trzech pakietów R do tworzenia tych wykresów z SGA. Pakiet RCurl zapewnia interfejs do biblioteki libcurl, której będziemy używać do wysyłania żądań HTTP do SGA. Następnie użyjemy RJSONIO do parsowania JSON zwróconego przez SGA na listach R. Tak się składa, że oba te pakiety opracował Duncan Temple Lang, który opracował wiele niezbędnych pakietów R (patrz http://www.omegahat.org/). Na koniec użyjemy igraph do budowy i przechowywania obiektów sieciowych. Pakiet igraph to potężny pakiet R do tworzenia obiektów graficznych i manipulowania nimi, a jego elastyczność będzie dla nas bardzo cenna, gdy zaczniemy pracować z naszymi danymi sieciowymi.
library(RCurl)
library(RJSONIO)
library(igraph)
Pierwsza funkcja, którą napiszemy, działa z SGA na najwyższym poziomie. Wywołamy funkcję twitter.network, aby wysłać zapytanie do SGA dla danego użytkownika początkowego, przeanalizować JSON i zwrócić reprezentację sieci ego tego użytkownika. Ta funkcja przyjmuje pojedynczy parametr user, który jest ciągiem odpowiadającym początkowemu użytkownikowi Twittera. Następnie konstruuje odpowiedni adres URL żądania SGA GET i używa funkcji getURL w RCurl do wykonania żądania HTTP. Ponieważ wyszukiwanie śnieżkami będzie wymagało wielu żądań HTTP naraz, budujemy pętlę while, która sprawdza, czy strona zwrócona nie wskazuje przerwy w działaniu usługi. Bez tego skrypt nieumyślnie pominie te węzły podczas wyszukiwania kuli śnieżnej, ale dzięki tej kontroli po prostu wróci i spróbuje wykonać nowe żądanie, dopóki dane nie zostaną zebrane. Kiedy wiemy, że żądanie API zwróciło JSON, używamy funkcji fromJSON w pakiecie RJSONIO do analizy JSON.
twitter.network <- function(user) {
api.url <-
paste(“https://socialgraph.googleapis.com/lookup?q=http://twitter.com/”,
user, “&edo=1&edi=1″, sep=””)
api.get <- getURL(api.url)
# To guard against web-request issues, we create this loop
# to ensure we actually get something back from getURL.
while(grepl(“Service Unavailable. Please try again later.”, api.get)) {
api.get <- getURL(api.url)
}
api.json <- fromJSON(api.get)
return(build.ego(api.json))
}
Po przeanalizowaniu JSON musimy zbudować sieć z tych danych. Przypomnijmy, że dane zawierają dwa typy relacji: nodes_referenced (out-degree) i nodes_referenced_by (in-degree). Musimy więc wykonać dwa przejścia przez dane, aby uwzględnić oba typy krawędzi. W tym celu napiszemy funkcję build.ego, która pobiera przeanalizowaną SGA JSON jako listę i buduje sieć. Zanim jednak zaczniemy, musimy wyczyścić dane relacyjne zwrócone przez SGA. Jeśli dokładnie przejrzysz wszystkie węzły zwrócone przez żądanie API, zauważysz, że niektóre wpisy nie są właściwymi użytkownikami Twittera. W wynikach uwzględniono relacje niezwiązane z tym ćwiczeniem. Na przykład istnieje wiele list na Twitterze, a także adresy URL, które odnoszą się do przekierowań „konto”. Chociaż wszystkie są częścią grafu społecznościowego Twittera, nie jesteśmy zainteresowani włączeniem tych węzłów do naszego wykresu, dlatego musimy napisać funkcję pomocnika, aby je usunąć.
find.twitter <- function(node.vector) {
twitter.nodes <- node.vector[grepl(“http://twitter.com/”, node.vector,
fixed=TRUE)]
if(length(twitter.nodes) > 0) {
twitter.users <- strsplit(twitter.nodes, “/”)
user.vec <- sapply(1:length(twitter.users),
function(i) (ifelse(twitter.users[[i]][4]==”account”,
NA, twitter.users[[i]][4])))
return(user.vec[which(!is.na(user.vec))])
}
else {
return(character(0))
}
}
Funkcja find.twitter wykorzystuje to, wykorzystując strukturę adresów URL zwracanych przez SGA, aby sprawdzić, czy faktycznie są użytkownikami Twittera, a nie jakąś inną częścią grafu społecznościowego. Pierwszą rzeczą, którą robi ta funkcja, jest sprawdzenie, które węzły zwrócone przez SGA są faktycznie z Twittera, za pomocą funkcji grepl w celu sprawdzenia wzorca http://twitter.com/. W przypadku pasujących adresów URL musimy dowiedzieć się, które odpowiadają rzeczywistym kontom, a nie listom lub przekierowaniom. Jednym ze sposobów jest podzielenie adresów URL ukośnikiem odwrotnym, a następnie sprawdzenie słowa „konto”, które wskazuje adres URL użytkownika spoza Twittera. Po zidentyfikowaniu indeksów pasujących do tego wzorca innego niż Twitter, po prostu zwracamy adresy URL, które nie pasują do tego wzorca. Poinformuje to funkcję build.ego, które węzły powinny zostać dodane do sieci.
build.ego <- function(json) {
# Find the Twitter user associated with the seed user
ego <- find.twitter(names(json$nodes))
# Build the in- and out-degree edgelist for the user
nodes.out <- names(json$nodes[[1]]$nodes_referenced)
if(length(nodes.out) > 0) {
# No connections, at all
twitter.friends <- find.twitter(nodes.out)
if(length(twitter.friends) > 0) {
# No twitter connections
friends <- cbind(ego, twitter.friends)
}
else {
friends <- c(integer(0), integer(0))
}
}
else {
friends <- c(integer(0), integer(0))
}
nodes.in <- names(json$nodes[[1]]$nodes_referenced_by)
if(length(nodes.in) > 0) {
twitter.followers <- find.twitter(nodes.in)
if(length(twitter.followers) > 0) {
followers <- cbind(twitter.followers, ego)
}
else {
followers <- c(integer(0), integer(0))
}
}
else {
followers <- c(integer(0), integer(0))
}
ego.el <- rbind(friends, followers)
return(ego.el)
}
Po zidentyfikowaniu odpowiednich węzłów na liście możemy teraz zacząć budować sieć. Aby to zrobić, użyjemy build.ego, aby stworzyć „listę brzegową” opisującą relacje wewnątrz i na zewnątrz naszego użytkownika początkowego. Lista krawędzi to po prostu macierz dwukolumnowa reprezentująca relacje na grafie kierunkowym. Pierwsza kolumna zawiera źródła krawędzi, a druga kolumna zawiera cele. Możesz myśleć o tym jak o węzłach w pierwszej kolumnie łączących się z węzłami w drugiej. Najczęściej listy krawędzi będą zawierać etykiety liczb całkowitych lub ciągów dla węzłów. W takim przypadku użyjemy ciągu nazwy użytkownika Twittera. Chociaż funkcja build.ego jest długa pod względem liczenia wierszy, jej funkcjonalność jest w rzeczywistości dość prosta. Większość kodu służy do organizowania i sprawdzania błędów budowania list krawędzi. Jak widać, najpierw sprawdzamy, czy API wywołuje zarówno nodes_referenced, jak i nodes_referenced_by zwróciło przynajmniej niektóre relacje. Następnie sprawdzamy, które węzły faktycznie odnoszą się do użytkowników Twittera. Jeśli którakolwiek z tych kontroli nic nie zwróci, zwrócimy specjalny wektor jako wynik naszej funkcji: c (liczba całkowita (0), liczba całkowita (0)). W ramach tego procesu tworzymy dwie matryce list krawędzi, trafnie nazywanych przyjaciółmi i obserwującymi. W ostatnim kroku używamy rbind do powiązania tych dwóch macierzy w jednej liście krawędzi, zwanej ego.el. Zastosowaliśmy specjalny wektor c (liczba całkowita (0), liczba całkowita (0)) w przypadkach, w których nie ma danych, ponieważ rbind skutecznie je zignoruje i nie wpłynie to na nasze wyniki. Możesz to zobaczyć, wpisując następujące polecenie na konsoli R.
rbind(c(1,2), c(integer(0), integer(0)))
[,1] [,2]
[1,] 1 2
Być może zastanawiasz się, dlaczego budujemy tylko listę relacji, zamiast budować wykres bezpośrednio za pomocą igraph. Ze względu na sposób, w jaki igraph przechowuje wykresy w pamięci, iteracyjne tworzenie wykresów może być trudne w sposób wymagany przez próbkę śnieżki. Dlatego dużo łatwiej jest najpierw zbudować cały relacyjny zestaw danych (w tym przypadku jako listę brzegową), a następnie przekonwertować te dane na wykres. Napisaliśmy teraz rdzeń naszego skryptu do tworzenia wykresów. Funkcja build.ego wykonuje trudną pracę polegającą na pobraniu przeanalizowanego JSON-a z SGA i przekształceniu go w coś, co igraph może przekształcić w sieć. Ostatnim elementem, który będziemy musieli zbudować, jest połączenie wszystkiego w celu wygenerowania próbki kuli śnieżnej. Stworzymy funkcję twitter.snowball, aby wygenerować tę próbkę i jedną małą funkcję pomocniczą, get.seeds, aby wygenerować listę nowych nasion do odwiedzenia. Podobnie jak w build.ego, głównym celem twitter.snowball jest wygenerowanie listy krawędzi relacji sieciowych. W tym przypadku będziemy jednak wiązać wyniki wielu wywołań build.ego i zwracać obiekt wykresu igraph. Dla uproszczenia zaczynamy od funkcji get.seeds, chociaż będzie ona dotyczyć rzeczy
generated in twitter.snowball.
get.seeds <- function(snowball.el, seed) {
new.seeds <- unique(c(snowball.el[,1],snowball.el[,2]))
return(new.seeds[which(new.seeds!=seed)])
}
Celem get.seeds jest znalezienie unikalnego zestawu węzłów z listy krawędzi, które nie są węzłami początkowymi. Dokonuje się tego bardzo łatwo, redukując macierz dwukolumnową do pojedynczego wektora, a następnie znajdując unikalne elementy tego wektora. Ten czysty wektor nazywa się new.seed, a następnie zwracane są tylko te elementy, które nie są ziarnem. Jest to prosta i skuteczna metoda, ale jest jedna rzecz, którą musimy wziąć pod uwagę, korzystając z tej metody. Funkcja get.seeds sprawdza tylko, czy nowe nasiona nie są takie same jak bieżące nasiona. Chcielibyśmy uwzględnić coś, co zapewni, że podczas naszej próby śnieżki nie będziemy ponownie odwiedzać węzłów w sieci, dla których struktura została już zregenerowana. Z technicznego punktu widzenia nie jest to koniecznie ważne, ponieważ moglibyśmy bardzo łatwo usunąć zduplikowane wiersze z końcowej listy krawędzi. Jest to jednak ważne z praktycznego punktu widzenia. Usuwając węzły, które były już odwiedzane z listy potencjalnych nowych nasion przed budowaniem nowej struktury, zmniejszyliśmy liczbę wywołań API, które musimy wykonać. To z kolei zmniejsza prawdopodobieństwo uderzenia w limit prędkości SGA i skraca czas działania naszego skryptu. Zamiast obsługiwać to przez funkcję get.seeds, dodajemy tę funkcję do twitter.snowball, gdzie cała lista brzegowa sieci jest już zapisana w pamięci. Zapewnia to, że nie ma wielu kopii całej listy krawędzi w pamięci, ponieważ budujemy całą śnieżkę. Pamiętaj, że w tych skalach musimy być bardzo świadomi pamięci, zwłaszcza w języku takim jak R.
twitter.snowball <- function(seed, k=2) {
# Get the ego-net for the seed user. We will build onto
# this network to create the full snowball search.
snowball.el <- twitter.network(seed)
# Use neighbors as seeds in the next round of the snowball
new.seeds <- get.seeds(snowball.el, seed)
rounds <- 1 # We have now completed the first round of the snowball!
# A record of all nodes hit, this is done to reduce the amount of
# API calls done.
all.nodes <- seed
# Begin the snowball search…
while(rounds < k) {
next.seeds <- c()
for(user in new.seeds) {
# Only get network data if we haven’t already visited this node
if(!user %in% all.nodes) {
user.el <- twitter.network(user)
if(dim(user.el)[2] > 0) {
snowball.el <- rbind(snowball.el, user.el)
next.seeds <- c(next.seeds, get.seeds(user.el, user))
all.nodes <- c(all.nodes, user)
}
}
}
new.seeds <- unique(next.seeds)
new.seeds <- new.seeds[!which(new.seeds %in% all.nodes)]
rounds <- rounds + 1
}
# It is likely that this process has created duplicate rows.
# As a matter of housekeeping we will remove them because
# the true Twitter social graph does not contain parallel edges.
snowball.el <- snowball.el[!duplicated(snowball.el),]
return(graph.edgelist(snowball.el))
}
Funkcjonalność funkcji twitter.snowball jest taka, jak można się spodziewać. Funkcja przyjmuje dwa parametry: seed i k. Pierwszy to ciąg znaków dla użytkownika Twittera, w naszym przypadku może to być „DrawConway” lub „JohnJyleswhite”. K jest liczbą całkowitą większą lub równą dwa i określa liczbę rund dla śnieżki Szukaj. W mowie sieciowej wartość k ogólnie odnosi się do stopnia lub odległości na wykresie. W tym przypadku odpowiada to odległości od nasion w sieci, do której osiągnie próbka śnieżki. Naszym pierwszym krokiem jest zbudowanie początkowej sieci ego z nasion; z tego otrzymamy nowe nasiona do następnej rundy. Robimy to za pomocą połączeń z twitter.network i get.seeds. Teraz można rozpocząć iteracyjną pracę próbki kuli śnieżnej. Całe próbkowanie odbywa się w pętli while twitter.snowball. Podstawowa struktura logiczna pętli jest następująca. Najpierw sprawdź, czy nie osiągnęliśmy końca naszej próbki odległości. Jeśli nie, przejdź do budowy kolejnej warstwy śnieżki. Robimy to poprzez iteracyjne budowanie sieci ego każdego z naszych nowych użytkowników nasion. Obiekt all.nodes służy do śledzenia już istniejących węzłów odwiedzonych; innymi słowy, zanim zbudujemy sieć ego węzła, najpierw sprawdzamy, czy nie ma go już we wszystkich węzłach. Jeśli nie jest, dodajemy relacje tego użytkownika do snowball.el, next.seeds i all.nodes. Zanim przejdziemy do następnej rundy śnieżki, sprawdzamy, czy nowy wektor new.seeds nie zawiera żadnych duplikatów. Ponownie robi się to, aby zapobiec ponownym odwiedzinom węzłów. Na koniec zwiększamy licznik rund. W ostatnim kroku bierzemy macierz snowball.el, która reprezentuje listę krawędzi całej próbki kuli śnieżnej, i przekształcamy ją w obiekt wykresu igraph za pomocą funkcji listy graph.edge. Jest jeszcze jedna ostatnia część sprzątania, którą musimy zrobić przed konwersją. Ponieważ dane w SGA są niedoskonałe, czasami zdarzają się zduplikowane relacje między krawędziami próbki. Graficznie teoretycznie moglibyśmy pozostawić te relacje i użyć specjalnej klasy wykresu zwanej „multi-graf” jako naszej sieci na Twitterze. Multi-wykres to po prostu wykres z wieloma krawędziami między tymi samymi węzłami. Chociaż ten paradygmat może być użyteczny w niektórych kontekstach, istnieje bardzo niewiele wskaźników analizy sieci społecznościowych, które są prawidłowe dla takiego modelu. Ponadto, ponieważ w tym przypadku dodatkowe krawędzie są wynikiem błędów, usuniemy te zduplikowane krawędzie przed konwersją na obiekt wykresu. Mamy teraz wszystkie funkcje potrzebne do budowy sieci Twittera. W następnej sekcji zbudujemy dodatkowe skrypty, które ułatwią szybkie wygenerowanie struktury sieci od danego użytkownika początkowego i wykonanie na nich podstawowych wykresów analiz strukturalnych y, aby odkryć w nich strukturę społeczności lokalnej.