Introduction to CooRTweet

Nicola Righetti & Paul Balluff

Overview

The CooRTweet package is a R tool for detecting and analyzing coordinated behavior across social media platforms. Named after Twitter, a quintessential site for coordinated message amplification through its features like hashtags and trending topics, CooRTweet is applicable to any social media platform, enabling analysis on mono-platform, multi-platform, and cross-platform datasets. Besides being platform-independent, it is also content-independent, supporting a wide range of content types (including hashtags, URLs, images, and any other objects of interest to the researcher). The package allows for flexible thresholds to identify coordination while also accounting for the uncoordinated network in which the coordination is contextualized. CooRTweet is one of the first software tools for coordinated detection to have undergone rigorous validation. With its output, researchers can effectively explore networks of coordinated activity.

The methodological rationale and a cross-platform case study are described in the CooRTweet paper (Righetti and Balluff 2025).

Installation

You can install the CooRTweet package from CRAN or GitHub:

# Install from CRAN
install.packages("CooRTweet")

# Or install the development version from GitHub
devtools::install_github("nicolarighetti/CooRTweet")

Key Features

  1. Flexible Data Handling: Works with mono-modal, multi-modal, and cross-platform datasets, and any type of object.
  2. Customizable Thresholds: Set time intervals and repetition/edge-weight thresholds to detect coordinated activities.
  3. Graph-Based Analysis: Outputs coordination networks as igraph objects for further analysis and visualization.
  4. Included Data: Comes with datasets for learning Righetti et al. (2022) and a simulate_data function to generate synthetic coordinated networks. The installed package includes german_elections_sample, an anonymized excerpt for examples, not the full published study.

Getting Started

Input Data Format

The input dataset should include the following columns. prep_data() can rename source columns to these names and, by default, removes rows with missing values and duplicate shares. Use drop_na = FALSE or drop_duplicates = FALSE to retain them.

Example:

library(CooRTweet)
head(russian_coord_tweets)
#>                          object_id                       account_id
#> 1 85d2d12251a735ce05255061f7f231e2 0fb4232d1b7b37069c13ee17579bd10e
#> 2 89864519cd34cabd6f5a801b6857fea6 7d0e462c10d52c4ec1db5af953bf9b26
#> 3 6f04c951961f5cf8c05df6f284fc7c17 2e1140330c02584ffabaf1da362f8e10
#> 4 025b1b3dc82df1cc6c6b766e9c651251 38b5eea36ec86e4a9aac60980ebf6526
#> 5 025b1b3dc82df1cc6c6b766e9c651251 536783d1fd886a85ab697f299f153d3b
#> 6 280d86b602da34926d3797b94d0a7e15 285ddd6b5c9b35ea56257071d4cb6b4d
#>                         content_id timestamp_share
#> 1 114e22f1b2b648528277b76f0b0224e7      1623881091
#> 2 60e1901aa670f6c05a2d7f7e74cadaf3      1623879483
#> 3 85d2d12251a735ce05255061f7f231e2      1623865591
#> 4 321677c8cae729398e86c5c044b3fbba      1623864621
#> 5 707b5dc150f7405711f992341d0dd32f      1623864188
#> 6 c2592fc64962853976bb75d183a4301b      1623656233

Detect Coordinated Groups

Use the detect_groups() function to find groups of accounts coordinating within a specified time window.

