Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ Two further points of style:
- Pick a word narrow enough to own the family. `regularity` is preferred over `similarity` because the latter is broad enough to be overrun later, and because generic similarities (`to_cosine()`, `to_correlation()`) belong to `{manynet}` and are consumed here through `distance =` and `cluster_*()`, so they would never live in this family anyway.
- The dispatching function should name the method in its `snet_info()` message by interpolation, e.g. `manynet::snet_info("...using {.fn regularity_{regularity}}.")`. This surfaces the convention to users at run time, and makes it obvious if the argument and the prefix ever drift apart.

One known exception: `node_in_equivalence()`'s `motif =` argument is fed by `node_x_*()` functions rather than `motif_*()` ones. Motifs are one of the four core families above and cannot be renamed to suit this rule, so leave that as it is.
One known exception: `node_in_equivalence()`'s `motif =` argument is fed by `node_x_*()` functions rather than `motif_*()` ones.
Motifs are one of the four core families above and cannot be renamed to suit this rule, so leave that as it is.

### Naming within the membership family

Expand Down Expand Up @@ -255,6 +256,12 @@ a multiplex, a multilevel and a longitudinal network, and decide each case delib
([R/netrics-utils.R](../R/netrics-utils.R)), which drops to the positive ties and says so.
Do not simply drop the attribute: that reads a negative tie as a path of length one,
when a negative tie is hostility rather than a channel along which cohesion travels.
Where the measure instead counts a tie however it is signed, as a census does,
call `.to_unsigned()` in the same file, which keeps every tie and reads each by
its magnitude. A tie-level function takes this branch even where it measures
distance, since `.to_positive()` drops ties and a tie measure must hold one
value per tie; `tie_by_betweenness()` is the case, and says so in its
`@section Signed networks:`.
- **Multiplex.** Take one layer at a time with `manynet::to_uniplex()`.
That drops nodes holding none of the retained ties, so results of different lengths
would otherwise be recycled against each other; `uniplex_degree()`
Expand Down
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: netrics
Title: Many Marks, Measures, Memberships, and Motifs for Networks
Version: 1.0.1
Version: 1.0.2
Description: Many tools for calculating network, node, or tie
marks, measures, motifs and memberships of many different types of networks.
Marks identify structural positions, measures quantify network properties,
Expand All @@ -15,7 +15,7 @@ Encoding: UTF-8
LazyData: true
Depends:
R (>= 4.1.0),
manynet (>= 2.2.3)
manynet (>= 2.3.1)
Imports:
dplyr,
igraph (>= 2.1.0)
Expand Down
42 changes: 42 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
# netrics 1.0.2

## Package

- Fixed some warning tests related to changes in snet output in manynet
- Raised `{manynet}` floor to 2.3.1, retiring `to_ties()` and `net_waves()` shims

## Marks

