Data Understanding

Saya menggunakan dataset twitter yang berisikan tweet bertopik NFT. Analisis ini bertujuan untuk memahami bagaimana interaksi akun-akun yang mengisi topik tersebut, berdasarkan aktivitas mention.

Memuat library

library(tidyverse)

# graph
library(tidygraph)
library(ggraph)
library(igraph)

Read Data

tweets <- read_rds("nft.RDS")
head(tweets, 10)
tail(tweets, 20)

Memeriksa struktur data

glimpse(tweets)
#> Rows: 17,681
#> Columns: 90
#> $ user_id                 <chr> "1482989135279554561", "1465321321018101774", ~
#> $ status_id               <chr> "1484077026751705091", "1484077026546008068", ~
#> $ created_at              <dttm> 2022-01-20 08:15:10, 2022-01-20 08:15:10, 202~
#> $ screen_name             <chr> "MdMurad46986696", "artur_litau", "uzayhayat17~
#> $ text                    <chr> "<U+0001F6F8>There are different crystals on the earth. ~
#> $ source                  <chr> "Twitter Web App", "Twitter for iPhone", "Twit~
#> $ display_text_width      <dbl> 140, 13, 4, 69, 140, 140, 140, 140, 139, 140, ~
#> $ reply_to_status_id      <chr> NA, "1484076661943513088", "148407336790914663~
#> $ reply_to_user_id        <chr> NA, "1469819305", "1728743684", NA, NA, NA, NA~
#> $ reply_to_screen_name    <chr> NA, "drmctchr333", "misscryptolog", NA, NA, NA~
#> $ is_quote                <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
#> $ is_retweet              <lgl> TRUE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, TR~
#> $ favorite_count          <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0~
#> $ retweet_count           <int> 4105, 0, 0, 1305, 822, 264, 13, 1, 344, 1942, ~
#> $ quote_count             <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ reply_count             <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ hashtags                <list> NA, NA, NA, NA, NA, <"NFTs", "HODL", "gems", ~
#> $ symbols                 <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "REVO~
#> $ urls_url                <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ urls_t.co               <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ urls_expanded_url       <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ media_url               <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ media_t.co              <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ media_expanded_url      <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ media_type              <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ ext_media_url           <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ ext_media_t.co          <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ ext_media_expanded_url  <list> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ ext_media_type          <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ mentions_user_id        <list> "717023423696871424", <"1469819305", "1441155~
#> $ mentions_screen_name    <list> "SpaceDAOBSC", <"drmctchr333", "BabyGhosts_NF~
#> $ lang                    <chr> "en", "en", "en", "en", "en", "en", "en", "en"~
#> $ quoted_status_id        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_text             <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_created_at       <dttm> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N~
#> $ quoted_source           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_favorite_count   <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_retweet_count    <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_user_id          <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_screen_name      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_name             <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_followers_count  <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_friends_count    <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_statuses_count   <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_location         <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_description      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ quoted_verified         <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ retweet_status_id       <chr> "1483999732905111552", NA, NA, "14840733679091~
#> $ retweet_text            <chr> "<U+0001F6F8>There are different crystals on the earth. ~
#> $ retweet_created_at      <dttm> 2022-01-20 03:08:02, NA, NA, 2022-01-20 08:00~
#> $ retweet_source          <chr> "Twitter Web App", NA, NA, "Twitter for iPhone~
#> $ retweet_favorite_count  <int> 4046, NA, NA, 369, 813, 272, 19, 1, 377, 1173,~
#> $ retweet_retweet_count   <int> 4105, NA, NA, 1305, 822, 264, 13, 1, 344, 1942~
#> $ retweet_user_id         <chr> "717023423696871424", NA, NA, "1728743684", "3~
#> $ retweet_screen_name     <chr> "SpaceDAOBSC", NA, NA, "misscryptolog", "Brecc~
#> $ retweet_name            <chr> "SpaceDAO", NA, NA, "Miss Cryptolog", "Brecci"~
#> $ retweet_followers_count <int> 51808, NA, NA, 557859, 187762, 124776, 341, 78~
#> $ retweet_friends_count   <int> 128, NA, NA, 281, 846, 235, 2, 3149, 166, 11, ~
#> $ retweet_statuses_count  <int> 48, NA, NA, 13019, 10258, 2349, 71, 259, 122, ~
#> $ retweet_location        <chr> "Metaverse", NA, NA, "", "DM for PAID promo <U+0001F525>~
#> $ retweet_description     <chr> "SpaceDAO innovation on GameFi world!\nTelegra~
#> $ retweet_verified        <lgl> FALSE, NA, NA, FALSE, TRUE, FALSE, FALSE, FALS~
#> $ place_url               <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ place_name              <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ place_full_name         <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ place_type              <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ country                 <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ country_code            <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ geo_coords              <list> <NA, NA>, <NA, NA>, <NA, NA>, <NA, NA>, <NA, ~
#> $ coords_coords           <list> <NA, NA>, <NA, NA>, <NA, NA>, <NA, NA>, <NA, ~
#> $ bbox_coords             <list> <NA, NA, NA, NA, NA, NA, NA, NA>, <NA, NA, NA~
#> $ status_url              <chr> "https://twitter.com/MdMurad46986696/status/14~
#> $ name                    <chr> "Md Murad", "Artur Litau", "Uzay Hayat", "Uzay~
#> $ location                <chr> "", "", "", "", "", "", "", "", "", "", "Lagos~
#> $ description             <chr> "", "", "#SUPERFAMILY\n@LaCryptoMonkey\n#RichQ~
#> $ url                     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "h~
#> $ protected               <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
#> $ followers_count         <int> 2, 4, 302, 302, 96, 96, 299, 299, 6, 6, 43, 52~
#> $ friends_count           <int> 55, 34, 4163, 4163, 4838, 4838, 732, 732, 1357~
#> $ listed_count            <int> 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0~
#> $ statuses_count          <int> 166, 44, 21653, 21653, 50394, 50394, 2316, 231~
#> $ favourites_count        <int> 78, 76, 17321, 17321, 9710, 9710, 2076, 2076, ~
#> $ account_created_at      <dttm> 2022-01-17 08:12:36, 2021-11-29 14:07:40, 202~
#> $ verified                <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
#> $ profile_url             <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "h~
#> $ profile_expanded_url    <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "h~
#> $ account_lang            <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ profile_banner_url      <chr> NA, NA, "https://pbs.twimg.com/profile_banners~
#> $ profile_background_url  <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA~
#> $ profile_image_url       <chr> "http://pbs.twimg.com/profile_images/148298921~