result <- detect_groups(
  x = russian_coord_tweets,
  min_participation = 2,
  time_window = 60
)
head(result)
#>                           object_id                       content_id
#>                              <char>                           <char>
#> 1: 4e04165c28ea7dd3cf4c8512c3f490d7 2944a8d714bba22c120aad58fca851d8
#> 2: 4e04165c28ea7dd3cf4c8512c3f490d7 7b92b077328ca532e5d1c4781484ce35
#> 3: 4e04165c28ea7dd3cf4c8512c3f490d7 7b92b077328ca532e5d1c4781484ce35
#> 4: 4e04165c28ea7dd3cf4c8512c3f490d7 e9a4787f1d970dbe4c3868a783ba1535
#> 5: 4e04165c28ea7dd3cf4c8512c3f490d7 1283205f0e23fda216c4de27bec4df80
#> 6: bc46f3cae46cd00726c4c1992145ae20 54cecb686722d539dd556a9c6e8d72e0
#>                        content_id_y time_delta                       account_id
#>                              <char>      <num>                           <char>
#> 1: 354126c2d9e2e69676c9dbdcc167d3d7         36 d9fe8e4d34b6dcfba8cbcf1be4b28717
#> 2: 354126c2d9e2e69676c9dbdcc167d3d7         59 94b2413eb4e850246c07ba1bd55625c2
#> 3: 2944a8d714bba22c120aad58fca851d8         23 94b2413eb4e850246c07ba1bd55625c2
#> 4: c5b0dcb930f979202600a59bf64db452         48 7289281c087ccc0342d96604243d0069
#> 5: e9a4787f1d970dbe4c3868a783ba1535         41 6c051ab25467ae690fb24cd2c6c3ad99
#> 6: cf2b35e31413d2da92940bc571c2c6a2          5 f442b084eb6be4c7f66dffa386c01e4b
#>                        account_id_y
#>                              <char>
#> 1: 0fb4232d1b7b37069c13ee17579bd10e
#> 2: 0fb4232d1b7b37069c13ee17579bd10e
#> 3: d9fe8e4d34b6dcfba8cbcf1be4b28717
#> 4: 47a750359d66201ddefe7f7efbfed0b9
#> 5: 7289281c087ccc0342d96604243d0069
#> 6: 3dced626839250a9c9bf41a381234214

Select an Inclusive Time Interval

Set time_window_low to select one interval of time differences. Both limits are included. The shares below occur at 0, 10, and 30 seconds; selecting 10 through 20 seconds keeps the pairs exactly 10 and 20 seconds apart:

shares <- data.table::data.table(
  object_id = "url_1",
  account_id = c("A", "B", "C"),
  content_id = c("p1", "p2", "p3"),
  timestamp_share = c(0, 10, 30)
)

interval_result <- detect_groups(
  shares, time_window = 20, time_window_low = 10,
  min_participation = 1
)
sort(interval_result$time_delta)
#> [1] 10 20

The default time_window_low = 0 also includes pairs shared at the same second.

Generate Coordination Networks

Convert detected groups into a coordination network using generate_coordinated_network(). By default, the function returns the graph filtered by the median edge weight (edge_weight = 0.5, subgraph = 1). This is the conventional coordinated-sharing network of accounts connected by repeated sharing (e.g., Giglietto et al. (2020)); the edge-weight threshold can be adjusted. To return the full graph instead, set subgraph = 0; its weight_threshold edge attribute is 0 if an edge does not exceed the threshold and 1 otherwise.

graph <- generate_coordinated_network(result)
graph
#> IGRAPH 704b64c UNW- 94 60 -- 
#> + attr: name (v/c), weight (e/n), avg_time_delta (e/n), n_content_id
#> | (e/n), n_content_id_y (e/n), edge_symmetry_score (e/n),
#> | weight_threshold (e/n)
#> + edges from 704b64c (vertex names):
#> [1] 129b936e6bb06defdd2d41331d9e5d0e--a75cc8452d4f73bd2c9096a641ffa97e
#> [2] 23303497264ceeee35265a7ddc6f2221--a66b033469b405a53adf6eafd4ed0f43
#> [3] 572f3f02cfe0fa8ea0bce4daee92da74--f3e2f918e96d18c70b9a55bffba81b3f
#> [4] 23303497264ceeee35265a7ddc6f2221--e7c63206a504dd0565d1055e65dde548
#> [5] 8fa271d51bacc2baca543d012dc1afe1--93699703a9664d74c290406babef8a8b
#> [6] 371fe944976cd0125c36872bf2900cb4--9e9172bcac37f28bf6eb3b45faecf2df
#> + ... omitted several edges