- Fixed `node_is_fold()` erroring on signed networks (closes #25)
- Fixed `node_is_independent()` and `node_is_core()` erroring on multilevel networks (closes #26)
- Fixed `tie_is_transitive()`, `tie_is_triplet()` and `tie_is_cyclical()` erroring on signed networks (closes #25)
- Fixed `tie_is_max()` and `tie_is_min()` to accept a numeric vector, and to name what they mark
- Fixed `tie_is_path()` to name `from` and `to` where either is missing (closes #26)

## Measures

- Fixed distance measures erroring on signed networks to use only positive ties (closes #25)
- `node_by_closeness()`, `node_by_harmonic()`, `node_by_reach()`, `node_by_decay()`,
`node_by_integration()`, `node_by_radiality()`, `node_by_eccentricity()`, `node_by_vitality()`,
`node_by_betweenness()`, `node_by_induced()`, `node_x_path()`
- `net_by_closeness()`, `net_by_betweenness()`, `net_by_connectedness()`, `net_by_reach()`,
`net_by_harmonic()`, `net_by_decay()`, `net_by_integration()`
- `mode_by_closeness()`, `mode_by_betweenness()`
- Fixed eigenvector measures erroring or warning on signed networks, which now read each tie by its magnitude (closes #25)
- Fixed `tie_by_betweenness()` erroring on signed networks, which reads each tie by its magnitude so that every tie keeps a value (closes #25)
- Fixed `net_by_modularity()` erroring on signed networks (closes #25)
- Fixed eigenvector and structural hole measures erroring on multilevel networks, which are now measured whole rather than projected (closes #26)
- Fixed `net_by_core()` and `net_by_factions()` to stop with a clear message on a multilevel network, since a `create_*()` ideal covers one layer (closes #26)
- Fixed `net_by_spatial()` to name a non-numeric attribute (closes #26)
- Added `limit=` to `net_by_strength()` and `net_by_toughness()`, which take a minimum over 2^n subsets and so hang above about twenty nodes or ties (closes #34)
- Reduced run time of fragmentation examples by using `fict_greys` instead of `fict_marvel`

## Memberships

- Fixed `node_in_community()` erroring on signed networks, which now considers only `node_in_spinglass()` (closes #25)
- Fixed `node_in_core()` and `node_by_core()` returning two values per node on multilevel networks (closes #26)

## Methods

- Fixed `coreness_rich()` reading a multilevel network as a plain two-mode one (closes #26)

# netrics 1.0.1

## Methods
Expand Down
16 changes: 15 additions & 1 deletion R/mark_nodes.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
#' - `node_is_neighbor()` marks nodes that are neighbours of a given node.
#' @template param_data
#' @template node_mark
#' @section Signed networks:
#' `node_is_fold()` reads a tie as a distance, and a negative tie is
#' hostility rather than a channel along which cohesion travels, so where the
#' network is signed it considers only the positive ties.
#' @section Multilevel networks:
#' A multilevel network reports itself as two-mode, but holds ties within a
#' mode as well as between them, so it cannot be projected onto one mode.
#' `node_is_independent()` therefore marks a multilevel network whole, as
#' [net_by_independence()] does.
NULL

#' @rdname mark_nodes
Expand All @@ -31,7 +40,11 @@ NULL
#' @export
node_is_independent <- function(.data){
.data <- manynet::expect_nodes(.data)
if(manynet::is_twomode(.data)){
# A multilevel network reports itself as two-mode, but holds ties within a
# mode as well as between them, so it cannot be projected. Its matrix is
# already square over every node, so it takes the one-mode branch, as
# `net_by_independence()` does.
if(manynet::is_twomode(.data) && !.is_multilevel(.data)){
samp <- igraph::largest_ivs(manynet::to_mode1(.data))
if(manynet::is_labelled(.data)){
out <- manynet::node_names(.data) %in%
Expand Down Expand Up @@ -94,6 +107,7 @@ node_is_cutpoint <- function(.data){
#' @export
node_is_fold <- function(.data){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
mult_tri <- igraph::count_triangles(.data)>1
tris <- igraph::triangles(.data)
tris <- matrix(tris, length(tris)/3, 3, byrow = TRUE)
Expand Down
32 changes: 31 additions & 1 deletion R/mark_ties.R
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ tie_is_bridge <- function(.data){
#' @export
tie_is_path <- function(.data, from, to, all_paths = FALSE){
.data <- manynet::expect_ties(.data)
# a path runs between two named nodes, so neither endpoint has a default
if(missing(from) || missing(to))
manynet::snet_abort("{.fn tie_is_path} needs both {.arg from} and",
"{.arg to}, the nodes the path runs between,",
"e.g. {.code tie_is_path(.data, from = 1, to = 7)}.")
out <- igraph::all_shortest_paths(.data, from = from, to = to,
mode = "out")$epath
if(all_paths){
Expand Down Expand Up @@ -136,6 +141,11 @@ tie_is_reciprocated <- function(.data){
#' @template tie_mark
#' @family cohesion
#' @name mark_triangles
#' @section Signed networks:
#' These marks ask only whether a two-path exists, as a census does, so a tie
#' counts however it is signed. Where the network is signed, each tie is
#' therefore read by its magnitude, and every tie keeps its place in the
#' returned vector.
NULL

#' @rdname mark_triangles
Expand Down Expand Up @@ -166,6 +176,8 @@ tie_is_triangular <- function(.data){
#' @export
tie_is_transitive <- function(.data){
.data <- manynet::expect_ties(.data)
# once, outside the loop, since `.to_unsigned()` reports what it did
.data <- .to_unsigned(.data)
nodes <- manynet::as_edgelist(manynet::to_unnamed(.data))
out <- vapply(seq_len(manynet::net_ties(.data)), function(x){
igraph::distances(manynet::delete_ties(.data, x),
Expand All @@ -182,6 +194,7 @@ tie_is_transitive <- function(.data){
#' @export
tie_is_triplet <- function(.data){
.data <- manynet::expect_ties(.data)
.data <- .to_unsigned(.data)
nodes <- manynet::as_edgelist(manynet::to_unnamed(.data))
trans <- tie_is_transitive(.data)
altpath <- unlist(lapply(which(trans), function(x){
Expand All @@ -203,6 +216,8 @@ tie_is_triplet <- function(.data){
#' @export
tie_is_cyclical <- function(.data){
.data <- manynet::expect_ties(.data)
# once, outside the loop, since `.to_unsigned()` reports what it did
.data <- .to_unsigned(.data)
out <- vapply(seq_len(manynet::net_ties(.data)), function(x){
nodes <- manynet::as_edgelist(manynet::to_unnamed(.data))[x,]
igraph::distances(manynet::delete_ties(.data, x),
Expand Down Expand Up @@ -349,12 +364,26 @@ tie_is_random <- function(.data, select = 1){
make_tie_mark(out, .data)
}

# These two mark a vector of tie values, not a network. Passing a network
# reaches `as.numeric()`, which reports a list that cannot be coerced rather
# than the argument that was wanted.
.check_tie_measure <- function(tie_measure, fun){
if(inherits(tie_measure, "tie_measure") || is.numeric(tie_measure))
return(tie_measure)
manynet::snet_abort("{.fn {fun}} marks a tie measure or a numeric vector,",
"not a {.cls {class(tie_measure)[1]}}.",
"Measure the ties first, e.g.",
"{.code {fun}(tie_by_betweenness(.data))}.")
}

#' @rdname mark_select_tie
#' @param tie_measure An object created by a `tie_` measure.
#' @param tie_measure An object created by a `tie_` measure,
#' or a plain numeric vector holding one value per tie.
#' @examples
#' tie_is_max(tie_by_betweenness(ison_brandes))
#' @export
tie_is_max <- function(tie_measure){
tie_measure <- .check_tie_measure(tie_measure, "tie_is_max")
out <- as.numeric(tie_measure) == max(as.numeric(tie_measure))
class(out) <- c("tie_mark", class(out))
out
Expand All @@ -365,6 +394,7 @@ tie_is_max <- function(tie_measure){
#' tie_is_min(tie_by_betweenness(ison_brandes))
#' @export
tie_is_min <- function(tie_measure){
tie_measure <- .check_tie_measure(tie_measure, "tie_is_min")
out <- as.numeric(tie_measure) == min(as.numeric(tie_measure))
class(out) <- c("tie_mark", class(out))
out
Expand Down
27 changes: 27 additions & 0 deletions R/measure_centrality_between.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
#' @template node_measure
#' @param cutoff The maximum path length to consider when calculating betweenness.
#' If negative or NULL (the default), there's no limit to the path lengths considered.
#' @section Signed networks:
#' These measures read a tie as a distance, and a negative tie is hostility
#' rather than a channel along which cohesion travels.
#' Where the network is signed, they therefore consider only the positive
#' ties, and say so.
#' Use [manynet::to_unsigned()] first to control this yourself.

NULL

#' @rdname measure_central_between
Expand Down Expand Up @@ -75,6 +82,8 @@ node_by_betweenness <- function(.data, normalized = TRUE,
cutoff = NULL){

.data <- manynet::expect_nodes(.data)

.data <- .to_positive(.data)
weights <- `if`(manynet::is_weighted(.data),
manynet::tie_weights(.data), NA)
graph <- manynet::as_igraph(.data)
Expand Down Expand Up @@ -129,6 +138,7 @@ node_by_betweenness <- function(.data, normalized = TRUE,
node_by_induced <- function(.data, normalized = TRUE,
cutoff = NULL){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
endog <- sum(node_by_betweenness(.data, normalized = normalized, cutoff = cutoff),
na.rm = TRUE)
exog <- vapply(seq.int(manynet::net_nodes(.data)),
Expand Down Expand Up @@ -220,6 +230,13 @@ node_by_stress <- function(.data, normalized = TRUE){
#' @family betweenness
#' @family centrality
#' @template tie_measure
#' @section Signed networks:
#' A tie measure holds one value per tie, so `tie_by_betweenness()` cannot
#' drop the negative ties as the distance measures at the node and network
#' level do. It reads each tie by its magnitude instead, which is what
#' [node_by_betweenness()] in effect does for a network whose ties are signed
#' but not weighted. Use [manynet::to_unsigned()] first to control this
#' yourself.
NULL

#' @rdname measure_central_tie_between
Expand Down Expand Up @@ -247,6 +264,7 @@ NULL
#' @export
tie_by_betweenness <- function(.data, normalized = TRUE){
.data <- manynet::expect_ties(.data)
.data <- .to_unsigned(.data)
.data <- manynet::as_igraph(.data)
eddies <- manynet::as_edgelist(.data)
eddies <- paste(eddies[["from"]], eddies[["to"]], sep = "-")
Expand Down Expand Up @@ -311,6 +329,13 @@ tie_by_betweenness <- function(.data, normalized = TRUE){
#' against the other nodes of its own mode. Since a two-mode incidence
#' structure gives these no distinct "out" counterpart,
#' `mode_by_betweenness()` accepts only `"all"` and `"in"`.
#' @section Signed networks:
#' These measures read a tie as a distance, and a negative tie is hostility
#' rather than a channel along which cohesion travels.
#' Where the network is signed, they therefore consider only the positive
#' ties, and say so.
#' Use [manynet::to_unsigned()] first to control this yourself.

NULL

#' @rdname measure_centralisation_between
Expand All @@ -319,6 +344,7 @@ NULL
#' @export
net_by_betweenness <- function(.data, normalized = TRUE) {
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
graph <- manynet::as_igraph(.data)

if (manynet::is_twomode(.data)) {
Expand Down Expand Up @@ -346,6 +372,7 @@ net_by_betweenness <- function(.data, normalized = TRUE) {
mode_by_betweenness <- function(.data, normalized = TRUE,
direction = c("all", "in")) {
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
direction <- match.arg(direction)
graph <- manynet::as_igraph(.data)

Expand Down
27 changes: 26 additions & 1 deletion R/measure_centrality_closeness.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
#' @family closeness
#' @family centrality
#' @template node_measure
#' @section Signed networks:
#' These measures read a tie as a distance, and a negative tie is hostility
#' rather than a channel along which cohesion travels.
#' Where the network is signed, they therefore consider only the positive
#' ties, and say so.
#' Use [manynet::to_unsigned()] first to control this yourself.

NULL

#' @rdname measure_central_close
Expand Down Expand Up @@ -83,6 +90,8 @@ node_by_closeness <- function(.data, normalized = TRUE,
direction = c("out", "in", "all"), cutoff = NULL){

.data <- manynet::expect_nodes(.data)

.data <- .to_positive(.data)
direction <- match.arg(direction)
weights <- `if`(manynet::is_weighted(.data),
manynet::tie_weights(.data), NA)
Expand Down Expand Up @@ -141,6 +150,7 @@ node_by_closeness <- function(.data, normalized = TRUE,
node_by_harmonic <- function(.data, normalized = TRUE, cutoff = -1,
decay = NULL, direction = c("out", "in")){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
direction <- match.arg(direction)
if(is.null(decay)){
out <- igraph::harmonic_centrality(as_igraph(.data), # weighted if present
Expand Down Expand Up @@ -199,6 +209,7 @@ node_by_harmonic <- function(.data, normalized = TRUE, cutoff = -1,
#' @export
node_by_reach <- function(.data, normalized = TRUE, cutoff = 2){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
if(manynet::is_weighted(.data)){
tore <- manynet::as_matrix(.data)/mean(manynet::as_matrix(.data))
out <- 1/tore
Expand Down Expand Up @@ -273,6 +284,7 @@ node_by_decay <- function(.data, normalized = TRUE, decay = 0.5,
node_by_integration <- function(.data, normalized = TRUE,
direction = c("in", "out")){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
direction <- match.arg(direction)
dists <- igraph::distances(manynet::as_igraph(.data),
mode = ifelse(direction == "in", "in", "out"))
Expand Down Expand Up @@ -369,6 +381,7 @@ node_by_information <- function(.data, normalized = TRUE){
#' @export
node_by_eccentricity <- function(.data, normalized = TRUE){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
if(!manynet::is_connected(.data))
manynet::snet_unavailable("Eccentricity centrality is only available for connected networks.")
disties <- igraph::distances(as_igraph(.data))
Expand Down Expand Up @@ -446,6 +459,7 @@ node_by_distance <- function(.data, from, to, normalized = TRUE){
#' @export
node_by_vitality <- function(.data, normalized = TRUE){
.data <- manynet::expect_nodes(.data)
.data <- .to_positive(.data)
.data <- manynet::as_igraph(.data)
out <- vapply(manynet::snet_progress_nodes(.data), function(x){
sum(igraph::distances(.data)) -
Expand Down Expand Up @@ -571,7 +585,7 @@ NULL
#' @export
tie_by_closeness <- function(.data, normalized = TRUE){
.data <- manynet::expect_ties(.data)
edge_adj <- .to_linegraph(.data)
edge_adj <- manynet::to_linegraph(.data)
out <- node_by_closeness(edge_adj, normalized = normalized)
class(out) <- "numeric"
make_tie_measure(out, .data, measure = "closeness centrality",
Expand Down Expand Up @@ -620,6 +634,13 @@ tie_by_closeness <- function(.data, normalized = TRUE){
#' `net_by_*()` functions return a `network_measure` scalar;
#' `mode_by_closeness()` returns a `mode_measure` numeric vector of length two,
#' giving one centralization score per mode.
#' @section Signed networks:
#' These measures read a tie as a distance, and a negative tie is hostility
#' rather than a channel along which cohesion travels.
#' Where the network is signed, they therefore consider only the positive
#' ties, and say so.
#' Use [manynet::to_unsigned()] first to control this yourself.

NULL

#' @rdname measure_centralisation_close
Expand All @@ -630,6 +651,8 @@ net_by_closeness <- function(.data, normalized = TRUE,
direction = c("all", "out", "in")){

.data <- manynet::expect_nodes(.data)

.data <- .to_positive(.data)
direction <- match.arg(direction)
graph <- manynet::as_igraph(.data)

Expand Down Expand Up @@ -660,6 +683,8 @@ mode_by_closeness <- function(.data, normalized = TRUE,
direction = c("all", "out", "in")){

.data <- manynet::expect_nodes(.data)

.data <- .to_positive(.data)
direction <- match.arg(direction)
graph <- manynet::as_igraph(.data)

Expand Down
Loading
Loading