Melihat banyak data yang merupakan retweet

tweets %>% 
  count(is_retweet)

Melihat akun dengan jumlah retweet terbanyak

tweets %>% 
  filter(is_retweet) %>% 
  select(retweet_count) %>% 
  summary()
#>  retweet_count  
#>  Min.   :    0  
#>  1st Qu.:   88  
#>  Median :  458  
#>  Mean   : 1379  
#>  3rd Qu.: 1395  
#>  Max.   :49552
tweets %>% 
  filter(retweet_count == 49552) %>% 
  select(screen_name,user_id, text)

User dengan screen name b1nary_eth merupakan akun dengan jumlah retweet terbanyak pada dataset. adapun isi tweet tersebut dapat kita pahami sebagai pertanyaan sekaligus pertanyaan tentang in-game-currency yang menggunakan NFT. Bagaiamana kemudian berbagai game-NFT dapat bertahan, karena banyak entitas serupa justru tidak mampu bertahan karena kebijakan in game currency nya sendiri (menggunakan NFT/kripto).

(lihat referensi di sini)

Data cleansing, eksplorasi data

cleansing <- tweets %>% 
  select(screen_name, mentions_screen_name) %>%
  mutate(mentions_screen_name = as.character(mentions_screen_name)) 
head(cleansing, 100)

Normalisasi mentions_screen_name menggunakan regular expression

–> sekaligus menyiapkan objek edge_df