Advanced Usage

Multi-Modal and Multi-Platform Analysis

To analyze multiple types of content (e.g., URLs and images), run detect_groups() separately for each type and combine the results.

german_elections_sample is included in the installed package. It contains 97,413 anonymized rows from 15 August through 7 September 2021 (UTC), drawn from the data used by Righetti et al. (2022). It includes Facebook and Twitter posts and illustrates both URL and image coordination. It is a temporal excerpt for learning, not a representative sample or the full study dataset, and cannot reproduce the paper’s figures.

The full anonymized german_elections dataset is not installed with the package. It is available in the CooRTweet source repository at data-raw/german_elections_full.rda. From the root of a source checkout, load("data-raw/german_elections_full.rda") creates the german_elections object. The paper reproduction script is at vignettes/reproduce_examples.Rmd in that checkout; it is not an installed package vignette. Some panels require non-public data and are commented out in the public script.

# Example datasets for different content types
head(german_elections_sample)
#> # A data frame: 6 × 7
#>   account_id post_id url_id hashtag_id domain_id phash_id  timestamp
#> * <chr>        <int>  <int>      <int>     <int>    <int>      <dbl>
#> 1 fb_12670    129235  23678         NA      3498       NA 1629589836
#> 2 fb_5966      84441     NA         NA      6756       NA 1629589069
#> 3 fb_5966      84443  29871         NA      5534       NA 1629589050
#> 4 fb_5966      84445     NA         NA        NA     9280 1629589022
#> 5 fb_5966      84446  30435         NA      5639       NA 1629589009
#> 6 fb_9045     104337  13609         NA      5804       NA 1629588823

First we prepare shared URLs:

# URLs
urls_data <- prep_data(german_elections_sample,
                       object_id = "url_id",
                       account_id = "account_id",
                       content_id = "post_id",
                       timestamp_share = "timestamp")

urls_data$object_id <- paste0("url_", urls_data$object_id)

Next, we prepare images. We used the pHash algorithm to uniquely identify images. The algorithm is implemented in the OpenImageR package (Mouselimis 2023).

# images (pHash)
img_data <- prep_data(german_elections_sample,
                      object_id = "phash_id",
                      account_id = "account_id",
                      content_id = "post_id",
                      timestamp_share = "timestamp")

img_data$object_id <- paste0("hash_", img_data$object_id)

Next, we perform the first step of coordination detection on each subset of the data with the detect_groups function:

# Detect coordinated groups for URLs and images  --------------------
result_urls <- detect_groups(urls_data, time_window = 30,
                             min_participation = 2)

result_images <- detect_groups(img_data, time_window = 30,
                               min_participation = 2)

To study only URL shares 10 to 30 seconds apart, we can make a separate call. Both 10 and 30 seconds are included:

result_urls_10_30 <- detect_groups(
  urls_data, time_window = 30, time_window_low = 10,
  min_participation = 2
)
head(result_urls_10_30)
#>    object_id content_id content_id_y time_delta account_id account_id_y
#>       <char>     <char>       <char>      <num>     <char>       <char>
#> 1: url_10813      93413        81826         30    fb_7103      fb_5761
#> 2: url_21770      54564       123648         28    fb_4039     fb_12065
#> 3: url_21770     117955        45559         11   fb_11199      fb_3297
#> 4: url_14583     179086       117972         15   fb_18649     fb_11202
#> 5: url_14583      36588        54562         18    fb_2258      fb_4039
#> 6: url_30435     204242       108918         21   fb_21069      fb_9754

Then we can simply stack the original URL and image results:

# Combine results  --------------------
library(data.table)

combined_results <- rbindlist( 
    list(result_urls, result_images),
    use.names = TRUE,
    fill = TRUE
)

