├── apt.txt ├── .github ├── .gitignore ├── workflows │ ├── pkgdown.yaml │ ├── R-CMD-check.yaml │ └── test-coverage.yaml └── CONTRIBUTING.md ├── runtime.txt ├── install.R ├── src ├── .gitignore ├── complex_walks.cpp ├── circArc.cpp ├── triadCensus.cpp ├── optimBlocks.cpp ├── optimBlocksGen.cpp └── RcppExports.cpp ├── vignettes ├── .gitignore ├── balance_triples.png ├── small_signed2mode.png ├── blockmodel_example.png ├── blockmodel_general.png ├── signed_networks.Rmd ├── complex_matrices.Rmd ├── centrality.Rmd ├── blockmodeling.Rmd ├── structural_balance.Rmd └── signed_2mode.Rmd ├── LICENSE ├── data ├── avatar.rda ├── cowList.rda └── tribes.rda ├── tests ├── testthat.R └── testthat │ ├── test-plot.R │ ├── test-utils.R │ ├── test-random_graphs.R │ ├── test-balance_scores.R │ ├── test-blockmodel.R │ ├── test-laplace_matrix.R │ ├── test-signed_triangles.R │ ├── test-centrality_indices.R │ └── test-complex_matrices.R ├── cran-comments.md ├── man ├── figures │ ├── logo.png │ ├── balance_triples.png │ ├── block_example.pdf │ ├── signed_triads.png │ ├── README-block_example-1.png │ └── README-general_example-1.png ├── is_signed.Rd ├── avatar.Rd ├── cowList.Rd ├── as_adj_complex.Rd ├── triad_census_signed.Rd ├── as_adj_signed.Rd ├── as_incidence_signed.Rd ├── as_complex_edges.Rd ├── signed_triangles.Rd ├── graph_circular_signed.Rd ├── count_signed_triangles.Rd ├── complex_walks.Rd ├── tribes.Rd ├── graph_from_edgelist_signed.Rd ├── degree_signed.Rd ├── count_complex_triangles.Rd ├── graph_from_adjacency_matrix_signed.Rd ├── sample_islands_signed.Rd ├── laplacian_matrix_complex.Rd ├── as_signed_proj.Rd ├── sample_gnp_signed.Rd ├── ggblock.Rd ├── laplacian_matrix_signed.Rd ├── ggsigned.Rd ├── as_incidence_complex.Rd ├── as_unsigned_2mode.Rd ├── pn_index.Rd ├── frustration_exact.Rd ├── sample_bipartite_signed.Rd ├── eigen_centrality_signed.Rd ├── signed_blockmodel.Rd ├── signed_blockmodel_general.Rd └── balance_score.Rd ├── _quarto.yml ├── R ├── signnet-package.R ├── data_avatar.R ├── data_cow.R ├── data_tribes.R ├── laplace_matrix.R ├── RcppExports.R ├── utils.R ├── blockmodel.R ├── centrality_indices.R ├── plot.R ├── balance_scores.R └── random_graphs.R ├── codecov.yml ├── signnet.Rproj ├── .Rbuildignore ├── _pkgdown.yml ├── inst └── CITATION ├── .gitignore ├── NAMESPACE ├── LICENSE.md ├── DESCRIPTION ├── postBuild ├── NEWS.md ├── CODE_OF_CONDUCT.md ├── README.Rmd ├── README.md ├── data-raw ├── paper.bib └── paper.md ├── methodshub.qmd └── CITATION.cff /apt.txt: -------------------------------------------------------------------------------- 1 | zip -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | r-4.4.2-2024-10-31 -------------------------------------------------------------------------------- /install.R: -------------------------------------------------------------------------------- 1 | install.packages("signnet") 2 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: David Schoch 3 | -------------------------------------------------------------------------------- /data/avatar.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/data/avatar.rda -------------------------------------------------------------------------------- /data/cowList.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/data/cowList.rda -------------------------------------------------------------------------------- /data/tribes.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/data/tribes.rda -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(signnet) 3 | 4 | test_check("signnet") 5 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | # Update from 1.0.5 to 1.0.6 2 | 3 | - fix deprecated igraph calls and bug fixes -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | title: signnet 3 | type: default 4 | render: 5 | - methodshub.qmd 6 | -------------------------------------------------------------------------------- /R/signnet-package.R: -------------------------------------------------------------------------------- 1 | #' @useDynLib signnet, .registration = TRUE 2 | #' @importFrom Rcpp sourceCpp 3 | NULL 4 | -------------------------------------------------------------------------------- /man/figures/balance_triples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/man/figures/balance_triples.png -------------------------------------------------------------------------------- /man/figures/block_example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/man/figures/block_example.pdf -------------------------------------------------------------------------------- /man/figures/signed_triads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/man/figures/signed_triads.png -------------------------------------------------------------------------------- /vignettes/balance_triples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/vignettes/balance_triples.png -------------------------------------------------------------------------------- /vignettes/small_signed2mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/vignettes/small_signed2mode.png -------------------------------------------------------------------------------- /vignettes/blockmodel_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/vignettes/blockmodel_example.png -------------------------------------------------------------------------------- /vignettes/blockmodel_general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/vignettes/blockmodel_general.png -------------------------------------------------------------------------------- /man/figures/README-block_example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/man/figures/README-block_example-1.png -------------------------------------------------------------------------------- /man/figures/README-general_example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schochastics/signnet/HEAD/man/figures/README-general_example-1.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 1% 13 | -------------------------------------------------------------------------------- /R/data_avatar.R: -------------------------------------------------------------------------------- 1 | #' Signed networks from Avatar: The Last Airbender 2 | #' @description Allies/Enemy relations from Avatar: The Last Airbender 3 | 4 | #' @format igraph object 5 | #' @source scraped from Avatar Wiki (https://avatar.fandom.com/wiki/Category:Characters) 6 | "avatar" 7 | -------------------------------------------------------------------------------- /tests/testthat/test-plot.R: -------------------------------------------------------------------------------- 1 | test_that("ggblock works", { 2 | data("tribes") 3 | clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = TRUE) 4 | p <- ggblock(tribes, clu$membership, show_blocks = TRUE, show_labels = TRUE) 5 | expect_true(all(p$data$value %in% c(-1, 1))) 6 | expect_equal(length(p$layers), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /R/data_cow.R: -------------------------------------------------------------------------------- 1 | #' Signed networks from Correlates of War 2 | #' @description 51 signed networks of inter state relations 3 | 4 | #' @format List of igraph objects 5 | #' @source http://mrvar.fdv.uni-lj.si/pajek/SVG/CoW/default.htm 6 | #' @references Doreian, P. and Mrvar, A. (2015). "Structural Balance and Signed International Relations". *Journal of Social Structure*, 16(2) 7 | "cowList" 8 | -------------------------------------------------------------------------------- /man/is_signed.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{is_signed} 4 | \alias{is_signed} 5 | \title{Check if network is a signed network} 6 | \usage{ 7 | is_signed(g) 8 | } 9 | \arguments{ 10 | \item{g}{igraph object} 11 | } 12 | \value{ 13 | logical scalar 14 | } 15 | \description{ 16 | Check if network is a signed network 17 | } 18 | \examples{ 19 | g <- sample_islands_signed(2, 5, 1, 5) 20 | is_signed(g) 21 | } 22 | -------------------------------------------------------------------------------- /signnet.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/avatar.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data_avatar.R 3 | \docType{data} 4 | \name{avatar} 5 | \alias{avatar} 6 | \title{Signed networks from Avatar: The Last Airbender} 7 | \format{ 8 | igraph object 9 | } 10 | \source{ 11 | scraped from Avatar Wiki (https://avatar.fandom.com/wiki/Category:Characters) 12 | } 13 | \usage{ 14 | avatar 15 | } 16 | \description{ 17 | Allies/Enemy relations from Avatar: The Last Airbender 18 | } 19 | \keyword{datasets} 20 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^signnet\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^\.travis\.yml$ 6 | ^codecov\.yml$ 7 | ^doc$ 8 | ^Meta$ 9 | ^cran-comments\.md$ 10 | ^CRAN-RELEASE$ 11 | ^_pkgdown\.yml$ 12 | ^docs$ 13 | ^pkgdown$ 14 | ^\.github$ 15 | ^CRAN-SUBMISSION$ 16 | paper.md 17 | paper.pdf 18 | paper.bib 19 | ^CODE_OF_CONDUCT\.md$ 20 | LICENSE.md 21 | ^data-raw$ 22 | ^CITATION\.cff$ 23 | ^install\.R$ 24 | ^postBuild$ 25 | ^apt\.txt$ 26 | ^runtime\.txt$ 27 | ^_quarto\.yml$ 28 | ^\.quarto$ 29 | ^methodshub 30 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://schochastics.github.io/signnet/ 2 | template: 3 | bootstrap: 5 4 | bslib: 5 | bg: '#F9F7F7' 6 | fg: '#112D4E' 7 | primary: '#3F72AF' 8 | text-muted: '#FFFFFF' 9 | base_font: 10 | google: Roboto 11 | heading_font: 12 | google: Roboto Slab 13 | code_font: 14 | google: JetBrains Mono 15 | navbar: 16 | bg: none 17 | type: light 18 | structure: 19 | left: 20 | - intro 21 | - reference 22 | - articles 23 | - news 24 | right: github 25 | 26 | -------------------------------------------------------------------------------- /R/data_tribes.R: -------------------------------------------------------------------------------- 1 | #' Signed network of New Guinean highland tribes 2 | #' @description Signed social network of tribes of the Gahuku–Gama alliance structure of the Eastern Central Highlands of New Guinea, from Kenneth Read. 3 | #' The network contains sixteen tribes connected by friendship ("rova") and enmity ("hina"). 4 | #' @format An igraph object 5 | #' @source http://vlado.fmf.uni-lj.si/pub/networks/data/ucinet/gama.dat 6 | #' @references Read, K. E. (1954) Cultures of the central highlands, New Guinea. *Southwestern Journal of Anthropology*, 1–43. 7 | "tribes" 8 | -------------------------------------------------------------------------------- /man/cowList.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data_cow.R 3 | \docType{data} 4 | \name{cowList} 5 | \alias{cowList} 6 | \title{Signed networks from Correlates of War} 7 | \format{ 8 | List of igraph objects 9 | } 10 | \source{ 11 | http://mrvar.fdv.uni-lj.si/pajek/SVG/CoW/default.htm 12 | } 13 | \usage{ 14 | cowList 15 | } 16 | \description{ 17 | 51 signed networks of inter state relations 18 | } 19 | \references{ 20 | Doreian, P. and Mrvar, A. (2015). "Structural Balance and Signed International Relations". \emph{Journal of Social Structure}, 16(2) 21 | } 22 | \keyword{datasets} 23 | -------------------------------------------------------------------------------- /src/complex_walks.cpp: -------------------------------------------------------------------------------- 1 | // [[Rcpp::depends(RcppArmadillo)]] 2 | #include 3 | 4 | // [[Rcpp::export]] 5 | arma::cx_mat cxmatmul(arma::cx_mat A,arma::cx_mat B) { 6 | int n=A.n_rows; 7 | arma::cx_mat C(n,n); 8 | arma::cx_vec tmp(n); 9 | arma::vec Repart(n); 10 | arma::vec Impart(n); 11 | 12 | for(int i=0;i 2 | using namespace Rcpp; 3 | 4 | 5 | // [[Rcpp::export]] 6 | double arcDist(NumericVector x,NumericVector y, double r) { 7 | static const double pi = 3.14159265; 8 | double c = sqrt((x[0]-y[0])*(x[0]-y[0])+(x[1]-y[1])*(x[1]-y[1])); 9 | double theta = acos((2*r*r-c*c)/(2*r*r)); 10 | return 2*pi*r*theta/(2*pi); 11 | } 12 | 13 | 14 | // [[Rcpp::export]] 15 | NumericMatrix arcDistMat(NumericMatrix X,double r) { 16 | int n = X.nrow(); 17 | NumericMatrix D(n,n); 18 | for(int i=0;i, blockmodeling algorithms 10 | from Doreian (2008) , various 11 | centrality indices, and projections of signed two-mode networks 12 | introduced by Schoch (2020) . 13 | License: MIT + file LICENSE 14 | URL: https://github.com/schochastics/signnet, https://schochastics.github.io/signnet/ 15 | BugReports: https://github.com/schochastics/signnet/issues 16 | Depends: 17 | R (>= 3.2.0) 18 | Imports: 19 | igraph (>= 2.2.0), 20 | Matrix, 21 | Rcpp 22 | Suggests: 23 | covr, 24 | ggplot2, 25 | ggraph, 26 | knitr, 27 | ompr, 28 | ompr.roi, 29 | rmarkdown, 30 | ROI, 31 | ROI.plugin.glpk, 32 | testthat (>= 2.1.0) 33 | LinkingTo: 34 | Rcpp, 35 | RcppArmadillo 36 | VignetteBuilder: 37 | knitr 38 | Encoding: UTF-8 39 | LazyData: true 40 | Roxygen: list(markdown = TRUE) 41 | RoxygenNote: 7.3.2 42 | -------------------------------------------------------------------------------- /man/sample_bipartite_signed.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/random_graphs.R 3 | \name{sample_bipartite_signed} 4 | \alias{sample_bipartite_signed} 5 | \title{Bipartite random signed graphs} 6 | \usage{ 7 | sample_bipartite_signed( 8 | n1, 9 | n2, 10 | p, 11 | p_neg, 12 | directed = FALSE, 13 | mode = c("out", "in", "all") 14 | ) 15 | } 16 | \arguments{ 17 | \item{n1}{Integer scalar, the number of bottom vertices.} 18 | 19 | \item{n2}{Integer scalar, the number of top vertices.} 20 | 21 | \item{p}{The probability for drawing an edge between two arbitrary vertices.} 22 | 23 | \item{p_neg}{The probability of a drawn edge to be a negative tie} 24 | 25 | \item{directed}{logical, whether the graph will be directed. defaults to FALSE.} 26 | 27 | \item{mode}{Character scalar, specifies how to direct the edges in directed graphs. If it is ‘out’, then directed edges point from bottom vertices to top vertices. If it is ‘in’, edges point from top vertices to bottom vertices. ‘out’ and ‘in’ do not generate mutual edges. If this argument is ‘all’, then each edge direction is considered independently and mutual edges might be generated. This argument is ignored for undirected graphs.} 28 | } 29 | \value{ 30 | A signed bipartite igraph graph. 31 | } 32 | \description{ 33 | Bipartite random signed graphs 34 | } 35 | \examples{ 36 | sample_bipartite_signed(10, 10, 0.5, 0.5) 37 | } 38 | -------------------------------------------------------------------------------- /tests/testthat/test-balance_scores.R: -------------------------------------------------------------------------------- 1 | test_that("triangle balance index works", { 2 | g <- igraph::make_full_graph(5) 3 | igraph::E(g)$sign <- 1 4 | expect_equal(balance_score(g, method = "triangles"), 1) 5 | }) 6 | 7 | test_that("walk balance index works", { 8 | g <- igraph::make_full_graph(5) 9 | igraph::E(g)$sign <- 1 10 | expect_equal(balance_score(g, method = "walk"), 1) 11 | }) 12 | 13 | test_that("frustration balance index works", { 14 | g <- igraph::make_full_graph(5) 15 | igraph::E(g)$sign <- 1 16 | expect_equal(balance_score(g, method = "frustration"), 1) 17 | }) 18 | 19 | 20 | test_that("directed check works", { 21 | g <- igraph::make_full_graph(5, directed = TRUE) 22 | igraph::E(g)$sign <- 1 23 | expect_error(balance_score(g)) 24 | }) 25 | 26 | test_that("sign check works", { 27 | g <- igraph::make_full_graph(5, directed = FALSE) 28 | expect_error(balance_score(g)) 29 | }) 30 | 31 | test_that("wrong sign values check works", { 32 | g <- igraph::make_full_graph(5, directed = FALSE) 33 | igraph::E(g)$sign <- 2 34 | expect_error(balance_score(g)) 35 | }) 36 | 37 | test_that("frustration exact error handling", { 38 | g <- igraph::make_full_graph(5, directed = FALSE) 39 | igraph::E(g)$sign <- 2 40 | expect_error(frustration_exact(g)) 41 | g <- igraph::make_full_graph(5, directed = TRUE) 42 | igraph::E(g)$sign <- 1 43 | expect_error(frustration_exact(g)) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/testthat/test-blockmodel.R: -------------------------------------------------------------------------------- 1 | test_that("blockmodeling works", { 2 | data("tribes") 3 | clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = TRUE) 4 | expect_lte(max(clu$membership), 3) 5 | }) 6 | 7 | test_that("blockmodeling no anneal works ", { 8 | data("tribes") 9 | clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = FALSE) 10 | expect_lte(max(clu$membership), 3) 11 | }) 12 | 13 | test_that("blockmodeling sign check works", { 14 | g <- igraph::make_full_graph(5) 15 | expect_error(signed_blockmodel(g)) 16 | }) 17 | 18 | test_that("blockmodeling k error works", { 19 | data("tribes") 20 | expect_error(signed_blockmodel(tribes)) 21 | }) 22 | 23 | test_that("general blockmodeling works", { 24 | data("tribes") 25 | clu <- signed_blockmodel_general( 26 | tribes, 27 | blockmat = matrix(c(1, -1, -1, -1, 1, -1, -1, -1, 1), 3, 3, byrow = T) 28 | ) 29 | expect_lte(max(clu$membership), 3) 30 | }) 31 | 32 | test_that("general blockmodeling blockmat error works", { 33 | data("tribes") 34 | expect_error(signed_blockmodel_general(tribes)) 35 | }) 36 | 37 | test_that("general blockmodeling blockmat error 2 works", { 38 | data("tribes") 39 | B <- matrix(c(2, 2, 2, 2), 2, 2) 40 | expect_error(signed_blockmodel_general(tribes, blockmat = B)) 41 | }) 42 | 43 | test_that("general blockmodeling sign check works", { 44 | g <- igraph::make_full_graph(5) 45 | expect_error(signed_blockmodel_general(g)) 46 | }) 47 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - uses: r-lib/actions/setup-pandoc@v2 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | with: 31 | use-public-rspm: true 32 | 33 | - uses: r-lib/actions/setup-r-dependencies@v2 34 | with: 35 | extra-packages: any::pkgdown, local::. 36 | needs: website 37 | 38 | - name: Build site 39 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 40 | shell: Rscript {0} 41 | 42 | - name: Deploy to GitHub pages 🚀 43 | if: github.event_name != 'pull_request' 44 | uses: JamesIves/github-pages-deploy-action@v4.4.1 45 | with: 46 | clean: false 47 | branch: gh-pages 48 | folder: docs 49 | -------------------------------------------------------------------------------- /R/RcppExports.R: -------------------------------------------------------------------------------- 1 | # Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | arcDist <- function(x, y, r) { 5 | .Call(`_signnet_arcDist`, x, y, r) 6 | } 7 | 8 | arcDistMat <- function(X, r) { 9 | .Call(`_signnet_arcDistMat`, X, r) 10 | } 11 | 12 | cxmatmul <- function(A, B) { 13 | .Call(`_signnet_cxmatmul`, A, B) 14 | } 15 | 16 | blockCriterion <- function(A, clu, alpha) { 17 | .Call(`_signnet_blockCriterion`, A, clu, alpha) 18 | } 19 | 20 | critUpdate <- function(A, v, from, to, clu, alpha) { 21 | .Call(`_signnet_critUpdate`, A, v, from, to, clu, alpha) 22 | } 23 | 24 | optimBlocks1 <- function(A, clu, k, alpha) { 25 | .Call(`_signnet_optimBlocks1`, A, clu, k, alpha) 26 | } 27 | 28 | blockCriterion1 <- function(clu, A, alpha, k) { 29 | .Call(`_signnet_blockCriterion1`, clu, A, alpha, k) 30 | } 31 | 32 | blockCriterionS <- function(A, clu, alpha, sgrp) { 33 | .Call(`_signnet_blockCriterionS`, A, clu, alpha, sgrp) 34 | } 35 | 36 | critUpdateS <- function(A, v, from, to, clu, alpha, sgrp) { 37 | .Call(`_signnet_critUpdateS`, A, v, from, to, clu, alpha, sgrp) 38 | } 39 | 40 | optimBlocksSimS <- function(A, clu, sgrp, alpha) { 41 | .Call(`_signnet_optimBlocksSimS`, A, clu, sgrp, alpha) 42 | } 43 | 44 | triadCensusSign <- function(A, n) { 45 | .Call(`_signnet_triadCensusSign`, A, n) 46 | } 47 | 48 | triadCensusSign1 <- function(A, adj, n) { 49 | .Call(`_signnet_triadCensusSign1`, A, adj, n) 50 | } 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macos-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | 34 | - uses: r-lib/actions/setup-pandoc@v2 35 | 36 | - uses: r-lib/actions/setup-r@v2 37 | with: 38 | r-version: ${{ matrix.config.r }} 39 | http-user-agent: ${{ matrix.config.http-user-agent }} 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::rcmdcheck 45 | needs: check 46 | 47 | - uses: r-lib/actions/check-r-package@v2 48 | with: 49 | upload-snapshots: true 50 | -------------------------------------------------------------------------------- /man/eigen_centrality_signed.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/centrality_indices.R 3 | \name{eigen_centrality_signed} 4 | \alias{eigen_centrality_signed} 5 | \title{Signed Eigenvector centrality} 6 | \usage{ 7 | eigen_centrality_signed(g, scale = TRUE) 8 | } 9 | \arguments{ 10 | \item{g}{igraph object with a sign edge attribute.} 11 | 12 | \item{scale}{Logical scalar, whether to scale the result to have a maximum score of one. If no scaling is used then the result vector is the same as returned by \code{eigen()}.} 13 | } 14 | \value{ 15 | centrality scores as numeric vector. 16 | } 17 | \description{ 18 | returns the eigenvector associated with the dominant eigenvalue from the adjacency matrix. 19 | } 20 | \details{ 21 | Note that, with negative values, the adjacency matrix may not have a dominant eigenvalue. 22 | This means it is not clear which eigenvector should be used. In addition it is possible for the adjacency matrix to have repeated eigenvalues and hence multiple linearly independent eigenvectors. In this case certain centralities can be arbitrarily assigned. The function returns an error if this is the case. 23 | } 24 | \examples{ 25 | library(igraph) 26 | data("tribes") 27 | eigen_centrality_signed(tribes) 28 | } 29 | \references{ 30 | Bonacich, P. and Lloyd, P. (2004). "Calculating Status with Negative Relations." \emph{Social Networks} 26 (4): 331–38. 31 | 32 | Everett, M. and Borgatti, S.P. (2014). "Networks Containing Negative Ties." \emph{Social Networks} 38: 111–20. 33 | } 34 | \author{ 35 | David Schoch 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: | 31 | covr::codecov( 32 | quiet = FALSE, 33 | clean = FALSE, 34 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 35 | ) 36 | shell: Rscript {0} 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: | 41 | ## -------------------------------------------------------------------- 42 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 43 | shell: bash 44 | 45 | - name: Upload test results 46 | if: failure() 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: coverage-test-failures 50 | path: ${{ runner.temp }}/package 51 | -------------------------------------------------------------------------------- /tests/testthat/test-laplace_matrix.R: -------------------------------------------------------------------------------- 1 | test_that("laplacian matrix correct", { 2 | g <- igraph::make_full_graph(5) 3 | igraph::E(g)$sign <- -1 4 | L <- laplacian_matrix_signed(g) 5 | expect_equal(diag(L), rep(4, 5)) 6 | }) 7 | 8 | test_that("laplacian matrix norm correct", { 9 | g <- igraph::make_full_graph(5) 10 | igraph::E(g)$sign <- -1 11 | L <- laplacian_matrix_signed(g, norm = TRUE) 12 | L_true <- structure( 13 | c( 14 | 1, 15 | 0.25, 16 | 0.25, 17 | 0.25, 18 | 0.25, 19 | 0.25, 20 | 1, 21 | 0.25, 22 | 0.25, 23 | 0.25, 24 | 0.25, 25 | 0.25, 26 | 1, 27 | 0.25, 28 | 0.25, 29 | 0.25, 30 | 0.25, 31 | 0.25, 32 | 1, 33 | 0.25, 34 | 0.25, 35 | 0.25, 36 | 0.25, 37 | 0.25, 38 | 1 39 | ), 40 | .Dim = c(5L, 5L) 41 | ) 42 | expect_equal(L, L_true) 43 | }) 44 | 45 | test_that("laplacian matrix error graph correct", { 46 | expect_error(laplacian_matrix_signed(g = 5)) 47 | }) 48 | 49 | test_that("laplacian matrix error sign correct", { 50 | g <- igraph::make_full_graph(5) 51 | expect_error(laplacian_matrix_signed(g)) 52 | }) 53 | 54 | # 55 | # test_that("laplacian angle sign is correct", { 56 | # g <- igraph::make_full_graph(3) 57 | # igraph::E(g)$sign <- 1 58 | # ang <- laplacian_angle(g) 59 | # ang_true <- c(0.95531662, -0.61547971, -0.61547971) 60 | # }) 61 | # 62 | # test_that("laplacian angle sign is correct", { 63 | # g <- igraph::make_full_graph(3) 64 | # igraph::E(g)$type <- "P" 65 | # ang <- laplacian_angle(g,"complex",attr="type") 66 | # ang_true <- c(0, 0, 3.14159265) 67 | # }) 68 | -------------------------------------------------------------------------------- /postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S bash -v 2 | 3 | # determine which version of Quarto to install 4 | QUARTO_VERSION=1.6.37 5 | 6 | # See whether we need to lookup a Quarto version 7 | if [ $QUARTO_VERSION = "prerelease" ]; then 8 | QUARTO_JSON="_prerelease.json" 9 | elif [ $QUARTO_VERSION = "release" ]; then 10 | QUARTO_JSON="_download.json" 11 | fi 12 | 13 | if [ $QUARTO_JSON != "" ]; then 14 | 15 | # create a python script and run it 16 | PYTHON_SCRIPT=_quarto_version.py 17 | if [ -e $PYTHON_SCRIPT ]; then 18 | rm -rf $PYTHON_SCRIPT 19 | fi 20 | 21 | cat > $PYTHON_SCRIPT < 2 | // [[Rcpp::depends(RcppArmadillo)]] 3 | using namespace Rcpp; 4 | 5 | 6 | // [[Rcpp::export]] 7 | IntegerVector triadCensusSign(NumericMatrix A, int n) 8 | { 9 | 10 | int code = 0; 11 | IntegerVector triads(729); 12 | 13 | for (int u = 0; u < n; ++u) 14 | { 15 | for (int v = 0; v < n; ++v) 16 | { 17 | for (int w = 0; w < n; ++w) 18 | { 19 | if ((u < v) && (v < w)) 20 | { 21 | code = A(u, v) + 3 * A(u, w) + 9 * A(v, u) + 27 * A(v, w) + 81 * A(w, u) + 243 * A(w, v); 22 | triads[code] = triads[code] + 1; 23 | } 24 | } 25 | } 26 | } 27 | return triads; 28 | } 29 | 30 | // [[Rcpp::export]] 31 | DoubleVector triadCensusSign1(const arma::sp_mat &A, List adj, int n) 32 | { 33 | long long code = 0; 34 | DoubleVector triads(729); 35 | for (int u = 0; u < n; u++) 36 | { 37 | IntegerVector Nu = as(adj[u]); 38 | int nu = Nu.length(); 39 | for (int j = 0; j < nu; j++) 40 | { 41 | int v = Nu[j]; 42 | if (u < v) 43 | { 44 | IntegerVector Nv = as(adj[v]); 45 | IntegerVector S = union_(Nu, Nv); 46 | IntegerVector uv = {u, v}; 47 | S = setdiff(S, uv); 48 | code = (A(u, v) + 1) + 3 + 9 * (A(v, u) + 1) + 27 + 81 + 243; 49 | triads[code] = triads[code] + n - S.length() - 2; 50 | for (int k = 0; k < S.length(); k++) 51 | { 52 | int w = S[k]; 53 | if ((v < w) || ((u < w) && (w < v) && (A(u, w) == 0) && (A(w, u) == 0))) 54 | { 55 | code = (A(u, v) + 1) + 3 * (A(u, w) + 1) + 9 * (A(v, u) + 1) + 27 * (A(v, w) + 1) + 81 * (A(w, u) + 1) + 243 * (A(w, v) + 1); 56 | triads[code] = triads[code] + 1; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | return triads; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /man/signed_blockmodel_general.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/blockmodel.R 3 | \name{signed_blockmodel_general} 4 | \alias{signed_blockmodel_general} 5 | \title{Generalized blockmodeling for signed networks} 6 | \usage{ 7 | signed_blockmodel_general(g, blockmat, alpha = 0.5) 8 | } 9 | \arguments{ 10 | \item{g}{igraph object with a sign edge attribute.} 11 | 12 | \item{blockmat}{Integer Matrix. Specifies the inter/intra group patterns of ties} 13 | 14 | \item{alpha}{see details} 15 | } 16 | \value{ 17 | numeric vector of block assignments and the associated criterion value 18 | } 19 | \description{ 20 | Finds blocks of nodes with specified inter/intra group ties 21 | } 22 | \details{ 23 | The function minimizes P(C)=\eqn{\alpha}N+(1-\eqn{\alpha})P, 24 | where N is the total number of negative ties within plus-sets and P be the total number of 25 | positive ties between plus-sets. This function implements the generalized model. For the structural balance 26 | version see \link{signed_blockmodel}. 27 | } 28 | \examples{ 29 | library(igraph) 30 | # create a signed network with three groups and different inter/intra group ties 31 | g1 <- g2 <- g3 <- make_full_graph(5) 32 | 33 | V(g1)$name <- as.character(1:5) 34 | V(g2)$name <- as.character(6:10) 35 | V(g3)$name <- as.character(11:15) 36 | 37 | g <- Reduce("\%u\%", list(g1, g2, g3)) 38 | E(g)$sign <- 1 39 | E(g)$sign[1:10] <- -1 40 | g <- add_edges(g, c(rbind(1:5, 6:10)), attr = list(sign = -1)) 41 | g <- add_edges(g, c(rbind(1:5, 11:15)), attr = list(sign = -1)) 42 | g <- add_edges(g, c(rbind(11:15, 6:10)), attr = list(sign = 1)) 43 | 44 | # specify the link patterns between groups 45 | blockmat <- matrix(c(1, -1, -1, -1, 1, 1, -1, 1, -1), 3, 3, byrow = TRUE) 46 | signed_blockmodel_general(g, blockmat, 0.5) 47 | } 48 | \references{ 49 | Doreian, Patrick and Andrej Mrvar (2009). Partitioning signed social networks. \emph{Social Networks} 31(1) 1-11 50 | } 51 | \author{ 52 | David Schoch 53 | } 54 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # signnet 1.0.6 2 | 3 | * fixed a bug in eigenvector centrality calculation 4 | 5 | # signnet 1.0.5 6 | 7 | * fix all deprecated igraph calls 8 | 9 | # signnet 1.0.4 10 | 11 | * fix deprecated support of adjacency matrices with character values #26 12 | 13 | # signnet 1.0.3 14 | 15 | * code refactoring 16 | * more tests #17 17 | * removed deprecated calls to aes_ #23 18 | 19 | # signnet 1.0.2 20 | 21 | * fixed a bug in `signed_triangles()` that resulted in wrong vertex ids (#20) 22 | 23 | # signnet 1.0.1 24 | 25 | * fixed an error which occurs with the new version of igraph (https://github.com/igraph/rigraph/pull/633) 26 | 27 | # signnet 1.0.0 28 | 29 | * added code of conduct 30 | * added contributing guide 31 | * added `frustration_exact()` to vignette 32 | * added utility functions `is_signed`,`graph_from_adjacency_matrix_signed`, and `graph_from_edgelist_signed()` 33 | * added random graph models `sample_gnp_signed()`, `sample_bipartite_signed()` 34 | 35 | # signnet 0.8.1 36 | 37 | * fixed existing check errors 38 | 39 | # signnet 0.8.0 40 | 41 | * added `frustration_exact()` to compute the exact number of frustrated edges 42 | * fixed issue with aggregate on r-devel 43 | 44 | # signnet 0.7.1 45 | 46 | * fixed #7 47 | * fixed copy paste error in `as_unsigned_2mode()` 48 | * fixed aggregate error in `as_signed_proj()` 49 | 50 | # signnet 0.7.0 51 | 52 | * added `triad_census_signed()` 53 | 54 | # signnet 0.6.0 55 | 56 | * added `avatar` dataset 57 | * speed up of blockmodeling for larger networks 58 | 59 | # signnet 0.5.3 60 | 61 | * fixed issue in `complex_walks()` 62 | * fixed faulty calculation of directed `pn_index()` 63 | 64 | # signnet 0.5.2 65 | 66 | * fixed `stringsAsFactors` issue in `complex_matrices.R` 67 | 68 | # signnet 0.5.1 69 | 70 | * fixed C++ issue for circular arc graphs 71 | * fixed failing eigen centrality test 72 | 73 | # signnet 0.5.0 74 | 75 | * added vignettes and tests 76 | 77 | # signnet 0.1.0 78 | 79 | * initial version 80 | 81 | 82 | -------------------------------------------------------------------------------- /man/balance_score.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/balance_scores.R 3 | \name{balance_score} 4 | \alias{balance_score} 5 | \title{balancedness of signed network} 6 | \usage{ 7 | balance_score(g, method = "triangles") 8 | } 9 | \arguments{ 10 | \item{g}{igraph object with a sign edge attribute.} 11 | 12 | \item{method}{string indicating the method to be used. See details for options} 13 | } 14 | \value{ 15 | numeric balancedness score between 0 and 1 16 | } 17 | \description{ 18 | Implements several indices to assess the balancedness of a network. 19 | } 20 | \details{ 21 | The method parameter can be one of 22 | \describe{ 23 | \item{\emph{triangles}}{Fraction of balanced triangles. Maximal (=1) if all triangles are balanced.} 24 | \item{\emph{walk}}{\eqn{\sum exp(\lambda_i) / \sum exp(\mu_i)}} where \eqn{\lambda_i} are the eigenvalues of the 25 | signed adjacency matrix and \eqn{\mu_i} of the unsigned adjacency matrix. Maximal (=1) if all walks are balanced. 26 | \item{\emph{frustration}}{The frustration index assumes that the network can be partitioned into two groups, where intra group edges are positive and inter group edges are negative. The index is defined as the sum of intra group negative and inter group positive edges. Note that the problem is NP complete and only an upper bound is returned (based on simulated annealing). Exact methods can be found in the work of Aref. The index is normalized such that it is maximal (=1) if the network is balanced.} 27 | } 28 | } 29 | \examples{ 30 | library(igraph) 31 | g <- make_full_graph(4) 32 | E(g)$sign <- c(-1, 1, 1, -1, -1, 1) 33 | 34 | balance_score(g, method = "triangles") 35 | balance_score(g, method = "walk") 36 | } 37 | \references{ 38 | Estrada, E. (2019). Rethinking structural balance in signed social networks. \emph{Discrete Applied Mathematics}. 39 | 40 | Samin Aref, Mark C Wilson (2018). Measuring partial balance in signed networks. \emph{Journal of Complex Networks}, 6(4): 566–595, https://doi.org/10.1093/comnet/cnx044 41 | } 42 | \author{ 43 | David Schoch 44 | } 45 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Check if network is a signed network 2 | #' 3 | #' @param g igraph object 4 | #' 5 | #' @return logical scalar 6 | #' @export 7 | #' 8 | #' @examples 9 | #' g <- sample_islands_signed(2, 5, 1, 5) 10 | #' is_signed(g) 11 | is_signed <- function(g) { 12 | if (!igraph::is_igraph(g)) { 13 | stop("Not a graph object") 14 | } 15 | if (!"sign" %in% igraph::edge_attr_names(g)) { 16 | return(FALSE) 17 | } 18 | eattrV <- igraph::edge_attr(g, "sign") 19 | if (!all(eattrV %in% c(-1, 1))) { 20 | return(FALSE) 21 | } 22 | return(TRUE) 23 | } 24 | 25 | #' Create signed graphs from adjacency matrices 26 | #' 27 | #' @param A square adjacency matrix of a signed graph 28 | #' @param mode Character scalar, specifies how to interpret the supplied matrix. Possible values are: directed, undirected 29 | #' @param ... additional parameters for `from_adjacency()` 30 | #' 31 | #' @return a signed network as igraph object 32 | #' @export 33 | #' 34 | #' @examples 35 | #' A <- matrix(c(0, 1, -1, 1, 0, 1, -1, 1, 0), 3, 3) 36 | #' graph_from_adjacency_matrix_signed(A) 37 | graph_from_adjacency_matrix_signed <- function(A, mode = "undirected", ...) { 38 | if (!all(A %in% c(-1, 0, 1))) { 39 | stop("A should only have entries -1,0,1") 40 | } 41 | igraph::graph_from_adjacency_matrix(A, mode = mode, weighted = "sign", ...) 42 | } 43 | 44 | #' Create a signed graph from an edgelist matrix 45 | #' 46 | #' @param el The edgelist, a two column matrix, character or numeric. 47 | #' @param signs vector indicating the sign of edges. Entries must be 1 or -1. 48 | #' @param directed whether to create a directed graph. 49 | #' 50 | #' @return a signed network as igraph object 51 | #' @export 52 | #' 53 | #' @examples 54 | #' el <- matrix(c("foo", "bar", "bar", "foobar"), ncol = 2, byrow = TRUE) 55 | #' signs <- c(-1, 1) 56 | #' graph_from_edgelist_signed(el, signs) 57 | graph_from_edgelist_signed <- function(el, signs, directed = FALSE) { 58 | if (!is.matrix(el) || ncol(el) != 2) { 59 | stop("graph_from_edgelist_sgned expects a matrix with two columns") 60 | } 61 | if (length(signs) != nrow(el)) { 62 | stop("signs and el must have the same length") 63 | } 64 | if (!all(signs %in% c(-1, 1))) { 65 | stop("signs should only have entries -1 or 1") 66 | } 67 | g <- igraph::graph_from_edgelist(el, directed = directed) 68 | igraph::E(g)$sign <- signs 69 | g 70 | } 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to signnet 2 | 3 | This outlines how to propose a change to signnet. 4 | For more detailed info about contributing to this, and other tidyverse packages, please see the 5 | [**development contributing guide**](https://rstd.io/tidy-contrib). 6 | 7 | ## Fixing typos 8 | 9 | You can fix typos, spelling mistakes, or grammatical errors in the documentation directly using the GitHub web interface, as long as the changes are made in the _source_ file. 10 | This generally means you'll need to edit [roxygen2 comments](https://roxygen2.r-lib.org/articles/roxygen2.html) in an `.R`, not a `.Rd` file. 11 | You can find the `.R` file that generates the `.Rd` by reading the comment in the first line. 12 | 13 | ## Bigger changes 14 | 15 | If you want to make a bigger change, it's a good idea to first file an issue and make sure someone from the team agrees that it’s needed. 16 | If you’ve found a bug, please file an issue that illustrates the bug with a minimal 17 | [reprex](https://www.tidyverse.org/help/#reprex) (this will also help you write a unit test, if needed). 18 | 19 | ### Pull request process 20 | 21 | * Fork the package and clone onto your computer. If you haven't done this before, we recommend using `usethis::create_from_github("schochastics/signnet", fork = TRUE)`. 22 | 23 | * Install all development dependencies with `devtools::install_dev_deps()`, and then make sure the package passes R CMD check by running `devtools::check()`. 24 | If R CMD check doesn't pass cleanly, it's a good idea to ask for help before continuing. 25 | * Create a Git branch for your pull request (PR). We recommend using `usethis::pr_init("brief-description-of-change")`. 26 | 27 | * Make your changes, commit to git, and then create a PR by running `usethis::pr_push()`, and following the prompts in your browser. 28 | The title of your PR should briefly describe the change. 29 | The body of your PR should contain `Fixes #issue-number`. 30 | 31 | * For user-facing changes, add a bullet to the top of `NEWS.md` (i.e. just below the first header). Follow the style described in . 32 | 33 | ### Code style 34 | 35 | * New code should follow the tidyverse [style guide](https://style.tidyverse.org). 36 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to apply these styles, but please don't restyle code that has nothing to do with your PR. 37 | 38 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2), with [Markdown syntax](https://cran.r-project.org/web/packages/roxygen2/vignettes/rd-formatting.html), for documentation. 39 | 40 | * We use [testthat](https://cran.r-project.org/package=testthat) for unit tests. 41 | Contributions with test cases included are easier to accept. 42 | 43 | ## Code of Conduct 44 | 45 | Please note that the signnet project is released with a 46 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this 47 | project you agree to abide by its terms. 48 | -------------------------------------------------------------------------------- /vignettes/signed_networks.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Signed Networks" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{01 Signed Networks} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message=FALSE, 15 | warning=FALSE 16 | ) 17 | ``` 18 | 19 | This vignette describes signed networks and how they are implemented in the `signnet` package. 20 | 21 | ```{r setup} 22 | library(igraph) 23 | library(signnet) 24 | ``` 25 | 26 | ## What are signed networks? 27 | 28 | Traditional SNA usually deals with relations among entities (e.g. people) that are positive, including "friendship", "advice seeking", etc. Most network analytic tools are devised under this premise, be that centrality indices, clustering tools and so forth. 29 | But of course not all occurring relations are positive. People can be friends but also foes. 30 | 31 | This gives rise to signed networks. These networks are usually composed of both, positive and negative, ties 32 | measured among a set of entities. Traditional network analytic tools are not applicable to such networks without 33 | adapting for negative ties. 34 | 35 | The `signnet` package brings together methods that have been developed to analyse signed networks. This includes 36 | 37 | * Structural balance ([vignette](structural_balance.html)) 38 | * Blockmodeling ([vignette](blockmodeling.html)) 39 | * Centrality ([vignette](centrality.html)) 40 | * Signed two-mode networks ([vignette](signed_2mode.html)) 41 | * Complex matrices ([vignette](complex_matrices.html)) 42 | 43 | ## Data structures for signed networks in `signnet` 44 | 45 | The foundation of `signnet` is provided by `igraph`. All functions in the package assume 46 | that an igraph object is a signed network if it has an edge attribute "sign" with values 1 (positive) or -1 (negative). 47 | 48 | ```{r example} 49 | g <- make_full_graph(5,directed = FALSE,loops = FALSE) 50 | E(g)$sign <- 1 51 | g 52 | ``` 53 | 54 | All methods (should) throw an error if the sign attribute is missing or contains other values than -1 and 1. 55 | 56 | Matrices associated with a signed network follow the `igraph` naming scheme. 57 | The signed adjacency matrix can be obtained with `as_adj_signed()`. 58 | ```{r signed_adj} 59 | data("tribes") 60 | as_adj_signed(tribes)[1:5,1:5] 61 | ``` 62 | The signed Laplacian matrix is obtained by `laplacian_matrix_signed()`. 63 | ```{r signed_lap} 64 | laplacian_matrix_signed(tribes)[1:5,1:5] 65 | ``` 66 | 67 | ## Included datasets 68 | 69 | The package includes two well known datasets. 70 | 71 | The "tribes" dataset is a signed social network of tribes of the Gahuku–Gama alliance structure of the Eastern Central Highlands of New Guinea. The network contains sixteen tribes connected by friendship ("rova") and enmity ("hina"). 72 | 73 | The "cowList" dataset contains a list of 52 signed networks of inter-state relations over time (1946-1999). 74 | Two countries are connected by a positive tie if they form an alliance or have a peace treaty. A negative tie 75 | exists between countries who are at war or in other kinds of conflicts. The dataset is derrived from the 76 | [correlates of war](https://correlatesofwar.org/). 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/optimBlocks.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // [[Rcpp::depends(RcppArmadillo)]] 4 | using namespace Rcpp; 5 | 6 | // [[Rcpp::export]] 7 | double blockCriterion(arma::sp_mat A,IntegerVector clu,double alpha){ 8 | double P = 0; 9 | double N = 0; 10 | 11 | for (arma::sp_mat::const_iterator i = A.begin(); i != A.end(); ++i) { 12 | if((clu[i.row()]==clu[i.col()]) && (*i==-1)){ 13 | N+=1; 14 | } 15 | if((clu[i.row()]!=clu[i.col()]) && (*i==1)){ 16 | P+=1; 17 | } 18 | } 19 | return alpha*N+(1-alpha)*P; 20 | } 21 | 22 | // [[Rcpp::export]] 23 | double critUpdate(arma::sp_mat A, int v, int from, int to,IntegerVector clu,double alpha){ 24 | double P = 0; 25 | double N = 0; 26 | arma::sp_mat::const_col_iterator start = A.begin_col(v); 27 | arma::sp_mat::const_col_iterator end = A.end_col(v); 28 | for(arma::sp_mat::const_col_iterator j = start; j != end; ++j){ 29 | 30 | if((clu[j.row()]==from) && (A(j.row(),v)==-1)){ 31 | N-=1; 32 | } 33 | if((clu[j.row()]==to) && (A(j.row(),v)==-1)){ 34 | N+=1; 35 | } 36 | if((clu[j.row()]==to) && (A(j.row(),v)==1)){ 37 | P-=1; 38 | } 39 | if((clu[j.row()]==from) && (A(j.row(),v)==1)){ 40 | P+=1; 41 | } 42 | } 43 | A=A.t(); 44 | start = A.begin_col(v); 45 | end = A.end_col(v); 46 | for(arma::sp_mat::const_col_iterator j = start; j != end; ++j){ 47 | 48 | if((clu[j.row()]==from) && (A(j.row(),v)==-1)){ 49 | N-=1; 50 | } 51 | if((clu[j.row()]==to) && (A(j.row(),v)==-1)){ 52 | N+=1; 53 | } 54 | if((clu[j.row()]==to) && (A(j.row(),v)==1)){ 55 | P-=1; 56 | } 57 | if((clu[j.row()]==from) && (A(j.row(),v)==1)){ 58 | P+=1; 59 | } 60 | } 61 | return alpha*N+(1-alpha)*P; 62 | 63 | } 64 | 65 | // [[Rcpp::export]] 66 | List optimBlocks1(arma::sp_mat A,IntegerVector clu, int k, double alpha){ 67 | 68 | double crit = blockCriterion(A,clu,alpha); 69 | int n = A.n_cols; 70 | int maxiter = n*n; 71 | int iter=0; 72 | 73 | IntegerVector cluSizes(k); 74 | for(int i=0; i 5 | %\VignetteIndexEntry{06 Complex Matrices} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE, 15 | warning = FALSE 16 | ) 17 | ``` 18 | 19 | This vignette describes the use of complex matrices for signed networks with ambivalent ties. 20 | 21 | ```{r setup} 22 | library(igraph) 23 | library(signnet) 24 | ``` 25 | 26 | ## Representing networks with ambivalent ties 27 | 28 | The vignette on [signed two-mode network](signed_2mode.html) introduces a third type of tie for signed networks, the ambivalent tie. 29 | 30 | ```{r projection_example} 31 | # construct network 32 | el <- matrix(c(1, "a", 1, "b", 1, "c", 2, "a", 2, "b"), ncol = 2, byrow = TRUE) 33 | g <- graph_from_edgelist(el, directed = FALSE) 34 | E(g)$sign <- c(1, 1, -1, 1, -1) 35 | V(g)$type <- c(FALSE, TRUE, TRUE, TRUE, FALSE) 36 | 37 | # vertex duplication 38 | gu <- as_unsigned_2mode(g, primary = TRUE) 39 | 40 | # project and binarize 41 | pu <- bipartite_projection(gu, which = "true") 42 | pu <- delete_edge_attr(pu, "weight") 43 | 44 | # vertex contraction 45 | ps <- as_signed_proj(pu) 46 | igraph::as_data_frame(ps, "edges") 47 | ``` 48 | 49 | Ambivalent ties add a new level of complexity for analytic tasks (especially involving matrices) since it is not clear which 50 | value to assign to them. Intuitively they should be "somewhere" between a positive and a negative tie but zero is already taken for the null tie. 51 | 52 | We can construct a kind of adjacency matrix with the character values, but we can't really work with characters analytically. 53 | 54 | This is where complex matrices come in. Instead of thinking about edge values being only in one dimension, we can add a second one for negative 55 | ties. That is, a positive tie would be coded as $(1,0)$ and a negative one as $(0,1)$. It is much easier in this case to include ambivalent ties by assigning $(0.5,0.5)$ to them. 56 | 57 | Tuples like these can also be written as a complex number, i.e. $(1,0)$ turns into $1+0i$, $(0,1)$ into $0+1i$, and $(0.5,0.5)$ into $0.5+0.5i$. 58 | Complex numbers may be scary to some, but they have a kind of intuitive interpretation here. The real part is the positive value of an edge and the imaginary part is the negative part. So we could actually also have something like $0.3+0.7i$ which is an edge that is 30% positive and 70% negative. For now, though, the three values from above suffice. 59 | 60 | The function `as_adj_complex()` can be used to return the complex adjacency matrix of a signed network with ambivalent ties. 61 | ```{r complex_adj,eval=FALSE} 62 | as_adj_complex(ps, attr = "type") 63 | ``` 64 | 65 | ```{r sneaky_show,echo=FALSE} 66 | structure(c( 67 | 0 + 0i, 0.5 - 0.5i, 0 - 1i, 0.5 + 0.5i, 0 + 0i, 0 - 1i, 0 + 1i, 68 | 0 + 1i, 0 + 0i 69 | ), .Dim = c(3L, 3L), .Dimnames = list(c("a", "b", "c"), c("a", "b", "c"))) 70 | ``` 71 | 72 | 73 | When there is a complex adjacency matrix, then there is also a complex Laplacian matrix. This matrix can be obtained with `laplacian_matrix_complex()`. 74 | ```{r complex_lapl, eval=FALSE} 75 | laplacian_matrix_complex(ps, attr = "type") 76 | ``` 77 | 78 | ```{r sneaky_show1,echo=FALSE} 79 | structure(c( 80 | 1.70710678118655 + 0i, -0.5 + 0.5i, 0 + 1i, -0.5 - 0.5i, 81 | 1.70710678118655 + 0i, 0 + 1i, 0 - 1i, 0 - 1i, 2 + 0i 82 | ), .Dim = c(3L, 3L), .Dimnames = list(c("a", "b", "c"), c("a", "b", "c"))) 83 | ``` 84 | 85 | ## Functions supporting ambivalent ties 86 | 87 | So far, only the triangle routines support networks with ambivalent ties. 88 | 89 | ```{r ambi_net} 90 | g <- make_full_graph(5) 91 | E(g)$type <- c(rep("P", 3), rep("N", 3), rep("A", 4)) 92 | 93 | count_complex_triangles(g, attr = "type") 94 | ``` 95 | 96 | 97 | -------------------------------------------------------------------------------- /vignettes/centrality.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Centrality" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{04 Centrality} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message=FALSE, 15 | warning=FALSE 16 | ) 17 | ``` 18 | 19 | This vignette describes the use of centrality in signed networks. 20 | 21 | ```{r setup} 22 | library(igraph) 23 | library(signnet) 24 | ``` 25 | 26 | ## Centrality indices for signed networks 27 | 28 | There exist dozens of indices for networks with positive ties, but for signed networks they are rather scarce. 29 | The package implements three indices so far. Versions of degree and eigenvector centrality, and PN centrality by Everett & Borgatti. 30 | 31 | Degree centrality can be calculated in four different ways with `degree_signed()`, specified by the `type` parameter: 32 | 33 | * `type="pos"` count only positive neighbors 34 | * `type="neg"` count only negative neighbors 35 | * `type="ratio"` positive neighbors/(positive neighbors+negative neighbors) 36 | * `type="net"` positive neighbors-negative neighbors 37 | 38 | The `mode` parameter can be used to get "in" and "out" versions for directed networks. 39 | 40 | The PN index is very similar to Katz status and Hubbell's measure for networks with only positive ties. 41 | The technical details can be found in the paper by Everett & Borgatti. 42 | 43 | The below example illustrates all indices with a network where signed degree can not distinguish vertices. 44 | ```{r deg_same} 45 | A <- matrix(c(0, 1, 0, 1, 0, 0, 0, -1, -1, 0, 46 | 1, 0, 1, -1, 1, -1, -1, 0, 0, 0, 47 | 0, 1, 0, 1, -1, 0, 0, 0, -1, 0, 48 | 1, -1, 1, 0, 1, -1, -1, 0, 0, 0, 49 | 0, 1, -1, 1, 0, 1, 0, -1, 0, -1, 50 | 0, -1, 0, -1, 1, 0, 1, 0, 1, -1, 51 | 0, -1, 0, -1, 0, 1, 0, 1, -1, 1, 52 | -1, 0, 0, 0, -1, 0, 1, 0, 1, 0, 53 | -1, 0, -1, 0, 0, 1, -1, 1, 0, 1, 54 | 0, 0, 0, 0, -1, -1, 1, 0, 1, 0),10,10) 55 | 56 | g <- graph_from_adjacency_matrix(A,"undirected",weighted = "sign") 57 | 58 | degree_signed(g,type="ratio") 59 | eigen_centrality_signed(g) 60 | pn_index(g) 61 | 62 | ``` 63 | 64 | Note that PN centrality and eigenvector centrality differ significantly for this network. 65 | ```{r cor} 66 | cor(eigen_centrality_signed(g),pn_index(g),method = "kendall") 67 | ``` 68 | 69 | ## A note on eigenvector centrality 70 | 71 | The adjacency matrix of a signed network may not have a dominant eigenvalue. This means it is not clear which eigenvector should be used. In addition it is possible for the adjacency matrix to have repeated eigenvalues and hence multiple linearly independent eigenvectors. In this case certain centralities can be arbitrarily assigned. The `eigen_centrality_signed()` function returns an error if this is the case. 72 | 73 | ```{r non_dominant,error=TRUE} 74 | A <- matrix(c( 0, 1, 1, -1, 0, 0, -1, 0, 0, 75 | 1, 0, 1, 0, -1, 0, 0, -1, 0, 76 | 1, 1, 0, 0, 0, -1, 0, 0, -1, 77 | -1, 0, 0, 0, 1, 1, -1, 0, 0, 78 | 0, -1, 0, 1, 0, 1, 0, -1, 0, 79 | 0, 0, -1, 1, 1, 0, 0, 0, -1, 80 | -1, 0, 0, -1, 0, 0, 0, 1, 1, 81 | 0, -1, 0, 0, -1, 0, 1, 0, 1, 82 | 0, 0, -1, 0, 0, -1, 1, 1, 0), 9, 9) 83 | 84 | g <- igraph::graph_from_adjacency_matrix(A,"undirected",weighted = "sign") 85 | eigen_centrality_signed(g) 86 | 87 | ``` 88 | 89 | 90 | ## References 91 | 92 | Everett, Martin G., and Stephen P. Borgatti. 2014. "Networks Containing Negative Ties." Social Networks 38: 111–20. 93 | 94 | Bonacich, Phillip, and Paulette Lloyd. 2004. "Calculating Status with Negative Relations." Social Networks 26 (4): 331–38. 95 | -------------------------------------------------------------------------------- /tests/testthat/test-signed_triangles.R: -------------------------------------------------------------------------------- 1 | test_that("only positive works", { 2 | g <- igraph::make_full_graph(3) 3 | igraph::E(g)$sign <- 1 4 | res_code <- count_signed_triangles(g) 5 | res_true <- c("+++" = 1, "++-" = 0, "+--" = 0, "---" = 0) 6 | expect_equal(res_code, res_true) 7 | }) 8 | 9 | test_that("no triangles works", { 10 | g <- igraph::make_star(5, "undirected") 11 | igraph::E(g)$sign <- 1 12 | expect_warning(count_signed_triangles(g)) 13 | }) 14 | 15 | test_that("directed check works", { 16 | g <- igraph::make_full_graph(5, directed = TRUE) 17 | igraph::E(g)$sign <- 1 18 | expect_error(count_signed_triangles(g)) 19 | }) 20 | 21 | test_that("sign check works", { 22 | g <- igraph::make_full_graph(5, directed = FALSE) 23 | expect_error(count_signed_triangles(g)) 24 | }) 25 | 26 | test_that("wrong sign values check works", { 27 | g <- igraph::make_full_graph(5, directed = FALSE) 28 | igraph::E(g)$sign <- 2 29 | expect_error(count_signed_triangles(g)) 30 | }) 31 | 32 | 33 | test_that("no triangles works", { 34 | g <- igraph::make_star(5, "undirected") 35 | igraph::E(g)$sign <- 1 36 | expect_warning(signed_triangles(g)) 37 | }) 38 | 39 | test_that("directed check works", { 40 | g <- igraph::make_full_graph(5, directed = TRUE) 41 | igraph::E(g)$sign <- 1 42 | expect_error(signed_triangles(g)) 43 | }) 44 | 45 | test_that("sign check works", { 46 | g <- igraph::make_full_graph(5, directed = FALSE) 47 | expect_error(signed_triangles(g)) 48 | }) 49 | 50 | test_that("wrong sign values check works", { 51 | g <- igraph::make_full_graph(5, directed = FALSE) 52 | igraph::E(g)$sign <- 2 53 | expect_error(signed_triangles(g)) 54 | }) 55 | 56 | test_that("signed triangle listing works", { 57 | g <- igraph::make_full_graph(4) 58 | igraph::E(g)$sign <- c(1, 1, 1, -1, -1, -1) 59 | res_code <- signed_triangles(g) 60 | res_true <- structure( 61 | c( 62 | 1L, 63 | 1L, 64 | 1L, 65 | 2L, 66 | 2L, 67 | 2L, 68 | 3L, 69 | 3L, 70 | 4L, 71 | 3L, 72 | 4L, 73 | 4L, 74 | 2L, 75 | 2L, 76 | 2L, 77 | 0L 78 | ), 79 | dim = c(4L, 4L), 80 | dimnames = list( 81 | NULL, 82 | c( 83 | "V1", 84 | "V2", 85 | "V3", 86 | "P" 87 | ) 88 | ) 89 | ) 90 | expect_equal(res_code, res_true) 91 | }) 92 | 93 | test_that("complex triangle count works", { 94 | g <- igraph::make_full_graph(4) 95 | igraph::E(g)$type <- c("P", "N", "A", "A", "P", "N") 96 | res <- count_complex_triangles(g, attr = "type") 97 | res_true <- c( 98 | PPP = 0, 99 | PPN = 0, 100 | PNN = 0, 101 | NNN = 0, 102 | PPA = 1, 103 | PNA = 2, 104 | NNA = 1, 105 | PAA = 0, 106 | NAA = 0, 107 | AAA = 0 108 | ) 109 | expect_equal(res, res_true) 110 | }) 111 | 112 | test_that("no complex triangles works", { 113 | g <- igraph::make_star(5, "undirected") 114 | igraph::E(g)$type <- "P" 115 | expect_warning(count_complex_triangles(g, attr = "type")) 116 | }) 117 | 118 | test_that("complex directed check works", { 119 | g <- igraph::make_full_graph(5, directed = TRUE) 120 | igraph::E(g)$type <- "P" 121 | expect_error(count_complex_triangles(g, attr = "type")) 122 | }) 123 | 124 | test_that("complex check works", { 125 | g <- igraph::make_full_graph(5, directed = FALSE) 126 | expect_error(count_complex_triangles(g)) 127 | }) 128 | 129 | test_that("wrong complex values check works", { 130 | g <- igraph::make_full_graph(5, directed = FALSE) 131 | igraph::E(g)$type <- "L" 132 | expect_error(count_complex_triangles(g, attr = "type")) 133 | }) 134 | 135 | test_that("signed triad census check works", { 136 | g <- igraph::make_full_graph(5, directed = FALSE) 137 | igraph::E(g)$sign <- -1 138 | expect_error(triad_census_signed(g)) 139 | }) 140 | 141 | test_that("signed triad census works", { 142 | g <- igraph::make_full_graph(5, directed = TRUE) 143 | igraph::E(g)$sign <- -1 144 | census <- triad_census_signed(g) 145 | expect_equal(census[["300-NNNNNN"]], 10) 146 | }) 147 | -------------------------------------------------------------------------------- /vignettes/blockmodeling.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Blockmodeling" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{03 Blockmodeling} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message=FALSE, 15 | warning=FALSE 16 | ) 17 | ``` 18 | 19 | This vignette describes the two implemented methods for blockmodeling in signed networks. 20 | 21 | ```{r setup} 22 | library(igraph) 23 | library(signnet) 24 | ``` 25 | 26 | ## Traditional Blockmodeling 27 | 28 | In signed blockmodeling, the goal is to determine `k` blocks of nodes such that all intra-block edges 29 | are positive and inter-block edges are negative. In the example below, we construct a network with a perfect 30 | block structure with `sample_islands_signed()`. The network consists of 10 blocks with 10 vertices each, where each block 31 | has a density of 1 (of positive edges). The function `signed_blockmodel()` is used to construct the blockmodel. 32 | The parameter `k` is the number of desired blocks. `alpha` is a trade-off parameter. The function minimizes $P(C)=\alpha N+(1-\alpha)P$, where $N$ is the total number of negative ties within blocks and $P$ be the total number of positive ties between blocks. 33 | 34 | ```{r blockmod_ex} 35 | g <- sample_islands_signed(10,10,1,20) 36 | clu <- signed_blockmodel(g,k = 10,alpha = 0.5) 37 | table(clu$membership) 38 | clu$criterion 39 | ``` 40 | 41 | The function returns a list with two entries. The block membership of nodes and the value of $P(C)$. 42 | 43 | The function `ggblock()` can be used to plot the outcome of the blockmodel (`ggplot2` is required). 44 | ```{r blockmodel_ex_plot,eval=FALSE} 45 | ggblock(g,clu$membership,show_blocks = TRUE) 46 | ``` 47 | 48 | 49 | ```{r example, echo=FALSE,out.width = "80%",fig.align='center'} 50 | knitr::include_graphics("blockmodel_example.png") 51 | ``` 52 | 53 | If the parameter `annealing` is set to TRUE, simulated annealing is used in the optimization step. 54 | This generally leads to better results but longer runtimes. 55 | ```{r blockmodel_tribes} 56 | data("tribes") 57 | set.seed(44) #for reproducibility 58 | 59 | signed_blockmodel(tribes,k = 3,alpha=0.5,annealing = TRUE) 60 | signed_blockmodel(tribes,k = 3,alpha=0.5,annealing = FALSE) 61 | ``` 62 | 63 | 64 | ## Generalized Blockmodeling 65 | 66 | The function `signed_blockmodel()` is only able to provide a blockmodel where the diagonal blocks are positive and 67 | off-diagonal blocks are negative. The function `signed_blockmodel_general()` can be used to specify different block structures. 68 | In the below example, we construct a network that contains three blocks. Two have positive and one has negative intra-group ties. 69 | The inter-group edges are negative between group one and two, and one and three. Between group two and three, all edges are positive. 70 | ```{r general_example} 71 | g1 <- g2 <- g3 <- make_full_graph(5) 72 | 73 | V(g1)$name <- as.character(1:5) 74 | V(g2)$name <- as.character(6:10) 75 | V(g3)$name <- as.character(11:15) 76 | 77 | g <- Reduce("%u%",list(g1,g2,g3)) 78 | E(g)$sign <- 1 79 | E(g)$sign[1:10] <- -1 80 | g <- add.edges(g,c(rbind(1:5,6:10)),attr = list(sign=-1)) 81 | g <- add.edges(g,c(rbind(1:5,11:15)),attr = list(sign=-1)) 82 | g <- add.edges(g,c(rbind(11:15,6:10)),attr = list(sign=1)) 83 | ``` 84 | 85 | The parameter `blockmat` is used to specify the desired block structure. 86 | ```{r general_blocks} 87 | set.seed(424) #for reproducibility 88 | blockmat <- matrix(c(1,-1,-1,-1,1,1,-1,1,-1),3,3,byrow = TRUE) 89 | blockmat 90 | 91 | general <- signed_blockmodel_general(g,blockmat,alpha = 0.5) 92 | traditional <- signed_blockmodel(g,k = 3,alpha = 0.5,annealing = TRUE) 93 | 94 | c(general$criterion,traditional$criterion) 95 | ``` 96 | 97 | ```{r general, echo=FALSE,out.width = "90%",fig.align='center'} 98 | knitr::include_graphics("blockmodel_general.png") 99 | ``` 100 | 101 | ## References 102 | 103 | Doreian, Patrick, and Andrej Mrvar. 1996. "A Partitioning Approach to Structural Balance." Social Networks 18 (2): 149–68. 104 | 105 | Doreian, Patrick, and Andrej Mrvar. 2009. "Partitioning Signed Social Networks." Social Networks 31 (1): 1–11. 106 | 107 | Doreian, Patrick, and Andrej Mrvar. 2015. "Structural Balance and Signed International Relations." Journal of Social Structure 16: 1. 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /tests/testthat/test-centrality_indices.R: -------------------------------------------------------------------------------- 1 | test_that("degree error no graph works", { 2 | expect_error(degree_signed(g = 5)) 3 | }) 4 | 5 | test_that("degree error no sign works", { 6 | g <- igraph::make_full_graph(5, FALSE) 7 | expect_error(degree_signed(g)) 8 | }) 9 | 10 | test_that("degree error directed works", { 11 | g <- igraph::make_full_graph(5, TRUE) 12 | igraph::E(g)$sign <- 1 13 | expect_error(degree_signed(g, mode = "all")) 14 | }) 15 | 16 | test_that("pos degree works", { 17 | g <- igraph::make_full_graph(4) 18 | igraph::E(g)$sign <- c(-1, 1, 1, -1, -1, 1) 19 | expect_equal(degree_signed(g, mode = "all", type = "pos"), c(2, 0, 2, 2)) 20 | }) 21 | 22 | test_that("neg degree works", { 23 | g <- igraph::make_full_graph(4) 24 | igraph::E(g)$sign <- c(-1, 1, 1, -1, -1, 1) 25 | expect_equal(degree_signed(g, mode = "all", type = "neg"), c(1, 3, 1, 1)) 26 | }) 27 | 28 | test_that("pos degree works", { 29 | g <- igraph::make_full_graph(4) 30 | igraph::E(g)$sign <- c(-1, 1, 1, -1, -1, 1) 31 | expect_equal(degree_signed(g, mode = "all", type = "pos"), c(2, 0, 2, 2)) 32 | }) 33 | 34 | test_that("net in degree works", { 35 | g <- igraph::make_full_graph(4, directed = TRUE) 36 | igraph::E(g)$sign <- rep(c(-1, 1, 1, -1, -1, 1), 2) 37 | expect_equal(degree_signed(g, mode = "in", type = "net"), c(-3, -1, 1, 3)) 38 | }) 39 | 40 | test_that("net out degree works", { 41 | g <- igraph::make_full_graph(4, directed = TRUE) 42 | igraph::E(g)$sign <- rep(c(-1, 1, 1, -1, -1, 1), 2) 43 | expect_equal(degree_signed(g, mode = "out", type = "net"), c(1, -1, 1, -1)) 44 | }) 45 | 46 | test_that("pn index error no graph works", { 47 | expect_error(pn_index(g = 5)) 48 | }) 49 | 50 | test_that("pn index error no sign works", { 51 | g <- igraph::make_full_graph(5, FALSE) 52 | expect_error(pn_index(g)) 53 | }) 54 | 55 | test_that("pn index error directed works", { 56 | g <- igraph::make_full_graph(5, TRUE) 57 | igraph::E(g)$sign <- 1 58 | expect_error(pn_index(g, mode = "all")) 59 | }) 60 | 61 | test_that("pn index works", { 62 | g <- igraph::make_full_graph(5, FALSE) 63 | igraph::E(g)$sign <- 1 64 | expect_equal(pn_index(g, mode = "in"), c(2, 2, 2, 2, 2)) 65 | }) 66 | 67 | test_that("evcent not graph error works", { 68 | expect_error(eigen_centrality_signed(g = 5)) 69 | }) 70 | 71 | test_that("evcent no sign error works", { 72 | g <- igraph::make_full_graph(5) 73 | expect_error(eigen_centrality_signed(g)) 74 | }) 75 | 76 | test_that("evcent not dominant works", { 77 | A <- matrix( 78 | c( 79 | 0, 80 | 1, 81 | 1, 82 | -1, 83 | 0, 84 | 0, 85 | -1, 86 | 0, 87 | 0, 88 | 1, 89 | 0, 90 | 1, 91 | 0, 92 | -1, 93 | 0, 94 | 0, 95 | -1, 96 | 0, 97 | 1, 98 | 1, 99 | 0, 100 | 0, 101 | 0, 102 | -1, 103 | 0, 104 | 0, 105 | -1, 106 | -1, 107 | 0, 108 | 0, 109 | 0, 110 | 1, 111 | 1, 112 | -1, 113 | 0, 114 | 0, 115 | 0, 116 | -1, 117 | 0, 118 | 1, 119 | 0, 120 | 1, 121 | 0, 122 | -1, 123 | 0, 124 | 0, 125 | 0, 126 | -1, 127 | 1, 128 | 1, 129 | 0, 130 | 0, 131 | 0, 132 | -1, 133 | -1, 134 | 0, 135 | 0, 136 | -1, 137 | 0, 138 | 0, 139 | 0, 140 | 1, 141 | 1, 142 | 0, 143 | -1, 144 | 0, 145 | 0, 146 | -1, 147 | 0, 148 | 1, 149 | 0, 150 | 1, 151 | 0, 152 | 0, 153 | -1, 154 | 0, 155 | 0, 156 | -1, 157 | 1, 158 | 1, 159 | 0 160 | ), 161 | 9, 162 | 9 163 | ) 164 | g <- graph_from_adjacency_matrix_signed(A, "undirected") 165 | expect_error(eigen_centrality_signed(g)) 166 | }) 167 | 168 | test_that("evcent works", { 169 | g <- igraph::make_full_graph(5) 170 | igraph::E(g)$sign <- 1 171 | igraph::E(g)$sign[1] <- -1 172 | ev <- abs(round(eigen_centrality_signed(g, scale = TRUE), 8)) 173 | ev_true <- c(0.68614066, 0.68614066, 1, 1, 1) 174 | expect_equal(stats::cor(ev, ev_true), 1) 175 | }) 176 | 177 | test_that("evcent no scale works", { 178 | g <- igraph::make_full_graph(5) 179 | igraph::E(g)$sign <- 1 180 | igraph::E(g)$sign[1] <- -1 181 | ev <- abs(round(eigen_centrality_signed(g, scale = FALSE), 8)) 182 | ev_true <- c(0.34560347, 0.34560347, 0.50369186, 0.50369186, 0.50369186) 183 | expect_equal(stats::cor(ev, ev_true), 1) 184 | }) 185 | -------------------------------------------------------------------------------- /vignettes/structural_balance.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Structural Balance" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{02 Structural Balance} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message=FALSE, 15 | warning=FALSE 16 | ) 17 | ``` 18 | 19 | This vignette describes structural balance theory and implemented functions in `signnet` associated with it. 20 | 21 | ```{r setup} 22 | library(igraph) 23 | library(signnet) 24 | ``` 25 | 26 | 27 | ## Structural Balance 28 | 29 | The principles underlying structural balance are based on a theory in social psychology dating back to the work of Heider in the 1940s, which was generalized and extended to graphs by Cartwright and Harary in the 1950s. In its simplest form, it is defined via triangles. 30 | A triangle is balanced if all ties are positive (“the friend of a friend is a friend”) or only one tie is positive (“the enemy of my enemy is my friend”). The remaining configurations are said to be unbalanced. 31 | 32 | ```{r triangles, echo=FALSE,out.width = "100%"} 33 | knitr::include_graphics("balance_triples.png") 34 | ``` 35 | 36 | A network is balanced if i.a., it can be partitioned into two vertex subsets, such that intra-group edges are all positive and inter-group edges are all negative. 37 | 38 | A (random) balanced network can be obtained with the function `sample_islands_signed()` which is pretty much the 39 | same as `sample_islands()` from the `igraph` package. 40 | 41 | ```{r example_balanced} 42 | g <- sample_islands_signed(islands.n = 2,islands.size = 10, 43 | islands.pin = 0.8,n.inter = 5) 44 | ``` 45 | (The function `ggsigned()` can be used to visualize signed networks. Note that this requires the package `ggraph` to be installed.) 46 | Increasing `islands.n` leads to "clusterable" networks as defined by Davis. 47 | 48 | A balanced network only contains balanced triangles. This can be verified with `count_signed_triangles()`. 49 | ```{r triangle_count} 50 | count_signed_triangles(g) 51 | ``` 52 | Note the absence of `++-` and `---` triangles. 53 | 54 | To list all triangles use `signed_triangles()`. 55 | ```{r triangle_list} 56 | head(signed_triangles(g)) 57 | ``` 58 | The column P indicated the number of positive ties in the triangle. A value of 3 indicates that the triangle is "+++". 59 | 60 | 61 | 62 | ## Balancedness 63 | Determining if a network is balanced or not is easy, but measuring a degree of balancedness (i.e. how close is a network to be balanced?) is not. The package, so far, implements three methods to calculate balance scores. All are defined such that a value of one indicates perfect balance and zero perfect unbalance. Though for intermediate networks, results may vary significantly. Check the paper by Samin Aref (and his other work) for more details. 64 | 65 | ```{r perfect_balance} 66 | balance_score(g, method = "triangles") 67 | balance_score(g, method = "walk") 68 | balance_score(g, method = "frustration") 69 | ``` 70 | 71 | "triangles" returns the fraction of balanced triangles. 72 | 73 | "walk" is based on eigenvalues of the signed and underlying unsigned network. Check the paper by Estrada for details. 74 | 75 | "frustration" assumes that the network can be partitioned into two groups, where intra group edges are positive and inter group edges are negative. The index is defined as the sum of intra group negative and inter group positive edges. Note that the problem is NP complete and only an upper bound is returned (based on simulated annealing). The function `frustration_exact()` implements an integer program to solve the exact optimization problem. More details can be found in the work of Aref. 76 | 77 | There disagreement for non-balanced networks can be seen with the included "tribes" dataset. 78 | ```{r nonperfect} 79 | data("tribes") 80 | balance_score(tribes, method = "triangles") 81 | balance_score(tribes, method = "walk") 82 | balance_score(tribes, method = "frustration") 83 | ``` 84 | 85 | 86 | ## References 87 | 88 | Heider, Fritz. 1946. "Attitudes and Cognitive Organization." The Journal of Psychology 21 (1): 107–12. 89 | 90 | Cartwright, Dorwin, and Frank Harary. 1956. "Structural Balance: A Generalization of Heider’s Theory." Psychological Review 63 (5): 277. 91 | 92 | Davis, James A. 1967. "Clustering and Structural Balance in Graphs." Human Relations 20 (2): 181–87. 93 | 94 | Aref, Samin, and Mark C. Wilson. 2018. "Measuring Partial Balance in Signed Networks." Journal of Complex Networks 6 (4): 566–95. 95 | 96 | Estrada, Ernesto. 2019. "Rethinking Structural Balance in Signed Social Networks." Discrete Applied Mathematics 97 | -------------------------------------------------------------------------------- /src/optimBlocksGen.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // [[Rcpp::depends(RcppArmadillo)]] 4 | using namespace Rcpp; 5 | 6 | // [[Rcpp::export]] 7 | double blockCriterionS(arma::sp_mat A,IntegerVector clu,double alpha,IntegerMatrix sgrp){ 8 | double P = 0; 9 | double N = 0; 10 | 11 | for (arma::sp_mat::const_iterator i = A.begin(); i != A.end(); ++i) { 12 | if((clu[i.row()]==clu[i.col()]) && (*i!=sgrp(clu[i.row()],clu[i.col()]))){ 13 | if(sgrp(clu[i.row()],clu[i.col()])==1){ 14 | P+=1; 15 | } else{ 16 | N+=1; 17 | } 18 | 19 | } 20 | if((clu[i.row()]!=clu[i.col()]) && (*i!=sgrp(clu[i.row()],clu[i.col()]))){ 21 | if(sgrp(clu[i.row()],clu[i.col()])==1){ 22 | P+=1; 23 | } else{ 24 | N+=1; 25 | } 26 | } 27 | } 28 | return alpha*N+(1-alpha)*P; 29 | } 30 | 31 | // [[Rcpp::export]] 32 | double critUpdateS(arma::sp_mat A, int v, int from, int to,IntegerVector clu,double alpha,IntegerMatrix sgrp){ 33 | double P = 0; 34 | double N = 0; 35 | arma::sp_mat::const_col_iterator start = A.begin_col(v); 36 | arma::sp_mat::const_col_iterator end = A.end_col(v); 37 | for(arma::sp_mat::const_col_iterator j = start; j != end; ++j){ 38 | 39 | if((A(j.row(),v))==sgrp(from,clu[j.row()])){ 40 | if(sgrp(from,clu[j.row()])==1){ 41 | P+=1; 42 | } else{ 43 | N+=1; 44 | } 45 | } 46 | if((A(j.row(),v))!=sgrp(from,clu[j.row()])){ 47 | if(sgrp(from,clu[j.row()])==1){ 48 | P-=1; 49 | } else{ 50 | N-=1; 51 | } 52 | } 53 | if((A(j.row(),v))==sgrp(to,clu[j.row()])){ 54 | if(sgrp(to,clu[j.row()])==1){ 55 | P-=1; 56 | } else{ 57 | N-=1; 58 | } 59 | } 60 | if((A(j.row(),v))!=sgrp(to,clu[j.row()])){ 61 | if(sgrp(to,clu[j.row()])==1){ 62 | P+=1; 63 | } else{ 64 | N+=1; 65 | } 66 | } 67 | } 68 | 69 | // A=A.t(); 70 | // start = A.begin_col(v); 71 | // end = A.end_col(v); 72 | // for(arma::sp_mat::const_col_iterator j = start; j != end; ++j){ 73 | // 74 | // if((A(j.row(),v))==sgrp(from,clu[j.row()])){ 75 | // if(sgrp(from,clu[j.row()])==1){ 76 | // P+=1; 77 | // } else{ 78 | // N+=1; 79 | // } 80 | // } 81 | // if((A(j.row(),v))!=sgrp(from,clu[j.row()])){ 82 | // if(sgrp(from,clu[j.row()])==1){ 83 | // P-=1; 84 | // } else{ 85 | // N-=1; 86 | // } 87 | // } 88 | // if((A(j.row(),v))==sgrp(to,clu[j.row()])){ 89 | // if(sgrp(to,clu[j.row()])==1){ 90 | // P-=1; 91 | // } else{ 92 | // N-=1; 93 | // } 94 | // } 95 | // if((A(j.row(),v))!=sgrp(to,clu[j.row()])){ 96 | // if(sgrp(to,clu[j.row()])==1){ 97 | // P+=1; 98 | // } else{ 99 | // N+=1; 100 | // } 101 | // } 102 | // } 103 | return alpha*N+(1-alpha)*P; 104 | 105 | } 106 | 107 | // [[Rcpp::export]] 108 | List optimBlocksSimS(arma::sp_mat A,IntegerVector clu, IntegerMatrix sgrp, double alpha){ 109 | int k = sgrp.nrow(); 110 | double crit = blockCriterionS(A,clu,alpha,sgrp); 111 | double crit_min =crit; 112 | int n = A.n_cols; 113 | int v; 114 | int newc; 115 | double deltaC; 116 | int max_iter = n*n; 117 | double temp = 100; 118 | IntegerVector clu_min(n); 119 | 120 | IntegerVector cluSizes(k); 121 | for(int i=0; i0.1){ 126 | for(int q=0;q 5 | %\VignetteIndexEntry{05 Signed Two-Mode Networks} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message=FALSE, 15 | warning=FALSE 16 | ) 17 | ``` 18 | 19 | This vignette describes methods implemented to analyze signed two-mode networks. 20 | 21 | ```{r setup} 22 | library(igraph) 23 | library(signnet) 24 | ``` 25 | 26 | ## Projections 27 | 28 | A common analytic tool for two-mode networks is to project the network onto on relevant mode. 29 | This is easily done using the adjacency matrix $A$. $AA^T$ yields the row projection and $A^TA$ the column 30 | projection. The resulting networks will thus be weighted. Several methods exist to turn a weighted projection 31 | into an unweighted network where only the most significant edges are included. A number of these methods are implemented in the [backbone](https://cran.r-project.org/package=backbone) package. 32 | 33 | Projecting signed networks, however, is not as straightforward. Consider the following simple example. 34 | 35 | ```{r simple s2mode} 36 | el <- matrix(c(1,"a",1,"b",1,"c",2,"a",2,"b"),ncol = 2,byrow = TRUE) 37 | g <- graph_from_edgelist(el,directed = FALSE) 38 | E(g)$sign <- c(1,1,-1,1,-1) 39 | V(g)$type <- c(FALSE,TRUE,TRUE,TRUE,FALSE) 40 | ``` 41 | 42 | ```{r graph, echo=FALSE,out.width = "50%",fig.align='center'} 43 | knitr::include_graphics("small_signed2mode.png") 44 | ``` 45 | 46 | If we use the regular projection rules we obtain 47 | ```{r matmul} 48 | A <- as_incidence_signed(g) 49 | R <- A%*%t(A) 50 | C <- t(A)%*%A 51 | R 52 | C 53 | ``` 54 | 55 | The row projection suggests that there is no relation between 1 and 2, when in fact there is a negative path (via b) and 56 | a positive path (via a) between them. The same holds for the column projection and the nodes a and b. 57 | 58 | The paper of Schoch introduces two projection methods that circumvent this "nullification". The package implements the *duplication* approach since it plays well with existing binarization tools. The first stepp is to turn the signed two-mode network into an unsigned one. 59 | This is done by duplicating all vertices of the primary mode (i.e. the one to project on). For example, vertex a turns into two 60 | vertices "a-pos" and "a-neg". The vertices of the secondary mode connect to these new vertices depending on the sign of edge. 61 | For instance, 1 has a positive edge to a and thus 1 connects to a-pos. 62 | 63 | This can be done for the whole network with the function `as_unsigned_2mode()` by specifying the primary mode (either TRUE or FALSE). 64 | ```{r duplicate} 65 | gu <- as_unsigned_2mode(g,primary = TRUE) 66 | gu 67 | ``` 68 | 69 | Now, any binarization toll (e.g. from the `backbone` package) can be applied since the network is an unsigned 70 | two-mode network. For illustration, we include all edges with a weight greater one (the "universal" approach) since it can be 71 | done without the `backbone` package. 72 | 73 | ```{r binarize} 74 | pu <- bipartite_projection(gu,which = "true") 75 | pu <- delete_edge_attr(pu,"weight") 76 | pu 77 | ``` 78 | 79 | After binarization, the network is turned back to an unsigned network using a *contraction rule*. 80 | The contraction rule works as follows. 81 | 82 | If there is an edge (a-pos,b-pos) or (a-neg,b-neg) in the projection 83 | then there is a positive edge (a,b) in the signed projection. 84 | 85 | If there is an edge (a-pos,b-neg) or (a-neg,b-pos) in the projection 86 | then there is a negative edge (a,b) in the signed projection. 87 | 88 | If there is an edge (a-pos,b-pos) **and** (a-neg,b-pos) (or, e.g., (a-neg,b-neg) **and** (a-pos,b-neg)) in the projection 89 | then there is an *ambivalent edge* (a,b) in the signed projection. 90 | 91 | This is done with the function `as_signed_proj()`. 92 | ```{r contract} 93 | ps <- as_signed_proj(pu) 94 | as_data_frame(ps,"edges") 95 | ``` 96 | 97 | The projection of a signed two-mode network thus may contain three types of edges (positive ("P"), negative ("N") or ambivalent ("A")). 98 | The concept of ambivalent ties comes from work by Abelson & Rosenberg and Cartwright & Harary. 99 | 100 | More technical details can be found in the original paper by Schoch. 101 | Consult the vignette about [complex matrices](complex_matrices.html) to learn about analyzing signed networks with ambivalent ties. 102 | 103 | ## References 104 | 105 | Doreian, Patrick, Paulette Lloyd, and Andrej Mrvar. 2013. "Partitioning Large Signed Two-Mode Networks: Problems and Prospects." Social Networks, Special Issue on Advances in Two-mode Social Networks, 35 (2): 178–203. 106 | 107 | Schoch, David. 2020. "Projecting Signed Two-Mode Networks" Mathematical Sociology, forthcoming 108 | 109 | Abelson, Robert P., and Milton J. Rosenberg. 1958. “Symbolic Psycho-Logic: A Model of Attitudinal Cognition.” Behavioral Science 3 (1): 1–13. 110 | 111 | Cartwright, Dorwin, and Frank Harary. 1970. “Ambivalence and Indifference in Generalizations of Structural Balance.” Behavioral Science 15 (6). 112 | 113 | -------------------------------------------------------------------------------- /R/blockmodel.R: -------------------------------------------------------------------------------- 1 | #' @title Blockmodeling for signed networks 2 | #' @description Finds blocks of nodes with intra-positive and inter-negative edges 3 | #' @param g igraph object with a sign edge attribute. 4 | #' @param k number of blocks 5 | #' @param alpha see details 6 | #' @param annealing logical. if TRUE, use simulated annealing (Default: FALSE) 7 | #' @return numeric vector of block assignments and the associated criterion value 8 | #' @details The function minimizes P(C)=\eqn{\alpha}N+(1-\eqn{\alpha})P, 9 | #' where N is the total number of negative ties within plus-sets and P be the total number of 10 | #' positive ties between plus-sets. This function implements the structural balance model. That is, 11 | #' all diagonal blocks are positive and off-diagonal blocks negative. 12 | #' For the generalized version see [signed_blockmodel_general]. 13 | #' @author David Schoch 14 | #' @references 15 | #' Doreian, Patrick and Andrej Mrvar (2009). Partitioning signed social networks. *Social Networks* 31(1) 1-11 16 | #' @examples 17 | #' library(igraph) 18 | #' 19 | #' g <- sample_islands_signed(10, 10, 1, 20) 20 | #' clu <- signed_blockmodel(g, k = 10, alpha = 0.5) 21 | #' table(clu$membership) 22 | #' clu$criterion 23 | #' 24 | #' # Using simulated annealing (less change of getting trapped in local optima) 25 | #' data("tribes") 26 | #' clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = TRUE) 27 | #' table(clu$membership) 28 | #' clu$criterion 29 | #' @export 30 | signed_blockmodel <- function(g, k, alpha = 0.5, annealing = FALSE) { 31 | if (!is_signed(g)) { 32 | stop("network is not a signed graph") 33 | } 34 | if (missing(k)) { 35 | stop('argument "k" is missing, with no default') 36 | } 37 | A <- igraph::as_adjacency_matrix( 38 | g, 39 | type = "both", 40 | attr = "sign", 41 | sparse = TRUE 42 | ) 43 | if (!annealing) { 44 | init_cluster <- sample(0:(k - 1), nrow(A), replace = TRUE) 45 | res <- optimBlocks1(A, init_cluster, k, alpha) 46 | res$membership <- res$membership + 1 47 | } else { 48 | init_cluster <- sample(1:k, nrow(A), replace = TRUE) 49 | tmp <- stats::optim( 50 | par = init_cluster, 51 | fn = blockCriterion1, 52 | A = A, 53 | alpha = alpha, 54 | k = k, 55 | gr = genclu, 56 | method = "SANN", 57 | control = list( 58 | maxit = 50000, 59 | temp = 100, 60 | tmax = 500, 61 | trace = FALSE, 62 | REPORT = 5 63 | ) 64 | ) 65 | tmp <- stats::optim( 66 | par = tmp$par, 67 | fn = blockCriterion1, 68 | A = A, 69 | alpha = alpha, 70 | k = k, 71 | gr = genclu, 72 | method = "SANN", 73 | control = list( 74 | maxit = 5000, 75 | temp = 5, 76 | tmax = 500, 77 | trace = FALSE, 78 | REPORT = 5 79 | ) 80 | ) 81 | 82 | res <- list(membership = tmp$par, criterion = tmp$value) 83 | } 84 | res 85 | } 86 | 87 | #' @title Generalized blockmodeling for signed networks 88 | #' @description Finds blocks of nodes with specified inter/intra group ties 89 | #' @param g igraph object with a sign edge attribute. 90 | #' @param blockmat Integer Matrix. Specifies the inter/intra group patterns of ties 91 | #' @param alpha see details 92 | #' @return numeric vector of block assignments and the associated criterion value 93 | #' @details The function minimizes P(C)=\eqn{\alpha}N+(1-\eqn{\alpha})P, 94 | #' where N is the total number of negative ties within plus-sets and P be the total number of 95 | #' positive ties between plus-sets. This function implements the generalized model. For the structural balance 96 | #' version see [signed_blockmodel]. 97 | #' @author David Schoch 98 | #' @references 99 | #' Doreian, Patrick and Andrej Mrvar (2009). Partitioning signed social networks. *Social Networks* 31(1) 1-11 100 | #' @examples 101 | #' library(igraph) 102 | #' # create a signed network with three groups and different inter/intra group ties 103 | #' g1 <- g2 <- g3 <- make_full_graph(5) 104 | #' 105 | #' V(g1)$name <- as.character(1:5) 106 | #' V(g2)$name <- as.character(6:10) 107 | #' V(g3)$name <- as.character(11:15) 108 | #' 109 | #' g <- Reduce("%u%", list(g1, g2, g3)) 110 | #' E(g)$sign <- 1 111 | #' E(g)$sign[1:10] <- -1 112 | #' g <- add_edges(g, c(rbind(1:5, 6:10)), attr = list(sign = -1)) 113 | #' g <- add_edges(g, c(rbind(1:5, 11:15)), attr = list(sign = -1)) 114 | #' g <- add_edges(g, c(rbind(11:15, 6:10)), attr = list(sign = 1)) 115 | #' 116 | #' # specify the link patterns between groups 117 | #' blockmat <- matrix(c(1, -1, -1, -1, 1, 1, -1, 1, -1), 3, 3, byrow = TRUE) 118 | #' signed_blockmodel_general(g, blockmat, 0.5) 119 | #' @export 120 | #' 121 | 122 | signed_blockmodel_general <- function(g, blockmat, alpha = 0.5) { 123 | if (!is_signed(g)) { 124 | stop("network is not a signed graph") 125 | } 126 | if (missing(blockmat)) { 127 | stop('argument "blockmat" is missing, with no default') 128 | } 129 | if (!all(blockmat %in% c(-1, 1))) { 130 | stop('"blockmat" may only contain -1 and 1') 131 | } 132 | A <- igraph::as_adjacency_matrix( 133 | g, 134 | type = "both", 135 | attr = "sign", 136 | sparse = TRUE 137 | ) 138 | init_cluster <- sample(0:(nrow(blockmat) - 1), nrow(A), replace = TRUE) 139 | res <- optimBlocksSimS(A, init_cluster, blockmat, alpha) 140 | res$membership <- res$membership + 1 141 | res 142 | } 143 | 144 | # helper function to create a new solution during simulated annealing 145 | genclu <- function(blocks, A, alpha, k) { 146 | v <- sample(seq_along(blocks), 1) 147 | clu <- 1:k 148 | clu <- clu[-blocks[v]] 149 | new <- sample(clu, 1) 150 | blocks[v] <- new 151 | blocks 152 | } 153 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at david@schochastics.net. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at . 125 | 126 | [homepage]: https://www.contributor-covenant.org 127 | -------------------------------------------------------------------------------- /R/centrality_indices.R: -------------------------------------------------------------------------------- 1 | #' @title PN Centrality Index 2 | #' @description centrality index for signed networks by Everett and Borgatti 3 | #' 4 | #' @param g igraph object with a sign edge attribute. 5 | #' @param mode character string, “out” for out-pn, “in” for in-pn or “all” for undirected networks. 6 | #' @return centrality scores as numeric vector. 7 | #' @references Everett, M. and Borgatti, S. (2014) Networks containing negative ties. *Social Networks* 38 111-120 8 | #' @author David Schoch 9 | #' @importFrom Matrix t 10 | #' @examples 11 | #' A <- matrix(c( 12 | #' 0, 1, 0, 1, 0, 0, 0, -1, -1, 0, 13 | #' 1, 0, 1, -1, 1, -1, -1, 0, 0, 0, 14 | #' 0, 1, 0, 1, -1, 0, 0, 0, -1, 0, 15 | #' 1, -1, 1, 0, 1, -1, -1, 0, 0, 0, 16 | #' 0, 1, -1, 1, 0, 1, 0, -1, 0, -1, 17 | #' 0, -1, 0, -1, 1, 0, 1, 0, 1, -1, 18 | #' 0, -1, 0, -1, 0, 1, 0, 1, -1, 1, 19 | #' -1, 0, 0, 0, -1, 0, 1, 0, 1, 0, 20 | #' -1, 0, -1, 0, 0, 1, -1, 1, 0, 1, 21 | #' 0, 0, 0, 0, -1, -1, 1, 0, 1, 0 22 | #' ), 10, 10) 23 | 24 | #' g <- graph_from_adjacency_matrix_signed(A,"undirected") 25 | #' pn_index(g) 26 | #' @export 27 | 28 | pn_index <- function(g, mode = c("all", "in", "out")) { 29 | if (!is_signed(g)) { 30 | stop("network is not a signed graph") 31 | } 32 | 33 | mode <- match.arg(mode, c("all", "in", "out")) 34 | if (!igraph::is_directed(g)) { 35 | mode <- "all" 36 | } 37 | if (igraph::is_directed(g) && mode == "all") { 38 | stop('"all" only works with undirected networks.') 39 | } 40 | 41 | A <- as_adj_signed(g, sparse = TRUE) 42 | n <- nrow(A) 43 | 44 | P <- (A > 0) + 0 45 | N <- (A < 0) + 0 46 | I <- diag(1, n) 47 | A <- P - 2 * N 48 | 49 | res <- switch( 50 | mode, 51 | all = solve(I - 1 / (2 * n - 2) * A), 52 | `in` = solve(I - 1 / (4 * (n - 1)^2) * Matrix::t(A) %*% A) %*% 53 | (I + 1 / (2 * n - 2) * Matrix::t(A)), 54 | out = solve(I - 1 / (4 * (n - 1)^2) * A %*% Matrix::t(A)) %*% 55 | (I + 1 / (2 * n - 2) * A) 56 | ) 57 | return(Matrix::rowSums(res)) 58 | } 59 | 60 | #' @title Signed Degree 61 | #' @description several options to calculate the signed degree of vertices 62 | #' 63 | #' @param g igraph object with a sign edge attribute. 64 | #' @param mode character string, “out” for out-degree, “in” for in-degree or “all” for undirected networks. 65 | #' @param type character string, “pos” or “neg” for counting positive or negative neighbors only, 66 | #' "ratio" for pos/(pos+neg), or "net" for pos-neg. 67 | #' @return centrality scores as numeric vector. 68 | #' @author David Schoch 69 | #' @importFrom Matrix t 70 | #' @export 71 | 72 | degree_signed <- function( 73 | g, 74 | mode = c("all", "in", "out"), 75 | type = c("pos", "neg", "ratio", "net") 76 | ) { 77 | if (!is_signed(g)) { 78 | stop("network is not a signed graph") 79 | } 80 | mode <- match.arg(mode, c("all", "in", "out")) 81 | if (!igraph::is_directed(g)) { 82 | mode <- "all" 83 | } 84 | if (igraph::is_directed(g) && mode == "all") { 85 | stop('"all" only works with undirected networks.') 86 | } 87 | type <- match.arg(type, c("pos", "neg", "ratio", "net")) 88 | 89 | A <- as_adj_signed(g) 90 | P <- (A > 0) + 0 91 | N <- (A < 0) + 0 92 | 93 | if (mode == "all") { 94 | res <- switch( 95 | type, 96 | pos = Matrix::rowSums(P), 97 | neg = Matrix::rowSums(N), 98 | ratio = Matrix::rowSums(P) / (Matrix::rowSums(P) + Matrix::rowSums(N)), 99 | net = Matrix::rowSums(P) - Matrix::rowSums(N) 100 | ) 101 | res 102 | } else if (mode == "out") { 103 | res <- switch( 104 | type, 105 | pos = Matrix::rowSums(P), 106 | neg = Matrix::rowSums(N), 107 | ratio = Matrix::rowSums(P) / (Matrix::rowSums(P) + Matrix::rowSums(N)), 108 | net = Matrix::rowSums(P) - Matrix::rowSums(N) 109 | ) 110 | res 111 | } else if (mode == "in") { 112 | res <- switch( 113 | type, 114 | pos = Matrix::colSums(P), 115 | neg = Matrix::colSums(N), 116 | ratio = Matrix::colSums(P) / (Matrix::colSums(P) + Matrix::colSums(N)), 117 | net = Matrix::colSums(P) - Matrix::colSums(N) 118 | ) 119 | return(res) 120 | } 121 | } 122 | 123 | #' @title Signed Eigenvector centrality 124 | #' @description returns the eigenvector associated with the dominant eigenvalue from the adjacency matrix. 125 | #' @details Note that, with negative values, the adjacency matrix may not have a dominant eigenvalue. 126 | #' This means it is not clear which eigenvector should be used. In addition it is possible for the adjacency matrix to have repeated eigenvalues and hence multiple linearly independent eigenvectors. In this case certain centralities can be arbitrarily assigned. The function returns an error if this is the case. 127 | #' @param g igraph object with a sign edge attribute. 128 | #' @param scale Logical scalar, whether to scale the result to have a maximum score of one. If no scaling is used then the result vector is the same as returned by `eigen()`. 129 | #' @return centrality scores as numeric vector. 130 | #' @references 131 | #' Bonacich, P. and Lloyd, P. (2004). "Calculating Status with Negative Relations." *Social Networks* 26 (4): 331–38. 132 | #' 133 | #' Everett, M. and Borgatti, S.P. (2014). "Networks Containing Negative Ties." *Social Networks* 38: 111–20. 134 | #' 135 | #' @author David Schoch 136 | #' @examples 137 | #' library(igraph) 138 | #' data("tribes") 139 | #' eigen_centrality_signed(tribes) 140 | #' @export 141 | 142 | eigen_centrality_signed <- function(g, scale = TRUE) { 143 | if (!is_signed(g)) { 144 | stop("network is not a signed graph") 145 | } 146 | 147 | sA <- eigen(as_adj_signed(g)) 148 | evals <- round(sA$values, 8) 149 | max_evals <- which(abs(evals) == max(abs(evals))) 150 | 151 | if (length(max_evals) != 1) { 152 | stop("no dominant eigenvalue exists") 153 | } else { 154 | evcent <- sA$vectors[, max_evals] 155 | } 156 | 157 | if (scale) { 158 | evcent <- evcent / max(abs(evcent)) 159 | } 160 | 161 | return(evcent) 162 | } 163 | -------------------------------------------------------------------------------- /R/plot.R: -------------------------------------------------------------------------------- 1 | #' @title Plot Blockmodel matrix 2 | #' @param g igraph object with a sign edge attribute. 3 | #' @param blocks vector of block membership as obtained, e.g. from [signed_blockmodel] 4 | #' @param cols colors used for negative and positive ties 5 | #' @param show_blocks logical. Should block borders be displayed? (Default: FALSE) 6 | #' @param show_labels logical. Should node labels be displayed? (Default: FALSE) 7 | #' @return ggplot2 object 8 | #' @author David Schoch 9 | #' @examples 10 | #' \dontrun{ 11 | #' library(igraph) 12 | #' data("tribes") 13 | #' clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = TRUE) 14 | #' ggblock(tribes, clu$membership, show_blocks = TRUE, show_labels = TRUE) 15 | #' } 16 | #' @export 17 | #' 18 | ggblock <- function( 19 | g, 20 | blocks = NULL, 21 | cols = NULL, 22 | show_blocks = FALSE, 23 | show_labels = FALSE 24 | ) { 25 | if (!requireNamespace("ggplot2", quietly = TRUE)) { 26 | stop("The package 'ggplot2' is needed for this function.") 27 | } 28 | if (is.null(cols)) { 29 | cols <- c("firebrick", "steelblue") 30 | } 31 | if (is.null(blocks)) { 32 | permI <- 1:igraph::vcount(g) 33 | blocks <- rep(1, igraph::vcount(g)) 34 | } else { 35 | permI <- order(blocks) 36 | } 37 | if (show_blocks) { 38 | bsizes <- unname(table(blocks)) 39 | bsizes <- cumsum(bsizes) + 0.5 40 | rsizes <- igraph::vcount(g) - bsizes[-length(bsizes)] + 1 41 | csizes <- bsizes[-length(bsizes)] 42 | } 43 | if (!"name" %in% igraph::vertex_attr_names(g)) { 44 | g <- igraph::set_vertex_attr(g, "name", value = 1:igraph::vcount(g)) 45 | } 46 | A <- as.matrix(igraph::as_adjacency_matrix(g, type = "both", attr = "sign")) 47 | df <- data.frame( 48 | from = rep(rownames(A), ncol(A)), 49 | to = rep(colnames(A), each = nrow(A)), 50 | value = c(A) 51 | ) 52 | df[["from"]] <- factor(df[["from"]], levels = rev(rownames(A)[permI])) 53 | df[["to"]] <- factor(df[["to"]], levels = colnames(A)[permI]) 54 | df <- df[df[["value"]] != 0, ] 55 | df[["value"]] <- as.factor(df[["value"]]) 56 | p <- ggplot2::ggplot( 57 | df, 58 | ggplot2::aes(y = !!ggplot2::sym("from"), x = !!ggplot2::sym("to")) 59 | ) + 60 | ggplot2::geom_tile( 61 | ggplot2::aes(fill = !!ggplot2::sym("value")), 62 | col = "white" 63 | ) + 64 | ggplot2::scale_fill_manual(values = cols) + 65 | ggplot2::theme_void() + 66 | ggplot2::theme(legend.position = "none") + 67 | ggplot2::coord_fixed() 68 | if (show_blocks) { 69 | p <- p + 70 | ggplot2::geom_vline(xintercept = csizes) + 71 | ggplot2::geom_hline(yintercept = rsizes) 72 | } 73 | if (show_labels) { 74 | p <- p + 75 | ggplot2::scale_x_discrete(position = "top") + 76 | ggplot2::theme(axis.text.y = ggplot2::element_text()) 77 | } 78 | p 79 | } 80 | 81 | #' @title Plot a signed or complex network 82 | #' @param g igraph object. Must have a "sign" edge attribute or an attribute containing "P", "N", "A" 83 | #' @param type character string. either "signed" or "complex" 84 | #' @param attr character string. edge attribute that containing "P", "N", "A" if type="complex" 85 | #' @param edge_cols colors used for negative and positive (and ambivalent) ties 86 | #' @param weights logical. If TRUE, weights are computed based on sign. Defaults to FALSE 87 | #' @details This is a very rudimentary visualization of a signed network. If you are fluent in 'ggraph', you can probably cook up something more sophisticated. The function is thus mostly meant to give a quick overview of the network. 88 | #' @return ggplot2 object 89 | #' @author David Schoch 90 | #' @export 91 | ggsigned <- function( 92 | g, 93 | type = "signed", 94 | attr = NULL, 95 | edge_cols = NULL, 96 | weights = FALSE 97 | ) { 98 | if (!requireNamespace("ggraph", quietly = TRUE)) { 99 | stop("The package 'ggraph' is needed for this function.") 100 | } 101 | type <- match.arg(type, c("signed", "complex")) 102 | if (is.null(edge_cols) && type == "signed") { 103 | edge_cols <- c(`-1` = "firebrick", `1` = "steelblue") 104 | } 105 | if (is.null(edge_cols) && type == "complex") { 106 | edge_cols <- c( 107 | `N` = "firebrick", 108 | `P` = "steelblue", 109 | `A` = "darkorchid3" 110 | ) 111 | } 112 | if (!is.null(edge_cols) && type == "signed") { 113 | if (length(edge_cols) != 2) { 114 | stop(paste0(length(edge_cols), " colors provided but 2 are needed")) 115 | } 116 | } 117 | if (!is.null(edge_cols) && type == "complex") { 118 | if (length(edge_cols) != 3) { 119 | stop(paste0(length(edge_cols), " colors provided but 3 are needed")) 120 | } 121 | } 122 | if (is.null(attr) && type == "complex") { 123 | stop('"attr" must be specified for type="complex"') 124 | } 125 | if (type == "signed") { 126 | if (weights) { 127 | igraph::E(g)$weight <- ifelse(igraph::E(g)$sign == 1, 3, 1) 128 | } else { 129 | igraph::E(g)$weight <- 1 130 | } 131 | ggraph::ggraph(g, "stress", weights = igraph::E(g)$weight) + 132 | ggraph::geom_edge_link0(ggplot2::aes( 133 | col = as.factor(!!ggplot2::sym("sign")) 134 | )) + 135 | ggraph::geom_node_point(shape = 21, fill = "grey25", size = 5) + 136 | ggraph::scale_edge_color_manual(values = edge_cols) + 137 | ggraph::theme_graph() + 138 | ggplot2::theme(legend.position = "none") 139 | } else { 140 | if (weights) { 141 | igraph::E(g)$type <- igraph::get.edge.attribute(g, attr) 142 | igraph::E(g)$weight <- ifelse( 143 | igraph::E(g)$type == "P", 144 | 3, 145 | ifelse(igraph::E(g)$type == "A", 2, 1) 146 | ) 147 | } else { 148 | igraph::E(g)$weight <- 1 149 | } 150 | ggraph::ggraph(g, "stress", weights = igraph::E(g)$weight) + 151 | ggraph::geom_edge_link0(ggplot2::aes( 152 | col = as.factor(!!ggplot2::sym("type")) 153 | )) + 154 | ggraph::geom_node_point(shape = 21, fill = "grey25", size = 5) + 155 | ggraph::scale_edge_color_manual(values = edge_cols) + 156 | ggraph::theme_graph() + 157 | ggplot2::theme(legend.position = "none") 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /R/balance_scores.R: -------------------------------------------------------------------------------- 1 | #' @title balancedness of signed network 2 | #' @description Implements several indices to assess the balancedness of a network. 3 | #' 4 | #' @param g igraph object with a sign edge attribute. 5 | #' @param method string indicating the method to be used. See details for options 6 | #' @details The method parameter can be one of 7 | #' \describe{ 8 | #' \item{*triangles*}{Fraction of balanced triangles. Maximal (=1) if all triangles are balanced.} 9 | #' \item{*walk*}{\eqn{\sum exp(\lambda_i) / \sum exp(\mu_i)}} where \eqn{\lambda_i} are the eigenvalues of the 10 | #' signed adjacency matrix and \eqn{\mu_i} of the unsigned adjacency matrix. Maximal (=1) if all walks are balanced. 11 | #' \item{*frustration*}{The frustration index assumes that the network can be partitioned into two groups, where intra group edges are positive and inter group edges are negative. The index is defined as the sum of intra group negative and inter group positive edges. Note that the problem is NP complete and only an upper bound is returned (based on simulated annealing). Exact methods can be found in the work of Aref. The index is normalized such that it is maximal (=1) if the network is balanced.} 12 | #' } 13 | #' @return numeric balancedness score between 0 and 1 14 | #' @author David Schoch 15 | #' @references 16 | #' Estrada, E. (2019). Rethinking structural balance in signed social networks. *Discrete Applied Mathematics*. 17 | #' 18 | #' Samin Aref, Mark C Wilson (2018). Measuring partial balance in signed networks. *Journal of Complex Networks*, 6(4): 566–595, https://doi.org/10.1093/comnet/cnx044 19 | #' @examples 20 | #' library(igraph) 21 | #' g <- make_full_graph(4) 22 | #' E(g)$sign <- c(-1, 1, 1, -1, -1, 1) 23 | #' 24 | #' balance_score(g, method = "triangles") 25 | #' balance_score(g, method = "walk") 26 | #' @export 27 | balance_score <- function(g, method = "triangles") { 28 | method <- match.arg(method, c("triangles", "walk", "frustration")) 29 | if (!is_signed(g)) { 30 | stop("network is not a signed graph") 31 | } 32 | if (igraph::is_directed(g)) { 33 | stop("g must be undirected") 34 | } 35 | 36 | if (method == "triangles") { 37 | tria_count <- count_signed_triangles(g) 38 | return(unname((tria_count["+++"] + tria_count["+--"]) / sum(tria_count))) 39 | } else if (method == "walk") { 40 | A <- as_adj_signed(g, sparse = TRUE) 41 | EigenS <- eigen(A)$values 42 | EigenU <- eigen(abs(A))$values 43 | return(sum(exp(EigenS)) / sum(exp(EigenU))) 44 | } else if (method == "frustration") { 45 | clu <- signed_blockmodel(g, k = 2, alpha = 0.5, annealing = TRUE) 46 | return(1 - clu$criterion / (igraph::ecount(g) / 2)) 47 | } 48 | } 49 | 50 | #' @title Exact frustration index of a signed network 51 | #' @description Computes the exact frustration index of a signed network using linear programming 52 | #' 53 | #' @param g signed network 54 | #' @param ... additional parameters for the ompr solver 55 | #' @details The frustration index indicates the minimum number of edges whose removal results in a balance 56 | #' network. The function needs the following packages to be installed: `ompr`, `ompr.roi`,`ROI`, and `ROI.plugin.glpk`. 57 | #' The function Implements the AND model in Aref et al., 2020 58 | #' @return list containing the frustration index and the bipartition of nodes 59 | #' @author David Schoch 60 | #' @references 61 | #' 62 | #' Aref, Samin, Andrew J. Mason, and Mark C. Wilson. "Computing the line index of balance using linear programming optimisation." 63 | #' Optimization problems in graph theory. Springer, Cham, 2018. 65-84. 64 | #' 65 | #' Aref, Samin, Andrew J. Mason, and Mark C. Wilson. "A modeling and computational study of the frustration index in signed networks." Networks 75.1 (2020): 95-110. 66 | #' @export 67 | 68 | frustration_exact <- function(g, ...) { 69 | if (!is_signed(g)) { 70 | stop("network is not a signed graph") 71 | } 72 | if (igraph::is_directed(g)) { 73 | stop("g must be undirected") 74 | } 75 | 76 | if (!requireNamespace("ompr", quietly = TRUE)) { 77 | stop("the package 'ompr' is needed for this function to work") 78 | } 79 | if (!requireNamespace("ompr.roi", quietly = TRUE)) { 80 | stop("the package 'ompr.roi' is needed for this function to work") 81 | } 82 | if (!requireNamespace("ROI", quietly = TRUE)) { 83 | stop("the package 'ROI' is needed for this function to work") 84 | } 85 | if (!requireNamespace("ROI.plugin.glpk", quietly = TRUE)) { 86 | stop("the package 'ROI.plugin.glpk' is needed for this function to work") 87 | } 88 | 89 | A <- as_adj_signed(g) 90 | n <- igraph::vcount(g) 91 | 92 | x <- matrix(0, n, n) 93 | y <- rep(0, n) 94 | i <- j <- 0 95 | # AND model 96 | result <- ompr::MIPModel() 97 | result <- ompr::add_variable(result, y[i], i = 1:n, type = "binary") 98 | result <- ompr::add_variable( 99 | result, 100 | x[i, j], 101 | i = 1:n, 102 | j = 1:n, 103 | i < j & A[i, j] != 0, 104 | type = "binary" 105 | ) 106 | result <- ompr::set_objective( 107 | result, 108 | ompr::sum_over( 109 | y[i] + y[j] - 2 * x[i, j], 110 | i = 1:n, 111 | j = 1:n, 112 | i < j & A[i, j] == 1 113 | ) + 114 | ompr::sum_over( 115 | 1 - (y[i] + y[j] - 2 * x[i, j]), 116 | i = 1:n, 117 | j = 1:n, 118 | i < j & A[i, j] == -1 119 | ), 120 | "min" 121 | ) 122 | result <- ompr::add_constraint( 123 | result, 124 | x[i, j] <= y[i], 125 | i = 1:n, 126 | j = 1:n, 127 | i < j & A[i, j] == 1 128 | ) 129 | result <- ompr::add_constraint( 130 | result, 131 | x[i, j] <= y[j], 132 | i = 1:n, 133 | j = 1:n, 134 | i < j & A[i, j] == 1 135 | ) 136 | result <- ompr::add_constraint( 137 | result, 138 | x[i, j] >= y[i] + y[j] - 1, 139 | i = 1:n, 140 | j = 1:n, 141 | i < j & A[i, j] == -1 142 | ) 143 | result <- ompr::solve_model(result, ompr.roi::with_ROI(solver = "glpk", ...)) 144 | 145 | partition <- ompr::get_solution(result, y[i]) 146 | partition <- partition$value 147 | frustration <- result$objective_value 148 | 149 | list(frustration = frustration, partition = partition) 150 | } 151 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | # signnet 16 | 17 | 18 | [![R-CMD-check](https://github.com/schochastics/signnet/workflows/R-CMD-check/badge.svg)](https://github.com/schochastics/signnet/actions) 19 | [![CRAN status](https://www.r-pkg.org/badges/version/signnet)](https://cran.r-project.org/package=signnet) 20 | [![Downloads](https://cranlogs.r-pkg.org/badges/signnet)](https://CRAN.R-project.org/package=signnet) 21 | [![Codecov test coverage](https://codecov.io/gh/schochastics/signnet/branch/main/graph/badge.svg)]( https://app.codecov.io/gh/schochastics/signnet?branch=main) 22 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7522563.svg)](https://doi.org/10.5281/zenodo.7522563) 23 | [![JOSS](https://joss.theoj.org/papers/10.21105/joss.04987/status.svg)](https://doi.org/10.21105/joss.04987) 24 | 25 | 26 | The package provides methods to analyse signed networks (i.e. networks with both positive and negative ties). 27 | 28 | ## Installation 29 | 30 | You can install the released version of signnet from CRAN with: 31 | 32 | ```{r inst, eval=FALSE} 33 | install.packages("signnet") 34 | ``` 35 | 36 | The development version from is available with: 37 | 38 | ``` {r inst2, eval=FALSE} 39 | # install.packages("devtools") 40 | devtools::install_github("schochastics/signnet") 41 | ``` 42 | 43 | ## Structural Balance and Triads 44 | 45 | The principles underlying structural balance are based on a theory in social psychology dating back to the work of Heider in the 1940s, which was generalized and extended to graphs by Cartwright and Harary in the 1950s. In its simplest form, it is defined via triangles. A triangle is balanced if 46 | all ties are positive ("the friend of a friend is a friend") or only one tie is positive ("the enemy of my enemy is my friend"). The remaining configurations are said to be unbalanced. 47 | 48 | 49 | 50 | A network is balanced if i.a., it can be partitioned into two vertex subsets, such that intra-group edges are all positive and inter-group edges are all negative. Determining this is easy, but measuring a *degree of balancedness* (i.e. how close is a network to be balanced?) is not. The package, so far, implements three methods to calculate balance scores. All are defined such that a value of one indicates perfect balance and zero perfect unbalance. Though for intermediate networks, results may vary significantly. Check [this paper](https://doi.org/10.1093/comnet/cnx044) by Samin Aref (and his other work) for more details. 51 | 52 | ```{r balance_example,message=FALSE} 53 | library(igraph) 54 | library(signnet) 55 | data("tribes") 56 | 57 | balance_score(tribes, method = "triangles") 58 | balance_score(tribes, method = "walk") 59 | balance_score(tribes, method = "frustration") 60 | ``` 61 | 62 | For directed signed networks, `triad_census_signed()` can be used to compute the count for all 138 non-isomorphic signed triads. 63 | *(The code to reproduce this figure can be found in [this gist](https://gist.github.com/schochastics/dd1974b42cfa5367cf6d8cb9e43bae32))* 64 | 65 | 66 | # Blockmodeling 67 | 68 | The package implements two different blockmodeling algorithms. The classic one tries to partition the 69 | network into a specified set of groups such that intra group edges are positive and inter group edges are negative. 70 | 71 | ```{r block_code,message=FALSE} 72 | clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = TRUE) 73 | clu 74 | ``` 75 | 76 | The parameter *k* is the number of groups and *alpha* specifies the penalty of negative inter group and positive intra group edges. 77 | If `alpha = 0` (`alpha = 1`) then only positive inter group (negative intra group) edges are penalized. Set `alpha = 0.5` for equal penalization 78 | The algorithm is not exact and just a heuristic. If `annealing = TRUE`, then simulated annealing is used. This improves the result, but may take additional time. 79 | 80 | The result of the blockmodel can be visualized with `ggblock` (requires `ggplot2`) 81 | ```{r block_example} 82 | ggblock(tribes, clu$membership, show_blocks = TRUE, show_labels = TRUE) 83 | ``` 84 | 85 | 86 | The second blockmodeling technique is known as *generalized blockmodeling*. This method removes the restriction of positve (negative) inter (intra) group edges. Instead, a blockmatrix is passed to the function with the desired block structure. The example below illustrates the technique with a network composed of three groups with differing inter/intra group edge patterns. 87 | ```{r general_example} 88 | # create a signed network with three groups and different inter/intra group ties 89 | g1 <- g2 <- g3 <- make_full_graph(5) 90 | 91 | V(g1)$name <- as.character(1:5) 92 | V(g2)$name <- as.character(6:10) 93 | V(g3)$name <- as.character(11:15) 94 | 95 | g <- Reduce("%u%", list(g1, g2, g3)) 96 | E(g)$sign <- 1 97 | E(g)$sign[1:10] <- -1 98 | g <- add.edges(g, c(rbind(1:5, 6:10)), attr = list(sign = -1)) 99 | g <- add.edges(g, c(rbind(1:5, 11:15)), attr = list(sign = -1)) 100 | g <- add.edges(g, c(rbind(11:15, 6:10)), attr = list(sign = 1)) 101 | 102 | # specify the link patterns between groups 103 | blockmat <- matrix(c(1, -1, -1, -1, 1, 1, -1, 1, -1), 3, 3, byrow = TRUE) 104 | blockmat 105 | 106 | clu <- signed_blockmodel_general(g, blockmat, 0.5) 107 | clu 108 | ggblock(g, clu$membership, show_blocks = TRUE, show_labels = FALSE) 109 | ``` 110 | 111 | # How to reach out? 112 | 113 | ### Where do I report bugs? 114 | 115 | Simply [open an issue](https://github.com/schochastics/signnet/issues/new) on GitHub. 116 | 117 | ### How do I contribute to the package? 118 | 119 | If you have an idea (but no code yet), [open an issue](https://github.com/schochastics/signnet/issues/new) on GitHub. If you want to contribute with a specific feature and have the code ready, fork the repository, add your code, and create a pull request. 120 | 121 | ### Do you need support? 122 | 123 | The easiest way is to [open an issue](https://github.com/schochastics/signnet/issues/new) - this way, your question is also visible to others who may face similar problems. 124 | 125 | ### Code of Conduct 126 | 127 | Please note that the signnet project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. 128 | 129 | -------------------------------------------------------------------------------- /R/random_graphs.R: -------------------------------------------------------------------------------- 1 | #' Generate random signed graphs according to the G(n,p) Erdos-Renyi model 2 | #' 3 | #' @param n The number of vertices in the graph. 4 | #' @param p The probability for drawing an edge between two arbitrary vertices. 5 | #' @param p_neg The probability of a drawn edge to be a negative tie 6 | #' @param directed logical, whether the graph will be directed. defaults to FALSE. 7 | #' @param loops logical, whether to add loop edges, defaults to FALSE. 8 | #' @references Erdos, P. and Renyi, A., On random graphs, *Publicationes Mathematicae 6*, 290–297 (1959). 9 | #' @return a signed igraph graph object 10 | #' @export 11 | #' 12 | #' @examples 13 | #' sample_gnp_signed(10, 0.4, 0.5) 14 | sample_gnp_signed <- function(n, p, p_neg, directed = FALSE, loops = FALSE) { 15 | g <- igraph::sample_gnp(n = n, p = p, directed = directed, loops = loops) 16 | if (missing(p_neg)) { 17 | stop("p_neg missing with no default") 18 | } 19 | igraph::E(g)$sign <- sample( 20 | c(-1, 1), 21 | igraph::ecount(g), 22 | replace = TRUE, 23 | prob = c(p_neg, 1 - p_neg) 24 | ) 25 | g 26 | } 27 | 28 | #' Bipartite random signed graphs 29 | #' 30 | #' @param n1 Integer scalar, the number of bottom vertices. 31 | #' @param n2 Integer scalar, the number of top vertices. 32 | #' @param p The probability for drawing an edge between two arbitrary vertices. 33 | #' @param p_neg The probability of a drawn edge to be a negative tie 34 | #' @param directed logical, whether the graph will be directed. defaults to FALSE. 35 | #' @param mode Character scalar, specifies how to direct the edges in directed graphs. If it is ‘out’, then directed edges point from bottom vertices to top vertices. If it is ‘in’, edges point from top vertices to bottom vertices. ‘out’ and ‘in’ do not generate mutual edges. If this argument is ‘all’, then each edge direction is considered independently and mutual edges might be generated. This argument is ignored for undirected graphs. 36 | #' 37 | #' @return A signed bipartite igraph graph. 38 | #' @export 39 | #' 40 | #' @examples 41 | #' sample_bipartite_signed(10, 10, 0.5, 0.5) 42 | sample_bipartite_signed <- function( 43 | n1, 44 | n2, 45 | p, 46 | p_neg, 47 | directed = FALSE, 48 | mode = c("out", "in", "all") 49 | ) { 50 | g <- igraph::sample_bipartite_gnp( 51 | n1 = n1, 52 | n2 = n2, 53 | p = p, 54 | directed = directed, 55 | mode = mode 56 | ) 57 | if (missing(p_neg)) { 58 | stop("p_neg missing with no default") 59 | } 60 | igraph::E(g)$sign <- sample( 61 | c(-1, 1), 62 | igraph::ecount(g), 63 | replace = TRUE, 64 | prob = c(p_neg, 1 - p_neg) 65 | ) 66 | g 67 | } 68 | 69 | #' @title A graph with random subgraphs connected by negative edges 70 | #' @description Create a number of Erdos-Renyi random graphs with identical parameters, and connect them with the specified number of negative ties. 71 | #' @param islands.n The number of islands in the graph. 72 | #' @param islands.size The size of the islands in the graph. 73 | #' @param islands.pin The probability of intra-island edges. 74 | #' @param n.inter number of negative edges between two islands. 75 | #' @return a signed igraph graph 76 | #' @author David Schoch 77 | #' @examples 78 | #' library(igraph) 79 | #' sample_islands_signed(3, 10, 0.5, 1) 80 | #' @export 81 | sample_islands_signed <- function( 82 | islands.n, 83 | islands.size, 84 | islands.pin, 85 | n.inter 86 | ) { 87 | if (islands.n <= 1) { 88 | stop("islands.n should be greater than one") 89 | } 90 | if (islands.size <= 1) { 91 | stop("islands.size should be greater than one") 92 | } 93 | if (islands.pin < 0 || islands.pin > 1) { 94 | stop("islands.pin should be between zero and one") 95 | } 96 | if (n.inter <= 0) { 97 | stop("n.inter should be greater than zero") 98 | } 99 | el <- matrix(0, 0, 2) 100 | for (i in 1:islands.n) { 101 | tmp <- t(utils::combn(((i - 1) * islands.size + 1):(i * islands.size), 2)) 102 | tmp <- tmp[ 103 | sample( 104 | c(FALSE, TRUE), 105 | nrow(tmp), 106 | replace = TRUE, 107 | prob = c(1 - islands.pin, islands.pin) 108 | ), 109 | ] 110 | el <- rbind(el, tmp) 111 | } 112 | el <- cbind(el, 1) 113 | for (i in 1:islands.n) { 114 | outside <- setdiff( 115 | 1:(islands.size * islands.n), 116 | ((i - 1) * islands.size + 1):(i * islands.size) 117 | ) 118 | inside <- ((i - 1) * islands.size + 1):(i * islands.size) 119 | to <- sample(outside, n.inter, replace = TRUE) 120 | from <- sample(inside, n.inter, replace = TRUE) 121 | tmp <- cbind(from, to, -1) 122 | el <- rbind(el, tmp) 123 | } 124 | g <- igraph::graph_from_edgelist(el[, 1:2], directed = FALSE) 125 | igraph::E(g)$sign <- el[, 3] 126 | igraph::V(g)$grp <- as.character(rep(1:islands.n, each = islands.size)) 127 | g <- igraph::delete_edges(g, which(igraph::which_multiple(g))) 128 | g 129 | } 130 | 131 | #' @title circular signed graph 132 | #' @description circular graph with positive and negative edges. 133 | #' 134 | #' @param n number of nodes 135 | #' @param r radius 136 | #' @param pos distance fraction between positive edges 137 | #' @param neg distance fraction between negative edges 138 | #' @return igraph graph 139 | #' @author David Schoch 140 | #' @examples 141 | #' library(igraph) 142 | #' graph_circular_signed(n = 50) 143 | #' @export 144 | # 145 | graph_circular_signed <- function(n, r = 1, pos = 0.1, neg = 0.1) { 146 | if (missing(n)) { 147 | stop('argument "n" is missing, with no default') 148 | } 149 | pts <- circleFun(r = r, npoints = n) 150 | 151 | D <- arcDistMat(as.matrix(pts), r) 152 | 153 | thr <- (2 * pi * r) * pos 154 | anti <- arc_dist(c(0, r), c(0, -r), r) * (1 - neg) 155 | P <- (D <= thr & D != 0) + 0 156 | N <- (D >= anti & D != 0) + 0 157 | 158 | A <- P - N 159 | g <- igraph::graph_from_adjacency_matrix( 160 | A, 161 | mode = "undirected", 162 | weighted = TRUE 163 | ) 164 | igraph::E(g)$sign <- ifelse(igraph::E(g)$weight == 1, 1, -1) 165 | g <- igraph::delete_edge_attr(g, "weight") 166 | igraph::V(g)$x <- pts$x 167 | igraph::V(g)$y <- pts$y 168 | g 169 | } 170 | 171 | # create points on a circle with radius r around center 172 | circleFun <- function(center = c(0, 0), r = 1, npoints = 20) { 173 | pts_seq <- seq(0, 2 * pi, length.out = npoints * 100) 174 | pts_samp <- sample(pts_seq, npoints) 175 | 176 | xx <- center[1] + r * cos(pts_samp) 177 | yy <- center[2] + r * sin(pts_samp) 178 | return(data.frame(x = xx, y = yy)) 179 | } 180 | 181 | # distance between two points x and y on a circle with radius r 182 | arc_dist <- function(x, y, r) { 183 | c <- sqrt((x[1] - y[1])^2 + (x[2] - y[2])^2) 184 | theta <- acos((2 * r^2 - c^2) / (2 * r^2)) 185 | 2 * pi * r * theta / (2 * pi) 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # signnet 5 | 6 | 7 | 8 | [![R-CMD-check](https://github.com/schochastics/signnet/workflows/R-CMD-check/badge.svg)](https://github.com/schochastics/signnet/actions) 9 | [![CRAN 10 | status](https://www.r-pkg.org/badges/version/signnet)](https://cran.r-project.org/package=signnet) 11 | [![Downloads](https://cranlogs.r-pkg.org/badges/signnet)](https://CRAN.R-project.org/package=signnet) 12 | [![Codecov test 13 | coverage](https://codecov.io/gh/schochastics/signnet/branch/main/graph/badge.svg)](https://app.codecov.io/gh/schochastics/signnet?branch=main) 14 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7522563.svg)](https://doi.org/10.5281/zenodo.7522563) 15 | [![JOSS](https://joss.theoj.org/papers/10.21105/joss.04987/status.svg)](https://doi.org/10.21105/joss.04987) 16 | 17 | 18 | The package provides methods to analyse signed networks (i.e. networks 19 | with both positive and negative ties). 20 | 21 | ## Installation 22 | 23 | You can install the released version of signnet from CRAN with: 24 | 25 | ``` r 26 | install.packages("signnet") 27 | ``` 28 | 29 | The development version from is available with: 30 | 31 | ``` r 32 | # install.packages("devtools") 33 | devtools::install_github("schochastics/signnet") 34 | ``` 35 | 36 | ## Structural Balance and Triads 37 | 38 | The principles underlying structural balance are based on a theory in 39 | social psychology dating back to the work of Heider in the 1940s, which 40 | was generalized and extended to graphs by Cartwright and Harary in the 41 | 1950s. In its simplest form, it is defined via triangles. A triangle is 42 | balanced if all ties are positive (“the friend of a friend is a friend”) 43 | or only one tie is positive (“the enemy of my enemy is my friend”). The 44 | remaining configurations are said to be unbalanced. 45 | 46 | 47 | 48 | A network is balanced if i.a., it can be partitioned into two vertex 49 | subsets, such that intra-group edges are all positive and inter-group 50 | edges are all negative. Determining this is easy, but measuring a 51 | *degree of balancedness* (i.e. how close is a network to be balanced?) 52 | is not. The package, so far, implements three methods to calculate 53 | balance scores. All are defined such that a value of one indicates 54 | perfect balance and zero perfect unbalance. Though for intermediate 55 | networks, results may vary significantly. Check [this 56 | paper](https://doi.org/10.1093/comnet/cnx044) by Samin Aref (and his 57 | other work) for more details. 58 | 59 | ``` r 60 | library(igraph) 61 | library(signnet) 62 | data("tribes") 63 | 64 | balance_score(tribes, method = "triangles") 65 | #> [1] 0.8676471 66 | balance_score(tribes, method = "walk") 67 | #> [1] 0.3575761 68 | balance_score(tribes, method = "frustration") 69 | #> [1] 0.7586207 70 | ``` 71 | 72 | For directed signed networks, `triad_census_signed()` can be used to 73 | compute the count for all 138 non-isomorphic signed triads. *(The code 74 | to reproduce this figure can be found in [this 75 | gist](https://gist.github.com/schochastics/dd1974b42cfa5367cf6d8cb9e43bae32))* 76 | 77 | 78 | # Blockmodeling 79 | 80 | The package implements two different blockmodeling algorithms. The 81 | classic one tries to partition the network into a specified set of 82 | groups such that intra group edges are positive and inter group edges 83 | are negative. 84 | 85 | ``` r 86 | clu <- signed_blockmodel(tribes, k = 3, alpha = 0.5, annealing = TRUE) 87 | clu 88 | #> $membership 89 | #> [1] 3 3 2 2 1 2 2 2 1 1 2 2 1 1 3 3 90 | #> 91 | #> $criterion 92 | #> [1] 2 93 | ``` 94 | 95 | The parameter *k* is the number of groups and *alpha* specifies the 96 | penalty of negative inter group and positive intra group edges. If 97 | `alpha = 0` (`alpha = 1`) then only positive inter group (negative intra 98 | group) edges are penalized. Set `alpha = 0.5` for equal penalization The 99 | algorithm is not exact and just a heuristic. If `annealing = TRUE`, then 100 | simulated annealing is used. This improves the result, but may take 101 | additional time. 102 | 103 | The result of the blockmodel can be visualized with `ggblock` (requires 104 | `ggplot2`) 105 | 106 | ``` r 107 | ggblock(tribes, clu$membership, show_blocks = TRUE, show_labels = TRUE) 108 | ``` 109 | 110 | 111 | 112 | The second blockmodeling technique is known as *generalized 113 | blockmodeling*. This method removes the restriction of positve 114 | (negative) inter (intra) group edges. Instead, a blockmatrix is passed 115 | to the function with the desired block structure. The example below 116 | illustrates the technique with a network composed of three groups with 117 | differing inter/intra group edge patterns. 118 | 119 | ``` r 120 | # create a signed network with three groups and different inter/intra group ties 121 | g1 <- g2 <- g3 <- make_full_graph(5) 122 | 123 | V(g1)$name <- as.character(1:5) 124 | V(g2)$name <- as.character(6:10) 125 | V(g3)$name <- as.character(11:15) 126 | 127 | g <- Reduce("%u%", list(g1, g2, g3)) 128 | E(g)$sign <- 1 129 | E(g)$sign[1:10] <- -1 130 | g <- add.edges(g, c(rbind(1:5, 6:10)), attr = list(sign = -1)) 131 | #> Warning: `add.edges()` was deprecated in igraph 2.0.0. 132 | #> ℹ Please use `add_edges()` instead. 133 | #> This warning is displayed once every 8 hours. 134 | #> Call `lifecycle::last_lifecycle_warnings()` to see where this 135 | #> warning was generated. 136 | g <- add.edges(g, c(rbind(1:5, 11:15)), attr = list(sign = -1)) 137 | g <- add.edges(g, c(rbind(11:15, 6:10)), attr = list(sign = 1)) 138 | 139 | # specify the link patterns between groups 140 | blockmat <- matrix(c(1, -1, -1, -1, 1, 1, -1, 1, -1), 3, 3, byrow = TRUE) 141 | blockmat 142 | #> [,1] [,2] [,3] 143 | #> [1,] 1 -1 -1 144 | #> [2,] -1 1 1 145 | #> [3,] -1 1 -1 146 | 147 | clu <- signed_blockmodel_general(g, blockmat, 0.5) 148 | clu 149 | #> $membership 150 | #> [1] 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 151 | #> 152 | #> $criterion 153 | #> [1] 0 154 | ggblock(g, clu$membership, show_blocks = TRUE, show_labels = FALSE) 155 | ``` 156 | 157 | 158 | 159 | # How to reach out? 160 | 161 | ### Where do I report bugs? 162 | 163 | Simply [open an 164 | issue](https://github.com/schochastics/signnet/issues/new) on GitHub. 165 | 166 | ### How do I contribute to the package? 167 | 168 | If you have an idea (but no code yet), [open an 169 | issue](https://github.com/schochastics/signnet/issues/new) on GitHub. If 170 | you want to contribute with a specific feature and have the code ready, 171 | fork the repository, add your code, and create a pull request. 172 | 173 | ### Do you need support? 174 | 175 | The easiest way is to [open an 176 | issue](https://github.com/schochastics/signnet/issues/new) - this way, 177 | your question is also visible to others who may face similar problems. 178 | 179 | ### Code of Conduct 180 | 181 | Please note that the signnet project is released with a [Contributor 182 | Code of 183 | Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). 184 | By contributing to this project, you agree to abide by its terms. 185 | -------------------------------------------------------------------------------- /data-raw/paper.bib: -------------------------------------------------------------------------------- 1 | @article{cn-ispcnr-06, 2 | title = {The Igraph Software Package for Complex Network Research}, 3 | author = {Csardi, Gabor and Nepusz, Tamas}, 4 | year = {2006}, 5 | journal = {InterJournal, Complex Systems}, 6 | volume = {1695}, 7 | number = {5}, 8 | pages = {1--9} 9 | } 10 | 11 | @article{b-snas-08, 12 | title = {Social {{Network Analysis}} with {{SNA}}}, 13 | author = {Butts, Carter T.}, 14 | year = {2008}, 15 | journal = {Journal of Statistical Software}, 16 | volume = {24}, 17 | number = {6}, 18 | issn = {1548-7660}, 19 | doi = {10.18637/jss.v024.i06} 20 | } 21 | 22 | @article{ch-sbght-56, 23 | title = {Structural Balance: A Generalization of {{Heider}}'s Theory.}, 24 | author = {Cartwright, Dorwin and Harary, Frank}, 25 | year = {1956}, 26 | journal = {Psychological Review}, 27 | volume = {63}, 28 | number = {5}, 29 | pages = {277}, 30 | publisher = {American Psychological Association}, 31 | doi = {10.1037/h0046049} 32 | } 33 | 34 | @article{h-aco-46, 35 | title = {Attitudes and {{Cognitive Organization}}}, 36 | author = {Heider, Fritz}, 37 | year = {1946}, 38 | month = jan, 39 | journal = {The Journal of Psychology}, 40 | volume = {21}, 41 | number = {1}, 42 | pages = {107--112}, 43 | issn = {0022-3980}, 44 | doi = {10.1080/00223980.1946.9917275}, 45 | pmid = {21010780} 46 | } 47 | 48 | @article{aw-mpbsn-18, 49 | title = {Measuring Partial Balance in Signed Networks}, 50 | author = {Aref, Samin and Wilson, Mark C.}, 51 | year = {2018}, 52 | journal = {Journal of Complex Networks}, 53 | volume = {6}, 54 | number = {4}, 55 | pages = {566--595}, 56 | issn = {2051-1310}, 57 | doi = {10.1093/comnet/cnx044} 58 | } 59 | 60 | @article{aw-bfsn-19, 61 | title = {Balance and Frustration in Signed Networks}, 62 | author = {Aref, Samin and Wilson, Mark C}, 63 | year = {2019}, 64 | journal = {Journal of Complex Networks}, 65 | volume = {7}, 66 | number = {2}, 67 | pages = {163--189}, 68 | issn = {2051-1329}, 69 | doi = {10.1093/comnet/cny015} 70 | } 71 | 72 | @article{dm-pasb-96, 73 | title = {A Partitioning Approach to Structural Balance}, 74 | author = {Doreian, Patrick and Mrvar, Andrej}, 75 | year = {1996}, 76 | journal = {Social Networks}, 77 | volume = {18}, 78 | number = {2}, 79 | pages = {149--168}, 80 | issn = {0378-8733}, 81 | doi = {10.1016/0378-8733(95)00259-6} 82 | } 83 | 84 | @article{n-stwspuc1-20, 85 | title = {A Sign of the Times? {{Weak}} and Strong Polarization in the {{U}}.{{S}}. {{Congress}}, 1973\textendash 2016}, 86 | author = {Neal, Zachary P.}, 87 | year = {2020}, 88 | journal = {Social Networks}, 89 | series = {Social {{Network Research}} on {{Negative Ties}} and {{Signed Graphs}}}, 90 | volume = {60}, 91 | pages = {103--112}, 92 | issn = {0378-8733}, 93 | doi = {10.1016/j.socnet.2018.07.007} 94 | } 95 | 96 | @inproceedings{bklv-nacsw-09, 97 | title = {Network Analysis of Collaboration Structure in {{Wikipedia}}}, 98 | booktitle = {Proceedings of the 18th International Conference on {{World}} Wide Web}, 99 | author = {Brandes, Ulrik and Kenis, Patrick and Lerner, J{\"u}rgen and Van Raaij, Denise}, 100 | year = {2009}, 101 | pages = {731--740}, 102 | organization = {{ACM}}, 103 | doi = {10.1145/1526709.1526808} 104 | } 105 | 106 | @inproceedings{klb-szmsnne-09, 107 | title = {The {{Slashdot Zoo}}: {{Mining}} a {{Social Network}} with {{Negative Edges}}}, 108 | shorttitle = {The {{Slashdot Zoo}}}, 109 | booktitle = {Proceedings of the 18th {{International Conference}} on {{World Wide Web}}}, 110 | author = {Kunegis, J{\'e}r{\^o}me and Lommatzsch, Andreas and Bauckhage, Christian}, 111 | year = {2009}, 112 | series = {{{WWW}} '09}, 113 | pages = {741--750}, 114 | publisher = {{ACM}}, 115 | address = {{New York, NY, USA}}, 116 | doi = {10.1145/1526709.1526809} 117 | } 118 | 119 | @article{eb-ncnt-14, 120 | title = {Networks Containing Negative Ties}, 121 | author = {Everett, Martin G. and Borgatti, Stephen P.}, 122 | year = {2014}, 123 | journal = {Social Networks}, 124 | volume = {38}, 125 | pages = {111--120}, 126 | issn = {0378-8733}, 127 | doi = {10.1016/j.socnet.2014.03.005} 128 | } 129 | 130 | @article{bl-csnr-04, 131 | title = {Calculating Status with Negative Relations}, 132 | author = {Bonacich, Phillip and Lloyd, Paulette}, 133 | year = {2004}, 134 | journal = {Social Networks}, 135 | volume = {26}, 136 | number = {4}, 137 | pages = {331--338}, 138 | issn = {0378-8733}, 139 | doi = {10.1016/j.socnet.2004.08.007}, 140 | } 141 | 142 | @article{k-nsidsa-53, 143 | title = {A New Status Index Derived from Sociometric Analysis}, 144 | author = {Katz, Leo}, 145 | year = {1953}, 146 | journal = {Psychometrika}, 147 | volume = {18}, 148 | number = {1}, 149 | pages = {39--43}, 150 | issn = {0033-3123, 1860-0980}, 151 | doi = {10.1007/BF02289026}, 152 | } 153 | 154 | @article{s-pstn-21, 155 | title = {Projecting Signed Two-Mode Networks}, 156 | author = {Schoch, David}, 157 | year = {2021}, 158 | journal = {The Journal of Mathematical Sociology}, 159 | volume = {45}, 160 | number = {1}, 161 | pages = {37--50}, 162 | issn = {0022-250X}, 163 | doi = {10.1080/0022250X.2019.1711376} 164 | } 165 | 166 | @article{dns-brpebbp-21, 167 | title = {Backbone: {{An R}} Package for Extracting the Backbone of Bipartite Projections}, 168 | shorttitle = {Backbone}, 169 | author = {Domagalski, Rachel and Neal, Zachary P. and Sagan, Bruce}, 170 | year = {2021}, 171 | month = jan, 172 | journal = {PLOS ONE}, 173 | volume = {16}, 174 | number = {1}, 175 | pages = {e0244363}, 176 | publisher = {{Public Library of Science}}, 177 | issn = {1932-6203}, 178 | doi = {10.1371/journal.pone.0244363} 179 | } 180 | 181 | @misc{csr-avacpbsn-22, 182 | title = {Analyzing and {{Visualizing American Congress Polarization}} and {{Balance}} with {{Signed Networks}}}, 183 | author = {Capozzi, Arthur and Semeraro, Alfonso and Ruffo, Giancarlo}, 184 | year = {2022}, 185 | month = sep, 186 | number = {arXiv:2209.00676}, 187 | eprint = {2209.00676}, 188 | eprinttype = {arxiv}, 189 | primaryclass = {cs}, 190 | publisher = {{arXiv}}, 191 | doi = {10.48550/arXiv.2209.00676}, 192 | archiveprefix = {arXiv} 193 | } 194 | 195 | @article{e-rsbssn-19, 196 | title = {Rethinking Structural Balance in Signed Social Networks}, 197 | author = {Estrada, Ernesto}, 198 | year = {2019}, 199 | month = may, 200 | journal = {Discrete Applied Mathematics}, 201 | issn = {0166-218X}, 202 | doi = {10.1016/j.dam.2019.04.019} 203 | } 204 | 205 | @misc{fmtk-ergmdsnair-22, 206 | title = {Exponential {{Random Graph Models}} for {{Dynamic Signed Networks}}: {{An Application}} to {{International Relations}}}, 207 | shorttitle = {Exponential {{Random Graph Models}} for {{Dynamic Signed Networks}}}, 208 | author = {Fritz, Cornelius and Mehrl, Marius and Thurner, Paul W. and {kauermann}, G{\"o}ran}, 209 | year = {2022}, 210 | month = may, 211 | number = {arXiv:2205.13411}, 212 | eprint = {2205.13411}, 213 | eprinttype = {arxiv}, 214 | primaryclass = {cs, stat}, 215 | publisher = {{arXiv}}, 216 | doi = {10.48550/arXiv.2205.13411} 217 | } 218 | 219 | @article{an-dcopsnpc-20, 220 | title = {Detecting Coalitions by Optimally Partitioning Signed Networks of Political Collaboration}, 221 | author = {Aref, Samin and Neal, Zachary}, 222 | year = {2020}, 223 | journal = {Scientific Reports}, 224 | volume = {10}, 225 | number = {1}, 226 | pages = {1506}, 227 | publisher = {{Nature Publishing Group}}, 228 | issn = {2045-2322}, 229 | doi = {10.1038/s41598-020-58471-z} 230 | } 231 | 232 | @manual{s-omsmilp-22, 233 | title = {ompr: Model and Solve Mixed Integer Linear Programs}, 234 | author = {Dirk Schumacher}, 235 | year = {2022}, 236 | note = {R package version 1.0.3}, 237 | url = {https://CRAN.R-project.org/package=ompr}, 238 | } 239 | 240 | @article{n-brpenb-22, 241 | doi = {10.1371/journal.pone.0269137}, 242 | author = {Neal, Zachary P.}, 243 | journal = {PLOS ONE}, 244 | publisher = {Public Library of Science}, 245 | title = {backbone: An {{R}} package to extract network backbones}, 246 | year = {2022}, 247 | month = {05}, 248 | volume = {17}, 249 | url = {https://doi.org/10.1371/journal.pone.0269137}, 250 | pages = {1-24}, 251 | number = {5} 252 | } 253 | -------------------------------------------------------------------------------- /data-raw/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'signnet: An R package for analyzing signed networks' 3 | tags: 4 | - R 5 | - network analysis 6 | - signed networks 7 | - structural balance theory 8 | date: "02 November 2022" 9 | output: pdf_document 10 | bibliography: paper.bib 11 | affiliations: 12 | - name: GESIS - Leibniz Institute for the Social Sciences 13 | index: 1 14 | authors: 15 | - name: David Schoch 16 | orcid: 0000-0003-2952-4812 17 | affiliation: 1 18 | --- 19 | 20 | 21 | # Summary 22 | 23 | Network analysis usually deals with relations among entities which are positive, 24 | such as friendship, or advice seeking. Most analytic tools are constructed 25 | with this assumption, be that centrality indices, or clustering tools. However, 26 | not all conceivable relationships are positive. People can be friends but also 27 | enemies. A signed network is a network where both, positive and negative 28 | relationships may occur. Common network analytic tools are not applicable to 29 | such networks without adapting for the existence of negative ties. The R 30 | package `signnet` brings together methods that have been developed to 31 | analyze signed networks. This includes known blockmodeling techniques, centrality 32 | indices and tools for two-mode networks, as well as unique analytic techniques 33 | surrounding structural balance theory. 34 | 35 | # Statement of need 36 | 37 | Signed networks are increasingly popular in empirical network science since many 38 | phenomena can be modeled with positive and negative ties. Examples include 39 | studies of polarization [@n-stwspuc1-20], collaborations on Wikipedia 40 | [@bklv-nacsw-09], international relations [@e-rsbssn-19], and relations on 41 | social media [@klb-szmsnne-09]. General purpose packages for network analysis 42 | such as `igraph` [@cn-ispcnr-06] and `sna` [@b-snas-08] implement all commonly 43 | used network analytic methods but do not offer any functionality for signed 44 | networks. `signnet` closes this gap and makes many tools for signed 45 | networks available in R. The package has already found its place in empirical research 46 | [@csr-avacpbsn-22;@fmtk-ergmdsnair-22] and the R package `backbone` [@n-brpenb-22] uses the data 47 | structure suggested by `signnet` for signed backbones of networks. 48 | 49 | # Implementation details 50 | The package is modeled with `igraph` compatibility in mind and follows its 51 | function naming scheme. All functions in the package assume that an `igraph` object is a 52 | signed network if it has an edge attribute "sign" with values 1 (positive) or -1 53 | (negative). If a function from `igraph` was adapted for signed networks, it can be 54 | called via `_signed()`. Prominent examples include 55 | `as_adj_signed()`, `graph_from_adjacency_matrix_signed()`, `degree_signed()`, 56 | and `triad_census_signed()`. 57 | 58 | # Functionalities 59 | 60 | This section highlights some of the main methods implemented in the package. 61 | For more details for each subsection see the respective package vignette or 62 | consult . 63 | 64 | ```R 65 | install.packages("signnet") 66 | library(signnet) 67 | data("tribes") # dataset included in signnet 68 | ``` 69 | 70 | ## Structural balance 71 | In its simplest form, structural balance is defined via triangles [@h-aco-46]. A 72 | triangle in a network is balanced if all ties are positive ("the friend of a 73 | friend is a friend") or only one tie is positive ("the enemy of my enemy is my 74 | friend"). The remaining configurations are said to be unbalanced. A whole 75 | network is balanced if it can be partitioned into two node sets, such that 76 | intra-group edges are all positive and inter-group edges are all negative 77 | [@ch-sbght-56]. 78 | 79 | Determining if a network is balanced or not is easy, but measuring the degree of "balancedness" (i.e. how close is a network to be balanced?) is not. `signnet` implements several methods to calculate balance scores[@aw-mpbsn-18]. All are defined such that a value of one indicates perfect balance and zero perfect imbalance. 80 | 81 | ```R 82 | balance_score(tribes, method = "triangles") 83 | #> 0.867647 84 | ``` 85 | The method based on triangles simply counts the fraction of triangles that are balanced. 86 | Alternatively, the frustration index can be used, which computes the minimum number of edges 87 | whose removal results in a balance network [@aw-bfsn-19]. To use the function `frustation_exact()`, 88 | `ompr` and its auxiliary packages need to be installed first [@s-omsmilp-22]. 89 | 90 | ```R 91 | install.packages(c("ompr", "ompr.roi", "ROI", "ROI.plugin.glpk")) 92 | frustration_exact(tribes) 93 | #> $frustration 94 | #> [1] 7 95 | #> 96 | #> $partition 97 | #> [1] 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 98 | ``` 99 | 100 | The return value `partition` gives the optimal partition into the two node sets for 101 | which the optimal frustration is achieved. The implemented algorithm can deal with fairly large 102 | networks, even though the problem is NP hard [@an-dcopsnpc-20]. 103 | 104 | ## Blockmodeling 105 | In signed blockmodeling, the goal is to determine $k$ blocks of nodes such that 106 | all intra-block edges are positive and inter-block edges are negative 107 | [@dm-pasb-96]. The function `signed_blockmodel()` is used to construct such a 108 | model. The parameter $k$ is the number of desired blocks. $\alpha$ is a trade-off 109 | parameter. The function minimizes $P(C)=\alpha N+(1-\alpha)P$, where $N$ is the 110 | total number of negative ties within blocks and $P$ be the total number of 111 | positive ties between blocks. 112 | 113 | ```R 114 | tribe_blocks <- signed_blockmodel(tribes, k = 3, 115 | alpha = 0.5, annealing = TRUE) 116 | tribe_blocks 117 | #> $membership 118 | #> [1] 2 2 1 1 3 1 1 1 3 3 1 1 3 3 2 2 119 | #> 120 | #> $criterion 121 | #> [1] 2 122 | ``` 123 | 124 | The result can be visualized with `ggblock`, as shown in Figure \ref{fig:block_ex}. 125 | ```R 126 | ggblock(tribes, tribe_blocks$membership, show_blocks = TRUE) 127 | ``` 128 | \begin{figure}[htb] 129 | \centering 130 | \includegraphics[width=0.5\textwidth]{../man/figures/block_example.pdf} 131 | \caption{Blocks of the tribes network. Blue squares indicate positive and red 132 | squares negative ties.} 133 | \label{fig:block_ex} 134 | \end{figure} 135 | 136 | ## Centrality 137 | There exist hundreds of indices for networks with only positive ties, but for signed 138 | networks they are rather scarce. The `signnet` package implements three indices. 139 | Versions of degree and eigenvector centrality [@bl-csnr-04], and PN centrality [@eb-ncnt-14]. 140 | The PN index is very similar to Katz status for networks with only positive ties 141 | [@k-nsidsa-53]. The technical details can be found in the paper by Everett & 142 | Borgatti. 143 | 144 | The below example illustrates all indices with a network where signed degree can not distinguish vertices. 145 | ```R 146 | A <- matrix(c( 0, 1, 0, 1, 0, 0, 0, -1, -1, 0, 147 | 1, 0, 1, -1, 1, -1, -1, 0, 0, 0, 148 | 0, 1, 0, 1, -1, 0, 0, 0, -1, 0, 149 | 1, -1, 1, 0, 1, -1, -1, 0, 0, 0, 150 | 0, 1, -1, 1, 0, 1, 0, -1, 0, -1, 151 | 0, -1, 0, -1, 1, 0, 1, 0, 1, -1, 152 | 0, -1, 0, -1, 0, 1, 0, 1, -1, 1, 153 | -1, 0, 0, 0, -1, 0, 1, 0, 1, 0, 154 | -1, 0, -1, 0, 0, 1, -1, 1, 0, 1, 155 | 0, 0, 0, 0, -1, -1, 1, 0, 1, 0),10,10) 156 | 157 | g <- graph_from_adjacency_matrix_signed(A, "undirected") 158 | 159 | degree_signed(g, type = "ratio") 160 | #> [1] 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 161 | eigen_centrality_signed(g) 162 | #> [1] 0.6221496 1.0000000 0.7451885 1.0000000 0.8999004 0.6428959 0.3582816 163 | #> [8] 0.3747192 0.2808741 0.0783457 164 | pn_index(g) 165 | #> [1] 0.900975 0.861348 0.907700 0.861348 0.841066 0.849656 0.861732 166 | #> [8] 0.901591 0.850985 0.907293 167 | ``` 168 | 169 | ## Signed two-mode networks 170 | A common analytic tool for two-mode networks is to project the network onto on 171 | relevant mode. This is easily done using the adjacency matrix $A$. $AA^T$ 172 | yields the row projection and $A^TA$ the column projection. The resulting 173 | networks will thus be weighted. Several methods exist to turn a weighted 174 | projection into an unweighted network where only the most significant edges are 175 | included [@n-brpenb-22]. Projecting signed networks is not as 176 | straightforward, because "nullification" of edges can occur. @s-pstn-21 177 | introduces two methods to deal with this issue which are implemented in 178 | `signnet`. The trick is to convert the signed network into an a special 179 | unsigned one with `as_unsigned_2mode()`, do the projection as usual and the turn 180 | it back to a signed graph with `as_signed_proj()`. The details can be found in 181 | the original paper and in two designated vignettes. 182 | 183 | # References 184 | -------------------------------------------------------------------------------- /tests/testthat/test-complex_matrices.R: -------------------------------------------------------------------------------- 1 | test_that("incidence: error not graph works", { 2 | expect_error(as_incidence_signed(g = 5)) 3 | }) 4 | 5 | test_that("incidence: error no sign works", { 6 | expect_error(as_incidence_signed(igraph::make_full_graph( 7 | 3, 8 | directed = FALSE 9 | ))) 10 | }) 11 | 12 | test_that("incidence: error no type works", { 13 | g <- igraph::make_full_graph(3, directed = FALSE) 14 | igraph::E(g)$sign <- 1 15 | expect_error(as_incidence_signed(g)) 16 | }) 17 | 18 | test_that("signed incidence works", { 19 | A_true <- matrix( 20 | c( 21 | 1, 22 | 1, 23 | 1, 24 | -1, 25 | -1, 26 | -1, 27 | -1, 28 | 1, 29 | 1, 30 | 1, 31 | -1, 32 | -1, 33 | -1, 34 | -1, 35 | 1, 36 | 1, 37 | 1, 38 | -1, 39 | -1, 40 | -1, 41 | -1, 42 | -1, 43 | -1, 44 | -1, 45 | 1, 46 | 1, 47 | 1, 48 | 1, 49 | -1, 50 | -1, 51 | -1, 52 | 1, 53 | 1, 54 | 1, 55 | 1 56 | ), 57 | 5, 58 | 7, 59 | byrow = T 60 | ) 61 | rownames(A_true) <- letters[1:5] 62 | colnames(A_true) <- 1:7 63 | g <- igraph::graph_from_biadjacency_matrix(A_true, weighted = "sign") 64 | A <- as_incidence_signed(g) 65 | expect_equal(A, A_true) 66 | }) 67 | 68 | 69 | test_that("complex: error not graph works", { 70 | expect_error(as_adj_complex(g = 5)) 71 | }) 72 | 73 | test_that("complex: error directed works", { 74 | expect_error(as_adj_complex(igraph::make_full_graph(3, directed = TRUE))) 75 | }) 76 | 77 | test_that("complex: error no attribute works", { 78 | expect_error(as_adj_complex(igraph::make_full_graph(3, directed = FALSE))) 79 | }) 80 | 81 | test_that("complex: error wrong attribute works", { 82 | g <- igraph::make_full_graph(3, directed = FALSE) 83 | igraph::E(g)$type <- "F" 84 | expect_error(as_adj_complex(g, "type")) 85 | }) 86 | 87 | test_that("complex: complex adj works", { 88 | g <- igraph::make_full_graph(3, directed = FALSE) 89 | igraph::E(g)$type <- "N" 90 | A_true <- structure( 91 | c( 92 | 0 + 0i, 93 | 0 - 1i, 94 | 0 - 1i, 95 | 0 + 1i, 96 | 0 + 0i, 97 | 0 - 1i, 98 | 0 + 1i, 99 | 0 + 1i, 100 | 0 + 0i 101 | ), 102 | .Dim = c(3L, 3L) 103 | ) 104 | A <- as_adj_complex(g, "type") 105 | expect_equal(A, A_true) 106 | }) 107 | 108 | test_that("signed: error not graph works", { 109 | expect_error(as_adj_signed(g = 5)) 110 | }) 111 | 112 | test_that("signed: error attribute works", { 113 | expect_error(as_adj_complex(igraph::make_full_graph(3))) 114 | }) 115 | 116 | test_that("complex laplace: error not graph works", { 117 | expect_error(laplacian_matrix_complex(g = 5)) 118 | }) 119 | 120 | test_that("complex laplace: error directed works", { 121 | expect_error(laplacian_matrix_complex(igraph::make_full_graph( 122 | 3, 123 | directed = TRUE 124 | ))) 125 | }) 126 | 127 | test_that("complex laplacian: complex laplacian works", { 128 | g <- igraph::make_full_graph(3, directed = FALSE) 129 | igraph::E(g)$type <- "P" 130 | 131 | L <- laplacian_matrix_complex(g, "type") 132 | L_true <- structure( 133 | c( 134 | 2 + 0i, 135 | -1 + 0i, 136 | -1 + 0i, 137 | -1 + 0i, 138 | 2 + 0i, 139 | -1 + 0i, 140 | -1 + 0i, 141 | -1 + 0i, 142 | 2 + 0i 143 | ), 144 | .Dim = c(3L, 3L) 145 | ) 146 | expect_equal(L, L_true) 147 | }) 148 | 149 | test_that("complex laplacian: complex laplacian norm works", { 150 | g <- igraph::make_full_graph(3, directed = FALSE) 151 | igraph::E(g)$type <- "P" 152 | 153 | L <- laplacian_matrix_complex(g, "type", norm = TRUE) 154 | L_true <- structure( 155 | c( 156 | 1 + 0i, 157 | -0.5 + 0i, 158 | -0.5 + 0i, 159 | -0.5 + 0i, 160 | 1 + 0i, 161 | -0.5 + 0i, 162 | -0.5 + 0i, 163 | -0.5 + 0i, 164 | 1 + 0i 165 | ), 166 | .Dim = c(3L, 3L) 167 | ) 168 | expect_equal(L, L_true) 169 | }) 170 | 171 | test_that("complex incidence: incidence works", { 172 | g <- igraph::make_full_graph(3, directed = FALSE) 173 | igraph::E(g)$type <- c("P", "N", "A") 174 | 175 | S <- round(as_incidence_complex(g, "type"), 4) 176 | S_true <- structure( 177 | c( 178 | 1 + 0i, 179 | -1 + 0i, 180 | 0 + 0i, 181 | 0.7071 + 0.7071i, 182 | 0 + 0i, 183 | -0.7071 + 0.7071i, 184 | 0 + 0i, 185 | 0.7769 + 0.3218i, 186 | -0.7769 + 0.3218i 187 | ), 188 | .Dim = c(3L, 3L) 189 | ) 190 | expect_equal(S, S_true) 191 | }) 192 | 193 | test_that("as_complex_edges works", { 194 | g <- igraph::make_full_graph(3, directed = FALSE) 195 | igraph::E(g)$sign <- -1 196 | g1 <- as_complex_edges(g) 197 | expect_equal(igraph::E(g1)$type, c("N", "N", "N")) 198 | }) 199 | 200 | test_that("as_complex_edges error not graph works", { 201 | expect_error(as_complex_edges(g = 5)) 202 | }) 203 | 204 | test_that("as_complex_edges error no sign works", { 205 | g <- igraph::make_full_graph(3, directed = FALSE) 206 | # igraph::E(g)$sign <- -1 207 | expect_error(as_complex_edges(g)) 208 | }) 209 | 210 | test_that("as_complex_edges error wrong sign works", { 211 | g <- igraph::make_full_graph(3, directed = FALSE) 212 | igraph::E(g)$sign <- 3 213 | expect_error(as_complex_edges(g)) 214 | }) 215 | 216 | test_that("as_unsigned_2mode works for FALSE", { 217 | df <- data.frame( 218 | from = c(1, 1, 1, 2, 2), 219 | to = c("a", "b", "c", "a", "b"), 220 | sign = c(1, 1, -1, 1, -1) 221 | ) 222 | vert <- data.frame( 223 | name = c(1, 2, "a", "b", "c"), 224 | type = c(TRUE, TRUE, FALSE, FALSE, FALSE) 225 | ) 226 | g <- igraph::graph_from_data_frame(df, directed = FALSE, vertices = vert) 227 | 228 | g1 <- as_unsigned_2mode(g, primary = FALSE) 229 | el_true <- structure( 230 | list( 231 | from = c("a-pos", "b-pos", "c-neg", "a-pos", "b-neg"), 232 | to = c("1", "1", "1", "2", "2") 233 | ), 234 | class = "data.frame", 235 | row.names = c(NA, 5L) 236 | ) 237 | expect_equal(igraph::as_data_frame(g1, "edges"), el_true) 238 | }) 239 | 240 | test_that("as_unsigned_2mode works for TRUE", { 241 | df <- data.frame( 242 | from = c(1, 1, 1, 2, 2), 243 | to = c("a", "b", "c", "a", "b"), 244 | sign = c(1, 1, -1, 1, -1) 245 | ) 246 | vert <- data.frame( 247 | name = c(1, 2, "a", "b", "c"), 248 | type = c(TRUE, TRUE, FALSE, FALSE, FALSE) 249 | ) 250 | g <- igraph::graph_from_data_frame(df, directed = FALSE, vertices = vert) 251 | 252 | g1 <- as_unsigned_2mode(g, primary = TRUE) 253 | el_true <- structure( 254 | list( 255 | from = c("1-pos", "1-pos", "1-neg", "2-pos", "2-neg"), 256 | to = c("a", "b", "c", "a", "b") 257 | ), 258 | class = "data.frame", 259 | row.names = c(NA, 5L) 260 | ) 261 | expect_equal(igraph::as_data_frame(g1, "edges"), el_true) 262 | }) 263 | 264 | test_that("as_unsigned_2mode 2mode error works", { 265 | df <- data.frame( 266 | from = c(1, 1, 1, 2, 2), 267 | to = c("a", "b", "c", "a", "b"), 268 | sign = c(1, 1, -1, 1, -1) 269 | ) 270 | vert <- data.frame(name = c(1, 2, "a", "b", "c")) 271 | g <- igraph::graph_from_data_frame(df, directed = FALSE, vertices = vert) 272 | expect_error(as_unsigned_2mode(g, primary = TRUE)) 273 | }) 274 | 275 | test_that("as_signed_proj works", { 276 | df <- data.frame( 277 | from = c(1, 1, 1, 2, 2), 278 | to = c("a", "b", "c", "a", "b"), 279 | sign = c(1, 1, -1, 1, -1) 280 | ) 281 | vert <- data.frame(name = c(1, 2, "a", "b", "c"), type = c(T, T, F, F, F)) 282 | g <- igraph::graph_from_data_frame(df, directed = FALSE, vertices = vert) 283 | 284 | g1 <- as_unsigned_2mode(g, primary = TRUE) 285 | p <- igraph::bipartite_projection(g1, which = "true") 286 | p1 <- as_signed_proj(p) 287 | el <- igraph::as_data_frame(p1, "edges") 288 | el_true <- structure( 289 | list(from = "1", to = "2", type = "A"), 290 | class = "data.frame", 291 | row.names = 1L 292 | ) 293 | expect_equal(el, el_true) 294 | }) 295 | 296 | test_that("complex walks works", { 297 | g <- igraph::make_full_graph(3, directed = FALSE) 298 | igraph::E(g)$type <- c("P", "P", "N") 299 | W <- complex_walks(g, "type", 3) 300 | W_true <- structure( 301 | c( 302 | 0 + 2i, 303 | 3 + 0i, 304 | 3 + 0i, 305 | 3 + 0i, 306 | 0 + 2i, 307 | 0 + 3i, 308 | 3 + 0i, 309 | 0 + 3i, 310 | 0 + 2i 311 | ), 312 | .Dim = c(3L, 3L) 313 | ) 314 | expect_equal(W, W_true) 315 | }) 316 | -------------------------------------------------------------------------------- /src/RcppExports.cpp: -------------------------------------------------------------------------------- 1 | // Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | // Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | #include 5 | #include 6 | 7 | using namespace Rcpp; 8 | 9 | #ifdef RCPP_USE_GLOBAL_ROSTREAM 10 | Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); 11 | Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); 12 | #endif 13 | 14 | // arcDist 15 | double arcDist(NumericVector x, NumericVector y, double r); 16 | RcppExport SEXP _signnet_arcDist(SEXP xSEXP, SEXP ySEXP, SEXP rSEXP) { 17 | BEGIN_RCPP 18 | Rcpp::RObject rcpp_result_gen; 19 | Rcpp::RNGScope rcpp_rngScope_gen; 20 | Rcpp::traits::input_parameter< NumericVector >::type x(xSEXP); 21 | Rcpp::traits::input_parameter< NumericVector >::type y(ySEXP); 22 | Rcpp::traits::input_parameter< double >::type r(rSEXP); 23 | rcpp_result_gen = Rcpp::wrap(arcDist(x, y, r)); 24 | return rcpp_result_gen; 25 | END_RCPP 26 | } 27 | // arcDistMat 28 | NumericMatrix arcDistMat(NumericMatrix X, double r); 29 | RcppExport SEXP _signnet_arcDistMat(SEXP XSEXP, SEXP rSEXP) { 30 | BEGIN_RCPP 31 | Rcpp::RObject rcpp_result_gen; 32 | Rcpp::RNGScope rcpp_rngScope_gen; 33 | Rcpp::traits::input_parameter< NumericMatrix >::type X(XSEXP); 34 | Rcpp::traits::input_parameter< double >::type r(rSEXP); 35 | rcpp_result_gen = Rcpp::wrap(arcDistMat(X, r)); 36 | return rcpp_result_gen; 37 | END_RCPP 38 | } 39 | // cxmatmul 40 | arma::cx_mat cxmatmul(arma::cx_mat A, arma::cx_mat B); 41 | RcppExport SEXP _signnet_cxmatmul(SEXP ASEXP, SEXP BSEXP) { 42 | BEGIN_RCPP 43 | Rcpp::RObject rcpp_result_gen; 44 | Rcpp::RNGScope rcpp_rngScope_gen; 45 | Rcpp::traits::input_parameter< arma::cx_mat >::type A(ASEXP); 46 | Rcpp::traits::input_parameter< arma::cx_mat >::type B(BSEXP); 47 | rcpp_result_gen = Rcpp::wrap(cxmatmul(A, B)); 48 | return rcpp_result_gen; 49 | END_RCPP 50 | } 51 | // blockCriterion 52 | double blockCriterion(arma::sp_mat A, IntegerVector clu, double alpha); 53 | RcppExport SEXP _signnet_blockCriterion(SEXP ASEXP, SEXP cluSEXP, SEXP alphaSEXP) { 54 | BEGIN_RCPP 55 | Rcpp::RObject rcpp_result_gen; 56 | Rcpp::RNGScope rcpp_rngScope_gen; 57 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 58 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 59 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 60 | rcpp_result_gen = Rcpp::wrap(blockCriterion(A, clu, alpha)); 61 | return rcpp_result_gen; 62 | END_RCPP 63 | } 64 | // critUpdate 65 | double critUpdate(arma::sp_mat A, int v, int from, int to, IntegerVector clu, double alpha); 66 | RcppExport SEXP _signnet_critUpdate(SEXP ASEXP, SEXP vSEXP, SEXP fromSEXP, SEXP toSEXP, SEXP cluSEXP, SEXP alphaSEXP) { 67 | BEGIN_RCPP 68 | Rcpp::RObject rcpp_result_gen; 69 | Rcpp::RNGScope rcpp_rngScope_gen; 70 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 71 | Rcpp::traits::input_parameter< int >::type v(vSEXP); 72 | Rcpp::traits::input_parameter< int >::type from(fromSEXP); 73 | Rcpp::traits::input_parameter< int >::type to(toSEXP); 74 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 75 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 76 | rcpp_result_gen = Rcpp::wrap(critUpdate(A, v, from, to, clu, alpha)); 77 | return rcpp_result_gen; 78 | END_RCPP 79 | } 80 | // optimBlocks1 81 | List optimBlocks1(arma::sp_mat A, IntegerVector clu, int k, double alpha); 82 | RcppExport SEXP _signnet_optimBlocks1(SEXP ASEXP, SEXP cluSEXP, SEXP kSEXP, SEXP alphaSEXP) { 83 | BEGIN_RCPP 84 | Rcpp::RObject rcpp_result_gen; 85 | Rcpp::RNGScope rcpp_rngScope_gen; 86 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 87 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 88 | Rcpp::traits::input_parameter< int >::type k(kSEXP); 89 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 90 | rcpp_result_gen = Rcpp::wrap(optimBlocks1(A, clu, k, alpha)); 91 | return rcpp_result_gen; 92 | END_RCPP 93 | } 94 | // blockCriterion1 95 | double blockCriterion1(IntegerVector clu, arma::sp_mat A, double alpha, int k); 96 | RcppExport SEXP _signnet_blockCriterion1(SEXP cluSEXP, SEXP ASEXP, SEXP alphaSEXP, SEXP kSEXP) { 97 | BEGIN_RCPP 98 | Rcpp::RObject rcpp_result_gen; 99 | Rcpp::RNGScope rcpp_rngScope_gen; 100 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 101 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 102 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 103 | Rcpp::traits::input_parameter< int >::type k(kSEXP); 104 | rcpp_result_gen = Rcpp::wrap(blockCriterion1(clu, A, alpha, k)); 105 | return rcpp_result_gen; 106 | END_RCPP 107 | } 108 | // blockCriterionS 109 | double blockCriterionS(arma::sp_mat A, IntegerVector clu, double alpha, IntegerMatrix sgrp); 110 | RcppExport SEXP _signnet_blockCriterionS(SEXP ASEXP, SEXP cluSEXP, SEXP alphaSEXP, SEXP sgrpSEXP) { 111 | BEGIN_RCPP 112 | Rcpp::RObject rcpp_result_gen; 113 | Rcpp::RNGScope rcpp_rngScope_gen; 114 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 115 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 116 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 117 | Rcpp::traits::input_parameter< IntegerMatrix >::type sgrp(sgrpSEXP); 118 | rcpp_result_gen = Rcpp::wrap(blockCriterionS(A, clu, alpha, sgrp)); 119 | return rcpp_result_gen; 120 | END_RCPP 121 | } 122 | // critUpdateS 123 | double critUpdateS(arma::sp_mat A, int v, int from, int to, IntegerVector clu, double alpha, IntegerMatrix sgrp); 124 | RcppExport SEXP _signnet_critUpdateS(SEXP ASEXP, SEXP vSEXP, SEXP fromSEXP, SEXP toSEXP, SEXP cluSEXP, SEXP alphaSEXP, SEXP sgrpSEXP) { 125 | BEGIN_RCPP 126 | Rcpp::RObject rcpp_result_gen; 127 | Rcpp::RNGScope rcpp_rngScope_gen; 128 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 129 | Rcpp::traits::input_parameter< int >::type v(vSEXP); 130 | Rcpp::traits::input_parameter< int >::type from(fromSEXP); 131 | Rcpp::traits::input_parameter< int >::type to(toSEXP); 132 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 133 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 134 | Rcpp::traits::input_parameter< IntegerMatrix >::type sgrp(sgrpSEXP); 135 | rcpp_result_gen = Rcpp::wrap(critUpdateS(A, v, from, to, clu, alpha, sgrp)); 136 | return rcpp_result_gen; 137 | END_RCPP 138 | } 139 | // optimBlocksSimS 140 | List optimBlocksSimS(arma::sp_mat A, IntegerVector clu, IntegerMatrix sgrp, double alpha); 141 | RcppExport SEXP _signnet_optimBlocksSimS(SEXP ASEXP, SEXP cluSEXP, SEXP sgrpSEXP, SEXP alphaSEXP) { 142 | BEGIN_RCPP 143 | Rcpp::RObject rcpp_result_gen; 144 | Rcpp::RNGScope rcpp_rngScope_gen; 145 | Rcpp::traits::input_parameter< arma::sp_mat >::type A(ASEXP); 146 | Rcpp::traits::input_parameter< IntegerVector >::type clu(cluSEXP); 147 | Rcpp::traits::input_parameter< IntegerMatrix >::type sgrp(sgrpSEXP); 148 | Rcpp::traits::input_parameter< double >::type alpha(alphaSEXP); 149 | rcpp_result_gen = Rcpp::wrap(optimBlocksSimS(A, clu, sgrp, alpha)); 150 | return rcpp_result_gen; 151 | END_RCPP 152 | } 153 | // triadCensusSign 154 | IntegerVector triadCensusSign(NumericMatrix A, int n); 155 | RcppExport SEXP _signnet_triadCensusSign(SEXP ASEXP, SEXP nSEXP) { 156 | BEGIN_RCPP 157 | Rcpp::RObject rcpp_result_gen; 158 | Rcpp::RNGScope rcpp_rngScope_gen; 159 | Rcpp::traits::input_parameter< NumericMatrix >::type A(ASEXP); 160 | Rcpp::traits::input_parameter< int >::type n(nSEXP); 161 | rcpp_result_gen = Rcpp::wrap(triadCensusSign(A, n)); 162 | return rcpp_result_gen; 163 | END_RCPP 164 | } 165 | // triadCensusSign1 166 | DoubleVector triadCensusSign1(const arma::sp_mat& A, List adj, int n); 167 | RcppExport SEXP _signnet_triadCensusSign1(SEXP ASEXP, SEXP adjSEXP, SEXP nSEXP) { 168 | BEGIN_RCPP 169 | Rcpp::RObject rcpp_result_gen; 170 | Rcpp::RNGScope rcpp_rngScope_gen; 171 | Rcpp::traits::input_parameter< const arma::sp_mat& >::type A(ASEXP); 172 | Rcpp::traits::input_parameter< List >::type adj(adjSEXP); 173 | Rcpp::traits::input_parameter< int >::type n(nSEXP); 174 | rcpp_result_gen = Rcpp::wrap(triadCensusSign1(A, adj, n)); 175 | return rcpp_result_gen; 176 | END_RCPP 177 | } 178 | 179 | static const R_CallMethodDef CallEntries[] = { 180 | {"_signnet_arcDist", (DL_FUNC) &_signnet_arcDist, 3}, 181 | {"_signnet_arcDistMat", (DL_FUNC) &_signnet_arcDistMat, 2}, 182 | {"_signnet_cxmatmul", (DL_FUNC) &_signnet_cxmatmul, 2}, 183 | {"_signnet_blockCriterion", (DL_FUNC) &_signnet_blockCriterion, 3}, 184 | {"_signnet_critUpdate", (DL_FUNC) &_signnet_critUpdate, 6}, 185 | {"_signnet_optimBlocks1", (DL_FUNC) &_signnet_optimBlocks1, 4}, 186 | {"_signnet_blockCriterion1", (DL_FUNC) &_signnet_blockCriterion1, 4}, 187 | {"_signnet_blockCriterionS", (DL_FUNC) &_signnet_blockCriterionS, 4}, 188 | {"_signnet_critUpdateS", (DL_FUNC) &_signnet_critUpdateS, 7}, 189 | {"_signnet_optimBlocksSimS", (DL_FUNC) &_signnet_optimBlocksSimS, 4}, 190 | {"_signnet_triadCensusSign", (DL_FUNC) &_signnet_triadCensusSign, 2}, 191 | {"_signnet_triadCensusSign1", (DL_FUNC) &_signnet_triadCensusSign1, 3}, 192 | {NULL, NULL, 0} 193 | }; 194 | 195 | RcppExport void R_init_signnet(DllInfo *dll) { 196 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 197 | R_useDynamicSymbols(dll, FALSE); 198 | } 199 | -------------------------------------------------------------------------------- /methodshub.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: 3 | html: 4 | embed-resources: true 5 | gfm: default 6 | --- 7 | 8 | # signnet - Methods to Analyse Signed Networks 9 | 28 | 29 | ## Description 30 | 38 | 39 | Methods for the analysis of signed networks. This includes 40 | several measures for structural balance as introduced by Cartwright 41 | and Harary (1956) [](https://doi.org/10.1037/h0046049), blockmodeling algorithms 42 | from Doreian (2008) [](https://doi.org/10.1016/j.socnet.2008.03.005), various 43 | centrality indices, and projections of signed two-mode networks 44 | introduced by Schoch (2020) [](https://doi.org/10.1080/0022250X.2019.1711376). 45 | 46 | 47 | ## Keywords 48 | 49 | 50 | 51 | * Network Analysis 52 | * Signed Networks 53 | * Blockmodelling 54 | 55 | ## Use Cases 56 | 61 | 62 | Signet networks offer valuable insights into the dynamics of relationships in social science. By modeling networks with positive and negative ties, such as trust and distrust or alliance and rivalry, researchers can explore the nuanced interplay of influence and group structures within societies. Signed centrality helps identify key actors who exert positive influence or act as polarizing figures, while blockmodeling reveals cohesive subgroups and structural roles, such as brokers or marginalized factions, within the network. These tools are instrumental in studying phenomena like political polarization, social cohesion, and conflict resolution, offering a framework for analyzing both cooperative and antagonistic interactions in complex social systems. 63 | 64 | ## Input Data 65 | 70 | 71 | `signnet` accepts network data in `igraph` format and includes two example data sets that can be used to test the methods. The sign information is stored in a node attribute `sign`. 72 | 73 | ## Output Data 74 | 78 | 79 | ## Environment Setup 80 | 84 | 85 | With R installed: 86 | 87 | ```r 88 | install.packages("signnet") 89 | ``` 90 | 91 | ## How to Use 92 | 96 | 97 | The package comes with a set of vignettes that can be found [here](https://schochastics.github.io/signnet/). 98 | 99 | ## Technical Details 100 | 108 | 109 | See the official [CRAN page](https://doi.org/10.32614/CRAN.package.signnet) for further information about technical details. 110 | 111 | 112 | 116 | 117 | 118 | 122 | 123 | 124 | 128 | 129 | ## Contact Details 130 | 133 | 134 | Maintainer: David Schoch 135 | 136 | Issue Tracker: [https://github.com/schochastics/signnet/issues](https://github.com/schochastics/signnet/issues) 137 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # -------------------------------------------- 2 | # CITATION file created with {cffr} R package 3 | # See also: https://docs.ropensci.org/cffr/ 4 | # -------------------------------------------- 5 | 6 | cff-version: 1.2.0 7 | message: 'To cite package "signnet" in publications use:' 8 | type: software 9 | license: MIT 10 | title: 'signnet: Methods to Analyse Signed Networks' 11 | version: 1.0.4 12 | doi: 10.21105/joss.04987 13 | identifiers: 14 | - type: doi 15 | value: 10.32614/CRAN.package.signnet 16 | abstract: Methods for the analysis of signed networks. This includes several measures 17 | for structural balance as introduced by Cartwright and Harary (1956) , 18 | blockmodeling algorithms from Doreian (2008) , 19 | various centrality indices, and projections of signed two-mode networks introduced 20 | by Schoch (2020) . 21 | authors: 22 | - family-names: Schoch 23 | given-names: David 24 | email: david@schochastics.net 25 | orcid: https://orcid.org/0000-0003-2952-4812 26 | preferred-citation: 27 | type: article 28 | title: 'signnet: An R package for analyzing signed networks' 29 | authors: 30 | - family-names: Schoch 31 | given-names: David 32 | email: david@schochastics.net 33 | orcid: https://orcid.org/0000-0003-2952-4812 34 | doi: 10.21105/joss.04987 35 | url: https://doi.org/10.21105/joss.04987 36 | year: '2023' 37 | publisher: 38 | name: The Open Journal 39 | volume: '8' 40 | issue: '81' 41 | journal: Journal of Open Source Software 42 | start: '4987' 43 | repository: https://CRAN.R-project.org/package=signnet 44 | repository-code: https://github.com/schochastics/signnet 45 | url: https://schochastics.github.io/signnet/ 46 | contact: 47 | - family-names: Schoch 48 | given-names: David 49 | email: david@schochastics.net 50 | orcid: https://orcid.org/0000-0003-2952-4812 51 | keywords: 52 | - network-analysis 53 | - signed-networks 54 | - sna 55 | references: 56 | - type: software 57 | title: 'R: A Language and Environment for Statistical Computing' 58 | notes: Depends 59 | url: https://www.R-project.org/ 60 | authors: 61 | - name: R Core Team 62 | institution: 63 | name: R Foundation for Statistical Computing 64 | address: Vienna, Austria 65 | year: '2024' 66 | version: '>= 3.2.0' 67 | - type: software 68 | title: igraph 69 | abstract: 'igraph: Network Analysis and Visualization' 70 | notes: Imports 71 | url: https://r.igraph.org/ 72 | repository: https://CRAN.R-project.org/package=igraph 73 | authors: 74 | - family-names: Csárdi 75 | given-names: Gábor 76 | email: csardi.gabor@gmail.com 77 | orcid: https://orcid.org/0000-0001-7098-9676 78 | - family-names: Nepusz 79 | given-names: Tamás 80 | email: ntamas@gmail.com 81 | orcid: https://orcid.org/0000-0002-1451-338X 82 | - family-names: Traag 83 | given-names: Vincent 84 | orcid: https://orcid.org/0000-0003-3170-3879 85 | - family-names: Horvát 86 | given-names: Szabolcs 87 | email: szhorvat@gmail.com 88 | orcid: https://orcid.org/0000-0002-3100-523X 89 | - family-names: Zanini 90 | given-names: Fabio 91 | email: fabio.zanini@unsw.edu.au 92 | orcid: https://orcid.org/0000-0001-7097-8539 93 | - family-names: Noom 94 | given-names: Daniel 95 | - family-names: Müller 96 | given-names: Kirill 97 | email: kirill@cynkra.com 98 | orcid: https://orcid.org/0000-0002-1416-3412 99 | year: '2024' 100 | doi: 10.32614/CRAN.package.igraph 101 | - type: software 102 | title: Matrix 103 | abstract: 'Matrix: Sparse and Dense Matrix Classes and Methods' 104 | notes: Imports 105 | url: https://R-forge.R-project.org/tracker/?atid=294&group_id=61 106 | repository: https://CRAN.R-project.org/package=Matrix 107 | authors: 108 | - family-names: Bates 109 | given-names: Douglas 110 | orcid: https://orcid.org/0000-0001-8316-9503 111 | - family-names: Maechler 112 | given-names: Martin 113 | email: mmaechler+Matrix@gmail.com 114 | orcid: https://orcid.org/0000-0002-8685-9910 115 | - family-names: Jagan 116 | given-names: Mikael 117 | orcid: https://orcid.org/0000-0002-3542-2938 118 | year: '2024' 119 | doi: 10.32614/CRAN.package.Matrix 120 | - type: software 121 | title: Rcpp 122 | abstract: 'Rcpp: Seamless R and C++ Integration' 123 | notes: Imports 124 | url: https://www.rcpp.org 125 | repository: https://CRAN.R-project.org/package=Rcpp 126 | authors: 127 | - family-names: Eddelbuettel 128 | given-names: Dirk 129 | email: edd@debian.org 130 | orcid: https://orcid.org/0000-0001-6419-907X 131 | - family-names: Francois 132 | given-names: Romain 133 | orcid: https://orcid.org/0000-0002-2444-4226 134 | - family-names: Allaire 135 | given-names: JJ 136 | orcid: https://orcid.org/0000-0003-0174-9868 137 | - family-names: Ushey 138 | given-names: Kevin 139 | orcid: https://orcid.org/0000-0003-2880-7407 140 | - family-names: Kou 141 | given-names: Qiang 142 | orcid: https://orcid.org/0000-0001-6786-5453 143 | - family-names: Russell 144 | given-names: Nathan 145 | - family-names: Ucar 146 | given-names: Iñaki 147 | orcid: https://orcid.org/0000-0001-6403-5550 148 | - family-names: Bates 149 | given-names: Doug 150 | orcid: https://orcid.org/0000-0001-8316-9503 151 | - family-names: Chambers 152 | given-names: John 153 | year: '2024' 154 | doi: 10.32614/CRAN.package.Rcpp 155 | - type: software 156 | title: ggplot2 157 | abstract: 'ggplot2: Create Elegant Data Visualisations Using the Grammar of Graphics' 158 | notes: Suggests 159 | url: https://ggplot2.tidyverse.org 160 | repository: https://CRAN.R-project.org/package=ggplot2 161 | authors: 162 | - family-names: Wickham 163 | given-names: Hadley 164 | email: hadley@posit.co 165 | orcid: https://orcid.org/0000-0003-4757-117X 166 | - family-names: Chang 167 | given-names: Winston 168 | orcid: https://orcid.org/0000-0002-1576-2126 169 | - family-names: Henry 170 | given-names: Lionel 171 | - family-names: Pedersen 172 | given-names: Thomas Lin 173 | email: thomas.pedersen@posit.co 174 | orcid: https://orcid.org/0000-0002-5147-4711 175 | - family-names: Takahashi 176 | given-names: Kohske 177 | - family-names: Wilke 178 | given-names: Claus 179 | orcid: https://orcid.org/0000-0002-7470-9261 180 | - family-names: Woo 181 | given-names: Kara 182 | orcid: https://orcid.org/0000-0002-5125-4188 183 | - family-names: Yutani 184 | given-names: Hiroaki 185 | orcid: https://orcid.org/0000-0002-3385-7233 186 | - family-names: Dunnington 187 | given-names: Dewey 188 | orcid: https://orcid.org/0000-0002-9415-4582 189 | - family-names: Brand 190 | given-names: Teun 191 | name-particle: van den 192 | orcid: https://orcid.org/0000-0002-9335-7468 193 | year: '2024' 194 | doi: 10.32614/CRAN.package.ggplot2 195 | - type: software 196 | title: knitr 197 | abstract: 'knitr: A General-Purpose Package for Dynamic Report Generation in R' 198 | notes: Suggests 199 | url: https://yihui.org/knitr/ 200 | repository: https://CRAN.R-project.org/package=knitr 201 | authors: 202 | - family-names: Xie 203 | given-names: Yihui 204 | email: xie@yihui.name 205 | orcid: https://orcid.org/0000-0003-0645-5666 206 | year: '2024' 207 | doi: 10.32614/CRAN.package.knitr 208 | - type: software 209 | title: rmarkdown 210 | abstract: 'rmarkdown: Dynamic Documents for R' 211 | notes: Suggests 212 | url: https://pkgs.rstudio.com/rmarkdown/ 213 | repository: https://CRAN.R-project.org/package=rmarkdown 214 | authors: 215 | - family-names: Allaire 216 | given-names: JJ 217 | email: jj@posit.co 218 | - family-names: Xie 219 | given-names: Yihui 220 | email: xie@yihui.name 221 | orcid: https://orcid.org/0000-0003-0645-5666 222 | - family-names: Dervieux 223 | given-names: Christophe 224 | email: cderv@posit.co 225 | orcid: https://orcid.org/0000-0003-4474-2498 226 | - family-names: McPherson 227 | given-names: Jonathan 228 | email: jonathan@posit.co 229 | - family-names: Luraschi 230 | given-names: Javier 231 | - family-names: Ushey 232 | given-names: Kevin 233 | email: kevin@posit.co 234 | - family-names: Atkins 235 | given-names: Aron 236 | email: aron@posit.co 237 | - family-names: Wickham 238 | given-names: Hadley 239 | email: hadley@posit.co 240 | - family-names: Cheng 241 | given-names: Joe 242 | email: joe@posit.co 243 | - family-names: Chang 244 | given-names: Winston 245 | email: winston@posit.co 246 | - family-names: Iannone 247 | given-names: Richard 248 | email: rich@posit.co 249 | orcid: https://orcid.org/0000-0003-3925-190X 250 | year: '2024' 251 | doi: 10.32614/CRAN.package.rmarkdown 252 | - type: software 253 | title: testthat 254 | abstract: 'testthat: Unit Testing for R' 255 | notes: Suggests 256 | url: https://testthat.r-lib.org 257 | repository: https://CRAN.R-project.org/package=testthat 258 | authors: 259 | - family-names: Wickham 260 | given-names: Hadley 261 | email: hadley@posit.co 262 | year: '2024' 263 | doi: 10.32614/CRAN.package.testthat 264 | version: '>= 2.1.0' 265 | - type: software 266 | title: RcppArmadillo 267 | abstract: 'RcppArmadillo: ''Rcpp'' Integration for the ''Armadillo'' Templated Linear 268 | Algebra Library' 269 | notes: LinkingTo 270 | url: https://dirk.eddelbuettel.com/code/rcpp.armadillo.html 271 | repository: https://CRAN.R-project.org/package=RcppArmadillo 272 | authors: 273 | - family-names: Eddelbuettel 274 | given-names: Dirk 275 | email: edd@debian.org 276 | orcid: https://orcid.org/0000-0001-6419-907X 277 | - family-names: Francois 278 | given-names: Romain 279 | orcid: https://orcid.org/0000-0002-2444-4226 280 | - family-names: Bates 281 | given-names: Doug 282 | orcid: https://orcid.org/0000-0001-8316-9503 283 | - family-names: Ni 284 | given-names: Binxiang 285 | - family-names: Sanderson 286 | given-names: Conrad 287 | orcid: https://orcid.org/0000-0002-0049-4501 288 | year: '2024' 289 | doi: 10.32614/CRAN.package.RcppArmadillo 290 | 291 | --------------------------------------------------------------------------------