edge_df <- 
cleansing %>% 
  select(screen_name, mentions_screen_name) %>% 
  mutate(mentions_screen_name = str_replace_all(string = mentions_screen_name, 
                                               pattern = "^c\\(|\\)$",
                                               replacement = "")) %>% 
  separate_rows(mentions_screen_name, sep = ",") %>% 
  na.omit() %>% 
  mutate(mentions_screen_name = str_replace_all(string = mentions_screen_name,
                                                pattern = "[[:punct]]+",
                                                replacement = "")) %>%
  rename(from = screen_name,
         to = mentions_screen_name)

head(edge_df, 100)

Membuat data frame nodes_df (kumpulan screen_name pada edge_df)

nodes_df <- data.frame(name = unique(c(edge_df$from,edge_df$to)),
                        stringsAsFactors = F)

tail(nodes_df,10)

Antara data edge dan nodes belum terlalu rapi, masih ada quotation mark (" "), saya ingin membersikan menggunakan regex pada function str_replace_all

nodes_df1 <- nodes_df %>%  
  mutate(name = str_replace_all(string = name,
                                pattern = "\"",
                                replacement = ""))
head(nodes_df1)
edge_df1 <- edge_df %>% 
  mutate(to = str_replace_all(string = to,
                                pattern = "\"",
                                replacement = ""))

head(edge_df1)

Membuat graph dengan fungsi tbl_graph()

graph_tweets <- tbl_graph(nodes = nodes_df1, 
                          edges = edge_df1,
                          directed = F)
graph_tweets
#> # A tbl_graph: 18490 nodes and 33011 edges
#> #
#> # An undirected multigraph with 1882 components
#> #
#> # Node Data: 18,490 x 1 (active)
#>   name           
#>   <chr>          
#> 1 MdMurad46986696
#> 2 artur_litau    
#> 3 uzayhayat17    
#> 4 _andierna_     
#> 5 Cardatson      
#> 6 minguinnnnn_   
#> # ... with 18,484 more rows
#> #
#> # Edge Data: 33,011 x 2
#>    from    to
#>   <int> <int>
#> 1     1  9504
#> 2     2  9505
#> 3     2  9506
#> # ... with 33,008 more rows

Saya menggunakan undirected graph karena ingin mengabaikan arah interaksi antara from dan to, dan apakah interaksi terjadi secara dua arah atau searah.

Centrality Measurement

Kita akan mengukur centrality dari setiap Nodes dengan parameter berikut:

  • Degree: untuk menemukan user yang terkoneksi secara lokal (dalam satu cluster)

  • Betweenness: untuk menemukan user yang menjadi perantara terhadap beberapa cluster

  • Closeness: untuk menemukan user yang memiliki bisa memberi influence tercepat terhadap yang lain

  • Eigen: mengukur pengaruh dari setiap user dari banyaknya keterhubungan dengan node-node lainnya; lalu dipertimbangkan juga koneksi yang dimiliki oleh setiap user; bobot dari berbagai koneksi yang dimiliki

options(scipen = 100)
graph_tweets <- graph_tweets %>% 
  activate(nodes) %>%
  mutate(degree = centrality_degree(), # Calculate degree centrality
         between = centrality_betweenness(normalized = T), # Calculate betweeness centrality
         closeness = centrality_closeness(), # Calculate closeness centrality
         eigen = centrality_eigen()
         )  # Calculate eigen centrality

graph_tweets
#> # A tbl_graph: 18490 nodes and 33011 edges
#> #
#> # An undirected multigraph with 1882 components
#> #
#> # Node Data: 18,490 x 5 (active)
#>   name            degree      between     closeness    eigen
#>   <chr>            <dbl>        <dbl>         <dbl>    <dbl>
#> 1 MdMurad46986696      1 0            0.00000000950 1.92e- 7
#> 2 artur_litau          3 0.000112     0.00000000949 3.21e-11
#> 3 uzayhayat17          4 0.0000000173 0.00000000950 6.88e- 2
#> 4 _andierna_           3 0.000000984  0.00000000950 1.12e- 3
#> 5 Cardatson            2 0.0000000117 0.00000000293 0       
#> 6 minguinnnnn_         3 0.0000130    0.00000000950 7.81e- 5
#> # ... with 18,484 more rows
#> #
#> # Edge Data: 33,011 x 2
#>    from    to
#>   <int> <int>
#> 1     1  9504
#> 2     2  9505
#> 3     2  9506
#> # ... with 33,008 more rows
network_act_df <- graph_tweets %>% 
  activate(nodes) %>% 
  as.data.frame()