Now we can generate a filtered network with subgraph = 1 (also the current default) to find accounts that show coordinated behavior in terms of image and URL sharing. The input includes both platforms, but the filtered graph in this example has no Facebook–Twitter edges; including two platforms does not itself establish cross-platform coordination.

# Generate the coordinated multi-modal network  --------------------
graph <- generate_coordinated_network(combined_results, subgraph = 1)
graph
#> IGRAPH 8a8ef1a UNW- 167 231 -- 
#> + attr: name (v/c), weight (e/n), avg_time_delta (e/n), n_content_id
#> | (e/n), n_content_id_y (e/n), edge_symmetry_score (e/n),
#> | weight_threshold (e/n)
#> + edges from 8a8ef1a (vertex names):
#>  [1] fb_5761 --fb_7103  fb_12065--fb_4039  fb_11199--fb_3297  fb_2258 --fb_4039 
#>  [5] fb_21069--fb_9754  fb_18649--fb_4039  fb_4039 --fb_7548  fb_12670--fb_17402
#>  [9] fb_10893--fb_14704 fb_13917--fb_6467  fb_16134--fb_9754  fb_4039 --fb_16062
#> [13] fb_12670--fb_7103  fb_7548 --fb_17635 fb_14704--fb_17694 fb_17326--fb_16062
#> [17] fb_21582--fb_6019  fb_4550 --fb_6019  fb_21582--fb_4550  fb_18649--fb_7548 
#> [21] fb_17966--fb_18029 fb_16865--fb_18029 fb_17966--fb_16865 fb_10893--fb_15306
#> + ... omitted several edges

Visualization

Visualize the coordination network using igraph:

library(igraph)
plot.igraph(
    graph,
    layout = layout.fruchterman.reingold,
    edge.width = 0.5,
    edge.curved = 0.3,
    vertex.size = 3,
    vertex.frame.color = "grey",
    vertex.frame.width = 0.1,
    vertex.label = NA
)

Additional features

The CooRTweet package includes several additional functions and features that enable refined exploration of coordinated networks, as detailed in the package documentation.

Conclusion

The CooRTweet package enables researchers to study coordinated behaviors with a high degree of flexibility and precision. Its generalized architecture makes it adaptable to various contexts and datasets, empowering social media research and analysis.

Coordinated Sharing Detection Service powered by CooRTweet

CooRTweet also powers the Coordinated Sharing Detection Service, a web-based service built on a streamlined version of the R package. It enables data journalists, fact-checkers, researchers, and other practitioners to analyze and visualize patterns of coordinated sharing in social media data.

Acknowledgments

This work was partially supported by the vera.ai project, funded by the European Union through the Horizon Europe program (Grant Agreement ID 101070093).

References

Giglietto, Fabio, Nicola Righetti, Luca Rossi, and Giada Marino. 2020. “It Takes a Village to Manipulate the Media: Coordinated Link Sharing Behavior During 2018 and 2019 Italian Elections.” Information, Communication & Society 23 (6): 867–91. https://doi.org/10.1080/1369118X.2020.1739732.
Kulichkina, Aytalina, Nicola Righetti, and Annie Waldherr. 2024. “Protest and Repression on Social Media: Pro-Navalny and Pro-Government Mobilization Dynamics and Coordination Patterns on Russian Twitter.” New Media & Society, ahead of print. https://doi.org/10.1177/14614448241254126.
Mouselimis, Lampros. 2023. OpenImageR: An Image Processing Toolkit. https://CRAN.R-project.org/package=OpenImageR.
Righetti, Nicola, and Paul Balluff. 2025. “CooRTweet: A Generalized R Software for Coordinated Network Detection.” Computational Communication Research 7 (1): 1. https://doi.org/10.5117/CCR2025.1.7.RIGH.
Righetti, Nicola, Fabio Giglietto, Azade Esther Kakavand, et al. 2022. “Political Advertisement and Coordinated Behavior on Social Media in the Lead-up to the 2021 German Federal Elections.” Dusseldorf: Media Authority of North Rhine-Westphalia.