head(network_act_df,10)

Eksplorasi data pada akun yang memiliki centrality tinggi

kp_activity <- data.frame(
  network_act_df %>% arrange(-degree) %>% select(name) %>% slice(1:6),
  network_act_df %>% arrange(-between) %>% select(name) %>% slice(1:6),
  network_act_df %>% arrange(-closeness) %>% select(name) %>% slice(1:6),
  network_act_df %>% arrange(-eigen) %>% select(name) %>% slice(1:6)
) %>% setNames(c("degree","betweenness","closeness","eigen"))
kp_activity

Berdasarkan hasil analisis centralities, terlihat bahwa akun f_sara termasuk dalam semua golongan centrality, yang berarti akun tersebut paling populer secara lokal maupun global (eigen).
Selain itu, besar kemungkinan jugaf_sara terhubung dengan kebanyakan cluster, sehingga arus informasi dari semua cluster diakomodasi oleh akun tersebut.

Namun, temuan dapat saja berbeda apabila terdapat aktivitas mentioning yang berlebihan yang ditujukan pada akun f_sara, terlepas dari apa dan siapa identitas akun tersebut.

# Memahami siapa f_sara

lookup <- tweets %>% 
  select(screen_name, mentions_screen_name, text, retweet_count) %>%
  mutate(mentions_screen_name = as.character(mentions_screen_name)) %>% 
  mutate(mentions_screen_name = str_replace_all(string = mentions_screen_name, 
                                               pattern = "^c\\(|\\)$",
                                               replacement = "")) %>% 
  na.omit() %>% 
  mutate(mentions_screen_name = str_replace_all(string = mentions_screen_name,
                                                pattern = "[[:punct]]+",
                                                replacement = "")) %>%
  mutate(mentions_screen_name = str_replace_all(string = mentions_screen_name,
                                pattern = "\"",
                                replacement = "")) %>%
  separate_rows(mentions_screen_name, sep = ",")

lookup
inspect_sara <- lookup %>% 
  filter(mentions_screen_name == "f_sara") %>% 
  arrange(desc(retweet_count)) %>% 
  distinct(text, mentions_screen_name, screen_name)

inspect_sara[41:105,]

Setelah pencarian lebih lanjut, sayangnya akun dengan nama screen_name f_sara tidak terkait dengan tweet bertopik NFT/kripto.

Berdasarkan data, kita dapat melihat bahwa f_sara sering terkait dengan tweet dari sebuah akun yang bernama @nft_spartan.

nft_spartan merupakan kolektor dan investor NFT, akun tersebut sering terlibat dalam suatu givaway maupun info dan ‘tips and trick’.
Dalam dunia yang sarat dengan unsur FOMO (Fear Of Missing Out) seperti game, cryptocurrency maupun NFT, kita mungkin sering melihat akun yang “menggaungkan isu” secara intensif/spamming.

Analisis saya, karena pada saat ini kita mengacu pada mentions_screen_name, kemungkinan besar akun nft_spartan melakukan mention secara acak dan hidden kepada beberapa screen name untuk memperkenalkan isi kontennya. Namun disayangkan, karena target mentionnya terlihat seperti akun nonaktif.

Graph Visualization

Membuat Cluster

set.seed(123)
graph_tweets <- graph_tweets %>% 
  activate(nodes) %>% 
  mutate(community = group_louvain()) %>% 
  activate(edges) %>% 
  filter(!edge_is_loop())  # Remove loop edges
graph_tweets %>% 
  activate(nodes) %>% 
  as.data.frame() %>% 
  count(community)

Mengambil cluster 1-5 saja, dengan asumsi 5 data dari klaster terbanyak sudah representatif (jumlah dari n1-n5)

# fungsi untuk mendapatkan orang orang penting di tiap cluster
important_user <- function(data) {
  name_person <- data %>%
  as.data.frame() %>% 
  filter(community %in% 1:5) %>% 
  select(-community) %>% 
  pivot_longer(-name, names_to = "measures", values_to = "values") %>% 
  group_by(measures) %>% 
  arrange(desc(values)) %>% 
  slice(1:6) %>% 
  ungroup() %>% 
  distinct(name) %>% 
  pull(name)
  
  return(name_person)
}

Visualisasi data graph yang sudah dibuat

important_person <- 
graph_tweets %>% 
  activate(nodes) %>% 
  important_user()
set.seed(100)
graph_tweets %>%
  activate(nodes) %>%
  mutate(ids = row_number(),
         community = as.character(community)) %>%
  filter(community %in% 1:5) %>% 
  arrange(community,ids) %>%
  mutate(node_label = ifelse(name %in% important_person, name, NA)) %>%
  ggraph(layout = "fr") +
  geom_edge_link(alpha = 0.3 ) +
  geom_node_point(aes(size = degree, fill = community), shape = 21, alpha = 0.7, color = "grey30") +
  geom_node_label(aes(label = node_label), repel = T, alpha = 0.8 ) +
  guides(size = "none") +
  labs(title = "Top 5 Community of #NFT", 
       color = "Interaction",
       fill = "Community") +
  theme_void() +
  theme(legend.position = "top")

Memeriksa kembali “orang penting”

Saya sedikit skeptis dengan hasil top 5 community diatas, bagaimana eksistensi akunnya di twitter, apakah sama seperti f_sara.

Maka dari itu saya ingin melihat screen_name, mentions_screen_name, dan text yang melibatkan akun yang saya curigai.

Saya tertarik untuk inspect akun missryolog.

missryolog <- lookup %>% 
  filter(mentions_screen_name == "missryolog") %>% 
  arrange(desc(retweet_count)) %>% 
  distinct(text, mentions_screen_name, screen_name)

head(missryolog,100)

Dapat kita lihat dari isi tweet, bahwa pada akun missryolog pun masih berkaitan dengan spamming nft_spartan.

Selanjutnya, sebetulnya apa isi dari akun missryolog?

knitr::include_graphics("images/ryolog.png")

Ternyata, missryolog bukanlah merupakan akun yang aktif (atau bahkan salah alamat). Dari hasil eksplorasi object yang sudah saya buat secara khusus, saya memiliki perkiraan bahwa missryolog merupakan kesalahan penulisan dari misscryptolog.

missryolog[991:1052,]
knitr::include_graphics("images/cryptolog.png")

Kesimpulan

Berdasarkan hasil visualisasi dan centrality, dapat disimpulkan bahwa cluster yang berwarna hijau yang memiliki keterhubungan dengan nodes dari cluster lainnya. Namun, sepertinya terdapat beberapa nama akun yang tidak valid; entah itu dikarenakan proses cleansing atau karena data asalnya (mohon pencerahan).

Banyak akun investor NFT yang melakukan spamming terhadap akun-akun lain secara acak, dilihat dari relevansi mentions_screen_name terhadap pemeriksaan eksistensi akun tersebut di twitter (contohnya: f_sara).

Akun @nft_spartan was a great spammer

Rekomendasi: apakah clustering SNA bisa dijadikan interaktif?

NetworkD3 Experiment

library(networkD3)
src <- c("A", "A", "A", "A",
         "B", "B", "C", "C", "D")

target <- c("B", "C", "D", "J", "E",
            "F", "G", "H", "I")

networkData <- data.frame(src, target)

simpleNetwork(networkData)

Load data dummy

data("Mislinks")
data("Misnodes")

Visualisasi graph interaktif

forceNetwork(Links = MisLinks, Nodes = MisNodes, Source = "source", 
             Target = "target", Value = "value", NodeID = "name", 
             Nodesize = "size", 
             radiusCalculation = "Math.sqrt(d.nodesize)+6",
             Group = "group", opacity = 1, opacityNoHover = F, width = 1000, height = 700, fontSize = 12)