| 1 |
#' ENARotationSet R6class |
|
| 2 |
# |
|
| 3 |
#' @docType class |
|
| 4 |
#' @importFrom R6 R6Class |
|
| 5 |
#' @import data.table |
|
| 6 |
#' @export |
|
| 7 |
# |
|
| 8 |
#' @field rotation TBD |
|
| 9 |
#' @field node.positions TBD |
|
| 10 |
#' @field codes TBD |
|
| 11 |
#' @field eigenvalues TBD |
|
| 12 |
ENARotationSet = R6::R6Class("ENARotationSet",
|
|
| 13 |
public = list( |
|
| 14 | ||
| 15 |
## Public Functions ---- |
|
| 16 |
#' Create ENARotationSet |
|
| 17 |
#' |
|
| 18 |
#' @param rotation TBD |
|
| 19 |
#' @param codes TBD |
|
| 20 |
#' @param node.positions TBD |
|
| 21 |
#' @param eigenvalues TBD |
|
| 22 |
#' |
|
| 23 |
#' @return ENARotationsSet |
|
| 24 |
initialize = function( |
|
| 25 |
rotation, |
|
| 26 |
codes, |
|
| 27 |
node.positions, |
|
| 28 |
eigenvalues = NULL |
|
| 29 |
) {
|
|
| 30 | 8x |
self$node.positions = node.positions; |
| 31 | 8x |
self$rotation = rotation; |
| 32 | 8x |
self$codes = codes; |
| 33 | 8x |
if(!is.null(codes) && !is.null(self$node.positions)) {
|
| 34 | 1x |
rownames(self$node.positions) = codes; |
| 35 |
} |
|
| 36 | 8x |
self$eigenvalues = eigenvalues; |
| 37 |
}, |
|
| 38 | ||
| 39 |
## Public Properties ---- |
|
| 40 |
rotation = NULL, |
|
| 41 |
node.positions = NULL, |
|
| 42 |
codes = NULL, |
|
| 43 |
eigenvalues = NULL |
|
| 44 |
), |
|
| 45 |
private = list( |
|
| 46 |
##### |
|
| 47 |
## Private Properties |
|
| 48 |
##### |
|
| 49 |
args = NULL |
|
| 50 |
##### |
|
| 51 |
## END: Private Properties |
|
| 52 |
##### |
|
| 53 |
) |
|
| 54 |
) |
| 1 |
#' @title hENA rotation for ENA |
|
| 2 |
#' |
|
| 3 |
#' @description hENA rotation function. |
|
| 4 |
#' |
|
| 5 |
#' @param enaset ena set |
|
| 6 |
#' @param params list of parameters |
|
| 7 |
#' |
|
| 8 |
#' @return ena set |
|
| 9 |
#' @export |
|
| 10 |
ena.rotation.h <- function( |
|
| 11 |
enaset, |
|
| 12 |
params |
|
| 13 |
) {
|
|
| 14 |
# check arguments |
|
| 15 | ! |
if ( !is.list(params) || is.null(params$x_var) ) {
|
| 16 | ! |
stop("params must be provided as a list() and provide `x_var`")
|
| 17 |
} |
|
| 18 | ! |
x_var = params$x_var; |
| 19 | ! |
y_var = params$y_var; |
| 20 | ! |
control_vars = params$control_vars; |
| 21 | ! |
centering = ifelse(!is.null(params$centering), params$centering, TRUE); |
| 22 | ! |
include_xy = ifelse(!is.null(params$include_xy), params$include_xy, FALSE); |
| 23 | ! |
formula = params$formula; |
| 24 | ||
| 25 |
# get centered data |
|
| 26 | ! |
if (!is.null(enaset$model$points.for.projection)) {
|
| 27 | ! |
data = data.table::copy(enaset$model$points.for.projection) |
| 28 |
} |
|
| 29 |
else {
|
|
| 30 | ! |
data = data.table::copy(enaset$points.normed.centered) |
| 31 |
} |
|
| 32 | ||
| 33 |
# Prep |
|
| 34 | ! |
value_vars = colnames(as.matrix(data)) |
| 35 | ! |
data.table::set(x = data, j = value_vars, value = data[, lapply(.SD, function(x) x - mean(x)), .SDcols = value_vars]) |
| 36 | ||
| 37 |
# dummy code x_var |
|
| 38 | ! |
if (!is.numeric(data[[x_var]])) {
|
| 39 | ! |
x_var_f = paste0(x_var,"_f") |
| 40 | ! |
data[[x_var_f]] = data.table::rleidv(x = data, cols = x_var) - 1 |
| 41 | ! |
x_var = x_var_f; |
| 42 |
} |
|
| 43 | ||
| 44 |
# dummy code y_var |
|
| 45 | ! |
if (!is.null(y_var) && !is.numeric(data[[y_var]])) {
|
| 46 | ! |
y_var_f = paste0(y_var,"_f") |
| 47 | ! |
data[[y_var_f]] = data.table::rleidv(x = data, cols = y_var) - 1 |
| 48 | ! |
y_var = y_var_f; |
| 49 |
} |
|
| 50 | ! |
both_vars = c(x_var, y_var) |
| 51 | ||
| 52 |
# centering x_var and y_var |
|
| 53 | ! |
if ( centering ) {
|
| 54 | ! |
data[, c(both_vars) := lapply(.SD, function(x) x - mean(x)), .SDcols = c(both_vars)] |
| 55 |
} |
|
| 56 | ||
| 57 |
# prepare regression formula |
|
| 58 | ! |
f = paste(c(both_vars, control_vars), collapse = " + ") |
| 59 | ||
| 60 | ! |
if ( include_xy ) {
|
| 61 | ! |
xy_var = paste(both_vars, collapse = "_"); |
| 62 | ! |
data[[xy_var]] = data[[x_var]] * data[[y_var]]; |
| 63 | ! |
f = paste(c(f, xy_var), collapse = " + "); |
| 64 |
} |
|
| 65 | ||
| 66 | ! |
if (!is.null(formula)) {
|
| 67 | ! |
f = formula; |
| 68 |
} |
|
| 69 | ||
| 70 |
# run regression models and get slope variables |
|
| 71 | ! |
v = matrix(sapply(value_vars, function(v) {
|
| 72 | ! |
formula = as.formula(paste0("data$`", v, "` ~ ", f));
|
| 73 | ! |
lm(formula, data = data)$coefficients[seq_along(both_vars) + 1]; |
| 74 | ! |
}), ncol = length(both_vars), byrow = TRUE) |
| 75 | ||
| 76 |
# Prep deflation |
|
| 77 | ! |
R = NULL; |
| 78 | ! |
'..value_vars' = NULL; |
| 79 | ! |
A = as.matrix(data[, ..value_vars]); |
| 80 | ||
| 81 |
# Normalize x rotation vector |
|
| 82 | ! |
v1 = v[, 1, drop = FALSE]; |
| 83 | ! |
norm_v1 = sqrt(sum( v1 * v1 )); |
| 84 | ! |
if (norm_v1 != 0) {
|
| 85 | ! |
v1 = v1 / norm_v1; |
| 86 | ! |
R = v1; |
| 87 |
} |
|
| 88 | ! |
defA = as.matrix(A) - as.matrix(A) %*% v1 %*% t(v1); |
| 89 | ||
| 90 |
# Normalize y rotation vector, if applicable |
|
| 91 | ! |
v2 = NULL; |
| 92 | ! |
if (!is.null(y_var)) {
|
| 93 | ! |
v2 = v[, 2] |
| 94 | ! |
v2 = as.numeric(v2) - as.numeric(t(v2) %*% v1) * v1; |
| 95 | ! |
norm_v2 = sqrt(sum( v2 * v2 )); |
| 96 | ||
| 97 | ! |
if (norm_v2 != 0) {
|
| 98 | ! |
v2 = v2 / norm_v2; |
| 99 | ! |
if( is.null(R) ) {
|
| 100 | ! |
R = matrix(c(v2), ncol = 1) |
| 101 |
} |
|
| 102 |
else {
|
|
| 103 | ! |
R = matrix(c(R, v2), ncol = 2) |
| 104 |
} |
|
| 105 |
} |
|
| 106 | ||
| 107 | ! |
defA = defA - defA %*% v2 %*% t(v2); |
| 108 |
} |
|
| 109 | ||
| 110 |
# get svd for deflated points |
|
| 111 |
# svd_result = svd(defA) |
|
| 112 |
# svd_v = svd_result$v; |
|
| 113 | ! |
svd_result = prcomp(defA, retx=FALSE, scale=FALSE, center=FALSE, tol=0) |
| 114 | ! |
svd_v = svd_result$rotation |
| 115 | ||
| 116 |
# Merge rotation vectors |
|
| 117 | ! |
vcount = ncol(R); |
| 118 | ! |
combined = cbind(R, svd_v[, 1:(ncol(svd_v) - vcount)]); |
| 119 | ||
| 120 | ! |
colnames(combined) = c( |
| 121 | ! |
paste(c("x","y")[seq_len(vcount)], both_vars[seq_len(vcount)], sep = "_"),
|
| 122 | ! |
paste0("SVD", ((vcount + 1):ncol(combined)))
|
| 123 |
); |
|
| 124 | ||
| 125 |
# put into ENARotationSet |
|
| 126 |
# browser() |
|
| 127 | ! |
rotation_set <- ENARotationSet$new( |
| 128 | ! |
node.positions = NULL, |
| 129 | ! |
rotation = combined, |
| 130 | ! |
codes = enaset$rotation$codes, |
| 131 | ! |
eigenvalues = svd_result$sdev ^ 2 |
| 132 |
) |
|
| 133 | ||
| 134 |
# Done |
|
| 135 | ! |
return(rotation_set) |
| 136 |
} |
| 1 |
#' @title ENAplot Class |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' The ENAplot R6 class provides a structure for visualizing ENAset objects using plotly. |
|
| 5 |
#' It encapsulates the ENAset data, the plotly visualization, and related plotting parameters. |
|
| 6 |
#' |
|
| 7 |
#' @section Fields: |
|
| 8 |
#' \describe{
|
|
| 9 |
#' \item{enaset}{The \code{\link{ENAset}} object from which the ENAplot was constructed.}
|
|
| 10 |
#' \item{plot}{The plot as an htmlwidget: a plotly object, or a qeviz widget
|
|
| 11 |
#' when the plot was created with \code{backend = "qeviz"}.}
|
|
| 12 |
#' \item{qe}{With \code{backend = "qeviz"}, the \code{qeviz::qe_plot()} the
|
|
| 13 |
#' layer functions build on; \code{NULL} for plotly.}
|
|
| 14 |
#' \item{axes}{Axes information for the plot (TBD).}
|
|
| 15 |
#' \item{point}{Point information for the plot (TBD).}
|
|
| 16 |
#' \item{palette}{Color palette used for plotting (TBD).}
|
|
| 17 |
#' \item{plotted}{Indicates whether the plot has been rendered (TBD).}
|
|
| 18 |
#' } |
|
| 19 |
#' |
|
| 20 |
#' @examples |
|
| 21 |
#' # Example usage: |
|
| 22 |
#' # enaplot <- ENAplot$new(enaset = myENAset) |
|
| 23 |
#' |
|
| 24 |
#' @docType class |
|
| 25 |
#' @importFrom R6 R6Class |
|
| 26 |
#' @import data.table |
|
| 27 |
#' @export |
|
| 28 |
#' |
|
| 29 |
#' @field enaset - The \code{\link{ENAset}} object from which the ENAplot was constructed
|
|
| 30 |
#' @field plot - The plot as an htmlwidget (plotly, or qeviz with \code{backend = "qeviz"})
|
|
| 31 |
#' @field qe - The \code{qeviz::qe_plot()} behind a qeviz-backend plot, else \code{NULL}
|
|
| 32 |
#' @field axes A list or object specifying the axes configuration for the ENA plot, such as axis labels, limits, or scaling. |
|
| 33 |
#' @field point A structure representing the data points to be plotted, including coordinates and visual properties. |
|
| 34 |
#' @field palette A set of colors or a function defining the color scheme used for plotting elements in the ENA plot. |
|
| 35 |
#' @field plotted A logical or status indicator showing whether the plot has been rendered or updated. |
|
| 36 |
#' @field title Character. The title of the plot. |
|
| 37 |
#' @field showticklabels Logical. Indicates whether to show tick labels on the axes. |
|
| 38 |
#' @field autosize Logical. Indicates whether the plot should automatically resize. |
|
| 39 |
#' @field automargin Logical. Indicates whether the plot should automatically adjust margins. |
|
| 40 |
#' @field axispadding Numeric. Padding factor for the axes. |
|
| 41 |
ENAplot = R6::R6Class("ENAplot",
|
|
| 42 | ||
| 43 |
public = list( |
|
| 44 | ||
| 45 |
## Public Functions ---- |
|
| 46 |
#' Create ENApolot |
|
| 47 |
#' |
|
| 48 |
#' @param enaset An ENA set object containing the data to be plotted. |
|
| 49 |
#' @param title The title of the plot. |
|
| 50 |
#' @param dimension.labels Labels for the dimensions shown in the plot. |
|
| 51 |
#' @param font.size Numeric value specifying the font size for plot text. |
|
| 52 |
#' @param font.color Color value for the plot text. |
|
| 53 |
#' @param font.family Font family to use for plot text. |
|
| 54 |
#' @param scale.to Numeric value to scale the plot axes. |
|
| 55 |
#' @param showticklabels Logical; whether to display axis tick labels. |
|
| 56 |
#' @param autosize Logical; whether the plot should automatically size itself. |
|
| 57 |
#' @param automargin Logical; whether the plot should automatically adjust margins. |
|
| 58 |
#' @param axispadding Numeric value specifying padding around axes. |
|
| 59 |
#' @param backend \code{"plotly"} or \code{"qeviz"}; see \link{rENA-plot-backends}.
|
|
| 60 |
#' @param ... Additional arguments passed to the plotting function. #' |
|
| 61 |
#' |
|
| 62 |
#' @return ENAplot |
|
| 63 |
initialize = function( |
|
| 64 |
enaset = NULL, |
|
| 65 | ||
| 66 |
title = "ENA Plot", |
|
| 67 | ||
| 68 |
dimension.labels = c("",""),
|
|
| 69 | ||
| 70 |
font.size = 14, |
|
| 71 |
font.color = "#000000", |
|
| 72 |
font.family = "Arial", |
|
| 73 |
scale.to = "network", |
|
| 74 |
backend = c("plotly", "qeviz"),
|
|
| 75 |
... |
|
| 76 |
) {
|
|
| 77 | 88x |
backend <- match.arg(backend) |
| 78 | 87x |
if (is(enaset, "ENAset")) {
|
| 79 | 1x |
warning(paste0("Usage of ENAset objects will be deprecated ",
|
| 80 | 1x |
"and potentially removed altogether in future versions.")) |
| 81 | ||
| 82 | 1x |
enaset <- ena.set(enaset); |
| 83 |
} |
|
| 84 | ||
| 85 | 87x |
code.cols = !colnames(enaset$line.weights) %in% colnames(enaset$meta.data) |
| 86 | ||
| 87 | 87x |
args = list(...); |
| 88 | 87x |
if(!is.null(args$multiplier)) {
|
| 89 | 2x |
private$multiplier = args$multiplier |
| 90 |
} |
|
| 91 |
# qeviz backend: an explicit edge magnification (ena.plotter's |
|
| 92 |
# multipliers), labelled "(scaled Nx)" on the plot. |
|
| 93 | 87x |
if(!is.null(args$magnify)) {
|
| 94 | 3x |
private$magnify = args$magnify |
| 95 |
} |
|
| 96 | 87x |
if(!is.null(args$point.size)) {
|
| 97 | 1x |
self$point$size = args$point.size |
| 98 |
} |
|
| 99 | 87x |
if(!is.null(args$showticklabels)) {
|
| 100 | ! |
self$showticklabels = args$showticklabels |
| 101 |
} |
|
| 102 | 87x |
if(!is.null(args$axispadding)) {
|
| 103 | ! |
self$axispadding = args$axispadding |
| 104 |
} |
|
| 105 | 87x |
if(!is.null(args$autosize)) {
|
| 106 | ! |
self$autosize = args$autosize |
| 107 |
} |
|
| 108 | 87x |
if(!is.null(args$automargin)) {
|
| 109 | ! |
self$automargin = args$automargin |
| 110 |
} |
|
| 111 | 87x |
self$enaset <- list( |
| 112 | 87x |
connection.counts = data.table::copy(enaset$connection.counts), |
| 113 | 87x |
meta.data = data.table::copy(enaset$meta.data), |
| 114 | 87x |
model = list( |
| 115 | 87x |
model.type = enaset$model$model.type, |
| 116 | 87x |
raw.input = data.table::copy(enaset$model$raw.input), |
| 117 | 87x |
row.connection.counts = data.table::copy(enaset$model$row.connection.counts), |
| 118 | 87x |
unit.labels = enaset$model$unit.labels, |
| 119 | 87x |
points.for.projection = data.table::copy(enaset$model$points.for.projection), |
| 120 | 87x |
centroids = data.table::copy(enaset$model$centroids), |
| 121 | 87x |
variance = enaset$model$variance |
| 122 |
), |
|
| 123 | 87x |
points = data.table::copy(enaset$points), |
| 124 | 87x |
line.weights = data.table::copy(enaset$line.weights), |
| 125 | 87x |
rotation = list( |
| 126 | 87x |
adjacency.key = data.table::copy(enaset$rotation$adjacency.key), |
| 127 | 87x |
codes = enaset$rotation$codes, |
| 128 | 87x |
rotation.matrix = data.table::copy(enaset$rotation$rotation.matrix), |
| 129 | 87x |
center.vec = enaset$rotation$center.vec, |
| 130 | 87x |
nodes = data.table::copy(enaset$rotation$nodes) |
| 131 |
), |
|
| 132 | 87x |
plots = list() |
| 133 |
); |
|
| 134 | 87x |
self$title <- title; |
| 135 | ||
| 136 | 87x |
private$dimension.labels <- dimension.labels; |
| 137 | 87x |
private$font.size <- font.size; |
| 138 | 87x |
private$font.color <- font.color; |
| 139 | 87x |
private$font.family <- font.family; |
| 140 | 87x |
private$font = list ( |
| 141 | 87x |
size = private$font.size, |
| 142 | 87x |
color = private$font.color, |
| 143 | 87x |
family = private$font.family |
| 144 |
); |
|
| 145 | 87x |
private$backend <- backend |
| 146 | 87x |
if (backend == "qeviz") {
|
| 147 | 38x |
.require_qeviz() |
| 148 | 38x |
self$palette <- qeviz::qe_palette() |
| 149 | 38x |
self$qe <- .qe_base_plot(enaset, title, dimension.labels, scale.to, |
| 150 | 38x |
font.size, font.color, font.family) |
| 151 | 37x |
return(invisible(self)) |
| 152 |
} |
|
| 153 | ||
| 154 | 49x |
self$plot <- plotly::plot_ly( |
| 155 | 49x |
mode = "markers", |
| 156 | 49x |
type ="scatter" |
| 157 |
); |
|
| 158 | ||
| 159 | 49x |
self$plot <- plotly::config(p = self$plot, displayModeBar = args$displayModeBar); |
| 160 | ||
| 161 | 49x |
if (is.list(scale.to)) {
|
| 162 | 3x |
max.axis = max(abs(as.matrix(enaset$points)))*self$axispadding |
| 163 | 3x |
if(is.null(scale.to$x)) {
|
| 164 | 1x |
axis.range.x = c(-max.axis, max.axis) |
| 165 |
} |
|
| 166 |
else {
|
|
| 167 | 2x |
axis.range.x = scale.to$x |
| 168 |
} |
|
| 169 | 3x |
if(is.null(scale.to$y)) {
|
| 170 | 1x |
axis.range.y = c(-max.axis, max.axis) |
| 171 |
} |
|
| 172 |
else {
|
|
| 173 | 2x |
axis.range.y = scale.to$y |
| 174 |
} |
|
| 175 |
} |
|
| 176 |
else {
|
|
| 177 | 46x |
if(is.character(scale.to) && scale.to == "points") {
|
| 178 | 1x |
max.axis = max(abs(as.matrix(enaset$points)))*self$axispadding |
| 179 |
} |
|
| 180 | 45x |
else if (is.numeric(scale.to)) {
|
| 181 | 1x |
max.axis = tail(scale.to, 1) |
| 182 |
} |
|
| 183 |
else {
|
|
| 184 | 44x |
max.axis = max(abs(as.matrix(enaset$rotation$nodes)))*self$axispadding; |
| 185 |
} |
|
| 186 | 46x |
axis.range.x = axis.range.y = c(-max.axis, max.axis) |
| 187 |
} |
|
| 188 | ||
| 189 | 49x |
graph.axis <- list( |
| 190 | 49x |
titlefont = private$font, |
| 191 | 49x |
showgrid = F, |
| 192 | 49x |
zeroline = T, |
| 193 | 49x |
showticklabels = self$showticklabels, |
| 194 | 49x |
showgrid = T |
| 195 |
# range=c(-max.axis,max.axis) |
|
| 196 |
); |
|
| 197 | 49x |
if(!is.null(args$ticks)) {
|
| 198 | 1x |
graph.axis$showticklabels = T; |
| 199 | 1x |
graph.axis$ticks = args$ticks$location; |
| 200 | 1x |
graph.axis$tickcolor = args$ticks$color; |
| 201 | 1x |
graph.axis$tickangle = args$ticks$angle; |
| 202 |
} |
|
| 203 | 49x |
self$axes$x = graph.axis |
| 204 | 49x |
self$axes$x$title = dimension.labels[1]; |
| 205 | 49x |
self$axes$x$range = axis.range.x |
| 206 | 49x |
self$axes$y = graph.axis |
| 207 | 49x |
self$axes$y$title = dimension.labels[2]; |
| 208 | 49x |
self$axes$y$range = axis.range.y |
| 209 | ||
| 210 | 49x |
self$plot = plotly::layout( |
| 211 | 49x |
self$plot, |
| 212 | 49x |
title = title, |
| 213 | 49x |
xaxis = self$axes$x, |
| 214 | 49x |
yaxis = self$axes$y, |
| 215 | 49x |
autosize = self$autosize, |
| 216 | 49x |
font = list ( |
| 217 | 49x |
size = 12, |
| 218 | 49x |
color = private$font.color, |
| 219 | 49x |
family = private$font.family |
| 220 |
) |
|
| 221 |
); |
|
| 222 |
}, |
|
| 223 | ||
| 224 |
#' Print ENA plot |
|
| 225 |
#' |
|
| 226 |
#' @return |
|
| 227 |
print = function() {
|
|
| 228 | ! |
print(self$plot); |
| 229 |
}, |
|
| 230 | ||
| 231 |
#' Get property from object |
|
| 232 |
#' |
|
| 233 |
#' @param x character key to retrieve from object |
|
| 234 |
#' @return value from object at x |
|
| 235 |
get = function(x) {
|
|
| 236 | 705x |
return(private[[x]]) |
| 237 |
}, |
|
| 238 | ||
| 239 |
## Public Properties ---- |
|
| 240 |
enaset = NULL, |
|
| 241 |
title = "ENA Plot", |
|
| 242 |
qe = NULL, |
|
| 243 |
axes = list( |
|
| 244 |
x = NULL, y = NULL |
|
| 245 |
), |
|
| 246 |
point = list( |
|
| 247 |
size = 5 |
|
| 248 |
), |
|
| 249 |
showticklabels = F, |
|
| 250 |
autosize = F, |
|
| 251 |
automargin = T, |
|
| 252 |
axispadding = 1.2, |
|
| 253 |
palette = c("#386CB0", "#F0027F", "#7FC97F", "#BEAED4",
|
|
| 254 |
"#FDC086","#FFFF99", "#BF5B17"), |
|
| 255 |
plotted = list( |
|
| 256 |
points = list(), networks = list(), |
|
| 257 |
trajectories = list(), means = list() |
|
| 258 |
) |
|
| 259 |
), |
|
| 260 | ||
| 261 |
active = list( |
|
| 262 |
# plotly: the stored plotly object. qeviz: a widget built from self$qe on |
|
| 263 |
# each read, so it always reflects the layers added so far. |
|
| 264 |
plot = function(value) {
|
|
| 265 | 1657x |
if (missing(value)) {
|
| 266 | 23x |
if (identical(private$backend, "qeviz")) return(qeviz::qe_widget(self$qe)) |
| 267 | 810x |
return(private$.plot) |
| 268 |
} |
|
| 269 | 824x |
if (identical(private$backend, "qeviz")) |
| 270 | 1x |
stop("$plot of a qeviz-backend ENAplot is read-only; add layers with ",
|
| 271 | 1x |
"ena.plot.network() / ena.plot.points() / ena.plot.group().", call. = FALSE) |
| 272 | 823x |
private$.plot <- value |
| 273 |
} |
|
| 274 |
), |
|
| 275 | ||
| 276 |
private = list( |
|
| 277 |
#### |
|
| 278 |
## Private Properties |
|
| 279 |
#### |
|
| 280 |
backend = "plotly", |
|
| 281 |
.plot = NULL, |
|
| 282 |
magnify = NULL, |
|
| 283 | ||
| 284 |
dimension.labels = c("X","Y"),
|
|
| 285 | ||
| 286 |
font = list(), |
|
| 287 |
font.size = 14, |
|
| 288 |
font.color = "#000000", |
|
| 289 |
font.family = "Arial", |
|
| 290 |
#plot.color = I("black"),
|
|
| 291 | ||
| 292 |
multiplier = 5 |
|
| 293 |
#### |
|
| 294 |
## END: Private Properties |
|
| 295 |
#### |
|
| 296 |
) |
|
| 297 |
) |
|
| 298 | ||
| 299 | ||
| 300 |
# The empty qeviz plot an ENAplot starts from (backend = "qeviz"). The full |
|
| 301 |
# set is extracted so colour ramps and widths use model-wide scales; nothing |
|
| 302 |
# is drawn until the layer functions add networks, points or means. |
|
| 303 |
.qe_base_plot <- function(enaset, title, dimension.labels, scale.to, |
|
| 304 |
font.size, font.color, font.family) {
|
|
| 305 | 38x |
if (is.list(scale.to)) |
| 306 | 1x |
stop("scale.to = list(x =, y =) is not supported on the qeviz backend: ",
|
| 307 | 1x |
"qeviz ranges are symmetric. Use \"network\", \"points\" or a number.", |
| 308 | 1x |
call. = FALSE) |
| 309 | 37x |
range <- if (is.numeric(scale.to)) utils::tail(scale.to, 1) |
| 310 | 37x |
else if (identical(scale.to, "points")) "points" |
| 311 | 37x |
else "network" |
| 312 | 37x |
lbl <- function(i) {
|
| 313 | 74x |
v <- if (length(dimension.labels) >= i) dimension.labels[i] else "" |
| 314 | 4x |
if (is.null(v) || is.na(v) || !nzchar(v)) FALSE else v |
| 315 |
} |
|
| 316 | 37x |
p <- qeviz::qe_plot(enaset, title = if (!is.null(title) && nzchar(title)) title, |
| 317 | 37x |
group_col = NULL, range = range, scale_points = FALSE) |
| 318 | 37x |
p <- qeviz::qe_group(p, show = FALSE) |
| 319 | 37x |
p <- qeviz::qe_axes(p, x = lbl(1), y = lbl(2)) |
| 320 | 37x |
qeviz::qe_labels(p, font_size = font.size, font_family = font.family, |
| 321 | 37x |
font_color = font.color) |
| 322 |
} |
| 1 |
#' @title with.ena.matrix |
|
| 2 |
#' @description This function sets up a context using the provided data (typically an ENA matrix), |
|
| 3 |
#' allowing the evaluation of an expression (`expr`) with access to both the matrix and |
|
| 4 |
#' its metadata. Optionally, a custom matrix `V` and other arguments can be supplied. |
|
| 5 |
#' |
|
| 6 |
#' @param data An ENA matrix or data frame containing the data to be used. |
|
| 7 |
#' @param expr An R expression to be evaluated within the context of the ENA matrix. |
|
| 8 |
#' @param ... Additional arguments, including an optional custom matrix `V` and other parameters. |
|
| 9 |
#' |
|
| 10 |
#' @details |
|
| 11 |
#' - If a custom matrix `V` is provided in `...`, it will be used; otherwise, `data` is converted to a matrix. |
|
| 12 |
#' - Metadata columns are coerced to numeric if they are character vectors. |
|
| 13 |
#' - The expression is evaluated with access to both the matrix (`V`) and metadata. |
|
| 14 |
#' |
|
| 15 |
#' @return The result of evaluating `expr` in the constructed context. |
|
| 16 |
#' |
|
| 17 |
#' @export |
|
| 18 |
with.ena.matrix <- function(data, expr, ...) {
|
|
| 19 | ! |
dot_args <- list(...); |
| 20 | ||
| 21 |
# Points |
|
| 22 | ! |
V <- NULL; |
| 23 | ! |
if(length(dot_args) > 0 && !is.null(dot_args$V)) {
|
| 24 | ! |
V <- dot_args$V; |
| 25 |
} |
|
| 26 |
else {
|
|
| 27 | ! |
V <- as.matrix(data); |
| 28 |
} |
|
| 29 | ||
| 30 |
# Meta data |
|
| 31 | ! |
x <- unclass(data); |
| 32 | ! |
l <- lapply(x, function(i_val) {
|
| 33 |
# i_val <- get(i); |
|
| 34 | ! |
if(is.character(i_val)) {
|
| 35 | ! |
i_val <- as.numeric(as.factor(i_val)); |
| 36 |
} |
|
| 37 | ! |
return(i_val); |
| 38 |
}); |
|
| 39 | ||
| 40 |
# frm <- dot_args$frm; |
|
| 41 |
# if(!is(frm, "formula")) {
|
|
| 42 |
# frm <- formula(frm); |
|
| 43 |
# } |
|
| 44 | ||
| 45 | ! |
l$V <- V; |
| 46 |
# with(l, {
|
|
| 47 |
# lm(formula = frm) |
|
| 48 |
# }) |
|
| 49 | ||
| 50 | ! |
ll <- c(l, dot_args); |
| 51 | ! |
eval(substitute(expr), ll, enclos = parent.frame()); |
| 52 |
# lm(formula = frm, data = l) |
|
| 53 |
} |
|
| 54 | ||
| 55 |
### |
|
| 56 |
#' @title ENA Rotate by regression (second way) |
|
| 57 |
#' |
|
| 58 |
#' @description This function allows user to provide a regression formula for rotation on x and optionally on y. |
|
| 59 |
#' If regression formula for y is not provide, svd is applied to the residual data deflated by x to get y coordinates. |
|
| 60 |
#' The regression formula should use ENA points as major predictors and a binary or numerical variable as dependent variable. |
|
| 61 |
#' Control and interaction variables are allowed to be included as predictors in the formula. |
|
| 62 |
#' |
|
| 63 |
#' @param enaset An \code{\link{ENAset}}
|
|
| 64 |
#' @param params list of parameters, may include: |
|
| 65 |
#' x_var: Regression formula for x direction, such as "lm(formula= Condition ~ V + GameHalf + Condition : GameHalf)", |
|
| 66 |
#' where V always stands for the ENA points. |
|
| 67 |
#' y_var: Regression formula, similar to x_var for y direction (optional). |
|
| 68 |
#' |
|
| 69 |
#' @export |
|
| 70 |
#' @return \code{\link{ENARotationSet}}
|
|
| 71 |
ena.rotate.by.hena.regression_2 = function( enaset, params ) {
|
|
| 72 | ||
| 73 |
# check arguments |
|
| 74 | ! |
if ( !is.list(params) || is.null(params$x_var) ) {
|
| 75 | ! |
stop("params must be provided as a list() and provide `x_var`")
|
| 76 |
} |
|
| 77 | ||
| 78 | ! |
x <- formula(params$x_var); |
| 79 | ||
| 80 | ! |
if (is.null(enaset$points.normed.centered)) {
|
| 81 | ! |
p <- as.matrix(enaset$model$points.for.projection); |
| 82 |
} |
|
| 83 |
else {
|
|
| 84 | ! |
p <- as.matrix(enaset$points.normed.centered); |
| 85 |
} |
|
| 86 | ||
| 87 |
#get variables |
|
| 88 | ! |
V <- as.matrix(p); |
| 89 | ! |
n <- ncol(V); |
| 90 | ||
| 91 |
#regress to get v1 using x regression formula |
|
| 92 |
# attach(enaset$meta.data,warn.conflicts = F) |
|
| 93 |
# v1 <- eval(parse(text = x))$coefficients; |
|
| 94 |
# v1_res <- with(enaset$model$points.for.projection, NULL, formula = x); |
|
| 95 | ! |
v1_res <- with.ena.matrix(enaset$model$points.for.projection, {
|
| 96 | ! |
prm_var <- params$x_var; |
| 97 | ! |
prm <- if(is.character(prm_var)) |
| 98 | ! |
prm_var |
| 99 |
else |
|
| 100 | ! |
enquote(prm_var) |
| 101 |
; |
|
| 102 | ! |
vars <- all.vars(formula(prm)); |
| 103 | ! |
all_exists <- sapply(vars, function(x) x == "V" || exists(x)) |
| 104 | ! |
if(!all(all_exists)) {
|
| 105 | ! |
stop(paste0("The following columns in the formula are not found in the unique metadata for the units: ", paste0(vars[!all_exists], collapse = ", ")))
|
| 106 |
} |
|
| 107 | ! |
lm(formula(prm)); |
| 108 |
}); |
|
| 109 | ! |
v1 <- v1_res$coefficients; |
| 110 | ||
| 111 |
# remove intercept |
|
| 112 | ! |
if(is.null(dim(v1))) {
|
| 113 | ! |
v1 <- v1[2:(n+1)]; |
| 114 |
} |
|
| 115 |
else {
|
|
| 116 | ! |
v1 <- v1[2,]; |
| 117 |
} |
|
| 118 | ||
| 119 |
# make v1 a unit vector |
|
| 120 | ! |
norm_v1 <- sqrt(sum(v1 * v1)); |
| 121 | ! |
if (norm_v1 != 0) {
|
| 122 | ! |
v1 <- v1 / norm_v1; |
| 123 |
} |
|
| 124 | ||
| 125 |
# name v1 vector |
|
| 126 | ! |
if(is.na(all.vars(x)[2])) {
|
| 127 | ! |
xName <- names(v1)[1]; |
| 128 |
} |
|
| 129 |
else {
|
|
| 130 | ! |
xName <- all.vars(x)[2]; |
| 131 |
} |
|
| 132 | ||
| 133 |
# Save v1 |
|
| 134 | ! |
R <- matrix(c(v1), ncol = 1); |
| 135 | ! |
colnames(R) <- c(paste0(xName,"_reg")); |
| 136 | ||
| 137 |
#deflate matrix by x dimension |
|
| 138 | ! |
A <- as.matrix(p); |
| 139 | ! |
defA <- as.matrix(A) - as.matrix(A) %*% v1 %*% t(v1); |
| 140 | ||
| 141 |
#if y formula is given, regress by y formula |
|
| 142 | ! |
if (!is.null(params$y_var)) {
|
| 143 | ! |
y <- formula(params$y_var); |
| 144 | ||
| 145 |
# regress to get v2 vector using formula y |
|
| 146 | ! |
V <- defA; |
| 147 | ||
| 148 | ! |
v2_res <- with.ena.matrix(enaset$model$points.for.projection, {
|
| 149 | ! |
prm_var <- params$y_var; |
| 150 | ! |
prm <- if(is.character(prm_var)) |
| 151 | ! |
prm_var |
| 152 |
else |
|
| 153 | ! |
enquote(prm_var) |
| 154 |
; |
|
| 155 | ! |
vars <- all.vars(formula(prm)); |
| 156 | ! |
all_exists <- sapply(vars, function(x) x == "V" || exists(x)) |
| 157 | ! |
if(!all(all_exists)) {
|
| 158 | ! |
stop(paste0("The following columns in the formula are not found in the unique metadata for the units: ", paste0(vars[!all_exists], collapse = ", ")))
|
| 159 |
} |
|
| 160 | ! |
lm(formula(prm)); |
| 161 |
}); |
|
| 162 | ! |
v2 <- v2_res$coefficients; |
| 163 | ! |
v2 <- v2[2:length(v2)]; |
| 164 | ||
| 165 |
#make v2 a unit vector |
|
| 166 | ! |
norm_v2 <- sqrt(sum(v2 * v2)); |
| 167 | ||
| 168 | ! |
if (norm_v2 != 0) {
|
| 169 | ! |
v2 <- v2 / norm_v2; |
| 170 |
} |
|
| 171 | ||
| 172 |
#name v2 vector |
|
| 173 | ! |
if(is.na(all.vars(y)[2])) {
|
| 174 | ! |
yName <- names(v2)[1]; |
| 175 |
} |
|
| 176 |
else {
|
|
| 177 | ! |
yName <- all.vars(y)[2]; |
| 178 |
} |
|
| 179 | ||
| 180 |
# save both v1 and v2 |
|
| 181 | ! |
R <- cbind(v1, v2); |
| 182 | ! |
colnames(R) <- c(paste0(xName,"_reg"), paste0(yName,"_reg")); |
| 183 | ||
| 184 |
#deflat by v2 |
|
| 185 | ! |
defA <- as.matrix(defA) - as.matrix(defA) %*% v2 %*% t(v2); |
| 186 |
} |
|
| 187 | ||
| 188 |
# get svd for deflated points |
|
| 189 | ! |
svd_result <- prcomp(defA, retx=FALSE, scale=FALSE, center=FALSE, tol=0); |
| 190 | ! |
svd_v <- svd_result$rotation; |
| 191 | ||
| 192 |
# Merge rotation vectors |
|
| 193 | ! |
vcount <- ncol(R); |
| 194 | ! |
colNamesR <- colnames(R); |
| 195 | ! |
combined <- cbind(R, svd_v[, 1:(ncol(svd_v) - vcount)]); |
| 196 | ! |
colnames(combined) <- c( |
| 197 | ! |
colNamesR, |
| 198 | ! |
paste0("SVD", ((vcount + 1):ncol(combined)))
|
| 199 |
); |
|
| 200 | ||
| 201 |
#create rotation set |
|
| 202 | ! |
rotation_set <- ENARotationSet$new( |
| 203 | ! |
node.positions = NULL, |
| 204 | ! |
rotation = combined, |
| 205 | ! |
codes = enaset$rotation$codes, |
| 206 | ! |
eigenvalues = NULL |
| 207 |
) |
|
| 208 | ||
| 209 | ! |
return(rotation_set); |
| 210 |
} |
|
| 211 |
| 1 |
ena.set <- function(x) {
|
|
| 2 | 130x |
newset = list() |
| 3 | 130x |
class(newset) <- c("ena.set", class(newset))
|
| 4 | 130x |
x.is.set <- T |
| 5 | 130x |
if("ENAdata" %in% class(x)) {
|
| 6 | 127x |
x <- list(enadata = x); |
| 7 | 127x |
x.is.set <- F |
| 8 |
} |
|
| 9 | 130x |
code.columns <- apply(x$enadata$adjacency.matrix, 2, paste, collapse = " & ") |
| 10 | ||
| 11 | 130x |
newset$connection.counts <- x$enadata$adjacency.vectors; |
| 12 | 130x |
colnames(newset$connection.counts) <- code.columns |
| 13 | 130x |
for (i in seq(ncol(newset$connection.counts))) {
|
| 14 | 1613x |
set(newset$connection.counts, j = i, value = as.ena.co.occurrence(newset$connection.counts[[i]])) |
| 15 |
} |
|
| 16 | ||
| 17 | 130x |
if (grepl(x = x$enadata$model, pattern = "Traj", ignore.case = T)) {
|
| 18 | 10x |
newset$meta.data <- data.table::copy(x$enadata$trajectories$units) |
| 19 | 10x |
newset$meta.data[, ENA_UNIT := apply(x$enadata$trajectories$units, 1, paste, collapse = "::")] |
| 20 | ||
| 21 | 10x |
newset$trajectories <- cbind(newset$meta.data, x$enadata$trajectories$step) |
| 22 | 10x |
for (i in seq(ncol(newset$trajectories))) {
|
| 23 | 44x |
set(newset$trajectories, j = i, value = as.ena.metadata(newset$trajectories[[i]])) |
| 24 |
} |
|
| 25 |
} |
|
| 26 |
else {
|
|
| 27 | 120x |
newset$meta.data <- x$enadata$metadata |
| 28 |
} |
|
| 29 | ||
| 30 | 130x |
if (!is.null(newset$meta.data) && ncol(newset$meta.data) > 0) {
|
| 31 | 130x |
for (i in seq(ncol(newset$meta.data))) {
|
| 32 | 527x |
set(newset$meta.data, j = i, |
| 33 | 527x |
value = as.ena.metadata(newset$meta.data[[i]])) |
| 34 |
} |
|
| 35 |
} |
|
| 36 | 130x |
newset$meta.data <- as.ena.matrix(newset$meta.data); |
| 37 | ||
| 38 | 130x |
if (x.is.set) {
|
| 39 | 3x |
newset$line.weights <- as.data.table(cbind(x$enadata$metadata, x$line.weights)) |
| 40 | 3x |
to_cols <- names(which(!find_meta_cols(newset$line.weights))) |
| 41 | 3x |
for(col in to_cols) {
|
| 42 | 40x |
set(x = newset$line.weights, j = col, value = as.ena.co.occurrence(newset$line.weights[[col]])) |
| 43 |
} |
|
| 44 | 3x |
class(newset$line.weights) <- c("ena.line.weights", class(newset$line.weights))
|
| 45 | ||
| 46 | 3x |
newset$points <- cbind(x$enadata$metadata, x$points.rotated) |
| 47 | 3x |
to_cols <- names(which(!find_meta_cols(newset$points))) |
| 48 | 3x |
for(col in to_cols) {
|
| 49 | 40x |
set(x = newset$points, j = col, value = as.ena.dimension(newset$points[[col]])) |
| 50 |
} |
|
| 51 | 3x |
newset$points <- as.ena.matrix(newset$points, "ena.points") |
| 52 | ||
| 53 | 3x |
newset$rotation.matrix <- x$rotation.set$rotation |
| 54 |
} |
|
| 55 | ||
| 56 | 130x |
newset$connection.counts <- cbind(newset$meta.data, newset$connection.counts) |
| 57 | 130x |
class(newset$connection.counts) <- c("ena.connections",
|
| 58 | 130x |
class(newset$connection.counts)) |
| 59 | ||
| 60 | 130x |
newset$model <- list( |
| 61 | 130x |
model.type = x$enadata$model, |
| 62 | 130x |
raw.input = x$enadata$raw, |
| 63 | 130x |
row.connection.counts = x$enadata$accumulated.adjacency.vectors[, |
| 64 | 130x |
unique(names(x$enadata$accumulated.adjacency.vectors)), with = F], |
| 65 | 130x |
unit.labels = x$enadata$unit.names |
| 66 |
) |
|
| 67 | ||
| 68 |
##### |
|
| 69 |
# if(quote(x$enadata$function.params$weight.by) != "binary") {
|
|
| 70 |
# newset$model$unweighted.connection.counts <- x$enadata$adjacency.vectors.raw |
|
| 71 |
# class(newset$model$unweighted.connection.counts) <- c("ena.connections",
|
|
| 72 |
# class(newset$model$unweighted.connection.counts)) |
|
| 73 |
# are.codes <- find_code_cols(newset$model$unweighted.connection.counts) |
|
| 74 |
# for (i in seq(are.codes)) {
|
|
| 75 |
# if (are.codes[i]) {
|
|
| 76 |
# set(newset$model$unweighted.connection.counts, j = i, |
|
| 77 |
# value = as.ena.co.occurrence( |
|
| 78 |
# newset$model$unweighted.connection.counts[[i]] |
|
| 79 |
# ) |
|
| 80 |
# ) |
|
| 81 |
# } else {
|
|
| 82 |
# set(newset$model$unweighted.connection.counts, j = i, |
|
| 83 |
# value = as.ena.metadata( |
|
| 84 |
# newset$model$unweighted.connection.counts[[i]] |
|
| 85 |
# ) |
|
| 86 |
# ) |
|
| 87 |
# } |
|
| 88 |
# } |
|
| 89 |
# } |
|
| 90 |
##### |
|
| 91 | ||
| 92 | 130x |
cols <- grep("adjacency.code", colnames(newset$model$row.connection.counts))
|
| 93 | 130x |
colnames(newset$model$row.connection.counts)[cols] <- code.columns |
| 94 | 130x |
for(i in cols) {
|
| 95 | 1613x |
set(newset$model$row.connection.counts, j = i, |
| 96 | 1613x |
value = as.ena.co.occurrence(newset$model$row.connection.counts[[i]])) |
| 97 |
} |
|
| 98 | 130x |
for (i in which(colnames(newset$model$row.connection.counts) |
| 99 | 130x |
%in% colnames(newset$meta.data)) |
| 100 |
) {
|
|
| 101 | 404x |
set(newset$model$row.connection.counts, j = i, |
| 102 | 404x |
value = as.ena.metadata(newset$model$row.connection.counts[[i]])) |
| 103 |
} |
|
| 104 | 130x |
for (i in which(colnames(newset$model$row.connection.counts) %in% |
| 105 | 130x |
x$enadata$codes) |
| 106 |
) {
|
|
| 107 | 697x |
set(newset$model$row.connection.counts, j = i, |
| 108 | 697x |
value = as.ena.code(newset$model$row.connection.counts[[i]])) |
| 109 |
} |
|
| 110 | 130x |
class(newset$model$row.connection.counts) <- c("row.connections",
|
| 111 | 130x |
class(newset$model$row.connection.counts)) |
| 112 | ||
| 113 | 130x |
if (x.is.set) {
|
| 114 | 3x |
newset$model$centroids <- x$centroids |
| 115 | 3x |
newset$model$correlations <- x$correlations |
| 116 | 3x |
newset$model$function.call <- x$function.call |
| 117 | 3x |
newset$model$function.params <- x$function.params |
| 118 | 3x |
newset$model$points.for.projection <- cbind(x$enadata$metadata, |
| 119 | 3x |
x$points.normed.centered) |
| 120 | 3x |
newset$model$variance <- x$variance |
| 121 | 3x |
names(newset$model$variance) <- colnames(newset$rotation.matrix) |
| 122 |
} |
|
| 123 | ||
| 124 | 130x |
newset$rotation <- list( |
| 125 | 130x |
adjacency.key = as.data.table(x$enadata$adjacency.matrix), |
| 126 | 130x |
codes = x$enadata$codes |
| 127 |
) |
|
| 128 | 130x |
class(newset$rotation) <- c("ena.rotation.set", class(newset$rotation))
|
| 129 | ||
| 130 | 130x |
for (i in seq(ncol(newset$rotation$adjacency.key))) {
|
| 131 | 1613x |
set(newset$rotation$adjacency.key, j = i, |
| 132 | 1613x |
value = as.ena.codes(newset$rotation$adjacency.key[[i]])) |
| 133 |
} |
|
| 134 | ||
| 135 | 130x |
if(x.is.set) {
|
| 136 | 3x |
newset$rotation$eigenvalues = x$rotation.set$eigenvalues |
| 137 | 3x |
newset$rotation$nodes = x$node.positions |
| 138 | 3x |
newset$rotation$rotation.matrix = x$rotation.set$rotation |
| 139 |
} |
|
| 140 | ||
| 141 | 130x |
newset$`_function.call` <- sys.calls()[[1]] |
| 142 | 130x |
back.frame <- sapply(sys.frames(), function(f) {
|
| 143 | 130x |
"window.size.back" %in% ls(envir = f) }) |
| 144 | 130x |
if (any(back.frame)) {
|
| 145 | 126x |
call.frame <- sys.frame(which(back.frame)) |
| 146 | 126x |
newset$`_function.params` <- mget(ls(envir = call.frame), |
| 147 | 126x |
envir = call.frame) |
| 148 |
} else {
|
|
| 149 | 4x |
newset$`_function.params` <- list() |
| 150 |
} |
|
| 151 | ||
| 152 | 130x |
return(newset); |
| 153 |
} |
| 1 |
.onLoad <- function(libname, pkgname) {
|
|
| 2 | ! |
globalVariables(c( |
| 3 | ! |
".","ENA_ROW_IDX","ENA_UNIT","V1","V2","V3", |
| 4 | ! |
"ci.x","ci.y","e","handle","name","unit.groups", |
| 5 | ! |
"V","graph_from_data_frame","|>","X1","X2", |
| 6 | ! |
"dfDT.points","points.raw","lines","KEYCOL","ENA_CONV", |
| 7 | ! |
"..groupCol","..units","..metadata", "..codes", "..conversation","ENA_GROUP_NAME", |
| 8 | ! |
"label.font.color","label.font.family","label.font.size", |
| 9 | ! |
"label.offset","legend.include.edges","legend.name", |
| 10 | ! |
"network.edges.shapes","nodes","rows.to.keep","show.legend", |
| 11 | ! |
"..connection_name", "..dimension_names", "..first_meta", |
| 12 | ! |
"..codeNames", "..conversation_cols", |
| 13 | ! |
"count","window","entropy","ground_type","n_obs","n_unique", |
| 14 | ! |
"method","n_conversations",".group" |
| 15 |
)) |
|
| 16 |
# op <- options() |
|
| 17 |
# op.rENA <- list( |
|
| 18 |
# UNIT_NAMES = "ena.unit.names", |
|
| 19 |
# TRAJ_TYPES = c("accumulated","non-accumulated")
|
|
| 20 |
# ); |
|
| 21 |
# |
|
| 22 |
# toset <- !("rENA" %in% names(op))
|
|
| 23 |
# print(paste("ToSet:", toset));
|
|
| 24 |
# |
|
| 25 |
# if(toset) {
|
|
| 26 |
# options(rENA = op.rENA) |
|
| 27 |
# } |
|
| 28 |
# |
|
| 29 |
# invisible() |
|
| 30 |
} |
|
| 31 | ||
| 32 |
.onAttach <- function(libname, pkgname) {
|
|
| 33 | 2x |
packageStartupMessage("For the latest features and updates, install from https://cran.qe-libs.org");
|
| 34 | 2x |
invisible(); |
| 35 |
} |
| 1 |
## ── qeviz backend for the ena.plot.* layer functions ───────────────────────── |
|
| 2 |
## |
|
| 3 |
## With ena.plot(backend = "qeviz"), ena.plot.network() / ena.plot.points() / |
|
| 4 |
## ena.plot.group() hand off to the functions here, which add layers to the |
|
| 5 |
## ENAplot's qeviz::qe_plot() ($qe) instead of plotly traces. The mapping is |
|
| 6 |
## documented in qeviz's docs/rena-qeviz-backend.md. |
|
| 7 |
## |
|
| 8 |
## Arguments qeviz cannot honour warn once per session when they change the |
|
| 9 |
## picture (see .qe_ignored()); plotly-only layout options are ignored quietly. |
|
| 10 |
## ───────────────────────────────────────────────────────────────────────────── |
|
| 11 | ||
| 12 |
.qe_backend <- function(enaplot) {
|
|
| 13 | 89x |
!is.null(enaplot) && is(enaplot, "ENAplot") && identical(enaplot$get("backend"), "qeviz")
|
| 14 |
} |
|
| 15 | ||
| 16 |
.qe_warned <- new.env(parent = emptyenv()) |
|
| 17 | ||
| 18 |
# Warn once per session for each supplied argument qeviz ignores. |
|
| 19 |
.qe_ignored <- function(fn, supplied, args, why) {
|
|
| 20 | 56x |
for (a in intersect(args, supplied)) {
|
| 21 | 3x |
key <- paste(fn, a, sep = "::") |
| 22 | 1x |
if (isTRUE(.qe_warned[[key]])) next |
| 23 | 2x |
assign(key, TRUE, envir = .qe_warned) |
| 24 | 2x |
warning(sprintf("%s(): '%s' is ignored on the qeviz backend (%s).", fn, a, why),
|
| 25 | 2x |
call. = FALSE) |
| 26 |
} |
|
| 27 |
} |
|
| 28 | ||
| 29 |
# Colours as hex (qeviz derives complements from hex; rENA passes names / I()). |
|
| 30 |
.qe_hex <- function(colors) {
|
|
| 31 | ! |
if (is.null(colors)) return(NULL) |
| 32 | 26x |
cols <- as.character(unclass(colors)) |
| 33 | 26x |
m <- grDevices::col2rgb(cols, alpha = FALSE) |
| 34 | 26x |
stats::setNames(grDevices::rgb(m[1, ], m[2, ], m[3, ], maxColorValue = 255), names(colors)) |
| 35 |
} |
|
| 36 | ||
| 37 |
# Plot-wide label fonts, applied only when a layer call supplied them. |
|
| 38 |
.qe_fonts <- function(qe, supplied, size, family, color) {
|
|
| 39 | 29x |
args <- list(qe) |
| 40 | ! |
if ("label.font.size" %in% supplied) args$font_size <- size
|
| 41 | ! |
if ("label.font.family" %in% supplied) args$font_family <- family
|
| 42 | ! |
if ("label.font.color" %in% supplied) args$font_color <- color
|
| 43 | ! |
if (length(args) > 1) do.call(qeviz::qe_labels, args) else qe |
| 44 |
} |
|
| 45 | ||
| 46 |
# Points in rENA's accepted forms (ena.points, data.frame / matrix, a numeric |
|
| 47 |
# vector for one point) -> id + the first two dimension columns, named as the |
|
| 48 |
# model's axes so qeviz reads them. |
|
| 49 |
.qe_point_df <- function(points, enaplot, ids = NULL) {
|
|
| 50 | 10x |
m <- enaplot$qe$model |
| 51 | 1x |
if (is.numeric(points) && is.null(dim(points))) points <- matrix(points, nrow = 1) |
| 52 | 10x |
unit <- if (!is.null(ids)) as.character(ids) |
| 53 | 10x |
else if (is.data.frame(points) && "ENA_UNIT" %in% names(points)) as.character(points$ENA_UNIT) |
| 54 | 10x |
else if (!is.null(rownames(points))) rownames(points) |
| 55 | 10x |
else NULL |
| 56 | 10x |
dims <- as.data.frame(if (is(points, "ena.points") || is.data.table(points)) remove_meta_data(points) |
| 57 | 10x |
else points, check.names = FALSE) |
| 58 | 10x |
dims <- dims[, vapply(dims, is.numeric, logical(1)), drop = FALSE] |
| 59 | ! |
if (ncol(dims) < 2) stop("points need at least two dimension columns.", call. = FALSE)
|
| 60 | 10x |
out <- data.frame(id = if (is.null(unit)) as.character(seq_len(nrow(dims))) else unit, |
| 61 | 10x |
stringsAsFactors = FALSE) |
| 62 | 10x |
names(out) <- m$id_col |
| 63 | 10x |
out[[m$x_col]] <- as.numeric(dims[[1]]) |
| 64 | 10x |
out[[m$y_col]] <- as.numeric(dims[[2]]) |
| 65 | 10x |
out |
| 66 |
} |
|
| 67 | ||
| 68 |
# ── ena.plot.network ───────────────────────────────────────────────────────── |
|
| 69 |
.qe_plot_network <- function(enaplot, supplied, network, node.positions, adjacency.key, |
|
| 70 |
colors, edge_type, show.all.nodes, threshold, |
|
| 71 |
thin.lines.in.front, layers, labels, |
|
| 72 |
label.font.size, label.font.color, label.font.family, |
|
| 73 |
legend.name, dots) {
|
|
| 74 | 21x |
fn <- "ena.plot.network" |
| 75 | 21x |
.qe_ignored(fn, supplied, |
| 76 | 21x |
c("thickness", "opacity", "saturation", "scale.range", "node.size", "scale.weights"),
|
| 77 | 21x |
"qeviz's model-wide width and colour scaling replaces them") |
| 78 | 21x |
.qe_ignored(fn, supplied, c("label.offset", "legend.include.edges"), "not supported")
|
| 79 | 21x |
if ("edge_type" %in% supplied && !identical(edge_type, "line"))
|
| 80 | 1x |
.qe_ignored(fn, "edge_type", "edge_type", "edges are always solid lines") |
| 81 | 21x |
if ("thin.lines.in.front" %in% supplied && isFALSE(thin.lines.in.front))
|
| 82 | ! |
.qe_ignored(fn, "thin.lines.in.front", "thin.lines.in.front", "thin lines are always drawn on top") |
| 83 | ||
| 84 | 21x |
qe <- enaplot$qe |
| 85 | 21x |
if (is.data.frame(network) || is.matrix(network)) {
|
| 86 | ! |
nw <- as.data.frame(network, check.names = FALSE) |
| 87 | ! |
network <- colMeans(as.matrix(nw[, vapply(nw, is.numeric, logical(1)), drop = FALSE])) |
| 88 |
} |
|
| 89 | 21x |
network <- stats::setNames(as.numeric(unclass(network)), names(network)) |
| 90 |
# Same length rule as the plotly path: one weight per pair of nodes, or per |
|
| 91 |
# pair plus each node's self-connection (upper triangle with the diagonal). |
|
| 92 | 21x |
n_nodes <- NROW(node.positions) |
| 93 | 21x |
n_self <- n_nodes * (n_nodes + 1) / 2 |
| 94 | 21x |
if (!length(network) %in% c(choose(n_nodes, 2), n_self)) |
| 95 | 2x |
stop(paste0("Network vector needs to be of length ", choose(n_nodes, 2),
|
| 96 | 2x |
" (or ", n_self, " with self-connections)"), call. = FALSE) |
| 97 |
# Unnamed weights follow the adjacency key, exactly as the plotly path reads |
|
| 98 |
# them: the key given, else the one implied by node.positions' codes. |
|
| 99 | 19x |
if (!is.null(adjacency.key) || is.null(names(network))) {
|
| 100 | 2x |
codes <- if (is(node.positions, "ena.nodes")) as.character(node.positions$code) |
| 101 | 2x |
else rownames(node.positions) |
| 102 | 2x |
key <- as.matrix(if (!is.null(adjacency.key)) adjacency.key |
| 103 | 2x |
else namesToAdjacencyKey(codes, diagonal = length(network) == n_self && |
| 104 | 2x |
n_self != choose(n_nodes, 2))) |
| 105 | 2x |
names(network) <- paste(key[1, ], key[2, ], sep = " & ") |
| 106 |
} |
|
| 107 | ||
| 108 | 19x |
if ("node.positions" %in% supplied)
|
| 109 | ! |
qe <- qeviz::qe_nodes(qe, positions = node.positions) |
| 110 | 19x |
if ("labels" %in% supplied && !is.null(labels)) {
|
| 111 | 1x |
codes <- if (is(node.positions, "ena.nodes")) as.character(node.positions$code) |
| 112 | 1x |
else rownames(node.positions) |
| 113 | 1x |
qe <- qeviz::qe_nodes(qe, labels = stats::setNames(as.character(labels), codes)) |
| 114 |
} |
|
| 115 | 19x |
show <- if (!"edges" %in% layers) "always" else if (!"nodes" %in% layers) "never" else "auto" |
| 116 | 19x |
qe <- qeviz::qe_nodes(qe, show = show, unconnected = if (isFALSE(show.all.nodes)) "hide" else "dot") |
| 117 | 1x |
if (isTRUE(dots$labels.hide)) qe <- qeviz::qe_labels(qe, nodes = "off") |
| 118 | ||
| 119 | 19x |
thr <- threshold[is.finite(threshold)] |
| 120 | 18x |
if (!length(thr) || (length(thr) == 1 && thr[1] <= 0)) thr <- NULL |
| 121 |
# Magnification: a layer's own (add_network(edge.multiplier)), else the |
|
| 122 |
# plot's (ena.plotter multipliers), else ena.plot(multiplier) relative to |
|
| 123 |
# rENA's default of 5. |
|
| 124 | 19x |
mult <- enaplot$get("multiplier")
|
| 125 | 19x |
magnify <- if (!is.null(dots$magnify)) dots$magnify |
| 126 | 19x |
else if (!is.null(enaplot$get("magnify"))) enaplot$get("magnify")
|
| 127 | 19x |
else if (!is.null(mult) && mult != 5) mult / 5 |
| 128 | 19x |
if ("edges" %in% layers)
|
| 129 | 16x |
qe <- qeviz::qe_edges(qe, weights = network, colors = unname(.qe_hex(colors)), |
| 130 | 16x |
threshold = thr, name = legend.name, magnify = magnify) |
| 131 | 19x |
enaplot$qe <- .qe_fonts(qe, supplied, label.font.size, label.font.family, label.font.color) |
| 132 | 19x |
enaplot |
| 133 |
} |
|
| 134 | ||
| 135 |
# ── ena.plot.points ────────────────────────────────────────────────────────── |
|
| 136 |
.qe_plot_points <- function(enaplot, supplied, points, point.size, labels, shape, colors, |
|
| 137 |
label.font.size, label.font.color, label.font.family, texts, |
|
| 138 |
legend.name) {
|
|
| 139 | 3x |
fn <- "ena.plot.points" |
| 140 | 3x |
.qe_ignored(fn, supplied, c("confidence.interval.values", "outlier.interval.values",
|
| 141 | 3x |
"confidence.interval", "outlier.interval"), |
| 142 | 3x |
"per-point intervals are not supported; use ena.plot.group() for means") |
| 143 | 3x |
.qe_ignored(fn, supplied, c("label.offset", "label.group", "show.legend"), "not supported")
|
| 144 | ! |
if (is.null(points)) points <- enaplot$enaset$points |
| 145 | 3x |
df <- .qe_point_df(points, enaplot, |
| 146 | 3x |
ids = if (!is.null(labels) && length(labels) == NROW(points) && |
| 147 | 3x |
!(is.numeric(points) && is.null(dim(points)))) labels) |
| 148 | 3x |
qe <- qeviz::qe_points(enaplot$qe, points = df, |
| 149 | 3x |
color = unname(.qe_hex(colors)), |
| 150 | 3x |
shape = if ("shape" %in% supplied) shape,
|
| 151 | 3x |
labels = if (!is.null(texts)) as.character(texts), |
| 152 | 3x |
size = if ("point.size" %in% supplied) point.size / 2,
|
| 153 |
# legend.name names the point set (add_points() passes |
|
| 154 |
# one per group); calls with the same name replace it. |
|
| 155 | 3x |
name = if ("legend.name" %in% supplied) legend.name)
|
| 156 | 3x |
enaplot$qe <- .qe_fonts(qe, supplied, label.font.size, label.font.family, label.font.color) |
| 157 | 3x |
enaplot |
| 158 |
} |
|
| 159 | ||
| 160 |
# ── ena.plot.group ─────────────────────────────────────────────────────────── |
|
| 161 |
.qe_plot_group <- function(enaplot, supplied, points, method, labels, colors, shape, |
|
| 162 |
confidence.interval, outlier.interval, |
|
| 163 |
label.font.size, label.font.color, label.font.family) {
|
|
| 164 | 8x |
fn <- "ena.plot.group" |
| 165 | 8x |
if (!is.null(method) && !identical(method, "mean")) |
| 166 | 1x |
stop("ena.plot.group(): only method = \"mean\" is supported on the qeviz backend.",
|
| 167 | 1x |
call. = FALSE) |
| 168 | ! |
if (is.null(points)) stop("Points must be provided.")
|
| 169 | 7x |
.qe_ignored(fn, supplied, c("label.offset", "show.legend", "legend.name"), "not supported")
|
| 170 | 7x |
if (identical(outlier.interval, "crosshairs")) |
| 171 | ! |
.qe_ignored(fn, "outlier.interval", "outlier.interval", |
| 172 | ! |
"crosshairs are not available for the outlier interval; it is drawn as a box") |
| 173 | ||
| 174 | 7x |
df <- .qe_point_df(points, enaplot) |
| 175 | 7x |
cols <- if ("colors" %in% supplied) .qe_hex(colors)
|
| 176 |
# rENA draws one mean per distinct colour when colors is given per point. |
|
| 177 | 7x |
split <- if (length(cols) == nrow(df) && length(unique(cols)) > 1) unique(cols) else NULL |
| 178 | 7x |
groups <- if (is.null(split)) list(list(rows = seq_len(nrow(df)), color = cols[1], label = labels[1])) |
| 179 | 7x |
else lapply(seq_along(split), function(k) |
| 180 | 7x |
list(rows = which(cols == split[k]), color = split[k], |
| 181 | 7x |
label = if (length(labels) >= k) labels[k])) |
| 182 | 7x |
qe <- enaplot$qe |
| 183 | 7x |
for (g in groups) {
|
| 184 | 8x |
sub <- df[g$rows, , drop = FALSE] |
| 185 | 8x |
qe <- qeviz::qe_group(qe, points = sub, label = g$label, color = unname(g$color), |
| 186 | 8x |
shape = shape, |
| 187 | 8x |
intervals = if (nrow(sub) < 2) "none" else confidence.interval, |
| 188 | 8x |
outlier = nrow(sub) >= 2 && outlier.interval != "none") |
| 189 |
} |
|
| 190 | 7x |
enaplot$qe <- .qe_fonts(qe, supplied, label.font.size, label.font.family, label.font.color) |
| 191 | 7x |
enaplot |
| 192 |
} |
|
| 193 | ||
| 194 |
# ── Multipliers (ena.plotter, ena.plot.subtraction, add_network) ───────────── |
|
| 195 |
# The plotly path multiplies the weights. On qeviz the weights stay as they are |
|
| 196 |
# (so they sit correctly on the model-wide colour scale) and the multiplier |
|
| 197 |
# becomes the plot's magnification instead. |
|
| 198 | 24x |
.qe_weight_mult <- function(backend, k) if (identical(backend, "qeviz")) 1 else k |
| 199 |
.qe_magnify <- function(backend, k, labelled = k != 1) {
|
|
| 200 | 3x |
if (identical(backend, "qeviz") && isTRUE(labelled)) k else NULL |
| 201 |
} |
|
| 202 | ||
| 203 |
# ── check_range on qeviz ───────────────────────────────────────────────────── |
|
| 204 |
# Same rule as the plotly path: grow the symmetric range to 1.2x the largest |
|
| 205 |
# plotted point / mean when they exceed it, shrink when they use under half. |
|
| 206 |
.qe_check_range <- function(x) {
|
|
| 207 | 1x |
numbers <- as.numeric(sapply(x$plotted$points, function(p) max(abs(as.matrix(remove_meta_data(p$data[[1]])))))) |
| 208 | 1x |
means <- as.numeric(sapply(x$plotted$means, function(p) max(abs(as.numeric(p$data))))) |
| 209 | ! |
if (!length(numbers) && !length(means)) return(x) |
| 210 | 1x |
curr_max <- max(c(numbers, means)) |
| 211 | 1x |
m <- x$qe$model |
| 212 | 1x |
current <- x$qe$graph_opts[["range"]] |
| 213 | 1x |
current <- if (is.numeric(current)) current else {
|
| 214 | 1x |
nodes <- do.call(rbind, lapply(m$nodes$data, function(r) c(r[[m$x_col]], r[[m$y_col]]))) |
| 215 | 1x |
max(abs(nodes)) * 1.2 |
| 216 |
} |
|
| 217 | 1x |
if (curr_max * 1.2 > current || curr_max < current * 0.5) |
| 218 | 1x |
x$qe <- qeviz::qe_range(x$qe, curr_max * 1.2) |
| 219 | 1x |
x |
| 220 |
} |
|
| 221 | ||
| 222 |
# ── Trajectories / movies: plotly only ─────────────────────────────────────── |
|
| 223 |
# Not drawn on the qeviz backend (qeviz has no trajectories yet). The call is a |
|
| 224 |
# no-op with a warning; the plotly path is unchanged. |
|
| 225 |
.qe_no_trajectories <- function(fn) {
|
|
| 226 | 4x |
warning(structure(class = c("rENA_qeviz_unsupported", "warning", "condition"), list(
|
| 227 | 4x |
message = sprintf(paste0("%s() is not drawn on the qeviz backend; the plot is returned ",
|
| 228 | 4x |
"unchanged. Use ena.plot(..., backend = \"plotly\") for trajectories."), fn), |
| 229 | 4x |
call = NULL))) |
| 230 |
} |
| 1 |
accumulate.data <- function(enadata) {
|
|
| 2 | 147x |
dfDT <- enadata$raw; |
| 3 | ||
| 4 | 147x |
units.used <- enadata$get("units.used")
|
| 5 | 147x |
units.by <- enadata$get("units.by")
|
| 6 | 147x |
trajectory.by <- enadata$get("trajectory.by")
|
| 7 | 147x |
codes <- enadata$codes |
| 8 | ||
| 9 | 147x |
if (is.data.frame(codes)) {
|
| 10 | 1x |
codes <- colnames(codes); |
| 11 |
} |
|
| 12 | ||
| 13 | 147x |
conversations.by <- enadata$get("conversations.by")
|
| 14 | 147x |
window <- enadata$get("window.size")
|
| 15 |
# binaryStanzas <- F |
|
| 16 | 147x |
units.exclude <- enadata$get("units.exclude")
|
| 17 | ||
| 18 | 147x |
if(is.null(trajectory.by)) {
|
| 19 | 147x |
trajectory.by = conversations.by |
| 20 |
} |
|
| 21 | ||
| 22 |
### should work to determine if binary is desired |
|
| 23 | 147x |
binary <- T; |
| 24 | 147x |
if (!identical(enadata$get("weight.by"), "binary")) {
|
| 25 | 14x |
binary <- F |
| 26 |
} |
|
| 27 |
else {
|
|
| 28 | 133x |
binary <- T |
| 29 |
} |
|
| 30 | ||
| 31 |
### We need data |
|
| 32 | 147x |
if (is.null(dfDT) || nrow(dfDT) < 1) {
|
| 33 | 1x |
stop("The provided data is NULL")
|
| 34 |
} |
|
| 35 | ||
| 36 |
### |
|
| 37 |
# We need a data.table, it's worth it. |
|
| 38 |
### |
|
| 39 | 146x |
if(!data.table::is.data.table(dfDT)) {
|
| 40 | 1x |
dfDT <- data.table::as.data.table(dfDT) |
| 41 |
} |
|
| 42 | ||
| 43 |
### |
|
| 44 |
# Make a copy of the data for safe usage |
|
| 45 |
### |
|
| 46 | 146x |
dfDT_codes <- data.table::copy(dfDT) |
| 47 | ||
| 48 |
### |
|
| 49 |
# Create a column representing the ENA_UNIT as defined |
|
| 50 |
# by the the `units.by` parameter |
|
| 51 |
### |
|
| 52 | 146x |
if(!"ENA_UNIT" %in% colnames(dfDT_codes)) {
|
| 53 | 1x |
dfDT_codes$ENA_UNIT <- enadata$raw$ENA_UNIT <- merge_columns_c( |
| 54 | 1x |
dfDT_codes, |
| 55 | 1x |
cols = units.by, sep = "::" |
| 56 |
) |
|
| 57 |
} |
|
| 58 | ||
| 59 |
## |
|
| 60 |
# String vector of codesnames representing the names of the co-occurrences |
|
| 61 |
## |
|
| 62 | 146x |
vL <- length(codes); |
| 63 | 146x |
adjacency.length <- ( (vL * (vL + 1)) / 2) - vL ; |
| 64 | 146x |
codedTriNames <- paste("adjacency.code",rep(1:adjacency.length), sep=".");
|
| 65 | ||
| 66 | 146x |
initial_cols <- c(units.by, codes) |
| 67 | 146x |
just_codes <- c(codes) |
| 68 | ||
| 69 |
## |
|
| 70 |
# Accumulated windows appended to the end of each row |
|
| 71 |
# |
|
| 72 |
# FIXME: Don't append on the results to the initial data.table, |
|
| 73 |
# keep a separate to lookup the results for the co-occurred |
|
| 74 |
# values later on. |
|
| 75 |
## |
|
| 76 | 146x |
if (window$back == 1 && window$forward == 0) {
|
| 77 | 49x |
dfDT.co.occurrences <- dfDT_codes[,{
|
| 78 | 49x |
ocs <- data.table::as.data.table( |
| 79 | 49x |
rows_to_co_occurrences( |
| 80 | 49x |
.SD[,.SD,.SDcols=codes, with=T], |
| 81 | 49x |
binary = binary |
| 82 |
) |
|
| 83 |
); |
|
| 84 | ||
| 85 |
# Return value from data.table back to dfDT.co.occurrences |
|
| 86 | 49x |
data.table::data.table(.SD, ocs) |
| 87 |
}, |
|
| 88 | 49x |
.SDcols = c(codes, conversations.by, trajectory.by), |
| 89 | 49x |
with = T |
| 90 |
] |
|
| 91 | ||
| 92 |
### Generate the ENA_UNIT column |
|
| 93 | 49x |
dfDT.co.occurrences$ENA_UNIT <- dfDT_codes$ENA_UNIT |
| 94 | ||
| 95 |
### Keep original columns used for units |
|
| 96 | 49x |
dfDT.co.occurrences[, (units.by) := dfDT_codes[, .SD, .SDcols = units.by]] |
| 97 |
} |
|
| 98 | 97x |
else if (window$back == "Conversation") {
|
| 99 |
### |
|
| 100 |
# First sum all lines by conversation and unit to get vectors of codes |
|
| 101 |
# occurring in the whole conversation for each unit |
|
| 102 |
### |
|
| 103 | 2x |
dfDT.conv.sum <- dfDT_codes[, |
| 104 | 2x |
lapply(.SD, sum), by = c(unique(conversations.by)), |
| 105 | 2x |
.SDcols = c(codes), |
| 106 | 2x |
with = T |
| 107 |
] |
|
| 108 | ||
| 109 |
### |
|
| 110 |
# Convert each units converstation sums into adjacency vectors |
|
| 111 |
### |
|
| 112 |
# browser() |
|
| 113 | 2x |
dfDT.co.occurrences <- dfDT.conv.sum[,{
|
| 114 | 2x |
ocs = data.table::as.data.table(rows_to_co_occurrences(.SD[,.SD,.SDcols=codes, with=T], binary = binary)); |
| 115 | 2x |
data.table::data.table(.SD,ocs, ENA_UNIT=merge_columns_c(.SD, cols = units.by, sep="::")) |
| 116 |
}, |
|
| 117 | 2x |
.SDcols=unique(c(codes, conversations.by, trajectory.by, units.by)), |
| 118 | 2x |
with=T |
| 119 |
]; |
|
| 120 |
} |
|
| 121 |
else {
|
|
| 122 |
## parallell: https://stackoverflow.com/questions/14759905/data-table-and-parallel-computing |
|
| 123 |
### Calculate occurrences of code within the provided window |
|
| 124 | ||
| 125 |
# if(enadata$function.params$in.par == T) {
|
|
| 126 |
# grainSize = ifelse(!is.null(enadata$function.params$grainSize), enadata$function.params$grainSize, 10); |
|
| 127 |
# dfDT.co.occurrences = dfDT_codes[, |
|
| 128 |
# (codedTriNames) := try_one( |
|
| 129 |
# .SD[,.SD, .SDcols=just_codes], |
|
| 130 |
# window=window$back, |
|
| 131 |
# binary = binary, |
|
| 132 |
# grainSize = grainSize |
|
| 133 |
# ), |
|
| 134 |
# by=conversations.by, |
|
| 135 |
# .SDcols=initial_cols, |
|
| 136 |
# with=T |
|
| 137 |
# ]; |
|
| 138 |
# |
|
| 139 |
# } else {
|
|
| 140 |
# ,binaryStanzas = binaryStanzas |
|
| 141 | 95x |
dfDT.co.occurrences <- dfDT_codes[, |
| 142 | 95x |
(codedTriNames) := ref_window_df( |
| 143 | 95x |
.SD[, .SD, .SDcols = just_codes], |
| 144 | 95x |
windowSize = window$back, |
| 145 | 95x |
windowForward = window$forward, |
| 146 | 95x |
binary = binary |
| 147 |
), |
|
| 148 | 95x |
by = conversations.by, |
| 149 | 95x |
.SDcols = initial_cols, |
| 150 | 95x |
with = T |
| 151 |
]; |
|
| 152 |
# } |
|
| 153 |
} |
|
| 154 |
# browser() |
|
| 155 | ||
| 156 | 146x |
if( is.function(enadata$get("weight.by")) ) {
|
| 157 | 10x |
cols <- colnames(dfDT.co.occurrences)[ |
| 158 | 10x |
grep("adjacency.code", colnames(dfDT.co.occurrences))
|
| 159 |
] |
|
| 160 | 10x |
dfDT.co.occurrences <- dfDT.co.occurrences[, |
| 161 | 10x |
(cols) := lapply( |
| 162 | 10x |
.SD, |
| 163 | 10x |
enadata$get("weight.by")
|
| 164 |
), |
|
| 165 | 10x |
.SDcols = cols, |
| 166 | 10x |
by = 1:nrow(dfDT.co.occurrences) |
| 167 |
] |
|
| 168 |
} |
|
| 169 | ||
| 170 | ||
| 171 |
### |
|
| 172 |
# Convert the generic `V` names to corresponding `adjacency.vector` names |
|
| 173 |
### |
|
| 174 | 146x |
vCols <- grep("V\\d+", colnames(dfDT.co.occurrences))
|
| 175 | 146x |
if(length(vCols) == length(codedTriNames)) {
|
| 176 | 51x |
colnames(dfDT.co.occurrences)[vCols] <- codedTriNames |
| 177 |
} |
|
| 178 | ||
| 179 |
## |
|
| 180 |
# If units aren't supplied, use all available |
|
| 181 |
## |
|
| 182 | 146x |
if (is.null(units.used)) {
|
| 183 | 146x |
units.used <- dfDT_codes$ENA_UNIT |
| 184 |
} |
|
| 185 | ||
| 186 | ||
| 187 |
### |
|
| 188 |
# Trajectory Checks |
|
| 189 |
### |
|
| 190 | ||
| 191 |
## Not a Trajectory |
|
| 192 | 146x |
if (enadata$model == "EndPoint") {
|
| 193 |
### |
|
| 194 |
# Sum each unit found in dfDT.co.occurrences |
|
| 195 |
### |
|
| 196 | 134x |
dfDT.summed.units <- dfDT.co.occurrences[ENA_UNIT %in% units.used,lapply(.SD,sum),by=units.by,.SDcols=codedTriNames] |
| 197 | 134x |
dfDT.summed.units$ENA_UNIT <- merge_columns_c(dfDT.summed.units, units.by, sep="::"); |
| 198 | ||
| 199 | 134x |
enadata$unit.names <- dfDT.summed.units$ENA_UNIT; |
| 200 |
} |
|
| 201 |
## Trajectory |
|
| 202 |
else {
|
|
| 203 |
## First sum all units within each Trajectory Group (trajectory.by) |
|
| 204 | 12x |
dfDT.summed.traj.by <- dfDT.co.occurrences[ |
| 205 | 12x |
ENA_UNIT %in% units.used, |
| 206 |
{
|
|
| 207 | 2128x |
sums <- lapply(.SD, sum) |
| 208 | 2128x |
data.frame(ENA_ROW_IDX = .GRP, sums); # Return value |
| 209 |
}, |
|
| 210 | 12x |
by = c(units.by, trajectory.by), |
| 211 | 12x |
.SDcols = (codedTriNames) |
| 212 |
]; |
|
| 213 | 12x |
dfDT.summed.traj.by$ENA_UNIT <- merge_columns_c( |
| 214 | 12x |
dfDT.summed.traj.by, units.by, sep = "::" |
| 215 |
) |
|
| 216 | 12x |
dfDT.summed.traj.by$TRAJ_UNIT <- merge_columns_c( |
| 217 | 12x |
dfDT.summed.traj.by, trajectory.by, sep = "::" |
| 218 |
); |
|
| 219 | ||
| 220 | 12x |
enadata$trajectories$step <- dfDT.summed.traj.by$TRAJ_UNIT; |
| 221 | ||
| 222 |
# Accumulated |
|
| 223 | 12x |
if (enadata$model == opts$TRAJ_TYPES[1]) {
|
| 224 | 7x |
dfDT.summed.units <- dfDT.summed.traj.by[ |
| 225 | 7x |
ENA_UNIT %in% unique(units.used), {
|
| 226 | 198x |
cols <- colnames(.SD) |
| 227 | 198x |
ENA_UNIT <- paste(as.character(.BY), collapse = "::") |
| 228 | 198x |
TRAJ_UNIT <- .SD[, c(trajectory.by), with = F] |
| 229 | 198x |
inc_cols <- cols[! cols %in% c(trajectory.by, "ENA_ROW_IDX")] |
| 230 | 198x |
lag <- ref_window_lag(.SD[, .SD, .SDcols = inc_cols], .N) |
| 231 | ||
| 232 | 198x |
data.table::data.table( |
| 233 | 198x |
ENA_ROW_IDX, |
| 234 | 198x |
TRAJ_UNIT, lag, ENA_UNIT = ENA_UNIT |
| 235 |
) |
|
| 236 |
}, |
|
| 237 | 7x |
by = c(units.by), |
| 238 | 7x |
.SDcols = c(codedTriNames, trajectory.by, "ENA_ROW_IDX") |
| 239 |
] |
|
| 240 | 7x |
dfDT.summed.units$TRAJ_UNIT <- merge_columns_c( |
| 241 | 7x |
dfDT.summed.units, trajectory.by, sep = "::" |
| 242 |
) |
|
| 243 |
} |
|
| 244 |
# Non-accumulated |
|
| 245 | 5x |
else if (enadata$model == opts$TRAJ_TYPES[2]) {
|
| 246 | 3x |
dfDT.summed.units <- dfDT.summed.traj.by; |
| 247 |
} |
|
| 248 |
else {
|
|
| 249 | 2x |
stop("Unsupported Model type.");
|
| 250 |
} |
|
| 251 | ||
| 252 | 10x |
dfDT.summed.units$ENA_UNIT <- merge_columns_c( |
| 253 | 10x |
dfDT.summed.units, units.by, sep = "::" |
| 254 |
) |
|
| 255 |
} |
|
| 256 |
### |
|
| 257 |
# END: Trajectory Checks |
|
| 258 |
### |
|
| 259 | ||
| 260 |
### |
|
| 261 |
# Name the rows and columns accordingly |
|
| 262 |
### |
|
| 263 | 144x |
colnames(dfDT.summed.units)[ |
| 264 | 144x |
grep("V\\d+", colnames(dfDT.summed.units))
|
| 265 | 144x |
] <- codedTriNames |
| 266 | ||
| 267 |
### |
|
| 268 |
# Set attributes |
|
| 269 |
# |
|
| 270 |
# TODO Most of this should be moved to a more prominent spot on ENAdata |
|
| 271 |
### |
|
| 272 | 144x |
adjRows <- triIndices(length(codes)) + 1 |
| 273 | 144x |
codedRow1 <- codes[adjRows[1, ]] |
| 274 | 144x |
codedRow2 <- codes[adjRows[2, ]] |
| 275 | 144x |
attr(dfDT.summed.units, "adjacency.matrix") <- rbind(codedRow1, codedRow2) |
| 276 | 144x |
attr(dfDT.summed.units, "adjacency.codes") <- codedTriNames |
| 277 | 144x |
attr(dfDT.summed.units, opts$UNIT_NAMES) <- dfDT.summed.units[, |
| 278 | 144x |
.SD, with = T, .SDcols = units.by] |
| 279 | ||
| 280 | 144x |
enadata$adjacency.matrix <- rbind(codedRow1, codedRow2) |
| 281 | 144x |
enadata$accumulated.adjacency.vectors <- dfDT.co.occurrences |
| 282 | 144x |
enadata$adjacency.vectors <- dfDT.summed.units |
| 283 |
### |
|
| 284 |
# END: Set attributes |
|
| 285 |
### |
|
| 286 | ||
| 287 | 144x |
return(enadata); |
| 288 |
} |
| 1 |
## |
|
| 2 |
#' @title Plot of ENA set groups |
|
| 3 |
#' |
|
| 4 |
#' @description Plot a point based on a summary statistic computed from a given method (typically, mean) for a set of points in a projected ENA space |
|
| 5 |
#' |
|
| 6 |
#' @details Plots a point based on a summary statistic for a group (typically, mean) |
|
| 7 |
#' |
|
| 8 |
#' @export |
|
| 9 |
#' |
|
| 10 |
#' @param enaplot \code{\link{ENAplot}} object to use for plotting
|
|
| 11 |
#' @param points A matrix or data.frame where columns contain coordinates of points in a projected ENA space |
|
| 12 |
#' @param method A function for computing a summary statistic for each column of points |
|
| 13 |
#' @param labels A character which will be the label for the group's point |
|
| 14 |
#' @param colors A character, determines color of the group's point, default: enaplot$color |
|
| 15 |
#' @param shape A character, determines shape of the group's point, choices: square, triangle, diamond, circle, default: square |
|
| 16 |
#' @param confidence.interval A character that determines how the confidence interval is displayed, choices: none, box, crosshair, default: none |
|
| 17 |
#' @param outlier.interval A character that determines how outlier interval is displayed, choices: none, box, crosshair, default: none |
|
| 18 |
#' @param label.offset character: top left (default), top center, top right, middle left, middle center, middle right, bottom left, bottom center, bottom right |
|
| 19 |
#' @param label.font.size An integer which determines the font size for label, default: enaplot$font.size |
|
| 20 |
#' @param label.font.color A character which determines the color of label, default: enaplot$font.color |
|
| 21 |
#' @param label.font.family A character which determines font type, choices: Arial, Courier New, Times New Roman, default: enaplot$font.family |
|
| 22 |
#' @param show.legend Logical indicating whether to show the point labels in the in legend |
|
| 23 |
#' @param legend.name Character indicating the name to show above the plot legend |
|
| 24 |
#' @param ... Additional parameters |
|
| 25 |
#' |
|
| 26 |
#' @seealso \code{\link{ena.plot}}, \code{ena.plot.points}
|
|
| 27 |
#' |
|
| 28 |
#' @examples |
|
| 29 |
#' data(RS.data) |
|
| 30 |
#' |
|
| 31 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 32 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 33 |
#' |
|
| 34 |
#' accum = ena.accumulate.data( |
|
| 35 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 36 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 37 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 38 |
#' codes = RS.data[,codeNames], |
|
| 39 |
#' window.size.back = 4 |
|
| 40 |
#' ) |
|
| 41 |
#' |
|
| 42 |
#' set = ena.make.set( |
|
| 43 |
#' enadata = accum, |
|
| 44 |
#' rotation.by = ena.rotate.by.mean, |
|
| 45 |
#' rotation.params = list( |
|
| 46 |
#' accum$meta.data$Condition=="FirstGame", |
|
| 47 |
#' accum$meta.data$Condition=="SecondGame" |
|
| 48 |
#' ) |
|
| 49 |
#' ) |
|
| 50 |
#' |
|
| 51 |
#' plot = ena.plot(set) |
|
| 52 |
#' |
|
| 53 |
#' unitNames = set$enadata$units |
|
| 54 |
#' |
|
| 55 |
#' ### Plot Condition 1 Group Mean |
|
| 56 |
#' plot = ena.plot.group(plot, as.matrix(set$points$Condition$FirstGame), labels = "FirstGame", |
|
| 57 |
#' colors = "red", confidence.interval = "box") |
|
| 58 |
#' |
|
| 59 |
#' ### plot Condition 2 Group Mean |
|
| 60 |
#' plot = ena.plot.group(plot, as.matrix(set$points$Condition$SecondGame), labels = "SecondGame", |
|
| 61 |
#' colors = "blue", confidence.interval = "box") |
|
| 62 |
#' |
|
| 63 |
#' print(plot); |
|
| 64 |
#' |
|
| 65 |
#' @return The \code{\link{ENAplot}} provided to the function, with its plot updated to include the new group point.
|
|
| 66 |
## |
|
| 67 |
ena.plot.group <- function( |
|
| 68 |
enaplot, |
|
| 69 |
points = NULL, |
|
| 70 |
method = "mean", |
|
| 71 |
labels = NULL, |
|
| 72 |
colors = default.colors[1], |
|
| 73 |
shape = c("square", "triangle-up", "diamond", "circle"),
|
|
| 74 |
confidence.interval = c("none", "crosshairs", "box"),
|
|
| 75 |
outlier.interval = c("none", "crosshairs", "box"),
|
|
| 76 |
label.offset = "bottom right", |
|
| 77 |
label.font.size = NULL, |
|
| 78 |
label.font.color = NULL, |
|
| 79 |
label.font.family = NULL, |
|
| 80 |
show.legend = T, |
|
| 81 |
legend.name = NULL, |
|
| 82 |
... |
|
| 83 |
) {
|
|
| 84 | 11x |
shape = match.arg(shape); |
| 85 | 11x |
confidence.interval = match.arg(confidence.interval); |
| 86 | 11x |
outlier.interval = match.arg(outlier.interval); |
| 87 | ||
| 88 | 11x |
if (.qe_backend(enaplot)) |
| 89 | 8x |
return(.qe_plot_group(enaplot, supplied = names(match.call())[-1], |
| 90 | 8x |
points = points, method = method, labels = labels, colors = colors, shape = shape, |
| 91 | 8x |
confidence.interval = confidence.interval, outlier.interval = outlier.interval, |
| 92 | 8x |
label.font.size = label.font.size, label.font.color = label.font.color, |
| 93 | 8x |
label.font.family = label.font.family)) |
| 94 | ||
| 95 | 3x |
if(is.null(points)) {
|
| 96 | 1x |
stop("Points must be provided.");
|
| 97 |
} |
|
| 98 | 2x |
else if(is(points, "ena.points")) {
|
| 99 | 2x |
points = remove_meta_data(points) |
| 100 |
} |
|
| 101 | ||
| 102 |
### problem if outlier and confidence intervals selected for crosshair |
|
| 103 | 2x |
if(confidence.interval == "crosshairs" && outlier.interval == "crosshairs") {
|
| 104 | 1x |
message("Confidence Interval and Outlier Interval cannot both be crosshair. Plotting Outlier Interval as box");
|
| 105 | 1x |
outlier.interval = "box"; |
| 106 |
} |
|
| 107 | ||
| 108 |
### if group more than one row, combine to mean |
|
| 109 | 2x |
confidence.interval.values = NULL; |
| 110 | 2x |
outlier.interval.values = NULL; |
| 111 |
if( |
|
| 112 | 2x |
(is(points, "data.frame") || is(points, "matrix")) && |
| 113 | 2x |
nrow(points) > 1 |
| 114 |
) {
|
|
| 115 | 2x |
if(is.null(method) || method == "mean") {
|
| 116 | 2x |
if(confidence.interval != "none") {
|
| 117 | 1x |
confidence.interval.values = matrix( |
| 118 | 1x |
c(as.vector(t.test(points[,1], conf.level = 0.95)$conf.int), as.vector(t.test(points[,2], conf.level = 0.95)$conf.int)), |
| 119 | 1x |
ncol=2 |
| 120 |
); |
|
| 121 |
} |
|
| 122 | 2x |
if(outlier.interval != "none") {
|
| 123 | 1x |
outlier.interval.values = c(IQR(points[,1]), IQR(points[,2])) * 1.5; |
| 124 | 1x |
outlier.interval.values = matrix(rep(outlier.interval.values, 2), ncol = 2, byrow = T) * c(-1, 1) |
| 125 |
} |
|
| 126 | ||
| 127 | 2x |
if(length(unique(colors)) > 1) {
|
| 128 | 1x |
points = t(sapply(unique(colors), function(color) colMeans(points[color == colors,]), simplify = T)) |
| 129 | 1x |
colors = unique(colors) |
| 130 | 1x |
attr(enaplot, "means") <- length(attr(enaplot, "means")) + length(colors) |
| 131 |
} else {
|
|
| 132 | 1x |
points = colMeans(points); |
| 133 | 1x |
attr(enaplot, "means") <- length(attr(enaplot, "means")) + 1 |
| 134 |
} |
|
| 135 |
} |
|
| 136 |
else {
|
|
| 137 | ! |
if(confidence.interval != "none") warning("Confidence Intervals can only be used when method=`mean`")
|
| 138 | ! |
if(outlier.interval != "none") warning("Outlier Intervals can only be used when method=`mean`")
|
| 139 | ||
| 140 | ! |
points = apply(points, 2, function(x) do.call(method, list(x)) ) |
| 141 | ! |
attr(enaplot, "means") <- length(attr(enaplot, "means")) + 1 |
| 142 |
} |
|
| 143 |
} |
|
| 144 | ||
| 145 | 2x |
enaplot <- ena.plot.points( |
| 146 | 2x |
enaplot, |
| 147 | 2x |
points = points, |
| 148 | 2x |
labels = labels, |
| 149 | 2x |
colors = colors, |
| 150 | 2x |
shape = shape, |
| 151 | 2x |
confidence.interval = confidence.interval, |
| 152 | 2x |
confidence.interval.values = confidence.interval.values, |
| 153 | 2x |
outlier.interval = outlier.interval, |
| 154 | 2x |
outlier.interval.values = outlier.interval.values, |
| 155 | 2x |
label.offset = label.offset, |
| 156 | 2x |
label.font.size = label.font.size, |
| 157 | 2x |
label.font.color = label.font.color, |
| 158 | 2x |
label.font.family = label.font.family, |
| 159 | 2x |
show.legend = show.legend, |
| 160 | 2x |
legend.name = legend.name, |
| 161 |
... |
|
| 162 |
) |
|
| 163 | 2x |
return(enaplot) |
| 164 | ||
| 165 |
# |
|
| 166 |
# group.layout = data.frame(dfDT.points); |
|
| 167 |
# |
|
| 168 |
# ### INTERVAL CALCULATIONS |
|
| 169 |
# error = NULL; |
|
| 170 |
# lines = list(); |
|
| 171 |
# |
|
| 172 |
# if(confidence.interval == "crosshair") {
|
|
| 173 |
# ci.x = t.test(points.raw, conf.level = .95)$conf.int[1]; |
|
| 174 |
# ci.y = t.test(points.raw, conf.level = .95)$conf.int[2]; |
|
| 175 |
# error = list( |
|
| 176 |
# x = list(type = "data", array = ci.x), |
|
| 177 |
# y = list(type = "data", array = ci.y) |
|
| 178 |
# ) |
|
| 179 |
# } else if(outlier.interval == "crosshair") {
|
|
| 180 |
# oi.x = IQR(points.raw$V1) * 1.5; |
|
| 181 |
# oi.y = IQR(points.raw$V2) * 1.5; |
|
| 182 |
# error = list( |
|
| 183 |
# x = list(type = "data", array = oi.x), |
|
| 184 |
# y = list(type = "data", array = oi.y) |
|
| 185 |
# ) |
|
| 186 |
# } |
|
| 187 |
# |
|
| 188 |
# if(confidence.interval == "box") {
|
|
| 189 |
# |
|
| 190 |
# conf.ints = t.test(points.raw, conf.level = .95)$conf.int; |
|
| 191 |
# dfDT.points[,c("ci.x", "ci.y") := .(conf.ints[1], conf.ints[2])]
|
|
| 192 |
# |
|
| 193 |
# #add cols for coordinates of CI lines |
|
| 194 |
# dfDT.points[, c("ci.x1", "ci.x2", "ci.y1", "ci.y2") := .(V1 - ci.x, V1 + ci.x, V2 - ci.y, V2 + ci.y)]
|
|
| 195 |
# |
|
| 196 |
# lines.CI = apply(dfDT.points,1,function(x) {
|
|
| 197 |
# list( |
|
| 198 |
# "type" = "square", |
|
| 199 |
# "line" = list( |
|
| 200 |
# width = 1, |
|
| 201 |
# color = color, |
|
| 202 |
# dash="dash" |
|
| 203 |
# ), |
|
| 204 |
# "xref" = "x", |
|
| 205 |
# "yref" = "y", |
|
| 206 |
# "x0" = x[['ci.x1']], |
|
| 207 |
# "x1" = x[['ci.x2']], |
|
| 208 |
# "y0" = x[['ci.y1']], |
|
| 209 |
# "y1" = x[['ci.y2']] |
|
| 210 |
# ); |
|
| 211 |
# }); |
|
| 212 |
# lines = lines.CI; |
|
| 213 |
# } |
|
| 214 |
# if(outlier.interval == "box") {
|
|
| 215 |
# |
|
| 216 |
# oi.x = IQR(points.raw$V1) * 1.5; |
|
| 217 |
# oi.y = IQR(points.raw$V2) * 1.5; |
|
| 218 |
# |
|
| 219 |
# dfDT.points[,c("oi.x", "oi.y") := .(oi.x, oi.y)]
|
|
| 220 |
# |
|
| 221 |
# #add cols for coordinates of CI lines |
|
| 222 |
# dfDT.points[, c("oi.x1", "oi.x2", "oi.y1", "oi.y2") := .(V1 - oi.x, V1 + oi.x, V2 - oi.y, V2 + oi.y)]
|
|
| 223 |
# |
|
| 224 |
# lines.OI = apply(dfDT.points,1,function(x) {
|
|
| 225 |
# list( |
|
| 226 |
# "type" = "square", |
|
| 227 |
# "line" = list( |
|
| 228 |
# width = 1, |
|
| 229 |
# color = color, |
|
| 230 |
# dash="dash" |
|
| 231 |
# ), |
|
| 232 |
# "xref" = "x", |
|
| 233 |
# "yref" = "y", |
|
| 234 |
# "x0" = x[['oi.x1']], |
|
| 235 |
# "x1" = x[['oi.x2']], |
|
| 236 |
# "y0" = x[['oi.y1']], |
|
| 237 |
# "y1" = x[['oi.y2']] |
|
| 238 |
# ); |
|
| 239 |
# }); |
|
| 240 |
# |
|
| 241 |
# lines = c(lines, lines.OI); |
|
| 242 |
# } |
|
| 243 |
# |
|
| 244 |
# |
|
| 245 |
# if(!is.null(error)) {
|
|
| 246 |
# #plot group w/ crosshair error bars |
|
| 247 |
# enaplot$plot = plotly::add_trace( |
|
| 248 |
# enaplot$plot, |
|
| 249 |
# data = group.layout, |
|
| 250 |
# type="scatter", |
|
| 251 |
# x = ~V1, y = ~V2, |
|
| 252 |
# mode="markers", |
|
| 253 |
# marker = list( |
|
| 254 |
# symbol = shape, |
|
| 255 |
# color = color, |
|
| 256 |
# size = size |
|
| 257 |
# ), |
|
| 258 |
# error_x = error$x, |
|
| 259 |
# error_y = error$y, |
|
| 260 |
# showlegend = F, |
|
| 261 |
# text = label, |
|
| 262 |
# hoverinfo = "text+x+y" |
|
| 263 |
# ) |
|
| 264 |
# } else {
|
|
| 265 |
# #plot group w/o crosshair error bars |
|
| 266 |
# enaplot$plot = plotly::add_trace( |
|
| 267 |
# enaplot$plot, |
|
| 268 |
# data = group.layout, |
|
| 269 |
# type="scatter", |
|
| 270 |
# x = ~V1, y = ~V2, |
|
| 271 |
# mode="markers", |
|
| 272 |
# marker = list( |
|
| 273 |
# symbol = shape, #c(rep("circle",nrow(data)),rep("square", ifelse(!is.null(dfDT.groups), nrow(dfDT.groups), 0))),
|
|
| 274 |
# color = color, |
|
| 275 |
# #size = c(rep(unit.size * unit.size.multiplier, nrow(data)), rep(group.size, ifelse(!is.null(dfDT.groups),nrow(dfDT.groups), 0))) |
|
| 276 |
# size = size |
|
| 277 |
# ), |
|
| 278 |
# showlegend = F, |
|
| 279 |
# text = label, |
|
| 280 |
# hoverinfo = "text+x+y" |
|
| 281 |
# ) |
|
| 282 |
# } |
|
| 283 |
# |
|
| 284 |
# ##### WEIGHTING OFFSET |
|
| 285 |
# if(is.null(label.offset)) { label.offset = c(.05,.05) }
|
|
| 286 |
# else label.offset = c(label.offset[1] * 0.1, label.offset[2] * 0.1) |
|
| 287 |
# |
|
| 288 |
# enaplot$plot = plotly::add_annotations( |
|
| 289 |
# enaplot$plot, |
|
| 290 |
# x = group.layout$V1[1] + label.offset[1], |
|
| 291 |
# y = group.layout$V2[1] + label.offset[2], |
|
| 292 |
# text = label, |
|
| 293 |
# font = text.info, |
|
| 294 |
# xref = "x", |
|
| 295 |
# yref = "y", |
|
| 296 |
# ax = label.offset[1], |
|
| 297 |
# ay = label.offset[2], |
|
| 298 |
# #xanchor = "left", |
|
| 299 |
# showarrow = F |
|
| 300 |
# ); |
|
| 301 |
# |
|
| 302 |
# enaplot$plot = plotly::layout( |
|
| 303 |
# enaplot$plot, |
|
| 304 |
# shapes = lines |
|
| 305 |
# #annotations = label.info |
|
| 306 |
# ) |
|
| 307 |
# |
|
| 308 |
# return(enaplot); |
|
| 309 |
} |
| 1 |
##### |
|
| 2 |
#' @title Plot of ENA trajectories |
|
| 3 |
#' |
|
| 4 |
#' @description Function used to plot trajectories |
|
| 5 |
#' |
|
| 6 |
#' @export |
|
| 7 |
#' |
|
| 8 |
#' @param enaplot \code{\link{ENAplot}} object to use for plotting
|
|
| 9 |
#' @param points dataframe of matrix - first two column are X and Y coordinates, each row is a point in a trajectory |
|
| 10 |
#' @param by vector used to subset points into individual trajectories, length nrow(points) |
|
| 11 |
#' @param names character vector - labels for each trajectory of points, length length(unique(by)) |
|
| 12 |
#' @param labels character vector - point labels, length nrow(points) |
|
| 13 |
#' @param labels.show A character choice: Always, Hover, Both. Default: Both |
|
| 14 |
#' @param colors A character vector, that determines marker color, default NULL results in |
|
| 15 |
#' alternating random colors. If single color is supplied, it will be used for all |
|
| 16 |
#' trajectories, otherwise the length of the supplied color vector should be equal |
|
| 17 |
#' to the length of the supplied names (i.e a color for each trajectory being plotted) |
|
| 18 |
#' @param shape A character which determines the shape of markers, choices: square, triangle, diamond, circle, default: circle |
|
| 19 |
#' @param label.offset A numeric vector of an x and y value to offset labels from the coordinates of the points |
|
| 20 |
#' @param label.font.size An integer which determines the font size for labels, default: enaplot$font.size |
|
| 21 |
#' @param label.font.color A character which determines the color of label font, default: enaplot$font.color |
|
| 22 |
#' @param label.font.family A character which determines font type, choices: Arial, Courier New, Times New Roman, default: enaplot$font.family |
|
| 23 |
#' @param default.hidden A logical indicating if the trajectories should start hidden (click on the legend to show them) Default: FALSE |
|
| 24 |
#' @param smooth Character choice: "none" (connect discrete points) or "poly" (fit orthogonal polynomial curve with LOOCV-2D). Default: "none" |
|
| 25 |
#' @param poly.degree Fixed integer polynomial degree (default NULL = auto-select via LOOCV-2D / AIC) |
|
| 26 |
#' @param poly.max.degree Maximum polynomial degree to search when auto-selecting (default: 3) |
|
| 27 |
#' @param poly.criterion Criterion for polynomial degree selection: "loocv" or "aic" |
|
| 28 |
#' @param poly.n.eval Number of evaluation points along the curve (default: 200) |
|
| 29 |
#' @param show.points Logical: whether to display original discrete points when smooth = "poly" (default: TRUE) |
|
| 30 |
#' @param show.curve Logical: whether to display smooth polynomial curve when smooth = "poly" (default: TRUE) |
|
| 31 |
#' |
|
| 32 |
#' @seealso \code{\link{ena.plot}}
|
|
| 33 |
#' |
|
| 34 |
#' @examples |
|
| 35 |
#' data(RS.data) |
|
| 36 |
#' |
|
| 37 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 38 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 39 |
#' |
|
| 40 |
#' accum = ena.accumulate.data( |
|
| 41 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 42 |
#' conversation = RS.data[,c("GroupName","ActivityNumber")],
|
|
| 43 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post","C.Change")],
|
|
| 44 |
#' codes = RS.data[,codeNames], |
|
| 45 |
#' window.size.back = 4, |
|
| 46 |
#' model = "A" |
|
| 47 |
#' ); |
|
| 48 |
#' |
|
| 49 |
#' set = ena.make.set(accum); |
|
| 50 |
#' |
|
| 51 |
#' ### get mean network plots |
|
| 52 |
#' first.game.lineweights = as.matrix(set$line.weights$Condition$FirstGame) |
|
| 53 |
#' first.game.mean = colMeans(first.game.lineweights) |
|
| 54 |
#' |
|
| 55 |
#' second.game.lineweights = as.matrix(set$line.weights$Condition$SecondGame) |
|
| 56 |
#' second.game.mean = colMeans(second.game.lineweights) |
|
| 57 |
#' |
|
| 58 |
#' subtracted.network = first.game.mean - second.game.mean |
|
| 59 |
#' |
|
| 60 |
#' # Plot dimension 1 against ActivityNumber metadata |
|
| 61 |
#' dim.by.activity = cbind( |
|
| 62 |
#' as.matrix(set$points)[,1], |
|
| 63 |
#' set$trajectories$ActivityNumber * .8/14-.4 #scale down to dimension 1 |
|
| 64 |
#' ) |
|
| 65 |
#' |
|
| 66 |
#' plot = ena.plot(set) |
|
| 67 |
#' plot = ena.plot.network(plot, network = subtracted.network, legend.name="Network") |
|
| 68 |
#' plot = ena.plot.trajectory( |
|
| 69 |
#' plot, |
|
| 70 |
#' points = dim.by.activity, |
|
| 71 |
#' names = unique(set$model$unit.label), |
|
| 72 |
#' by = set$trajectories$ENA_UNIT |
|
| 73 |
#' ); |
|
| 74 |
#' print(plot) |
|
| 75 |
#' |
|
| 76 |
#' @return The \code{\link{ENAplot}} provided to the function, with its plot updated to include the trajectories
|
|
| 77 |
#' ##### |
|
| 78 |
ena.plot.trajectory = function( |
|
| 79 |
enaplot, |
|
| 80 |
points, |
|
| 81 |
by = NULL, |
|
| 82 |
labels = NULL, |
|
| 83 |
labels.show = c("Always","Hover","Both"),
|
|
| 84 |
names = NULL, |
|
| 85 |
label.offset = NULL, |
|
| 86 |
label.font.size = enaplot$get("font.size"),
|
|
| 87 |
label.font.color = enaplot$get("font.color"),
|
|
| 88 |
label.font.family = c("Arial", "Courier New", "Times New Roman"),
|
|
| 89 |
shape = c("circle", "square", "triangle-up", "diamond"),
|
|
| 90 |
colors = NULL, |
|
| 91 |
default.hidden = FALSE, |
|
| 92 |
smooth = c("none", "poly"),
|
|
| 93 |
poly.degree = NULL, |
|
| 94 |
poly.max.degree = 3L, |
|
| 95 |
poly.criterion = c("loocv", "aic"),
|
|
| 96 |
poly.n.eval = 200L, |
|
| 97 |
show.points = TRUE, |
|
| 98 |
show.curve = TRUE |
|
| 99 |
) {
|
|
| 100 | 3x |
if (.qe_backend(enaplot)) {
|
| 101 | 1x |
.qe_no_trajectories("ena.plot.trajectory")
|
| 102 | 1x |
return(enaplot) |
| 103 |
} |
|
| 104 | ||
| 105 | 2x |
if(!is.character(label.font.family)) {
|
| 106 | ! |
label.font.size = enaplot$get("font.family");
|
| 107 |
} |
|
| 108 | 2x |
labels.show <- match.arg(labels.show); |
| 109 | 2x |
shape <- match.arg(shape); |
| 110 | 2x |
smooth <- match.arg(smooth); |
| 111 | 2x |
poly.criterion <- match.arg(poly.criterion); |
| 112 | ||
| 113 | 2x |
if(is.null(by)) {
|
| 114 | ! |
by <- list(all = rep(TRUE, nrow(points))); |
| 115 |
} |
|
| 116 | 2x |
if (is.character(by) && length(by) == 1L && by %in% names(points)) {
|
| 117 | ! |
by <- points[[by]] |
| 118 |
} |
|
| 119 | ||
| 120 | 2x |
if (is(points, "ena.matrix") || any(find_meta_cols(points))) {
|
| 121 | ! |
clean_points <- remove_meta_data(points) |
| 122 |
} else {
|
|
| 123 | 2x |
clean_points <- points |
| 124 |
} |
|
| 125 | ||
| 126 | 2x |
if(length(colors) == 1 && !is.null(names)) |
| 127 | ! |
colors <- rep(colors, length(names)) |
| 128 | ||
| 129 | 2x |
mode <- "lines+markers+text"; |
| 130 | 2x |
hoverinfo <- "x+y"; |
| 131 | 2x |
tbl <- data.table::as.data.table(clean_points); |
| 132 | 2x |
if (!is.null(labels)) {
|
| 133 | ! |
if (labels.show %in% c("Always","Both"))
|
| 134 | ! |
mode <- paste0(mode,"+text"); |
| 135 | ! |
if (labels.show %in% c("Hover","Both"))
|
| 136 | ! |
hoverinfo <- paste0(hoverinfo,"+text"); |
| 137 | ||
| 138 | ! |
tbl[, labels := labels] |
| 139 |
} |
|
| 140 | ||
| 141 | 2x |
if(!is.null(by)) {
|
| 142 | 2x |
if(is.character(by) && length(by) == nrow(tbl)) |
| 143 | 2x |
by <- as.factor(by) |
| 144 | ||
| 145 | 2x |
dfdt_trajs <- tbl[,{ data.table::data.table(lines = list(.SD)) }, by = by]
|
| 146 |
} else {
|
|
| 147 | ! |
dfdt_trajs <- tbl[,{ data.table::data.table(lines = list(.SD)) }]
|
| 148 |
} |
|
| 149 | ||
| 150 | 2x |
valid_label_offsets = c("top left","top center","top right","middle left",
|
| 151 | 2x |
"middle center","middle right","bottom left","bottom center", |
| 152 | 2x |
"bottom right") |
| 153 | 2x |
if(!all(label.offset %in% valid_label_offsets)) |
| 154 | ! |
stop(sprintf( "Unrecognized label.offsets: %s", |
| 155 | ! |
paste(unique(label.offset[!(label.offset %in% valid_label_offsets)]), |
| 156 | ! |
collapse = ", ") )) |
| 157 | ||
| 158 | 2x |
if(length(label.offset) == 1) |
| 159 | ! |
label.offset = rep(label.offset, nrow(dfdt_trajs)) |
| 160 | ||
| 161 | 2x |
if (!is.null(colors) && |
| 162 | 2x |
length(colors) > 1 && !is.null(names) && length(colors) != length(names) |
| 163 |
) {
|
|
| 164 | ! |
stop("Length of the colors must be 1 or the same length as by")
|
| 165 |
} |
|
| 166 | ||
| 167 | 2x |
for (x in 1:nrow(dfdt_trajs)) {
|
| 168 | 4x |
d <- as.data.frame(dfdt_trajs[x,]$lines[[1]]) |
| 169 | 4x |
d.names <- colnames(d) |
| 170 | 4x |
traj_name <- if (!is.null(names) && length(names) >= x) names[x] else paste0("Trajectory ", x)
|
| 171 | 4x |
traj_color <- if(!is.null(colors) && length(colors) >= x) colors[x] else NULL |
| 172 | ||
| 173 | 4x |
if (smooth == "poly" && nrow(d) >= 2L && ncol(d) >= 2L) {
|
| 174 | 2x |
curve_fit <- .fit_etm_poly_curve( |
| 175 | 2x |
points = d[, 1:2, drop = FALSE], |
| 176 | 2x |
degree = poly.degree, |
| 177 | 2x |
max_degree = poly.max.degree, |
| 178 | 2x |
criterion = poly.criterion |
| 179 |
) |
|
| 180 | ||
| 181 |
# Smooth curve evaluation |
|
| 182 | 2x |
if (isTRUE(show.curve)) {
|
| 183 | 2x |
curve_df <- .eval_etm_poly_curve(curve_fit, poly.n.eval) |
| 184 | ||
| 185 | 2x |
enaplot$plot = plotly::add_trace( |
| 186 | 2x |
enaplot$plot, |
| 187 | 2x |
data = curve_df, |
| 188 | 2x |
x = ~x, |
| 189 | 2x |
y = ~y, |
| 190 | 2x |
name = paste0(traj_name, " (fit deg ", curve_fit$degree, ")"), |
| 191 | 2x |
mode = "lines", |
| 192 | 2x |
hoverinfo = "none", |
| 193 | 2x |
showlegend = !isTRUE(show.points), |
| 194 | 2x |
line = list ( |
| 195 | 2x |
color = traj_color, |
| 196 | 2x |
width = 2.5 |
| 197 |
), |
|
| 198 | 2x |
visible = ifelse(default.hidden, "legendonly", TRUE) |
| 199 |
) |
|
| 200 |
} |
|
| 201 | ||
| 202 |
# Original discrete points |
|
| 203 | 2x |
if (isTRUE(show.points)) {
|
| 204 | 2x |
pts_mode <- if (!is.null(labels) && labels.show %in% c("Always", "Both")) "markers+text" else "markers"
|
| 205 | 2x |
enaplot$plot = plotly::add_trace( |
| 206 | 2x |
enaplot$plot, |
| 207 | 2x |
data = d, |
| 208 | 2x |
x = as.formula(paste0("~", d.names[1])),
|
| 209 | 2x |
y = as.formula(paste0("~", d.names[2])),
|
| 210 | 2x |
name = traj_name, |
| 211 | 2x |
mode = pts_mode, |
| 212 | 2x |
text = dfdt_trajs[x,]$lines[[1]]$labels, |
| 213 | 2x |
textposition = label.offset[x], |
| 214 | 2x |
hoverinfo = hoverinfo, |
| 215 | 2x |
showlegend = TRUE, |
| 216 | 2x |
marker = list ( |
| 217 | 2x |
symbol = shape, |
| 218 | 2x |
color = traj_color, |
| 219 | 2x |
size = 6 |
| 220 |
), |
|
| 221 | 2x |
textfont = list ( |
| 222 | 2x |
family = label.font.family, |
| 223 | 2x |
size = label.font.size, |
| 224 | 2x |
color = label.font.color |
| 225 |
), |
|
| 226 | 2x |
visible = ifelse(default.hidden, "legendonly", TRUE) |
| 227 |
) |
|
| 228 |
} |
|
| 229 |
} else {
|
|
| 230 |
# Standard piecewise linear trajectory |
|
| 231 | 2x |
enaplot$plot = plotly::add_trace( |
| 232 | 2x |
enaplot$plot, |
| 233 | 2x |
data = d, |
| 234 | 2x |
x = as.formula(paste0("~", d.names[1])),
|
| 235 | 2x |
y = as.formula(paste0("~", d.names[2])),
|
| 236 | 2x |
name = traj_name, |
| 237 | 2x |
mode = mode, |
| 238 | 2x |
text = dfdt_trajs[x,]$lines[[1]]$labels, |
| 239 | 2x |
textposition = label.offset[x], |
| 240 | 2x |
hoverinfo = hoverinfo, |
| 241 | 2x |
showlegend = TRUE, |
| 242 | 2x |
line = list ( |
| 243 | 2x |
color = traj_color |
| 244 |
), |
|
| 245 | 2x |
marker = list ( |
| 246 | 2x |
symbol = shape, |
| 247 | 2x |
color = traj_color |
| 248 |
), |
|
| 249 | 2x |
textfont = list ( |
| 250 | 2x |
family = label.font.family, |
| 251 | 2x |
size = label.font.size, |
| 252 | 2x |
color = label.font.color |
| 253 |
), |
|
| 254 | 2x |
visible = ifelse(default.hidden, "legendonly", TRUE) |
| 255 |
); |
|
| 256 |
} |
|
| 257 |
} |
|
| 258 | ||
| 259 | 2x |
enaplot$plotted$trajectories[[ |
| 260 | 2x |
length(enaplot$plotted$trajectories) + 1 |
| 261 | 2x |
]] <- dfdt_trajs |
| 262 | ||
| 263 | 2x |
return(enaplot); |
| 264 |
} |
| 1 |
#' Compute Between-Group Scatter Matrix |
|
| 2 |
#' |
|
| 3 |
#' This function calculates the between-group scatter matrix (\code{SB}) for a given numeric matrix and grouping variable.
|
|
| 4 |
#' |
|
| 5 |
#' @param A A numeric matrix of dimensions \code{m x n}, where rows represent observations and columns represent features.
|
|
| 6 |
#' @param g A grouping variable of length \code{m}, either a factor or a character vector, indicating group membership for each observation.
|
|
| 7 |
#' |
|
| 8 |
#' @return A numeric matrix representing the between-group scatter matrix (\code{SB}).
|
|
| 9 |
#' |
|
| 10 |
#' @details |
|
| 11 |
#' The function computes the total mean of the matrix \code{A} and the mean for each group defined by \code{g}.
|
|
| 12 |
#' It then calculates the between-group scatter matrix by summing the outer product of the mean differences, weighted by the group sizes. |
|
| 13 |
#' |
|
| 14 |
#' @examples |
|
| 15 |
#' # Example usage: |
|
| 16 |
#' A <- matrix(rnorm(20), nrow = 5, ncol = 4) |
|
| 17 |
#' g <- factor(c("A", "B", "A", "B", "A"))
|
|
| 18 |
#' SB <- rENA:::compute_SB(A, g) |
|
| 19 |
compute_SB <- function(A, g) {
|
|
| 20 | ! |
if (!is.matrix(A)) stop("A must be a numeric matrix.")
|
| 21 | ! |
if (length(g) != nrow(A)) stop("g must have the same length as number of rows in A.")
|
| 22 | ||
| 23 | 2x |
g <- as.factor(g); |
| 24 | 2x |
groups <- levels(g); |
| 25 | 2x |
n_features <- ncol(A); |
| 26 | 2x |
m <- nrow(A); |
| 27 | ||
| 28 |
# Total mean |
|
| 29 | 2x |
mu_total <- colMeans(A); |
| 30 | ||
| 31 |
# Initialize matrices |
|
| 32 | 2x |
SB <- matrix(0, n_features, n_features); |
| 33 | ||
| 34 | 2x |
for (grp in groups) {
|
| 35 | 4x |
idx <- which(g == grp); |
| 36 | 4x |
A_grp <- A[idx, , drop = FALSE]; |
| 37 | 4x |
n_g <- nrow(A_grp); |
| 38 | 4x |
mu_g <- colMeans(A_grp); |
| 39 | ||
| 40 |
# Between-group component |
|
| 41 | 4x |
mean_diff <- matrix(mu_g - mu_total, ncol = 1); |
| 42 | 4x |
SB <- SB + n_g * (mean_diff %*% t(mean_diff)); |
| 43 |
} |
|
| 44 | ||
| 45 | 2x |
return(SB); |
| 46 |
} |
|
| 47 | ||
| 48 | ||
| 49 |
#' Generalized Means Rotation (GMR) with optional subsetting and interaction control |
|
| 50 |
#' |
|
| 51 |
#' Computes a rotation (direction) `r` representing the contribution of the |
|
| 52 |
#' first column of `X` to the multivariate ENA matrix `V`. Supports optional |
|
| 53 |
#' subsetting by `groups`, optional inclusion of interaction terms when |
|
| 54 |
#' computing adjusted contributions. |
|
| 55 |
#' |
|
| 56 |
#' @param V Numeric ENA matrix (units x connections) ready for rotation. |
|
| 57 |
#' @param X Data frame or matrix of predictors; the first column is the target. |
|
| 58 |
#' @param groups Optional vector specifying target groups to subset. If `NULL` |
|
| 59 |
#' (default), all rows are used. |
|
| 60 |
#' @param alpha Elastic-net mixing parameter forwarded to `get_x1_main_effect` |
|
| 61 |
#' (default `1` - Lasso). |
|
| 62 |
#' @param lambda Lambda selection for `cv.glmnet` forwarded to |
|
| 63 |
#' `get_x1_main_effect` (default `"lambda.min"`). |
|
| 64 |
#' @param interactions Logical; if `TRUE` (default) interactions are included when computing the adjusted contribution. |
|
| 65 |
#' @param verbose Logical; if `TRUE` (default) the function emits messages about |
|
| 66 |
#' fails or successes. |
|
| 67 |
#' |
|
| 68 |
#' @return A numeric vector `r` (length = ncol(V)) giving the normalized |
|
| 69 |
#' rotation direction. Attributes attached: |
|
| 70 |
#' \describe{
|
|
| 71 |
#' \item{`target`}{The full-length target vector (un-subsetted).}
|
|
| 72 |
#' \item{`Vx1`}{The unadjusted fitted values (`lm(V ~ target)`) embedded in
|
|
| 73 |
#' a full-length matrix (rows outside subset filled with zeros).} |
|
| 74 |
#' } |
|
| 75 |
#' If no valid direction can be found (including SVD failure), returns `NULL` |
|
| 76 |
#' and issues a warning. |
|
| 77 |
#' |
|
| 78 |
#' @examples |
|
| 79 |
#' \dontrun{
|
|
| 80 |
#' set.seed(1) |
|
| 81 |
#' V <- matrix(rnorm(200), nrow = 40) |
|
| 82 |
#' X <- data.frame(group = rep(letters[1:4], each = 10), |
|
| 83 |
#' x2 = rnorm(40), x3 = rnorm(40)) |
|
| 84 |
#' r_all <- gmr2(V, X) |
|
| 85 |
#' r_subset <- gmr2(V, X, groups = c("a", "b"), interactions = TRUE)
|
|
| 86 |
#' } |
|
| 87 |
#' |
|
| 88 |
#' @seealso [get_x1_main_effect()] |
|
| 89 |
#' @importFrom stats lm model.matrix |
|
| 90 |
#' @importFrom glmnet cv.glmnet |
|
| 91 |
#' @export |
|
| 92 | ||
| 93 |
gmr <- function(V, X, groups = NULL, alpha = 1, lambda = "lambda.min", |
|
| 94 |
interactions = TRUE, verbose = TRUE) {
|
|
| 95 |
# prepare a function for almost zero check |
|
| 96 | 2x |
is_zero <- function(x, tol = 1e-12) all(abs(x) < tol) |
| 97 |
# get full target variable, namely, the first variable in X |
|
| 98 | 2x |
target_full <- X[[1]] |
| 99 | ! |
if (is.list(target_full)) target_full <- unlist(target_full, recursive = FALSE) |
| 100 | 2x |
target_full <- as.vector(target_full) |
| 101 | ||
| 102 |
# --- Fail if target is constant --- |
|
| 103 | 2x |
unique_targets <- unique(target_full) |
| 104 | 2x |
if (length(unique_targets) == 1) {
|
| 105 | ! |
warning("Target variable is constant; returning NULL.")
|
| 106 | ! |
return(NULL) |
| 107 |
} |
|
| 108 | ||
| 109 |
# --- Subset by groups if selected groups are provided --- |
|
| 110 | 2x |
if (!is.null(groups)) {
|
| 111 | ! |
valid_groups <- intersect(groups, unique(target_full)) |
| 112 | ! |
if (length(valid_groups) > 1) {
|
| 113 | ! |
subset_idx <- which(target_full %in% valid_groups) |
| 114 | ! |
V_sub <- V[subset_idx, , drop = FALSE] |
| 115 | ! |
X_sub <- X[subset_idx, , drop = FALSE] |
| 116 | ! |
target_sub <- target_full[subset_idx] |
| 117 |
} else {
|
|
| 118 | ! |
warning("Less than 2 valid groups selected; returning NULL.")
|
| 119 | ! |
return(NULL) |
| 120 |
} |
|
| 121 |
} else { # use full data if no groups are selected
|
|
| 122 | 2x |
V_sub <- V |
| 123 | 2x |
X_sub <- X |
| 124 | 2x |
target_sub <- target_full |
| 125 | 2x |
subset_idx <- NULL |
| 126 |
} |
|
| 127 | ||
| 128 |
# --- Base regression model --- |
|
| 129 | 2x |
model <- lm(V_sub ~ target_sub) |
| 130 | 2x |
Vx1_sub <- model$fitted.values |
| 131 | ||
| 132 |
# --- Compute contributions via Lasso (if covariates exist) --- |
|
| 133 | 2x |
Vx_sub <- NULL |
| 134 | 2x |
if (ncol(X_sub) == 1) { # no corariates, use base model
|
| 135 | ! |
Vx_sub <- Vx1_sub |
| 136 |
} else { # covariates exist, use Lasso model
|
|
| 137 | 2x |
Vx_sub <- get_x1_main_effect(V_sub, X_sub, alpha = alpha, |
| 138 | 2x |
lambda = lambda, include_interactions = interactions) |
| 139 |
} |
|
| 140 | 2x |
if (is_zero(Vx_sub)) {
|
| 141 | ! |
warning("Regression resulted in zeor contribution; returning NULL.")
|
| 142 | ! |
return(NULL) |
| 143 |
} |
|
| 144 |
# --- Compute rotation direction r --- |
|
| 145 | 2x |
r <- NULL |
| 146 | 2x |
if (is.numeric(target_sub)) {
|
| 147 | 1x |
if (verbose) message("Computing direction for numeric target...")
|
| 148 | 1x |
model = model <- lm(Vx_sub ~ target_sub) |
| 149 | 1x |
beta <- model$coefficients[2,] |
| 150 | 1x |
if (is_zero(beta)) {
|
| 151 | ! |
warning("Numerical target with zero beta; returning NULL.")
|
| 152 | ! |
return(NULL) |
| 153 |
} else {
|
|
| 154 | 1x |
r <- beta |
| 155 |
} |
|
| 156 |
} else {
|
|
| 157 | 1x |
if (verbose) message("Computing direction for categorical target...")
|
| 158 | 1x |
sb <- compute_SB(Vx_sub, target_sub) |
| 159 | 1x |
r <- tryCatch(svd(sb)$v[, 1], error = function(e) NULL) |
| 160 |
} |
|
| 161 | ||
| 162 |
# --- Final SVD fallback if r is NULL or zero --- |
|
| 163 | 2x |
if (is.null(r) || all(r == 0)) {
|
| 164 | ! |
warning("Uable to compute any valid direction; returning NULL.")
|
| 165 | ! |
return(NULL) |
| 166 |
} |
|
| 167 | ||
| 168 |
# --- Normalize --- |
|
| 169 | 2x |
r <- r / sqrt(sum(r^2)) |
| 170 | ||
| 171 |
# --- Build full-length Vx1 --- |
|
| 172 | 2x |
Vx1_full <- matrix(0, nrow = nrow(V), ncol = ncol(V)) |
| 173 | 2x |
Vx1_full[subset_idx %||% seq_len(nrow(V)), ] <- Vx_sub |
| 174 | 2x |
colnames(Vx1_full) <- colnames(V) |
| 175 | ||
| 176 |
# --- Attach metadata --- |
|
| 177 | 2x |
attr(r, "target") <- target_full |
| 178 | 2x |
attr(r, "Vx1") <- Vx1_full |
| 179 |
#attr(r, "fallback_stage") <- fallback_stage |
|
| 180 | ||
| 181 | 2x |
if (verbose) message(" gmr completed successfully ")
|
| 182 | 2x |
return(r) |
| 183 |
} |
|
| 184 | ||
| 185 |
#' Extract the Main Effect of X on V with Optional Interactions |
|
| 186 |
#' |
|
| 187 |
#' Computes the main-effect contribution of the first column of `X` (the |
|
| 188 |
#' "target") to the multivariate ENA matrix `V`. The function fits penalized |
|
| 189 |
#' regression models (via glmnet) and can optionally include interactions |
|
| 190 |
#' between the target and other covariates. It returns the fitted contribution |
|
| 191 |
#' matrix (units x connections). |
|
| 192 |
#' |
|
| 193 |
#' The function can compute contributions using either only main-effect columns |
|
| 194 |
#' (no interactions) or main-effect plus all interaction columns that start |
|
| 195 |
#' with the target name. If no matching columns are found or all fitted |
|
| 196 |
#' coefficients are zero, the function returns a zero matrix and emits a |
|
| 197 |
#' warning. |
|
| 198 |
#' |
|
| 199 |
#' @param V A numeric matrix (units x connections) of dependent variables. |
|
| 200 |
#' @param X A data frame or matrix of predictors / covariates. The **first** |
|
| 201 |
#' column is treated as the target variable whose contribution will be extracted. |
|
| 202 |
#' @param alpha Elastic-net mixing parameter passed to `cv.glmnet`. `alpha = 1` |
|
| 203 |
#' (default) is Lasso; `alpha = 0` is ridge. |
|
| 204 |
#' @param lambda Character or numeric. Which lambda from the `cv.glmnet` fit to |
|
| 205 |
#' use; e.g. `"lambda.min"` (default) or `"lambda.1se"`, or a numeric value. |
|
| 206 |
#' @param include_interactions Logical; if `TRUE`, include main-effect columns |
|
| 207 |
#' **and** all interaction columns that begin with the target name (default: |
|
| 208 |
#' `FALSE`, only main-effect columns). |
|
| 209 |
#' |
|
| 210 |
#' @return A numeric matrix with the same dimensions as `V` containing the |
|
| 211 |
#' estimated contribution of `X[,1]` to each response. If no columns are |
|
| 212 |
#' matched or all coefficients are zero, a zero matrix is returned and a |
|
| 213 |
#' warning is issued. |
|
| 214 |
#' |
|
| 215 |
#' @details |
|
| 216 |
#' Internally this function builds `model.matrix(~ .^2, data = X)` to obtain |
|
| 217 |
#' main effects and pairwise interactions. It sets a `penalty.factor` that |
|
| 218 |
#' leaves the target-related columns unpenalized (0) and fits a multivariate |
|
| 219 |
#' `glmnet` (`family = "mgaussian"`). The returned matrix is dense (numeric). |
|
| 220 |
#' |
|
| 221 |
#' @examples |
|
| 222 |
#' \dontrun{
|
|
| 223 |
#' set.seed(1) |
|
| 224 |
#' V <- matrix(rnorm(50), ncol = 5) |
|
| 225 |
#' X <- data.frame(CONFIDENCE = rnorm(10), Condition = factor(rep(1:2, 5))) |
|
| 226 |
#' # main effects only |
|
| 227 |
#' Vx_main <- get_x1_main_effect(V, X, include_interactions = FALSE) |
|
| 228 |
#' # include interactions |
|
| 229 |
#' Vx_full <- get_x1_main_effect(V, X, include_interactions = TRUE, alpha = 0) # ridge |
|
| 230 |
#' } |
|
| 231 |
#' |
|
| 232 |
#' @seealso [gmr2()] for the rotation routine that uses this function. |
|
| 233 |
#' @importFrom stats lm model.matrix |
|
| 234 |
#' @importFrom glmnet cv.glmnet |
|
| 235 |
#' @export |
|
| 236 | ||
| 237 |
get_x1_main_effect <- function(V, X, alpha = 1, lambda = "lambda.min", include_interactions = FALSE) {
|
|
| 238 | 3x |
x1_name <- colnames(X)[1] |
| 239 | ||
| 240 |
# 1. Formula & Model Matrix |
|
| 241 | 3x |
formula_str <- if (include_interactions) "~ .^2" else "~ ." |
| 242 | 3x |
mm <- model.matrix(as.formula(formula_str), data = X)[, -1, drop = FALSE] |
| 243 | ||
| 244 |
# 2. Identify Main Effect Columns for x1 |
|
| 245 | 3x |
safe_x1 <- gsub("([.|()\\^{}+$*?]|\\[|\\])", "\\\\\\1", x1_name)
|
| 246 | 3x |
x1_main_regex <- paste0("^", safe_x1, "[^:]*$")
|
| 247 | 3x |
x1_cols <- grep(x1_main_regex, colnames(mm)) |
| 248 | ||
| 249 | 3x |
if (length(x1_cols) == 0) {
|
| 250 | ! |
warning("No main effect columns found for X[,1]; returning zeros.")
|
| 251 | ! |
return(matrix(0, nrow = nrow(V), ncol = ncol(V), dimnames = list(NULL, colnames(V)))) |
| 252 |
} |
|
| 253 | ||
| 254 |
# 3. Penalty Factors |
|
| 255 | 3x |
p <- ncol(mm) |
| 256 | 3x |
penalty_factors <- rep(1, p) |
| 257 | 3x |
penalty_factors[x1_cols] <- 0 |
| 258 | ||
| 259 |
# 4. Fitting Logic |
|
| 260 | 3x |
x1_contribution <- matrix(0, nrow = nrow(V), ncol = ncol(V), dimnames = list(NULL, colnames(V))) |
| 261 | 3x |
use_ols <- (p <= (nrow(X) - 10)) # Heuristic: Use OLS only if we have enough degrees of freedom |
| 262 | ||
| 263 | 3x |
if (!use_ols) {
|
| 264 | ! |
fit <- tryCatch( |
| 265 |
# We add lower.limits/upper.limits or tiny penalty to ensure x1 is NEVER zero if it has signal |
|
| 266 | ! |
glmnet::cv.glmnet(x = mm, y = V, family = "mgaussian", |
| 267 | ! |
alpha = alpha, penalty.factor = penalty_factors), |
| 268 | ! |
error = function(e) NULL |
| 269 |
) |
|
| 270 | ||
| 271 | ! |
if (!is.null(fit)) {
|
| 272 | ! |
coefs_list <- coef(fit, s = lambda) |
| 273 |
# coefs_list is a list of sparse matrices (one per response) |
|
| 274 | ! |
for (i in seq_along(coefs_list)) {
|
| 275 |
# Extract coefs, skipping intercept ([1,]) |
|
| 276 |
# Force to numeric to avoid sparse matrix indexing issues |
|
| 277 | ! |
beta_all <- as.matrix(coefs_list[[i]])[-1, , drop = FALSE] |
| 278 | ! |
beta_x1 <- beta_all[x1_cols, , drop = FALSE] |
| 279 | ! |
x1_contribution[, i] <- mm[, x1_cols, drop = FALSE] %*% beta_x1 |
| 280 |
} |
|
| 281 | ! |
return(x1_contribution) |
| 282 |
} |
|
| 283 | ! |
use_ols <- TRUE |
| 284 |
} |
|
| 285 | ||
| 286 | 3x |
if (use_ols) {
|
| 287 | 3x |
fit_ols <- lm(V ~ mm) |
| 288 |
# as.matrix handles the 'incorrect number of dimensions' for single response |
|
| 289 | 3x |
beta_ols <- as.matrix(coef(fit_ols))[-1, , drop = FALSE] |
| 290 | 3x |
beta_x1_ols <- beta_ols[x1_cols, , drop = FALSE] |
| 291 | ||
| 292 |
# Handle NAs that OLS produces for rank-deficient matrices |
|
| 293 | 3x |
beta_x1_ols[is.na(beta_x1_ols)] <- 0 |
| 294 | 3x |
x1_contribution <- mm[, x1_cols, drop = FALSE] %*% beta_x1_ols |
| 295 |
} |
|
| 296 | ||
| 297 | 3x |
return(x1_contribution) |
| 298 |
} |
|
| 299 |
| 1 |
## ── qeviz interactive plot integration ─────────────────────────────────────── |
|
| 2 |
## |
|
| 3 |
## Public API (thin wrappers over the qeviz package) |
|
| 4 |
## ena.plot.interactive() — create an interactive qeviz htmlwidget |
|
| 5 |
## ena.export.html() — write a self-contained HTML file |
|
| 6 |
## enaInteractiveOutput() — Shiny output binding |
|
| 7 |
## renderEnaInteractive() — Shiny render function |
|
| 8 |
## |
|
| 9 |
## The widget, its JavaScript bundle and the ena.set -> ModelData conversion |
|
| 10 |
## live in qeviz (qe_plot(), qe_widget(), qeOutput(), renderQe()). |
|
| 11 |
## ───────────────────────────────────────────────────────────────────────────── |
|
| 12 | ||
| 13 | ||
| 14 |
.require_qeviz <- function() {
|
|
| 15 | 45x |
if (!requireNamespace("qeviz", quietly = TRUE) || utils::packageVersion("qeviz") < "0.5.0") {
|
| 16 | ! |
stop("The 'qeviz' package (>= 0.5.0) is required for interactive plots. Install with:\n",
|
| 17 | ! |
" install.packages(\"qeviz\", repos = c(\"https://cran.qe-libs.org\", getOption(\"repos\")))", |
| 18 | ! |
call. = FALSE) |
| 19 |
} |
|
| 20 |
} |
|
| 21 | ||
| 22 |
#' Interactive ENA plot using qeviz |
|
| 23 |
#' |
|
| 24 |
#' Renders an interactive ENA plot inside RStudio, R Markdown / Quarto, and |
|
| 25 |
#' Shiny using the qeviz visualization library. This is a shortcut for a |
|
| 26 |
#' \code{qeviz::qe_plot()} chain; use qeviz directly for full control.
|
|
| 27 |
#' |
|
| 28 |
#' @param set An \code{\link{ena.make.set}} result.
|
|
| 29 |
#' @param group_col Character. Name of the grouping column in \code{set$points}
|
|
| 30 |
#' (e.g. \code{"Condition"}). Controls point colours and group
|
|
| 31 |
#' mean networks. |
|
| 32 |
#' @param group Character. Which group's mean network to display. Defaults |
|
| 33 |
#' to the first group. |
|
| 34 |
#' @param unit Character. A specific unit ID to display its individual |
|
| 35 |
#' network instead of a group mean. |
|
| 36 |
#' @param compare Character. Second group or unit for a subtraction view |
|
| 37 |
#' (\code{group} minus \code{compare}).
|
|
| 38 |
#' @param also Character. Second group for an overlay view (both networks |
|
| 39 |
#' drawn simultaneously). |
|
| 40 |
#' @param dim_cols Character vector of two dimension names to plot. |
|
| 41 |
#' Default \code{c("SVD1", "SVD2")}.
|
|
| 42 |
#' @param label_nodes \code{"on"} | \code{"off"} | \code{"auto"} | \code{"click"}.
|
|
| 43 |
#' Visibility of code-node labels. Default \code{"on"}.
|
|
| 44 |
#' @param label_means Visibility of group-mean labels. Default \code{"on"}.
|
|
| 45 |
#' @param label_points Visibility of unit-point labels. Default \code{"off"}.
|
|
| 46 |
#' @param confidence Logical. Draw the confidence interval box around each |
|
| 47 |
#' shown group mean. Default \code{TRUE}.
|
|
| 48 |
#' @param outlier Logical. Also draw the outlier interval (Q1 - 1.5 IQR to |
|
| 49 |
#' Q3 + 1.5 IQR). Default \code{FALSE}.
|
|
| 50 |
#' @param scale_points Logical. Rescale unit points to match the node coordinate |
|
| 51 |
#' space. Default \code{TRUE}.
|
|
| 52 |
#' @param conf_level Numeric. Confidence level for CI boxes. Default \code{0.95}.
|
|
| 53 |
#' @param iqr_factor Deprecated; the outlier interval always uses 1.5 IQR. |
|
| 54 |
#' @param width,height Widget size in pixels or CSS units. \code{NULL} uses
|
|
| 55 |
#' the qeviz defaults (100\% x 440). |
|
| 56 |
#' |
|
| 57 |
#' @return An \code{htmlwidget} object that renders in RStudio Viewer, R Markdown,
|
|
| 58 |
#' Quarto, and Shiny. |
|
| 59 |
#' |
|
| 60 |
#' @examples |
|
| 61 |
#' \dontrun{
|
|
| 62 |
#' data(RS.data) |
|
| 63 |
#' codeNames <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 64 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", "Collaboration") |
|
| 65 |
#' accum <- ena.accumulate.data( |
|
| 66 |
#' units = RS.data[, c("UserName", "Condition")],
|
|
| 67 |
#' conversation = RS.data[, c("Condition", "GroupName")],
|
|
| 68 |
#' codes = RS.data[, codeNames], |
|
| 69 |
#' window.size.back = 4 |
|
| 70 |
#' ) |
|
| 71 |
#' set <- ena.make.set(enadata = accum) |
|
| 72 |
#' |
|
| 73 |
#' # Basic plot coloured by Condition |
|
| 74 |
#' ena.plot.interactive(set, group_col = "Condition") |
|
| 75 |
#' |
|
| 76 |
#' # Show only FirstGame mean network |
|
| 77 |
#' ena.plot.interactive(set, group_col = "Condition", group = "FirstGame") |
|
| 78 |
#' |
|
| 79 |
#' # Subtraction: FirstGame minus SecondGame |
|
| 80 |
#' ena.plot.interactive(set, group_col = "Condition", |
|
| 81 |
#' group = "FirstGame", compare = "SecondGame") |
|
| 82 |
#' } |
|
| 83 |
#' |
|
| 84 |
#' @export |
|
| 85 |
ena.plot.interactive <- function( |
|
| 86 |
set, |
|
| 87 |
group_col = NULL, |
|
| 88 |
group = NULL, |
|
| 89 |
unit = NULL, |
|
| 90 |
compare = NULL, |
|
| 91 |
also = NULL, |
|
| 92 |
dim_cols = c("SVD1", "SVD2"),
|
|
| 93 |
label_nodes = "on", |
|
| 94 |
label_means = "on", |
|
| 95 |
label_points = "off", |
|
| 96 |
confidence = TRUE, |
|
| 97 |
outlier = FALSE, |
|
| 98 |
scale_points = TRUE, |
|
| 99 |
conf_level = 0.95, |
|
| 100 |
iqr_factor = 1.5, |
|
| 101 |
width = NULL, |
|
| 102 |
height = NULL |
|
| 103 |
) {
|
|
| 104 | 7x |
.require_qeviz() |
| 105 | 7x |
if (!missing(iqr_factor) && !identical(iqr_factor, 1.5)) |
| 106 | 1x |
warning("ena.plot.interactive(): iqr_factor is deprecated and ignored; ",
|
| 107 | 1x |
"the outlier interval uses 1.5 IQR.", call. = FALSE) |
| 108 | ||
| 109 | 7x |
p <- qeviz::qe_plot(set, dims = dim_cols, scale_points = scale_points, |
| 110 | 7x |
group_col = group_col, |
| 111 | 7x |
width = if (is.null(width)) "100%" else width, |
| 112 | 7x |
height = if (is.null(height)) 440 else height) |
| 113 | ||
| 114 |
# A group mean network: the requested group, else the first group (as |
|
| 115 |
# before). A unit network is drawn instead when `unit` is given. |
|
| 116 | 7x |
groups <- if (is.null(group_col)) character(0) |
| 117 | 7x |
else unique(as.character(as.data.frame(set$points)[[group_col]])) |
| 118 | 4x |
if (is.null(group) && is.null(unit) && length(groups)) group <- groups[1L] |
| 119 | 7x |
if (!is.null(group) || !is.null(unit)) |
| 120 | 6x |
p <- qeviz::qe_edges(p, group = group, unit = unit, compare = compare, also = also) |
| 121 | ||
| 122 |
# Means (with CI) for the groups whose networks are shown. |
|
| 123 |
# Means (with CI) for the groups whose networks are shown; none otherwise. |
|
| 124 | 7x |
shown <- intersect(c(group, compare, also), groups) |
| 125 | 7x |
p <- if (length(shown)) |
| 126 | 7x |
do.call(qeviz::qe_group, c(list(p), as.list(shown), list( |
| 127 | 7x |
intervals = if (isTRUE(confidence)) "box" else "none", |
| 128 | 7x |
conf_level = conf_level, |
| 129 | 7x |
outlier = isTRUE(outlier)))) |
| 130 | 7x |
else qeviz::qe_group(p, show = FALSE) |
| 131 | ||
| 132 | 7x |
p <- qeviz::qe_axes(p) |
| 133 | 7x |
p <- qeviz::qe_labels(p, nodes = label_nodes, means = label_means, points = label_points) |
| 134 | 7x |
qeviz::qe_widget(p) |
| 135 |
} |
|
| 136 | ||
| 137 |
#' Export a self-contained interactive ENA plot as HTML |
|
| 138 |
#' |
|
| 139 |
#' Writes a single \code{.html} file containing the qeviz bundle and embedded
|
|
| 140 |
#' model data. No R, no Python, and no server are required to open the file — |
|
| 141 |
#' share it with collaborators, attach it to a paper submission, or archive it |
|
| 142 |
#' as supplementary material. |
|
| 143 |
#' |
|
| 144 |
#' @param set An \code{\link{ena.make.set}} result.
|
|
| 145 |
#' @param file Output file path, e.g. \code{"ena_plot.html"}.
|
|
| 146 |
#' @param group_col Character. Grouping column in \code{set$points}.
|
|
| 147 |
#' @param ... Additional arguments passed to \code{\link{ena.plot.interactive}}
|
|
| 148 |
#' (e.g. \code{group}, \code{compare}, \code{label_nodes}).
|
|
| 149 |
#' @param width,height Plot dimensions in pixels. Default 700 × 600. |
|
| 150 |
#' @param selfcontained Logical. Inline the qeviz bundle in the HTML file. |
|
| 151 |
#' Default \code{TRUE}. Set to \code{FALSE} to reference the
|
|
| 152 |
#' bundle via a relative path (smaller file, not portable). |
|
| 153 |
#' |
|
| 154 |
#' @return The resolved absolute path of the written file (invisibly). |
|
| 155 |
#' |
|
| 156 |
#' @examples |
|
| 157 |
#' \dontrun{
|
|
| 158 |
#' set <- ena.make.set(enadata = accum) |
|
| 159 |
#' ena.export.html(set, "model.html", group_col = "Condition") |
|
| 160 |
#' } |
|
| 161 |
#' |
|
| 162 |
#' @export |
|
| 163 |
ena.export.html <- function( |
|
| 164 |
set, |
|
| 165 |
file, |
|
| 166 |
group_col = NULL, |
|
| 167 |
..., |
|
| 168 |
width = 700L, |
|
| 169 |
height = 600L, |
|
| 170 |
selfcontained = TRUE |
|
| 171 |
) {
|
|
| 172 | 1x |
widget <- ena.plot.interactive( |
| 173 | 1x |
set, |
| 174 | 1x |
group_col = group_col, |
| 175 | 1x |
width = width, |
| 176 | 1x |
height = height, |
| 177 |
... |
|
| 178 |
) |
|
| 179 | 1x |
abs_file <- normalizePath(file, mustWork = FALSE) |
| 180 | 1x |
htmlwidgets::saveWidget(widget, abs_file, selfcontained = selfcontained) |
| 181 | 1x |
message("Written: ", abs_file)
|
| 182 | 1x |
invisible(abs_file) |
| 183 |
} |
|
| 184 | ||
| 185 |
#' Shiny output binding for interactive ENA plots |
|
| 186 |
#' |
|
| 187 |
#' Equivalent to \code{qeviz::qeOutput()}.
|
|
| 188 |
#' |
|
| 189 |
#' @param outputId Shiny output ID. |
|
| 190 |
#' @param width,height CSS dimensions. Defaults: \code{"100\%"}, \code{"600px"}.
|
|
| 191 |
#' @export |
|
| 192 |
enaInteractiveOutput <- function(outputId, width = "100%", height = "600px") {
|
|
| 193 | ! |
.require_qeviz() |
| 194 | ! |
qeviz::qeOutput(outputId, width = width, height = height) |
| 195 |
} |
|
| 196 | ||
| 197 |
#' Shiny render function for interactive ENA plots |
|
| 198 |
#' |
|
| 199 |
#' Equivalent to \code{qeviz::renderQe()}; \code{expr} may return an
|
|
| 200 |
#' \code{\link{ena.plot.interactive}} widget or a \code{qeviz::qe_plot()}.
|
|
| 201 |
#' |
|
| 202 |
#' @param expr Expression that returns an \code{\link{ena.plot.interactive}} widget.
|
|
| 203 |
#' @param env Environment for \code{expr}. Default: \code{parent.frame()}.
|
|
| 204 |
#' @param quoted Logical. Is \code{expr} already quoted? Default \code{FALSE}.
|
|
| 205 |
#' @export |
|
| 206 |
renderEnaInteractive <- function(expr, env = parent.frame(), quoted = FALSE) {
|
|
| 207 | ! |
.require_qeviz() |
| 208 | ! |
if (!quoted) expr <- substitute(expr) |
| 209 | ! |
qeviz::renderQe(expr, env = env, quoted = TRUE) |
| 210 |
} |
| 1 |
#' @title ENA SVD |
|
| 2 |
#' @description Computes a dimensional reduction of points in an ENA set using Singular Value Decomposition (SVD). |
|
| 3 |
#' @param enaset An \code{ENAset} object containing the points to be reduced.
|
|
| 4 |
#' @param params A list of parameters. Use \code{params$as_object = TRUE} to return an ENARotationSet object, or \code{FALSE} (default) to return a list.
|
|
| 5 |
#' @details This function computes the SVD of the points in the ENA set and returns either an ENARotationSet object or a list with the rotation matrix, codes, node positions, and eigenvalues, depending on \code{params$as_object}.
|
|
| 6 |
#' @return An ENARotationSet object or a list containing: |
|
| 7 |
#' \item{rotation}{The rotation matrix from SVD}
|
|
| 8 |
#' \item{codes}{The code names used for the matrix}
|
|
| 9 |
#' \item{node.positions}{(Currently NULL) Node positions}
|
|
| 10 |
#' \item{eigenvalues}{The eigenvalues (squared singular values) from SVD}
|
|
| 11 |
#' @examples |
|
| 12 |
#' data(RS.data) |
|
| 13 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 14 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 15 |
#' "Collaboration") |
|
| 16 |
#' units <- c("Condition", "UserName")
|
|
| 17 |
#' horizon <- c("Condition", "GroupName")
|
|
| 18 |
#' enaset <- RS.data |> |
|
| 19 |
#' accumulate(units, codes, horizon) |> |
|
| 20 |
#' model() |
|
| 21 |
#' # SVD as list: |
|
| 22 |
#' svd_result <- ena.svd(enaset, list(as_object = FALSE)) |
|
| 23 |
#' # SVD as ENARotationSet object: |
|
| 24 |
#' svd_obj <- ena.svd(enaset, list(as_object = TRUE)) |
|
| 25 |
#' @export |
|
| 26 |
ena.svd <- function(enaset, params) {
|
|
| 27 |
# to.norm = data.table::data.table( |
|
| 28 |
# enaset$points.normed.centered, |
|
| 29 |
# enaset$enadata$unit.names |
|
| 30 |
# ) |
|
| 31 |
# to.norm = as.matrix(to.norm[,tail(.SD,n=1),.SDcols=colnames(to.norm)[which(colnames(to.norm) != "V2")],by=c("V2")][,2:ncol(to.norm)]);
|
|
| 32 |
# pcaResults = pca_c(to.norm, dims = enaset$get("dimensions"));
|
|
| 33 |
# pcaResults = pca_c(enaset$points.normed.centered, dims = enaset$get("dimensions"));
|
|
| 34 | 72x |
as_object = FALSE; |
| 35 | 72x |
if(!is.null(params$as_object)) {
|
| 36 | ! |
as_object = params$as_object |
| 37 |
} |
|
| 38 | ||
| 39 |
# pts = enaset$model$points.for.projection[,!colnames(enaset$model$points.for.projection) %in% colnames(enaset$meta.data), with=F] |
|
| 40 | 72x |
pts = as.matrix(enaset$model$points.for.projection) |
| 41 | 72x |
pcaResults = prcomp(pts, retx=FALSE, scale=FALSE, center=FALSE, tol=0) |
| 42 | ||
| 43 |
### used to be enaset$data$centered$pca |
|
| 44 |
#enaset$rotation.set = pcaResults$pca; |
|
| 45 | ||
| 46 | ||
| 47 | 72x |
colnames(pcaResults$rotation) = c( |
| 48 | 72x |
paste('SVD',as.character(1:ncol(pcaResults$rotation)), sep='')
|
| 49 |
); |
|
| 50 | ||
| 51 |
# rotationSet = ENARotationSet$new(rotation = pcaResults$pca, codes = enaset$codes, node.positions = NULL, eigenvalues = pcaResults$latent) |
|
| 52 | 72x |
if(isTRUE(as_object)) {
|
| 53 | ! |
rotationSet = ENARotationSet$new( |
| 54 | ! |
rotation = pcaResults$rotation, |
| 55 | ! |
codes = enaset$rotation$codes, |
| 56 | ! |
node.positions = NULL, |
| 57 | ! |
eigenvalues = pcaResults$sdev^2 |
| 58 |
) |
|
| 59 |
} |
|
| 60 |
else {
|
|
| 61 | 72x |
rotationSet <- list( |
| 62 | 72x |
rotation = pcaResults$rotation, |
| 63 | 72x |
codes = enaset$rotation$codes, |
| 64 | 72x |
node.positions = NULL, |
| 65 | 72x |
eigenvalues = pcaResults$sdev^2 |
| 66 |
) |
|
| 67 |
} |
|
| 68 | 72x |
return(rotationSet) |
| 69 |
} |
|
| 70 | ||
| 71 |
ena.svd.R6 <- function(enaset, ...) {
|
|
| 72 | 7x |
pcaResults = prcomp(enaset$points.normed.centered, retx=FALSE,scale=FALSE,center=FALSE, tol=0) |
| 73 | ||
| 74 | 7x |
colnames(pcaResults$rotation) = c( |
| 75 | 7x |
paste('SVD',as.character(1:ncol(pcaResults$rotation)), sep='')
|
| 76 |
); |
|
| 77 | ||
| 78 | 7x |
rotationSet = ENARotationSet$new( |
| 79 | 7x |
rotation = pcaResults$rotation, codes = enaset$codes, |
| 80 | 7x |
node.positions = NULL, eigenvalues = pcaResults$sdev^2 |
| 81 |
) |
|
| 82 | 7x |
return(rotationSet) |
| 83 |
} |
| 1 |
## |
|
| 2 |
#' @title Generate ENA Set |
|
| 3 |
#' |
|
| 4 |
#' @description Generates an ENA model by constructing a dimensional reduction of adjacency (co-occurrence) vectors in an ENA data object |
|
| 5 |
#' |
|
| 6 |
#' @details This function generates an ENAset object from an ENAdata object. Takes |
|
| 7 |
#' the adjacency (co-occurrence) vectors from enadata, computes a dimensional |
|
| 8 |
#' reduction (projection), and calculates node positions in the projected ENA |
|
| 9 |
#' space. Returns location of the units in the projected space, as well as |
|
| 10 |
#' locations for node positions, and normalized adjacency (co-occurrence) vectors |
|
| 11 |
#' to construct network graphs |
|
| 12 |
#' |
|
| 13 |
#' @export |
|
| 14 |
#' |
|
| 15 |
#' @param enadata \code{\link{ENAdata}} that will be used to generate an ENA model
|
|
| 16 |
#' @param dimensions The number of dimensions to include in the dimensional reduction |
|
| 17 |
#' @param norm.by A function to be used to normalize adjacency (co-occurrence) vectors before computing the dimensional reduction, default: sphere_norm_c() |
|
| 18 |
#' @param rotation.by A function to be used to compute the dimensional reduction, default: ena.svd() |
|
| 19 |
#' @param rotation.params (optional) A character vector containing additional parameters for the function in rotation.by, if needed |
|
| 20 |
#' @param rotation.set A previously-constructed ENARotationSet object to use for the dimensional reduction |
|
| 21 |
#' @param endpoints.only A logical variable which determines whether to only show endpoints for trajectory models |
|
| 22 |
#' @param center.align.to.origin A logical variable when TRUE (default) determines aligns both point center and centroid center to the origin |
|
| 23 |
#' @param node.position.method A function to be used to determine node positions based on the dimensional reduction, default: lws.position.es() |
|
| 24 |
#' @param as.list R6 objects will be deprecated, but if this is TRUE, the original R6 object will be returned, otherwise a list with class `ena.set` |
|
| 25 |
#' @param ... additional parameters addressed in inner function |
|
| 26 |
#' |
|
| 27 |
#' @examples |
|
| 28 |
#' data(RS.data) |
|
| 29 |
#' |
|
| 30 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 31 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 32 |
#' |
|
| 33 |
#' accum = ena.accumulate.data( |
|
| 34 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 35 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 36 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 37 |
#' codes = RS.data[,codeNames], |
|
| 38 |
#' window.size.back = 4 |
|
| 39 |
#' ) |
|
| 40 |
#' |
|
| 41 |
#' set = ena.make.set( |
|
| 42 |
#' enadata = accum |
|
| 43 |
#' ) |
|
| 44 |
#' |
|
| 45 |
#' set.means.rotated = ena.make.set( |
|
| 46 |
#' enadata = accum, |
|
| 47 |
#' rotation.by = ena.rotate.by.mean, |
|
| 48 |
#' rotation.params = list( |
|
| 49 |
#' accum$meta.data$Condition=="FirstGame", |
|
| 50 |
#' accum$meta.data$Condition=="SecondGame" |
|
| 51 |
#' ) |
|
| 52 |
#' ) |
|
| 53 |
#' |
|
| 54 |
#' @seealso \code{\link{ena.accumulate.data}}, \code{\link{ENAset}}
|
|
| 55 |
#' |
|
| 56 |
#' @return \code{\link{ENAset}} class object that can be further processed for analysis or plotting
|
|
| 57 |
## |
|
| 58 |
ena.make.set <- function( |
|
| 59 |
enadata, |
|
| 60 |
dimensions = 2, |
|
| 61 |
norm.by = fun_sphere_norm, |
|
| 62 |
rotation.by = ena.svd, |
|
| 63 |
rotation.params = NULL, |
|
| 64 |
rotation.set = NULL, |
|
| 65 |
endpoints.only = TRUE, |
|
| 66 |
center.align.to.origin = TRUE, |
|
| 67 |
node.position.method = lws.positions.sq, |
|
| 68 |
as.list = TRUE, |
|
| 69 |
... |
|
| 70 |
) {
|
|
| 71 | 102x |
if (as.list == F) {
|
| 72 | 14x |
warning(paste0("Usage of ENAdata and ENAset objects will be deprecated ",
|
| 73 | 14x |
"and potentially removed altogether in future versions.")) |
| 74 | ||
| 75 | 14x |
if (!is(enadata, "ENAdata")) {
|
| 76 | 1x |
stop(paste0("Use of ena.make.set with as.list=FALSE requires `enadata` ",
|
| 77 | 1x |
"be an ENAdata object. Re-run the accumulation with as.list=FALSE")) |
| 78 |
} |
|
| 79 | ||
| 80 | 13x |
set <- ENAset$new( |
| 81 | 13x |
enadata = enadata, |
| 82 | 13x |
dimensions = dimensions, |
| 83 | 13x |
rotation.by = ifelse( |
| 84 | 13x |
!is.null(rotation.by) && identical(rotation.by, ena.svd), |
| 85 | 13x |
ena.svd.R6, |
| 86 | 13x |
rotation.by |
| 87 |
), |
|
| 88 | 13x |
rotation.params = rotation.params, |
| 89 | 13x |
rotation.set = rotation.set, |
| 90 | 13x |
norm.by = norm.by, |
| 91 | 13x |
node.position.method = ifelse( |
| 92 | 13x |
identical(node.position.method, lws.positions.sq), |
| 93 | 13x |
lws.positions.sq.R6, |
| 94 | 13x |
node.position.method |
| 95 |
), |
|
| 96 | 13x |
endpoints.only = endpoints.only, |
| 97 | 13x |
center.align.to.origin = center.align.to.origin, |
| 98 |
... |
|
| 99 |
) |
|
| 100 | 13x |
return(set$process()); |
| 101 |
} |
|
| 102 |
else {
|
|
| 103 | 88x |
if ("ENAdata" %in% class(enadata)) {
|
| 104 | 1x |
warning(paste0("Usage of ENAdata objects will be deprecated and ",
|
| 105 | 1x |
"potentially removed altogether in future versions. See ", |
| 106 | 1x |
"ena.accumulate.data() or ena.set().")) |
| 107 | ||
| 108 | 1x |
enadata <- ena.set(enadata) |
| 109 |
} |
|
| 110 | ||
| 111 | 88x |
enadata$`_function.params`$center.align.to.origin <- center.align.to.origin; |
| 112 | 88x |
enadata$`_function.params`$rotation.by <- rotation.by; |
| 113 | 88x |
enadata$`_function.params`$rotation.params <- rotation.params; |
| 114 | ||
| 115 |
### |
|
| 116 |
# Convert the string vector of code names to their corresponding |
|
| 117 |
# co-occurence names |
|
| 118 |
##### |
|
| 119 | 88x |
code_columns <- svector_to_ut(enadata$rotation$codes) |
| 120 | ||
| 121 |
### |
|
| 122 |
# Normalize the raw data using self$function.params$norm.by, |
|
| 123 |
# which defaults to calling rENA::dont_sphere_norm_c |
|
| 124 |
##### |
|
| 125 | 88x |
line.weights <- norm.by(as.matrix(enadata$connection.counts)) |
| 126 | 88x |
colnames(line.weights) <- code_columns |
| 127 | ||
| 128 | 88x |
line.weights.dt <- data.table::as.data.table(line.weights) |
| 129 | 88x |
for (i in seq(ncol(line.weights.dt))) |
| 130 | 1305x |
set(line.weights.dt, j = i, |
| 131 | 1305x |
value = as.ena.co.occurrence(line.weights.dt[[i]])) |
| 132 | ||
| 133 | 88x |
enadata$line.weights <- cbind(enadata$meta.data, line.weights.dt) |
| 134 | 88x |
class(enadata$line.weights) <- c("ena.line.weights", "ena.matrix",
|
| 135 | 88x |
class(enadata$line.weights)) |
| 136 |
##### |
|
| 137 | ||
| 138 |
### |
|
| 139 |
# Center the normed data |
|
| 140 |
##### |
|
| 141 |
# if ( inherits(rotation.set, "ena.rotation.set") ) {
|
|
| 142 | ||
| 143 |
# if ( !is.null(rotation.by) && is.null(rotation.set) ) {
|
|
| 144 |
# points.for.projection <- center_data_c(line.weights) |
|
| 145 |
# } |
|
| 146 | 88x |
if ( !is.null(rotation.set) ) {
|
| 147 | 5x |
if( inherits(rotation.set, "ena.rotation.set") ) {
|
| 148 | 4x |
if(center.align.to.origin) {
|
| 149 | 4x |
points.for.projection <- line.weights |
| 150 | ||
| 151 | 4x |
non_zero_rows <- rowSums(as.matrix(line.weights)) != 0; |
| 152 | 4x |
if(sum(non_zero_rows) > 0) {
|
| 153 | 4x |
points.for.projection[rowSums(as.matrix(line.weights))!=0,] <- center.projection(lws = line.weights[rowSums(as.matrix(line.weights))!=0,], rotation = rotation.set); |
| 154 |
} |
|
| 155 |
else {
|
|
| 156 | ! |
stop("There were no co-occurrences of codes for any of the units within the model as defined.");
|
| 157 |
} |
|
| 158 |
} |
|
| 159 |
else {
|
|
| 160 | ! |
points.for.projection <- center.projection(lws = line.weights, rotation = rotation.set); |
| 161 |
} |
|
| 162 |
} |
|
| 163 |
else {
|
|
| 164 | 1x |
stop("Supplied rotation.set is not an instance of ENARotationSet");
|
| 165 |
} |
|
| 166 |
} |
|
| 167 |
else {
|
|
| 168 | 83x |
if(center.align.to.origin) {
|
| 169 | 81x |
points.for.projection <- line.weights |
| 170 | ||
| 171 | 81x |
non_zero_rows <- rowSums(as.matrix(line.weights))!=0; |
| 172 | 81x |
if(sum(non_zero_rows) > 0) {
|
| 173 | 81x |
points.for.projection[rowSums(as.matrix(line.weights))!=0,] <- center_data_c(line.weights[rowSums(as.matrix(line.weights))!=0,]) |
| 174 |
} |
|
| 175 |
else {
|
|
| 176 | ! |
stop("There were no co-occurrences of codes for any of the units within the model as defined.");
|
| 177 |
} |
|
| 178 |
} |
|
| 179 |
else {
|
|
| 180 | 2x |
points.for.projection <- center_data_c(line.weights) |
| 181 |
} |
|
| 182 |
} |
|
| 183 | ||
| 184 | 86x |
colnames(points.for.projection) <- code_columns; |
| 185 | 86x |
enadata$model$points.for.projection = data.table::as.data.table(points.for.projection) |
| 186 | 86x |
for (i in seq(ncol(enadata$model$points.for.projection))) {
|
| 187 | 1275x |
set( |
| 188 | 1275x |
enadata$model$points.for.projection, |
| 189 | 1275x |
j = i, |
| 190 | 1275x |
value = as.ena.co.occurrence(enadata$model$points.for.projection[[i]]) |
| 191 |
) |
|
| 192 |
} |
|
| 193 | 86x |
enadata$model$points.for.projection <- as.ena.matrix(cbind( |
| 194 | 86x |
enadata$meta.data, |
| 195 | 86x |
enadata$model$points.for.projection |
| 196 | 86x |
), "ena.points") |
| 197 |
##### |
|
| 198 | ||
| 199 |
### |
|
| 200 | ||
| 201 |
### |
|
| 202 |
# Generate and Assign the rotation set |
|
| 203 |
##### |
|
| 204 | 86x |
if (!is.null(rotation.by) && is.null(rotation.set)) {
|
| 205 | 82x |
rotation <- do.call(rotation.by, list(enadata, rotation.params)) |
| 206 |
# added by Carl, 2026.1.6 |
|
| 207 | 81x |
if(is.null(rotation)) |
| 208 |
{
|
|
| 209 | ! |
stop("Unable to create a rotation set")
|
| 210 |
} |
|
| 211 |
# |
|
| 212 | 81x |
enadata$rotation.matrix <- as.data.table(rotation$rotation, keep.rownames = "codes") |
| 213 | 81x |
for (i in seq(ncol(enadata$rotation.matrix))) {
|
| 214 | 1281x |
if(i == 1) {
|
| 215 | 81x |
set(enadata$rotation.matrix, |
| 216 | 81x |
j = i, value = as.ena.metadata(enadata$rotation.matrix[[i]]) |
| 217 |
) |
|
| 218 |
} |
|
| 219 |
else {
|
|
| 220 | 1200x |
set(enadata$rotation.matrix, |
| 221 | 1200x |
j = i, value = as.ena.dimension(enadata$rotation.matrix[[i]]) |
| 222 |
) |
|
| 223 |
} |
|
| 224 |
} |
|
| 225 | 81x |
class(enadata$rotation.matrix) <- c("ena.rotation.matrix", class(enadata$rotation.matrix))
|
| 226 | ||
| 227 | 81x |
enadata$rotation$rotation.matrix <- enadata$rotation.matrix |
| 228 | 81x |
enadata$rotation$eigenvalues <- rotation$eigenvalues; |
| 229 | 81x |
if(center.align.to.origin) {
|
| 230 | 79x |
enadata$rotation$center.vec = colMeans(line.weights[rowSums(as.matrix(line.weights))!=0,]) # ADD CENTERING VEC HERE |
| 231 |
} |
|
| 232 |
else {
|
|
| 233 | 2x |
enadata$rotation$center.vec = colMeans(line.weights) # ADD CENTERING VEC HERE |
| 234 |
} |
|
| 235 |
} |
|
| 236 | 4x |
else if (!is.null(rotation.set)) {
|
| 237 | 3x |
if (is(rotation.set, "ena.rotation.set")) {
|
| 238 | 3x |
enadata$rotation.matrix <- rotation.set$rotation.matrix |
| 239 | 3x |
enadata$rotation$rotation.matrix <- rotation.set$rotation.matrix |
| 240 | 3x |
enadata$rotation$nodes <- rotation.set$nodes; |
| 241 | 3x |
enadata$rotation$eigenvalues <- rotation.set$eigenvalues |
| 242 | 3x |
enadata$rotation$center.vec = rotation.set$center.vec # ADD CENTERING VEC HERE |
| 243 |
} |
|
| 244 |
else {
|
|
| 245 | ! |
stop("Supplied rotation.set is not an instance of ENARotationSet")
|
| 246 |
} |
|
| 247 |
} |
|
| 248 |
else {
|
|
| 249 | 1x |
stop("Unable to find or create a rotation set")
|
| 250 |
} |
|
| 251 |
##### |
|
| 252 | ||
| 253 |
### |
|
| 254 |
# Generate the rotated points |
|
| 255 |
##### |
|
| 256 | 84x |
if (!is.null(enadata$rotation.matrix)) {
|
| 257 | 83x |
points <- points.for.projection %*% as.matrix(enadata$rotation.matrix) |
| 258 | 83x |
points.dt <- as.data.table(points) |
| 259 | 83x |
for (i in seq(ncol(points.dt))) {
|
| 260 | 1230x |
set(points.dt, j = i, value = as.ena.dimension(points.dt[[i]])) |
| 261 |
} |
|
| 262 | 83x |
if(grepl(x = enadata$model$model.type, pattern = "Trajectory")) {
|
| 263 | 4x |
enadata$points <- cbind(enadata$trajectories, points.dt) |
| 264 |
} |
|
| 265 |
else {
|
|
| 266 | 79x |
enadata$points <- cbind(enadata$meta.data, points.dt) |
| 267 |
} |
|
| 268 | 83x |
enadata$points <- as.ena.matrix(enadata$points, "ena.points") |
| 269 |
} |
|
| 270 |
else {
|
|
| 271 | 1x |
stop(paste0("There is no rotation matrix, if you supplied a custom ",
|
| 272 | 1x |
"rotation.set, be sure it contains a rotation.matrix")) |
| 273 |
} |
|
| 274 |
##### |
|
| 275 | ||
| 276 |
### |
|
| 277 |
# Calculate node positions |
|
| 278 |
# - The supplied methoed is responsible is expected to return a list |
|
| 279 |
# with two keys, "node.positions" and "centroids" |
|
| 280 |
##### |
|
| 281 | 83x |
if (exists("rotation") && !is.null(rotation) && is.null(rotation.set)) {
|
| 282 | 81x |
positions <- node.position.method(enadata) |
| 283 | ||
| 284 | 81x |
if (all(names(positions) %in% c("node.positions", "centroids"))) {
|
| 285 | 80x |
enadata$rotation$nodes <- as.data.table(positions$node.positions) |
| 286 | 80x |
colnames(enadata$rotation$nodes) <- colnames(points) |
| 287 | 80x |
rownames(enadata$rotation$nodes) <- enadata$rotation$codes |
| 288 | ||
| 289 | 80x |
for (i in seq(ncol(enadata$rotation$nodes))) {
|
| 290 | 1185x |
set(enadata$rotation$nodes, j = i, |
| 291 | 1185x |
value = as.ena.dimension(enadata$rotation$nodes[[i]])) |
| 292 |
} |
|
| 293 | 80x |
enadata$rotation$nodes <- data.table( |
| 294 | 80x |
code = structure(enadata$rotation$codes, |
| 295 | 80x |
class = c("code", class(enadata$rotation$codes))),
|
| 296 | 80x |
enadata$rotation$nodes |
| 297 |
) |
|
| 298 | 80x |
class(enadata$rotation$nodes) = c("ena.nodes",
|
| 299 | 80x |
class(enadata$rotation$nodes)) |
| 300 | ||
| 301 | 80x |
enadata$model$centroids <- as.data.table(positions$centroids) |
| 302 | 80x |
for (i in seq(ncol(enadata$model$centroids))) {
|
| 303 | 1185x |
set(enadata$model$centroids, j = i, |
| 304 | 1185x |
value = as.ena.dimension(enadata$model$centroids[[i]]) |
| 305 |
) |
|
| 306 |
} |
|
| 307 | 80x |
colnames(enadata$model$centroids) <- colnames(as.matrix(enadata$rotation.matrix)) |
| 308 | 80x |
enadata$model$centroids = cbind( |
| 309 | 80x |
data.table(unit = enadata$model$unit.labels), |
| 310 | 80x |
enadata$model$centroids |
| 311 |
) |
|
| 312 | 80x |
set(enadata$model$centroids, j = 1L, |
| 313 | 80x |
value = as.ena.metadata(enadata$model$centroids[[1L]]) |
| 314 |
) |
|
| 315 | 80x |
enadata$model$centroids <- as.ena.matrix(enadata$model$centroids) |
| 316 |
} |
|
| 317 |
else {
|
|
| 318 | 1x |
stop(paste0("The node position method didn't return back the ",
|
| 319 | 1x |
"expected objects:\n", |
| 320 | 1x |
"\tExpected: c('node.positions','centroids')\n",
|
| 321 | 1x |
"\tReceived: ", names(positions), sep = "")) |
| 322 |
} |
|
| 323 | 2x |
} else if (!is.null(rotation.set)) {
|
| 324 | 2x |
enadata$rotation$nodes <- rotation.set$nodes |
| 325 |
} |
|
| 326 | ||
| 327 | 82x |
if (is.null(enadata$rotation$nodes)) {
|
| 328 | 1x |
stop("Unable to determine the node positions either by calculating
|
| 329 | 1x |
them using `node.position.method` or using a supplied |
| 330 | 1x |
`rotation.set`") |
| 331 |
} |
|
| 332 |
##### |
|
| 333 | ||
| 334 |
### |
|
| 335 |
# Variance |
|
| 336 |
##### |
|
| 337 | 81x |
var_rot_data <- var(points) |
| 338 | 81x |
diagonal_variance <- as.vector(diag(var_rot_data)) |
| 339 | 81x |
enadata$model$variance <- diagonal_variance / sum(diagonal_variance) |
| 340 | 81x |
names(enadata$model$variance) <- colnames(enadata$rotation$rotation.matrix)[-1] |
| 341 |
##### |
|
| 342 | ||
| 343 | 81x |
enadata$plots <- list() #default = ena.plot(enadata, ...)) |
| 344 |
# class(enadata$model$plot) <- c("ena.plot", class(enadata$model$plot))
|
|
| 345 | ||
| 346 | 81x |
enadata$`_function.params`$norm.by <- norm.by |
| 347 | 81x |
return(enadata) |
| 348 |
} |
|
| 349 |
} |
| 1 |
plot_nodes <- function(...) {
|
|
| 2 | 29x |
enaplot$plot <- plotly::add_trace( |
| 3 | 29x |
enaplot$plot, |
| 4 | 29x |
type = "scatter", |
| 5 | 29x |
data = nodes, |
| 6 | 29x |
x = ~X1, |
| 7 | 29x |
y = ~X2, |
| 8 | 29x |
mode = mode, |
| 9 | 29x |
textposition = label.offset[rows.to.keep], |
| 10 | 29x |
marker = list( |
| 11 | 29x |
color = "#000000", |
| 12 | 29x |
size = abs(nodes$weight), |
| 13 | 29x |
line = list( |
| 14 | 29x |
width = 0 |
| 15 |
) |
|
| 16 |
#,name = labels[i] #rownames(nodes)[i] |
|
| 17 |
), |
|
| 18 | 29x |
textfont = list ( |
| 19 | 29x |
family = label.font.family, |
| 20 | 29x |
size = label.font.size, |
| 21 | 29x |
color = label.font.color |
| 22 |
), |
|
| 23 | 29x |
text = labels[rows.to.keep], #rownames(nodes), |
| 24 | 29x |
legendgroup = legend.name, |
| 25 | 29x |
name = legend.name, |
| 26 | 29x |
showlegend = show.legend, |
| 27 | 29x |
hoverinfo = 'none' |
| 28 |
); |
|
| 29 | ||
| 30 | 29x |
return(enaplot$plot); |
| 31 |
} |
|
| 32 | ||
| 33 |
plot_edges <- function(...) {
|
|
| 34 | 29x |
if (length(network.edges.shapes) > 0 ) {
|
| 35 | 29x |
enaplot$plotted$networks[[length(enaplot$plotted$networks) + 1]] <- network.edges.shapes |
| 36 | ||
| 37 | 29x |
for (n in 1:length(network.edges.shapes)) {
|
| 38 | 429x |
e = network.edges.shapes[[n]]; |
| 39 | ||
| 40 | 429x |
name = NULL; |
| 41 | 429x |
show.legend = F; |
| 42 | 429x |
this.name = paste(e$nodes[1],e$nodes[2], sep=".") |
| 43 | 429x |
if(legend.include.edges) {
|
| 44 | ! |
name = this.name; |
| 45 | ! |
show.legend = T; |
| 46 |
} |
|
| 47 | ||
| 48 | 429x |
if (isTRUE(e$self)) {
|
| 49 | 18x |
if (e$line$width > 0) {
|
| 50 | 13x |
enaplot$plot = plotly::add_trace( |
| 51 | 13x |
enaplot$plot, |
| 52 | 13x |
type = "scatter", |
| 53 | 13x |
mode = "markers", |
| 54 | 13x |
data = data.frame(X1 = e$x0, X2 = e$y0), |
| 55 | 13x |
x = ~X1, y = ~X2, |
| 56 | 13x |
marker = list(color = e$line$color, size = e$line$width, line = list(width = 0)), |
| 57 | 13x |
opacity = e$opacity, |
| 58 | 13x |
legendgroup = if(legend.include.edges == T) this.name else legend.name, |
| 59 | 13x |
showlegend = show.legend, |
| 60 | 13x |
name = name, |
| 61 | 13x |
hoverinfo = 'none' |
| 62 |
) |
|
| 63 |
} |
|
| 64 | 18x |
next |
| 65 |
} |
|
| 66 | ||
| 67 | 411x |
enaplot$plot = plotly::add_trace( |
| 68 | 411x |
enaplot$plot, |
| 69 | 411x |
type = "scatter", |
| 70 | 411x |
mode = "lines", |
| 71 | 411x |
data = data.frame(X1=c(e$x0,e$x1), X2=c(e$y0,e$y1)), |
| 72 | 411x |
x = ~X1, y = ~X2, |
| 73 | 411x |
line = e$line, |
| 74 | 411x |
opacity = e$opacity, |
| 75 | 411x |
legendgroup = if(legend.include.edges == T) this.name else legend.name, |
| 76 | 411x |
showlegend = show.legend, |
| 77 | 411x |
name = name |
| 78 |
) |
|
| 79 |
} |
|
| 80 |
} |
|
| 81 | ||
| 82 | 29x |
return(enaplot$plot); |
| 83 |
} |
|
| 84 | ||
| 85 |
## |
|
| 86 |
#' @title Plot an ENA network |
|
| 87 |
#' |
|
| 88 |
#' @description Plot an ENA network: nodes and edges |
|
| 89 |
#' |
|
| 90 |
#' @details lots a network graph, including nodes (taken from codes in the ENAplot) and the edges (provided in network) |
|
| 91 |
#' |
|
| 92 |
#' @export |
|
| 93 |
#' |
|
| 94 |
#' @param enaplot \code{\link{ENAplot}} object to use for plotting
|
|
| 95 |
#' @param network dataframe or matrix containing the edge weights for the network graph; typically comes from ENAset$line.weights |
|
| 96 |
#' @param node.positions matrix containing the positiions of the nodes. Defaults to enaplot$enaset$node.positions |
|
| 97 |
#' @param adjacency.key matrix containing the adjacency key for looking up the names and positions |
|
| 98 |
#' @param colors A String or vector of colors for positive and negative line weights. E.g. red or c(pos= red, neg = blue), default: c(pos= red, neg = blue) |
|
| 99 |
#' @param edge_type A String representing the type of line to draw, either "line", "dash", or "dot" |
|
| 100 |
#' @param show.all.nodes A Logical variable, default: true |
|
| 101 |
#' @param threshold A vector of numeric min/max values, default: c(0,Inf) plotting . Edge weights below the min value will not be displayed; edge weights above the max value will be shown at the max value. |
|
| 102 |
#' @param thin.lines.in.front A logical, default: true |
|
| 103 |
#' @param layers ordering of layers, default: c("nodes", "edges")
|
|
| 104 |
#' @param thickness A vector of numeric min/max values for thickness, default: c(min(abs(network)), max(abs(network))) |
|
| 105 |
#' @param opacity A vector of numeric min/max values for opacity, default: thickness |
|
| 106 |
#' @param saturation A vector of numeric min/max values for saturation, default: thickness |
|
| 107 |
#' @param scale.range A vector of numeric min/max to scale from, default: c(0.1,1) or if min(network) is 0, c(0,1) |
|
| 108 |
#' @param node.size A lower and upper bound used for scaling the size of the nodes, default c(0, 20) |
|
| 109 |
#' @param labels A character vector of node labels, default: code names |
|
| 110 |
#' @param label.offset A character vector of representing the positional offset relative to the respective node. Defaults to "middle right" for all nodes. If a single values is provided, it is used for all positions, else the length of the |
|
| 111 |
#' @param label.font.size An integer which determines the font size for graph labels, default: enaplot$font.size |
|
| 112 |
#' @param label.font.color A character which determines the color of label font, default: enaplot$font.color |
|
| 113 |
#' @param label.font.family A character which determines font type, choices: Arial, Courier New, Times New Roman, default: enaplot$font.family |
|
| 114 |
#' @param legend.name A character name used in the plot legend. Not included in legend when NULL (Default), if legend.include.edges is TRUE will always be "Nodes" |
|
| 115 |
#' @param legend.include.edges Logical value indicating if the edge names should be included in the plot legend. Forces legend.name to be "Nodes" |
|
| 116 |
#' @param scale.weights Logical indicating to scale the supplied network |
|
| 117 |
#' @param ... Additional parameters |
|
| 118 |
#' |
|
| 119 |
#' @seealso \code{\link{ena.plot}}, \code{\link{ena.plot.points}}
|
|
| 120 |
#' @importFrom scales rescale |
|
| 121 | ||
| 122 |
#' @examples |
|
| 123 |
#' data(RS.data) |
|
| 124 |
#' |
|
| 125 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 126 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 127 |
#' |
|
| 128 |
#' accum = ena.accumulate.data( |
|
| 129 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 130 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 131 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 132 |
#' codes = RS.data[,codeNames], |
|
| 133 |
#' window.size.back = 4 |
|
| 134 |
#' ) |
|
| 135 |
#' |
|
| 136 |
#' set = ena.make.set( |
|
| 137 |
#' enadata = accum, |
|
| 138 |
#' rotation.by = ena.rotate.by.mean, |
|
| 139 |
#' rotation.params = list( |
|
| 140 |
#' accum$meta.data$Condition=="FirstGame", |
|
| 141 |
#' accum$meta.data$Condition=="SecondGame" |
|
| 142 |
#' ) |
|
| 143 |
#' ) |
|
| 144 |
#' |
|
| 145 |
#' plot = ena.plot(set) |
|
| 146 |
#' |
|
| 147 |
#' ### Subset rotated points and plot Condition 1 Group Mean |
|
| 148 |
#' as.matrix(set$points$Condition$FirstGame) |
|
| 149 |
#' |
|
| 150 |
#' first.game.points = as.matrix(set$points$Condition$FirstGame) |
|
| 151 |
#' plot = ena.plot.group(plot, first.game.points, labels = "FirstGame", |
|
| 152 |
#' colors = "red", confidence.interval = "box") |
|
| 153 |
#' |
|
| 154 |
#' ### Subset rotated points and plot Condition 2 Group Mean |
|
| 155 |
#' second.game.points = as.matrix(set$points$Condition$SecondGame) |
|
| 156 |
#' plot = ena.plot.group(plot, second.game.points, labels = "SecondGame", |
|
| 157 |
#' colors = "blue", confidence.interval = "box") |
|
| 158 |
#' |
|
| 159 |
#' ### get mean network plots |
|
| 160 |
#' first.game.lineweights = as.matrix(set$line.weights$Condition$FirstGame) |
|
| 161 |
#' first.game.mean = colMeans(first.game.lineweights) |
|
| 162 |
#' |
|
| 163 |
#' second.game.lineweights = as.matrix(set$line.weights$Condition$SecondGame) |
|
| 164 |
#' second.game.mean = colMeans(second.game.lineweights) |
|
| 165 |
#' |
|
| 166 |
#' subtracted.network = first.game.mean - second.game.mean |
|
| 167 |
#' plot = ena.plot.network(plot, network = subtracted.network) |
|
| 168 |
#' print(plot) |
|
| 169 |
#' |
|
| 170 |
#' @return The \code{\link{ENAplot}} provided to the function, with its plot updated to include the nodes and provided connecting lines.
|
|
| 171 |
## |
|
| 172 |
ena.plot.network = function( |
|
| 173 |
enaplot = NULL, |
|
| 174 |
network = NULL, |
|
| 175 |
node.positions = enaplot$enaset$rotation$nodes, |
|
| 176 |
adjacency.key = NULL, #enaplot$enaset$enadata$adjacency.matrix, |
|
| 177 |
colors = c(pos=enaplot$palette[1], enaplot$palette[2]), |
|
| 178 |
edge_type = "line", #c("line", "dash", "dot"),
|
|
| 179 |
show.all.nodes = T, |
|
| 180 |
threshold = c(0), |
|
| 181 |
thin.lines.in.front = T, |
|
| 182 |
layers = c("nodes", "edges"),
|
|
| 183 | ||
| 184 |
thickness = c(min(abs(network)), max(abs(network))), |
|
| 185 |
opacity = thickness, |
|
| 186 |
saturation = thickness, |
|
| 187 |
scale.range = c(ifelse(min(network)==0, 0, 0.1), 1), |
|
| 188 | ||
| 189 |
node.size = c(3,10), |
|
| 190 | ||
| 191 |
labels = NULL, |
|
| 192 |
label.offset = "middle right", |
|
| 193 |
label.font.size = enaplot$get("font.size"),
|
|
| 194 |
label.font.color = enaplot$get("font.color"),
|
|
| 195 |
label.font.family = enaplot$get("font.family"),
|
|
| 196 |
legend.name = NULL, |
|
| 197 |
legend.include.edges = F, |
|
| 198 |
scale.weights = F, |
|
| 199 |
... |
|
| 200 |
) {
|
|
| 201 | 51x |
if (.qe_backend(enaplot)) |
| 202 | 21x |
return(.qe_plot_network(enaplot, supplied = names(match.call())[-1], |
| 203 | 21x |
network = network, node.positions = node.positions, adjacency.key = adjacency.key, |
| 204 | 21x |
colors = colors, edge_type = edge_type, show.all.nodes = show.all.nodes, |
| 205 | 21x |
threshold = threshold, thin.lines.in.front = thin.lines.in.front, layers = layers, |
| 206 | 21x |
labels = labels, label.font.size = label.font.size, label.font.color = label.font.color, |
| 207 | 21x |
label.font.family = label.font.family, legend.name = legend.name, dots = list(...))) |
| 208 | ||
| 209 |
# One weight per pair of nodes, or per pair plus each node's self-connection |
|
| 210 |
# (upper triangle with the diagonal, as accumulate(self_connections = TRUE)). |
|
| 211 | 30x |
n.nodes <- nrow(node.positions) |
| 212 | 30x |
expected_codes <- c(choose(n.nodes, 2), n.nodes * (n.nodes + 1) / 2) |
| 213 | 30x |
if(!length(network) %in% expected_codes) {
|
| 214 | 1x |
if(is.data.frame(network) && ncol(as.matrix(network)) %in% expected_codes) {
|
| 215 | ! |
network = as.vector(as.matrix(network)) |
| 216 |
} |
|
| 217 |
else {
|
|
| 218 | 1x |
stop(paste0("Network vector needs to be of length ", choose(n.nodes, 2),
|
| 219 | 1x |
" (or ", n.nodes * (n.nodes + 1) / 2, " with self-connections)")) |
| 220 |
} |
|
| 221 |
} |
|
| 222 | 29x |
diagonal <- length(network) == expected_codes[2] && expected_codes[2] != expected_codes[1] |
| 223 | 29x |
node.rows <- NULL |
| 224 | 29x |
if(is(node.positions, "ena.nodes")) {
|
| 225 | 27x |
if(is.null(adjacency.key)) {
|
| 226 | 27x |
adjacency.key <- namesToAdjacencyKey(node.positions$code, diagonal = diagonal) |
| 227 |
} |
|
| 228 | 27x |
node.rows <- node.positions$code |
| 229 | ||
| 230 | 27x |
if(is.null(labels)) {
|
| 231 | 27x |
labels <- node.positions$code |
| 232 |
} |
|
| 233 |
} |
|
| 234 |
else {
|
|
| 235 | 2x |
if(is.matrix(node.positions)) {
|
| 236 | 2x |
node.positions <- as.data.frame(node.positions) |
| 237 |
} |
|
| 238 | 2x |
adjacency.key <- namesToAdjacencyKey(rownames(node.positions), diagonal = diagonal) |
| 239 | 2x |
node.rows <- rownames(node.positions) |
| 240 | 2x |
if(is.null(labels)) {
|
| 241 | 2x |
labels <- rownames(node.positions) |
| 242 |
} |
|
| 243 |
} |
|
| 244 | 29x |
args = list(...); |
| 245 | 29x |
network.edges.shapes = list(); |
| 246 | 29x |
edge_type = match.arg(arg = edge_type, choices = c("line", "dash", "dot"));
|
| 247 | ||
| 248 | 29x |
nodes = data.frame(as.matrix(node.positions)); |
| 249 | 29x |
colnames(nodes) = paste0("X", seq(colnames(nodes)))
|
| 250 | 29x |
nodes$weight = rep(0, nrow(nodes)) |
| 251 | 29x |
nodes$color = "black"; |
| 252 | ||
| 253 |
# Handle label parameters |
|
| 254 | 29x |
if(length(label.offset) == 1) {
|
| 255 | 29x |
label.offset = rep(label.offset[1], length(labels)) |
| 256 |
} |
|
| 257 | 29x |
if(length(label.offset) != length(labels)) {
|
| 258 | ! |
stop("length(label.offset) must be equal to 1 or length(labels)")
|
| 259 |
} |
|
| 260 | ||
| 261 |
# Handle legend parameters |
|
| 262 | 29x |
if(legend.include.edges == T && !is.null(legend.name)) {
|
| 263 | ! |
legend.name = "Nodes" |
| 264 |
} |
|
| 265 | ||
| 266 | 29x |
network.scaled = network; |
| 267 | 29x |
if(!is.null(threshold)) {
|
| 268 | 29x |
multiplier.mask = ((network.scaled >= 0) * 1) - ((network.scaled < 0) * 1) |
| 269 | 29x |
if(length(threshold) == 1) {
|
| 270 | 29x |
threshold[2] = Inf; |
| 271 |
} |
|
| 272 | ! |
else if(threshold[2] < threshold[1]) {
|
| 273 | ! |
stop("Minimum threshold value must be less than the maximum value.");
|
| 274 |
} |
|
| 275 | ||
| 276 | 29x |
if(threshold[1] > 0) {
|
| 277 |
# network.scaled = network.scaled[sizes > threshold[1]] |
|
| 278 | ! |
network.scaled[abs(network.scaled) < threshold[1]] = 0 |
| 279 |
} |
|
| 280 | 29x |
if(threshold[2] < Inf && any(abs(network.scaled) > threshold[2])) {
|
| 281 | ! |
to.threshold = abs(network.scaled) > threshold[2] |
| 282 | ! |
network.scaled[to.threshold] = threshold[2] |
| 283 | ! |
network.scaled[to.threshold] = network.scaled[to.threshold] * multiplier.mask[to.threshold] |
| 284 |
} |
|
| 285 |
} |
|
| 286 | 29x |
network.thickness = abs(network.scaled); |
| 287 | 29x |
network.saturation = abs(network.scaled); |
| 288 | 29x |
network.opacity = abs(network.scaled); |
| 289 | ||
| 290 | 29x |
network.to.keep = (network != 0) * 1 |
| 291 | 29x |
if(scale.weights == T && any(network != 0)) {
|
| 292 | 3x |
weight.multiplier = 1 / max(abs(network)); |
| 293 | 3x |
network.scaled = network * weight.multiplier; |
| 294 |
# thickness/saturation/opacity ranges are given in unscaled units; bring |
|
| 295 |
# them onto the same scale as network.scaled so the rescales stay in range. |
|
| 296 |
# Force the saturation/opacity defaults (= thickness) before thickness changes. |
|
| 297 | 3x |
force(saturation); force(opacity); |
| 298 | 3x |
thickness = thickness * weight.multiplier; |
| 299 | 3x |
saturation = saturation * weight.multiplier; |
| 300 | 3x |
opacity = opacity * weight.multiplier; |
| 301 | 3x |
network.thickness = scales::rescale(x = abs(network.scaled), to = scale.range, from = thickness); |
| 302 |
} |
|
| 303 | 29x |
network.scaled = network.scaled * network.to.keep |
| 304 | 29x |
network.thickness = network.thickness * network.to.keep |
| 305 | ||
| 306 |
# hsv() and the edge opacity both require values in [0, 1] |
|
| 307 | 29x |
network.saturation = scales::squish(scales::rescale(x = abs(network.scaled), to = scale.range, from = saturation), c(0, 1)); |
| 308 | 29x |
network.opacity = scales::squish(scales::rescale(x = abs(network.scaled), to = scale.range, from = opacity), c(0, 1)); |
| 309 | ||
| 310 | 29x |
pos.inds = as.numeric(which(network.scaled >=0)); |
| 311 | 29x |
neg.inds = as.numeric(which(network.scaled < 0)); |
| 312 | ||
| 313 | 29x |
colors.hsv = rgb2hsv(col2rgb(colors)) |
| 314 | ||
| 315 | 29x |
if(ncol(colors.hsv) == 1) {
|
| 316 | 16x |
colors.hsv[[4]] = colors.hsv[1] + 0.5; |
| 317 | 16x |
if(colors.hsv[4] > 1) {
|
| 318 | 2x |
colors.hsv[4] = colors.hsv[4] - 1; |
| 319 |
} |
|
| 320 | ||
| 321 | 16x |
colors.hsv[[5]] = colors.hsv[2]; |
| 322 | 16x |
colors.hsv[[6]] = colors.hsv[3]; |
| 323 | 16x |
dim(colors.hsv) = c(3,2); |
| 324 |
} |
|
| 325 | ||
| 326 | 29x |
mat = as.matrix(adjacency.key); |
| 327 | 29x |
for (i in 1:length(network)) {
|
| 328 | 429x |
v0 <- nodes[node.rows==mat[1,i], ]; |
| 329 | 429x |
v1 <- nodes[node.rows==mat[2,i], ]; |
| 330 | 429x |
is.self <- mat[1,i] == mat[2,i] |
| 331 |
# A self-connection counts once toward its node's size, so the node |
|
| 332 |
# stays larger than the circle drawn on it. |
|
| 333 | 429x |
nodes[node.rows==mat[1,i],]$weight = nodes[node.rows==mat[1,i],]$weight + abs(network.thickness[i]); |
| 334 | 429x |
if(!is.self) {
|
| 335 | 411x |
nodes[node.rows==mat[2,i],]$weight = nodes[node.rows==mat[2,i],]$weight + abs(network.thickness[i]); |
| 336 |
} |
|
| 337 | ||
| 338 | 429x |
color = NULL |
| 339 | 429x |
if(i %in% pos.inds) {
|
| 340 | 405x |
color = colors.hsv[,1]; |
| 341 |
} else {
|
|
| 342 | 24x |
color = colors.hsv[,2]; |
| 343 |
} |
|
| 344 | 429x |
color[2] = network.saturation[i]; |
| 345 | ||
| 346 | 429x |
edge_shape = list( |
| 347 | 429x |
type = "line", |
| 348 | 429x |
opacity = network.opacity[i], |
| 349 | 429x |
nodes = c(mat[,i]), |
| 350 | 429x |
line = list( |
| 351 | 429x |
name = "test", |
| 352 | 429x |
color= hsv(color[1],color[2],color[3]), |
| 353 | 429x |
width= abs(network.thickness[i]) * enaplot$get("multiplier"),
|
| 354 | 429x |
dash = edge_type |
| 355 |
), |
|
| 356 | 429x |
x0 = as.numeric(v0[1]), |
| 357 | 429x |
y0 = as.numeric(v0[2]), |
| 358 | 429x |
x1 = as.numeric(v1[1]), |
| 359 | 429x |
y1 = as.numeric(v1[2]), |
| 360 | 429x |
layer = "below", |
| 361 | 429x |
size = as.numeric(abs(network.scaled[i])), |
| 362 | 429x |
self = is.self |
| 363 |
); |
|
| 364 | 429x |
network.edges.shapes[[i]] = edge_shape |
| 365 |
}; |
|
| 366 | ||
| 367 | 29x |
if(thin.lines.in.front) {
|
| 368 | 29x |
network.edges.shapes = network.edges.shapes[rev(order(sapply(network.edges.shapes, "[[", "size")))] |
| 369 |
} |
|
| 370 |
else {
|
|
| 371 | ! |
network.edges.shapes = network.edges.shapes[order(sapply(network.edges.shapes, "[[", "size"))] |
| 372 |
} |
|
| 373 | ||
| 374 | 29x |
rows.to.keep = rep(T, nrow(nodes)) |
| 375 | 29x |
if(show.all.nodes == F) {
|
| 376 | 1x |
rows.to.keep = nodes$weight != 0 |
| 377 |
# nodes = nodes[rownames(nodes) %in% unique(as.character(sapply(network.edges.shapes, "[[", "nodes"))), ] |
|
| 378 |
} |
|
| 379 | 29x |
nodes = nodes[rows.to.keep,]; |
| 380 | 29x |
mode = "markers+text" |
| 381 | 29x |
if(!is.null(args$labels.hide) && args$labels.hide == T) {
|
| 382 | ! |
mode="markers" |
| 383 |
} |
|
| 384 | 29x |
if( any(nodes$weight > 0)) {
|
| 385 | 29x |
nodes$weight = scales::rescale((nodes$weight * (1 / max(abs(nodes$weight)))), node.size) # * enaplot$get("multiplier"));
|
| 386 |
} |
|
| 387 |
else {
|
|
| 388 | ! |
nodes$weight = node.size[2] |
| 389 |
} |
|
| 390 |
# Self-connections are circles on their node, as wide as an edge of the same |
|
| 391 |
# weight (the edges' scale); a node is never drawn smaller than its circle. |
|
| 392 | 29x |
for (e in network.edges.shapes) {
|
| 393 | 429x |
if (isTRUE(e$self)) {
|
| 394 | 18x |
on.node <- node.rows[rows.to.keep] == e$nodes[1] |
| 395 | 18x |
nodes$weight[on.node] = pmax(nodes$weight[on.node], e$line$width) |
| 396 |
} |
|
| 397 |
} |
|
| 398 | ||
| 399 | 29x |
show.legend = !is.null(legend.name); |
| 400 | 29x |
if(legend.include.edges) {
|
| 401 | ! |
if(is.null(legend.name)) {
|
| 402 | ! |
legend.name = "Nodes" |
| 403 |
} |
|
| 404 | ! |
show.legend = T; |
| 405 |
} |
|
| 406 | ||
| 407 |
# browser() |
|
| 408 | 29x |
environment(plot_nodes) <- environment() |
| 409 | 29x |
environment(plot_edges) <- environment() |
| 410 | ||
| 411 | 29x |
for(layer in layers) {
|
| 412 | 58x |
enaplot$plot <- do.call(what = paste0("plot_", layer), args = list())
|
| 413 |
} |
|
| 414 | ||
| 415 | 29x |
enaplot |
| 416 |
} |
| 1 |
#' Accumulate Connection Counts for ENA |
|
| 2 |
#' |
|
| 3 |
#' This function takes a data.frame and accumulates co-occurrences of codes within specified units and conversations (horizon), preparing it for ENA. It's designed to be used with pipes (`|>`).. |
|
| 4 |
#' |
|
| 5 |
#' @param x A data.frame or similar object containing the data to be analyzed. |
|
| 6 |
#' @param units A character vector specifying the columns that define the units of analysis. |
|
| 7 |
#' @param codes A character vector specifying the columns that contain the codes for co-occurrence analysis. |
|
| 8 |
#' @param horizon A character vector specifying the columns that define the conversational boundaries (horizon). |
|
| 9 |
#' @param ... Additional arguments passed to underlying accumulation functions. |
|
| 10 |
#' @param ordered A logical value. If TRUE, creates ordered networks (A -> B is different from B -> A). Defaults to FALSE. |
|
| 11 |
#' @param binary A logical value. If TRUE, each line's connection counts are binarized (0 or 1) before they are summed per unit (unordered networks; ordered counts are always summed raw). Defaults to TRUE. Ignored when `weight_by` is given. |
|
| 12 |
#' @param weight_by Weight model applied to each line's connection counts before they are summed into the unit network (the same stage as `weight.by` in `ena.accumulate.data()`): `"binary"`, `"product"` (the raw, non-binarized counts), `"sqrt"`, or `"log1p"` (alias `"log"`). For unordered networks the weight is applied after each line is folded to the upper triangle; for ordered networks, to each directed cell, and `"binary"` keeps the raw directed counts. Defaults to `"binary"` when `binary = TRUE`, otherwise `"product"`. Passed to [tma::accumulate()]. |
|
| 13 |
#' @param misses A logical value. If TRUE (the default), also accumulate missed co-occurrences -- for each line, the ground count of code i when code j is absent from the response -- kept as `miss.counts` / `model$row.miss.counts` (and self-misses as `self.miss.counts`) alongside the connection counts; they are not used by the model. FALSE skips them (less memory for many codes or long data). Passed to [tma::accumulate()]. |
|
| 14 |
#' @param self_connections A logical value, default FALSE. Unordered networks only: also include each code's self-connection (ground i -> response i, weighted per line like the other columns) as a connection column (A & A, A & B, B & B, ...), so normalization, rotation and node positions include it; nodes then count a self-connection `self_weight` times, as ordered models do (see [model()]). Ordered networks always include the diagonal. Passed to [tma::accumulate()]. |
|
| 15 |
#' |
|
| 16 |
#' @return An ena.set object containing the accumulated connection counts and metadata. |
|
| 17 |
#' @export |
|
| 18 |
#' |
|
| 19 |
#' @examples |
|
| 20 |
#' data(RS.data) |
|
| 21 |
#' |
|
| 22 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 23 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 24 |
#' "Collaboration") |
|
| 25 |
#' units <- c("Condition", "UserName")
|
|
| 26 |
#' horizon <- c("Condition", "GroupName")
|
|
| 27 |
#' enaset <- RS.data |> |
|
| 28 |
#' accumulate(units, codes, horizon) |
|
| 29 |
#' |
|
| 30 |
#' # square-root weighting of each line's co-occurrence counts |
|
| 31 |
#' enaset_sqrt <- RS.data |> |
|
| 32 |
#' accumulate(units, codes, horizon, weight_by = "sqrt") |
|
| 33 |
#' |
|
| 34 |
accumulate <- function( |
|
| 35 |
x, |
|
| 36 |
units = rENA::units(x), |
|
| 37 |
codes = rENA::codes(x), |
|
| 38 |
horizon = rENA::horizon(x), |
|
| 39 |
..., |
|
| 40 |
ordered = FALSE, |
|
| 41 |
binary = TRUE, |
|
| 42 |
weight_by = NULL, |
|
| 43 |
misses = TRUE, |
|
| 44 |
self_connections = FALSE |
|
| 45 |
) {
|
|
| 46 |
# set <- ena.accumulate.data.file( |
|
| 47 |
# file = x, |
|
| 48 |
# units.by = units, |
|
| 49 |
# conversations.by = horizon, |
|
| 50 |
# codes = codes, |
|
| 51 |
# ... |
|
| 52 |
# ) |
|
| 53 | 62x |
args <- list(...) |
| 54 | 62x |
force(units); |
| 55 | 62x |
force(codes); |
| 56 | 62x |
force(horizon); |
| 57 | ||
| 58 | 62x |
hoo_rules <- list( |
| 59 | 62x |
str2lang(paste0("(", paste0(sapply(horizon, function(cb) paste0(cb, " %in% UNIT$", cb)), collapse = " & "), ")"))
|
| 60 |
) |
|
| 61 | 62x |
contexts <- tma::contexts( |
| 62 | 62x |
x, |
| 63 | 62x |
units_by = make.names(units), |
| 64 | 62x |
hoo_rules = hoo_rules, |
| 65 | 62x |
split_rules = function(unit, unit_context) {
|
| 66 | 3120x |
split(unit_context, by = horizon) |
| 67 |
} |
|
| 68 |
) |
|
| 69 | ||
| 70 | 62x |
win_wgts <- if(is.null(args$tensor)) {
|
| 71 | 62x |
args$default_window <- if (is.null(args$default_window)) 1 else args$default_window; |
| 72 | 62x |
args$default_weight <- if (is.null(args$default_weight)) 1 else args$default_weight; |
| 73 | 62x |
tma::context_tensor( |
| 74 | 62x |
df = x, |
| 75 | 62x |
sender_cols = args$tma_ground_cols, |
| 76 | 62x |
receiver_cols = args$tma_response_cols, |
| 77 | 62x |
mode_column = ifelse(is.null(args$mode_column), tma::ATTR_NAMES$CONTEXT_ID, args$mode_column), |
| 78 | 62x |
default_window = args$default_window, |
| 79 | 62x |
default_weight = args$default_weight |
| 80 |
) |
|
| 81 |
} |
|
| 82 |
else {
|
|
| 83 | ! |
args$tensor |
| 84 |
} |
|
| 85 | ||
| 86 |
# args$ordered <- if (is.null(args$ordered)) TRUE else FALSE |
|
| 87 |
# browser() |
|
| 88 | 62x |
set <- tma::accumulate( |
| 89 | 62x |
context_model = contexts, |
| 90 |
# multidim_arr = multidim_arr, |
|
| 91 | 62x |
tensor = win_wgts, |
| 92 |
# time_column = args$time_column, |
|
| 93 | 62x |
codes = make.names(codes), |
| 94 | 62x |
ordered = ordered, |
| 95 | 62x |
binary = binary, |
| 96 | 62x |
weight_by = weight_by, |
| 97 | 62x |
misses = misses, |
| 98 | 62x |
self_connections = self_connections |
| 99 |
) |
|
| 100 | ||
| 101 | 61x |
set$rotation <- list( |
| 102 | 61x |
rotation.matrix = NULL, |
| 103 | 61x |
codes = codes, |
| 104 | 61x |
adjacency.key = sapply(colnames(as.matrix(set$connection.counts)), function(y) strsplit(y, "\\s?&\\s?")[[1]], simplify = T), |
| 105 | 61x |
node.positions = NULL, |
| 106 | 61x |
eigenvalues = NULL, |
| 107 | 61x |
centervec = NULL |
| 108 |
) |
|
| 109 | ||
| 110 | 61x |
return(set) |
| 111 |
} |
|
| 112 | ||
| 113 |
##' Build a Complete ENA Model |
|
| 114 |
#' |
|
| 115 |
#' This function applies a full ENA modeling pipeline to accumulated data. It is a convenience wrapper that chains together normalization, centering, rotation, projection, and optional optimization. Each step can be customized by supplying an alternative function. |
|
| 116 |
#' |
|
| 117 |
#' @param data An `ena.set` object, typically the result of `accumulate()`. |
|
| 118 |
#' @param ... Additional arguments passed to the rotation function specified by `rotate_fun`. |
|
| 119 |
#' @param normalize A function to normalize the connection counts. Defaults to `sphere_norm`. |
|
| 120 |
#' @param center_with A function to center the normalized data. Defaults to `center`. |
|
| 121 |
#' @param rotate_with A function to perform the rotation (e.g., SVD). Defaults to `rotate`. |
|
| 122 |
#' @param project_with A function to project the points into the rotated space. Defaults to `project`. |
|
| 123 |
#' @param optimize_with A function to optimize node positions. Defaults to `optimize`. Can be set to `NULL` or `FALSE` to skip. |
|
| 124 |
#' @param rotate_fun The specific rotation function to be used by `rotate_with`. Defaults to `ena.rotate.by.generalized`. |
|
| 125 |
#' @param rotate_params A list of parameters passed to `rotate_fun` as its |
|
| 126 |
#' `params` argument -- for example the two group-membership vectors for |
|
| 127 |
#' `ena.rotate.by.mean`, or `list(x_var = "Condition")` for |
|
| 128 |
#' `ena.rotate.by.generalized`. The nested form `list(params = <params>)` is |
|
| 129 |
#' also accepted. When empty (the default), `rotate()` picks its default |
|
| 130 |
#' parameters. |
|
| 131 |
#' @param exclude_zero_networks A logical value passed to `center_with` and |
|
| 132 |
#' `optimize_with`. When `TRUE` (default), units whose line weights are all |
|
| 133 |
#' zero are left at the origin: they do not enter the centering mean, the |
|
| 134 |
#' rotation or the node optimization, and their points and centroids are |
|
| 135 |
#' zero. This is `ena.make.set(center.align.to.origin = TRUE)`, the webtool's |
|
| 136 |
#' rule, for every kind of set. `FALSE` centers on all units, as |
|
| 137 |
#' `center.align.to.origin = FALSE` does. |
|
| 138 |
#' @param center_to_origin A logical value. When `TRUE`, the mean of all projected |
|
| 139 |
#' points is subtracted from both the points and the node positions after |
|
| 140 |
#' optimization, placing the centroid of the space at the origin. This is the |
|
| 141 |
#' default behavior for ONA (Ordered Network Analysis). Defaults to `FALSE`. |
|
| 142 |
#' @param self_weight Ordered sets: weight of a self-connection in its code's |
|
| 143 |
#' node weight, passed to `optimize_with` when it takes `self_weight`. 2 |
|
| 144 |
#' (default) = the node's row plus column sum, so a self-connection counts |
|
| 145 |
#' twice (Zhiqiang Cai's 2021 rule); 1 counts it once. |
|
| 146 |
#' |
|
| 147 |
#' @return An `ena.set` object with a complete ENA model, including projected points and node positions. |
|
| 148 |
#' @export |
|
| 149 |
#' |
|
| 150 |
#' @examples |
|
| 151 |
#' data(RS.data) |
|
| 152 |
#' |
|
| 153 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 154 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 155 |
#' "Collaboration") |
|
| 156 |
#' units <- c("Condition", "UserName")
|
|
| 157 |
#' horizon <- c("Condition", "GroupName")
|
|
| 158 |
#' enaset <- RS.data |> |
|
| 159 |
#' accumulate(units, codes, horizon) |> |
|
| 160 |
#' model() |
|
| 161 |
model <- function( |
|
| 162 |
data, ..., |
|
| 163 |
normalize = sphere_norm, |
|
| 164 |
center_with = center, |
|
| 165 |
rotate_with = rotate, |
|
| 166 |
project_with = project, |
|
| 167 |
optimize_with = optimize, |
|
| 168 |
# Rotation specific parameters |
|
| 169 |
rotate_fun = ena.rotate.by.generalized, |
|
| 170 |
rotate_params = list(), |
|
| 171 |
# Centering options |
|
| 172 |
exclude_zero_networks = TRUE, |
|
| 173 |
center_to_origin = FALSE, |
|
| 174 |
# Ordered node optimization: weight of a self-connection in its node's weight |
|
| 175 |
self_weight = 2 |
|
| 176 |
) {
|
|
| 177 | 59x |
x <- normalize(data) |
| 178 | 59x |
x <- center_with(x, exclude_zero_networks = exclude_zero_networks) |
| 179 | ||
| 180 |
# rotate() hands `params` to the rotation function. rotate_params used to |
|
| 181 |
# be passed as `by` -- flattened with unlist() when it had more than one |
|
| 182 |
# element -- which rotate() either ignored (one element) or failed on |
|
| 183 |
# (several), e.g. the two groups of a means rotation. |
|
| 184 | 59x |
if (length(rotate_params) == 0) {
|
| 185 | 54x |
x <- rotate_with(x, wh = rotate_fun, by = rotate_params) |
| 186 |
} |
|
| 187 |
else {
|
|
| 188 | 5x |
params <- if (identical(names(rotate_params), "params")) rotate_params$params else rotate_params |
| 189 | 5x |
x <- rotate_with(x, wh = rotate_fun, params = params) |
| 190 |
} |
|
| 191 | ||
| 192 | 59x |
x <- project_with(x) |
| 193 | ||
| 194 | 59x |
if (!is.null(optimize_with) && !isFALSE(optimize_with)) {
|
| 195 | 59x |
opt_args <- list(x) |
| 196 | 59x |
opt_formals <- names(formals(optimize_with)) |
| 197 | 59x |
if ("self_weight" %in% opt_formals) opt_args$self_weight <- self_weight
|
| 198 | 59x |
if ("exclude_zero_networks" %in% opt_formals) opt_args$exclude_zero_networks <- exclude_zero_networks
|
| 199 | 59x |
x <- do.call(optimize_with, opt_args) |
| 200 |
} |
|
| 201 | ||
| 202 | 59x |
if (isTRUE(center_to_origin) && !is.null(x$points)) {
|
| 203 |
# x$points dimension columns are classed ena.co.occurrence (not ena.dimension), |
|
| 204 |
# so use !find_meta_cols to locate them. |
|
| 205 | ! |
dim_cols <- which(!find_meta_cols(x$points)) |
| 206 | ! |
node_dim_cols <- which(find_dimension_cols(x$rotation$nodes)) |
| 207 |
# Compute per-dimension means from the projected points |
|
| 208 | ! |
pt_means <- as.list(colMeans(x$points[, dim_cols, with = FALSE])) |
| 209 |
# Translate points so their centroid is at the origin |
|
| 210 | ! |
x$points[, c(dim_cols) := lapply(.SD, function(col) col - mean(col)), .SDcols = dim_cols] |
| 211 |
# Translate nodes by the same vector |
|
| 212 | ! |
x$rotation$nodes[, c(node_dim_cols) := Map(`-`, .SD, pt_means), .SDcols = node_dim_cols] |
| 213 |
} |
|
| 214 | ||
| 215 | 59x |
return(x) |
| 216 |
} |
|
| 217 | ||
| 218 |
##' Apply Spherical Normalization to ENA Data |
|
| 219 |
#' |
|
| 220 |
#' This function applies spherical normalization to the connection counts in an `ena.set` object or to a raw matrix of connection counts. Normalization is a key step before centering and rotation in ENA. |
|
| 221 |
#' |
|
| 222 |
#' @param x An `ena.set` object or a numeric matrix of connection counts. |
|
| 223 |
#' @param add.meta A logical value. If `TRUE` (the default), metadata from the `ena.set` is preserved and included in the output. This parameter is ignored if `x` is a matrix. |
|
| 224 |
#' |
|
| 225 |
#' @return If `x` is an `ena.set`, it returns the modified `ena.set` with a new `line.weights` matrix and an updated `centervec` in the `rotation` object. If `x` is a matrix, it returns a matrix of normalized line weights. |
|
| 226 |
#' @export |
|
| 227 |
#' |
|
| 228 |
#' @examples |
|
| 229 |
#' data(RS.data) |
|
| 230 |
#' |
|
| 231 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 232 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 233 |
#' "Collaboration") |
|
| 234 |
#' units <- c("Condition", "UserName")
|
|
| 235 |
#' horizon <- c("Condition", "GroupName")
|
|
| 236 |
#' enaset <- RS.data |> |
|
| 237 |
#' accumulate(units, codes, horizon) |> |
|
| 238 |
#' sphere_norm() |
|
| 239 |
sphere_norm <- function(x, add.meta = TRUE) {
|
|
| 240 | 65x |
x_ <- NULL |
| 241 | 65x |
names_ <- NULL |
| 242 | 65x |
meta_ <- NULL |
| 243 | ||
| 244 |
# verify that the connection.counts exist |
|
| 245 | ||
| 246 | 65x |
if (is(x, "ena.set")) {
|
| 247 | 65x |
if (is.null(x$connection.counts)) {
|
| 248 | ! |
stop("Connection counts are missing.")
|
| 249 |
} |
|
| 250 | ||
| 251 | 65x |
x_ <- as.matrix(x$connection.counts) |
| 252 | 65x |
names_ <- colnames(x_) |
| 253 | 65x |
if (isTRUE(add.meta)) {
|
| 254 | 65x |
meta_ <- x$meta.data |
| 255 |
} |
|
| 256 | ||
| 257 | 65x |
x$line.weights <- fun_sphere_norm(x_) |
| 258 | 65x |
colnames(x$line.weights) <- names_ |
| 259 | ||
| 260 | 65x |
x$line.weights <- as_line_weights_matrix(x$line.weights, meta_) |
| 261 | 65x |
x$rotation$centervec <- colMeans(x$line.weights) |
| 262 |
} |
|
| 263 |
else {
|
|
| 264 | ! |
x_ <- as.matrix(x); |
| 265 | ! |
names_ <- colnames(x_); |
| 266 | ! |
x <- fun_sphere_norm(x_); |
| 267 | ! |
colnames(x) <- names_; |
| 268 |
} |
|
| 269 | ||
| 270 | 65x |
return(x) |
| 271 |
} |
|
| 272 | ||
| 273 | ||
| 274 |
##' Scale ENA Data Without Spherical Normalization |
|
| 275 |
#' |
|
| 276 |
#' The counterpart of [sphere_norm()] for `norm.by = fun_skip_sphere_norm`: |
|
| 277 |
#' instead of scaling each unit's network to length 1, every network is divided |
|
| 278 |
#' by the length of the longest one, so relative magnitudes across units are |
|
| 279 |
#' kept. Pass it as `normalize` to [model()] (or to `ona::model()`, which hands |
|
| 280 |
#' it on) to build a model without sphere normalization. |
|
| 281 |
#' |
|
| 282 |
#' @param x An `ena.set` object or a numeric matrix of connection counts. |
|
| 283 |
#' @param add.meta A logical value. If `TRUE` (the default), metadata from the `ena.set` is preserved and included in the output. This parameter is ignored if `x` is a matrix. |
|
| 284 |
#' |
|
| 285 |
#' @return If `x` is an `ena.set`, it returns the modified `ena.set` with a new `line.weights` matrix and an updated `centervec` in the `rotation` object. If `x` is a matrix, it returns a matrix of scaled line weights. |
|
| 286 |
#' @export |
|
| 287 |
#' |
|
| 288 |
#' @examples |
|
| 289 |
#' data(RS.data) |
|
| 290 |
#' |
|
| 291 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 292 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 293 |
#' "Collaboration") |
|
| 294 |
#' units <- c("Condition", "UserName")
|
|
| 295 |
#' horizon <- c("Condition", "GroupName")
|
|
| 296 |
#' enaset <- RS.data |> |
|
| 297 |
#' accumulate(units, codes, horizon) |> |
|
| 298 |
#' model(normalize = skip_sphere_norm) |
|
| 299 |
skip_sphere_norm <- function(x, add.meta = TRUE) {
|
|
| 300 | 3x |
if (is(x, "ena.set")) {
|
| 301 | 2x |
if (is.null(x$connection.counts)) {
|
| 302 | ! |
stop("Connection counts are missing.")
|
| 303 |
} |
|
| 304 | ||
| 305 | 2x |
x_ <- as.matrix(x$connection.counts) |
| 306 | 2x |
names_ <- colnames(x_) |
| 307 | 2x |
meta_ <- if (isTRUE(add.meta)) x$meta.data else NULL |
| 308 | ||
| 309 | 2x |
x$line.weights <- fun_skip_sphere_norm(x_) |
| 310 | 2x |
colnames(x$line.weights) <- names_ |
| 311 | ||
| 312 | 2x |
x$line.weights <- as_line_weights_matrix(x$line.weights, meta_) |
| 313 | 2x |
x$rotation$centervec <- colMeans(x$line.weights) |
| 314 |
} |
|
| 315 |
else {
|
|
| 316 | 1x |
x_ <- as.matrix(x) |
| 317 | 1x |
names_ <- colnames(x_) |
| 318 | 1x |
x <- fun_skip_sphere_norm(x_) |
| 319 | 1x |
colnames(x) <- names_ |
| 320 |
} |
|
| 321 | ||
| 322 | 3x |
return(x) |
| 323 |
} |
|
| 324 | ||
| 325 |
as_points_matrix <- function(x, metadata = NULL) {
|
|
| 326 | 128x |
x_ <- data.table::as.data.table(x) |
| 327 | 128x |
for (i in seq(ncol(x_))) {
|
| 328 | 2508x |
set(x_, |
| 329 | 2508x |
j = i, |
| 330 | 2508x |
value = as.ena.co.occurrence(x_[[i]]) |
| 331 |
) |
|
| 332 |
} |
|
| 333 | ||
| 334 | 128x |
if (!is.null(metadata)) {
|
| 335 | 128x |
x_ <- cbind(metadata, x_) |
| 336 |
} |
|
| 337 | ||
| 338 | 128x |
class(x_) <- c("ena.points", "ena.matrix", class(x_))
|
| 339 | ||
| 340 | 128x |
return(x_) |
| 341 |
} |
|
| 342 | ||
| 343 |
as_line_weights_matrix <- function(x, metadata = NULL) {
|
|
| 344 | 67x |
line.weights.dt <- data.table::as.data.table(x) |
| 345 | 67x |
for (i in seq(ncol(line.weights.dt))) {
|
| 346 | 1320x |
set(line.weights.dt, |
| 347 | 1320x |
j = i, |
| 348 | 1320x |
value = as.ena.co.occurrence(line.weights.dt[[i]]) |
| 349 |
) |
|
| 350 |
} |
|
| 351 | ||
| 352 | 67x |
x_ <- line.weights.dt |
| 353 | 67x |
if (!is.null(metadata)) {
|
| 354 | 67x |
x_ <- cbind(metadata, line.weights.dt) |
| 355 |
} |
|
| 356 | ||
| 357 | 67x |
class(x_) <- c("ena.line.weights", "ena.matrix", class(line.weights.dt))
|
| 358 | ||
| 359 | 67x |
return(x_) |
| 360 |
} |
|
| 361 | ||
| 362 |
as_rotation_matrix <- function(x) {
|
|
| 363 | 122x |
x_ <- data.table::as.data.table(x, keep.rownames = "codes") |
| 364 | 122x |
for (i in seq(ncol(x_))) {
|
| 365 | 2540x |
if (i == 1) {
|
| 366 | 122x |
set(x_, j = i, value = as.ena.metadata(x_[[i]])) |
| 367 |
} else {
|
|
| 368 | 2418x |
set(x_, j = i, value = as.ena.dimension(x_[[i]])) |
| 369 |
} |
|
| 370 |
} |
|
| 371 | 122x |
class(x_) <- c("ena.rotation.matrix", class(x_))
|
| 372 | ||
| 373 | 122x |
return(x_) |
| 374 |
} |
|
| 375 | ||
| 376 |
as_nodes_matrix <- function(x, rows, cols = NULL, cls = "ena.matrix") {
|
|
| 377 | 122x |
x_ <- data.table::data.table(rows[[1]], x) |
| 378 | 122x |
rownames(x_) <- rows[[1]] |
| 379 | ||
| 380 | 122x |
if (!is.null(cols)) {
|
| 381 | 122x |
colnames(x_) <- c(names(rows), cols) |
| 382 |
} |
|
| 383 | ||
| 384 | 122x |
for (i in seq(ncol(x_))) {
|
| 385 | 2540x |
if (i == 1) {
|
| 386 | 122x |
set(x_, j = i, value = as.ena.metadata(x_[[i]])) |
| 387 |
} else {
|
|
| 388 | 2418x |
set(x_, j = i, value = as.ena.dimension(x_[[i]])) |
| 389 |
} |
|
| 390 |
} |
|
| 391 | ||
| 392 | 122x |
class(x_) <- c(cls, class(x_)) |
| 393 | ||
| 394 | 122x |
return(x_) |
| 395 |
} |
|
| 396 | ||
| 397 |
##' Center ENA Data |
|
| 398 |
#' |
|
| 399 |
#' This function centers the line weights of an `ena.set` by subtracting the mean of each connection from all units. This is a standard step in preparing data for rotation. |
|
| 400 |
#' |
|
| 401 |
#' @param x An `ena.set` object (typically after `sphere_norm()`) or a numeric matrix. |
|
| 402 |
#' @param add.meta A logical value. If `TRUE` (the default), metadata is preserved. Ignored if `x` is a matrix. |
|
| 403 |
#' @param exclude_zero_networks A logical value. If `TRUE`, units whose line |
|
| 404 |
#' weights are all zero are left at zero: the column means are computed from |
|
| 405 |
#' the other units and subtracted from those units only, so an empty network |
|
| 406 |
#' sits at the centroid of the others and projects to the origin. This is |
|
| 407 |
#' what `ena.make.set(center.align.to.origin = TRUE)` does. `FALSE` (default |
|
| 408 |
#' here; `model()` defaults to `TRUE`) subtracts the mean of all units from |
|
| 409 |
#' every unit, so an empty network becomes minus the mean. |
|
| 410 |
#' |
|
| 411 |
#' @return If `x` is an `ena.set`, it returns the modified `ena.set` with the centered data stored in `x$model$points.for.projection`. If `x` is a matrix, it returns a centered matrix. |
|
| 412 |
#' @export |
|
| 413 |
#' |
|
| 414 |
#' @examples |
|
| 415 |
#' data(RS.data) |
|
| 416 |
#' |
|
| 417 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 418 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 419 |
#' "Collaboration") |
|
| 420 |
#' units <- c("Condition", "UserName")
|
|
| 421 |
#' horizon <- c("Condition", "GroupName")
|
|
| 422 |
#' enaset <- RS.data |> |
|
| 423 |
#' accumulate(units, codes, horizon) |> |
|
| 424 |
#' sphere_norm() |> |
|
| 425 |
#' center() |
|
| 426 |
center <- function(x, add.meta = TRUE, exclude_zero_networks = FALSE) {
|
|
| 427 | 67x |
x_ <- NULL |
| 428 | 67x |
names_ <- NULL |
| 429 | 67x |
meta_ <- NULL |
| 430 | ||
| 431 |
# Center the non-zero rows on their own mean and leave all-zero rows at |
|
| 432 |
# zero, as ENAset's center.align.to.origin does: an empty network then sits |
|
| 433 |
# at the centroid of the others rather than at minus their mean. |
|
| 434 | 67x |
center_excluding_zeros <- function(m) {
|
| 435 | 60x |
nonzero <- rowSums(m) != 0 |
| 436 | 60x |
if (!any(nonzero)) {
|
| 437 | ! |
return(center_data_c(m)) |
| 438 |
} |
|
| 439 | 60x |
m[nonzero, ] <- center_data_c(m[nonzero, , drop = FALSE]) |
| 440 | 60x |
m |
| 441 |
} |
|
| 442 | ||
| 443 | 67x |
if (is(x, "ena.set")) {
|
| 444 |
# make sure the line weights exist and are a matrix |
|
| 445 | 67x |
if (is.null(x$line.weights)) {
|
| 446 | ! |
stop("Missing line.weights on the provided ENA set. This is typically created using the 'accumulate' and 'sphere_norm' functions.")
|
| 447 |
} |
|
| 448 | ||
| 449 | 67x |
x_ <- as.matrix(x$line.weights) |
| 450 |
# Column names from the codes, for the three layouts: unordered |
|
| 451 |
# (choose(p, 2)), unordered with self-connections (p(p+1)/2; A & A, A & B, |
|
| 452 |
# B & B, ...) and ordered (p^2). |
|
| 453 | 67x |
p_codes <- length(x$rotation$codes) |
| 454 | 67x |
key_ <- if (ncol(x_) == choose(p_codes, 2)) {
|
| 455 | 41x |
tma::adjacency_key(x$rotation$codes, TRUE) |
| 456 | 67x |
} else if (ncol(x_) == p_codes * (p_codes + 1) / 2) {
|
| 457 | 9x |
tma::adjacency_key(x$rotation$codes, TRUE, diagonal = TRUE) |
| 458 |
} else {
|
|
| 459 | 17x |
tma::adjacency_key(x$rotation$codes, FALSE) |
| 460 |
} |
|
| 461 | 67x |
names_ <- apply(key_, 2, paste, collapse = " & ") |
| 462 | 67x |
if (isTRUE(add.meta)) {
|
| 463 | 67x |
meta_ <- x$meta.data |
| 464 |
} |
|
| 465 | ||
| 466 | 67x |
centered <- if (isTRUE(exclude_zero_networks)) {
|
| 467 | 60x |
center_excluding_zeros(x_) |
| 468 |
} else {
|
|
| 469 | 7x |
center_data_c(x_) |
| 470 |
} |
|
| 471 | ||
| 472 | 67x |
colnames(centered) <- names_ |
| 473 | 67x |
x$model$points.for.projection <- as_points_matrix(centered, meta_) |
| 474 |
} |
|
| 475 |
else {
|
|
| 476 | ! |
x_ <- as.matrix(x) |
| 477 | ! |
names_ <- colnames(x_) |
| 478 | ! |
x <- if (isTRUE(exclude_zero_networks)) {
|
| 479 | ! |
center_excluding_zeros(x_) |
| 480 |
} else {
|
|
| 481 | ! |
center_data_c(x_) |
| 482 |
} |
|
| 483 | ! |
colnames(x) <- names_ |
| 484 |
} |
|
| 485 | ||
| 486 | 67x |
return(x) |
| 487 |
} |
|
| 488 | ||
| 489 |
#' Rotate ENA Data |
|
| 490 |
#' |
|
| 491 |
#' Rotates ENA data using a specified rotation function (default: SVD), optionally using formulas or grouping variables. |
|
| 492 |
#' |
|
| 493 |
#' @param x An \code{ena.set} object to be rotated.
|
|
| 494 |
#' @param ... Optional formulas or additional arguments for rotation. |
|
| 495 |
#' @param wh Function to use for rotation (default: \code{ena.svd}).
|
|
| 496 |
#' |
|
| 497 |
#' @return The rotated \code{ena.set} object with updated rotation matrices.
|
|
| 498 |
#' @export |
|
| 499 |
#' |
|
| 500 |
#' @examples |
|
| 501 |
#' # Assuming 'set' is an ena.set object: |
|
| 502 |
#' data(RS.data) |
|
| 503 |
#' |
|
| 504 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 505 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 506 |
#' "Collaboration") |
|
| 507 |
#' units <- c("Condition", "UserName")
|
|
| 508 |
#' horizon <- c("Condition", "GroupName")
|
|
| 509 |
#' enaset <- RS.data |> |
|
| 510 |
#' accumulate(units, codes, horizon) |> |
|
| 511 |
#' sphere_norm() |> |
|
| 512 |
#' center() |> |
|
| 513 |
#' rotate() |
|
| 514 |
rotate <- function( |
|
| 515 |
x, |
|
| 516 |
..., |
|
| 517 |
wh = ena.rotate.by.generalized |
|
| 518 |
) {
|
|
| 519 | 61x |
x_ <- NULL |
| 520 | 61x |
names_ <- NULL |
| 521 | 61x |
codes_ <- NULL |
| 522 | 61x |
meta_ <- NULL |
| 523 | 61x |
dot_args <- list(...) |
| 524 | ||
| 525 | 61x |
if (is(x, "ena.set")) {
|
| 526 |
# Make sure points.for.projection exists |
|
| 527 | 61x |
if (is.null(x$model$points.for.projection)) {
|
| 528 | ! |
stop("Missing `points.for.projection` on the provided ENA set. This is typically created using ?center()")
|
| 529 |
} |
|
| 530 | ||
| 531 | 61x |
if (!is.null(dot_args$add.meta) && isTRUE(dot_args$add.meta)) {
|
| 532 | ! |
meta_ <- x$meta.data |
| 533 |
} |
|
| 534 |
} |
|
| 535 |
else {
|
|
| 536 |
# Construct ENAset-like list from provided matrix |
|
| 537 | ! |
x_ <- as.matrix(x); |
| 538 | ! |
names_ <- colnames(as.matrix(x_)); |
| 539 | ! |
codes_ <- unique(unlist(strsplit(names_, " & "))); |
| 540 | ||
| 541 | ! |
x <- list( |
| 542 | ! |
model = list( |
| 543 | ! |
points.for.projection = x_ |
| 544 |
), |
|
| 545 | ! |
rotation = list( |
| 546 | ! |
codes = codes_ |
| 547 |
) |
|
| 548 |
) |
|
| 549 |
} |
|
| 550 | ||
| 551 | 61x |
by_vals <- NULL |
| 552 | ||
| 553 |
# if (length(dot_args) == 0) {
|
|
| 554 |
# wh <- ena.svd |
|
| 555 |
# } |
|
| 556 |
# else {
|
|
| 557 | 61x |
dot_formulas <- sapply(dot_args, function(d) {
|
| 558 | 60x |
d2 <- tryCatch( |
| 559 |
{
|
|
| 560 | 60x |
d3 <- as.formula(d) |
| 561 | ! |
TRUE |
| 562 |
}, |
|
| 563 | 60x |
error = function(e) FALSE |
| 564 |
) |
|
| 565 | 60x |
return(d2) |
| 566 |
}) |
|
| 567 | 61x |
if (any(dot_formulas)) {
|
| 568 | ! |
if (all(dot_formulas)) {
|
| 569 | ! |
wh <- ena.rotate.by.hena.regression_2 |
| 570 | ! |
by_vals <- list(params = dot_args) |
| 571 | ! |
names(by_vals$params) <- c("x_var", "y_var")[seq_along(by_vals)]
|
| 572 |
} |
|
| 573 |
else {
|
|
| 574 | ! |
stop("If rotating using a formula, all must be formulas")
|
| 575 |
} |
|
| 576 |
} |
|
| 577 |
else {
|
|
| 578 |
# Means rotation? |
|
| 579 | 61x |
by_vals <- list(); |
| 580 | 61x |
if (!is.null(dot_args$params)) {
|
| 581 | 6x |
by_vals <- dot_args$params |
| 582 |
} |
|
| 583 | 55x |
else if (!is.null(dot_args$by$params)) {
|
| 584 | ! |
by_vals <- dot_args$by$params |
| 585 |
} |
|
| 586 |
else {
|
|
| 587 | 55x |
by_vals <- list( |
| 588 | 55x |
x_var = NULL, |
| 589 | 55x |
y_var = NULL |
| 590 |
) |
|
| 591 | ||
| 592 | 55x |
first_meta <- setdiff(colnames(x$connection.counts)[find_meta_cols(x$connection.counts)], c("QEUNIT", "ENA_UNIT"))[1]
|
| 593 |
# args$rotate.by is a list of columns to subset from accum$connection.counts |
|
| 594 | 55x |
by_vals$x_var <- x$connection.counts[, ..first_meta, drop = FALSE]; |
| 595 |
} |
|
| 596 |
} |
|
| 597 |
# } |
|
| 598 | ||
| 599 |
# The rotation function returns a new rotation; keep what describes the |
|
| 600 |
# columns (the adjacency key, which node optimization reads) and the |
|
| 601 |
# centering vector. |
|
| 602 | 61x |
kept <- if (is.list(x$rotation)) x$rotation[intersect(c("adjacency.key", "centervec"), names(x$rotation))] else list()
|
| 603 | ||
| 604 | 61x |
x$rotation <- do.call(wh, list(enaset = x, params = by_vals)) |
| 605 | ||
| 606 |
# Ensure x$rotation is a list with required elements |
|
| 607 | 61x |
if (!is.list(x$rotation)) {
|
| 608 | ! |
stop("Rotation function did not return a list as expected.")
|
| 609 |
} |
|
| 610 | ||
| 611 |
# Only extract elements that exist in the returned list |
|
| 612 | 61x |
rotation_elements <- c("eigenvalues", "codes", "node.positions", "rotation")
|
| 613 | 61x |
x$rotation <- x$rotation[intersect(rotation_elements, names(x$rotation))] |
| 614 | 61x |
for (nm in names(kept)) if (!is.null(kept[[nm]])) x$rotation[[nm]] <- kept[[nm]] |
| 615 | ||
| 616 | 61x |
if (!is.null(x$rotation$rotation)) {
|
| 617 | 61x |
x$rotation.matrix <- as_rotation_matrix(x$rotation$rotation) |
| 618 | 61x |
x$rotation$rotation.matrix <- x$rotation.matrix |
| 619 | 61x |
x$rotation$rotation <- NULL |
| 620 |
} |
|
| 621 |
else {
|
|
| 622 | ! |
x$rotation.matrix <- NULL |
| 623 | ! |
x$rotation$rotation.matrix <- NULL |
| 624 |
} |
|
| 625 | ||
| 626 | 61x |
x$rotation.matrix <- as_rotation_matrix(x$rotation$rotation) |
| 627 | 61x |
x$rotation$rotation.matrix <- x$rotation.matrix |
| 628 | 61x |
x$rotation$rotation <- NULL |
| 629 | ||
| 630 | 61x |
return(x) |
| 631 |
} |
|
| 632 | ||
| 633 |
##' Project ENA Points onto Rotated Space |
|
| 634 |
#' |
|
| 635 |
#' This function projects ENA points onto the rotated space using the rotation matrix. |
|
| 636 |
#' Optionally, metadata can be included in the resulting points matrix. |
|
| 637 |
#' |
|
| 638 |
#' @param x An \code{ena.set} object containing the points for projection and rotation matrix.
|
|
| 639 |
#' @param rotation Optional. A rotation matrix to use for projection if \code{x} is not an \code{ena.set}.
|
|
| 640 |
#' @param add.meta Logical. If \code{TRUE} (default), metadata will be included in the output.
|
|
| 641 |
#' |
|
| 642 |
#' @return The input \code{ena.set} object with the projected points matrix (and metadata if requested).
|
|
| 643 |
#' @export |
|
| 644 |
#' |
|
| 645 |
#' @examples |
|
| 646 |
#' # Assuming 'set' is an ena.set object: |
|
| 647 |
#' data(RS.data) |
|
| 648 |
#' |
|
| 649 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 650 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 651 |
#' "Collaboration") |
|
| 652 |
#' units <- c("Condition", "UserName")
|
|
| 653 |
#' horizon <- c("Condition", "GroupName")
|
|
| 654 |
#' enaset <- RS.data |> |
|
| 655 |
#' accumulate(units, codes, horizon) |> |
|
| 656 |
#' sphere_norm() |> |
|
| 657 |
#' center() |> |
|
| 658 |
#' rotate() |> |
|
| 659 |
#' project() |
|
| 660 |
project <- function(x, rotation = NULL, add.meta = TRUE) {
|
|
| 661 | 61x |
meta_ <- NULL |
| 662 | ||
| 663 | 61x |
if (is(x, "ena.set")) {
|
| 664 | 61x |
points <- as.matrix(x$model$points.for.projection) %*% as.matrix(x$rotation.matrix); |
| 665 | ||
| 666 | 61x |
if (isTRUE(add.meta)) {
|
| 667 | 61x |
meta_ <- x$meta.data; |
| 668 |
} |
|
| 669 | 61x |
x$points <- as_points_matrix(points, meta_); |
| 670 | ||
| 671 | 61x |
var_rot_data <- var(points) |
| 672 | 61x |
diagonal_variance <- as.vector(diag(var_rot_data)) |
| 673 | 61x |
x$model$variance <- diagonal_variance / sum(diagonal_variance) |
| 674 | 61x |
names(x$model$variance) <- colnames(x$rotation$rotation.matrix)[-1] |
| 675 | ||
| 676 | 61x |
return(x) |
| 677 |
} |
|
| 678 |
else {
|
|
| 679 | ! |
if(is.null(rotation)) {
|
| 680 | ! |
stop("When providing a matrix, a rotation matrix must also be provided")
|
| 681 |
} |
|
| 682 | ||
| 683 | ! |
points <- as.matrix(x) %*% as.matrix(rotation); |
| 684 | ! |
return(points); |
| 685 |
} |
|
| 686 |
} |
|
| 687 | ||
| 688 | ||
| 689 |
##' Optimize Node and Centroid Positions in ENA Set |
|
| 690 |
#' |
|
| 691 |
#' This function computes and assigns node positions and centroids for an ENA set object |
|
| 692 |
#' using the current points and rotation information. |
|
| 693 |
#' |
|
| 694 |
#' @param x An \code{ena.set} object for which to optimize node and centroid positions.
|
|
| 695 |
#' @param weights Optional. A numeric matrix of connection weights. If provided, the function will use this matrix instead of the connection counts from the \code{ena.set}.
|
|
| 696 |
#' @param exclude_zero_networks A logical value. If `TRUE` (default), units |
|
| 697 |
#' whose line weights are all zero are left out of the fit, their centroids |
|
| 698 |
#' are zero, and for unordered sets the nodes and centroids are shifted so the |
|
| 699 |
#' mean centroid of the fitted units is at the origin, as |
|
| 700 |
#' `ena.make.set(center.align.to.origin = TRUE)` does. Ordered sets are fit |
|
| 701 |
#' on the other units without that shift, as ONA does. With no empty units |
|
| 702 |
#' the result is unchanged. |
|
| 703 |
#' @param self_weight Weight of a self-connection (a column connecting a code to itself: the ordered diagonal, or an unordered set's A & A columns from accumulate(self_connections = TRUE)) in its code's node weight. 2 (default) counts it in both the node's row and column sums, the in + out rule of Zhiqiang Cai's 2021 fix; 1 counts it once. Recorded as \code{x$rotation$self.weight}.
|
|
| 704 |
#' |
|
| 705 |
#' @return The input \code{ena.set} object with updated node and centroid positions.
|
|
| 706 |
#' @export |
|
| 707 |
#' |
|
| 708 |
#' @examples |
|
| 709 |
#' # Assuming 'set' is an ena.set object: |
|
| 710 |
#' data(RS.data) |
|
| 711 |
#' |
|
| 712 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 713 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 714 |
#' "Collaboration") |
|
| 715 |
#' units <- c("Condition", "UserName")
|
|
| 716 |
#' horizon <- c("Condition", "GroupName")
|
|
| 717 |
#' enaset <- RS.data |> |
|
| 718 |
#' accumulate(units, codes, horizon) |> |
|
| 719 |
#' sphere_norm() |> |
|
| 720 |
#' center() |> |
|
| 721 |
#' rotate() |> |
|
| 722 |
#' project() |> |
|
| 723 |
#' optimize() |
|
| 724 |
optimize <- function(x, weights = NULL, self_weight = 2, exclude_zero_networks = TRUE) {
|
|
| 725 | 61x |
if(!is(x, "ena.set")) {
|
| 726 | ! |
if(is.null(weights)) {
|
| 727 | ! |
stop("When providing a matrix, weights must also be provided")
|
| 728 |
} |
|
| 729 | ||
| 730 | ! |
x_ <- x; |
| 731 | ! |
x <- list( |
| 732 | ! |
points = x_, |
| 733 | ! |
line.weights = weights, |
| 734 | ! |
rotation = list( |
| 735 | ! |
codes = unique(unlist(strsplit(colnames(as.matrix(weights)), " & "))) |
| 736 |
) |
|
| 737 |
) |
|
| 738 |
} |
|
| 739 | ||
| 740 | 61x |
points = as.matrix(x$points); |
| 741 | 61x |
weights = as.matrix(x$line.weights); |
| 742 | 61x |
ordered <- is(x, "ena.ordered.set") |
| 743 | ||
| 744 |
# Units with no connections carry no information about where the nodes go: |
|
| 745 |
# fit on the others and give the empty units a zero centroid (they sit at |
|
| 746 |
# the origin after center(exclude_zero_networks = TRUE)). |
|
| 747 | 61x |
fit_rows <- rep(TRUE, nrow(weights)) |
| 748 | 61x |
if (isTRUE(exclude_zero_networks)) {
|
| 749 | 58x |
fit_rows <- rowSums(abs(weights)) != 0 |
| 750 | ! |
if (!any(fit_rows)) fit_rows[] <- TRUE |
| 751 |
} |
|
| 752 | 61x |
fit_points <- points[fit_rows, , drop = FALSE] |
| 753 | 61x |
fit_weights <- weights[fit_rows, , drop = FALSE] |
| 754 | ||
| 755 | 61x |
if(ordered) {
|
| 756 | 17x |
positions <- directed_node_positions(fit_weights, fit_points, ncol(points), self_weight = self_weight); |
| 757 |
} |
|
| 758 |
else {
|
|
| 759 |
# Unordered: libena's layout-agnostic solver, driven by the set's |
|
| 760 |
# adjacency key -- the same result as lws_lsq_positions() for the usual |
|
| 761 |
# choose(n, 2) columns, and it also places self-connection columns |
|
| 762 |
# (A & A; accumulate(self_connections = TRUE)), each counted |
|
| 763 |
# `self_weight` times as in ordered models. Sets without a key matching |
|
| 764 |
# the columns use the upper-triangle solver. |
|
| 765 | 44x |
key <- x$rotation$adjacency.key |
| 766 | 44x |
if (!is.null(key) && ncol(as.matrix(key)) == ncol(weights)) {
|
| 767 | 44x |
positions <- node_positions_by_key(fit_weights, fit_points, ncol(points), key, |
| 768 | 44x |
key_codes(key, x$rotation$codes), self_weight = self_weight); |
| 769 |
} |
|
| 770 |
else {
|
|
| 771 | ! |
positions <- lws_lsq_positions(fit_weights, fit_points, ncol(points)); |
| 772 |
} |
|
| 773 | 44x |
if (!all(fit_rows)) {
|
| 774 |
# ENAset's center.align.to.origin rule for unordered sets: put the mean |
|
| 775 |
# centroid of the fitted units at the origin, moving the nodes with it. |
|
| 776 | 5x |
mean_centroid <- colMeans(positions$centroids) |
| 777 | 5x |
positions$centroids <- sweep(positions$centroids, 2, mean_centroid, "-") |
| 778 | 5x |
positions$nodes <- sweep(positions$nodes, 2, mean_centroid, "-") |
| 779 |
} |
|
| 780 |
} |
|
| 781 | ||
| 782 | 61x |
if (!all(fit_rows)) {
|
| 783 | 8x |
centroids <- matrix(0, nrow = nrow(weights), ncol = ncol(positions$centroids)) |
| 784 | 8x |
centroids[fit_rows, ] <- positions$centroids |
| 785 | 8x |
positions$centroids <- centroids |
| 786 |
} |
|
| 787 | ||
| 788 |
# Recorded so post-fit centroid recomputation (move_nodes_to_unit_circle) |
|
| 789 |
# uses the weighting the nodes were fit with. |
|
| 790 | 61x |
x$rotation$self.weight <- self_weight; |
| 791 | 61x |
x$rotation$nodes <- as_nodes_matrix(positions$nodes, list("code" = x$rotation$codes), cols = colnames(as.matrix(x$points)), cls = "ena.nodes")
|
| 792 | ||
| 793 | 61x |
x$model$centroids <- as_nodes_matrix(positions$centroids, rows = list("ENA_UNIT" = x$points$ENA_UNIT), cols = colnames(as.matrix(x$points)))
|
| 794 | ||
| 795 | 61x |
return(x) |
| 796 |
} |
| 1 |
#' Plot an ena.set object |
|
| 2 |
#' |
|
| 3 |
#' @param x ena.set to plot |
|
| 4 |
#' @param y ignored. |
|
| 5 |
#' @param ... Additional parameters passed along to ena.plot functions |
|
| 6 |
#' @param empty Logical; if TRUE, creates an empty plot without points. Default is TRUE. |
|
| 7 |
#' @param title Character; title for the plot. Default is "ENA Plot". |
|
| 8 |
#' |
|
| 9 |
#' @examples |
|
| 10 |
#' |
|
| 11 |
#' data(RS.data) |
|
| 12 |
#' |
|
| 13 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 14 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 15 |
#' |
|
| 16 |
#' accum = ena.accumulate.data( |
|
| 17 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 18 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 19 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 20 |
#' codes = RS.data[,codeNames], |
|
| 21 |
#' window.size.back = 4 |
|
| 22 |
#' ) |
|
| 23 |
#' |
|
| 24 |
#' set = ena.make.set( |
|
| 25 |
#' enadata = accum |
|
| 26 |
#' ) |
|
| 27 |
#' |
|
| 28 |
#' plot(set) |> |
|
| 29 |
#' add_points(Condition$FirstGame, colors = "blue", with.mean = TRUE) |> |
|
| 30 |
#' add_points(Condition$SecondGame, colors = "red", with.mean = TRUE) |> |
|
| 31 |
#' with_means() |> |
|
| 32 |
#' add_nodes() |
|
| 33 |
#' |
|
| 34 |
#' myENAplot <- plot(set) |> |
|
| 35 |
#' add_network(Condition$FirstGame - Condition$SecondGame) |
|
| 36 |
#' |
|
| 37 |
#' |
|
| 38 |
#' # Add a group mean to an existing ENA plot |
|
| 39 |
#' add_group(myENAplot, wh = Condition$FirstGame) |
|
| 40 |
#' |
|
| 41 |
#' # Add a trajectory to an existing ENA plot |
|
| 42 |
#' add_trajectory(myENAplot, wh = Condition$FirstGame) |
|
| 43 |
#' |
|
| 44 |
#' @example inst/examples/example-plot-piping.R |
|
| 45 |
#' |
|
| 46 |
#' @return ena.plot.object |
|
| 47 |
#' @export |
|
| 48 |
plot.ena.set <- function(x, y, ..., empty = TRUE, title = "ENA Plot") {
|
|
| 49 | 1x |
args <- list(...); |
| 50 | ||
| 51 | 1x |
if(is(x, "ena.ordered.set")) {
|
| 52 | ! |
stop("Plotting of ena.ordered.set objects requires using the 'ona' package.");
|
| 53 |
} |
|
| 54 | ||
| 55 | 1x |
p = ena.plot(enaset = x, title = title, ...); |
| 56 | 1x |
if (isFALSE(empty)) {
|
| 57 | ! |
add_points(p, ...); |
| 58 |
} |
|
| 59 | ||
| 60 | 1x |
return(p) |
| 61 |
} |
|
| 62 | ||
| 63 | ||
| 64 |
#' Add points to an ENA plot |
|
| 65 |
#' |
|
| 66 |
#' This function adds points to an existing ENA plot or ENA set. It supports various input types for the `wh` parameter, including unevaluated expressions and language objects. |
|
| 67 |
#' |
|
| 68 |
#' @param x An `ENAplot` object or an ENA set containing plots. |
|
| 69 |
#' @param wh Specifies the points to plot. Can be an unevaluated expression or a language object. |
|
| 70 |
#' @param ... Additional parameters passed to the plotting functions. |
|
| 71 |
#' @param colors A vector of colors for the plotted points. Default is `NULL`. |
|
| 72 |
#' |
|
| 73 |
#' @details |
|
| 74 |
#' The function determines the type of the `wh` parameter and processes it accordingly: |
|
| 75 |
#' - If `wh` is an unevaluated expression, it is captured and evaluated in the parent frame. |
|
| 76 |
#' - If `wh` is a language object, it is processed to extract the relevant points information. |
|
| 77 |
#' |
|
| 78 |
#' The function updates the plot with the new points and stores the updated plot back in the ENA set. |
|
| 79 |
#' |
|
| 80 |
#' @example inst/examples/example-plot-piping.R |
|
| 81 |
#' |
|
| 82 |
#' @return Invisibly returns the modified ENA set. |
|
| 83 |
#' |
|
| 84 |
#' @export |
|
| 85 |
add_points <- function( |
|
| 86 |
x, |
|
| 87 |
wh = NULL, ..., |
|
| 88 |
colors = NULL |
|
| 89 |
) {
|
|
| 90 | 1x |
plot <- x; |
| 91 | 1x |
set <- plot$enaset; |
| 92 | ||
| 93 | 1x |
if(is.null(plot)) {
|
| 94 | ! |
stop("No existing plot found in the ENA set. Did you call plot(set) first?")
|
| 95 |
} |
|
| 96 |
# plot <- set$plots[[length(set$plots)]] |
|
| 97 | 1x |
more.args <- list(...) |
| 98 | ||
| 99 | 1x |
wh_subbed <- substitute(wh) |
| 100 | 1x |
if (is.language(wh_subbed)) {
|
| 101 |
# points <- list(do.call(`[`, list(x = set$points, i = wh))); |
|
| 102 | ! |
points <- list(eval(prefix_expr(wh_subbed, quote(set$points)))); |
| 103 | ! |
colors <- if (is.null(colors)) plot$palette[length(plot$plotted$points) + 1] else colors[1]; # one group, one color |
| 104 | ! |
named <- paste(as.character(wh_subbed)[-1], collapse = " "); |
| 105 |
} |
|
| 106 | 1x |
else if (!is.null(wh_subbed) && length(wh_subbed) > 0) {
|
| 107 | ! |
wh_subbed <- as.character(wh_subbed); |
| 108 | ! |
if (length(wh_subbed) > 1 && wh_subbed[[2]] %in% colnames(set$points)) {
|
| 109 | ! |
cc <- call(wh_subbed[[1]], set$points, wh_subbed[[2]]) |
| 110 | ! |
part1 <- eval(cc); |
| 111 | ! |
name <- paste(wh_subbed[-1], collapse = "$"); |
| 112 | ! |
if(grepl(set$model$model.type, pattern="Trajectory")) {
|
| 113 | ! |
points <- set$points[part1 == wh_subbed[[3]], ] |
| 114 | ! |
more.args$points = points[, .SD[nrow(.SD)], by = ENA_UNIT] |
| 115 |
} |
|
| 116 |
else {
|
|
| 117 | ! |
more.args$points = points <- set$points[part1 == wh_subbed[[3]], ] |
| 118 |
} |
|
| 119 | ||
| 120 | ! |
if(is.null(colors)) {
|
| 121 | ! |
colors = plot$palette[length(plot$plotted$points) + 1] |
| 122 |
} |
|
| 123 |
} |
|
| 124 | ! |
else if (length(wh_subbed) == 1 && wh_subbed[[1]] %in% colnames(set$points)) {
|
| 125 | ! |
more.args$points = points = set$points |
| 126 | ! |
if(is.null(colors)) {
|
| 127 | ! |
colors <- plot$palette[as.numeric(as.factor(set$points[[wh_subbed]])) + length(plot$plotted$points)] |
| 128 |
} |
|
| 129 |
else {
|
|
| 130 | ! |
colors <- colors[as.numeric(as.factor(set$points[[wh_subbed]]))] |
| 131 |
} |
|
| 132 |
} |
|
| 133 |
else {
|
|
| 134 | ! |
points <- wh |
| 135 | ! |
colors <- if (is.null(colors)) plot$palette[length(plot$plotted$points) + 1] else colors[1]; # one group, one color |
| 136 |
} |
|
| 137 |
} |
|
| 138 |
else {
|
|
| 139 |
# first_meta <- setdiff(colnames(set$connection.counts)[find_meta_cols(set$connection.counts)], c("QEUNIT", "ENA_UNIT"))[1]
|
|
| 140 |
# meta_grps <- split(set$points, by = first_meta) |
|
| 141 |
|
|
| 142 |
# points = meta_grps |
|
| 143 |
# named <- paste0(names(points), ".Points") |
|
| 144 |
# # colors = ifelse(is.null(colors), plot$palette[length(plot$plotted$points) + 1], colors) |
|
| 145 |
# colors <- plot$palette[seq.int(from=length(plot$plotted$points)+1,length.out=length(meta_grps))]; |
|
| 146 | 1x |
points <- list(set$points); |
| 147 | 1x |
named <- "all.points"; |
| 148 | 1x |
colors <- if (is.null(colors)) plot$palette[length(plot$plotted$points) + 1] else colors[1]; # one group, one color |
| 149 |
} |
|
| 150 | ||
| 151 | 1x |
mean <- ifelse(!is.null(more.args$mean), more.args$mean, FALSE); |
| 152 | 1x |
more.args$enaplot = plot |
| 153 | 1x |
for(i in seq_along(colors)) {
|
| 154 | 1x |
color <- colors[i]; |
| 155 | 1x |
name <- named[i]; |
| 156 | 1x |
pts <- points[[i]]; |
| 157 | 1x |
more.args$colors <- color; |
| 158 | 1x |
more.args$legend.name <- name; |
| 159 | 1x |
more.args$points <- pts; |
| 160 | 1x |
plot <- do.call(ena.plot.points, more.args); |
| 161 | ||
| 162 | 1x |
plot$plotted$points[[length(plot$plotted$points) + 1]] <- list( |
| 163 | 1x |
data = points, |
| 164 | 1x |
color = color |
| 165 |
) |
|
| 166 | 1x |
names(plot$plotted$points)[length(plot$plotted$points)] <- name; |
| 167 | ||
| 168 | 1x |
if(isTRUE(mean) && nrow(pts) > 1) {
|
| 169 | ! |
more.args$labels <- name; |
| 170 | ! |
plot <- do.call(ena.plot.group, more.args); |
| 171 |
} |
|
| 172 |
} |
|
| 173 |
# if(!is.null(colors)) {
|
|
| 174 |
# more.args$colors = colors |
|
| 175 |
# } |
|
| 176 |
# else {
|
|
| 177 |
# more.args$colors = plot$palette[length(plot$plotted$points) + 1] |
|
| 178 |
# } |
|
| 179 |
|
|
| 180 | ||
| 181 |
# if(!is.null(mean) && (is.list(mean) || mean == T)) {
|
|
| 182 |
# # if (is.list(mean)) {
|
|
| 183 |
# # more.args <- c(mean, more.args[!names(more.args) %in% names(mean)]) |
|
| 184 |
# # } |
|
| 185 |
# # more.args$enaplot <- plot |
|
| 186 |
# # more.args$points <- points |
|
| 187 |
# # more.args$labels <- name |
|
| 188 |
# # |
|
| 189 |
# # plot <- do.call(ena.plot.group, more.args). |
|
| 190 |
# set <- add_group(set, substitute(wh), ...); |
|
| 191 |
# } |
|
| 192 | ||
| 193 |
# set$plots[[length(set$plots)]] <- plot |
|
| 194 | ||
| 195 | 1x |
return(plot); |
| 196 |
} |
|
| 197 | ||
| 198 |
#' Add all groups to an ENA plot |
|
| 199 |
#' |
|
| 200 |
#' This function iterates over all unique values of the first metadata column (excluding 'QEUNIT' and 'ENA_UNIT') |
|
| 201 |
#' in the ENA set and adds each group as a set of points to the ENA plot. This is useful for quickly visualizing |
|
| 202 |
#' all groups in a categorical variable on the same plot. |
|
| 203 |
#' |
|
| 204 |
#' @param x An `ENAplot` object (as returned by `plot.ena.set`). |
|
| 205 |
#' @param wh (Ignored) Included for compatibility with other plotting functions. |
|
| 206 |
#' |
|
| 207 |
#' @details |
|
| 208 |
#' The function finds the first metadata column in the ENA set (excluding 'QEUNIT' and 'ENA_UNIT'), |
|
| 209 |
#' and for each unique value in that column, calls `add_points()` to add the group's points to the plot. |
|
| 210 |
#' |
|
| 211 |
#' @return The modified `ENAplot` object with all groups added as points. |
|
| 212 |
#' |
|
| 213 |
#' @example inst/examples/example-plot-piping.R |
|
| 214 |
#' |
|
| 215 |
#' @export |
|
| 216 |
group <- function(x, wh = NULL) {
|
|
| 217 | ! |
plot <- x; |
| 218 | ! |
set <- plot$enaset; |
| 219 | ||
| 220 | ! |
first_meta <- setdiff(colnames(set$connection.counts)[find_meta_cols(set$connection.counts)], c("QEUNIT", "ENA_UNIT"))[1]
|
| 221 |
|
|
| 222 | ! |
plot$plotted$points <- list(); |
| 223 |
# meta_grps <- split(set$points, by = first_meta); |
|
| 224 | ! |
meta_grps <- unique(set$points[[first_meta]]); |
| 225 | ! |
for(grp in meta_grps) {
|
| 226 | ! |
add_points(plot, wh = call("==", as.name(first_meta), grp));
|
| 227 |
} |
|
| 228 | ||
| 229 |
# points = meta_grps |
|
| 230 | ! |
return(plot); |
| 231 |
} |
|
| 232 | ||
| 233 |
#' Add a trajectory to an ENA plot |
|
| 234 |
#' |
|
| 235 |
#' This function adds a trajectory to an existing ENA plot or ENA set. It supports various input types for the `wh` parameter, including unevaluated expressions and language objects. |
|
| 236 |
#' |
|
| 237 |
#' @param x An `ENAplot` object or an ENA set containing plots. |
|
| 238 |
#' @param wh Specifies the trajectory to plot. Can be an unevaluated expression or a language object. |
|
| 239 |
#' @param ... Additional parameters passed to the plotting functions. |
|
| 240 |
#' @param name A character string specifying the name of the plot. Default is "plot". |
|
| 241 |
#' |
|
| 242 |
#' @details |
|
| 243 |
#' The function determines the type of the `wh` parameter and processes it accordingly: |
|
| 244 |
#' - If `wh` is an unevaluated expression, it is captured and evaluated in the parent frame. |
|
| 245 |
#' - If `wh` is a language object, it is processed to extract the relevant trajectory information. |
|
| 246 |
#' |
|
| 247 |
#' The function updates the plot with the new trajectory and stores the updated plot back in the ENA set. |
|
| 248 |
#' |
|
| 249 |
#' @return Invisibly returns the modified ENA set. |
|
| 250 |
#' |
|
| 251 |
#' @example inst/examples/example-plot-piping.R |
|
| 252 |
#' |
|
| 253 |
#' @export |
|
| 254 |
add_trajectory <- function(x, wh = NULL, ..., name = "plot") {
|
|
| 255 | 1x |
plot <- x; |
| 256 | 1x |
if (.qe_backend(plot)) {
|
| 257 | 1x |
.qe_no_trajectories("add_trajectory")
|
| 258 | 1x |
return(plot) |
| 259 |
} |
|
| 260 | ! |
set <- plot$enaset; |
| 261 | ||
| 262 | ! |
subbed <- substitute(wh) |
| 263 | ! |
args_list <- as.character(subbed) |
| 264 | ! |
points <- set$points |
| 265 | ||
| 266 | ! |
if (!is.null(args_list) && !is.null(subbed)) {
|
| 267 | ! |
if (length(args_list) > 1) {
|
| 268 | ! |
wh_subbed <- as.character(substitute(wh)) |
| 269 | ! |
cc <- call(wh_subbed[[1]], set$points, wh_subbed[[2]]) |
| 270 | ! |
part1 <- eval(cc) |
| 271 | ! |
points <- set$points[part1 == wh_subbed[[3]], ] |
| 272 | ! |
by <- "ENA_UNIT" |
| 273 |
} |
|
| 274 |
else {
|
|
| 275 | ! |
by <- args_list[[1]] |
| 276 |
} |
|
| 277 |
} |
|
| 278 |
else {
|
|
| 279 | ! |
by <- "ENA_UNIT" |
| 280 |
} |
|
| 281 | ! |
plot <- ena.plot.trajectory(plot, points = points, by = by) |
| 282 | ||
| 283 |
# set$model$plot <- plot |
|
| 284 |
# set$plots[[length(x$plots)]] <- plot |
|
| 285 |
|
|
| 286 |
# .return(set, from_plot = T, invisible = F) |
|
| 287 | ! |
return(plot) |
| 288 |
} |
|
| 289 | ||
| 290 | ||
| 291 |
#' Add a group mean to an ENA plot |
|
| 292 |
#' |
|
| 293 |
#' This function adds a group mean to an existing ENA plot or ENA set. It supports various input types for the `wh` parameter, including unevaluated expressions and language objects. |
|
| 294 |
#' |
|
| 295 |
#' @param x An `ENAplot` object or an ENA set containing plots. |
|
| 296 |
#' @param wh Specifies the group to plot. Can be an unevaluated expression or a language object. |
|
| 297 |
#' @param ... Additional parameters passed to the plotting functions. |
|
| 298 |
#' |
|
| 299 |
#' @details |
|
| 300 |
#' The function determines the type of the `wh` parameter and processes it accordingly: |
|
| 301 |
#' - If `wh` is an unevaluated expression, it is captured and evaluated in the parent frame. |
|
| 302 |
#' - If `wh` is a language object, it is processed to extract the relevant group information. |
|
| 303 |
#' |
|
| 304 |
#' The function updates the plot with the new group mean and stores the updated plot back in the ENA set. |
|
| 305 |
#' |
|
| 306 |
#' @example inst/examples/example-plot-piping.R |
|
| 307 |
#' |
|
| 308 |
#' @return Invisibly returns the modified ENA set. |
|
| 309 |
#' |
|
| 310 |
#' @export |
|
| 311 |
add_group <- function(x, wh = NULL, ...) {
|
|
| 312 | ! |
plot <- x; |
| 313 | ! |
set <- plot$enaset; |
| 314 | ||
| 315 |
# Capture the expression passed to wh |
|
| 316 | ! |
wh.expr <- substitute(wh) |
| 317 | ||
| 318 |
# Check if the expression is a call to `substitute()`. This happens when |
|
| 319 |
# add_group is called from another function like add_points, which has |
|
| 320 |
# already substituted the user's original input. |
|
| 321 | ! |
if (is.call(wh.expr) && deparse(wh.expr[[1]]) == "substitute") {
|
| 322 |
# If so, evaluate it in the parent frame to get the actual language object |
|
| 323 | ! |
wh.clean <- eval(wh.expr, parent.frame()) |
| 324 |
} else {
|
|
| 325 |
# Otherwise, the captured expression is what we want |
|
| 326 | ! |
wh.clean <- wh.expr |
| 327 |
} |
|
| 328 | ||
| 329 |
# set <- x |
|
| 330 |
# # plot <- set$model$plot |
|
| 331 |
# # plot <- set$plots[[length(set$plots)]] |
|
| 332 | ||
| 333 |
if ( |
|
| 334 | ! |
identical(as.character(wh.clean), "wh.clean") || |
| 335 | ! |
identical(as.character(wh.clean), "y") |
| 336 |
) {
|
|
| 337 | ! |
wh.clean <- wh; |
| 338 |
} |
|
| 339 | ||
| 340 | ! |
more_args = list(...) |
| 341 | ! |
more_args$enaplot <- plot |
| 342 | ! |
if(is.null(more_args$color)) {
|
| 343 | ! |
more_args$colors <- plot$palette[length(plot$plotted$means) + 1] |
| 344 |
} |
|
| 345 |
else {
|
|
| 346 | ! |
more_args$colors <- more_args$color; |
| 347 |
} |
|
| 348 | ||
| 349 | ! |
group.rows.log <- NULL; |
| 350 | ! |
if (is.null(wh.clean)) {
|
| 351 | ! |
plot <- do.call(ena.plot.group, more_args) |
| 352 | ! |
group.rows.log <- rep(TRUE, nrow(set$points)); |
| 353 |
} |
|
| 354 |
else {
|
|
| 355 | ! |
parts <- as.character(wh.clean) |
| 356 | ||
| 357 | ! |
if (parts[2] %in% colnames(set$line.weights)) {
|
| 358 | ! |
label <- parts[3] |
| 359 | ! |
group.rows.log <- set$points[[parts[2]]] == parts[3]; |
| 360 | ! |
group.rows <- set$points[group.rows.log, ] |
| 361 | ! |
if(nrow(group.rows) > 0) {
|
| 362 | ! |
group.means <- colMeans(group.rows) |
| 363 | ||
| 364 | ! |
more_args$points <- group.means |
| 365 | ! |
more_args$labels <- label |
| 366 | ! |
plot <- do.call(ena.plot.group, more_args) |
| 367 |
} |
|
| 368 |
else {
|
|
| 369 | ! |
warning("No points in the group")
|
| 370 |
} |
|
| 371 |
} |
|
| 372 |
else {
|
|
| 373 | ! |
warning("Unable to plot group")
|
| 374 |
} |
|
| 375 |
} |
|
| 376 | ||
| 377 | ! |
plot$plotted$means[[length(plot$plotted$means) + 1]] = list( |
| 378 | ! |
rows = group.rows.log, |
| 379 | ! |
data = more_args$points, |
| 380 | ! |
color = more_args$colors |
| 381 |
) |
|
| 382 | ||
| 383 |
# set$plots[[length(set$plots)]] <- plot |
|
| 384 |
|
|
| 385 |
# .return(plot, from_plot = T, invisible = F) |
|
| 386 | ! |
return(plot) |
| 387 |
} |
|
| 388 | ||
| 389 | ||
| 390 |
##' Add a network to an ENA plot |
|
| 391 |
#' |
|
| 392 |
#' Adds a network (set of edges) to an existing ENA plot or ENA set. The network can be specified in several ways, including as an unevaluated expression, a numeric matrix, or a language object. This function is typically used to visualize group means, differences between groups, or custom networks on an ENA plot. |
|
| 393 |
#' |
|
| 394 |
#' @param x An `ENAplot` object or an ENA set containing plots. |
|
| 395 |
#' @param wh Specifies the network to plot. Can be: |
|
| 396 |
#' \itemize{
|
|
| 397 |
#' \item An unevaluated expression (e.g., `Condition$FirstGame - Condition$SecondGame`) |
|
| 398 |
#' \item A numeric matrix or data.frame of edge weights |
|
| 399 |
#' \item A language object |
|
| 400 |
#' \item NULL (defaults to the mean network) |
|
| 401 |
#' } |
|
| 402 |
#' @param ... Additional parameters passed to the plotting functions. |
|
| 403 |
#' @param with.mean Logical; if `TRUE`, also plots the mean for the points in the network. Default is `FALSE`. |
|
| 404 |
#' @param edge.multiplier Numeric scalar to multiply the edge weights. Useful for scaling the network visualization. Default is 1. |
|
| 405 |
#' @param colors Optional vector of colors for the network. If not specified, colors are chosen from the plot palette. |
|
| 406 |
#' |
|
| 407 |
#' @details |
|
| 408 |
#' The function determines the type of the `wh` parameter and processes it accordingly: |
|
| 409 |
#' \itemize{
|
|
| 410 |
#' \item If `wh` is an unevaluated expression, it is captured and evaluated in the parent frame. This allows for flexible specification of group means or differences. |
|
| 411 |
#' \item If `wh` is a numeric matrix or data.frame, its column means are plotted; a numeric vector (e.g. a subtracted network) is plotted as is. |
|
| 412 |
#' \item If `wh` is a language object, it is processed to extract the relevant network information. |
|
| 413 |
#' \item If `wh` is NULL, the mean network is plotted. |
|
| 414 |
#' } |
|
| 415 |
#' |
|
| 416 |
#' The function updates the plot with the new network and returns the modified plot object. The ENA set is not modified in-place. |
|
| 417 |
#' |
|
| 418 |
#' @section Examples: |
|
| 419 |
#' See `inst/examples/example-plot-piping.R` for usage examples. |
|
| 420 |
#' |
|
| 421 |
#' @return The modified ENAplot object with the new network added. |
|
| 422 |
#' |
|
| 423 |
#' @export |
|
| 424 |
add_network <- function( |
|
| 425 |
x, wh = NULL, |
|
| 426 |
..., |
|
| 427 |
with.mean = F, |
|
| 428 |
edge.multiplier = 1, |
|
| 429 |
colors = NULL |
|
| 430 |
) {
|
|
| 431 | 1x |
plot <- x; |
| 432 | 1x |
set <- plot$enaset; |
| 433 | ||
| 434 | 1x |
more_args <- list(...); |
| 435 |
# qeviz: weights unscaled, edge.multiplier -> magnify on this layer. |
|
| 436 | 1x |
backend <- if (.qe_backend(plot)) "qeviz" else "plotly" |
| 437 | 1x |
em <- .qe_weight_mult(backend, edge.multiplier) |
| 438 | 1x |
more_args$magnify <- .qe_magnify(backend, edge.multiplier) |
| 439 | ||
| 440 | 1x |
wh_subbed <- substitute(wh) |
| 441 | 1x |
network <- colMeans(set$line.weights); |
| 442 |
|
|
| 443 | 1x |
if (is.language(wh_subbed)) {
|
| 444 | 1x |
network <- try(eval(wh_subbed, parent.frame()), silent = TRUE) |
| 445 | 1x |
if(inherits(network, "try-error")) {
|
| 446 | ! |
if(wh_subbed[[1]] == "-") {
|
| 447 | ! |
means <- sapply(c(wh_subbed[[2]], wh_subbed[[3]]), function(y) {
|
| 448 | ! |
colMeans(eval(prefix_expr(y, quote(set$line.weights)))); |
| 449 |
}) |
|
| 450 | ||
| 451 | ! |
network <- means[,1] - means[,2]; |
| 452 | ! |
named <- as.character(enquote(wh_subbed))[2]; |
| 453 | ! |
colors <- if(is.null(colors)) {
|
| 454 | ! |
plot$palette[seq.int(length(plot$plotted$points) + 1, 2)] |
| 455 |
} else {
|
|
| 456 | ! |
if(length(colors) < 2) {
|
| 457 | ! |
stop("Please provide two colors for the two groups being compared.")
|
| 458 |
} else {
|
|
| 459 | ! |
colors |
| 460 |
} |
|
| 461 |
} |
|
| 462 |
} |
|
| 463 |
else {
|
|
| 464 | ! |
network <- colMeans(eval(prefix_expr(wh_subbed, quote(set$line.weights)))); |
| 465 | ! |
if (is.null(colors)) colors <- plot$palette[length(plot$plotted$points) + 1]; |
| 466 | ! |
named <- paste(as.character(wh_subbed)[-1], collapse = " "); |
| 467 |
} |
|
| 468 |
} |
|
| 469 | 1x |
else if (is.matrix(network) || is.data.frame(network) || is.numeric(network)) {
|
| 470 |
# a matrix of networks is averaged; a single network is used as is |
|
| 471 | 1x |
if (!is.null(dim(network))) network <- colMeans(network); |
| 472 | 1x |
if (is.null(colors)) colors <- plot$palette[length(plot$plotted$points) + 1]; |
| 473 | 1x |
named <- paste(as.character(wh_subbed)[-1], collapse = " "); |
| 474 |
} |
|
| 475 |
} |
|
| 476 | ||
| 477 | 1x |
more_args$enaplot = plot; |
| 478 | 1x |
more_args$colors = colors; |
| 479 | 1x |
if(is.data.frame(network) || is.matrix(network) || is.numeric(network)) {
|
| 480 | 1x |
more_args$network = network * em; |
| 481 | 1x |
plot <- do.call(ena.plot.network, more_args); |
| 482 |
} |
|
| 483 | ||
| 484 |
|
|
| 485 |
# .return(set, from_plot = T, invisible = F) |
|
| 486 | 1x |
return(plot); |
| 487 |
} |
|
| 488 | ||
| 489 | ||
| 490 |
#' Add nodes to an ENA plot |
|
| 491 |
#' |
|
| 492 |
#' This function adds nodes to an existing ENA plot or ENA set. It can be used to customize the nodes displayed on the plot, including their size and other graphical parameters. |
|
| 493 |
#' |
|
| 494 |
#' @param x An \code{ENAplot} object or an ENA set containing plots.
|
|
| 495 |
#' @param ... Additional arguments passed to \code{ena.plot.points}, such as \code{nodes}, \code{size}, and other graphical parameters.
|
|
| 496 |
#' @param return_plot Logical; if \code{TRUE}, returns the modified ENA set. If \code{FALSE} (default), returns the modified plot invisibly.
|
|
| 497 |
#' |
|
| 498 |
#' @details |
|
| 499 |
#' If \code{x} is an \code{ENAplot}, the function extracts the associated ENA set and plot. Otherwise, it assumes \code{x} is an ENA set and uses the last plot in the set.
|
|
| 500 |
#' The nodes to be added can be specified via the \code{nodes} argument; otherwise, the default nodes from the set's rotation are used.
|
|
| 501 |
#' Node size can be customized via the \code{size} argument.
|
|
| 502 |
#' |
|
| 503 |
#' The function updates the plot with the new nodes and stores the updated plot back in the ENA set. |
|
| 504 |
#' |
|
| 505 |
#' @return Invisibly returns the modified plot or ENA set, depending on the value of \code{return_plot}.
|
|
| 506 |
#' |
|
| 507 |
#' @example inst/examples/example-plot-piping.R |
|
| 508 |
#' |
|
| 509 |
#' @export |
|
| 510 |
add_nodes <- function(x, ..., return_plot = FALSE) {
|
|
| 511 | ! |
plot <- x; |
| 512 | ! |
set <- plot$enaset; |
| 513 | ||
| 514 | ! |
dot_args <- list(...); |
| 515 | ! |
if(!is.null(dot_args$nodes)) {
|
| 516 | ! |
nodes <- dot_args$nodes; |
| 517 |
} |
|
| 518 |
else {
|
|
| 519 | ! |
nodes <- set$rotation$nodes; |
| 520 |
} |
|
| 521 | ||
| 522 | ! |
node_sizes <- 1; |
| 523 | ! |
if(!is.null(dot_args$size)) {
|
| 524 | ! |
node_sizes <- dot_args$size; |
| 525 |
} |
|
| 526 | ||
| 527 | ! |
plot <- ena.plot.points(plot, |
| 528 | ! |
points = as.matrix(nodes), |
| 529 | ! |
texts = as.character(nodes$code), |
| 530 | ! |
point.size = node_sizes, |
| 531 |
... |
|
| 532 |
); |
|
| 533 | ||
| 534 | ! |
plot$plotted$networks[[length(plot$plotted$networks) + 1]] <- list( |
| 535 | ! |
nodes = nodes, |
| 536 | ! |
data = NULL, |
| 537 | ! |
color = NULL |
| 538 |
); |
|
| 539 | ||
| 540 |
# set$plots[[length(set$plots)]] <- plot |
|
| 541 | ||
| 542 | ! |
return(plot); |
| 543 |
} |
|
| 544 | ||
| 545 |
#' Adds group means to the ENA plot. |
|
| 546 |
#' |
|
| 547 |
#' This function iterates over the plotted points in the ENA plot and calculates |
|
| 548 |
#' the mean for each group of points. The calculated means are then added to the |
|
| 549 |
#' plot as group means. |
|
| 550 |
#' |
|
| 551 |
#' @param x An ENA set object containing the plots. |
|
| 552 |
#' |
|
| 553 |
#' @return Invisibly returns the modified ENA set object with updated plots. |
|
| 554 |
#' |
|
| 555 |
#' @export |
|
| 556 |
with_means <- function(x) {
|
|
| 557 | ! |
plot <- x; |
| 558 | ! |
set <- plot$enaset; |
| 559 | ||
| 560 | ! |
for(point_group in plot$plotted$points) {
|
| 561 | ! |
plot <- ena.plot.group(plot, point_group$data[[1]], colors = point_group$color[1]) |
| 562 | ||
| 563 | ! |
plot$plotted$means[[length(plot$plotted$means) + 1]] <- list( |
| 564 | ! |
data = colMeans(point_group$data[[1]]), |
| 565 | ! |
color = point_group$color[1] |
| 566 |
) |
|
| 567 |
} |
|
| 568 | ||
| 569 | ! |
return(plot) |
| 570 |
} |
|
| 571 | ||
| 572 | ||
| 573 |
#' Adds trajectories to an ENA plot. |
|
| 574 |
#' |
|
| 575 |
#' This function generates trajectories for the plotted points in the ENA plot based on the specified grouping variables. |
|
| 576 |
#' It supports options for jittering, animation, and scaling. |
|
| 577 |
#' |
|
| 578 |
#' @param x An ENA set object containing the plots. |
|
| 579 |
#' @param ... Additional arguments passed to the plotting functions. |
|
| 580 |
#' @param by A character vector specifying the grouping variables for the trajectories. Default is the first conversation parameter in the ENA set. |
|
| 581 |
#' @param add_jitter Logical; if `TRUE`, adds jitter to the trajectory points. Default is `TRUE`. |
|
| 582 |
#' @param frame Numeric; the duration of each frame in the animation. Default is 1100. |
|
| 583 |
#' @param transition Numeric; the duration of the transition between frames. Default is 1000. |
|
| 584 |
#' @param easing A character string specifying the easing function for the animation. Default is "circle-in-out". |
|
| 585 |
#' |
|
| 586 |
#' @return Invisibly returns the modified ENA set object with updated plots. |
|
| 587 |
#' |
|
| 588 |
#' @export |
|
| 589 |
with_trajectory <- function( |
|
| 590 |
x, ..., |
|
| 591 |
by = x$`_function.params`$conversation[1], |
|
| 592 |
add_jitter = TRUE, |
|
| 593 |
frame = 1100, |
|
| 594 |
transition = 1000, |
|
| 595 |
easing = "circle-in-out" |
|
| 596 |
) {
|
|
| 597 | 1x |
set <- x |
| 598 | 1x |
if (length(set$plots) && .qe_backend(set$plots[[length(set$plots)]])) {
|
| 599 | 1x |
.qe_no_trajectories("with_trajectory")
|
| 600 | 1x |
return(invisible(set)) |
| 601 |
} |
|
| 602 | ! |
if(!grepl(x = set$model$model.type, pattern = "Trajectory")) {
|
| 603 | ! |
stop(paste0("Unable to plot trajectories on model of type: ", set$model$model.type))
|
| 604 |
} |
|
| 605 | ! |
plot <- set$plots[[length(set$plots)]] |
| 606 | ||
| 607 | ! |
args = list(...) |
| 608 | ||
| 609 | ! |
all_steps_w_zero <- data.table(rbind( |
| 610 | ! |
rep(0, length(by)), |
| 611 | ! |
expand.grid( |
| 612 | ! |
sapply(by, function(b) sort(unique(set$points[[b]]))), |
| 613 | ! |
stringsAsFactors = F |
| 614 |
) |
|
| 615 |
)) |
|
| 616 | ! |
colnames(all_steps_w_zero) <- by |
| 617 | ! |
point_group_names <- seq(plot$plotted$points) |
| 618 | ! |
points_cleaned <- lapply(point_group_names, function(n) {
|
| 619 | ! |
prepare_trajectory_data( |
| 620 | ! |
points = plot$plotted$points[[n]]$data, |
| 621 | ! |
by = by, |
| 622 | ! |
units = plot$plotted$points[[n]]$data, |
| 623 | ! |
units_by = set$`_function.params`$units, |
| 624 | ! |
steps = all_steps_w_zero |
| 625 |
) |
|
| 626 |
}) |
|
| 627 | ! |
names(points_cleaned) <- sapply(plot$plotted$points, "[[", "color") |
| 628 | ! |
points_cleaned <- rbindlist(points_cleaned, idcol = "color") |
| 629 | ||
| 630 | ! |
meta_data = unique(set$meta.data) |
| 631 | ! |
setkey(points_cleaned, ENA_UNIT) |
| 632 | ! |
setkey(meta_data, ENA_UNIT) |
| 633 | ! |
points_cleaned = meta_data[points_cleaned] |
| 634 | ! |
setkeyv(points_cleaned, by) |
| 635 | ||
| 636 | ! |
size = ifelse(is.null(args$size), 10, args$size) |
| 637 | ! |
opacity = ifelse(is.null(args$opacity), 1, args$opacity) |
| 638 | ||
| 639 | ! |
dims = as.matrix(points_cleaned[, find_dimension_cols(points_cleaned), with = F])[, 1:2] |
| 640 | ! |
if(add_jitter) {
|
| 641 | ! |
dims[, 1] = jitter(dims[, 1]) |
| 642 | ! |
dims[, 2] = jitter(dims[, 2]) |
| 643 |
} |
|
| 644 | ||
| 645 | ! |
if(is.null(args$scale)) {
|
| 646 | ! |
max_abs = max(abs(dims)) |
| 647 | ! |
scale = c(-1*max_abs, max_abs) |
| 648 |
} |
|
| 649 |
else {
|
|
| 650 | ! |
scale = args$scale |
| 651 |
} |
|
| 652 | ||
| 653 | ! |
ax <- list( |
| 654 | ! |
range = scale, title = "", |
| 655 | ! |
zeroline = TRUE, showline = FALSE, |
| 656 | ! |
showticklabels = FALSE, showgrid = FALSE |
| 657 |
) |
|
| 658 | ||
| 659 |
##### |
|
| 660 |
### Add to the plot |
|
| 661 |
##### |
|
| 662 | ! |
thisPlot <- plotly::plot_ly( |
| 663 | ! |
data = points_cleaned, |
| 664 | ! |
x = dims[,1], y = dims[,2], |
| 665 | ! |
text = ~ENA_UNIT, |
| 666 | ! |
frame = as.formula(paste0("~", by)),
|
| 667 | ! |
type = 'scatter', |
| 668 | ! |
mode = 'markers', |
| 669 | ! |
marker = list( |
| 670 | ! |
size = size, |
| 671 | ! |
opacity = opacity, |
| 672 | ! |
hoverinfo = "text", |
| 673 | ! |
color = as.numeric(as.factor(points_cleaned[["color"]])) |
| 674 |
) |
|
| 675 |
) |> |
|
| 676 | ! |
plotly::layout( |
| 677 | ! |
xaxis = ax, |
| 678 | ! |
yaxis = ax, |
| 679 | ! |
showlegend = T |
| 680 |
) |> |
|
| 681 | ! |
plotly::animation_opts( |
| 682 | ! |
frame = frame, |
| 683 | ! |
transition = transition, |
| 684 | ! |
easing = easing, |
| 685 | ! |
redraw = T |
| 686 |
) |
|
| 687 |
##### |
|
| 688 | ||
| 689 |
# set$model$plot <- plot |
|
| 690 | ! |
set$plots[[length(set$plots) + 1]] <- thisPlot |
| 691 | ! |
invisible(set) |
| 692 |
} |
|
| 693 | ||
| 694 |
#' Prepares trajectory data for an ENA plot. |
|
| 695 |
#' |
|
| 696 |
#' This function processes and prepares trajectory data for plotting in an ENA set. It handles rotation, grouping, and filling missing steps in the trajectory. |
|
| 697 |
#' |
|
| 698 |
#' @param x An ENA set object. If `NULL`, other parameters must be provided. |
|
| 699 |
#' @param by A character vector specifying the grouping variables for the trajectory. Default is the first conversation parameter in the ENA set. |
|
| 700 |
#' @param rotation_matrix A matrix used to rotate the points. Default is the rotation matrix from the ENA set. |
|
| 701 |
#' @param points A data table of points to be processed. Default is the points from the ENA set. |
|
| 702 |
#' @param units A data table of units corresponding to the points. Default is the trajectories or points from the ENA set. |
|
| 703 |
#' @param units_by A character vector specifying the unit grouping variables. Default is the unit parameters from the ENA set. |
|
| 704 |
#' @param steps A data table specifying the steps for the trajectory. If `NULL`, steps are generated automatically. |
|
| 705 |
#' |
|
| 706 |
#' @return A data table containing the processed trajectory data, including dimensions and metadata. |
|
| 707 |
prepare_trajectory_data <- function( |
|
| 708 |
x = NULL, |
|
| 709 |
by = x$`_function.params`$conversation[1], |
|
| 710 |
rotation_matrix = x$rotation.matrix, |
|
| 711 |
points = NULL, |
|
| 712 |
units = points, |
|
| 713 |
units_by = x$`_function.params`$units, |
|
| 714 |
steps = NULL |
|
| 715 |
) {
|
|
| 716 | ! |
if(is(x, "ena.set")) {
|
| 717 | ! |
if(is.null(points)) |
| 718 | ! |
points <- x$points |
| 719 | ! |
if(is.null(units)) |
| 720 | ! |
units <- x$trajectories #points[, find_meta_cols(points), with = FALSE] |
| 721 |
} |
|
| 722 | ||
| 723 | ! |
unique_unit_values <- unique(units[, c(units_by, "ENA_UNIT"), with = FALSE]) |
| 724 | ||
| 725 | ! |
if(!is.null(rotation_matrix)) {
|
| 726 | ! |
rotation_matrix = as.matrix(rotation_matrix) |
| 727 | ! |
full_data <- cbind(units, as.matrix(points) %*% rotation_matrix) |
| 728 |
} else {
|
|
| 729 | ! |
full_data <- cbind(units, as.matrix(points)) |
| 730 |
} |
|
| 731 | ! |
full_data <- full_data[, unique(names(full_data)), with = FALSE] |
| 732 | ||
| 733 | ! |
if(is.null(steps)) {
|
| 734 | ! |
all_steps_w_zero <- data.table(rbind( |
| 735 | ! |
rep(0, length(by)), |
| 736 | ! |
expand.grid( |
| 737 | ! |
sapply(by, function(b) sort(unique(units[[b]]))), |
| 738 | ! |
stringsAsFactors = F |
| 739 |
) |
|
| 740 |
)) |
|
| 741 | ! |
colnames(all_steps_w_zero) <- by |
| 742 |
} else {
|
|
| 743 | ! |
all_steps_w_zero <- steps |
| 744 |
} |
|
| 745 | ! |
all_step_data <- CJ(all_steps_w_zero[[by]], unique_unit_values$ENA_UNIT) |
| 746 | ! |
colnames(all_step_data) <- c(by, "ENA_UNIT") |
| 747 | ||
| 748 | ! |
dimension_col_names = colnames(points)[ |
| 749 | ! |
which(sapply(points, function(col) {
|
| 750 | ! |
is(col, "ena.dimension") |
| 751 |
})) |
|
| 752 |
] |
|
| 753 | ! |
all_step_data[, c(dimension_col_names) := 0] |
| 754 | ! |
all_step_data[[by]] = as.ena.metadata(all_step_data[[by]]) |
| 755 | ! |
all_step_data = merge(unique_unit_values, all_step_data, by = "ENA_UNIT") |
| 756 | ! |
setkey(all_step_data, "ENA_UNIT") |
| 757 | ||
| 758 | ! |
filled_data = all_step_data[ , {
|
| 759 | ! |
by_names = names(.BY) |
| 760 | ! |
user_rows = sapply(1:length(by_names), function(n) {
|
| 761 | ! |
full_data[[by_names[n]]] == .BY[n] |
| 762 |
}) |
|
| 763 | ! |
existing_row = which(rowSums(user_rows * 1) == 2) |
| 764 | ! |
if(length(existing_row) > 0) {
|
| 765 | ! |
full_data[existing_row, c(dimension_col_names), with = FALSE] |
| 766 |
} else {
|
|
| 767 | ! |
prev_row = tail(full_data[ENA_UNIT == .BY$ENA_UNIT & full_data[[by]] < .BY[[by]],], 1) |
| 768 | ! |
if(nrow(prev_row) == 0) {
|
| 769 | ! |
data.table(matrix(rep(0, length(dimension_col_names)), nrow = 1, dimnames = list(NULL, c(dimension_col_names)))) |
| 770 |
} else {
|
|
| 771 | ! |
prev_row[, c(dimension_col_names), with = FALSE] |
| 772 |
} |
|
| 773 |
} |
|
| 774 | ||
| 775 | ! |
}, by = c("ENA_UNIT", by)]
|
| 776 | ! |
for(col in dimension_col_names) {
|
| 777 | ! |
set(filled_data, j = col, value = as.ena.dimension(filled_data[[col]])) |
| 778 |
} |
|
| 779 | ! |
return(filled_data) |
| 780 |
} |
|
| 781 | ||
| 782 |
#' Clears specified plots from an ENA set. |
|
| 783 |
#' |
|
| 784 |
#' This function removes the plots specified by their indices from the `plots` field of the ENA set. |
|
| 785 |
#' |
|
| 786 |
#' @param x An ENA set object containing the plots. |
|
| 787 |
#' @param wh A numeric vector specifying the indices of the plots to clear. Default is all plots. |
|
| 788 |
#' |
|
| 789 |
#' @return Invisibly returns the modified ENA set object with the specified plots removed. |
|
| 790 |
#' |
|
| 791 |
#' @example inst/examples/example-plot-piping.R |
|
| 792 |
#' |
|
| 793 |
#' @export |
|
| 794 |
clear <- function(x, wh = seq(x$plots)) {
|
|
| 795 | ! |
if(length(wh) > 0) {
|
| 796 | ! |
x$plots[[wh]] <- NULL |
| 797 |
} |
|
| 798 | ! |
invisible(x) |
| 799 |
} |
|
| 800 | ||
| 801 |
#' Scales the points in an ENA set. |
|
| 802 |
#' |
|
| 803 |
#' This function adjusts the scale of the points in the ENA set to match the range of the network. |
|
| 804 |
#' |
|
| 805 |
#' @param x An ENAplot object containing the set to scale. |
|
| 806 |
#' @param center Unused parameter, included for compatibility. |
|
| 807 |
#' @param scale A numeric value specifying the scaling factor. If `NULL`, the function will determine the scale based on the data. |
|
| 808 |
#' |
|
| 809 |
#' @return The modified ENAplot object with scaled points. |
|
| 810 |
#' |
|
| 811 |
#' @export |
|
| 812 |
scale.ENAplot <- function(x, center = NULL, scale = NULL) {
|
|
| 813 | ! |
plot <- x |
| 814 | ! |
set <- plot$enaset; |
| 815 | ||
| 816 | ! |
point_range <- range(set$points); |
| 817 | ! |
network_range <- range(set$rotation$nodes); |
| 818 | ||
| 819 | ! |
if(is.null(scale)) {
|
| 820 | ! |
scale <- min(abs(network_range) / abs(point_range)); |
| 821 |
} |
|
| 822 | ||
| 823 | ! |
set$points <- set$points * scale; |
| 824 | ||
| 825 | ! |
return(plot) |
| 826 |
} |
|
| 827 | ||
| 828 |
#' Updates the axis ranges of an ENA plot based on the plotted data. |
|
| 829 |
#' |
|
| 830 |
#' This function adjusts the x and y axis ranges of the ENA plot to ensure that all plotted points, networks, and means are visible. |
|
| 831 |
#' |
|
| 832 |
#' @param x An ENA plot object containing the plotted data and axis configurations. |
|
| 833 |
#' |
|
| 834 |
#' @return The updated ENA plot object with adjusted axis ranges. |
|
| 835 |
#' |
|
| 836 |
#' @export |
|
| 837 |
check_range <- function(x) {
|
|
| 838 | 1x |
if (.qe_backend(x)) return(.qe_check_range(x)) |
| 839 | ! |
numbers <- as.numeric(sapply(x$plotted$points, function(p) max(as.matrix(p$data)))); |
| 840 | ! |
means <- as.numeric(sapply(x$plotted$means, function(p) max(as.matrix(p$data)))); |
| 841 | ||
| 842 | ! |
network <- NULL; |
| 843 | ! |
if(length(x$plotted$networks) > 0) {
|
| 844 | ! |
network <- abs(as.numeric(sapply(x$plotted$networks, function(nn) sapply(nn, `[`, c("x0","x1","y0","y1")))));
|
| 845 |
} |
|
| 846 | ||
| 847 |
if( |
|
| 848 | ! |
length(numbers) == 0 && |
| 849 | ! |
length(means) == 0 |
| 850 |
) {
|
|
| 851 | ! |
return(x) |
| 852 |
} |
|
| 853 | ||
| 854 | ! |
curr_max = max(c(numbers, network, means)) |
| 855 | ! |
if(curr_max*1.2 > max(x$axes$y$range)) {
|
| 856 | ! |
this.max = curr_max * 1.2 |
| 857 | ! |
x$axes$x$range = c(-this.max, this.max) |
| 858 | ! |
x$axes$y$range = c(-this.max, this.max) |
| 859 | ! |
x$plot = plotly::layout( |
| 860 | ! |
x$plot, |
| 861 | ! |
xaxis = x$axes$x, |
| 862 | ! |
yaxis = x$axes$y |
| 863 |
); |
|
| 864 | ! |
} else if (curr_max < max(x$axes$y$range*0.5)) {
|
| 865 | ! |
this.max = curr_max * 1.2 |
| 866 | ! |
x$axes$x$range = c(-this.max, this.max) |
| 867 | ! |
x$axes$y$range = c(-this.max, this.max) |
| 868 | ! |
x$plot = plotly::layout( |
| 869 | ! |
x$plot, |
| 870 | ! |
xaxis = x$axes$x, |
| 871 | ! |
yaxis = x$axes$y |
| 872 |
); |
|
| 873 |
} |
|
| 874 | ||
| 875 | ! |
x |
| 876 |
} |
|
| 877 | ||
| 878 |
#' Display and update plot objects within a custom object |
|
| 879 |
#' |
|
| 880 |
#' This function updates the plots within the provided object by applying the `check_range` function to each plot. |
|
| 881 |
#' It then prints the updated object using custom print options and returns the object invisibly. |
|
| 882 |
#' |
|
| 883 |
#' @param x An object containing a list of plots in the `plots` field. |
|
| 884 |
#' @param ... Additional arguments passed to the `print` method. |
|
| 885 |
#' |
|
| 886 |
#' @return The updated object `x`, returned invisibly. |
|
| 887 |
#' |
|
| 888 |
#' @export |
|
| 889 |
show <- function(x, ...) {
|
|
| 890 |
# browser() |
|
| 891 | ! |
x$plots <- lapply(x$plots, check_range) |
| 892 | ! |
print(x, ..., plot = T, set = F) |
| 893 | ! |
invisible(x) |
| 894 |
} |
|
| 895 | ||
| 896 |
# Prefix the leftmost name of a selector such as `Condition$FirstGame` with |
|
| 897 |
# `prefix`, giving `set$points$Condition$FirstGame`, without deparsing and |
|
| 898 |
# re-parsing the expression. |
|
| 899 |
prefix_expr <- function(expr, prefix) {
|
|
| 900 | ! |
if (is.symbol(expr)) return(call("$", prefix, expr))
|
| 901 | ! |
expr[[2]] <- prefix_expr(expr[[2]], prefix) |
| 902 | ! |
expr |
| 903 |
} |
| 1 |
# R wrappers that preserve rENA's long-standing function names. The math |
|
| 2 |
# lives in C++: ENA model code in libena (inst/include/libena, wrapped in |
|
| 3 |
# src/libena_rcpp.cpp), accumulation in tma (libtma) and generic numerics in |
|
| 4 |
# libqe. |
|
| 5 |
# |
|
| 6 |
# Public API functions (exported) are marked @export. |
|
| 7 |
# Internal functions (not exported) have no @export tag. |
|
| 8 | ||
| 9 |
# ── public API ──────────────────────────────────────────────────────────────── |
|
| 10 | ||
| 11 |
#' Merge data frame columns |
|
| 12 |
#' |
|
| 13 |
#' Paste together multiple columns of a data frame or data.table with a |
|
| 14 |
#' separator, used internally to construct unit-ID strings. |
|
| 15 |
#' |
|
| 16 |
#' @param df A data.frame or data.table |
|
| 17 |
#' @param cols Character vector of column names to paste together |
|
| 18 |
#' @param sep Separator string (default "::") |
|
| 19 |
#' @return A character vector of length \code{nrow(df)}
|
|
| 20 |
#' @export |
|
| 21 |
merge_columns_c <- function(df, cols, sep = "::") {
|
|
| 22 | 351x |
do.call(paste, c(lapply(cols, function(col) df[[col]]), list(sep = sep))) |
| 23 |
} |
|
| 24 | ||
| 25 |
#' Row-wise L2 (Sphere) Normalization |
|
| 26 |
#' |
|
| 27 |
#' Normalizes each row of a numeric data frame or matrix to unit L2 norm. |
|
| 28 |
#' |
|
| 29 |
#' @param dfM A data.frame or matrix |
|
| 30 |
#' @return A numeric matrix with each row normalized to unit L2 length |
|
| 31 |
#' @export |
|
| 32 |
fun_sphere_norm <- function(dfM) {
|
|
| 33 | 165x |
libqe::normalize_networks(as.matrix(dfM)) |
| 34 |
} |
|
| 35 | ||
| 36 |
#' Row-wise Max-Norm Scaling |
|
| 37 |
#' |
|
| 38 |
#' Scales all rows of a numeric data frame by dividing by the largest row |
|
| 39 |
#' L2 norm. |
|
| 40 |
#' |
|
| 41 |
#' @param dfM A data.frame or matrix |
|
| 42 |
#' @return A numeric matrix scaled by the largest row L2 norm |
|
| 43 |
#' @export |
|
| 44 |
fun_skip_sphere_norm <- function(dfM) {
|
|
| 45 | 7x |
libqe::scale_networks(as.matrix(dfM)) |
| 46 |
} |
|
| 47 | ||
| 48 |
#' Upper Triangle from Vector (numeric) |
|
| 49 |
#' |
|
| 50 |
#' Compute pairwise products v[j] * v[i] for all j < i. |
|
| 51 |
#' |
|
| 52 |
#' @param v Numeric vector or single-row matrix |
|
| 53 |
#' @return Numeric row vector of pairwise products |
|
| 54 |
#' @export |
|
| 55 |
vector_to_ut <- function(v) {
|
|
| 56 | ! |
libqe::code_connections(as.matrix(v)) |
| 57 |
} |
|
| 58 | ||
| 59 |
#' Directed ENA node positions |
|
| 60 |
#' |
|
| 61 |
#' Least-squares node positions for directed ENA. |
|
| 62 |
#' |
|
| 63 |
#' @param line_weights Numeric matrix (units x connections) |
|
| 64 |
#' @param points Numeric matrix of rotated points (units x dims) |
|
| 65 |
#' @param numDims Number of dimensions |
|
| 66 |
#' @param self_weight Weight of a self-connection (the diagonal: ground i -> response i) in its code's node weight: 2 (default) counts it in both the node's row and column sums (Zhiqiang Cai's 2021 in + out rule); 1 counts it once. |
|
| 67 |
#' @return List with nodes, centroids, weights, points |
|
| 68 |
#' @export |
|
| 69 |
directed_node_positions <- function(line_weights, points, numDims, self_weight = 2) {
|
|
| 70 | 19x |
directed_node_positions_c(line_weights, points, numDims, self_weight) |
| 71 |
} |
|
| 72 | ||
| 73 |
#' Directed node positions with ground+response combined |
|
| 74 |
#' |
|
| 75 |
#' Directed node positions with paired ground+response rows combined. |
|
| 76 |
#' |
|
| 77 |
#' @param line_weights Numeric matrix (units x connections) |
|
| 78 |
#' @param points Numeric matrix of rotated points (units x dims) |
|
| 79 |
#' @param numDims Number of dimensions |
|
| 80 |
#' @param self_weight Weight of a self-connection (the diagonal: ground i -> response i) in its code's node weight: 2 (default) counts it in both the node's row and column sums (Zhiqiang Cai's 2021 in + out rule); 1 counts it once. |
|
| 81 |
#' @return List with nodes, centroids, weights, points |
|
| 82 |
#' @export |
|
| 83 |
directed_node_positions_with_ground_response_added <- function(line_weights, |
|
| 84 |
points, |
|
| 85 |
numDims, |
|
| 86 |
self_weight = 2) {
|
|
| 87 | ! |
directed_node_positions_combine_pairs(line_weights, points, numDims, self_weight) |
| 88 |
} |
|
| 89 | ||
| 90 |
#' Layout-agnostic node positions from an adjacency key |
|
| 91 |
#' |
|
| 92 |
#' Node optimization for any connection layout -- undirected, undirected with |
|
| 93 |
#' self-connections, or ordered: every connection counts toward each code it |
|
| 94 |
#' connects, a self-connection (a column whose two codes are the same) toward |
|
| 95 |
#' its code \code{self_weight} times; each unit's weights are then
|
|
| 96 |
#' L1-normalised and the nodes solved by least squares against the points. |
|
| 97 |
#' With an undirected key it matches \code{lws_lsq_positions()}; with an
|
|
| 98 |
#' ordered key, \code{directed_node_positions()} at the same \code{self_weight}.
|
|
| 99 |
#' |
|
| 100 |
#' @param line_weights Numeric matrix (units x connections) |
|
| 101 |
#' @param points Numeric matrix of rotated points (units x dims) |
|
| 102 |
#' @param numDims Number of dimensions |
|
| 103 |
#' @param key Adjacency key: a 2 x n_connections matrix of code names, |
|
| 104 |
#' one column per connection (e.g. \code{set$rotation$adjacency.key})
|
|
| 105 |
#' @param codes Character vector of codes; node order |
|
| 106 |
#' @param self_weight Weight of a self-connection in its code's node weight |
|
| 107 |
#' (2 = row + column sum, as ordered models; 1 = once) |
|
| 108 |
#' @return List with nodes, centroids, weights, points |
|
| 109 |
#' @export |
|
| 110 |
node_positions_by_key <- function(line_weights, points, numDims, key, codes, self_weight = 2) {
|
|
| 111 | 51x |
ft <- key_indices(key, codes) |
| 112 | 50x |
node_positions_by_key_c(as.matrix(line_weights), as.matrix(points), numDims, |
| 113 | 50x |
ft$from, ft$to, length(codes), self_weight) |
| 114 |
} |
|
| 115 | ||
| 116 |
#' Layout-agnostic node centroids from an adjacency key |
|
| 117 |
#' |
|
| 118 |
#' The weighting of \code{node_positions_by_key()} times \code{nodes}.
|
|
| 119 |
#' |
|
| 120 |
#' @inheritParams node_positions_by_key |
|
| 121 |
#' @param nodes Numeric matrix of node positions (codes x dims), in \code{codes} order
|
|
| 122 |
#' @return Numeric matrix of centroids (units x dims) |
|
| 123 |
#' @export |
|
| 124 |
node_centroids_by_key <- function(line_weights, nodes, key, codes, self_weight = 2) {
|
|
| 125 | 1x |
ft <- key_indices(key, codes) |
| 126 | 1x |
node_centroids_by_key_c(as.matrix(line_weights), as.matrix(nodes), ft$from, ft$to, self_weight) |
| 127 |
} |
|
| 128 | ||
| 129 |
# The codes as the key names them: the key is built from the connection |
|
| 130 |
# column names, which may be make.names() of the codes. |
|
| 131 |
key_codes <- function(key, codes) {
|
|
| 132 | 78x |
named <- unique(as.character(as.matrix(key))) |
| 133 | ! |
if (all(named %in% codes)) codes else make.names(codes) |
| 134 |
} |
|
| 135 | ||
| 136 |
# 0-based code indices of each key column. |
|
| 137 |
key_indices <- function(key, codes) {
|
|
| 138 | 86x |
key <- as.matrix(key) |
| 139 | ! |
if (nrow(key) != 2) stop("key must have 2 rows (one column per connection)")
|
| 140 | 86x |
from <- match(as.character(key[1, ]), codes) |
| 141 | 86x |
to <- match(as.character(key[2, ]), codes) |
| 142 | 86x |
if (anyNA(c(from, to))) {
|
| 143 | 1x |
stop("key names codes not in `codes`: ",
|
| 144 | 1x |
paste(unique(c(key[1, is.na(from)], key[2, is.na(to)])), collapse = ", ")) |
| 145 |
} |
|
| 146 | 85x |
list(from = from - 1L, to = to - 1L) |
| 147 |
} |
|
| 148 | ||
| 149 |
#' Calculate ENA correlations |
|
| 150 |
#' |
|
| 151 |
#' Pearson correlation with confidence interval between ENA points and |
|
| 152 |
#' centroids. |
|
| 153 |
#' |
|
| 154 |
#' @param points Numeric matrix (units x dims) |
|
| 155 |
#' @param centroids Numeric matrix (units x dims) |
|
| 156 |
#' @param conf_level Confidence level (default 0.95) |
|
| 157 |
#' @return Numeric matrix with columns: r, lower CI, upper CI |
|
| 158 |
#' @export |
|
| 159 |
ena_correlation <- function(points, centroids, conf_level = 0.95) {
|
|
| 160 | ! |
ena_correlation_c(points, centroids, conf_level) |
| 161 |
} |
|
| 162 | ||
| 163 |
#' Confidence intervals around group mean positions |
|
| 164 |
#' |
|
| 165 |
#' Per-dimension t-based confidence intervals around the column means of a |
|
| 166 |
#' numeric matrix of ENA points. |
|
| 167 |
#' |
|
| 168 |
#' @param points Numeric matrix (units x dims) |
|
| 169 |
#' @param conf_level Confidence level (default 0.95) |
|
| 170 |
#' @return Numeric matrix (dims x 3): mean, lower CI, upper CI |
|
| 171 |
#' @export |
|
| 172 |
ena_mean_ci <- function(points, conf_level = 0.95) {
|
|
| 173 | 7x |
libqe::mean_ci(as.matrix(points), conf_level) |
| 174 |
} |
|
| 175 | ||
| 176 |
#' Outlier (Tukey-fence) intervals for group positions |
|
| 177 |
#' |
|
| 178 |
#' Per-dimension Tukey-fence intervals: Q1 - k*IQR to Q3 + k*IQR. |
|
| 179 |
#' |
|
| 180 |
#' @param points Numeric matrix (units x dims) |
|
| 181 |
#' @param iqr_factor IQR multiplier (default 1.5) |
|
| 182 |
#' @return Numeric matrix (dims x 2): lower fence, upper fence |
|
| 183 |
#' @export |
|
| 184 |
ena_outlier_ci <- function(points, iqr_factor = 1.5) {
|
|
| 185 | 6x |
libqe::outlier_ci(as.matrix(points), iqr_factor) |
| 186 |
} |
|
| 187 | ||
| 188 |
#' Two-group comparison statistics for ENA points |
|
| 189 |
#' |
|
| 190 |
#' Per-dimension parametric (Welch t-test, Cohen's d) and non-parametric |
|
| 191 |
#' (Wilcoxon rank-sum, rank-biserial r) statistics comparing two groups. |
|
| 192 |
#' |
|
| 193 |
#' @param g1 Numeric matrix of group 1 points (units x dims) |
|
| 194 |
#' @param g2 Numeric matrix of group 2 points (units x dims) |
|
| 195 |
#' @return List with: n1, n2, t, df, pvalue_t, cohens_d, means, sds, |
|
| 196 |
#' U, pvalue_u, effect_r, medians — each a vector/matrix of length dims |
|
| 197 |
#' @export |
|
| 198 |
ena_group_stats <- function(g1, g2) {
|
|
| 199 | 8x |
libqe::group_stats(as.matrix(g1), as.matrix(g2)) |
| 200 |
} |
|
| 201 | ||
| 202 |
# ── internal (not exported) ─────────────────────────────────────────────────── |
|
| 203 | ||
| 204 |
# Per-row upper-triangle co-occurrence. |
|
| 205 |
# @param df A data.frame or matrix of code columns |
|
| 206 |
# @param binary If TRUE, binarise non-zero products |
|
| 207 |
rows_to_co_occurrences <- function(df, binary = TRUE) {
|
|
| 208 | 51x |
tma::row_connections(as.matrix(df), binary) |
| 209 |
} |
|
| 210 | ||
| 211 |
# Stanza-window co-occurrence accumulation. |
|
| 212 |
# @param df A data.frame or matrix of code columns |
|
| 213 |
# @param windowSize Rows to look back (default 1; Inf = whole conversation) |
|
| 214 |
# @param windowForward Rows to look forward (default 0) |
|
| 215 |
# @param binary Binarise co-occurrence counts (default TRUE) |
|
| 216 |
ref_window_df <- function(df, windowSize = 1, windowForward = 0, |
|
| 217 |
binary = TRUE) {
|
|
| 218 | 2434x |
INT_MAX <- .Machine$integer.max |
| 219 | 2434x |
wb <- if (is.infinite(windowSize) || windowSize >= INT_MAX) INT_MAX |
| 220 | 2434x |
else as.integer(windowSize) |
| 221 | 2434x |
wf <- if (is.infinite(windowForward) || windowForward >= INT_MAX) INT_MAX |
| 222 | 2434x |
else as.integer(windowForward) |
| 223 | 2434x |
data.table::as.data.table(tma::accumulate_stanza(as.matrix(df), wb, wf, binary)) |
| 224 |
} |
|
| 225 | ||
| 226 |
# Rolling backward window sum of code columns. |
|
| 227 |
# @param df A data.frame or matrix of code columns |
|
| 228 |
# @param windowSize Number of rows to look back (default 0, treated as 1) |
|
| 229 |
# @param binary Unused; kept for API compatibility |
|
| 230 |
ref_window_lag <- function(df, windowSize = 0, binary = TRUE) {
|
|
| 231 | 198x |
tma::rolling_window_sum(as.matrix(df), windowSize) |
| 232 |
} |
|
| 233 | ||
| 234 |
# Upper-triangle index pairs (0-based, +1 before use as R indices). |
|
| 235 |
# @param len Side length of square code matrix |
|
| 236 |
# @param row -1 = both rows, 0 = row indices, 1 = col indices |
|
| 237 |
triIndices <- function(len, row = -1L) {
|
|
| 238 | 176x |
libqe::connection_indices(len, row) |
| 239 |
} |
|
| 240 | ||
| 241 |
# Least-squares node positions (undirected ENA). |
|
| 242 |
# @param adjMats Numeric matrix of line weights (units x connections) |
|
| 243 |
# @param t Numeric matrix of rotated points (units x dims) |
|
| 244 |
# @param numDims Number of dimensions |
|
| 245 |
lws_lsq_positions <- function(adjMats, t, numDims) {
|
|
| 246 | 89x |
node_positions(adjMats, t, numDims) |
| 247 |
} |
|
| 248 | ||
| 249 |
# String upper-triangle pairs: "A" "B" "C" -> "A & B" "A & C" "B & C". |
|
| 250 |
# @param v Character vector of code names |
|
| 251 |
svector_to_ut <- function(v) {
|
|
| 252 | 102x |
libqe::connection_names(v) |
| 253 |
} |
|
| 254 | ||
| 255 |
# Center data by subtracting column means. |
|
| 256 |
# @param values Numeric matrix or data.frame |
|
| 257 |
center_data_c <- function(values) {
|
|
| 258 | 163x |
libqe::center_points(as.matrix(values)) |
| 259 |
} |
| 1 |
#### |
|
| 2 |
#' ENAdata R6class |
|
| 3 |
#' |
|
| 4 |
#' @docType class |
|
| 5 |
#' @importFrom R6 R6Class |
|
| 6 |
#' @import data.table |
|
| 7 |
#' @export |
|
| 8 |
#' |
|
| 9 |
#' @field raw A data frame constructed from the unit, convo, code, and metadata parameters of ena.accumulate.data |
|
| 10 |
#' @field adjacency.vectors A data frame of adjacency (co-occurrence) vectors by row |
|
| 11 |
#' @field accumulated.adjacency.vectors A data frame of adjacency (co-occurrence) vectors accumulated per unit |
|
| 12 |
#' @field model The type of ENA model: EndPoint, Accumulated Trajectory, or Separate Trajectory |
|
| 13 |
#' @field units A data frame of columns that were combined to make the unique units. Includes column for trajectory selections. (unique) |
|
| 14 |
#' @field unit.names A vector of unique unit values |
|
| 15 |
#' @field metadata A data frame of unique metadata for each unit |
|
| 16 |
#' @field trajectories A list: units - data frame, for a given row tells which trajectory it's a part; step - data frame, where along the trajectory a row sits |
|
| 17 |
#' |
|
| 18 |
#' @field adjacency.matrix TBD |
|
| 19 |
#' @field adjacency.vectors.raw TBD |
|
| 20 |
#' @field codes A vector of code names |
|
| 21 |
#' @field function.call The string representation of function called and parameters provided |
|
| 22 |
#' @field function.params A list of all parameters sent to function call |
|
| 23 |
#### |
|
| 24 |
ENAdata <- R6::R6Class("ENAdata", public = list(
|
|
| 25 | ||
| 26 |
#' Construct ENAdata |
|
| 27 |
#' |
|
| 28 |
#' @param file TBD |
|
| 29 |
#' @param units TBD |
|
| 30 |
#' @param units.used TBD |
|
| 31 |
#' @param units.by TBD |
|
| 32 |
#' @param conversations.by TBD |
|
| 33 |
#' @param codes TBD |
|
| 34 |
#' @param model TBD |
|
| 35 |
#' @param weight.by TBD |
|
| 36 |
#' @param window.size.back TBD |
|
| 37 |
#' @param window.size.forward TBD |
|
| 38 |
#' @param mask TBD |
|
| 39 |
#' @param include.meta logical, if TRUE (default) unit metadata is attached to the resulting ENAdata object and accessible via the set; set to FALSE to omit metadata from the model output |
|
| 40 |
#' @param ... TBD |
|
| 41 |
#' |
|
| 42 |
#' @return |
|
| 43 |
initialize = function( |
|
| 44 |
file, |
|
| 45 |
units = NULL, |
|
| 46 |
units.used = NULL, |
|
| 47 |
units.by = NULL, |
|
| 48 |
conversations.by = NULL, |
|
| 49 |
codes = NULL, |
|
| 50 |
model = NULL, |
|
| 51 |
weight.by = "binary", |
|
| 52 |
window.size.back = 1, |
|
| 53 |
window.size.forward = 0, |
|
| 54 |
mask = NULL, |
|
| 55 |
include.meta = T, |
|
| 56 |
... |
|
| 57 |
) {
|
|
| 58 | 141x |
args <- list(...); |
| 59 | 141x |
self$function.call <- sys.call(-1); |
| 60 | 141x |
self$function.params <- list(); |
| 61 | ||
| 62 | 141x |
private$file <- file; |
| 63 | 141x |
self$units <- units; |
| 64 | 141x |
private$units.used <- units.used; |
| 65 | 141x |
private$units.by <- units.by |
| 66 | 141x |
private$conversations.by <- conversations.by; |
| 67 | 141x |
self$codes <- codes; |
| 68 | ||
| 69 | 141x |
if (is.data.frame(self$codes)) {
|
| 70 | 94x |
self$codes <- colnames(self$codes); |
| 71 |
} |
|
| 72 | ||
| 73 | 141x |
private$weight.by <- weight.by; |
| 74 | 141x |
private$window.size <- list( |
| 75 | 141x |
"back" = window.size.back, |
| 76 | 141x |
"forward" = window.size.forward |
| 77 |
); |
|
| 78 | ||
| 79 | 141x |
for (p in c("units", "units.used", "units.by",
|
| 80 | 141x |
"conversations.by", "codes", "model", "weight.by", |
| 81 | 141x |
"window.size.back", "window.size.forward", "mask", |
| 82 | 141x |
"in.par", "grainSize", "include.meta") |
| 83 |
) {
|
|
| 84 | 1833x |
if (exists(p, inherits = FALSE)) {
|
| 85 | 1551x |
self$function.params[[p]] <- get(p, inherits = FALSE) |
| 86 |
} |
|
| 87 | 282x |
else if (!is.null(args[[p]])) {
|
| 88 | 1x |
self$function.params[[p]] <- args[[p]] |
| 89 |
} |
|
| 90 |
} |
|
| 91 | ||
| 92 | 141x |
self$model <- model |
| 93 | ||
| 94 | 141x |
private$mask <- mask |
| 95 | ||
| 96 | 141x |
return(self) |
| 97 |
}, |
|
| 98 | ||
| 99 |
## Public Properties ---- |
|
| 100 |
model = NULL, |
|
| 101 |
raw = NULL, |
|
| 102 |
adjacency.vectors = NULL, |
|
| 103 |
adjacency.matrix = NULL, |
|
| 104 |
accumulated.adjacency.vectors = NULL, |
|
| 105 |
adjacency.vectors.raw = NULL, |
|
| 106 |
units = NULL, |
|
| 107 |
unit.names = NULL, |
|
| 108 |
metadata = NULL, |
|
| 109 |
trajectories = list( |
|
| 110 |
units = NULL, |
|
| 111 |
step = NULL |
|
| 112 |
), |
|
| 113 |
codes = NULL, |
|
| 114 |
function.call = NULL, |
|
| 115 |
function.params = NULL, |
|
| 116 | ||
| 117 |
## Public Functions ---- |
|
| 118 | ||
| 119 |
#' Process accumulation |
|
| 120 |
#' |
|
| 121 |
#' @return ENAdata |
|
| 122 |
process = function() {
|
|
| 123 | 141x |
private$loadFile(); |
| 124 |
}, |
|
| 125 | ||
| 126 |
#' Get property from object |
|
| 127 |
#' |
|
| 128 |
#' @param x character key to retrieve from object |
|
| 129 |
#' @return value from object at x |
|
| 130 |
get = function(x = "data") {
|
|
| 131 | 116183x |
return(private[[x]]) |
| 132 |
}, |
|
| 133 | ||
| 134 |
#' Add metadata |
|
| 135 |
#' |
|
| 136 |
#' @param merge logical (default: FALSE) |
|
| 137 |
#' |
|
| 138 |
#' @return data.frame |
|
| 139 |
add.metadata = function(merge = F) {
|
|
| 140 | 139x |
meta_avail <- colnames(self$raw)[ |
| 141 | 139x |
-which(colnames(self$raw) %in% |
| 142 | 139x |
c(self$codes, private$units.by, private$conversations.by))] |
| 143 |
# c(self$codes, private$units.by))] # private$conversations.by))] |
|
| 144 | ||
| 145 | 139x |
meta_avail <- meta_avail[which(meta_avail != "ENA_UNIT")] |
| 146 | 139x |
meta_cols_to_use <- meta_avail[apply(self$raw[, lapply(.SD, uniqueN), |
| 147 | 139x |
by = c(private$units.by), |
| 148 | 139x |
.SDcols = meta_avail |
| 149 | 139x |
][, c(meta_avail), with = F] |
| 150 | 139x |
, 2, function(x) all(x == 1)) |
| 151 |
] |
|
| 152 | 139x |
raw.meta <- self$raw[!duplicated(ENA_UNIT)][ |
| 153 | 139x |
ENA_UNIT %in% unique( |
| 154 | 139x |
self$accumulated.adjacency.vectors$ENA_UNIT |
| 155 |
), |
|
| 156 | 139x |
c("ENA_UNIT", private$units.by, meta_cols_to_use),
|
| 157 | 139x |
with = F |
| 158 |
] |
|
| 159 | ||
| 160 | 139x |
df_to_return <- raw.meta[ENA_UNIT %in% self$unit.names,]; |
| 161 | ||
| 162 | 139x |
return(df_to_return) |
| 163 |
} |
|
| 164 | ||
| 165 |
), |
|
| 166 | ||
| 167 |
### Private ---- |
|
| 168 |
private = list( |
|
| 169 | ||
| 170 |
## Private Properties ---- |
|
| 171 |
file = NULL, |
|
| 172 |
window.size = NULL, |
|
| 173 |
units.used = NULL, |
|
| 174 |
units.by = NULL, |
|
| 175 |
conversations.by = NULL, |
|
| 176 |
weight.by = NULL, |
|
| 177 |
trajectory.by = NULL, |
|
| 178 |
mask = NULL, |
|
| 179 | ||
| 180 |
## Private Functions ---- |
|
| 181 |
loadFile = function() {
|
|
| 182 | 141x |
if(any(class(private$file) == "data.table")) {
|
| 183 | 30x |
df_DT <- private$file |
| 184 |
} else {
|
|
| 185 | 111x |
if(any(class(private$file) == "data.frame")) {
|
| 186 | 111x |
df <- private$file |
| 187 |
} else {
|
|
| 188 | ! |
df <- read.csv(private$file) |
| 189 |
} |
|
| 190 | 111x |
df_DT <- data.table::as.data.table(df) |
| 191 |
} |
|
| 192 | ||
| 193 | 141x |
self$raw <- data.table::copy(df_DT) |
| 194 | 141x |
self$raw$ENA_UNIT <- merge_columns_c(self$raw, private$units.by, "::") |
| 195 | ||
| 196 | 141x |
self <- accumulate.data(self) |
| 197 | 140x |
self$units <- self$adjacency.vectors[, private$units.by, with = F] |
| 198 | ||
| 199 | 140x |
if (!self$model %in% c("AccumulatedTrajectory", "SeparateTrajectory")) {
|
| 200 | 130x |
self$unit.names <- self$adjacency.vectors$ENA_UNIT |
| 201 |
} |
|
| 202 |
else {
|
|
| 203 | 10x |
self$trajectories$units <- self$units |
| 204 | 10x |
conversation <- self$adjacency.vectors[, private$conversations.by, with = F]; |
| 205 | ||
| 206 | 10x |
self$trajectories$step <- conversation |
| 207 | 10x |
self$units <- cbind(self$units, conversation) |
| 208 | 10x |
self$unit.names <- paste( |
| 209 | 10x |
self$adjacency.vectors$ENA_UNIT, |
| 210 | 10x |
self$adjacency.vectors$TRAJ_UNIT, |
| 211 | 10x |
sep = "::" |
| 212 |
) |
|
| 213 |
} |
|
| 214 | ||
| 215 | 140x |
self$adjacency.vectors.raw <- self$adjacency.vectors |
| 216 | ||
| 217 | 140x |
adjCols <- colnames(self$adjacency.vectors)[ |
| 218 | 140x |
grep("adjacency.code", colnames(self$adjacency.vectors))
|
| 219 |
]; |
|
| 220 | ||
| 221 | 140x |
if (is.null(private$mask)) {
|
| 222 | 139x |
private$mask <- matrix(1, |
| 223 | 139x |
nrow = length(self$codes), |
| 224 | 139x |
ncol = length(self$codes), |
| 225 | 139x |
dimnames = list(self$codes, self$codes)) |
| 226 |
} |
|
| 227 | ||
| 228 | 140x |
self$adjacency.vectors[, c(adjCols)] <- |
| 229 | 140x |
self$adjacency.vectors[, c(adjCols), with = F] * |
| 230 | 140x |
rep( |
| 231 | 140x |
private$mask[upper.tri(private$mask)], |
| 232 | 140x |
rep(nrow(self$adjacency.vectors), length(adjCols)) |
| 233 |
) |
|
| 234 | ||
| 235 |
# if( is.function(private$weight.by) ) {
|
|
| 236 |
# cols <- colnames(self$adjacency.vectors)[ |
|
| 237 |
# grep("adjacency.code", colnames(self$adjacency.vectors))
|
|
| 238 |
# ] |
|
| 239 |
# self$adjacency.vectors <- self$adjacency.vectors[, |
|
| 240 |
# lapply( |
|
| 241 |
# .SD, |
|
| 242 |
# private$weight.by |
|
| 243 |
# ), |
|
| 244 |
# .SDcols = cols, |
|
| 245 |
# by = 1:nrow(self$adjacency.vectors) |
|
| 246 |
# ] |
|
| 247 |
# } |
|
| 248 | ||
| 249 | 140x |
if( self$function.params$include.meta == T) {
|
| 250 | 139x |
self$metadata <- self$add.metadata(merge = F); |
| 251 |
} else {
|
|
| 252 | 1x |
self$metadata <- data.frame(); |
| 253 |
} |
|
| 254 | ||
| 255 | 140x |
self$adjacency.vectors <- self$adjacency.vectors[, |
| 256 | 140x |
grep("adjacency.code",
|
| 257 | 140x |
colnames(self$adjacency.vectors)), |
| 258 | 140x |
with = F |
| 259 |
] |
|
| 260 | ||
| 261 | 140x |
return(self); |
| 262 |
} |
|
| 263 |
) |
|
| 264 |
) |
| 1 |
#' ENA line weights as matrix |
|
| 2 |
#' |
|
| 3 |
#' @param x ena.line.weights data.table to covert to matrix |
|
| 4 |
#' @param ... additional arguments to be passed to or from methods |
|
| 5 |
#' @param square [TBD] |
|
| 6 |
#' |
|
| 7 |
#' @return matrix |
|
| 8 |
#' @export |
|
| 9 |
as.matrix.ena.line.weights <- function(x, ..., square = FALSE) {
|
|
| 10 | 468x |
args = list(...) |
| 11 | ||
| 12 |
# if(!is.null(args$square)) |
|
| 13 |
# square = args$square |
|
| 14 | ||
| 15 |
# class(x) = class(x)[-1] |
|
| 16 | 468x |
x.unclass <- data.table::as.data.table(unclass(x)) |
| 17 | 468x |
rows = x.unclass[, !find_meta_cols(x.unclass), with = F] |
| 18 | ||
| 19 | 468x |
if(square) {
|
| 20 | 1x |
upperTriSize = ncol(rows) |
| 21 | 1x |
number = ( (ceiling(sqrt(2*upperTriSize)) ^ 2) ) - (2*upperTriSize) |
| 22 | 1x |
codes = unique(unlist(sapply(colnames(rows), strsplit, split = " & "))) |
| 23 | 1x |
cm = sapply(seq(nrow(rows)), function(unit) {
|
| 24 | 1x |
m = matrix(NA, number, number, dimnames = list(codes, codes)) |
| 25 | 1x |
m[upper.tri(m)] = as.numeric(rows[unit,]) |
| 26 | 1x |
m |
| 27 | 1x |
}, simplify = F); |
| 28 | 1x |
return(cm) |
| 29 |
} |
|
| 30 |
else {
|
|
| 31 | 467x |
as.matrix(remove_meta_data(rows), ...) |
| 32 |
} |
|
| 33 |
} |
|
| 34 | ||
| 35 |
#' ENA rotations as matrix |
|
| 36 |
#' |
|
| 37 |
#' @param x ena.rotation.matrix to conver to matrix |
|
| 38 |
#' @param ... additional arguments to be passed to or from methods |
|
| 39 |
#' |
|
| 40 |
#' @return matrix |
|
| 41 |
#' @export |
|
| 42 |
as.matrix.ena.rotation.matrix <- function(x, ...) {
|
|
| 43 | 244x |
class(x) = class(x)[-1] |
| 44 | 244x |
x = remove_meta_data(x) |
| 45 | 244x |
as.matrix(x, ...) |
| 46 |
} |
|
| 47 | ||
| 48 |
#' ENA points as matrix |
|
| 49 |
#' |
|
| 50 |
#' @param x ena.points to convert to a matrix |
|
| 51 |
#' @param ... additional arguments to be passed to or from methods |
|
| 52 |
#' |
|
| 53 |
#' @return matrix |
|
| 54 |
#' @export |
|
| 55 |
as.matrix.ena.points <- function(x, ...) {
|
|
| 56 | 580x |
class(x) = class(x)[-1] |
| 57 | 580x |
x = remove_meta_data(x) |
| 58 | 580x |
as.matrix(x, ...) |
| 59 |
} |
|
| 60 | ||
| 61 |
#' Matrix without metadata |
|
| 62 |
#' |
|
| 63 |
#' @param x Object to convert to a matrix |
|
| 64 |
#' @param ... additional arguments to be passed to or from methods |
|
| 65 |
#' |
|
| 66 |
#' @return matrix |
|
| 67 |
#' @export |
|
| 68 |
as.matrix.ena.matrix <- function(x, ...) {
|
|
| 69 | 78x |
class(x) = class(x)[-1] |
| 70 | 78x |
x = remove_meta_data(x) |
| 71 | 78x |
as.matrix(x, ...) |
| 72 |
} |
|
| 73 | ||
| 74 |
#' ENA nodes as matrix |
|
| 75 |
#' |
|
| 76 |
#' @param x ena.nodes to convert to matrix |
|
| 77 |
#' @param ... additional arguments to be passed to or from methods |
|
| 78 |
#' |
|
| 79 |
#' @return matrix |
|
| 80 |
#' @export |
|
| 81 |
as.matrix.ena.nodes <- function(x, ...) {
|
|
| 82 | 371x |
class(x) = class(x)[-1] |
| 83 | 371x |
as.matrix(x[,-c("code")], ...)
|
| 84 |
} |
|
| 85 | ||
| 86 |
#' ENA row connections as matrix |
|
| 87 |
#' |
|
| 88 |
#' @param x ena.row.connections to conver to a matrix |
|
| 89 |
#' @param ... additional arguments to be passed to or from methods |
|
| 90 |
#' |
|
| 91 |
#' @return matrix |
|
| 92 |
#' @export |
|
| 93 |
as.matrix.row.connections <- function(x, ...) {
|
|
| 94 | 20x |
class(x) = class(x)[-1] |
| 95 | 20x |
as.matrix(x[, sapply(x, is, class2="ena.co.occurrence"), with = F], ...) |
| 96 |
} |
|
| 97 | ||
| 98 | ||
| 99 |
#' ENA Connections as a matrix |
|
| 100 |
#' |
|
| 101 |
#' @param x ena.connections object |
|
| 102 |
#' @param ... additional arguments to be passed to or from methods |
|
| 103 |
# @param square Logical. If TRUE, each row is converted to a square matrix |
|
| 104 |
# @param simplify Logical. If TRUE, returns back a single result as vector |
|
| 105 |
# @param names Ignored |
|
| 106 |
#' |
|
| 107 |
#' @return If square is FALSE (default), a matrix with all metadata columns removed, otherwise a list with square matrices |
|
| 108 |
#' @export |
|
| 109 |
as.matrix.ena.connections <- function(x, ...) {
|
|
| 110 | 276x |
class(x) = class(x)[-1] |
| 111 | 276x |
xx = remove_meta_data(x) |
| 112 | 276x |
rows = as.data.frame(x)[, !find_meta_cols(x), drop = F] |
| 113 | ||
| 114 | 276x |
args = list(...) |
| 115 | 276x |
if(is.null(args$square)) |
| 116 | 271x |
square = F |
| 117 |
else |
|
| 118 | 5x |
square = args$square |
| 119 | ||
| 120 | 276x |
names = args$names |
| 121 | ||
| 122 | 276x |
if(is.null(args$simplify)) |
| 123 | 272x |
simplify = ifelse(nrow(x) > 1, F, T) |
| 124 |
else |
|
| 125 | 4x |
simplify = args$simplify |
| 126 | ||
| 127 | 276x |
if(square) {
|
| 128 | 5x |
upperTriSize = ncol(rows) |
| 129 | 5x |
number = ( (ceiling(sqrt(2*upperTriSize)) ^ 2) ) - (2*upperTriSize) |
| 130 | 5x |
codes = unique(unlist(sapply(colnames(rows), strsplit, split = " & "))) |
| 131 | 5x |
cm = sapply(seq(nrow(rows)), function(unit) {
|
| 132 | 99x |
m = matrix(NA, number, number, dimnames = list(codes, codes)) |
| 133 | 99x |
m[upper.tri(m)] = as.numeric(rows[unit,]) |
| 134 | 99x |
m |
| 135 | 5x |
}, simplify = F) |
| 136 | ||
| 137 | 5x |
if(simplify) {
|
| 138 | 2x |
cm = cm[[1]] |
| 139 |
} else {
|
|
| 140 | 3x |
names(cm) = names |
| 141 |
} |
|
| 142 |
} else {
|
|
| 143 | 271x |
cm = as.matrix(rows) |
| 144 | 271x |
rownames(cm) = names |
| 145 |
} |
|
| 146 | ||
| 147 | 276x |
cm |
| 148 |
} |
| 1 |
#' Tune Window Size for ENA Accumulation |
|
| 2 |
#' |
|
| 3 |
#' Estimates the optimal moving window size for discourse accumulation and returns a new |
|
| 4 |
#' accumulation object constructed with the tuned window size. By default, uses Cross-Covariance |
|
| 5 |
#' Decay (CCD; \code{method = "ccd"}) to estimate the discourse coherence length.
|
|
| 6 |
#' Alternatively, the legacy SVD projection stability heuristic (\code{method = "stability"})
|
|
| 7 |
#' can be used. |
|
| 8 |
#' |
|
| 9 |
#' @param accum_object An \code{ENAAccumulation}/\code{ENAdata}/\code{ena.set} object.
|
|
| 10 |
#' @param min_size Integer. The minimum window size (default=1) to test. |
|
| 11 |
#' @param max_size Integer. The maximum window size (default=20) to test. |
|
| 12 |
#' @param method Character. Window estimation method: \code{"ccd"} (default, Cross-Covariance Decay)
|
|
| 13 |
#' or \code{"stability"} (legacy SVD stability plateau).
|
|
| 14 |
#' @param cutoff Numeric. The threshold (default 0.95) of the maximum correlation used when |
|
| 15 |
#' \code{method = "stability"}.
|
|
| 16 |
#' @param min_overlap Integer. Minimum required overlapping rows per conversation when |
|
| 17 |
#' \code{method = "ccd"} (default=10).
|
|
| 18 |
#' @param ... Additional arguments passed to the underlying window estimation function. |
|
| 19 |
#' |
|
| 20 |
#' @details |
|
| 21 |
#' When \code{method = "ccd"} (default), the function uses \code{\link{ena.ccd.window}} to calculate
|
|
| 22 |
#' the discourse coherence half-life decay lag directly from the conversation stream. |
|
| 23 |
#' |
|
| 24 |
#' When \code{method = "stability"}, the function iteratively rebuilds accumulation objects across
|
|
| 25 |
#' candidate window sizes, generates ENA sets, and computes distance-space correlations |
|
| 26 |
#' between adjacent window sizes until reaching \code{cutoff * max_correlation}.
|
|
| 27 |
#' |
|
| 28 |
#' @return A new \code{ENAdata}/\code{ena.set} object accumulated with the \code{best_window_size}.
|
|
| 29 |
#' |
|
| 30 |
#' @export |
|
| 31 |
#' @importFrom rENA ena.make.set ena_space_dist_corr ena.ccd.window |
|
| 32 |
#' |
|
| 33 |
#' @examples |
|
| 34 |
#' \dontrun{
|
|
| 35 |
#' data(RS.data) |
|
| 36 |
#' codeNames <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 37 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", "Collaboration") |
|
| 38 |
#' accum <- ena.accumulate.data( |
|
| 39 |
#' units = RS.data[, c("UserName", "Condition")],
|
|
| 40 |
#' conversation = RS.data[, c("Condition", "GroupName")],
|
|
| 41 |
#' codes = RS.data[, codeNames], |
|
| 42 |
#' window.size.back = 4 |
|
| 43 |
#' ) |
|
| 44 |
#' tuned_accum <- ena.tune.window.size(accum, method = "ccd") |
|
| 45 |
#' } |
|
| 46 |
ena.tune.window.size <- function( |
|
| 47 |
accum_object, |
|
| 48 |
min_size = 1, |
|
| 49 |
max_size = 20, |
|
| 50 |
method = c("ccd", "stability"),
|
|
| 51 |
cutoff = 0.95, |
|
| 52 |
min_overlap = 10, |
|
| 53 |
... |
|
| 54 |
) {
|
|
| 55 | 3x |
method <- match.arg(method) |
| 56 | ||
| 57 | 3x |
if (method == "ccd") {
|
| 58 | 2x |
best_window_size <- ena.ccd.window( |
| 59 | 2x |
x = accum_object, |
| 60 | 2x |
max_window = max_size, |
| 61 | 2x |
min_overlap = min_overlap |
| 62 |
) |
|
| 63 |
} else {
|
|
| 64 |
# Legacy stability plateau method |
|
| 65 | 1x |
window_range <- min_size:max_size |
| 66 | 1x |
all_points <- list() |
| 67 | ||
| 68 | 1x |
for (i in seq_along(window_range)) {
|
| 69 | 4x |
window_size <- window_range[i] |
| 70 | 4x |
new_accum <- rebuild_accumulation(accum_object, window_size) |
| 71 | 4x |
if (is.null(new_accum)) |
| 72 | ! |
stop("method = \"stability\" needs an accumulation made by ena.accumulate.data().")
|
| 73 | 4x |
curr_set <- rENA::ena.make.set(new_accum) |
| 74 | 4x |
points <- as.matrix(curr_set$points) |
| 75 | 4x |
all_points[[i]] <- points |
| 76 |
} |
|
| 77 | ||
| 78 | 1x |
n_steps <- length(window_range) - 1 |
| 79 | 1x |
adj_correlations <- numeric(n_steps) |
| 80 | ||
| 81 | 1x |
for (i in 1:n_steps) {
|
| 82 | 3x |
adj_correlations[i] <- rENA::ena_space_dist_corr(all_points[[i]], all_points[[i + 1]]) |
| 83 |
} |
|
| 84 | ||
| 85 | 1x |
max_corr <- max(adj_correlations, na.rm = TRUE) |
| 86 | 1x |
threshold <- cutoff * max_corr |
| 87 | 1x |
best_idx <- which(adj_correlations >= threshold)[1] |
| 88 | 1x |
best_window_size <- window_range[best_idx] |
| 89 |
} |
|
| 90 | ||
| 91 |
# Rebuild accumulation at best_window_size |
|
| 92 |
# Method 1: re-run ena.accumulate.data() with the original arguments |
|
| 93 | 3x |
res <- rebuild_accumulation(accum_object, best_window_size) |
| 94 | 3x |
if (!is.null(res)) return(res) |
| 95 | ||
| 96 |
# Method 2: Robust fallback - rebuild directly from object fields |
|
| 97 | ! |
raw_df <- if (!is.null(accum_object$model$raw.input)) {
|
| 98 | ! |
as.data.frame(accum_object$model$raw.input) |
| 99 | ! |
} else if (!is.null(accum_object$raw)) {
|
| 100 | ! |
as.data.frame(accum_object$raw) |
| 101 | ! |
} else if (!is.null(accum_object$data)) {
|
| 102 | ! |
as.data.frame(accum_object$data) |
| 103 | ! |
} else if (!is.null(accum_object$`_function.params`$data)) {
|
| 104 | ! |
as.data.frame(accum_object$`_function.params`$data) |
| 105 |
} else {
|
|
| 106 | ! |
NULL |
| 107 |
} |
|
| 108 | ||
| 109 | ! |
if (!is.null(raw_df) && any(duplicated(colnames(raw_df)))) {
|
| 110 | ! |
raw_df <- raw_df[, !duplicated(colnames(raw_df)), drop = FALSE] |
| 111 |
} |
|
| 112 | ||
| 113 | ! |
units_by <- if (!is.null(accum_object$units.by)) {
|
| 114 | ! |
accum_object$units.by |
| 115 | ! |
} else if (!is.null(accum_object$model$units.by)) {
|
| 116 | ! |
accum_object$model$units.by |
| 117 | ! |
} else if (!is.null(accum_object$`_function.params`$units.by)) {
|
| 118 | ! |
accum_object$`_function.params`$units.by |
| 119 | ! |
} else if (!is.null(accum_object$unit.names)) {
|
| 120 | ! |
accum_object$unit.names |
| 121 |
} else {
|
|
| 122 | ! |
NULL |
| 123 |
} |
|
| 124 | ||
| 125 | ! |
convo_by <- if (!is.null(accum_object$conversations.by)) {
|
| 126 | ! |
accum_object$conversations.by |
| 127 | ! |
} else if (!is.null(accum_object$model$conversations.by)) {
|
| 128 | ! |
accum_object$model$conversations.by |
| 129 | ! |
} else if (!is.null(accum_object$`_function.params`$conversations.by)) {
|
| 130 | ! |
accum_object$`_function.params`$conversations.by |
| 131 |
} else {
|
|
| 132 | ! |
NULL |
| 133 |
} |
|
| 134 | ||
| 135 | ! |
codes_by <- if (!is.null(accum_object$model$codes)) {
|
| 136 | ! |
accum_object$model$codes |
| 137 | ! |
} else if (!is.null(accum_object$codes)) {
|
| 138 | ! |
if (is.data.frame(accum_object$codes) || is.matrix(accum_object$codes)) {
|
| 139 | ! |
colnames(accum_object$codes) |
| 140 |
} else {
|
|
| 141 | ! |
as.character(accum_object$codes) |
| 142 |
} |
|
| 143 | ! |
} else if (!is.null(accum_object$`_function.params`$codes)) {
|
| 144 | ! |
if (is.data.frame(accum_object$`_function.params`$codes) || is.matrix(accum_object$`_function.params`$codes)) {
|
| 145 | ! |
colnames(accum_object$`_function.params`$codes) |
| 146 |
} else {
|
|
| 147 | ! |
as.character(accum_object$`_function.params`$codes) |
| 148 |
} |
|
| 149 |
} else {
|
|
| 150 | ! |
NULL |
| 151 |
} |
|
| 152 | ||
| 153 | ! |
if (!is.null(raw_df) && !is.null(units_by) && !is.null(convo_by) && !is.null(codes_by)) {
|
| 154 | ! |
return(ena.accumulate.data( |
| 155 | ! |
units = raw_df[, units_by, drop = FALSE], |
| 156 | ! |
conversation = raw_df[, convo_by, drop = FALSE], |
| 157 | ! |
codes = raw_df[, codes_by, drop = FALSE], |
| 158 | ! |
window.size.back = best_window_size |
| 159 |
)) |
|
| 160 |
} |
|
| 161 | ||
| 162 | ! |
return(best_window_size) |
| 163 |
} |
|
| 164 | ||
| 165 |
# Re-run ena.accumulate.data() with the arguments `accum_object` was made with |
|
| 166 |
# (its `_function.params`), changing only window.size.back. Returns NULL when |
|
| 167 |
# those arguments aren't available (e.g. an accumulation from a file). |
|
| 168 |
# |
|
| 169 |
# The stored `_function.call` is never evaluated: it is the *outermost* call on |
|
| 170 |
# the stack when the set was made (a user's wrapper function, test_that(), a |
|
| 171 |
# Shiny observer, ...), so evaluating it re-runs arbitrary code, and for an |
|
| 172 |
# accumulation loaded from a file it is whatever call that file contains. |
|
| 173 |
# |
|
| 174 |
# The window argument is deliberately not called window.size.back: ena.set() |
|
| 175 |
# records the parameters of the first frame on the stack that has a variable |
|
| 176 |
# of that name, which must be ena.accumulate.data()'s, not this function's. |
|
| 177 |
rebuild_accumulation <- function(accum_object, window_size) {
|
|
| 178 | 7x |
params <- accum_object$`_function.params` |
| 179 | 7x |
arg_names <- setdiff(names(formals(ena.accumulate.data)), "...") |
| 180 | 7x |
args <- params[intersect(names(params), arg_names)] |
| 181 | ! |
if (!all(c("units", "conversation", "codes") %in% names(args))) return(NULL)
|
| 182 | 7x |
args[["window.size.back"]] <- window_size |
| 183 | 7x |
do.call(ena.accumulate.data, args) |
| 184 |
} |
| 1 |
#' Re-class matrix as ena.matrix |
|
| 2 |
#' |
|
| 3 |
#' @param x data.frame, data.table, or matrix to extend |
|
| 4 |
#' @param new.class Additional class to extend the matrix with, default: NULL |
|
| 5 |
#' |
|
| 6 |
#' @return Object of same st |
|
| 7 |
#' @export |
|
| 8 |
as.ena.matrix <- function(x, new.class = NULL) {
|
|
| 9 | 397x |
class(x) = c(new.class, "ena.matrix", class(x)) |
| 10 | 397x |
x |
| 11 |
} |
|
| 12 | ||
| 13 |
#' Re-class matrix as ena.metadata |
|
| 14 |
#' |
|
| 15 |
#' @param x data.frame, data.table, or matrix to extend |
|
| 16 |
#' |
|
| 17 |
#' @return Object of same st |
|
| 18 |
#' @export |
|
| 19 |
as.ena.metadata <- function(x) {
|
|
| 20 | 1392x |
if(is.factor(x)) {
|
| 21 | ! |
x = as.character(x) |
| 22 |
} |
|
| 23 | 1392x |
class(x) = c("ena.metadata", "character") # This fails in the $.ena.metadata if is extending character, class(x))
|
| 24 | 1392x |
x |
| 25 |
} |
|
| 26 |
as.ena.code <- function(x) {
|
|
| 27 | 698x |
if(is.factor(x)) {
|
| 28 | 1x |
x = as.character(x) |
| 29 |
} |
|
| 30 | 698x |
class(x) = c("ena.code", class(x))
|
| 31 | 698x |
x |
| 32 |
} |
|
| 33 |
as.ena.codes <- function(x) {
|
|
| 34 | 1614x |
if(is.factor(x)) {
|
| 35 | 1x |
x = as.character(x) |
| 36 |
} |
|
| 37 | 1614x |
class(x) = c("ena.codes", class(x))
|
| 38 | 1614x |
x |
| 39 |
} |
|
| 40 |
#' Re-class vector as ena.co.occurrence |
|
| 41 |
#' |
|
| 42 |
#' @param x Vector to re-class |
|
| 43 |
#' |
|
| 44 |
#' @return re-classed vector |
|
| 45 |
#' @export |
|
| 46 |
as.ena.co.occurrence <- function(x) {
|
|
| 47 | 9751x |
if(is.factor(x)) {
|
| 48 | 1x |
x = as.character(x) |
| 49 |
} |
|
| 50 | 9751x |
class(x) = c("ena.co.occurrence", class(x))
|
| 51 | 9751x |
x |
| 52 |
} |
|
| 53 |
#' Re-class vector as ena.dimension |
|
| 54 |
#' |
|
| 55 |
#' @param x Vector to re-class |
|
| 56 |
#' |
|
| 57 |
#' @return re-classed vector |
|
| 58 |
#' @export |
|
| 59 |
as.ena.dimension <- function(x) {
|
|
| 60 | 9827x |
if(is.factor(x)) {
|
| 61 | 1x |
x = as.character(x) |
| 62 |
} |
|
| 63 | 9827x |
class(x) = c("ena.dimension", class(x))
|
| 64 | 9827x |
x |
| 65 |
} |
| 1 |
## |
|
| 2 |
#' @title Compute summary statistic for groupings of units using given method (typically, mean) |
|
| 3 |
#' |
|
| 4 |
#' @description Computes summary statistics for groupings (given as vector) of units in ena data using given method (typically, mean); computes summary statistic for point locations and edge weights for each grouping |
|
| 5 |
#' |
|
| 6 |
#' @export |
|
| 7 |
#' |
|
| 8 |
#' @param enaset An \code{\link{ENAset}} or a vector of values to group.
|
|
| 9 |
#' @param by A vector of values the same length as units. Uses rotated points for group positions and normed data to get the group edge weights |
|
| 10 |
#' @param method A function that is used on grouped points. Default: mean(). If `enaset` is an ENAset, enaset$points.rotated will be groups using `mean` regardless of `method` provided |
|
| 11 |
#' @param names A vector of names to use for the results. Default: unique(by) |
|
| 12 |
#' |
|
| 13 |
#' @examples |
|
| 14 |
#' data(RS.data) |
|
| 15 |
#' |
|
| 16 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 17 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 18 |
#' |
|
| 19 |
#' accum = ena.accumulate.data( |
|
| 20 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 21 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 22 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 23 |
#' codes = RS.data[,codeNames], |
|
| 24 |
#' window.size.back = 4 |
|
| 25 |
#' ) |
|
| 26 |
#' |
|
| 27 |
#' set = ena.make.set( |
|
| 28 |
#' enadata = accum |
|
| 29 |
#' ) |
|
| 30 |
#' |
|
| 31 |
#' means = ena.group(set, "Condition") |
|
| 32 |
#' |
|
| 33 |
#' |
|
| 34 |
#' @return A list containing names, points, and edge weights for each of the unique groups formed by the function |
|
| 35 |
## |
|
| 36 |
ena.group <- function( |
|
| 37 |
enaset = NULL, |
|
| 38 |
by = NULL, |
|
| 39 |
method = mean, |
|
| 40 |
names = as.vector(unique(by)) |
|
| 41 |
) {
|
|
| 42 | 7x |
run.method = function(pts, m = method) {
|
| 43 | 12x |
to_matrix <- class(pts)[1]; |
| 44 | 12x |
points.dt = pts; |
| 45 | ||
| 46 | 12x |
if(is.logical(by)) {
|
| 47 | 2x |
points.dt.means = points.dt[by, { lapply(.SD, m) }, .SDcols = find_dimension_cols(points.dt) | find_code_cols(points.dt)];
|
| 48 | 2x |
if(length(names) == 1) {
|
| 49 | 2x |
points.dt.means[['ENA_GROUP_NAME']] <- as.ena.metadata(names) |
| 50 |
} |
|
| 51 |
} |
|
| 52 | 10x |
else if(all(by %in% colnames(pts))) {
|
| 53 | 2x |
points.dt.means <- points.dt[, |
| 54 | 4x |
{lapply(.SD, function(x) {
|
| 55 | 60x |
get(paste0("as.", class(x)[1]))(m(x))
|
| 56 |
})}, |
|
| 57 | 2x |
by = by, |
| 58 | 2x |
.SDcols = find_dimension_cols(points.dt) | find_code_cols(points.dt) |
| 59 |
]; |
|
| 60 | 2x |
points.dt.means[, ENA_GROUP_NAME := do.call(paste, c(.SD, sep = ".")) , .SDcols = c(by)] |
| 61 | 2x |
points.dt.means <- points.dt.means[, !find_meta_cols(points.dt.means), with = F] |
| 62 | 2x |
set(points.dt.means, j = "ENA_GROUP_NAME", value = as.ena.metadata(points.dt.means[["ENA_GROUP_NAME"]])) |
| 63 |
} |
|
| 64 |
else {
|
|
| 65 | 8x |
to_what <- get(paste0("as.", class(pts[[which(!find_meta_cols(pts))[1]]])[1]))
|
| 66 | 8x |
to_cols <- names(which(!find_meta_cols(pts))) |
| 67 | ||
| 68 | 8x |
points.dt.means = as.data.frame(aggregate(as.matrix(points.dt), by = list(by), FUN = m)) #"mean")) |
| 69 | 8x |
set(points.dt.means, j = "Group.1", value = as.ena.metadata(points.dt.means$Group.1)) |
| 70 | 8x |
colnames(points.dt.means)[colnames(points.dt.means) == "Group.1"] <- "ENA_GROUP_NAME" |
| 71 | 8x |
set(x = points.dt.means, j = to_cols, value = lapply(points.dt.means[, to_cols], to_what)) |
| 72 | 8x |
points.dt.means <- as.data.table(points.dt.means) |
| 73 | ||
| 74 |
# agg.df[as.vector(unique(group.by)),]u |
|
| 75 |
# return (points.dt.means[as.vector(unique(by)),]); |
|
| 76 | 8x |
return(as.ena.matrix(points.dt.means[which(points.dt.means$ENA_GROUP_NAME %in% unique(by)),], to_matrix)) |
| 77 |
} |
|
| 78 | ||
| 79 | 4x |
return(as.ena.matrix(points.dt.means, to_matrix)); |
| 80 |
} |
|
| 81 | ||
| 82 | 7x |
if(is.character(method)) {
|
| 83 | 1x |
method = get(method) |
| 84 |
} |
|
| 85 | ||
| 86 | 7x |
if(is(enaset, "ENAset")) {
|
| 87 | 1x |
enaset <- ena.set(enaset); |
| 88 |
} |
|
| 89 | ||
| 90 | 7x |
if (is(enaset, "ena.set")) {
|
| 91 | 5x |
pts <- run.method(enaset$points) |
| 92 | 5x |
return(list( |
| 93 | 5x |
"names" = pts$ENA_GROUP_NAME, |
| 94 | 5x |
"points" = pts, |
| 95 | 5x |
"line.weights" = run.method(enaset$line.weights) |
| 96 |
)); |
|
| 97 |
} |
|
| 98 |
else {
|
|
| 99 | 2x |
return(run.method(enaset)) |
| 100 |
} |
|
| 101 |
} |
| 1 |
#' Calculate ENA Moving Window Size via Cross-Covariance Decay (CCD) |
|
| 2 |
#' |
|
| 3 |
#' Calculates the recommended moving window size for Epistemic Network Analysis (ENA) |
|
| 4 |
#' by estimating the half-life decay lag of the noise-corrected Frobenius norm across |
|
| 5 |
#' pooled conversation cross-covariance matrices. |
|
| 6 |
#' |
|
| 7 |
#' @param x A \code{data.frame}, \code{data.table}, or an \code{ENAdata}/\code{ena.set} accumulation object.
|
|
| 8 |
#' @param codeNames A \code{character} vector of column names representing binary codes (optional if \code{x} is an ENA object).
|
|
| 9 |
#' @param conversation_cols A \code{character} vector of column names defining discrete conversations (optional if \code{x} is an ENA object).
|
|
| 10 |
#' @param max_window A \code{numeric} value indicating the maximum lag window size to evaluate. Default is \code{20}.
|
|
| 11 |
#' @param min_overlap A \code{numeric} value specifying the minimum required overlapping rows (\eqn{N - lag}) per conversation subset. Default is \code{10}.
|
|
| 12 |
#' |
|
| 13 |
#' @details |
|
| 14 |
#' The function computes pooled cross-covariance curves over lags \code{0:max_window}. It normalizes
|
|
| 15 |
#' the noise-corrected Frobenius norm relative to its peak value and identifies the first lag step strictly |
|
| 16 |
#' after the peak where the normalized norm falls below 0.5 (half-life threshold). |
|
| 17 |
#' |
|
| 18 |
#' @references |
|
| 19 |
#' Shaffer, D. W. & Cai, Z. (2026). Discourse Coherence Length: |
|
| 20 |
#' Noise-Corrected Covariance Estimation of Window-Size in Epistemic Network Analysis. |
|
| 21 |
#' International Conference on Quantitative Ethnography (ICQE26). |
|
| 22 |
#' |
|
| 23 |
#' @return An integer scalar representing the estimated half-life lag window size. |
|
| 24 |
#' |
|
| 25 |
#' @export |
|
| 26 |
#' |
|
| 27 |
#' @examples |
|
| 28 |
#' data(RS.data) |
|
| 29 |
#' codeNames <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 30 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", "Collaboration") |
|
| 31 |
#' w <- ena.ccd.window(RS.data, codeNames = codeNames, |
|
| 32 |
#' conversation_cols = c("Condition", "GroupName"))
|
|
| 33 |
#' print(w) |
|
| 34 |
ena.ccd.window <- function( |
|
| 35 |
x, |
|
| 36 |
codeNames = NULL, |
|
| 37 |
conversation_cols = NULL, |
|
| 38 |
max_window = 20, |
|
| 39 |
min_overlap = 10 |
|
| 40 |
) {
|
|
| 41 | 11x |
res <- ena.ccd( |
| 42 | 11x |
x = x, |
| 43 | 11x |
codeNames = codeNames, |
| 44 | 11x |
conversation_cols = conversation_cols, |
| 45 | 11x |
max_window = max_window, |
| 46 | 11x |
min_overlap = min_overlap |
| 47 |
) |
|
| 48 | 8x |
return(res$window_size) |
| 49 |
} |
|
| 50 | ||
| 51 |
#' Compute Cross-Covariance Decay Curves and Noise Floor |
|
| 52 |
#' |
|
| 53 |
#' Evaluates window-matched cross-covariance matrices and noise-floor corrected |
|
| 54 |
#' Frobenius norms across multiple conversation subsets over a range of lag steps. |
|
| 55 |
#' |
|
| 56 |
#' @param x A \code{data.frame}, \code{data.table}, or an \code{ENAdata}/\code{ena.set} accumulation object.
|
|
| 57 |
#' @param codeNames A \code{character} vector of column names corresponding to ENA codes.
|
|
| 58 |
#' @param conversation_cols A \code{character} vector of column names defining discrete conversations.
|
|
| 59 |
#' @param max_window An \code{integer} specifying the maximum lag to evaluate. Default is \code{20}.
|
|
| 60 |
#' @param min_overlap An \code{integer} specifying the minimum effective row overlap required per conversation at lag \eqn{l}. Default is \code{10}.
|
|
| 61 |
#' |
|
| 62 |
#' @return An S3 object of class \code{ena.ccd} containing:
|
|
| 63 |
#' \describe{
|
|
| 64 |
#' \item{window_size}{The estimated optimal window size (integer).}
|
|
| 65 |
#' \item{peak_lag}{The lag corresponding to the peak cross-covariance norm.}
|
|
| 66 |
#' \item{curves}{A \code{data.frame} of pooled cross-covariance metrics by lag.}
|
|
| 67 |
#' \item{codeNames}{The evaluated code names.}
|
|
| 68 |
#' \item{conversation_cols}{The conversation column names.}
|
|
| 69 |
#' } |
|
| 70 |
#' |
|
| 71 |
#' @export |
|
| 72 |
#' |
|
| 73 |
#' @examples |
|
| 74 |
#' data(RS.data) |
|
| 75 |
#' codeNames <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 76 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", "Collaboration") |
|
| 77 |
#' ccd_res <- ena.ccd(RS.data, codeNames = codeNames, |
|
| 78 |
#' conversation_cols = c("Condition", "GroupName"))
|
|
| 79 |
#' print(ccd_res) |
|
| 80 |
#' plot(ccd_res) |
|
| 81 |
ena.ccd <- function( |
|
| 82 |
x, |
|
| 83 |
codeNames = NULL, |
|
| 84 |
conversation_cols = NULL, |
|
| 85 |
max_window = 20, |
|
| 86 |
min_overlap = 10 |
|
| 87 |
) {
|
|
| 88 |
# 1. Extract from ENAdata / ena.set if provided |
|
| 89 | 14x |
if (inherits(x, c("ENAdata", "ena.set", "ENAset", "rENA.data", "rENA.set"))) {
|
| 90 | 4x |
if (is.null(codeNames)) {
|
| 91 | 4x |
if (!is.null(x$model$codes)) {
|
| 92 | ! |
codeNames <- x$model$codes |
| 93 | 4x |
} else if (!is.null(x$codes)) {
|
| 94 | ! |
codeNames <- if (is.data.frame(x$codes) || is.matrix(x$codes)) colnames(x$codes) else as.character(x$codes) |
| 95 | 4x |
} else if (!is.null(x$`_function.params`$codes)) {
|
| 96 | 4x |
codeNames <- if (is.data.frame(x$`_function.params`$codes) || is.matrix(x$`_function.params`$codes)) {
|
| 97 | 4x |
colnames(x$`_function.params`$codes) |
| 98 |
} else {
|
|
| 99 | ! |
as.character(x$`_function.params`$codes) |
| 100 |
} |
|
| 101 |
} |
|
| 102 |
} |
|
| 103 | 4x |
if (is.null(conversation_cols)) {
|
| 104 | 4x |
if (!is.null(x$conversations.by)) {
|
| 105 | ! |
conversation_cols <- x$conversations.by |
| 106 | 4x |
} else if (!is.null(x$model$conversations.by)) {
|
| 107 | ! |
conversation_cols <- x$model$conversations.by |
| 108 | 4x |
} else if (!is.null(x$`_function.params`$conversations.by)) {
|
| 109 | 4x |
conversation_cols <- x$`_function.params`$conversations.by |
| 110 |
} |
|
| 111 |
} |
|
| 112 | 4x |
raw_df <- if (!is.null(x$model$raw.input)) {
|
| 113 | 4x |
as.data.frame(x$model$raw.input) |
| 114 | 4x |
} else if (!is.null(x$raw)) {
|
| 115 | ! |
as.data.frame(x$raw) |
| 116 | 4x |
} else if (!is.null(x$data)) {
|
| 117 | ! |
as.data.frame(x$data) |
| 118 | 4x |
} else if (!is.null(x$`_function.params`$data)) {
|
| 119 | ! |
as.data.frame(x$`_function.params`$data) |
| 120 |
} else {
|
|
| 121 | ! |
NULL |
| 122 |
} |
|
| 123 | 10x |
} else if (is.data.frame(x)) {
|
| 124 | 10x |
raw_df <- as.data.frame(x) |
| 125 |
} else {
|
|
| 126 | ! |
stop("'x' must be a data.frame or an ENA accumulation/set object.")
|
| 127 |
} |
|
| 128 | ||
| 129 | 14x |
if (is.null(raw_df)) {
|
| 130 | ! |
stop("Could not extract raw data from the provided ENA object.")
|
| 131 |
} |
|
| 132 | ||
| 133 |
# Deduplicate columns in raw_df if present (e.g. from intersecting units/convo specifications) |
|
| 134 | 14x |
if (any(duplicated(colnames(raw_df)))) {
|
| 135 | 4x |
raw_df <- raw_df[, !duplicated(colnames(raw_df)), drop = FALSE] |
| 136 |
} |
|
| 137 | ||
| 138 | 14x |
if (is.null(codeNames) || length(codeNames) == 0) {
|
| 139 | ! |
stop("'codeNames' must be specified.")
|
| 140 |
} |
|
| 141 | 14x |
if (is.null(conversation_cols) || length(conversation_cols) == 0) {
|
| 142 | ! |
stop("'conversation_cols' must be specified.")
|
| 143 |
} |
|
| 144 | ||
| 145 |
# 2. Validate columns existence |
|
| 146 | 14x |
missing_cols <- setdiff(c(conversation_cols, codeNames), colnames(raw_df)) |
| 147 | 14x |
if (length(missing_cols) > 0) {
|
| 148 | 2x |
stop("The following columns were not found in 'x': ", paste(missing_cols, collapse = ", "))
|
| 149 |
} |
|
| 150 | ||
| 151 |
# 3. Numeric matrix conversion & conversation splitting |
|
| 152 |
# Factors and characters are converted by their labels ("0"/"1" -> 0/1);
|
|
| 153 |
# data.matrix() would use factor level positions (1/2) instead. Missing or |
|
| 154 |
# non-numeric values are an error rather than a silent 0. |
|
| 155 | 12x |
code_mat <- vapply(raw_df[, codeNames, drop = FALSE], function(col) {
|
| 156 | 2x |
if (is.factor(col)) col <- as.character(col) |
| 157 | 68x |
suppressWarnings(as.numeric(col)) |
| 158 | 12x |
}, numeric(nrow(raw_df))) |
| 159 | 12x |
code_mat <- matrix(code_mat, nrow = nrow(raw_df), dimnames = list(NULL, codeNames)) |
| 160 | 12x |
bad <- codeNames[colSums(is.na(code_mat)) > 0] |
| 161 | 12x |
if (length(bad)) {
|
| 162 | 1x |
stop("Code columns have missing or non-numeric values: ", paste(bad, collapse = ", "))
|
| 163 |
} |
|
| 164 | ||
| 165 | 11x |
conv_factor <- interaction(raw_df[conversation_cols], drop = TRUE, lex.order = FALSE) |
| 166 | 11x |
x_subsets <- split.data.frame(code_mat, conv_factor) |
| 167 | ||
| 168 | 11x |
lags <- 0:max_window |
| 169 | ||
| 170 |
# Pre-filter conversation subsets by length |
|
| 171 | 11x |
sub_lens <- vapply(x_subsets, nrow, integer(1)) |
| 172 | 11x |
valid_idx <- which(sub_lens >= min_overlap) |
| 173 | ||
| 174 | 11x |
if (length(valid_idx) == 0) {
|
| 175 | 1x |
warning(paste0("No conversation subsets had length >= min_overlap (", min_overlap, "). Returning window size = 1."))
|
| 176 | 1x |
out_curves <- data.frame( |
| 177 | 1x |
lag = lags, |
| 178 | 1x |
frob = NA_real_, |
| 179 | 1x |
frob_sq_unbiased = NA_real_, |
| 180 | 1x |
frob_unbiased_signed = NA_real_, |
| 181 | 1x |
total_weight = 0, |
| 182 | 1x |
stringsAsFactors = FALSE |
| 183 |
) |
|
| 184 | 1x |
res <- list( |
| 185 | 1x |
window_size = 1L, |
| 186 | 1x |
peak_lag = 0L, |
| 187 | 1x |
curves = out_curves, |
| 188 | 1x |
codeNames = codeNames, |
| 189 | 1x |
conversation_cols = conversation_cols |
| 190 |
) |
|
| 191 | 1x |
class(res) <- "ena.ccd" |
| 192 | 1x |
return(res) |
| 193 |
} |
|
| 194 | ||
| 195 | 10x |
x_subsets <- x_subsets[valid_idx] |
| 196 | ||
| 197 |
# 4. Delegate the cross-covariance decay computation to the shared libena |
|
| 198 |
# kernel (qe::ccd_window, src/libena_rcpp.cpp). The numeric core lives in C++ so that R, the |
|
| 199 |
# WASM build, and the Python package (ena) all share a single implementation; only the |
|
| 200 |
# data.frame wrangling and S3 assembly stay here. |
|
| 201 | 10x |
kern <- ccd_window( |
| 202 | 10x |
lapply(x_subsets, function(s) matrix(as.numeric(s), nrow = nrow(s))), |
| 203 | 10x |
max_window = max_window, |
| 204 | 10x |
min_overlap = min_overlap |
| 205 |
) |
|
| 206 | ||
| 207 | 10x |
pooled_out <- data.frame( |
| 208 | 10x |
lag = kern$lag, |
| 209 | 10x |
frob = kern$frob, |
| 210 | 10x |
frob_sq_unbiased = kern$frob_sq_unbiased, |
| 211 | 10x |
frob_unbiased_signed = kern$frob_unbiased_signed, |
| 212 | 10x |
total_weight = kern$total_weight, |
| 213 | 10x |
stringsAsFactors = FALSE |
| 214 |
) |
|
| 215 | ||
| 216 | 10x |
best_window <- as.integer(kern$window_size) |
| 217 | 10x |
peak_lag <- as.integer(kern$peak_lag) |
| 218 | ||
| 219 |
# 5. Preserve the informational warnings the pure-R implementation emitted. |
|
| 220 |
# (The window size / peak lag themselves are decided inside the kernel.) |
|
| 221 | 10x |
f <- pooled_out$frob_unbiased_signed |
| 222 | 10x |
if (peak_lag == 0L) {
|
| 223 | ! |
warning("Corrected Cross Covariance is non-positive or all NA across evaluated lags. Defaulting to window size = 1.")
|
| 224 |
} else {
|
|
| 225 | 10x |
max_f <- f[pooled_out$lag == peak_lag] |
| 226 | 10x |
after <- pooled_out$lag >= peak_lag |
| 227 | 10x |
if (!any((f[after] / max_f) <= 0.5, na.rm = TRUE)) {
|
| 228 | 1x |
warning(paste0("Cross-covariance did not decay below half-life (50%) within max_window = ", max_window, "."))
|
| 229 |
} |
|
| 230 |
} |
|
| 231 | ||
| 232 | 10x |
res <- list( |
| 233 | 10x |
window_size = best_window, |
| 234 | 10x |
peak_lag = peak_lag, |
| 235 | 10x |
curves = pooled_out, |
| 236 | 10x |
codeNames = codeNames, |
| 237 | 10x |
conversation_cols = conversation_cols |
| 238 |
) |
|
| 239 | 10x |
class(res) <- "ena.ccd" |
| 240 | 10x |
return(res) |
| 241 |
} |
|
| 242 | ||
| 243 |
#' @export |
|
| 244 |
print.ena.ccd <- function(x, ...) {
|
|
| 245 | 1x |
cat("ENA Cross-Covariance Decay (CCD) Window Size Estimation\n")
|
| 246 | 1x |
cat("------------------------------------------------------\n")
|
| 247 | 1x |
cat("Estimated Window Size :", x$window_size, "\n")
|
| 248 | 1x |
cat("Peak Lag :", x$peak_lag, "\n")
|
| 249 | 1x |
cat("Codes Evaluated :", paste(x$codeNames, collapse = ", "), "\n")
|
| 250 | 1x |
cat("Conversations By :", paste(x$conversation_cols, collapse = ", "), "\n")
|
| 251 | 1x |
invisible(x) |
| 252 |
} |
|
| 253 | ||
| 254 |
#' Plot Cross-Covariance Decay Curve |
|
| 255 |
#' |
|
| 256 |
#' Plots the noise-corrected Frobenius norm across lags with the half-life threshold marker. |
|
| 257 |
#' |
|
| 258 |
#' @param x An object of class \code{ena.ccd}.
|
|
| 259 |
#' @param ... Additional arguments passed to \code{plot}.
|
|
| 260 |
#' |
|
| 261 |
#' @return The \code{ena.ccd} object invisibly.
|
|
| 262 |
#' @export |
|
| 263 |
plot.ena.ccd <- function(x, ...) {
|
|
| 264 | 1x |
df <- x$curves |
| 265 | 1x |
valid <- !is.na(df$frob_unbiased_signed) |
| 266 | 1x |
df_sub <- df[valid, ] |
| 267 | ||
| 268 | 1x |
if (nrow(df_sub) == 0) {
|
| 269 | ! |
message("No valid cross-covariance curve points to plot.")
|
| 270 | ! |
return(invisible(x)) |
| 271 |
} |
|
| 272 | ||
| 273 | 1x |
ylim <- range(c(0, df_sub$frob_unbiased_signed, df_sub$frob), na.rm = TRUE) |
| 274 | 1x |
graphics::plot( |
| 275 | 1x |
df_sub$lag, df_sub$frob_unbiased_signed, |
| 276 | 1x |
type = "b", pch = 19, col = "royalblue", |
| 277 | 1x |
xlab = "Lag (Window Size)", |
| 278 | 1x |
ylab = "Frobenius Norm", |
| 279 | 1x |
main = paste0("Cross-Covariance Decay (Estimated Window = ", x$window_size, ")"),
|
| 280 | 1x |
ylim = ylim, |
| 281 |
... |
|
| 282 |
) |
|
| 283 | 1x |
graphics::lines(df_sub$lag, df_sub$frob, type = "l", lty = 2, col = "gray50") |
| 284 | 1x |
graphics::abline(v = x$window_size, col = "firebrick", lty = 2, lwd = 2) |
| 285 | 1x |
graphics::legend( |
| 286 | 1x |
"topright", |
| 287 | 1x |
legend = c("Corrected Norm", "Uncorrected Norm", paste0("Chosen Window (", x$window_size, ")")),
|
| 288 | 1x |
col = c("royalblue", "gray50", "firebrick"),
|
| 289 | 1x |
lty = c(1, 2, 2), pch = c(19, NA, NA), lwd = c(1, 1, 2), |
| 290 | 1x |
bty = "n" |
| 291 |
) |
|
| 292 | 1x |
invisible(x) |
| 293 |
} |
| 1 |
#' Find metadata columns |
|
| 2 |
#' |
|
| 3 |
#' @param x data.table (or frame) to search for columns of class ena.metadata |
|
| 4 |
#' |
|
| 5 |
#' @return logical vector |
|
| 6 |
#' @export |
|
| 7 |
find_meta_cols <- function(x) {
|
|
| 8 | 2496x |
sapply(x, is, class2 = "ena.metadata") |
| 9 |
} |
|
| 10 | ||
| 11 |
#' Find code columns |
|
| 12 |
#' |
|
| 13 |
#' @param x data.table (or frame) to search for columns of class ena.co.occurrence |
|
| 14 |
#' |
|
| 15 |
#' @return logical vector |
|
| 16 |
#' @export |
|
| 17 |
find_code_cols <- function(x) {
|
|
| 18 | 5x |
grepl("adjacency.code", x = names(x)) | sapply(x, function(col) {
|
| 19 | 90x |
is(col, class2 = "ena.co.occurrence") |
| 20 |
}) |
|
| 21 |
} |
|
| 22 | ||
| 23 |
#' Find Binary Columns |
|
| 24 |
#' |
|
| 25 |
#' Identifies columns in a data.frame or data.table that are binary (i.e., contain only two unique values), optionally including logical columns. |
|
| 26 |
#' |
|
| 27 |
#' @param x A data.frame or data.table to search for binary columns. |
|
| 28 |
#' @param include_logical Logical. If TRUE, logical columns are also considered binary. Default is FALSE. |
|
| 29 |
#' |
|
| 30 |
#' @return A character vector of column names that are binary, or NULL if none are found. |
|
| 31 |
#' @export |
|
| 32 |
#' |
|
| 33 |
#' @examples |
|
| 34 |
#' df <- data.frame(a = c(0, 1, 1), b = c(TRUE, FALSE, TRUE), c = c(1, 2, 3)) |
|
| 35 |
#' find_binary_cols(df) |
|
| 36 |
#' find_binary_cols(df, include_logical = TRUE) |
|
| 37 |
find_binary_cols <- function(x, include_logical = FALSE) {
|
|
| 38 | ! |
nm <- colnames(x)[sapply(x, is_binary_col, include_logical)]; |
| 39 | ! |
if(length(nm) > 0) nm else NULL; |
| 40 |
} |
|
| 41 | ||
| 42 |
#' Find dimension columns |
|
| 43 |
#' |
|
| 44 |
#' @param x data.table (or frame) to search for columns of class ena.dimension |
|
| 45 |
#' |
|
| 46 |
#' @return logical vector |
|
| 47 |
#' @export |
|
| 48 |
find_dimension_cols <- function(x) {
|
|
| 49 | 6x |
sapply(x, is, class2 = "ena.dimension") |
| 50 |
} |
|
| 51 | ||
| 52 |
#' Remove meta columns from data.table |
|
| 53 |
#' |
|
| 54 |
#' @param x [TBD] |
|
| 55 |
#' |
|
| 56 |
#' @return data.table withe columns of class ena.meta.data removed |
|
| 57 |
#' @export |
|
| 58 |
remove_meta_data <- function(x) {
|
|
| 59 | 1669x |
as.data.frame(x)[, !find_meta_cols(x), drop = F] |
| 60 |
} |
|
| 61 | ||
| 62 |
#' Extract metadata easily |
|
| 63 |
#' |
|
| 64 |
#' @param x [TBD] |
|
| 65 |
#' @param i [TBD] |
|
| 66 |
#' |
|
| 67 |
#' @return [TBD] |
|
| 68 |
#' @export |
|
| 69 |
"$.ena.metadata" <- function(x, i) {
|
|
| 70 |
#browser() |
|
| 71 | 22x |
parts <- unlist(strsplit( |
| 72 | 22x |
x = as.character(sys.call())[2], split = "\\$" |
| 73 | 22x |
))[1:2] |
| 74 | ||
| 75 | 22x |
set <- get(parts[1], envir = parent.frame()) |
| 76 | 22x |
m <- set[[parts[2]]][x == i, ] |
| 77 | 22x |
m |
| 78 |
} |
|
| 79 | ||
| 80 |
#' Extract line.weignts easily |
|
| 81 |
#' |
|
| 82 |
#' @param x [TBD] |
|
| 83 |
#' @param i [TBD] |
|
| 84 |
#' |
|
| 85 |
#' @return [TBD] |
|
| 86 |
#' @export |
|
| 87 |
"$.line.weights" <- function (x, i) {
|
|
| 88 | 1x |
vals <- x[[which(colnames(x) == i)]] |
| 89 | ||
| 90 | 1x |
vals |
| 91 |
} |
|
| 92 | ||
| 93 |
#' Extract points easily |
|
| 94 |
#' |
|
| 95 |
#' @param x [TBD] |
|
| 96 |
#' @param i [TBD] |
|
| 97 |
#' |
|
| 98 |
#' @return [TBD] |
|
| 99 |
#' @export |
|
| 100 |
"$.ena.points" <- function (x, i) {
|
|
| 101 |
# Match base `$`: an absent column is NULL, not an error. Without this, |
|
| 102 |
# which() returns integer(0) and x[[integer(0)]] raises "attempt to select |
|
| 103 |
# less than one element in get1index", so callers cannot test for an |
|
| 104 |
# optional column (e.g. ENA_DIRECTION) the ordinary way. Duplicate column |
|
| 105 |
# names take the first match rather than recursively indexing on a vector. |
|
| 106 | 109x |
idx <- which(colnames(x) == i) |
| 107 | 109x |
if (length(idx) == 0) {
|
| 108 | ! |
return(NULL) |
| 109 |
} |
|
| 110 | 109x |
vals <- x[[idx[1]]] |
| 111 | ||
| 112 | 109x |
vals |
| 113 |
} |
|
| 114 | ||
| 115 |
#' Extract from ena.matrix easily using metadata |
|
| 116 |
#' |
|
| 117 |
#' @param x [TBD] |
|
| 118 |
#' @param i [TBD] |
|
| 119 |
#' |
|
| 120 |
#' @return [TBD] |
|
| 121 |
#' @export |
|
| 122 |
"$.ena.matrix" <- function (x, i) {
|
|
| 123 |
# See `$.ena.points` above: absent column is NULL, duplicates take the first. |
|
| 124 | 42x |
idx <- which(colnames(x) == i) |
| 125 | 42x |
if (length(idx) == 0) {
|
| 126 | ! |
return(NULL) |
| 127 |
} |
|
| 128 | 42x |
vals <- x[[idx[1]]] |
| 129 | ||
| 130 | 42x |
vals |
| 131 |
} |
|
| 132 | ||
| 133 |
#' Multiply ena.matrix objects |
|
| 134 |
#' Element-wise multiplication of dimension columns in an ena.matrix by another ena.matrix or numeric matrix. |
|
| 135 |
#' If e2 is an ena.matrix, it is converted to a standard matrix before multiplication. |
|
| 136 |
#' The multiplication is applied only to the dimension columns of e1, while other columns remain |
|
| 137 |
#' unchanged. |
|
| 138 |
#' |
|
| 139 |
#' @param e1 An ena.matrix object whose dimension columns will be multiplied. |
|
| 140 |
#' @param e2 An ena.matrix or numeric matrix to multiply with the dimension columns of |
|
| 141 |
#' e1. |
|
| 142 |
#' |
|
| 143 |
#' @return An ena.matrix object with the dimension columns of e1 multiplied by e2. |
|
| 144 |
#' @exportS3Method "*" ena.matrix |
|
| 145 |
"*.ena.matrix" <- function (e1, e2) {
|
|
| 146 | ! |
e2m <- e2 |
| 147 | ! |
if(is(e2, "ena.matrix")) {
|
| 148 | ! |
e2m <- as.matrix(e2) |
| 149 |
} |
|
| 150 |
|
|
| 151 | ! |
dim_cols <- colnames(e1)[find_dimension_cols(e1)] |
| 152 | ! |
e1[, (dim_cols) := Map(function(col, mult) col * mult, .SD, as.data.frame(e2m)), .SDcols = dim_cols] |
| 153 |
} |
|
| 154 | ||
| 155 |
# "$.ena.plot" <- function(x, i) {
|
|
| 156 |
# browser() |
|
| 157 |
# } |
|
| 158 |
# "[[.ena.plot" <- function(x, i) {
|
|
| 159 |
# browser() |
|
| 160 |
# } |
|
| 161 |
#' @export |
|
| 162 |
.DollarNames.ena.metadata <- function(x, pattern = "") {
|
|
| 163 | 1x |
unique(x) |
| 164 |
} |
|
| 165 | ||
| 166 |
# "[.ena.matrix" = function(x, ...) |
|
| 167 |
# {
|
|
| 168 |
# browser() |
|
| 169 |
# original.class = class(x)[1] |
|
| 170 |
# class(x) = class(x)[-1] |
|
| 171 |
# x = x[...] |
|
| 172 |
# |
|
| 173 |
# # y = as.data.frame(x) |
|
| 174 |
# } |
|
| 175 | ||
| 176 |
#' @export |
|
| 177 |
summary.ena.set <- function(object, ...) {
|
|
| 178 | 1x |
x <- object |
| 179 | 1x |
print_dims <- function(n = 2) {
|
| 180 | 2x |
cat("\t", paste("Dimension", 1:n, collapse = "\t"), "\n")
|
| 181 |
} |
|
| 182 | 1x |
cat("Units: ", nrow(x$points), "\t\t")
|
| 183 | 1x |
cat("Codes: ", length(x$rotation$codes), "\n")
|
| 184 | ||
| 185 | 1x |
cat("Variance: \n")
|
| 186 | 1x |
print_dims() |
| 187 | 1x |
cat("\t", paste(round(x$model$variance[1:2], 3), collapse = "\t\t"), "\n\n")
|
| 188 | ||
| 189 | 1x |
cat("Eigenvalues: \n")
|
| 190 | 1x |
print_dims() |
| 191 | 1x |
cat("\t", paste(round(
|
| 192 | 1x |
x$rotation$eigenvalues[1:2], 3), collapse = "\t\t"), "\n\n") |
| 193 | ||
| 194 | 1x |
cat("Correlations: \n")
|
| 195 | 1x |
cors <- ena.correlations(x) |
| 196 | 1x |
rownames(cors) <- paste("Dimension", 1:2)
|
| 197 | 1x |
print(cors) |
| 198 |
} |
|
| 199 |
# as.data.frame.ena.connections <- function(x) {
|
|
| 200 |
# class(x) = class(x)[-1] |
|
| 201 |
# y = as.data.frame(x) |
|
| 202 |
# y |
|
| 203 |
# } |
|
| 204 |
# format.co.occurrence = format.metadata = function(x, justify = "none") {
|
|
| 205 |
# y = as.character(x) |
|
| 206 |
# format(y, justify = justify) |
|
| 207 |
# } |
|
| 208 | ||
| 209 |
#' Title |
|
| 210 |
#' |
|
| 211 |
#' @param x [TBD] |
|
| 212 |
#' @param ... [TBD] |
|
| 213 |
#' @param plot [TBD] |
|
| 214 |
#' @param set [TBD] |
|
| 215 |
#' |
|
| 216 |
#' @return [TBD] |
|
| 217 |
#' @export |
|
| 218 |
print.ena.set <- function(x, ..., plot = FALSE, set = TRUE) {
|
|
| 219 | 1x |
x.unclass <- unclass(x) |
| 220 | ||
| 221 |
if( |
|
| 222 | 1x |
!is.null(x.unclass$`_plot_op`) && |
| 223 | 1x |
x.unclass$`_plot_op` == T |
| 224 |
) {
|
|
| 225 | ! |
base::print(x.unclass$plots) |
| 226 |
} |
|
| 227 |
else {
|
|
| 228 | 1x |
if(plot == FALSE) {
|
| 229 | 1x |
x.unclass$plots <- NULL |
| 230 |
} |
|
| 231 | 1x |
base::print(x.unclass) |
| 232 |
} |
|
| 233 | ||
| 234 | 1x |
invisible(x); |
| 235 |
} |
|
| 236 | ||
| 237 |
#' Title |
|
| 238 |
#' |
|
| 239 |
#' @param x [TBD] |
|
| 240 |
#' @param by [TBD] |
|
| 241 |
#' @param model [TBD] |
|
| 242 |
#' @param ... [TBD] |
|
| 243 |
#' |
|
| 244 |
#' @return [TBD] |
|
| 245 |
#' @export |
|
| 246 |
as_trajectory <- function(x, |
|
| 247 |
by = x$`_function.params`$conversation[1], |
|
| 248 |
model = c("AccumulatedTrajectory", "SeperateTrajectory"),
|
|
| 249 |
... |
|
| 250 |
) {
|
|
| 251 | 2x |
model = match.arg(model) |
| 252 | 2x |
orig_args = x$`_function.params` |
| 253 | 2x |
orig_args$model = model |
| 254 | ||
| 255 | 2x |
more_args <- list(...) |
| 256 | 2x |
for(arg in names(more_args)) {
|
| 257 | 1x |
orig_args[[arg]] <- more_args[[arg]] |
| 258 |
} |
|
| 259 |
#c(mean, more.args[!names(more.args) %in% names(mean)]) |
|
| 260 | ||
| 261 | 2x |
do.call(ena, orig_args) |
| 262 |
} |
|
| 263 | ||
| 264 |
#' Title |
|
| 265 |
#' |
|
| 266 |
#' @param x [TBD] |
|
| 267 |
#' @param by [TBD] |
|
| 268 |
#' @param ... [TBD] |
|
| 269 |
#' |
|
| 270 |
#' @return [TBD] |
|
| 271 |
#' @export |
|
| 272 |
project_in <- function(x, by = NULL, ...) {
|
|
| 273 | 5x |
if(is.null(by)) {
|
| 274 | 1x |
stop("A second parameter (ena.set or rotation.set) is required")
|
| 275 |
} |
|
| 276 | ||
| 277 | 4x |
rotation.set <- NULL |
| 278 | 4x |
if(is(by, "ena.set")) {
|
| 279 | 2x |
rotation.set <- by$rotation |
| 280 | 2x |
} else if(is(by, "ena.rotation.set")) {
|
| 281 | 2x |
rotation.set <- by |
| 282 |
} |
|
| 283 | ||
| 284 | 4x |
if(!identical(x$rotation$adjacency.key, rotation.set$adjacency.key)) {
|
| 285 | 1x |
stop("Rotation sets must have identical adjacency keys")
|
| 286 |
} |
|
| 287 | ||
| 288 | 3x |
x$rotation.matrix <- rotation.set$rotation.matrix |
| 289 | 3x |
x$rotation$rotation.matrix <- rotation.set$rotation.matrix |
| 290 | 3x |
x$rotation$nodes <- rotation.set$nodes; |
| 291 | 3x |
x$rotation$eigenvalues <- rotation.set$eigenvalues |
| 292 | ||
| 293 | 3x |
points <- as.matrix(x$model$points.for.projection) %*% as.matrix(x$rotation.matrix) |
| 294 | 3x |
points.dt <- as.data.table(points) |
| 295 | 3x |
for (i in seq(ncol(points.dt))) {
|
| 296 | 45x |
set(points.dt, j = i, value = as.ena.dimension(points.dt[[i]])) |
| 297 |
} |
|
| 298 | 3x |
if(grepl(x = x$model$model.type, pattern = "Trajectory")) {
|
| 299 | 1x |
x$points <- cbind(x$trajectories, points.dt) |
| 300 |
} else {
|
|
| 301 | 2x |
x$points <- cbind(x$meta.data, points.dt) |
| 302 |
} |
|
| 303 | 3x |
x$points <- as.ena.matrix(x$points, "ena.points") |
| 304 | ||
| 305 | 3x |
.return(x, invisible = T) |
| 306 |
} |
|
| 307 | ||
| 308 |
#' Title |
|
| 309 |
#' |
|
| 310 |
#' @param x [TBD] |
|
| 311 |
#' @param on [TBD] |
|
| 312 |
#' |
|
| 313 |
#' @return [TBD] |
|
| 314 |
#' @export |
|
| 315 |
means_rotate <- function(x, on = NULL) {
|
|
| 316 | 3x |
groupVar = NULL |
| 317 | 3x |
groups = NULL |
| 318 | 3x |
if(is.null(on)) {
|
| 319 | 1x |
col_counts = as.numeric(x$model$raw.input[, lapply(.SD, function(s) {
|
| 320 | 2x |
length(unique(s)) |
| 321 |
}), |
|
| 322 | 1x |
.SDcols = c(x$`_function.params`$units) |
| 323 |
]) |
|
| 324 | 1x |
groupVar = x$`_function.params`$units[order(col_counts) == 1] |
| 325 | 1x |
group_vars = unique(x$model$raw.input[[groupVar]]) |
| 326 | 1x |
if(!is.null(levels(group_vars))) {
|
| 327 | ! |
groups = levels(group_vars)[1:2] |
| 328 |
} |
|
| 329 |
else {
|
|
| 330 | 1x |
groups = group_vars[1:2] |
| 331 |
} |
|
| 332 |
# on_grps = list() |
|
| 333 |
# on_grps[[on]] = sapply(on_vals, function(v) {
|
|
| 334 |
# x$meta.data[[on]] == v |
|
| 335 |
# }, simplify = F) |
|
| 336 | 2x |
} else if(!is.null(names(on))) {
|
| 337 | 1x |
groupVar = names(on) |
| 338 | 1x |
groups = on[[groupVar]] |
| 339 |
} |
|
| 340 | ||
| 341 | 3x |
if(is.null(groupVar) || is.null(groups)) {
|
| 342 | 1x |
stop("Unable to determine groups for rotation.")
|
| 343 |
} |
|
| 344 | ||
| 345 | 2x |
orig_args <- x$`_function.params` |
| 346 | 2x |
orig_args$groupVar = groupVar |
| 347 | 2x |
orig_args$groups = groups |
| 348 | 2x |
new_set <- do.call(ena, orig_args) |
| 349 | 2x |
new_set$plots <- x$plots |
| 350 | 2x |
invisible(new_set) |
| 351 |
} |
|
| 352 | ||
| 353 |
.return <- function(x, invisible = T, from_plot = F) {
|
|
| 354 | 3x |
x$`_plot_op` = from_plot |
| 355 | 3x |
if(isTRUE(from_plot)) {
|
| 356 |
|
|
| 357 |
} |
|
| 358 | ||
| 359 | 3x |
if(invisible == T) {
|
| 360 | 3x |
invisible(x) |
| 361 |
} |
|
| 362 |
else {
|
|
| 363 | ! |
return(x) |
| 364 |
} |
|
| 365 |
} |
|
| 366 | ||
| 367 |
is_logical_col <- function(col) {
|
|
| 368 | ! |
n_cols = col == TRUE | col == FALSE; |
| 369 | ! |
is_col <- is.logical(col) & all(n_cols); |
| 370 | ||
| 371 | ! |
return(is_col); |
| 372 |
} |
|
| 373 | ||
| 374 |
is_binary_col <- function(col, include_logical = TRUE) {
|
|
| 375 | ! |
n_cols = col == 1 | col == 0; |
| 376 | ! |
is_col <- is.numeric(col) && all(is.wholenumber(col) & all(n_cols)); |
| 377 | ||
| 378 | ! |
if(isTRUE(include_logical)) {
|
| 379 | ! |
is_col <- is_col | is_logical_col(col); |
| 380 |
} |
|
| 381 | ||
| 382 | ! |
return(is_col); |
| 383 |
} |
|
| 384 | ||
| 385 | ! |
is.wholenumber <- function(x, tol = .Machine$double.eps^0.5) abs(x - round(x)) < tol |
| 386 | ||
| 387 | ||
| 388 |
#' Extract points easily |
|
| 389 |
#' |
|
| 390 |
# @param x [TBD] |
|
| 391 |
# @param i [TBD] |
|
| 392 |
# @param j [TBD] |
|
| 393 |
# @param ... Passed to `[.data.table` |
|
| 394 |
# @param with.meta logical, currently defaults to TRUE, which includes the metadata columns. |
|
| 395 |
# |
|
| 396 |
# @return [TBD] |
|
| 397 |
# @export |
|
| 398 |
# "[.ena.matrix" <- function (x, i, j, by, keyby, ..., with.meta = TRUE) {
|
|
| 399 |
# orig.class <- class(x) |
|
| 400 |
# x.unclass <- data.table::as.data.table(unclass(x)) |
|
| 401 |
# |
|
| 402 |
# if(with.meta == FALSE) {
|
|
| 403 |
# x.nometa <- x.unclass[, !find_meta_cols(x.unclass), with = F] |
|
| 404 |
# x_ <- x.nometa[i, ..j, ...] |
|
| 405 |
# } |
|
| 406 |
# else {
|
|
| 407 |
# x_ <- x.unclass[i, j, by = by, keyby = keyby, ...] |
|
| 408 |
# # if (!is.null(j)) {
|
|
| 409 |
# # x_ <- x_[, ..j] |
|
| 410 |
# # } |
|
| 411 |
# } |
|
| 412 |
# class(x_) <- orig.class |
|
| 413 |
# x_ |
|
| 414 |
# } |
|
| 415 |
| 1 |
#' Correlation between distances in two ENA spaces |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Calculates the Pearson correlation between the pairwise Euclidean distances |
|
| 5 |
#' of points in two ENA spaces (\code{A} and \code{B}). For smaller datasets,
|
|
| 6 |
#' it computes the exact correlation. For larger datasets, it estimates the |
|
| 7 |
#' correlation using a sampled subset of pairs. |
|
| 8 |
#' |
|
| 9 |
#' @param A A matrix or data frame representing the first ENA space (rows as points). |
|
| 10 |
#' @param B A matrix or data frame representing the second ENA space (must have the same number of rows as A). |
|
| 11 |
#' @param max_sample_size Numeric. The maximum number of pairwise distances to compute. |
|
| 12 |
#' If the total possible pairs exceeds this value, sampling is used. Default is 100,000. |
|
| 13 |
#' |
|
| 14 |
#' @return A numeric value representing the Pearson correlation. |
|
| 15 |
#' @importFrom stats dist cor |
|
| 16 |
#' @export |
|
| 17 |
ena_space_dist_corr <- function(A, B, max_sample_size = 100000) {
|
|
| 18 | 3x |
m <- nrow(A) |
| 19 | ||
| 20 | 3x |
if (is.null(m) || m == 0 || nrow(B) != m) {
|
| 21 | ! |
stop("The spaces must have the same non-zero number of rows.")
|
| 22 |
} |
|
| 23 | ||
| 24 |
# Calculate total unique pairs m(m-1)/2 |
|
| 25 | 3x |
total_possible_pairs <- choose(m, 2) |
| 26 | ||
| 27 |
# Use Exact if total pairs is less than limit |
|
| 28 | 3x |
if (total_possible_pairs <= max_sample_size) {
|
| 29 |
# CASE 1: Small m - Exact calculation |
|
| 30 | 3x |
d_A <- as.vector(dist(A)) |
| 31 | 3x |
d_B <- as.vector(dist(B)) |
| 32 | 3x |
return(cor(d_A, d_B, method = "pearson")) |
| 33 | ||
| 34 |
} else {
|
|
| 35 |
# CASE 2: Large m - Simple Sample & Filter |
|
| 36 |
# Sample indices with replacement |
|
| 37 | ! |
idx1 <- sample.int(m, max_sample_size, replace = TRUE) |
| 38 | ! |
idx2 <- sample.int(m, max_sample_size, replace = TRUE) |
| 39 | ||
| 40 |
# Filter out identity pairs (per user instruction for large m) |
|
| 41 | ! |
keep <- idx1 != idx2 |
| 42 | ! |
idx1 <- idx1[keep] |
| 43 | ! |
idx2 <- idx2[keep] |
| 44 | ||
| 45 |
# Vectorized Euclidean Distance: sqrt(sum((x-y)^2)) |
|
| 46 | ! |
dist_A <- sqrt(rowSums((A[idx1, , drop = FALSE] - A[idx2, , drop = FALSE])^2)) |
| 47 | ! |
dist_B <- sqrt(rowSums((B[idx1, , drop = FALSE] - B[idx2, , drop = FALSE])^2)) |
| 48 | ||
| 49 | ! |
return(cor(dist_A, dist_B, method = "pearson")) |
| 50 |
} |
|
| 51 |
} |
| 1 |
##### |
|
| 2 |
#' @title Wrapper to generate plots of units, groups, and networks |
|
| 3 |
#' |
|
| 4 |
#' @description Plots individual units, all units, groups of units, networks, and network subtractions |
|
| 5 |
#' |
|
| 6 |
#' @details This function includes options to plots individual units, all units, |
|
| 7 |
#' groups of units, networks, and network subtractions, given an ena.set objects. Plots are stored |
|
| 8 |
#' on the supplied ena.set object. |
|
| 9 |
#' |
|
| 10 |
#' |
|
| 11 |
#' @param set an ena.set object |
|
| 12 |
#' @param groupVar vector, character, of column name containing group identifiers. |
|
| 13 |
#' @param groups vector, character, of values of groupVar column you wish to plot. Maxium of two groups allowed. |
|
| 14 |
#' @param points logical, TRUE will plot points (default: FALSE) |
|
| 15 |
#' @param mean logical, TRUE will plot the mean position of the groups defined in the groups argument (default: FALSE) |
|
| 16 |
#' @param network logical, TRUE will plot networks (default: TRUE) |
|
| 17 |
#' @param networkMultiplier numeric, scaling factor for non-subtracted networks (default: 1) |
|
| 18 |
#' @param subtractionMultiplier numeric, scaling factor for subtracted networks (default: 1) |
|
| 19 |
#' @param unit vector, character, name of a single unit to plot |
|
| 20 |
#' @param colors vector, character, of colors for groups or points. For two-group models supply two values (group1, group2); for single-group or no-group models supply one value. Defaults to "blue"/"red" for two groups and "black" otherwise. |
|
| 21 |
#' @param confidence.interval character, style of confidence interval shown on mean points: "box" (default), "crosshairs", or "none" |
|
| 22 |
#' @param print.plots logical, TRUE will show plots in the Viewer (default: FALSE) |
|
| 23 |
#' @param backend \code{"plotly"} (default) or \code{"qeviz"}; default
|
|
| 24 |
#' \code{getOption("rENA.plot.backend", "plotly")}. On qeviz the weights are
|
|
| 25 |
#' not multiplied: the multipliers become each plot's magnification |
|
| 26 |
#' (labelled "(scaled Nx)"). See \link{rENA-plot-backends}.
|
|
| 27 |
#' @param ... Additional parameters passed to set creation and plotting functions |
|
| 28 |
#' @export |
|
| 29 |
#' @return ena.set object |
|
| 30 |
##### |
|
| 31 |
ena.plotter = function( |
|
| 32 |
set, |
|
| 33 |
groupVar = NULL, |
|
| 34 |
groups = NULL, |
|
| 35 |
points = FALSE, |
|
| 36 |
mean = FALSE, |
|
| 37 |
network = TRUE, |
|
| 38 |
networkMultiplier = 1, |
|
| 39 |
subtractionMultiplier = 1, |
|
| 40 |
unit = NULL, |
|
| 41 |
colors = NULL, |
|
| 42 |
confidence.interval = "box", |
|
| 43 |
print.plots = F, |
|
| 44 |
backend = getOption("rENA.plot.backend", "plotly"),
|
|
| 45 |
... |
|
| 46 |
) {
|
|
| 47 | 17x |
backend <- match.arg(backend, c("plotly", "qeviz"))
|
| 48 |
# qeviz: weights unscaled, networkMultiplier -> magnify (see .qe_magnify()). |
|
| 49 | 17x |
wm <- .qe_weight_mult(backend, networkMultiplier) |
| 50 | 17x |
mag <- .qe_magnify(backend, networkMultiplier) |
| 51 | 17x |
data = set$connection.counts; |
| 52 | ||
| 53 |
# set$plots[[length(set$plots)]] <- plot |
|
| 54 |
# plot <- set$plots[[length(set$plots)]] |
|
| 55 | 17x |
if(is.null(unit) == FALSE) {
|
| 56 | 1x |
plot = ena.plot(enaset = set, title = unit, backend = backend, magnify = mag) |
| 57 | ||
| 58 | 1x |
if(any(set$points$ENA_UNIT == unit) == FALSE){
|
| 59 | ! |
stop("Unit does not exist!")
|
| 60 |
} |
|
| 61 | ||
| 62 | 1x |
point.row = set$points$ENA_UNIT == unit |
| 63 | 1x |
point = as.matrix(set$points)[point.row,] |
| 64 | 1x |
point.lw = as.matrix(set$line.weights)[point.row,]* wm |
| 65 | ||
| 66 | 1x |
unit.color = if (!is.null(colors)) colors[1] else "black" |
| 67 | 1x |
plot = ena.plot.points(enaplot = plot,points = point, colors = unit.color) |
| 68 | 1x |
plot = ena.plot.network(enaplot = plot, network = point.lw, colors = unit.color) |
| 69 | ||
| 70 | 1x |
set$plots[[length(set$plots) + 1]] <- plot |
| 71 | ||
| 72 | 1x |
if(print.plots == TRUE) {
|
| 73 | ! |
print(set$plots[[length(set$plots)]]) |
| 74 |
} |
|
| 75 | ||
| 76 | 1x |
return(set) |
| 77 |
} |
|
| 78 | ||
| 79 | 16x |
if(is.null(groupVar) == TRUE) {
|
| 80 | 11x |
plot = ena.plot(enaset = set, title = "All Units", backend = backend, magnify = mag) |
| 81 | 11x |
base.color = if (!is.null(colors)) colors[1] else "black" |
| 82 | ||
| 83 | 11x |
if(network == TRUE) {
|
| 84 | 11x |
lineweights = as.matrix(set$line.weights) |
| 85 | 11x |
mean.lineweights = colMeans(lineweights) * wm |
| 86 | ||
| 87 | 11x |
plot = ena.plot.network(plot, network = mean.lineweights, colors = base.color) |
| 88 |
} |
|
| 89 | ||
| 90 | 11x |
if(points == TRUE) {
|
| 91 | ! |
points.for.plot = as.matrix(set$points) |
| 92 | ||
| 93 | ! |
plot = ena.plot.points(enaplot = plot,points = points.for.plot,colors = base.color) |
| 94 |
} |
|
| 95 | ||
| 96 | 11x |
if(mean == TRUE) {
|
| 97 | ! |
points.for.plot = as.matrix(set$points) |
| 98 | ||
| 99 | ! |
plot = ena.plot.group(plot, points.for.plot, colors = base.color, labels = "Mean", confidence.interval = confidence.interval) |
| 100 |
} |
|
| 101 | ||
| 102 | 11x |
else if(TRUE %in% c(network,points, mean) == FALSE) {
|
| 103 | ! |
stop("You must set at least one of points, mean, or network to TRUE to obtain a plot.")
|
| 104 |
} |
|
| 105 | ||
| 106 | 11x |
set$plots[[length(set$plots) + 1]] <- plot |
| 107 | ||
| 108 | 11x |
if(print.plots == TRUE) {
|
| 109 | ! |
print(set$plots) |
| 110 |
} |
|
| 111 | ||
| 112 | 11x |
return(set) |
| 113 |
} |
|
| 114 | 5x |
else if(is.null(groups) == TRUE) {
|
| 115 | ! |
unique.groups = unique(data[[groupVar]]) |
| 116 | ||
| 117 | ! |
if(length(unique.groups) == 1){
|
| 118 | ! |
warning("No groups specified and group variable only contains one unique value. Generating plot for one group.")
|
| 119 | ||
| 120 | ! |
group = unique.groups |
| 121 | ||
| 122 | ! |
group.rows = set$points[[groupVar]] == group |
| 123 | ! |
g.plot = ena.plot(enaset = set, title = group, backend = backend, magnify = mag) |
| 124 | ! |
base.color = if (!is.null(colors)) colors[1] else "black" |
| 125 | ||
| 126 | ! |
if(network == TRUE) {
|
| 127 | ! |
g.lw = as.matrix(set$line.weights)[group.rows, , drop = FALSE] |
| 128 | ! |
g.mean.lw = colMeans(g.lw) * wm |
| 129 | ! |
g.plot = ena.plot.network(g.plot, network = g.mean.lw, colors = base.color) |
| 130 |
} |
|
| 131 | ||
| 132 | ! |
if(points == TRUE) {
|
| 133 | ! |
g.points.for.plot = as.matrix(set$points)[group.rows, , drop = FALSE] |
| 134 | ! |
g.plot = ena.plot.points(enaplot = g.plot,points = g.points.for.plot,colors = base.color) |
| 135 |
} |
|
| 136 | ||
| 137 | ! |
if(mean == TRUE) {
|
| 138 | ! |
g.points.for.plot = as.matrix(set$points)[group.rows, , drop = FALSE] |
| 139 | ! |
g.plot = ena.plot.group(g.plot, g.points.for.plot, colors = base.color, labels = group, confidence.interval = confidence.interval) |
| 140 |
} |
|
| 141 | ||
| 142 | ! |
else if(TRUE %in% c(network,points, mean) == FALSE) {
|
| 143 | ! |
stop("You must set at least one of points, mean, or network to TRUE to obtain a plot.")
|
| 144 |
} |
|
| 145 | ! |
set$plots[[length(set$plots) + 1]] <- g.plot |
| 146 | ||
| 147 | ! |
if(print.plots == TRUE) {
|
| 148 | ! |
print(set$plots) |
| 149 |
} |
|
| 150 | ||
| 151 | ! |
return(set) |
| 152 |
} |
|
| 153 |
else {
|
|
| 154 | ! |
group1 = unique.groups[1] |
| 155 | ! |
group2 = unique.groups[2] |
| 156 | ||
| 157 | ! |
warning(paste0("No groups specified. Generating plots of first two unique values of group variable: ",group1," and ",group2))
|
| 158 | ||
| 159 | ! |
set = ena.plot.subtraction(set = set, |
| 160 | ! |
groupVar = groupVar, |
| 161 | ! |
group1 = group1, |
| 162 | ! |
group2 = group2, |
| 163 | ! |
points = points, |
| 164 | ! |
mean = mean, |
| 165 | ! |
network = network, |
| 166 | ! |
networkMultiplier = networkMultiplier, |
| 167 | ! |
subtractionMultiplier = subtractionMultiplier, |
| 168 | ! |
group1.color = if (!is.null(colors)) colors[1] else "blue", |
| 169 | ! |
group2.color = if (!is.null(colors)) colors[2] else "red", |
| 170 | ! |
confidence.interval = confidence.interval, backend = backend) |
| 171 | ||
| 172 | ||
| 173 | ! |
if(print.plots == TRUE) {
|
| 174 | ! |
print(set$plots) |
| 175 |
} |
|
| 176 | ||
| 177 | ! |
return(set) |
| 178 |
} |
|
| 179 |
} |
|
| 180 | 5x |
else if(length(groups) == 1) {
|
| 181 | 2x |
group = groups |
| 182 | ||
| 183 | 2x |
if(any(data[[groupVar]] == group) == FALSE){
|
| 184 | ! |
stop("Group column does not contain group1 value!")
|
| 185 |
} |
|
| 186 | ||
| 187 | 2x |
group.rows = set$points[[groupVar]] == group |
| 188 | 2x |
g.plot = ena.plot(enaset = set, title = group, backend = backend, magnify = mag) |
| 189 | 2x |
base.color = if (!is.null(colors)) colors[1] else "black" |
| 190 | ||
| 191 | 2x |
if(network == TRUE) {
|
| 192 | 2x |
g.lw = as.matrix(set$line.weights)[group.rows, , drop = FALSE] |
| 193 | 2x |
g.mean.lw = colMeans(g.lw) * wm |
| 194 | ||
| 195 | 2x |
g.plot = ena.plot.network(g.plot, network = g.mean.lw, colors = base.color) |
| 196 |
} |
|
| 197 | ||
| 198 | 2x |
if(points == TRUE) {
|
| 199 | ! |
g.points.for.plot = as.matrix(set$points)[group.rows, , drop = FALSE] |
| 200 | ! |
g.plot = ena.plot.points(enaplot = g.plot,points = g.points.for.plot,colors = base.color) |
| 201 |
} |
|
| 202 | ||
| 203 | 2x |
if(mean == TRUE) {
|
| 204 | ! |
g.points.for.plot = as.matrix(set$points)[group.rows, , drop = FALSE] |
| 205 | ! |
g.plot = ena.plot.group(g.plot, g.points.for.plot, colors = base.color, labels = group, confidence.interval = confidence.interval) |
| 206 |
} |
|
| 207 | ||
| 208 | 2x |
else if(TRUE %in% c(network,points, mean) == FALSE) {
|
| 209 | ! |
stop("You must set at least one of points, mean, or network to TRUE to obtain a plot.")
|
| 210 |
} |
|
| 211 | 2x |
set$plots[[length(set$plots) + 1]] <- g.plot |
| 212 | ||
| 213 | 2x |
if(print.plots == TRUE) {
|
| 214 | ! |
print(set$plots) |
| 215 |
} |
|
| 216 | ||
| 217 | 2x |
return(set) |
| 218 |
} |
|
| 219 | 3x |
else if (length(groups) >= 2) {
|
| 220 | 3x |
if (length(groups) > 2) {
|
| 221 | ! |
warning(paste0("More than two groups specified. Plotting the first two groups: ", groups))
|
| 222 |
} |
|
| 223 | ||
| 224 | 3x |
groups.missing = groups[which(!groups %in% data[[groupVar]])] |
| 225 | 3x |
if(length(groups.missing) > 0) {
|
| 226 | ! |
stop(paste0("Group column does not contain group value(s): ", groups.missing))
|
| 227 |
} |
|
| 228 | ||
| 229 | 3x |
set = ena.plot.subtraction( |
| 230 | 3x |
set = set, |
| 231 | 3x |
groupVar = groupVar, |
| 232 | 3x |
group1 = groups[1], |
| 233 | 3x |
group2 = groups[2], |
| 234 | 3x |
points = points, |
| 235 | 3x |
mean = mean, |
| 236 | 3x |
network = network, |
| 237 | 3x |
networkMultiplier = networkMultiplier, |
| 238 | 3x |
subtractionMultiplier = subtractionMultiplier, |
| 239 | 3x |
group1.color = if (!is.null(colors)) colors[1] else "blue", |
| 240 | 3x |
group2.color = if (!is.null(colors)) colors[2] else "red", |
| 241 | 3x |
confidence.interval = confidence.interval, backend = backend, |
| 242 |
... |
|
| 243 |
) |
|
| 244 | ||
| 245 | 3x |
if(print.plots == TRUE) {
|
| 246 | ! |
print(set$plots) |
| 247 |
} |
|
| 248 | ||
| 249 | 3x |
return(set) |
| 250 |
} |
|
| 251 |
} |
|
| 252 |
| 1 |
## |
|
| 2 |
#' @title Find conversations by unit |
|
| 3 |
#' |
|
| 4 |
#' @description Find rows of conversations by unit |
|
| 5 |
#' |
|
| 6 |
#' @details [TBD] |
|
| 7 |
#' |
|
| 8 |
#' @param set [TBD] |
|
| 9 |
#' @param units [TBD] |
|
| 10 |
#' @param units.by [TBD] |
|
| 11 |
#' @param codes [TBD] |
|
| 12 |
#' @param conversation.by [TBD] |
|
| 13 |
#' @param window [TBD] |
|
| 14 |
#' @param conversation.exclude [TBD] |
|
| 15 |
#' |
|
| 16 |
#' @examples |
|
| 17 |
#' data(RS.data) |
|
| 18 |
#' |
|
| 19 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 20 |
#' 'Client.and.Consultant.Requests','Design.Reasoning', |
|
| 21 |
#' 'Collaboration'); |
|
| 22 |
#' |
|
| 23 |
#' accum = ena.accumulate.data( |
|
| 24 |
#' units = RS.data[,c("Condition","UserName")],
|
|
| 25 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 26 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre",
|
|
| 27 |
#' "CONFIDENCE.Post","C.Change")], |
|
| 28 |
#' codes = RS.data[,codeNames], |
|
| 29 |
#' model = "EndPoint", |
|
| 30 |
#' window.size.back = 4 |
|
| 31 |
#' ); |
|
| 32 |
#' set = ena.make.set( |
|
| 33 |
#' enadata = accum, |
|
| 34 |
#' rotation.by = ena.rotate.by.mean, |
|
| 35 |
#' rotation.params = list(accum$meta.data$Condition=="FirstGame", |
|
| 36 |
#' accum$meta.data$Condition=="SecondGame") |
|
| 37 |
#' ); |
|
| 38 |
#' ena.conversations(set = RS.data, |
|
| 39 |
#' units = c("FirstGame.steven z"), units.by=c("Condition","UserName"),
|
|
| 40 |
#' conversation.by = c("Condition","GroupName"),
|
|
| 41 |
#' codes=codeNames, window = 4 |
|
| 42 |
#' ) |
|
| 43 |
#' |
|
| 44 |
#' @export |
|
| 45 |
#' @return list containing row indices representing conversations |
|
| 46 |
## |
|
| 47 |
ena.conversations = function(set, units, units.by=NULL, codes=NULL, conversation.by = NULL, window = 4, conversation.exclude = c()) {
|
|
| 48 |
# rawData = data.table::copy(set$enadata$raw); |
|
| 49 | 4x |
if(is.null(units.by)) {
|
| 50 | 2x |
if(!is(set, "ena.set")) {
|
| 51 | 1x |
stop("If units.by is NULL, set must be an ena.set object")
|
| 52 |
} |
|
| 53 | 1x |
units.by = set$`_function.params`$units.by; |
| 54 |
} |
|
| 55 |
# conversation.by = set$enadata$function.params$conversations.by; |
|
| 56 |
# window = set$enadata$function.params$window.size.back; |
|
| 57 |
# rawAcc = data.table::copy(set$enadata$accumulated.adjacency.vectors); |
|
| 58 | 3x |
if(is(set, "ena.set")) {
|
| 59 | 2x |
rawAcc2 = set$model$raw.input |
| 60 |
} else {
|
|
| 61 | 1x |
rawAcc2 = data.table::data.table(set) #$enadata$raw); |
| 62 |
} |
|
| 63 | ||
| 64 |
# rawAcc$KEYCOL = merge_columns_c(rawAcc, conversation.by) |
|
| 65 | 3x |
rawAcc2$KEYCOL = merge_columns_c(rawAcc2, conversation.by, sep = "::") |
| 66 | ||
| 67 |
# conversationsTable = rawAcc[, paste(.I, collapse = ","), by = c(conversation.by)] |
|
| 68 | 3x |
conversationsTable2 = rawAcc2[, paste(.I, collapse = ","), by = c(conversation.by)] |
| 69 | ||
| 70 |
# rows = sapply(conversationsTable$V1, function(x) as.numeric(unlist(strsplit(x, split=","))),USE.NAMES = T) |
|
| 71 | 3x |
rows2 = lapply(conversationsTable2$V1, function(x) as.numeric(unlist(strsplit(x, split=",")))) |
| 72 |
# browser() |
|
| 73 |
# names(rows) = merge_columns_c(conversationsTable,conversation.by); #unique(rawAcc[,KEYCOL]) |
|
| 74 | 3x |
names(rows2) = merge_columns_c(conversationsTable2,conversation.by, sep = "::"); #unique(rawAcc[,KEYCOL]) |
| 75 | ||
| 76 |
# unitRows = merge_columns_c(rawAcc[,c(units.by),with=F], units.by) |
|
| 77 | 3x |
unitRows2 = merge_columns_c(rawAcc2[,c(units.by),with=F], units.by, sep = "::") |
| 78 | ||
| 79 |
# adjCol = set$enadata$adjacency.matrix[1,] %in% codes[1] & set$enadata$adjacency.matrix[2,] %in% codes[2] |
|
| 80 |
# adjColName = paste("adjacency.code.", which(adjCol), sep = "")
|
|
| 81 |
# codedUnitRows = which(unitRows %in% units & rawAcc[[adjColName]] == 1) |
|
| 82 | ||
| 83 | 3x |
codedRows = rawAcc2[, rowSums(.SD), .SDcols = codes] > 0 |
| 84 | 3x |
codedUnitRows2 = which(unitRows2 %in% units & codedRows) |
| 85 | 3x |
codedUnitRows2 = codedUnitRows2[!(codedUnitRows2 %in% as.vector(unlist(rows2[conversation.exclude])))] |
| 86 |
# codedUnitRowConvs = rawAcc[codedUnitRows,KEYCOL]; |
|
| 87 | 3x |
codedUnitRowConvs2 = rawAcc2[codedUnitRows2,KEYCOL]; |
| 88 | ||
| 89 | 3x |
codedUnitRowConvsAll = NULL; |
| 90 | 3x |
codedUnitRowConvsAll2 = NULL; |
| 91 | 3x |
unitRowsNotCooccurred = c() |
| 92 | 3x |
if(length(codedUnitRows2) > 0) {
|
| 93 | 3x |
codedUnitRowConvsAll = unique(unlist(sapply(X = 1:length(codedUnitRows2), simplify = F, FUN = function(x) {
|
| 94 | 279x |
thisConvRows = rows2[[codedUnitRowConvs2[x]]] |
| 95 | 279x |
thisRowInConv = which(thisConvRows == codedUnitRows2[x]) |
| 96 | 279x |
winUse = ifelse(is.infinite(window), thisRowInConv, window) |
| 97 | 279x |
thisRowAndWindow = rep(thisRowInConv,winUse) - (winUse-1):0 |
| 98 | 279x |
coOccursFound = all(rawAcc2[thisConvRows[thisRowAndWindow[thisRowAndWindow > 0]], lapply(.SD, sum), .SDcols=codes] > 0) |
| 99 | 279x |
if(coOccursFound) {
|
| 100 | 132x |
thisConvRows[thisRowAndWindow[thisRowAndWindow > 0]] |
| 101 |
} else {
|
|
| 102 | 147x |
unitRowsNotCooccurred <<- c(unitRowsNotCooccurred, thisConvRows[thisRowInConv]) |
| 103 |
# coOccursFound |
|
| 104 | 147x |
NULL |
| 105 |
} |
|
| 106 |
}))) |
|
| 107 |
} |
|
| 108 | 3x |
return(list( |
| 109 | 3x |
conversations = as.list(rows2), |
| 110 | 3x |
unitConvs = unique(rawAcc2[codedUnitRows2,KEYCOL]), |
| 111 | 3x |
allRows = codedUnitRowConvsAll, |
| 112 | 3x |
unitRows = codedUnitRows2, |
| 113 | 3x |
toRemove = unitRowsNotCooccurred |
| 114 |
)); |
|
| 115 |
} |
| 1 |
### |
|
| 2 |
#' @title ENA Rotate by regression |
|
| 3 |
#' |
|
| 4 |
#' @description This function allows user to provide a regression formula for rotation on x and optionally on y. |
|
| 5 |
#' If regression formula for y is not provide, svd is applied to the residual data deflated by x to get y coordinates. |
|
| 6 |
#' The regression formula uses ENA dimensions are dependent variables. |
|
| 7 |
#' The first predictor has to be two-group categorical, binary, or numerical. |
|
| 8 |
#' |
|
| 9 |
#' @param enaset An \code{\link{ENAset}}
|
|
| 10 |
#' @param params list of parameters, may include: |
|
| 11 |
#' x_var: Regression formula for x direction, such as "lm(formula=V ~ Condition + GameHalf + Condition : GameHalf)", |
|
| 12 |
#' where V always stands for the ENA points. |
|
| 13 |
#' y_var: Regression formula, similar to x_var, for y direction (optional). |
|
| 14 |
#' |
|
| 15 |
#' @export |
|
| 16 |
#' @return \code{\link{ENARotationSet}}
|
|
| 17 |
ena.rotate.by.hena.regression = function(enaset, params) {
|
|
| 18 |
# check arguments |
|
| 19 | ! |
if ( !is.list(params) || is.null(params$x_var) ) {
|
| 20 | ! |
stop("params must be provided as a list() and provide `x_var`")
|
| 21 |
} |
|
| 22 | ||
| 23 | ! |
x <- params$x_var; |
| 24 | ! |
y <- params$y_var; |
| 25 | ! |
points <- params$points; |
| 26 | ! |
fullNames <- params$fullNames; |
| 27 | ||
| 28 | ! |
if(is.null(fullNames)) {
|
| 29 | ! |
fullNames = F; |
| 30 |
} |
|
| 31 | ||
| 32 |
#get points |
|
| 33 | ! |
if(!is.null(points)) {
|
| 34 | ! |
p <- points |
| 35 |
} |
|
| 36 | ! |
else if (is.null(enaset$points.normed.centered)) {
|
| 37 | ! |
p <- as.matrix(enaset$model$points.for.projection); |
| 38 |
} |
|
| 39 |
else {
|
|
| 40 | ! |
p <- as.matrix(enaset$points.normed.centered); |
| 41 |
} |
|
| 42 | ||
| 43 |
#regress to get v1 using x |
|
| 44 | ! |
V <- p; |
| 45 | ||
| 46 |
# only works using attach() |
|
| 47 |
# attach(enaset$meta.data,warn.conflicts = F) |
|
| 48 |
# v1 = eval(parse(text = x))$coefficients[2,] |
|
| 49 | ||
| 50 |
# v1 <- with(enaset$meta.data, {
|
|
| 51 |
# eval(parse(text = x))$coefficients[2,] |
|
| 52 |
# }); |
|
| 53 |
# v1 <- with(enaset$model$points.for.projection, NULL, formula = x); |
|
| 54 | ! |
v1_res <- with.ena.matrix(enaset$model$points.for.projection, {
|
| 55 | ! |
lm(formula(params$x_var)); |
| 56 |
}); |
|
| 57 | ! |
v1 <- v1_res$coefficients[2,] |
| 58 | ||
| 59 |
# make v1 a unit vector |
|
| 60 | ! |
norm_v1 <- sqrt(sum(v1 * v1)); |
| 61 | ! |
if (norm_v1 != 0) {
|
| 62 | ! |
v1 <- v1 / norm_v1; |
| 63 |
} |
|
| 64 | ||
| 65 |
# name v1 vector |
|
| 66 | ! |
if(is.na(all.vars(x)[2])) {
|
| 67 | ! |
xName <- names(v1)[1]; |
| 68 |
} |
|
| 69 |
else {
|
|
| 70 | ! |
if(fullNames) {
|
| 71 | ! |
warning("FullName param is likely wrong.")
|
| 72 | ! |
xName <- parse(text = x)[[1]][["formula"]][[3]]; |
| 73 |
} |
|
| 74 |
else {
|
|
| 75 | ! |
xName <- all.vars(x)[2]; |
| 76 |
} |
|
| 77 |
} |
|
| 78 | ||
| 79 |
# Save v1 |
|
| 80 | ! |
R <- matrix(c(v1), ncol = 1); |
| 81 | ! |
colnames(R) <- c(paste0(xName,"_reg")); |
| 82 | ||
| 83 |
#deflate matrix by x dimension |
|
| 84 | ! |
A <- as.matrix(p) |
| 85 | ! |
defA <- as.matrix(A) - as.matrix(A) %*% v1 %*% t(v1) |
| 86 | ||
| 87 |
#if y formula is given, regress by y formula |
|
| 88 | ! |
if (!is.null(y)) {
|
| 89 | ||
| 90 |
# regress to get v2 vector using formula y |
|
| 91 | ! |
V <- defA; |
| 92 | ||
| 93 |
# Removed attach abvove |
|
| 94 |
# v2 = eval(parse(text = y))$coefficients[2,] |
|
| 95 |
# v2 <- with(enaset$meta.data, {
|
|
| 96 |
# eval(parse(text = y))$coefficients[2,] |
|
| 97 |
# }); |
|
| 98 |
# v2 <- with(enaset$model$points.for.projection, NULL, formula = y, V = V); |
|
| 99 | ! |
v2_res <- with.ena.matrix(enaset$model$points.for.projection, {
|
| 100 | ! |
lm(formula(params$y_var)); |
| 101 |
}); |
|
| 102 | ! |
v2 <- v2_res$coefficients[2,] |
| 103 | ||
| 104 |
#make v2 a unit vector |
|
| 105 | ||
| 106 | ! |
norm_v2 <- sqrt(sum(v2 * v2)); |
| 107 | ! |
if (norm_v2 != 0) {
|
| 108 | ! |
v2 <- v2 / norm_v2; |
| 109 |
} |
|
| 110 | ||
| 111 |
#name v2 vector |
|
| 112 | ! |
if(is.na(all.vars(y)[2])) {
|
| 113 | ! |
yName <- names(v2)[1] |
| 114 |
} |
|
| 115 |
else {
|
|
| 116 | ! |
if(fullNames) {
|
| 117 | ! |
warning("FullName param is likely wrong.")
|
| 118 | ! |
yName <- parse(text = y)[[1]][["formula"]][[3]]; |
| 119 |
} |
|
| 120 |
else {
|
|
| 121 | ! |
yName <- all.vars(y)[2] |
| 122 |
} |
|
| 123 |
} |
|
| 124 | ||
| 125 |
# save both v1 and v2 |
|
| 126 | ! |
R <- cbind(v1, v2); |
| 127 | ! |
colnames(R) <- c(paste0(xName,"_reg"), paste0(yName,"_reg")); |
| 128 | ||
| 129 |
#deflat by v2 |
|
| 130 | ! |
defA <- as.matrix(defA) - as.matrix(defA) %*% v2 %*% t(v2); |
| 131 |
} |
|
| 132 | ||
| 133 |
# get svd for deflated points |
|
| 134 | ! |
svd_result <- prcomp(defA, retx=FALSE, scale=FALSE, center=FALSE, tol=0); |
| 135 | ! |
svd_v <- svd_result$rotation; |
| 136 | ||
| 137 |
# Merge rotation vectors |
|
| 138 | ! |
vcount <- ncol(R); |
| 139 | ! |
colNamesR <- colnames(R); |
| 140 | ! |
combined <- cbind(R, svd_v[, 1:(ncol(svd_v) - vcount)]); |
| 141 | ! |
colnames(combined) <- c( |
| 142 | ! |
colNamesR, |
| 143 | ! |
paste0("SVD", ((vcount + 1):ncol(combined)))
|
| 144 |
); |
|
| 145 | ||
| 146 |
#create rotation set |
|
| 147 | ! |
rotation_set <- ENARotationSet$new( |
| 148 | ! |
node.positions = NULL, |
| 149 | ! |
rotation = combined, |
| 150 | ! |
codes = enaset$rotation$codes, |
| 151 | ! |
eigenvalues = NULL |
| 152 |
) |
|
| 153 | ||
| 154 | ! |
return(rotation_set); |
| 155 |
} |
| 1 |
## |
|
| 2 |
#' @title Plot points on an ENAplot |
|
| 3 |
#' |
|
| 4 |
#' @description Plot all or a subset of the points of an ENAplot using the plotly plotting library |
|
| 5 |
#' |
|
| 6 |
#' @export |
|
| 7 |
#' |
|
| 8 |
#' @param enaplot \code{\link{ENAplot}} object to use for plotting
|
|
| 9 |
#' @param points A dataframe of matrix where the first two column are X and Y coordinates |
|
| 10 |
#' @param point.size A data.frame or matrix where the first two column are X and Y coordinates of points to plot in a projected ENA space defined in ENAplot |
|
| 11 |
#' @param labels A character vector of point labels, length nrow(points); default: NULL |
|
| 12 |
#' @param confidence.interval A character determining markings to use for confidence intervals, choices: none, box, crosshair, default: none |
|
| 13 |
#' @param outlier.interval A character determining markings to use for outlier interval, choices: none, box, crosshair, default: none |
|
| 14 |
#' @param confidence.interval.values A matrix/dataframe where columns are CI x and y values for each point |
|
| 15 |
#' @param outlier.interval.values A matrix/dataframe where columns are OI x and y values for each point |
|
| 16 |
#' @param shape A character which determines the shape of point markers, choices: square, triangle, diamond, circle, default: circle |
|
| 17 |
#' @param colors A character vector of the point marker colors; if one given it is used for all, otherwise must be same length as points; default: black |
|
| 18 |
#' @param label.offset character: top left (default), top center, top right, middle left, middle center, middle right, bottom left, bottom center, bottom right |
|
| 19 |
#' @param label.group A string used to group the labels in the legend. Items plotted with the same label.group will show/hide together when clicked within the legend. |
|
| 20 |
#' @param label.font.size An integer which determines the font size for point labels, default: enaplot$font.size |
|
| 21 |
#' @param label.font.color A character which determines the color of label font, default: enaplot$font.color |
|
| 22 |
#' @param label.font.family A character which determines label font type, choices: Arial, Courier New, Times New Roman, default: enaplot$font.family |
|
| 23 |
#' @param show.legend Logical indicating whether to show the point labels in the in legend |
|
| 24 |
#' @param legend.name Character indicating the name to show above the plot legend |
|
| 25 |
#' @param texts [TBD] |
|
| 26 |
#' @param ... additional parameters addressed in inner function |
|
| 27 |
#' |
|
| 28 |
#' |
|
| 29 |
#' @seealso \code{\link{ena.plot}}, \code{\link{ENAplot}}, \code{\link{ena.plot.group}}
|
|
| 30 |
#' |
|
| 31 |
#' @examples |
|
| 32 |
#' data(RS.data) |
|
| 33 |
#' |
|
| 34 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 35 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 36 |
#' |
|
| 37 |
#' accum = ena.accumulate.data( |
|
| 38 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 39 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 40 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 41 |
#' codes = RS.data[,codeNames], |
|
| 42 |
#' window.size.back = 4 |
|
| 43 |
#' ) |
|
| 44 |
#' |
|
| 45 |
#' set = ena.make.set( |
|
| 46 |
#' enadata = accum, |
|
| 47 |
#' rotation.by = ena.rotate.by.mean, |
|
| 48 |
#' rotation.params = list( |
|
| 49 |
#' accum$meta.data$Condition=="FirstGame", |
|
| 50 |
#' accum$meta.data$Condition=="SecondGame" |
|
| 51 |
#' ) |
|
| 52 |
#' ) |
|
| 53 |
#' |
|
| 54 |
#' plot = ena.plot(set) |
|
| 55 |
#' |
|
| 56 |
#' group1.points = set$points[set$meta.data$Condition == "FirstGame",] |
|
| 57 |
#' group2.points = set$points[set$meta.data$Condition == "SecondGame",] |
|
| 58 |
#' plot = ena.plot.points(plot, points = group1.points); |
|
| 59 |
#' plot = ena.plot.points(plot, points = group2.points); |
|
| 60 |
#' print(plot); |
|
| 61 |
#' |
|
| 62 |
#' @return \code{\link{ENAplot}} The ENAplot provided to the function, with its plot updated to include the new points.
|
|
| 63 |
## |
|
| 64 |
ena.plot.points = function( |
|
| 65 |
enaplot, |
|
| 66 | ||
| 67 |
points = NULL, #vector of unit names or row indices |
|
| 68 |
point.size = enaplot$point$size, |
|
| 69 |
labels = NULL, #unique(enaplot$enaset$enadata$unit.names), |
|
| 70 |
label.offset = "top left", |
|
| 71 |
label.group = NULL, |
|
| 72 | ||
| 73 |
label.font.size = NULL, #enaplot$get("font.size"),
|
|
| 74 |
label.font.color = NULL, #enaplot$get("font.color"),
|
|
| 75 |
label.font.family = NULL, #enaplot$get("font.family"),
|
|
| 76 | ||
| 77 |
shape = "circle", |
|
| 78 |
colors = NULL, # c("blue"), #rep(I("black"), nrow(points)),
|
|
| 79 | ||
| 80 |
confidence.interval.values = NULL, |
|
| 81 |
confidence.interval = c("none", "crosshairs", "box"),
|
|
| 82 | ||
| 83 |
outlier.interval.values = NULL, |
|
| 84 |
outlier.interval = c("none", "crosshairs", "box"),
|
|
| 85 |
show.legend = T, |
|
| 86 |
legend.name = "Points", |
|
| 87 |
texts = NULL, |
|
| 88 |
... |
|
| 89 |
) {
|
|
| 90 |
### |
|
| 91 |
# Parameter Checking and Cleaning |
|
| 92 |
### |
|
| 93 | 18x |
if (.qe_backend(enaplot)) |
| 94 | 3x |
return(.qe_plot_points(enaplot, supplied = names(match.call())[-1], |
| 95 | 3x |
points = points, point.size = point.size, labels = labels, shape = shape, |
| 96 | 3x |
colors = colors, label.font.size = label.font.size, label.font.color = label.font.color, |
| 97 | 3x |
label.font.family = label.font.family, texts = texts, legend.name = legend.name)) |
| 98 | ||
| 99 | 15x |
env = environment(); |
| 100 | 15x |
for(n in c("font.size", "font.color", "font.family")) {
|
| 101 | 45x |
if(is.null(get(paste0("label.",n))))
|
| 102 | 43x |
env[[paste0("label.",n)]] = enaplot$get(n);
|
| 103 |
} |
|
| 104 | ||
| 105 | 15x |
if(is.null(points)) {
|
| 106 |
# stop("Must provide points to plot.")
|
|
| 107 | 5x |
points = enaplot$enaset$points |
| 108 |
} |
|
| 109 | ||
| 110 | 15x |
if(is(points, "numeric")){
|
| 111 | 1x |
points = matrix(points); |
| 112 | 1x |
dim(points) = c(1,nrow(points)) |
| 113 | 1x |
points.layout = data.table::data.table(points); |
| 114 |
} |
|
| 115 | 14x |
else if (is.data.table(points)) {
|
| 116 |
# points.layout = remove_meta_data(points) |
|
| 117 | 12x |
points.layout = data.table::copy(points) |
| 118 |
} |
|
| 119 |
else {
|
|
| 120 | 2x |
points.layout = data.table::data.table(points); |
| 121 |
} |
|
| 122 | ||
| 123 | 15x |
if(!is.character(label.font.family)) {
|
| 124 | 1x |
label.font.family = enaplot$get("font.family");
|
| 125 |
} |
|
| 126 | ||
| 127 | 15x |
confidence.interval = match.arg(confidence.interval); |
| 128 | 15x |
outlier.interval = match.arg(outlier.interval); |
| 129 | ||
| 130 |
# shape = match.arg(shape); |
|
| 131 | 15x |
valid.shapes = c("circle", "square", "triangle-up", "diamond");
|
| 132 | 15x |
if(!all(shape %in% valid.shapes)) |
| 133 | 1x |
stop(sprintf( "Unrecognized shapes: %s", paste(unique(shape[!(shape %in% valid.shapes)]), collapse = ", ") )) |
| 134 | 14x |
if(length(shape) == 1) |
| 135 | 14x |
shape = rep(shape, nrow(points.layout)) |
| 136 | ||
| 137 | 14x |
valid.label.offsets = c("top left","top center","top right","middle left","middle center","middle right","bottom left","bottom center","bottom right");
|
| 138 | 14x |
if(!all(label.offset %in% valid.label.offsets)) |
| 139 | 1x |
stop(sprintf( "Unrecognized label.offsets: %s", paste(unique(label.offset[!(label.offset %in% valid.label.offsets)]), collapse = ", ") )) |
| 140 | 13x |
if(length(label.offset) == 1) |
| 141 | 13x |
label.offset = rep(label.offset, nrow(points.layout)) |
| 142 | ||
| 143 | 13x |
if(grepl("^c", confidence.interval) && grepl("^c", outlier.interval)) {
|
| 144 | 1x |
message("Confidence Interval and Outlier Interval cannot both be crosshair");
|
| 145 | 1x |
message("Plotting Outlier Interval as box");
|
| 146 | 1x |
outlier.interval = "box"; |
| 147 |
} |
|
| 148 | ||
| 149 | 13x |
if(length(colors) == 1) {
|
| 150 | 1x |
colors = rep(colors, nrow(points.layout)) |
| 151 |
} |
|
| 152 | 13x |
if(length(point.size) == 1) |
| 153 | 13x |
point.size = rep(point.size, nrow(points.layout)) |
| 154 | 13x |
if(is.null(labels)) |
| 155 | 13x |
show.legend = F |
| 156 |
### |
|
| 157 |
# END: Parameter Checking and Cleaning |
|
| 158 |
### |
|
| 159 | ||
| 160 |
### |
|
| 161 |
# Set error value for CI|OI crosshair on plot |
|
| 162 |
### |
|
| 163 | 13x |
error = list(x = list(visible=T, type="data"), y = list(visible=T, type="data")); |
| 164 | 13x |
int.values = NULL; |
| 165 | 13x |
if(grepl("^c", confidence.interval) && !is.null(confidence.interval.values)) {
|
| 166 | 2x |
int.values = confidence.interval.values; |
| 167 |
} |
|
| 168 | 11x |
else if(grepl("^c", outlier.interval) && !is.null(outlier.interval.values)) {
|
| 169 | 1x |
int.values = outlier.interval.values; |
| 170 |
} |
|
| 171 | 13x |
error$x$array = int.values[, 1]; |
| 172 | 13x |
error$y$array = int.values[, 2]; |
| 173 |
### |
|
| 174 |
# END: Set error value for crosshair on plot |
|
| 175 |
### |
|
| 176 | ||
| 177 |
### |
|
| 178 |
# Set box value for CI|OI box on plot |
|
| 179 |
##### |
|
| 180 | 13x |
box.values = NULL; |
| 181 | 13x |
if(grepl("^b", confidence.interval) && !is.null(confidence.interval.values)) {
|
| 182 | 1x |
box.values = confidence.interval.values; |
| 183 | 1x |
box.label = "Conf. Int."; |
| 184 |
} |
|
| 185 | 13x |
if(grepl("^b", outlier.interval) && !is.null(outlier.interval.values)) {
|
| 186 | 2x |
box.values = outlier.interval.values; |
| 187 | 2x |
box.label = "Outlier Int."; |
| 188 |
} |
|
| 189 |
###### |
|
| 190 |
# END: Set box value for CI|OI box on plot |
|
| 191 |
### |
|
| 192 | ||
| 193 |
### |
|
| 194 |
# Plot |
|
| 195 |
##### |
|
| 196 | 13x |
points.matrix = remove_meta_data(points.layout) |
| 197 | 13x |
colnames(points.matrix) = paste0("X", rep(1:ncol(points.matrix)));
|
| 198 | 13x |
this.max = max(points.matrix); |
| 199 | 13x |
for(m in 1:nrow(points.matrix)) {
|
| 200 | 155x |
enaplot$plot = plotly::add_trace( |
| 201 | 155x |
p = enaplot$plot, |
| 202 | 155x |
data = points.matrix[m,], |
| 203 | 155x |
type ="scatter", |
| 204 | 155x |
x = ~X1, y = ~X2, |
| 205 | 155x |
mode = "markers+text", |
| 206 | 155x |
marker = list( |
| 207 | 155x |
symbol = shape[m], |
| 208 | 155x |
color = colors[m], |
| 209 | 155x |
size = point.size[m] |
| 210 |
), |
|
| 211 | 155x |
error_x = error$x, error_y = error$y, |
| 212 | 155x |
showlegend = show.legend, |
| 213 |
# legendgroup = label.group, |
|
| 214 |
# legendgroup = ifelse(!is.null(box.label), labels[1], NULL), |
|
| 215 | 155x |
name = labels[m], |
| 216 | 155x |
text = texts[m], #labels[m], |
| 217 | 155x |
textfont = list( |
| 218 | 155x |
family = label.font.family, |
| 219 | 155x |
size = label.font.size, |
| 220 | 155x |
color = label.font.color |
| 221 |
), |
|
| 222 | 155x |
legendgroup = legend.name, |
| 223 | 155x |
textposition = label.offset[m], |
| 224 | 155x |
hoverinfo = "x+y+name" |
| 225 |
) |
|
| 226 |
} |
|
| 227 | ||
| 228 | 13x |
if(!is.null(box.values)) {
|
| 229 | 3x |
boxv = data.frame( |
| 230 | 3x |
X1 = c(box.values[1,1], box.values[2,1], box.values[2,1], box.values[1,1] ,box.values[1,1]), |
| 231 | 3x |
X2 = c(box.values[1,2], box.values[1,2], box.values[2,2], box.values[2,2], box.values[1,2]) |
| 232 |
) |
|
| 233 | 3x |
this.max = max(boxv, this.max) |
| 234 | 3x |
enaplot$plot = plotly::add_trace( |
| 235 | 3x |
p = enaplot$plot, |
| 236 | 3x |
data = boxv, |
| 237 | 3x |
type = "scatter", |
| 238 | 3x |
x = ~X1, y = ~X2, |
| 239 | 3x |
mode = "lines", |
| 240 | 3x |
line = list( |
| 241 | 3x |
width = 1, |
| 242 | 3x |
color = colors[1], |
| 243 | 3x |
dash = "dash" |
| 244 |
), |
|
| 245 |
# "legendgroup" = labels[1], |
|
| 246 | 3x |
showlegend = show.legend, |
| 247 | 3x |
name = box.label |
| 248 |
) |
|
| 249 |
} |
|
| 250 | ||
| 251 | 13x |
if(this.max*1.2 > max(enaplot$axes$y$range)) {
|
| 252 | 1x |
this.max = this.max * 1.2 |
| 253 | 1x |
enaplot$axes$x$range = c(-this.max, this.max) |
| 254 | 1x |
enaplot$axes$y$range = c(-this.max, this.max) |
| 255 | 1x |
enaplot$plot = plotly::layout( |
| 256 | 1x |
enaplot$plot, |
| 257 | 1x |
xaxis = enaplot$axes$x, |
| 258 | 1x |
yaxis = enaplot$axes$y |
| 259 |
); |
|
| 260 |
} |
|
| 261 |
##### |
|
| 262 |
# END: Plot |
|
| 263 |
### |
|
| 264 | ||
| 265 | 13x |
return(enaplot); |
| 266 |
} |
|
| 267 |
| 1 |
# Post-fit node moves for display. The math is libena's |
|
| 2 |
# (inst/include/libena/positions.hpp: unit_circle_nodes, node_centroids), |
|
| 3 |
# shared with qe-ena, rena-wasm and ENA.jl. |
|
| 4 | ||
| 5 |
#' Move nodes onto a circle along their own rays |
|
| 6 |
#' |
|
| 7 |
#' A post-fit display transform: in the plane of two dimensions, pushes every |
|
| 8 |
#' node out along its own ray to the length of the longest node there, then |
|
| 9 |
#' recomputes the centroids for the moved nodes. Nothing is refit. |
|
| 10 |
#' |
|
| 11 |
#' Nodes of length zero in the plane are left where they are, as are the |
|
| 12 |
#' nodes' other dimensions. Unit points never change. The centroids, all |
|
| 13 |
#' dimensions of them, are the model's node weights times the moved nodes: |
|
| 14 |
#' each unit's line weights split between the codes they connect (half to |
|
| 15 |
#' each code for unordered networks; for ordered ones, as the fit weights |
|
| 16 |
#' them) and normalised to sum to 1, the weighting the fit's own centroids |
|
| 17 |
#' use. So the goodness of fit (\code{\link{ena.correlations}}) reflects the
|
|
| 18 |
#' moved nodes. |
|
| 19 |
#' |
|
| 20 |
#' Works for unordered and ordered (\code{accumulate(ordered = TRUE)}) sets.
|
|
| 21 |
#' |
|
| 22 |
#' @param set An \code{ena.set}, e.g. from \code{\link{model}} or
|
|
| 23 |
#' \code{\link{ena.make.set}}.
|
|
| 24 |
#' @param dimension_name_1 Name of the plane's x dimension (a column of |
|
| 25 |
#' \code{set$rotation$nodes}). Defaults to the first dimension.
|
|
| 26 |
#' @param dimension_name_2 Name of the plane's y dimension. Defaults to the |
|
| 27 |
#' second dimension. |
|
| 28 |
#' |
|
| 29 |
#' @return \code{set}, with \code{set$rotation$nodes} moved on the two
|
|
| 30 |
#' dimensions and \code{set$model$centroids} recomputed.
|
|
| 31 |
#' |
|
| 32 |
#' @seealso \code{\link{move_nodes_to_unit_circle_with_equal_space}}, which
|
|
| 33 |
#' also spaces the nodes equally around the circle. |
|
| 34 |
#' @examples |
|
| 35 |
#' data(RS.data) |
|
| 36 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 37 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 38 |
#' "Collaboration") |
|
| 39 |
#' set <- RS.data |> |
|
| 40 |
#' accumulate(c("Condition", "UserName"), codes, c("Condition", "GroupName")) |>
|
|
| 41 |
#' model() |
|
| 42 |
#' circled <- move_nodes_to_unit_circle(set) |
|
| 43 |
#' circled$rotation$nodes |
|
| 44 |
#' @export |
|
| 45 |
move_nodes_to_unit_circle <- function( |
|
| 46 |
set, |
|
| 47 |
dimension_name_1 = colnames(as.matrix(set$rotation$nodes))[1], |
|
| 48 |
dimension_name_2 = colnames(as.matrix(set$rotation$nodes))[2] |
|
| 49 |
) {
|
|
| 50 | 16x |
move_nodes_on_circle(set, dimension_name_1, dimension_name_2, "radial") |
| 51 |
} |
|
| 52 | ||
| 53 |
#' Move nodes onto a circle, equally spaced |
|
| 54 |
#' |
|
| 55 |
#' A post-fit display transform: in the plane of two dimensions, places the |
|
| 56 |
#' nodes on a circle with the radius of the longest node there, keeping |
|
| 57 |
#' their order around the origin and spacing them \eqn{2\pi/n} apart,
|
|
| 58 |
#' starting from the longest node, which stays where it is. Then recomputes |
|
| 59 |
#' the centroids for the moved nodes. Nothing is refit. |
|
| 60 |
#' |
|
| 61 |
#' The order around the origin is: nodes with a non-negative y coordinate by |
|
| 62 |
#' decreasing x, then nodes with a negative y coordinate by increasing x |
|
| 63 |
#' (ties keep the codes' order). When two nodes are equally long, the first |
|
| 64 |
#' stays fixed. Nodes of length zero in the plane are left where they are and |
|
| 65 |
#' do not count towards \eqn{n}. The rest is as for
|
|
| 66 |
#' \code{\link{move_nodes_to_unit_circle}}.
|
|
| 67 |
#' |
|
| 68 |
#' @inheritParams move_nodes_to_unit_circle |
|
| 69 |
#' @inherit move_nodes_to_unit_circle return |
|
| 70 |
#' |
|
| 71 |
#' @seealso \code{\link{move_nodes_to_unit_circle}}, which moves each node
|
|
| 72 |
#' along its own ray instead. |
|
| 73 |
#' @examples |
|
| 74 |
#' data(RS.data) |
|
| 75 |
#' codes <- c("Data", "Technical.Constraints", "Performance.Parameters",
|
|
| 76 |
#' "Client.and.Consultant.Requests", "Design.Reasoning", |
|
| 77 |
#' "Collaboration") |
|
| 78 |
#' set <- RS.data |> |
|
| 79 |
#' accumulate(c("Condition", "UserName"), codes, c("Condition", "GroupName")) |>
|
|
| 80 |
#' model() |
|
| 81 |
#' spaced <- move_nodes_to_unit_circle_with_equal_space(set) |
|
| 82 |
#' spaced$rotation$nodes |
|
| 83 |
#' |
|
| 84 |
#' # Another plane, e.g. the second and third dimensions |
|
| 85 |
#' dims <- colnames(as.matrix(set$rotation$nodes)) |
|
| 86 |
#' move_nodes_to_unit_circle_with_equal_space(set, dims[2], dims[3]) |
|
| 87 |
#' @export |
|
| 88 |
move_nodes_to_unit_circle_with_equal_space <- function( |
|
| 89 |
set, |
|
| 90 |
dimension_name_1 = colnames(as.matrix(set$rotation$nodes))[1], |
|
| 91 |
dimension_name_2 = colnames(as.matrix(set$rotation$nodes))[2] |
|
| 92 |
) {
|
|
| 93 | 26x |
move_nodes_on_circle(set, dimension_name_1, dimension_name_2, "equal") |
| 94 |
} |
|
| 95 | ||
| 96 |
# Shared body: libena moves the nodes (mode "radial" or "equal") and |
|
| 97 |
# recomputes the centroids; the set gets them back with its column classes. |
|
| 98 |
move_nodes_on_circle <- function(set, dimension_name_1, dimension_name_2, mode) {
|
|
| 99 | 42x |
nodes <- as.matrix(set$rotation$nodes) |
| 100 | 42x |
dimension_names <- c(dimension_name_1, dimension_name_2) |
| 101 | 42x |
idx <- match(dimension_names, colnames(nodes)) |
| 102 | 42x |
if (anyNA(idx)) {
|
| 103 | 1x |
stop("Unknown dimension(s): ",
|
| 104 | 1x |
paste(dimension_names[is.na(idx)], collapse = ", "), |
| 105 | 1x |
". Use columns of set$rotation$nodes: ", |
| 106 | 1x |
paste(colnames(nodes), collapse = ", ")) |
| 107 |
} |
|
| 108 | ||
| 109 |
# Ordered sets: recompute centroids with the self-connection weight the |
|
| 110 |
# nodes were fit with. Sets fit before it was recorded used 1 (libena |
|
| 111 |
# counted self-connections once from 2026-05 until rENA 0.4.16). |
|
| 112 | 41x |
self_weight <- if (is.null(set$rotation$self.weight)) 1 else set$rotation$self.weight |
| 113 | 41x |
lw <- as.matrix(set$line.weights) |
| 114 | 41x |
key <- set$rotation$adjacency.key |
| 115 | 41x |
moved <- if (!is(set, "ena.ordered.set") && !is.null(key) && ncol(as.matrix(key)) == ncol(lw)) {
|
| 116 |
# Unordered: centroids from the same key-driven weighting the fit used |
|
| 117 |
# (self-connection columns included). |
|
| 118 | 34x |
ft <- key_indices(key, key_codes(key, set$rotation$codes)) |
| 119 | 34x |
move_nodes_to_unit_circle_by_key_c(lw, nodes, idx[1] - 1L, idx[2] - 1L, mode, |
| 120 | 34x |
ft$from, ft$to, self_weight) |
| 121 |
} else {
|
|
| 122 | 7x |
move_nodes_to_unit_circle_c(lw, nodes, idx[1] - 1L, idx[2] - 1L, mode, |
| 123 | 7x |
is(set, "ena.ordered.set"), self_weight) |
| 124 |
} |
|
| 125 | ||
| 126 | 40x |
set$rotation$nodes <- replace_dimension_columns( |
| 127 | 40x |
set$rotation$nodes, moved$nodes[, idx, drop = FALSE], dimension_names |
| 128 |
) |
|
| 129 | 40x |
set$model$centroids <- replace_dimension_columns( |
| 130 | 40x |
set$model$centroids, moved$centroids, colnames(nodes) |
| 131 |
) |
|
| 132 | 40x |
set |
| 133 |
} |
|
| 134 | ||
| 135 |
# A copy of the data.table x (nodes or centroids) with columns `cols` replaced |
|
| 136 |
# by the columns of the matrix `values`, each keeping its attributes (class). |
|
| 137 |
replace_dimension_columns <- function(x, values, cols) {
|
|
| 138 | 80x |
x <- data.table::copy(x) |
| 139 | 80x |
for (k in seq_along(cols)) {
|
| 140 | 833x |
value <- as.numeric(values[, k]) |
| 141 | 833x |
attributes(value) <- attributes(x[[cols[k]]]) |
| 142 | 833x |
data.table::set(x, j = cols[k], value = value) |
| 143 |
} |
|
| 144 | 80x |
x |
| 145 |
} |
| 1 |
##### |
|
| 2 |
#' @title Animated movie of growing ENA trajectories |
|
| 3 |
#' |
|
| 4 |
#' @description Creates an animated Plotly visualization of trajectories growing |
|
| 5 |
#' across shared calendar time / conversation turns. |
|
| 6 |
#' |
|
| 7 |
#' @export |
|
| 8 |
#' |
|
| 9 |
#' @param enaplot \code{\link{ENAplot}} object to use for plotting base canvas
|
|
| 10 |
#' @param points Data frame or matrix of points with 2 coordinate dimensions |
|
| 11 |
#' @param by Vector identifying trajectory groupings (e.g. speaker/unit) |
|
| 12 |
#' @param time Monotonic time/turn index vector corresponding to points |
|
| 13 |
#' @param names Character vector of labels for each trajectory |
|
| 14 |
#' @param colors Character vector of colors for each trajectory |
|
| 15 |
#' @param smooth Character indicating smoothing method: "none" (default, point-to-point) or "poly" (LOOCV-2D polynomial curve) |
|
| 16 |
#' @param poly.degree Optional integer degree for polynomial fit. If NULL, selected automatically via LOOCV-2D |
|
| 17 |
#' @param poly.max.degree Maximum polynomial degree to test in LOOCV-2D (default 3) |
|
| 18 |
#' @param poly.eval.points Number of points along smooth curve (default 100) |
|
| 19 |
#' @param show.points Logical; whether to show discrete point markers along the trajectory (default TRUE) |
|
| 20 |
#' @param frame_ms Milliseconds per animation frame (default 250) |
|
| 21 |
#' @param show_tips Logical; whether to show glowing tip markers at current time position |
|
| 22 |
#' @param title Optional title string |
|
| 23 |
#' @return Plotly widget with play/pause controls and slider |
|
| 24 |
#' ##### |
|
| 25 |
ena.plot.movie = function( |
|
| 26 |
enaplot, |
|
| 27 |
points, |
|
| 28 |
by, |
|
| 29 |
time, |
|
| 30 |
names = NULL, |
|
| 31 |
colors = NULL, |
|
| 32 |
smooth = c("none", "poly"),
|
|
| 33 |
poly.degree = NULL, |
|
| 34 |
poly.max.degree = 3L, |
|
| 35 |
poly.eval.points = 100L, |
|
| 36 |
show.points = TRUE, |
|
| 37 |
frame_ms = 250, |
|
| 38 |
show_tips = TRUE, |
|
| 39 |
title = NULL |
|
| 40 |
) {
|
|
| 41 | 2x |
if (.qe_backend(enaplot)) {
|
| 42 | 1x |
.qe_no_trajectories("ena.plot.movie")
|
| 43 | 1x |
return(enaplot$plot) |
| 44 |
} |
|
| 45 | ||
| 46 | 1x |
if (!requireNamespace("plotly", quietly = TRUE)) {
|
| 47 | ! |
stop("plotly is required for animated trajectory movies")
|
| 48 |
} |
|
| 49 | ||
| 50 | 1x |
if (is(points, "ena.matrix") || any(find_meta_cols(points))) {
|
| 51 | ! |
pts_clean <- remove_meta_data(points) |
| 52 | ! |
pts_df <- data.frame( |
| 53 | ! |
X1 = as.numeric(pts_clean[[1L]]), |
| 54 | ! |
X2 = as.numeric(pts_clean[[2L]]) |
| 55 |
) |
|
| 56 | 1x |
} else if (is.matrix(points)) {
|
| 57 | 1x |
pts_df <- data.frame( |
| 58 | 1x |
X1 = as.numeric(points[, 1L]), |
| 59 | 1x |
X2 = as.numeric(points[, 2L]) |
| 60 |
) |
|
| 61 |
} else {
|
|
| 62 | ! |
pts_raw <- as.data.frame(points) |
| 63 | ! |
num_cols <- vapply(pts_raw, is.numeric, logical(1)) |
| 64 | ! |
if (sum(num_cols) >= 2L) {
|
| 65 | ! |
num_df <- pts_raw[, num_cols, drop = FALSE] |
| 66 | ! |
pts_df <- data.frame( |
| 67 | ! |
X1 = as.numeric(num_df[[1L]]), |
| 68 | ! |
X2 = as.numeric(num_df[[2L]]) |
| 69 |
) |
|
| 70 |
} else {
|
|
| 71 | ! |
stop("`points` must contain at least 2 numeric coordinate columns (X and Y).")
|
| 72 |
} |
|
| 73 |
} |
|
| 74 | 1x |
dim_cols <- c("X1", "X2")
|
| 75 | ||
| 76 | 1x |
by_vec <- as.character(by) |
| 77 | 1x |
time_vec <- as.numeric(time) |
| 78 | ||
| 79 | 1x |
unique_by <- unique(by_vec) |
| 80 | 1x |
n_by <- length(unique_by) |
| 81 | ||
| 82 | 1x |
if (is.null(names)) {
|
| 83 | 1x |
names <- unique_by |
| 84 |
} |
|
| 85 | 1x |
if (is.null(colors)) {
|
| 86 | 1x |
palette <- c("#1650FF", "#CD0BBC", "#00A86B", "#FF8C00", "#9932CC", "#008080")
|
| 87 | 1x |
colors <- palette[((seq_len(n_by) - 1L) %% length(palette)) + 1L] |
| 88 |
} |
|
| 89 | ||
| 90 | 1x |
smooth <- match.arg(smooth) |
| 91 | ||
| 92 |
# Precompute polynomial curves if smooth == "poly" |
|
| 93 | 1x |
poly_curves <- list() |
| 94 | 1x |
if (smooth == "poly") {
|
| 95 | ! |
for (b_idx in seq_along(unique_by)) {
|
| 96 | ! |
u <- unique_by[b_idx] |
| 97 | ! |
u_pts <- pts_df[by_vec == u, , drop = FALSE] |
| 98 | ! |
if (nrow(u_pts) >= 2L) {
|
| 99 | ! |
curve_fit <- .fit_etm_poly_curve( |
| 100 | ! |
points = u_pts[, dim_cols, drop = FALSE], |
| 101 | ! |
degree = poly.degree, |
| 102 | ! |
max_degree = poly.max.degree, |
| 103 | ! |
criterion = "loocv" |
| 104 |
) |
|
| 105 | ! |
poly_curves[[u]] <- as.matrix(.eval_etm_poly_curve(curve_fit, poly.eval.points)) |
| 106 |
} |
|
| 107 |
} |
|
| 108 |
} |
|
| 109 | ||
| 110 |
# All unique time steps in chronological order |
|
| 111 | 1x |
all_times <- sort(unique(time_vec)) |
| 112 | 1x |
if (length(all_times) == 0L) {
|
| 113 | ! |
stop("No valid time points provided for movie.")
|
| 114 |
} |
|
| 115 | ||
| 116 |
# Build cumulative frame data |
|
| 117 | 1x |
frame_list <- vector("list", length(all_times) * n_by * 3L)
|
| 118 | 1x |
fi <- 0L |
| 119 | ||
| 120 | 1x |
for (t_idx in seq_along(all_times)) {
|
| 121 | 30x |
cur_t <- all_times[t_idx] |
| 122 | 30x |
for (b_idx in seq_along(unique_by)) {
|
| 123 | 90x |
u <- unique_by[b_idx] |
| 124 | 90x |
u_all_mask <- (by_vec == u) |
| 125 | 90x |
u_mask <- u_all_mask & (time_vec <= cur_t) |
| 126 | 30x |
if (!any(u_mask)) next |
| 127 | ||
| 128 | 60x |
sub_pts <- pts_df[u_mask, , drop = FALSE] |
| 129 | 60x |
tip_pt <- sub_pts[nrow(sub_pts), , drop = FALSE] |
| 130 | ||
| 131 |
# Determine line coordinates: either smooth poly curve or discrete points |
|
| 132 | 60x |
if (smooth == "poly" && !is.null(poly_curves[[u]])) {
|
| 133 | ! |
total_n <- sum(u_all_mask) |
| 134 | ! |
cur_n <- nrow(sub_pts) |
| 135 | ! |
frac <- if (total_n > 1L) (cur_n - 1L) / (total_n - 1L) else 1.0 |
| 136 | ! |
n_eval <- max(2L, as.integer(ceiling(frac * nrow(poly_curves[[u]])))) |
| 137 | ! |
curve_sub <- poly_curves[[u]][seq_len(min(n_eval, nrow(poly_curves[[u]]))), , drop = FALSE] |
| 138 | ! |
line_x <- curve_sub[, 1L] |
| 139 | ! |
line_y <- curve_sub[, 2L] |
| 140 |
} else {
|
|
| 141 | 60x |
line_x <- sub_pts[[dim_cols[1L]]] |
| 142 | 60x |
line_y <- sub_pts[[dim_cols[2L]]] |
| 143 |
} |
|
| 144 | ||
| 145 | 60x |
fi <- fi + 1L |
| 146 | 60x |
frame_list[[fi]] <- data.frame( |
| 147 | 60x |
x = line_x, |
| 148 | 60x |
y = line_y, |
| 149 | 60x |
unit = u, |
| 150 | 60x |
unit_name = names[b_idx], |
| 151 | 60x |
color = colors[b_idx], |
| 152 | 60x |
time_frame = cur_t, |
| 153 | 60x |
trace_type = "line", |
| 154 | 60x |
stringsAsFactors = FALSE |
| 155 |
) |
|
| 156 | ||
| 157 | 60x |
if (isTRUE(show.points) && smooth == "poly") {
|
| 158 | ! |
fi <- fi + 1L |
| 159 | ! |
frame_list[[fi]] <- data.frame( |
| 160 | ! |
x = sub_pts[[dim_cols[1L]]], |
| 161 | ! |
y = sub_pts[[dim_cols[2L]]], |
| 162 | ! |
unit = u, |
| 163 | ! |
unit_name = names[b_idx], |
| 164 | ! |
color = colors[b_idx], |
| 165 | ! |
time_frame = cur_t, |
| 166 | ! |
trace_type = "points", |
| 167 | ! |
stringsAsFactors = FALSE |
| 168 |
) |
|
| 169 |
} |
|
| 170 | ||
| 171 | 60x |
if (isTRUE(show_tips)) {
|
| 172 | 60x |
fi <- fi + 1L |
| 173 | 60x |
frame_list[[fi]] <- data.frame( |
| 174 | 60x |
x = tip_pt[[dim_cols[1L]]], |
| 175 | 60x |
y = tip_pt[[dim_cols[2L]]], |
| 176 | 60x |
unit = u, |
| 177 | 60x |
unit_name = names[b_idx], |
| 178 | 60x |
color = colors[b_idx], |
| 179 | 60x |
time_frame = cur_t, |
| 180 | 60x |
trace_type = "tip", |
| 181 | 60x |
stringsAsFactors = FALSE |
| 182 |
) |
|
| 183 |
} |
|
| 184 |
} |
|
| 185 |
} |
|
| 186 | ||
| 187 | 1x |
movie_df <- do.call(rbind, frame_list[seq_len(fi)]) |
| 188 | ||
| 189 |
# Add traces onto base enaplot |
|
| 190 | 1x |
p <- enaplot$plot |
| 191 | ||
| 192 | 1x |
for (b_idx in seq_along(unique_by)) {
|
| 193 | 3x |
u <- unique_by[b_idx] |
| 194 | 3x |
u_line <- movie_df[movie_df$unit == u & movie_df$trace_type == "line", , drop = FALSE] |
| 195 | ||
| 196 | 3x |
p <- plotly::add_trace( |
| 197 | 3x |
p, |
| 198 | 3x |
data = u_line, |
| 199 | 3x |
x = ~x, |
| 200 | 3x |
y = ~y, |
| 201 | 3x |
frame = ~time_frame, |
| 202 | 3x |
name = names[b_idx], |
| 203 | 3x |
type = "scatter", |
| 204 | 3x |
mode = if (smooth == "poly") "lines" else (if (isTRUE(show.points)) "lines+markers" else "lines"), |
| 205 | 3x |
line = list(color = colors[b_idx], width = 2.5), |
| 206 | 3x |
marker = if (smooth != "poly" && isTRUE(show.points)) list(color = colors[b_idx], size = 5) else NULL, |
| 207 | 3x |
showlegend = TRUE |
| 208 |
) |
|
| 209 | ||
| 210 | 3x |
if (smooth == "poly" && isTRUE(show.points)) {
|
| 211 | ! |
u_pts <- movie_df[movie_df$unit == u & movie_df$trace_type == "points", , drop = FALSE] |
| 212 | ! |
p <- plotly::add_trace( |
| 213 | ! |
p, |
| 214 | ! |
data = u_pts, |
| 215 | ! |
x = ~x, |
| 216 | ! |
y = ~y, |
| 217 | ! |
frame = ~time_frame, |
| 218 | ! |
name = paste0(names[b_idx], " (points)"), |
| 219 | ! |
type = "scatter", |
| 220 | ! |
mode = "markers", |
| 221 | ! |
marker = list(color = colors[b_idx], size = 5, opacity = 0.7), |
| 222 | ! |
showlegend = FALSE |
| 223 |
) |
|
| 224 |
} |
|
| 225 | ||
| 226 | 3x |
if (isTRUE(show_tips)) {
|
| 227 | 3x |
u_tips <- movie_df[movie_df$unit == u & movie_df$trace_type == "tip", , drop = FALSE] |
| 228 | 3x |
p <- plotly::add_trace( |
| 229 | 3x |
p, |
| 230 | 3x |
data = u_tips, |
| 231 | 3x |
x = ~x, |
| 232 | 3x |
y = ~y, |
| 233 | 3x |
frame = ~time_frame, |
| 234 | 3x |
name = paste0(names[b_idx], " (head)"), |
| 235 | 3x |
type = "scatter", |
| 236 | 3x |
mode = "markers", |
| 237 | 3x |
marker = list(color = colors[b_idx], size = 12, symbol = "diamond"), |
| 238 | 3x |
showlegend = FALSE |
| 239 |
) |
|
| 240 |
} |
|
| 241 |
} |
|
| 242 | ||
| 243 | 1x |
p <- plotly::animation_opts( |
| 244 | 1x |
p, |
| 245 | 1x |
frame = frame_ms, |
| 246 | 1x |
transition = 0, |
| 247 | 1x |
redraw = TRUE |
| 248 |
) |
|
| 249 | ||
| 250 | 1x |
p <- plotly::animation_slider( |
| 251 | 1x |
p, |
| 252 | 1x |
currentvalue = list(prefix = "Time / Turn: ", font = list(color = "#333333")) |
| 253 |
) |
|
| 254 | ||
| 255 | 1x |
if (!is.null(title)) {
|
| 256 | ! |
p <- plotly::layout(p, title = list(text = title)) |
| 257 |
} |
|
| 258 | ||
| 259 | 1x |
p |
| 260 |
} |
| 1 |
# Ellipsoidal scaling version |
|
| 2 |
lws.positions.sq <- function(enaset) {
|
|
| 3 | 80x |
points = as.matrix(enaset$points) |
| 4 | 80x |
weights = as.matrix(enaset$line.weights) |
| 5 | 80x |
positions = lws_lsq_positions(weights, points, ncol(points)); |
| 6 | ||
| 7 | 80x |
node.positions = positions$nodes; |
| 8 | 80x |
rownames(node.positions) = enaset$rotation$codes |
| 9 | 80x |
colnames(node.positions) = colnames(points) |
| 10 |
|
|
| 11 | 80x |
return(list("node.positions" = node.positions, "centroids" = positions$centroids))
|
| 12 |
} |
|
| 13 | ||
| 14 |
lws.positions.sq.R6 <- function(enaset) {
|
|
| 15 | 7x |
if( enaset$function.params$center.align.to.origin ) {
|
| 16 | 7x |
non_zero_rows <- rowSums(as.matrix(enaset$line.weights)) != 0 |
| 17 | 7x |
positions = lws_lsq_positions(enaset$line.weights[non_zero_rows,], enaset$points.rotated[non_zero_rows,], ncol(enaset$points.rotated)); |
| 18 | 7x |
mean_centroids = colMeans(positions$centroids); |
| 19 | 7x |
centroids = enaset$points.rotated; |
| 20 | ||
| 21 | 7x |
non_zero_row_centroids = rowSums(as.matrix(centroids))!=0; |
| 22 | 7x |
centroids[non_zero_row_centroids,] = t(t(positions$centroids) - mean_centroids) |
| 23 | 7x |
positions$centroids = centroids; |
| 24 | 7x |
positions$nodes = t(t(positions$nodes)-mean_centroids) |
| 25 |
} |
|
| 26 |
else {
|
|
| 27 | ! |
positions = lws_lsq_positions(enaset$line.weights, enaset$points.rotated, ncol(enaset$points.rotated)); |
| 28 |
} |
|
| 29 | ||
| 30 | 7x |
node.positions = positions$nodes; |
| 31 | 7x |
rownames(node.positions) = enaset$enadata$codes; |
| 32 | ||
| 33 | 7x |
return(list("node.positions" = node.positions, "centroids" = positions$centroids))
|
| 34 | ||
| 35 |
} |
| 1 |
CLASS_NAMES <- list( |
|
| 2 |
data = "qe.data", |
|
| 3 |
meta = "qe.metadata", |
|
| 4 |
code = "qe.code", |
|
| 5 |
unit = "qe.unit", |
|
| 6 |
horizon = "qe.horizon" |
|
| 7 |
) |
|
| 8 | ||
| 9 |
WARNINGS <- list( |
|
| 10 |
data_from_vector = "Cannot transform vectors to `qe.data`", |
|
| 11 |
null_metadata = "`metadata` must be supplied as a vector of column names. No metadata classified.", |
|
| 12 |
null_codes = "`codes` must be supplied as a vector of column names. No codes classified.", |
|
| 13 |
null_units = "`units` must be supplied as a vector of column names. No units classified.", |
|
| 14 |
null_horizon = "`horizon` must be supplied as a vector of column names. No horizon classified." |
|
| 15 |
) |
|
| 16 | ||
| 17 |
#' Convert an object to 'qe.data' class |
|
| 18 |
#' |
|
| 19 |
#' This function converts an object to the 'qe.data' class. If the object is not a data.frame or matrix, it is first converted to a data.table. |
|
| 20 |
#' |
|
| 21 |
#' @param x An object. The object to be converted to 'qe.data' class. |
|
| 22 |
#' |
|
| 23 |
#' @return The modified object with the 'qe.data' class. |
|
| 24 |
#' @examples |
|
| 25 |
#' library(data.table) |
|
| 26 |
#' |
|
| 27 |
#' dt <- data.table( |
|
| 28 |
#' ID = 1:5, |
|
| 29 |
#' Name = c("Alice", "Bob", "Charlie", "David", "Eve"),
|
|
| 30 |
#' Age = c(25, 30, 35, 40, 45), |
|
| 31 |
#' Score = c(85, 90, 95, 80, 75) |
|
| 32 |
#' ) |
|
| 33 |
#' dt <- as.qe.data(dt); |
|
| 34 |
#' class(dt) # Should show 'qe.data' along with other classes |
|
| 35 |
#' |
|
| 36 |
#' @export |
|
| 37 |
as.qe.data <- function(x) {
|
|
| 38 | ! |
if(!is.qe.data(x)) {
|
| 39 | ! |
if(is.vector(x)) {
|
| 40 | ! |
warning(WARNINGS$data_from_vector); |
| 41 |
} |
|
| 42 |
else {
|
|
| 43 |
if( |
|
| 44 | ! |
is.matrix(x) || |
| 45 | ! |
(is.data.frame(x) && !data.table::is.data.table(x)) |
| 46 |
) {
|
|
| 47 | ! |
x <- data.table::as.data.table(x); |
| 48 |
} |
|
| 49 | ! |
class(x) <- c(CLASS_NAMES$data, class(x)); |
| 50 |
} |
|
| 51 |
} |
|
| 52 | ||
| 53 |
# return(data.table::copy(x)); |
|
| 54 | ! |
return(x); |
| 55 |
} |
|
| 56 | ||
| 57 |
#' Convert a vector to 'qe.code' class |
|
| 58 |
#' |
|
| 59 |
#' This function converts a vector to the 'qe.code' class. If the vector is a factor, it is first converted to a character vector. |
|
| 60 |
#' |
|
| 61 |
#' @param x A vector. The vector to be converted to 'qe.code' class. |
|
| 62 |
#' |
|
| 63 |
#' @return The modified vector with the 'qe.code' class. |
|
| 64 |
#' @examples |
|
| 65 |
#' vec <- factor(c("A", "B", "C"))
|
|
| 66 |
#' vec <- as.qe.code(vec) |
|
| 67 |
#' class(vec) # Should show 'qe.code' along with other classes |
|
| 68 |
#' @export |
|
| 69 |
as.qe.code <- function(x) {
|
|
| 70 | ! |
if(is.qe.code(x)) return(x); |
| 71 | ||
| 72 | ! |
if(is.factor(x)) {
|
| 73 | ! |
x <- as.character(x); |
| 74 |
} |
|
| 75 | ! |
class(x) <- c(CLASS_NAMES$code, class(x)); |
| 76 | ||
| 77 | ! |
return(x); |
| 78 |
} |
|
| 79 | ||
| 80 |
#' Convert a vector to 'qe.metadata' class |
|
| 81 |
#' |
|
| 82 |
#' This function converts a vector to the 'qe.metadata' class. If the vector is a factor, it is first converted to a character vector. |
|
| 83 |
#' |
|
| 84 |
#' @param x A vector. The vector to be converted to 'qe.metadata' class. |
|
| 85 |
#' |
|
| 86 |
#' @return The modified vector with the 'qe.metadata' class. |
|
| 87 |
#' @examples |
|
| 88 |
#' vec <- factor(c("A", "B", "C"))
|
|
| 89 |
#' vec <- as.qe.metadata(vec) |
|
| 90 |
#' class(vec) # Should show 'qe.metadata' along with other classes |
|
| 91 |
#' @export |
|
| 92 |
as.qe.metadata <- function(x) {
|
|
| 93 | ! |
if(is.qe.metadata(x)) return(x); |
| 94 | ||
| 95 | ! |
if(is.factor(x)) {
|
| 96 | ! |
x <- as.character(x); |
| 97 |
} |
|
| 98 | ! |
class(x) <- c(CLASS_NAMES$meta, class(x)); |
| 99 | ||
| 100 | ! |
return(x); |
| 101 |
} |
|
| 102 | ||
| 103 |
#' Convert a vector to 'qe.unit' class |
|
| 104 |
#' |
|
| 105 |
#' This function converts a vector to the 'qe.unit' class. If the vector is a factor, it is first converted to a character vector. |
|
| 106 |
#' |
|
| 107 |
#' @param x A vector. The vector to be converted to 'qe.unit' class. |
|
| 108 |
#' |
|
| 109 |
#' @return The modified vector with the 'qe.unit' class. |
|
| 110 |
#' @examples |
|
| 111 |
#' vec <- factor(c("A", "B", "C"))
|
|
| 112 |
#' vec <- as.qe.unit(vec) |
|
| 113 |
#' class(vec) # Should show 'qe.unit' along with other classes |
|
| 114 |
#' @export |
|
| 115 |
as.qe.unit <- function(x) {
|
|
| 116 | ! |
if(is.qe.unit(x)) return(x); |
| 117 | ||
| 118 | ! |
if(is.factor(x)) {
|
| 119 | ! |
x <- as.character(x); |
| 120 |
} |
|
| 121 | ! |
class(x) <- c(CLASS_NAMES$unit, class(x)); |
| 122 | ||
| 123 | ! |
return(x); |
| 124 |
} |
|
| 125 | ||
| 126 |
#' Convert a vector to 'qe.horizon' class |
|
| 127 |
#' |
|
| 128 |
#' This function converts a vector to the 'qe.horizon' class. If the vector is a factor, it is first converted to a character vector. |
|
| 129 |
#' |
|
| 130 |
#' @param x A vector. The vector to be converted to 'qe.horizon' class. |
|
| 131 |
#' |
|
| 132 |
#' @return The modified vector with the 'qe.horizon' class. |
|
| 133 |
#' @examples |
|
| 134 |
#' vec <- factor(c("A", "B", "C"))
|
|
| 135 |
#' vec <- as.qe.horizon(vec) |
|
| 136 |
#' class(vec) # Should show 'qe.horizon' along with other classes |
|
| 137 |
#' @export |
|
| 138 |
as.qe.horizon <- function(x) {
|
|
| 139 | ! |
if(is.qe.horizon(x)) return(x); |
| 140 | ||
| 141 | ! |
if(is.factor(x)) {
|
| 142 | ! |
x <- as.character(x); |
| 143 |
} |
|
| 144 | ! |
class(x) <- c(CLASS_NAMES$horizon, class(x)); |
| 145 | ||
| 146 | ! |
return(x); |
| 147 |
} |
|
| 148 | ||
| 149 |
#' Check if an object is of class 'qe.data' |
|
| 150 |
#' |
|
| 151 |
#' This function checks if an object is of class 'qe.data'. |
|
| 152 |
#' |
|
| 153 |
#' @param x An object. The object to be checked. |
|
| 154 |
#' |
|
| 155 |
#' @return A logical value. TRUE if the object is of class 'qe.data', otherwise FALSE. |
|
| 156 |
#' @examples |
|
| 157 |
#' library(data.table) |
|
| 158 |
#' |
|
| 159 |
#' dt <- data.table(ID = 1:5) |
|
| 160 |
#' class(dt) <- c("qe.data", class(dt))
|
|
| 161 |
#' is.qe.data(dt) # Should return TRUE |
|
| 162 |
#' @export |
|
| 163 |
is.qe.data <- function(x) {
|
|
| 164 | ! |
return(CLASS_NAMES$data %in% class(x)); |
| 165 |
} |
|
| 166 | ||
| 167 |
#' Check if an object is of class 'qe.code' |
|
| 168 |
#' |
|
| 169 |
#' This function checks if an object is of class 'qe.code'. |
|
| 170 |
#' |
|
| 171 |
#' @param x An object. The object to be checked. |
|
| 172 |
#' |
|
| 173 |
#' @return A logical value. TRUE if the object is of class 'qe.code', otherwise FALSE. |
|
| 174 |
#' @examples |
|
| 175 |
#' dt <- 1:5 |
|
| 176 |
#' class(dt) <- c("qe.code", class(dt))
|
|
| 177 |
#' is.qe.code(dt) # Should return TRUE |
|
| 178 |
#' @export |
|
| 179 |
is.qe.code <- function(x) {
|
|
| 180 | ! |
return(CLASS_NAMES$code %in% class(x)); |
| 181 |
} |
|
| 182 | ||
| 183 |
#' Check if an object is of class 'qe.metadata' |
|
| 184 |
#' |
|
| 185 |
#' This function checks if an object is of class 'qe.metadata'. |
|
| 186 |
#' |
|
| 187 |
#' @param x An object. The object to be checked. |
|
| 188 |
#' |
|
| 189 |
#' @return A logical value. TRUE if the object is of class 'qe.metadata', otherwise FALSE. |
|
| 190 |
#' @examples |
|
| 191 |
#' dt <- 1:5 |
|
| 192 |
#' class(dt) <- c("qe.metadata", class(dt))
|
|
| 193 |
#' is.qe.metadata(dt) # Should return TRUE |
|
| 194 |
#' @export |
|
| 195 |
is.qe.metadata <- function(x) {
|
|
| 196 | ! |
return(CLASS_NAMES$meta %in% class(x)); |
| 197 |
} |
|
| 198 | ||
| 199 | ||
| 200 |
#' Check if an object is of class 'qe.unit' |
|
| 201 |
#' |
|
| 202 |
#' This function checks if an object is of class 'qe.unit'. |
|
| 203 |
#' |
|
| 204 |
#' @param x An object. The object to be checked. |
|
| 205 |
#' |
|
| 206 |
#' @return A logical value. TRUE if the object is of class 'qe.unit', otherwise FALSE. |
|
| 207 |
#' @examples |
|
| 208 |
#' dt <- 1:5 |
|
| 209 |
#' class(dt) <- c("qe.unit", class(dt))
|
|
| 210 |
#' is.qe.unit(dt) # Should return TRUE |
|
| 211 |
#' @export |
|
| 212 |
is.qe.unit <- function(x) {
|
|
| 213 | ! |
return(CLASS_NAMES$unit %in% class(x)); |
| 214 |
} |
|
| 215 | ||
| 216 |
#' Check if an object is of class 'qe.horizon' |
|
| 217 |
#' |
|
| 218 |
#' This function checks if an object is of class 'qe.horizon'. |
|
| 219 |
#' |
|
| 220 |
#' @param x An object. The object to be checked. |
|
| 221 |
#' |
|
| 222 |
#' @return A logical value. TRUE if the object is of class 'qe.horizon', otherwise FALSE. |
|
| 223 |
#' @examples |
|
| 224 |
#' dt <- 1:5 |
|
| 225 |
#' class(dt) <- c("qe.horizon", class(dt))
|
|
| 226 |
#' is.qe.horizon(dt) # Should return TRUE |
|
| 227 |
#' @export |
|
| 228 |
is.qe.horizon <- function(x) {
|
|
| 229 | ! |
return(CLASS_NAMES$horizon %in% class(x)); |
| 230 |
} |
| 1 |
#### |
|
| 2 |
#' @title ENAset R6class |
|
| 3 |
#' |
|
| 4 |
#' @docType class |
|
| 5 |
#' @importFrom R6 R6Class |
|
| 6 |
#' @import data.table |
|
| 7 |
#' @export |
|
| 8 | ||
| 9 |
#' @field enadata An \code{\link{ENAdata}} object originally used to create the set
|
|
| 10 |
#' @field points.raw A data frame containing accumulated adjacency (co-occurrence) vectors per unit |
|
| 11 |
#' @field points.normed.centered A data frame of centered normed accumulated adjacency (co-occurrence) vectors for each unit |
|
| 12 |
#' @field points.rotated A data frame of point positions for number of dimensions specified in ena.make.set (i.e., the centered, normed, and rotated data) |
|
| 13 |
#' @field line.weights A data frame of connections strengths per unit (Data frame of normed accumu- lated adjacency (co-occurrence) vectors for each unit) |
|
| 14 |
#' @field node.positions - A data frame of positions for each code |
|
| 15 |
#' @field codes - A vector of code names |
|
| 16 |
#' @field rotation.set - An \code{\link{ENARotationSet}} object
|
|
| 17 |
#' @field variance - A vector of variance accounted for by each dimension specified |
|
| 18 |
#' @field centroids - A matrix of the calculated centroid positions |
|
| 19 |
#' @field function.call - The string representation of function called |
|
| 20 |
#' @field function.params - A list of all parameters sent to function call |
|
| 21 |
#' @field rotation_dists TBD |
|
| 22 |
#' @field points.rotated.scaled TBD |
|
| 23 |
#' @field points.rotated.non.zero TBD |
|
| 24 |
#' @field line.weights.unrotated TBD |
|
| 25 |
#' @field line.weights.non.zero TBD |
|
| 26 |
#' @field correlations A data frame of spearman and pearson correlations for each dimension specified |
|
| 27 |
#' @field center.align.to.origin - align point and centroid centers to origin |
|
| 28 |
#### |
|
| 29 |
ENAset = R6::R6Class("ENAset",
|
|
| 30 |
public = list( |
|
| 31 | ||
| 32 | ||
| 33 |
## Public Functions ---- |
|
| 34 |
#' Create ENAset |
|
| 35 |
#' |
|
| 36 |
#' @param enadata TBD |
|
| 37 |
#' @param dimensions TBD |
|
| 38 |
#' @param norm.by TBD |
|
| 39 |
#' @param rotation.by TBD |
|
| 40 |
#' @param rotation.params TBD |
|
| 41 |
#' @param rotation.set TBD |
|
| 42 |
#' @param node.position.method TBD |
|
| 43 |
#' @param endpoints.only TBD |
|
| 44 |
#' @param center.align.to.origin TBD |
|
| 45 |
#' @param ... TBD |
|
| 46 |
#' |
|
| 47 |
#' @return ENAset |
|
| 48 |
initialize = function( |
|
| 49 |
enadata, |
|
| 50 |
dimensions = 2, |
|
| 51 | ||
| 52 |
norm.by = fun_sphere_norm, |
|
| 53 | ||
| 54 |
rotation.by = ena.svd.R6, |
|
| 55 |
rotation.params = NULL, |
|
| 56 |
rotation.set = NULL, |
|
| 57 | ||
| 58 |
#center.data = center_data_c, ### made local to run |
|
| 59 |
node.position.method = lws.positions.sq.R6, |
|
| 60 |
endpoints.only = TRUE, |
|
| 61 |
center.align.to.origin = TRUE, |
|
| 62 |
... |
|
| 63 |
) {
|
|
| 64 | 13x |
self$enadata <- enadata; |
| 65 | ||
| 66 | 13x |
private$dimensions <- dimensions; |
| 67 | ||
| 68 | 13x |
self$codes <- enadata$codes; |
| 69 | ||
| 70 | 13x |
self$function.call <- sys.call(-1); |
| 71 | ||
| 72 | 13x |
self$function.params$norm.by <- norm.by; #was sphere_norm |
| 73 |
#self$function.params$center.data <- center.data; |
|
| 74 | 13x |
self$function.params$node.position.method <- node.position.method; #was position.method |
| 75 | 13x |
self$function.params$rotation.by <- rotation.by; |
| 76 | 13x |
self$function.params$rotation.params <- rotation.params; |
| 77 | 13x |
self$function.params$rotation.set <- rotation.set; |
| 78 | 13x |
self$function.params$endpoints.only <- endpoints.only; |
| 79 | 13x |
self$function.params$center.align.to.origin <- center.align.to.origin; |
| 80 | 13x |
private$args <- list(...); |
| 81 |
}, |
|
| 82 | ||
| 83 | ||
| 84 |
#' Process ENAset |
|
| 85 |
#' |
|
| 86 |
#' @return ENASet |
|
| 87 |
process = function() {
|
|
| 88 | 13x |
return(private$run()) |
| 89 |
}, |
|
| 90 | ||
| 91 |
#' Get property from object |
|
| 92 |
#' |
|
| 93 |
#' @param x character key to retrieve from object |
|
| 94 |
#' @return value from object at x |
|
| 95 |
get = function(x = "enadata") {
|
|
| 96 | ! |
return(private[[x]]) |
| 97 |
}, |
|
| 98 | ||
| 99 |
## Public Properties ---- |
|
| 100 |
rotation_dists = NULL, #leave for now - to be removed for a temp variable |
|
| 101 |
enadata = NULL, |
|
| 102 |
points.raw = NULL, #was data$raw |
|
| 103 |
points.normed.centered = NULL, #was data$centered$normed |
|
| 104 |
points.rotated = NULL, #was data$centered$rotated |
|
| 105 |
points.rotated.scaled = NULL, |
|
| 106 |
points.rotated.non.zero = NULL, |
|
| 107 |
line.weights = NULL, #was data$normed |
|
| 108 |
line.weights.non.zero = NULL, |
|
| 109 |
line.weights.unrotated = NULL, |
|
| 110 |
node.positions = NULL, #was nodes$positions$scaled |
|
| 111 |
codes = NULL, |
|
| 112 |
rotation.set = NULL, ## new - ENARotation object |
|
| 113 |
correlations = NULL, #not formerly listed, comes from optimized node positions in egr.positions |
|
| 114 |
variance = NULL, #was self$data$centered$latent |
|
| 115 |
centroids = NULL, |
|
| 116 |
center.align.to.origin = TRUE, |
|
| 117 |
function.call = NULL, #new - string reping function call |
|
| 118 |
function.params = list( #list containing parameters function was called with |
|
| 119 |
norm.by = NULL, |
|
| 120 |
node.position.method = NULL, |
|
| 121 |
rotation.by = NULL, |
|
| 122 |
rotation.params = NULL, |
|
| 123 |
endpoints.only = NULL, |
|
| 124 |
center.align.to.origin = TRUE |
|
| 125 |
) |
|
| 126 |
), |
|
| 127 | ||
| 128 |
private = list( |
|
| 129 | ||
| 130 |
## Private Properties ---- |
|
| 131 |
args = NULL, |
|
| 132 |
data.original = NULL, |
|
| 133 |
optim = NULL, |
|
| 134 | ||
| 135 |
#moved from public |
|
| 136 |
dimensions = 2, |
|
| 137 | ||
| 138 |
## Private Functions ---- |
|
| 139 |
run = function() {
|
|
| 140 | 13x |
df = self$enadata$adjacency.vectors; |
| 141 | ||
| 142 |
# Backup of ENA data, this is not touched again. |
|
| 143 |
#private$data.original = df[,grep("adjacency.code", colnames(df)), with=F];
|
|
| 144 | 13x |
private$data.original = df; |
| 145 | ||
| 146 |
# carry this out for node positioning |
|
| 147 | 13x |
self$function.params$center.align.to.origin = self$center.align.to.origin; |
| 148 | ||
| 149 |
# Copy of the original data, this is used for all |
|
| 150 |
# further operations. Unlike, `data.original`, this |
|
| 151 |
# is likely to be overwritten. |
|
| 152 | 13x |
self$points.raw = data.table::copy(private$data.original); |
| 153 | ||
| 154 |
### |
|
| 155 |
# Normalize the raw data using self$function.params$norm.by, |
|
| 156 |
# which defaults to calling rENA::.sphere_norm |
|
| 157 |
### |
|
| 158 | 13x |
self$line.weights = self$function.params$norm.by(self$points.raw); |
| 159 | ||
| 160 |
### |
|
| 161 |
# Convert the string vector of code names to their corresponding |
|
| 162 |
# co-occurence names and set as colnames for the self$line.weights |
|
| 163 |
## |
|
| 164 | 13x |
codeNames_tri = svector_to_ut(self$enadata$codes); |
| 165 | ||
| 166 | 13x |
colnames(self$line.weights) = codeNames_tri; |
| 167 |
# set the rownames to that of the original ENAdata file object |
|
| 168 | 13x |
rownames(self$line.weights) = rownames(df); |
| 169 | ||
| 170 | 13x |
attr(self$line.weights, opts$UNIT_NAMES) = attr(df, opts$UNIT_NAMES) #df[, .SD, with=T, .SDcols=self$enadata$get("unitsBy")];
|
| 171 |
### |
|
| 172 | ||
| 173 | ||
| 174 |
### |
|
| 175 |
# Center the normed data |
|
| 176 |
# FIX - store as $data$centered |
|
| 177 |
### |
|
| 178 |
#### ISSUE |
|
| 179 | 13x |
if (self$center.align.to.origin) {
|
| 180 |
# only centers non-zero networks |
|
| 181 | 13x |
self$points.normed.centered = self$line.weights; |
| 182 | ||
| 183 | 13x |
non_zero_rows <- rowSums(as.matrix(self$line.weights)) != 0; |
| 184 | 13x |
self$points.normed.centered[non_zero_rows,] = center_data_c(self$line.weights[non_zero_rows,]); |
| 185 |
} |
|
| 186 |
else {
|
|
| 187 | ! |
self$points.normed.centered = center_data_c(self$line.weights); |
| 188 |
} |
|
| 189 | 13x |
colnames(self$points.normed.centered) = codeNames_tri; |
| 190 | 13x |
rownames(self$points.normed.centered) = rownames(df); |
| 191 | 13x |
attr(self$points.normed.centered, opts$UNIT_NAMES) = attr(self$enadata$adjacency.vectors.raw, opts$UNIT_NAMES) |
| 192 | ||
| 193 |
### |
|
| 194 | ||
| 195 |
### |
|
| 196 |
# Generate and Assign the rotation set |
|
| 197 |
### |
|
| 198 | 13x |
if(is.function(self$function.params$rotation.by) && is.null(self$function.params$rotation.set)) {
|
| 199 | 8x |
self$rotation.set = do.call(self$function.params$rotation.by, list(self, self$function.params$rotation.params)); |
| 200 |
} |
|
| 201 | 5x |
else if (!is.null(self$function.params$rotation.set)) {
|
| 202 | 4x |
if(is(self$function.params$rotation.set, "ENARotationSet")) {
|
| 203 | 2x |
self$rotation.set = self$function.params$rotation.set; |
| 204 |
} else {
|
|
| 205 | 2x |
stop("Supplied rotation.set is not an instance of ENARotationSet")
|
| 206 |
} |
|
| 207 |
} |
|
| 208 |
else {
|
|
| 209 | 1x |
stop("Unable to find or create a rotation set")
|
| 210 |
} |
|
| 211 |
### |
|
| 212 | ||
| 213 |
### |
|
| 214 |
# Generated the rotated points |
|
| 215 |
### |
|
| 216 | 10x |
self$points.rotated = self$points.normed.centered %*% self$rotation.set$rotation; |
| 217 | 10x |
private$dimensions = min(private$dimensions, ncol(self$points.rotated)) |
| 218 | 10x |
attr(self$points.rotated, opts$UNIT_NAMES) = attr(self$points.normed.centered, opts$UNIT_NAMES); |
| 219 |
### |
|
| 220 | ||
| 221 |
### |
|
| 222 |
# Calculate node positions |
|
| 223 |
# - The supplied methoed is responsible is expected to return a list |
|
| 224 |
# with two keys, "node.positions" and "centroids" |
|
| 225 |
### |
|
| 226 | 10x |
if(!is.null(self$rotation.set) && is.null(self$function.params$rotation.set)) {
|
| 227 | 8x |
positions = self$function.params$node.position.method(self); |
| 228 | 8x |
if(all(names(positions) %in% c("node.positions","centroids"))) {
|
| 229 | 7x |
self$node.positions = positions$node.positions |
| 230 | 7x |
self$centroids = positions$centroids |
| 231 | ||
| 232 | 7x |
self$rotation.set$node.positions = positions$node.positions |
| 233 |
} |
|
| 234 |
else {
|
|
| 235 | 1x |
stop(paste( |
| 236 | 1x |
"The node position method didn't return back the expected objects:", |
| 237 | 1x |
"\tExpected: c('node.positions','centroids')",
|
| 238 | 1x |
paste("\tReceived: ", names(positions), sep=""),
|
| 239 | 1x |
sep = "\n" |
| 240 |
)); |
|
| 241 |
} |
|
| 242 |
} |
|
| 243 |
else {
|
|
| 244 | 2x |
if (!is.null(self$function.params$rotation.set) && !is.null(self$function.params$rotation.set$node.positions)) {
|
| 245 | 1x |
self$node.positions = self$function.params$rotation.set$node.positions |
| 246 |
} |
|
| 247 |
else {
|
|
| 248 | 1x |
stop("Unable to determine the node positions either by calculating
|
| 249 | 1x |
them using `node.position.method` or using a supplied |
| 250 | 1x |
`rotation.set`"); |
| 251 |
} |
|
| 252 |
} |
|
| 253 |
### |
|
| 254 | ||
| 255 |
### |
|
| 256 |
# Variance |
|
| 257 |
### |
|
| 258 | 8x |
variance.of.rotated.data = var(self$points.rotated) |
| 259 | 8x |
diagonal.of.variance.of.rotated.data = as.vector(diag(variance.of.rotated.data)) |
| 260 | 8x |
self$variance = diagonal.of.variance.of.rotated.data/sum(diagonal.of.variance.of.rotated.data) |
| 261 | ||
| 262 | 8x |
return(self); |
| 263 |
} |
|
| 264 |
) |
|
| 265 |
) |
| 1 |
##### |
|
| 2 |
#' |
|
| 3 |
#' @title Wrapper to generate an ENA model |
|
| 4 |
#' |
|
| 5 |
#' @description Generates an ENA model by constructing a dimensional reduction |
|
| 6 |
#' of adjacency (co-occurrence) vectors as defined by the supplied |
|
| 7 |
#' conversations, units, and codes. |
|
| 8 |
#' |
|
| 9 |
#' @details This function generates an ena.set object given a data.frame, units, |
|
| 10 |
#' conversations, and codes. After accumulating the adjacency (co-occurrence) |
|
| 11 |
#' vectors, computes a dimensional reduction (projection), and calculates node |
|
| 12 |
#' positions in the projected ENA space. Returns location of the units in the |
|
| 13 |
#' projected space, as well as locations for node positions, and normalized |
|
| 14 |
#' adjacency (co-occurrence) vectors to construct network graphs. Includes options |
|
| 15 |
#' for returning statistical tests between groups of units. |
|
| 16 |
#' |
|
| 17 |
#' @param data data.frame with containing metadata and coded columns |
|
| 18 |
#' @param codes vector, numeric or character, of columns with codes |
|
| 19 |
#' @param units vector, numeric or character, of columns representing units |
|
| 20 |
#' @param conversation vector, numeric or character, of columns to segment conversations by |
|
| 21 |
#' @param metadata vector, numeric or character, of columns with additional meta information for units |
|
| 22 |
#' @param model character, the ENA model to construct: \code{EndPoint} (default) produces a single adjacency vector per unit summing co-occurrences across all lines; \code{AccumulatedTrajectory} produces one adjacency vector per unit per conversation, where each successive conversation accumulates prior ones; \code{SeparateTrajectory} produces one adjacency vector per unit per conversation, each modeled independently
|
|
| 23 |
#' @param weight.by "binary" is default, can supply a function to call (e.g. sum) |
|
| 24 |
#' @param window MovingStanzaWindow (default) or Conversation |
|
| 25 |
#' @param window.size.back integer, number of lines back from each line to include in the stanza window (default: 1) |
|
| 26 |
#' @param window.size.forward integer, number of lines forward from each line to include in the stanza window (default: 0). Set to model bidirectional co-occurrence within a window. |
|
| 27 |
#' @param include.meta logical, if TRUE (default) unit metadata is attached to the resulting ENAdata object and accessible via the set; set to FALSE to omit metadata from the model output |
|
| 28 |
#' @param groupVar vector, character, of column name containing group identifiers. |
|
| 29 |
#' If column contains at least two unique values, will generate model using a means rotation (a dimensional reduction maximizing the variance between the means of the two groups) |
|
| 30 |
#' @param groups vector, character, of values of groupVar column used for means rotation or statistical tests |
|
| 31 |
#' @param runTest logical, TRUE will run a Student's t-Test and a Wilcoxon test for groups defined by the groups argument |
|
| 32 |
#' @param ... Additional parameters passed to model generation, including \code{mask} (an optional binary matrix of size ncol(codes) x ncol(codes) where 0 suppresses co-occurrence modeling between a pair of codes; see \code{\link{ena.accumulate.data}})
|
|
| 33 |
#' |
|
| 34 |
#' |
|
| 35 |
#' @return ena.set object |
|
| 36 |
##### |
|
| 37 |
ena.set.creator = function( |
|
| 38 |
data, |
|
| 39 |
codes, |
|
| 40 |
units, |
|
| 41 |
conversation, |
|
| 42 |
metadata = NULL, |
|
| 43 |
model = c("EndPoint", "AccumulatedTrajectory", "SeparateTrajectory"),
|
|
| 44 |
weight.by = "binary", |
|
| 45 |
window = c("MovingStanzaWindow", "Conversation"),
|
|
| 46 |
window.size.back = 1, |
|
| 47 |
window.size.forward = 0, |
|
| 48 |
include.meta = TRUE, |
|
| 49 |
groupVar = NULL, |
|
| 50 |
groups = NULL, |
|
| 51 |
runTest = FALSE, |
|
| 52 |
... |
|
| 53 |
) {
|
|
| 54 | 23x |
data <- data.table::data.table(data) |
| 55 | ||
| 56 | 23x |
model = match.arg(model) |
| 57 | 23x |
window = match.arg(window) |
| 58 | 23x |
accum = ena.accumulate.data( |
| 59 | 23x |
units = data[, ..units, drop = FALSE], |
| 60 | 23x |
conversation = data[, ..conversation, drop = FALSE], |
| 61 | 23x |
metadata = data[, ..metadata, drop = FALSE], |
| 62 | 23x |
codes = data[, ..codes, drop = FALSE], |
| 63 | 23x |
window = window, |
| 64 | 23x |
window.size.back = window.size.back, |
| 65 | 23x |
window.size.forward = window.size.forward, |
| 66 | 23x |
weight.by = weight.by, |
| 67 | 23x |
model = model, |
| 68 | 23x |
include.meta = include.meta, |
| 69 |
... |
|
| 70 |
); |
|
| 71 | ||
| 72 | 23x |
accum$model$raw.input <- as.data.table(data); |
| 73 | 23x |
accum$model$raw.input$ENA_UNIT <- merge_columns_c(accum$model$raw.input, units) |
| 74 | 23x |
group1 = NULL |
| 75 | 23x |
group2 = NULL |
| 76 | 23x |
group1.rows = NULL |
| 77 | 23x |
group2.rows = NULL |
| 78 | ||
| 79 | 23x |
set_params = list(...) |
| 80 | 23x |
set_params$enadata = accum |
| 81 | ||
| 82 |
### make set if no group column is specified |
|
| 83 | 23x |
if(is.null(groupVar)) {
|
| 84 | 12x |
if(runTest == TRUE) {
|
| 85 | 1x |
warning("Group variable and groups not specified. Unable to run test")
|
| 86 |
} |
|
| 87 |
} |
|
| 88 | ||
| 89 |
### make set if group column is specified, but groups are not |
|
| 90 | 11x |
else if(is.null(groups) == TRUE) {
|
| 91 | 4x |
unique.groups = unique(as.character(data[[groupVar]])) |
| 92 | ||
| 93 | 4x |
if(length(unique.groups) == 1) {
|
| 94 | 2x |
warning("Group variable only contains one unique value. ENAset has been created without means rotation")
|
| 95 | ||
| 96 | 2x |
if(runTest == TRUE) {
|
| 97 | 1x |
warning("Multiple groups not specified. Unable to run test")
|
| 98 |
} |
|
| 99 |
} |
|
| 100 | ||
| 101 |
else{
|
|
| 102 | 2x |
group1 = unique.groups[1] |
| 103 | 2x |
group2 = unique.groups[2] |
| 104 | ||
| 105 | 2x |
message(paste0("No groups specified. Defaulting to means rotation using first two unique group values of group variable: ",group1," and ",group2))
|
| 106 | ||
| 107 | 2x |
set_params$rotation.by = ena.rotate.by.mean |
| 108 | 2x |
set_params$rotation.params = list(accum$meta.data[[groupVar]] == group1, accum$meta.data[[groupVar]] == group2) |
| 109 | ||
| 110 | 2x |
if(runTest == TRUE) {
|
| 111 | 1x |
warning(paste0("No groups specified. Running test on the first two unique group values of the group variable: ",group1," and ",group2))
|
| 112 |
} |
|
| 113 |
} |
|
| 114 |
} |
|
| 115 | 7x |
else if(length(groups) == 1) {
|
| 116 | 2x |
message("Only one group value specified. ENAset has been created without means rotation")
|
| 117 | ||
| 118 | 2x |
if(runTest == TRUE) {
|
| 119 | 1x |
warning("Multiple groups not specified. Unable to run test")
|
| 120 |
} |
|
| 121 |
} |
|
| 122 |
else {
|
|
| 123 | 5x |
group1 = groups[1] |
| 124 | 5x |
group2 = groups[2] |
| 125 | ||
| 126 | 5x |
if(length(groups) > 2) {
|
| 127 | 2x |
warning(paste0("Only two groups are allowed for means rotation. ENAset has been created using a means rotation on the first two groups given: ",group1," and ",group2))
|
| 128 |
} |
|
| 129 | ||
| 130 | 5x |
groups.missing = groups[which(!groups %in% data[[groupVar]])] |
| 131 | 5x |
if(length(groups.missing) > 0) {
|
| 132 | 1x |
stop(paste("Group column does not contain supplied group value(s): ", groups.missing))
|
| 133 |
} |
|
| 134 | ||
| 135 | 4x |
if(runTest == TRUE) {
|
| 136 | 1x |
if(length(groups) > 2) {
|
| 137 | 1x |
warning(paste0("More than two groups specified. Running test on the first two groups: ",group1," and ",group2))
|
| 138 |
} |
|
| 139 |
} |
|
| 140 |
} |
|
| 141 | ||
| 142 | 22x |
if(!any(is.null(c(group1, group2)))) {
|
| 143 | 6x |
set_params$rotation.by = ena.rotate.by.mean |
| 144 | 6x |
set_params$rotation.params = list(accum$meta.data[[groupVar]] == group1, accum$meta.data[[groupVar]] == group2) |
| 145 | ||
| 146 | 6x |
group1.rows = accum$meta.data[[groupVar]] == group1 |
| 147 | 6x |
group2.rows = accum$meta.data[[groupVar]] == group2 |
| 148 |
} |
|
| 149 | ||
| 150 | 22x |
set = do.call(ena.make.set, set_params) |
| 151 | ||
| 152 |
if( |
|
| 153 | 22x |
runTest == TRUE && |
| 154 | 22x |
!any(is.null(c(group1.rows, group2.rows))) |
| 155 |
) {
|
|
| 156 | 2x |
group1.dim1 = as.matrix(set$points)[group1.rows,1] |
| 157 | 2x |
group2.dim1 = as.matrix(set$points)[group2.rows,1] |
| 158 | 2x |
group1.dim2 = as.matrix(set$points)[group1.rows,2] |
| 159 | 2x |
group2.dim2 = as.matrix(set$points)[group2.rows,2] |
| 160 | ||
| 161 | 2x |
set$tests = list( |
| 162 | 2x |
wilcox.test = list( |
| 163 | 2x |
test.dim1 = wilcox.test(x = group1.dim1, y = group2.dim1), |
| 164 | 2x |
test.dim2 = wilcox.test(x = group1.dim2, y = group2.dim2) |
| 165 |
), |
|
| 166 | 2x |
t.test = list( |
| 167 | 2x |
test.dim1 = t.test(x = group1.dim1, y = group2.dim1), |
| 168 | 2x |
test.dim2 = t.test(x = group1.dim2, y = group2.dim2) |
| 169 |
) |
|
| 170 |
) |
|
| 171 |
} else {
|
|
| 172 | 20x |
set$tests = NULL |
| 173 |
} |
|
| 174 | ||
| 175 | 22x |
return(set) |
| 176 |
} |
| 1 |
### plot subtraction ### |
|
| 2 | ||
| 3 |
ena.plot.subtraction = function( |
|
| 4 |
set, |
|
| 5 |
groupVar = NULL, |
|
| 6 |
group1 = NULL, |
|
| 7 |
group2 = NULL, |
|
| 8 |
points = FALSE, |
|
| 9 |
mean = FALSE, |
|
| 10 |
network = TRUE, |
|
| 11 |
networkMultiplier = 1, |
|
| 12 |
subtractionMultiplier = 1, |
|
| 13 |
group1.color = "blue", |
|
| 14 |
group2.color = "red", |
|
| 15 |
confidence.interval = "box", |
|
| 16 |
backend = getOption("rENA.plot.backend", "plotly"),
|
|
| 17 |
... |
|
| 18 |
) {
|
|
| 19 | 3x |
backend <- match.arg(backend, c("plotly", "qeviz"))
|
| 20 |
# qeviz: weights unscaled; the multipliers become magnify. When either is |
|
| 21 |
# set, the side plots are labelled too (at networkMultiplier), as webENA |
|
| 22 |
# labels its side plots "(scaled 1.0x)". |
|
| 23 | 3x |
any.mult <- networkMultiplier != 1 || subtractionMultiplier != 1 |
| 24 | 3x |
wm <- .qe_weight_mult(backend, networkMultiplier) |
| 25 | 3x |
sm <- .qe_weight_mult(backend, subtractionMultiplier) |
| 26 | 3x |
side.mag <- .qe_magnify(backend, networkMultiplier, any.mult) |
| 27 | 3x |
sub.mag <- .qe_magnify(backend, networkMultiplier * subtractionMultiplier, any.mult) |
| 28 | 3x |
group1.rows = set$points[[groupVar]] == group1 |
| 29 | 3x |
group2.rows = set$points[[groupVar]] == group2 |
| 30 | ||
| 31 | 3x |
g1.plot = ena.plot(enaset = set, title = group1, backend = backend, magnify = side.mag) |
| 32 | 3x |
g2.plot = ena.plot(enaset = set, title = group2, backend = backend, magnify = side.mag) |
| 33 | 3x |
sub.plot = ena.plot(enaset = set, title = paste0("Network Subtraction -- ",group1," vs ",group2),
|
| 34 | 3x |
backend = backend, magnify = sub.mag) |
| 35 | ||
| 36 | 3x |
if(network == TRUE) {
|
| 37 | 3x |
g1.lw = as.matrix(set$line.weights)[group1.rows,,drop=FALSE] |
| 38 | 3x |
g1.mean.lw = colMeans(g1.lw) * wm |
| 39 | ||
| 40 | 3x |
g2.lw = as.matrix(set$line.weights)[group2.rows,,drop=FALSE] |
| 41 | 3x |
g2.mean.lw = colMeans(g2.lw) * wm |
| 42 | ||
| 43 | 3x |
sub = (g1.mean.lw - g2.mean.lw) * sm |
| 44 | ||
| 45 | 3x |
g1.plot = ena.plot.network(g1.plot, network = g1.mean.lw, colors = group1.color) |
| 46 | 3x |
g2.plot = ena.plot.network(g2.plot, network = g2.mean.lw, colors = group2.color) |
| 47 | 3x |
sub.plot = ena.plot.network(sub.plot, network = sub) |
| 48 |
} |
|
| 49 | ||
| 50 | 3x |
if(points == TRUE) {
|
| 51 | ! |
g1.points.for.plot = as.matrix(set$points)[group1.rows,,drop=FALSE] |
| 52 | ! |
g2.points.for.plot = as.matrix(set$points)[group2.rows,,drop=FALSE] |
| 53 | ||
| 54 | ! |
g1.plot = ena.plot.points(enaplot = g1.plot, points = g1.points.for.plot, colors = group1.color) |
| 55 | ! |
g2.plot = ena.plot.points(enaplot = g2.plot, points = g2.points.for.plot, colors = group2.color) |
| 56 | ! |
sub.plot = ena.plot.points(enaplot = sub.plot, points = g1.points.for.plot, colors = group1.color) |
| 57 | ! |
sub.plot = ena.plot.points(enaplot = sub.plot, points = g2.points.for.plot, colors = group2.color) |
| 58 |
} |
|
| 59 | ||
| 60 | 3x |
if(mean == TRUE) {
|
| 61 | 1x |
g1.points.for.plot = as.matrix(set$points)[group1.rows,,drop=FALSE] |
| 62 | 1x |
g2.points.for.plot = as.matrix(set$points)[group2.rows,,drop=FALSE] |
| 63 | ||
| 64 | 1x |
g1.plot = ena.plot.group(g1.plot, g1.points.for.plot, colors = group1.color, labels = group1, confidence.interval = confidence.interval) |
| 65 | 1x |
g2.plot = ena.plot.group(g2.plot, g2.points.for.plot, colors = group2.color, labels = group2, confidence.interval = confidence.interval) |
| 66 | 1x |
sub.plot = ena.plot.group(sub.plot, g1.points.for.plot, colors = group1.color, labels = group1, confidence.interval = confidence.interval) |
| 67 | 1x |
sub.plot = ena.plot.group(sub.plot, g2.points.for.plot, colors = group2.color, labels = group2, confidence.interval = confidence.interval) |
| 68 |
} |
|
| 69 | ||
| 70 | 2x |
else if(TRUE %in% c(network,points, mean) == FALSE) {
|
| 71 | ! |
stop("You must set at least one of points, mean, or network to TRUE to obtain a plot.")
|
| 72 |
} |
|
| 73 | ||
| 74 | 3x |
set$plots[[group1]] = g1.plot |
| 75 | 3x |
set$plots[[group2]] = g2.plot |
| 76 | 3x |
set$plots[[paste0(group1,"-",group2)]] = sub.plot |
| 77 | ||
| 78 | 3x |
return(set) |
| 79 |
} |
| 1 |
##### |
|
| 2 |
#' @title Wrapper to generate, and optionally plot, an ENA model |
|
| 3 |
#' |
|
| 4 |
#' @description Convenience entry point for constructing an ENA model from a |
|
| 5 |
#' coded data frame. Handles accumulation, dimensional reduction, and optional |
|
| 6 |
#' plot generation in a single call, returning an \code{ena.set} object that
|
|
| 7 |
#' contains unit positions, network weights, node positions, and plots. |
|
| 8 |
#' |
|
| 9 |
#' @details |
|
| 10 |
#' \code{ena()} runs three phases internally:
|
|
| 11 |
#' |
|
| 12 |
#' \strong{1. Accumulation} — co-occurrence counts are computed for each unit
|
|
| 13 |
#' across stanza windows defined by \code{window}, \code{window.size.back}, and
|
|
| 14 |
#' \code{window.size.forward}.
|
|
| 15 |
#' |
|
| 16 |
#' \strong{2. Dimensional reduction} — accumulated vectors are normed, centered,
|
|
| 17 |
#' and rotated into a low-dimensional ENA space. When \code{groupVar} and two
|
|
| 18 |
#' \code{groups} are supplied the rotation maximises separation between the group
|
|
| 19 |
#' means (means rotation); otherwise SVD is used. |
|
| 20 |
#' |
|
| 21 |
#' \strong{3. Plotting} — plots are built and stored on the returned set in
|
|
| 22 |
#' \code{set$plots}. Pass \code{include.plots = FALSE} to skip this phase
|
|
| 23 |
#' entirely, which is useful for programmatic use (simulations, parameter |
|
| 24 |
#' sweeps) where plot objects are not needed. |
|
| 25 |
#' |
|
| 26 |
#' \strong{Plot defaults:} \code{network = TRUE} but \code{points = FALSE} and
|
|
| 27 |
#' \code{mean = FALSE}. For a two-group comparison you almost always want
|
|
| 28 |
#' \code{mean = TRUE} as well, to show group centroids and confidence intervals
|
|
| 29 |
#' alongside the network. |
|
| 30 |
#' |
|
| 31 |
#' \strong{Accessing results:} the returned \code{ena.set} object contains:
|
|
| 32 |
#' \describe{
|
|
| 33 |
#' \item{\code{$points}}{unit positions in the rotated ENA space (rows = units)}
|
|
| 34 |
#' \item{\code{$line.weights}}{normed co-occurrence weights per unit (rows = units, cols = code pairs)}
|
|
| 35 |
#' \item{\code{$node.positions}}{positions of each code node in the ENA space}
|
|
| 36 |
#' \item{\code{$plots}}{named list of \code{ENAplot} objects; two-group models
|
|
| 37 |
#' produce three plots keyed by \code{group1}, \code{group2}, and
|
|
| 38 |
#' \code{"group1-group2"}}
|
|
| 39 |
#' \item{\code{$tests}}{list of Wilcoxon and t-test results on dimensions 1
|
|
| 40 |
#' and 2, populated when \code{runTest = TRUE}}
|
|
| 41 |
#' \item{\code{$variance}}{proportion of variance explained by each dimension}
|
|
| 42 |
#' } |
|
| 43 |
#' |
|
| 44 |
#' @param data data.frame containing metadata and coded columns |
|
| 45 |
#' @param codes vector, numeric or character, of column names or indices containing the codes to model |
|
| 46 |
#' @param units vector, numeric or character, of column names that together uniquely identify each unit of analysis |
|
| 47 |
#' @param conversation vector, numeric or character, of column names used to segment the data into conversations (stanza boundaries reset at each new conversation) |
|
| 48 |
#' @param metadata vector, numeric or character, of column names to carry through as unit-level metadata (default: NULL) |
|
| 49 |
#' @param model character, the ENA model to construct: \code{EndPoint} (default) produces a single adjacency vector per unit summing co-occurrences across all lines; \code{AccumulatedTrajectory} produces one adjacency vector per unit per conversation, where each successive conversation accumulates prior ones; \code{SeparateTrajectory} produces one adjacency vector per unit per conversation, each modeled independently
|
|
| 50 |
#' @param weight.by how to weight co-occurrences: \code{"binary"} (default) counts each co-occurrence once per stanza window; supply a function (e.g. \code{sum}) to use raw counts
|
|
| 51 |
#' @param window stanza window type: \code{"MovingStanzaWindow"} (default) or \code{"Conversation"} (all lines in a conversation form one window)
|
|
| 52 |
#' @param window.size.back integer, number of lines back from each line to include in the stanza window (default: 1) |
|
| 53 |
#' @param window.size.forward integer, number of lines forward from each line to include in the stanza window (default: 0). Set to model bidirectional co-occurrence within a window. |
|
| 54 |
#' @param include.meta logical, if TRUE (default) unit metadata is attached to the resulting ENAdata object and accessible via the set; set to FALSE to omit metadata from the model output |
|
| 55 |
#' @param groupVar character, name of the column containing group labels. When supplied with two \code{groups}, the model uses a means rotation that maximises variance between group means.
|
|
| 56 |
#' @param groups vector, character, of exactly the group values from \code{groupVar} to use for means rotation, plotting, and statistical tests. If omitted, the first two unique values of \code{groupVar} are used with a warning.
|
|
| 57 |
#' @param runTest logical, if TRUE runs a Wilcoxon rank-sum test and a Student's t-test comparing the two groups on dimensions 1 and 2; results stored in \code{set$tests} (default: FALSE)
|
|
| 58 |
#' @param points logical, TRUE will plot individual unit points (default: FALSE) |
|
| 59 |
#' @param mean logical, TRUE will plot group mean positions with confidence intervals — recommended whenever \code{groupVar} is supplied (default: FALSE)
|
|
| 60 |
#' @param network logical, TRUE will plot mean networks (default: TRUE) |
|
| 61 |
#' @param networkMultiplier numeric, scaling factor applied to edge weights in non-subtracted network plots (default: 1) |
|
| 62 |
#' @param subtractionMultiplier numeric, scaling factor applied to edge weights in the subtracted network plot (default: 1) |
|
| 63 |
#' @param unit character, name of a single unit to plot in isolation; when supplied, all group plotting is skipped |
|
| 64 |
#' @param colors vector, character, of colors for groups or points. For two-group models, supply two values (group1, group2); for single-group or no-group models, supply one value. Defaults to "blue"/"red" for two groups and "black" otherwise. |
|
| 65 |
#' @param confidence.interval character, style of confidence interval shown on mean points: "box" (default), "crosshairs", or "none" |
|
| 66 |
#' @param include.plots logical, if TRUE (default) generates and attaches plot objects to the returned set; set to FALSE to skip all plotting for faster programmatic use |
|
| 67 |
#' @param print.plots logical, if TRUE renders plots in the Viewer as they are created (default: FALSE) |
|
| 68 |
#' @param ... Additional parameters passed to set creation and plotting functions, including \code{mask} (an optional binary matrix of size ncol(codes) x ncol(codes) where 0 suppresses co-occurrence modeling between a pair of codes; see \code{\link{ena.accumulate.data}})
|
|
| 69 |
#' |
|
| 70 |
#' @examples |
|
| 71 |
#' data(RS.data) |
|
| 72 |
#' |
|
| 73 |
#' codes = c('Data',
|
|
| 74 |
#' 'Technical.Constraints', |
|
| 75 |
#' 'Performance.Parameters', |
|
| 76 |
#' 'Client.and.Consultant.Requests', |
|
| 77 |
#' 'Design.Reasoning', |
|
| 78 |
#' 'Collaboration') |
|
| 79 |
#' |
|
| 80 |
#' # Minimal call: fit a model with no group comparison |
|
| 81 |
#' rs = ena( |
|
| 82 |
#' data = RS.data, |
|
| 83 |
#' units = c("UserName", "Condition", "GroupName"),
|
|
| 84 |
#' conversation = c("Condition", "GroupName"),
|
|
| 85 |
#' codes = codes, |
|
| 86 |
#' window.size.back = 4 |
|
| 87 |
#' ) |
|
| 88 |
#' |
|
| 89 |
#' # Two-group comparison with means rotation, centroids, and statistical tests |
|
| 90 |
#' rs = ena( |
|
| 91 |
#' data = RS.data, |
|
| 92 |
#' units = c("UserName", "Condition", "GroupName"),
|
|
| 93 |
#' conversation = c("Condition", "GroupName"),
|
|
| 94 |
#' codes = codes, |
|
| 95 |
#' window.size.back = 4, |
|
| 96 |
#' groupVar = "Condition", |
|
| 97 |
#' groups = c("FirstGame", "SecondGame"),
|
|
| 98 |
#' mean = TRUE, |
|
| 99 |
#' runTest = TRUE, |
|
| 100 |
#' print.plots = FALSE |
|
| 101 |
#' ) |
|
| 102 |
#' |
|
| 103 |
#' # Model fitting only, no plots (faster for programmatic use) |
|
| 104 |
#' rs = ena( |
|
| 105 |
#' data = RS.data, |
|
| 106 |
#' units = c("UserName", "Condition", "GroupName"),
|
|
| 107 |
#' conversation = c("Condition", "GroupName"),
|
|
| 108 |
#' codes = codes, |
|
| 109 |
#' window.size.back = 4, |
|
| 110 |
#' include.plots = FALSE |
|
| 111 |
#' ) |
|
| 112 |
#' |
|
| 113 |
#' @return An \code{ena.set} object. See the Details section for a description
|
|
| 114 |
#' of the key fields (\code{$points}, \code{$line.weights}, \code{$plots},
|
|
| 115 |
#' \code{$tests}, etc.).
|
|
| 116 |
#' @export |
|
| 117 |
##### |
|
| 118 |
ena <- function( |
|
| 119 |
data, |
|
| 120 |
codes, |
|
| 121 |
units, |
|
| 122 |
conversation, |
|
| 123 |
metadata = NULL, |
|
| 124 |
model = c("EndPoint", "AccumulatedTrajectory", "SeparateTrajectory"),
|
|
| 125 |
weight.by = "binary", |
|
| 126 |
window = c("MovingStanzaWindow", "Conversation"),
|
|
| 127 |
window.size.back = 1, |
|
| 128 |
window.size.forward = 0, |
|
| 129 |
include.meta = TRUE, |
|
| 130 |
groupVar = NULL, |
|
| 131 |
groups = NULL, |
|
| 132 |
runTest = FALSE, |
|
| 133 |
points = FALSE, |
|
| 134 |
mean = FALSE, |
|
| 135 |
network = TRUE, |
|
| 136 |
networkMultiplier = 1, |
|
| 137 |
subtractionMultiplier = 1, |
|
| 138 |
unit = NULL, |
|
| 139 |
colors = NULL, |
|
| 140 |
confidence.interval = "box", |
|
| 141 |
include.plots = T, |
|
| 142 |
print.plots = F, |
|
| 143 |
... |
|
| 144 |
) {
|
|
| 145 | 13x |
set <- ena.set.creator( |
| 146 | 13x |
data = data, |
| 147 | 13x |
codes = codes, |
| 148 | 13x |
units = units, |
| 149 | 13x |
conversation = conversation, |
| 150 | 13x |
metadata = metadata, |
| 151 | 13x |
model = model, |
| 152 | 13x |
weight.by = weight.by, |
| 153 | 13x |
window = window, |
| 154 | 13x |
window.size.back = window.size.back, |
| 155 | 13x |
window.size.forward = window.size.forward, |
| 156 | 13x |
include.meta = include.meta, |
| 157 | 13x |
groupVar = groupVar, |
| 158 | 13x |
groups = groups, |
| 159 | 13x |
runTest = runTest, |
| 160 |
... |
|
| 161 |
) |
|
| 162 | ||
| 163 | 13x |
if (include.plots) {
|
| 164 | 13x |
set <- ena.plotter( |
| 165 | 13x |
set = set, |
| 166 | 13x |
groupVar = groupVar, |
| 167 | 13x |
groups = groups, |
| 168 | 13x |
points = points, |
| 169 | 13x |
mean = mean, |
| 170 | 13x |
network = network, |
| 171 | 13x |
networkMultiplier = networkMultiplier, |
| 172 | 13x |
subtractionMultiplier = subtractionMultiplier, |
| 173 | 13x |
unit = unit, |
| 174 | 13x |
colors = colors, |
| 175 | 13x |
confidence.interval = confidence.interval, |
| 176 | 13x |
print.plots = print.plots, |
| 177 |
... |
|
| 178 |
) |
|
| 179 |
} |
|
| 180 | ||
| 181 | 13x |
return(set) |
| 182 |
} |
| 1 |
#' Calculate the Peak Ground Diversity Window |
|
| 2 |
#' |
|
| 3 |
#' This function acts as a wrapper to identify the sliding window size that yields |
|
| 4 |
#' the highest (peak) ground diversity/entropy for a given dataset. |
|
| 5 |
#' |
|
| 6 |
#' @param data A data.frame or data.table containing the conversation data. |
|
| 7 |
#' @param codeNames A character vector of column names representing the binary codes. |
|
| 8 |
#' @param conversation_cols A character vector of column names that define unique conversations. |
|
| 9 |
#' @param max_window An integer specifying the maximum window size to evaluate. Default is 20. |
|
| 10 |
#' |
|
| 11 |
#' @return An integer representing the window size that maximizes ground entropy. |
|
| 12 |
#' @export |
|
| 13 |
#' |
|
| 14 |
#' @examples |
|
| 15 |
#' \dontrun{
|
|
| 16 |
#' peak_w <- ena.gd.window(data = my_data, |
|
| 17 |
#' codeNames = c("CodeA", "CodeB"),
|
|
| 18 |
#' conversation_cols = "ConversationID", |
|
| 19 |
#' max_window = 15) |
|
| 20 |
#' } |
|
| 21 |
ena.gd.window <- function(data, codeNames, conversation_cols, max_window = 20) {
|
|
| 22 | 2x |
gd <- ena.ground.diversity(data, codeNames, conversation_cols, max_window) |
| 23 | 2x |
return(gd$peaks$peak_window) |
| 24 |
} |
|
| 25 | ||
| 26 |
#' Calculate Ground Diversity (Entropy) Curves and Peaks |
|
| 27 |
#' |
|
| 28 |
#' Evaluates the diversity of "ground types" (combinations of active codes) |
|
| 29 |
#' across a series of sliding window sizes. It returns both the curves and the |
|
| 30 |
#' peak window values. |
|
| 31 |
#' |
|
| 32 |
#' @param data A data.frame or data.table containing the conversation data. |
|
| 33 |
#' @param codeNames A character vector of column names representing the binary codes. |
|
| 34 |
#' @param conversation_cols A character vector of column names that define unique conversations. |
|
| 35 |
#' @param max_window An integer specifying the maximum window size to evaluate. Default is 20. |
|
| 36 |
#' |
|
| 37 |
#' @return A list containing three elements: |
|
| 38 |
#' \describe{
|
|
| 39 |
#' \item{curves}{A data.table containing aggregated entropy curves across windows.}
|
|
| 40 |
#' \item{peaks}{A data.table highlighting the window size(s) where entropy peaked.}
|
|
| 41 |
#' \item{conversation_curves}{A data.table showing calculated entropy per individual conversation per window.}
|
|
| 42 |
#' } |
|
| 43 |
#' @export |
|
| 44 |
#' |
|
| 45 |
#' @examples |
|
| 46 |
#' \dontrun{
|
|
| 47 |
#' gd_results <- ena.ground.diversity(data = my_data, |
|
| 48 |
#' codeNames = c("CodeA", "CodeB"),
|
|
| 49 |
#' conversation_cols = "ConversationID") |
|
| 50 |
#' } |
|
| 51 |
ena.ground.diversity <- function(data, codeNames, conversation_cols, max_window = 20) {
|
|
| 52 | 4x |
num_codes <- length(codeNames) |
| 53 | ||
| 54 | 4x |
counts <- ground.type.counts( |
| 55 | 4x |
data = data, |
| 56 | 4x |
codeNames = codeNames, |
| 57 | 4x |
conversation_cols = conversation_cols, |
| 58 | 4x |
max_window = max_window |
| 59 |
) |
|
| 60 | ||
| 61 | 3x |
ground.entropy.curves( |
| 62 | 3x |
counts = counts, |
| 63 | 3x |
conversation_cols = conversation_cols, |
| 64 | 3x |
num_codes = num_codes, |
| 65 | 3x |
methods = "mean", |
| 66 | 3x |
normalize = TRUE |
| 67 |
) |
|
| 68 |
} |
|
| 69 | ||
| 70 |
#' Calculate Entropy Curves from Ground Type Counts |
|
| 71 |
#' |
|
| 72 |
#' Internal helper function that takes ground type frequencies and computes Shannon entropy |
|
| 73 |
#' across different aggregation methods (mean, weighted, or pooled) and window sizes. |
|
| 74 |
#' |
|
| 75 |
#' @param counts A data.table generated by \code{\link{ground.type.counts}}.
|
|
| 76 |
#' @param conversation_cols A character vector of column names defining unique conversations. |
|
| 77 |
#' @param num_codes An integer representing the total number of unique codes (used for normalization base \eqn{2^C}).
|
|
| 78 |
#' @param group_cols A character vector of columns to group by (e.g., experimental conditions). Default is NULL. |
|
| 79 |
#' @param methods A character vector specifying which aggregation methods to calculate. Options include "mean", "weighted", and "pooled". Default is all three. |
|
| 80 |
#' @param normalize Logical; if TRUE (default), entropy is normalized to a \eqn{[0, 1]} scale.
|
|
| 81 |
#' |
|
| 82 |
#' @return A list containing: |
|
| 83 |
#' \describe{
|
|
| 84 |
#' \item{curves}{Aggregated entropy metrics across windows and methods.}
|
|
| 85 |
#' \item{peaks}{The optimal window size that maximized entropy for each method.}
|
|
| 86 |
#' \item{conversation_curves}{Entropy calculations mapped back to individual conversations.}
|
|
| 87 |
#' } |
|
| 88 |
#' @keywords internal |
|
| 89 |
ground.entropy.curves <- function( |
|
| 90 |
counts, |
|
| 91 |
conversation_cols, |
|
| 92 |
num_codes, |
|
| 93 |
group_cols = NULL, |
|
| 94 |
methods = c("mean", "weighted", "pooled"),
|
|
| 95 |
normalize = TRUE |
|
| 96 |
) {
|
|
| 97 | 3x |
dt <- as.data.table(counts) |
| 98 | ||
| 99 | 3x |
if (is.null(group_cols)) {
|
| 100 | 3x |
dt[, .group := "all"] |
| 101 | 3x |
group_cols <- ".group" |
| 102 |
} |
|
| 103 | ||
| 104 | 3x |
log_base <- if (normalize) log(2^num_codes) else 1 |
| 105 | ||
| 106 | 3x |
conv_entropy <- dt[ |
| 107 |
, |
|
| 108 |
{
|
|
| 109 | 1170x |
p <- count / sum(count) |
| 110 |
.( |
|
| 111 | 1170x |
entropy = -sum(p * log(p)) / log_base, |
| 112 | 1170x |
n_unique = .N, |
| 113 | 1170x |
n_obs = unique(n_obs) |
| 114 |
) |
|
| 115 |
}, |
|
| 116 | 3x |
by = c(conversation_cols, group_cols, "window") |
| 117 |
] |
|
| 118 | ||
| 119 | 3x |
results <- list() |
| 120 | ||
| 121 |
# --- Mean --- |
|
| 122 | 3x |
if ("mean" %in% methods) {
|
| 123 | 3x |
tmp <- conv_entropy[ |
| 124 |
, |
|
| 125 |
.( |
|
| 126 | 3x |
entropy = mean(entropy), |
| 127 | 3x |
n_unique = mean(n_unique), |
| 128 | 3x |
n_conversations = .N |
| 129 |
), |
|
| 130 | 3x |
by = c(group_cols, "window") |
| 131 |
] |
|
| 132 | 3x |
tmp[, method := "mean"] |
| 133 | 3x |
results[[length(results) + 1]] <- tmp |
| 134 |
} |
|
| 135 | ||
| 136 |
# --- Weighted --- |
|
| 137 | 3x |
if ("weighted" %in% methods) {
|
| 138 | ! |
tmp <- conv_entropy[ |
| 139 |
, |
|
| 140 |
.( |
|
| 141 | ! |
entropy = weighted.mean(entropy, n_obs), |
| 142 | ! |
n_unique = weighted.mean(n_unique, n_obs), |
| 143 | ! |
n_conversations = .N |
| 144 |
), |
|
| 145 | ! |
by = c(group_cols, "window") |
| 146 |
] |
|
| 147 | ! |
tmp[, method := "weighted"] |
| 148 | ! |
results[[length(results) + 1]] <- tmp |
| 149 |
} |
|
| 150 | ||
| 151 |
# --- Pooled --- |
|
| 152 | 3x |
if ("pooled" %in% methods) {
|
| 153 | ! |
tmp <- dt[ |
| 154 |
, |
|
| 155 | ! |
.(count = sum(count)), |
| 156 | ! |
by = c(group_cols, "window", "ground_type") |
| 157 |
][ |
|
| 158 |
, |
|
| 159 |
{
|
|
| 160 | ! |
p <- count / sum(count) |
| 161 |
.( |
|
| 162 | ! |
entropy = -sum(p * log(p)) / log_base, |
| 163 | ! |
n_unique = .N |
| 164 |
) |
|
| 165 |
}, |
|
| 166 | ! |
by = c(group_cols, "window") |
| 167 |
] |
|
| 168 | ! |
tmp[, method := "pooled"] |
| 169 | ! |
results[[length(results) + 1]] <- tmp |
| 170 |
} |
|
| 171 | ||
| 172 | 3x |
curves <- rbindlist(results, fill = TRUE) |
| 173 | ||
| 174 | 3x |
peaks <- curves[ |
| 175 |
, |
|
| 176 | 3x |
.SD[which.max(entropy)][1], |
| 177 | 3x |
by = c(group_cols, "method") |
| 178 |
][ |
|
| 179 |
, |
|
| 180 |
.( |
|
| 181 | 3x |
peak_window = window, |
| 182 | 3x |
peak_entropy = entropy, |
| 183 | 3x |
method = method |
| 184 |
), |
|
| 185 | 3x |
by = group_cols |
| 186 |
] |
|
| 187 | ||
| 188 | 3x |
list( |
| 189 | 3x |
curves = curves, |
| 190 | 3x |
peaks = peaks, |
| 191 | 3x |
conversation_curves = conv_entropy |
| 192 |
) |
|
| 193 |
} |
|
| 194 | ||
| 195 |
#' Generate Binary-Encoded Ground Type Counts |
|
| 196 |
#' |
|
| 197 |
#' Compiles the frequencies of unique co-occurrence patterns ("ground types")
|
|
| 198 |
#' across multiple sliding window sizes, using fast column-wise rolling maximum operations. |
|
| 199 |
#' |
|
| 200 |
#' @param data A data.frame or data.table. |
|
| 201 |
#' @param codeNames A character vector of column names representing the binary codes. |
|
| 202 |
#' @param conversation_cols A character vector of column names that define unique conversations. |
|
| 203 |
#' @param max_window An integer specifying the maximum window size to evaluate. Default is 20. |
|
| 204 |
#' |
|
| 205 |
#' @return A data.table containing the counts of ground types per conversation per window size. |
|
| 206 |
#' @keywords internal |
|
| 207 |
ground.type.counts <- function( |
|
| 208 |
data, |
|
| 209 |
codeNames, |
|
| 210 |
conversation_cols, |
|
| 211 |
max_window = 20 |
|
| 212 |
) {
|
|
| 213 | 5x |
stopifnot(all(codeNames %in% names(data))) |
| 214 | 5x |
stopifnot(all(conversation_cols %in% names(data))) |
| 215 | ||
| 216 | 5x |
dt <- data.table::as.data.table(data) |
| 217 | ||
| 218 |
# Ground-type encoding assumes each code is a binary presence/absence flag; |
|
| 219 |
# non-binary values would corrupt the bit-weighted `ground_type` id. |
|
| 220 | 5x |
code_vals <- unique(as.vector(as.matrix(dt[, ..codeNames]))) |
| 221 | 5x |
if (!all(code_vals %in% c(0, 1))) {
|
| 222 | 1x |
stop("All codeNames columns must be binary (0/1); found non-binary values.")
|
| 223 |
} |
|
| 224 | 4x |
bit_weights <- 2^(seq_along(codeNames) - 1) |
| 225 | ||
| 226 | 4x |
out <- list() |
| 227 | ||
| 228 | 4x |
for (w in seq_len(max_window)) {
|
| 229 | 44x |
temp_dt <- dt[, c(..conversation_cols, ..codeNames)] |
| 230 | ||
| 231 |
# The rolling max of a binary code column is simply "is any code active in |
|
| 232 |
# the window", i.e. frollsum(col, w) > 0. frollsum runs in C, whereas |
|
| 233 |
# frollapply(col, w, max) invokes an R closure at every window position, so |
|
| 234 |
# this is ~2 orders of magnitude faster for the same result. |
|
| 235 | 44x |
temp_dt[, (codeNames) := lapply(.SD, function(col) {
|
| 236 | 6436x |
rolled <- as.numeric(data.table::frollsum(col, w, align = "right") > 0) |
| 237 | 6436x |
na_indices <- which(is.na(rolled)) |
| 238 | 6436x |
if (length(na_indices) > 0) {
|
| 239 | 5830x |
rolled[na_indices] <- cummax(col[na_indices]) |
| 240 |
} |
|
| 241 | 6436x |
return(rolled) |
| 242 | 44x |
}), by = conversation_cols, .SDcols = codeNames] |
| 243 | ||
| 244 | 44x |
g_matrix <- as.matrix(temp_dt[, ..codeNames]) |
| 245 | 44x |
temp_dt[, ground_type := as.vector(g_matrix %*% bit_weights)] |
| 246 | ||
| 247 | 44x |
counts <- temp_dt[ |
| 248 |
, |
|
| 249 | 44x |
.(count = .N), |
| 250 | 44x |
by = c(conversation_cols, "ground_type") |
| 251 |
] |
|
| 252 | ||
| 253 | 44x |
counts[, window := w] |
| 254 | 44x |
counts[, n_obs := sum(count), by = conversation_cols] |
| 255 | ||
| 256 | 44x |
out[[w]] <- counts |
| 257 |
} |
|
| 258 | ||
| 259 | 4x |
data.table::rbindlist(out, use.names = TRUE) |
| 260 |
} |
| 1 |
## |
|
| 2 |
# @title Accumulate Data from csv |
|
| 3 |
# |
|
| 4 |
# @description This function accumulates rows of data. |
|
| 5 |
# |
|
| 6 |
# @details [TBD] |
|
| 7 |
# |
|
| 8 |
#@export |
|
| 9 |
# |
|
| 10 |
# @param file The csv file location or data.frame for the function |
|
| 11 |
# @param units.used Delimits columns based on the units (which specific units to use) |
|
| 12 |
# @param units.by unit columns to accumulate by |
|
| 13 |
# @param conversations.by Columns used in the conversation |
|
| 14 |
# @param codes Columns used based on codes |
|
| 15 |
# @param window.size.back Number of lines back to include window in stanza |
|
| 16 |
# @param window.size.forward Number of lines forward in stanza window |
|
| 17 |
# @param binary [TBD] |
|
| 18 |
# @param model [TBD] |
|
| 19 |
# @param window [TBD] |
|
| 20 |
# @param weight.by [TBD] |
|
| 21 |
# @param binary.stanzas [TBD] |
|
| 22 |
# @param mask [TBD] |
|
| 23 |
# @param ... additional parameters addressed in inner function |
|
| 24 |
# |
|
| 25 |
# |
|
| 26 |
# @seealso \code{\link{ena.make.set}}
|
|
| 27 |
# |
|
| 28 |
# @examples |
|
| 29 |
# \dontrun{
|
|
| 30 |
# codeNames = c( |
|
| 31 |
# "E.data","S.data","E.design","S.design","S.professional","E.client", |
|
| 32 |
# "V.client","E.consultant","V.consultant","S.collaboration","I.engineer", |
|
| 33 |
# "I.intern","K.actuator","K.rom","K.materials","K.power" |
|
| 34 |
# ) |
|
| 35 |
# |
|
| 36 |
# df.file <- system.file("extdata", "rs.data.csv", package="rENA")
|
|
| 37 |
# |
|
| 38 |
# # Given a csv file location |
|
| 39 |
# ena.accumulate.data( |
|
| 40 |
# df.file, units.by = c("UserName","Condition"),
|
|
| 41 |
# conversations.by = c("ActivityNumber","GroupName"),
|
|
| 42 |
# codes = codeNames |
|
| 43 |
# ) |
|
| 44 |
# } |
|
| 45 |
# @return \code{\link{ENAdata}} class object with accumulated data
|
|
| 46 |
# |
|
| 47 |
## |
|
| 48 |
ena.accumulate.data.file <- function( |
|
| 49 |
file, |
|
| 50 |
units.used = NULL, |
|
| 51 |
conversations.used = NULL, |
|
| 52 |
units.by, |
|
| 53 |
conversations.by, |
|
| 54 |
codes = NULL, |
|
| 55 |
model = c("EndPoint",
|
|
| 56 |
"AccumulatedTrajectory", |
|
| 57 |
"SeparateTrajectory"), |
|
| 58 |
window = c("Moving Stanza", "Conversation"),
|
|
| 59 |
window.size.back = 1, |
|
| 60 |
window.size.forward = 0, |
|
| 61 |
weight.by = "binary", |
|
| 62 |
binary.stanzas = F, |
|
| 63 |
mask = NULL, |
|
| 64 |
include.meta = T, |
|
| 65 |
as.list = T, |
|
| 66 |
... |
|
| 67 |
) {
|
|
| 68 | 46x |
if(is.null(file) || |
| 69 | 46x |
is.null(units.by) || |
| 70 | 46x |
is.null(conversations.by) || is.null(codes) |
| 71 |
) {
|
|
| 72 | 1x |
stop("Accumulation: file, units.by, conversations.by, and codes")
|
| 73 |
} |
|
| 74 | 45x |
if (is.data.frame(file) && is.character(codes) && all(codes %in% colnames(file))) {
|
| 75 | 45x |
check_code_values(as.data.frame(file)[, codes, drop = FALSE]) |
| 76 |
} |
|
| 77 | ||
| 78 | 45x |
units <- NULL; |
| 79 | 45x |
model <- match.arg(model); |
| 80 | 45x |
window <- match.arg(window); |
| 81 | ||
| 82 | 45x |
if (identical(window, "Conversation")) {
|
| 83 | 1x |
conversations.by = c(conversations.by, units.by); |
| 84 | 1x |
window.size.back = window; |
| 85 |
} |
|
| 86 | 45x |
data = ENAdata$new( |
| 87 | 45x |
file = file, |
| 88 | 45x |
units = units, |
| 89 | 45x |
units.used = units.used, |
| 90 | 45x |
units.by = units.by, |
| 91 | 45x |
conversations.by = conversations.by, |
| 92 | 45x |
codes = codes, |
| 93 | 45x |
window.size.back = window.size.back, |
| 94 | 45x |
window.size.forward = window.size.forward, |
| 95 | 45x |
weight.by = weight.by, |
| 96 | 45x |
model = model, |
| 97 | 45x |
mask = mask, |
| 98 | 45x |
include.meta = include.meta, |
| 99 |
... |
|
| 100 |
); |
|
| 101 | 45x |
data$process(); |
| 102 | ||
| 103 | 45x |
data$function.call = sys.call(); |
| 104 |
# output = match.arg(output); |
|
| 105 |
# if(output == "json") {
|
|
| 106 |
# output.class = get(class(data)) |
|
| 107 |
# |
|
| 108 |
# if(is.null(output.fields)) {
|
|
| 109 |
# output.fields = names(output.class$public_fields) |
|
| 110 |
# } |
|
| 111 |
# |
|
| 112 |
# r6.to.json(data, o.class = output.class, o.fields = output.fields) |
|
| 113 |
# } |
|
| 114 |
#else |
|
| 115 | ||
| 116 | 45x |
if(as.list) {
|
| 117 | 34x |
data = ena.set(data); |
| 118 |
} else {
|
|
| 119 | 11x |
warning("Usage of R6 data objects is deprecated and may be removed entirely in a future version. Consider upgrading to the new data object.")
|
| 120 |
} |
|
| 121 | 45x |
data |
| 122 |
} |
| 1 |
#' @title ENA Rotate by SVD (Principal Components) |
|
| 2 |
#' |
|
| 3 |
#' @description Performs a standard SVD (principal components) rotation on the |
|
| 4 |
#' ENA points. This is the rotation method used by default in Ordered Network |
|
| 5 |
#' Analysis (ONA). Unlike the generalized means rotation, no group labels are |
|
| 6 |
#' required. |
|
| 7 |
#' |
|
| 8 |
#' @param enaset An \code{\link{ENAset}} or compatible list with a
|
|
| 9 |
#' \code{model$points.for.projection} matrix.
|
|
| 10 |
#' @param params A list of additional parameters (currently unused; kept for |
|
| 11 |
#' interface compatibility with other rotation functions). |
|
| 12 |
#' |
|
| 13 |
#' @return A list with: |
|
| 14 |
#' \describe{
|
|
| 15 |
#' \item{rotation}{Rotation matrix (connection columns × dimensions),
|
|
| 16 |
#' with columns named \code{SVD1}, \code{SVD2}, …}
|
|
| 17 |
#' \item{codes}{Character vector of code names}
|
|
| 18 |
#' \item{eigenvalues}{Variance explained per component}
|
|
| 19 |
#' \item{node.positions}{NULL (not computed here)}
|
|
| 20 |
#' } |
|
| 21 |
#' |
|
| 22 |
#' @export |
|
| 23 |
ena.rotate.by.svd <- function(enaset, params = list()) {
|
|
| 24 | 6x |
pts <- as.matrix(enaset$model$points.for.projection) |
| 25 | ||
| 26 | 6x |
pca <- prcomp(pts, retx = FALSE, scale. = FALSE, center = FALSE, tol = 0) |
| 27 | ||
| 28 | 6x |
colnames(pca$rotation) <- paste0("SVD", seq_len(ncol(pca$rotation)))
|
| 29 | ||
| 30 | 6x |
list( |
| 31 | 6x |
rotation = pca$rotation, |
| 32 | 6x |
codes = enaset$rotation$codes, |
| 33 | 6x |
eigenvalues = pca$sdev ^ 2, |
| 34 | 6x |
node.positions = NULL |
| 35 |
) |
|
| 36 |
} |
| 1 |
### |
|
| 2 |
#' @title ENA Rotate by generalized means rotation (GMR) |
|
| 3 |
#' |
|
| 4 |
#' @description Computes a dimensional reduction from a matrix of ENA points |
|
| 5 |
#' such that the first dimension best represents the contribution of a target |
|
| 6 |
#' variable after controlling for covariates via Lasso. An optional second |
|
| 7 |
#' GMR axis can be computed for \code{y_var}; remaining dimensions are filled
|
|
| 8 |
#' by SVD of the doubly-deflated space. Computed in C++ by libena's |
|
| 9 |
#' \code{generalized_means_rotation}.
|
|
| 10 |
#' |
|
| 11 |
#' @param enaset An \code{\link{ENAset}} or compatible list with
|
|
| 12 |
#' \code{model$points.for.projection} (or \code{points.normed.centered}),
|
|
| 13 |
#' \code{line.weights}, and \code{rotation$codes}.
|
|
| 14 |
#' @param params A list with the following named elements: |
|
| 15 |
#' \describe{
|
|
| 16 |
#' \item{\code{x_var}}{Required. A \code{data.frame} (or character vector of
|
|
| 17 |
#' column names in \code{enaset$meta.data}) whose first column is the
|
|
| 18 |
#' target variable. Additional columns are treated as covariates and |
|
| 19 |
#' penalized via Lasso.} |
|
| 20 |
#' \item{\code{y_var}}{Optional. Same format as \code{x_var}. When provided
|
|
| 21 |
#' a second GMR axis is computed.} |
|
| 22 |
#' \item{\code{select_2_groups}}{Optional length-2 list/vector of group
|
|
| 23 |
#' labels. When given, the GMR fit for the x axis uses only rows whose |
|
| 24 |
#' target value is in these two groups. The group mean difference for x1 |
|
| 25 |
#' (the secondary axis that keeps group means on the x-axis) is always |
|
| 26 |
#' computed from the full data.} |
|
| 27 |
#' \item{\code{interactions}}{Logical; if \code{TRUE} (default) pairwise
|
|
| 28 |
#' interaction terms are added to the model matrix when covariates are |
|
| 29 |
#' present. Set \code{FALSE} for main-effects-only Lasso.}
|
|
| 30 |
#' } |
|
| 31 |
#' |
|
| 32 |
#' @importFrom stats model.matrix as.formula |
|
| 33 |
#' @export |
|
| 34 |
#' @return A list with \code{rotation} (q x q matrix, column names GMR1,
|
|
| 35 |
#' GMR2|SVD2, SVD3, …), \code{codes}, \code{eigenvalues}, and
|
|
| 36 |
#' \code{node.positions = NULL}, suitable for use inside \code{rotate()}.
|
|
| 37 |
### |
|
| 38 |
ena.rotate.by.generalized <- function(enaset, params) {
|
|
| 39 | ||
| 40 |
## ── Input validation ──────────────────────────────────────────────────────── |
|
| 41 | 53x |
if (!is.list(params) || is.null(params$x_var)) {
|
| 42 | ! |
stop("params must be provided as a list() and provide `x_var`")
|
| 43 |
} |
|
| 44 | ||
| 45 |
## ── Resolve x_var → data.frame ────────────────────────────────────────────── |
|
| 46 | 53x |
if (!is.data.frame(params$x_var)) {
|
| 47 | 4x |
if (all(params$x_var %in% colnames(enaset$meta.data))) {
|
| 48 | 4x |
x <- enaset$meta.data[, params$x_var, with = FALSE] |
| 49 |
} else {
|
|
| 50 | ! |
stop(paste("x_var incorrect:", paste(params$x_var, collapse = ", ")))
|
| 51 |
} |
|
| 52 |
} else {
|
|
| 53 | 49x |
x <- params$x_var |
| 54 |
} |
|
| 55 | ||
| 56 |
## ── ENA point matrix ──────────────────────────────────────────────────────── |
|
| 57 | 53x |
V <- if (!is.null(enaset$points.normed.centered)) |
| 58 | 53x |
as.matrix(enaset$points.normed.centered) |
| 59 |
else |
|
| 60 | 53x |
as.matrix(enaset$model$points.for.projection) |
| 61 | ||
| 62 |
## ── Target variable & encoding ────────────────────────────────────────────── |
|
| 63 |
## For categorical targets, encode as 0-based integer codes. |
|
| 64 |
## When select_2_groups is provided, the two selected groups are encoded as |
|
| 65 |
## 0 and 1 (required by the C++ x1 computation, which uses labels == 0/1). |
|
| 66 | 53x |
target_full <- as.vector(x[[1]]) |
| 67 | 53x |
x_categorical <- !is.numeric(target_full) |
| 68 | ||
| 69 | 53x |
if (x_categorical) {
|
| 70 | 53x |
grp <- params$select_2_groups |
| 71 | 53x |
if (!is.null(grp) && length(grp) == 2) {
|
| 72 | 1x |
all_levels <- c(grp[[1]], grp[[2]], |
| 73 | 1x |
setdiff(unique(target_full), c(grp[[1]], grp[[2]]))) |
| 74 |
} else {
|
|
| 75 | 52x |
all_levels <- unique(target_full) |
| 76 |
} |
|
| 77 | 53x |
x_target_enc <- as.numeric(factor(target_full, levels = all_levels)) - 1.0 |
| 78 | 53x |
x_n_groups <- as.integer(length(all_levels)) |
| 79 |
} else {
|
|
| 80 | ! |
x_target_enc <- as.numeric(target_full) |
| 81 | ! |
x_n_groups <- 0L |
| 82 |
} |
|
| 83 | ||
| 84 |
## ── Row subset (select_2_groups → 0-based integer indices) ────────────────── |
|
| 85 | 53x |
if (!is.null(params$select_2_groups) && length(params$select_2_groups) == 2) {
|
| 86 | 1x |
subset_rows <- which(target_full %in% params$select_2_groups) |
| 87 | 1x |
if (length(subset_rows) < 2L) {
|
| 88 | ! |
warning("select_2_groups produced < 2 matching rows; using all rows")
|
| 89 | ! |
x_subset <- integer(0) |
| 90 |
} else {
|
|
| 91 | 1x |
x_subset <- as.integer(subset_rows - 1L) |
| 92 |
} |
|
| 93 |
} else {
|
|
| 94 | 52x |
x_subset <- integer(0) |
| 95 |
} |
|
| 96 | ||
| 97 |
## ── Model matrix for x ────────────────────────────────────────────────────── |
|
| 98 |
## Interaction terms are included by default when covariates are present. |
|
| 99 | 53x |
interactions <- isTRUE(if (!is.null(params$interactions)) params$interactions else TRUE) |
| 100 | 53x |
fstr_x <- if (ncol(x) > 1L && interactions) "~ .^2" else "~ ." |
| 101 | 53x |
mm_x <- model.matrix(as.formula(fstr_x), data = x)[, -1L, drop = FALSE] |
| 102 | ||
| 103 |
## x1_cols (0-based): columns in mm_x that belong to the target variable |
|
| 104 |
## (main-effect columns only; interaction columns stay penalized) |
|
| 105 | 53x |
x1_name <- colnames(x)[1L] |
| 106 | 53x |
safe_x1 <- gsub("([.|()\\^{}+$*?]|\\[|\\])", "\\\\\\1", x1_name)
|
| 107 | 53x |
x1_regex <- paste0("^", safe_x1, "[^:]*$")
|
| 108 | 53x |
x1_cols <- as.integer(grep(x1_regex, colnames(mm_x)) - 1L) |
| 109 | ! |
if (length(x1_cols) == 0L) x1_cols <- 0L # guard: treat first col as target |
| 110 | ||
| 111 |
## ── Y axis ────────────────────────────────────────────────────────────────── |
|
| 112 | 53x |
has_y <- !is.null(params$y_var) |
| 113 | ||
| 114 | 53x |
if (has_y) {
|
| 115 | ! |
if (!is.data.frame(params$y_var)) {
|
| 116 | ! |
if (all(params$y_var %in% colnames(enaset$meta.data))) {
|
| 117 | ! |
y <- enaset$meta.data[, params$y_var, with = FALSE] |
| 118 |
} else {
|
|
| 119 | ! |
stop("y_var must be a data.frame or a column name in enaset$meta.data")
|
| 120 |
} |
|
| 121 |
} else {
|
|
| 122 | ! |
y <- params$y_var |
| 123 |
} |
|
| 124 | ! |
y_target_raw <- as.vector(y[[1]]) |
| 125 | ! |
y_categorical <- !is.numeric(y_target_raw) |
| 126 | ! |
if (y_categorical) {
|
| 127 | ! |
y_levels <- unique(y_target_raw) |
| 128 | ! |
y_target_enc <- as.numeric(factor(y_target_raw, levels = y_levels)) - 1.0 |
| 129 | ! |
y_n_groups <- as.integer(length(y_levels)) |
| 130 |
} else {
|
|
| 131 | ! |
y_target_enc <- as.numeric(y_target_raw) |
| 132 | ! |
y_n_groups <- 0L |
| 133 |
} |
|
| 134 | ! |
fstr_y <- if (ncol(y) > 1L && interactions) "~ .^2" else "~ ." |
| 135 | ! |
mm_y <- model.matrix(as.formula(fstr_y), data = y)[, -1L, drop = FALSE] |
| 136 | ! |
y1_name <- colnames(y)[1L] |
| 137 | ! |
safe_y1 <- gsub("([.|()\\^{}+$*?]|\\[|\\])", "\\\\\\1", y1_name)
|
| 138 | ! |
y1_regex <- paste0("^", safe_y1, "[^:]*$")
|
| 139 | ! |
y1_cols <- as.integer(grep(y1_regex, colnames(mm_y)) - 1L) |
| 140 | ! |
if (length(y1_cols) == 0L) y1_cols <- 0L |
| 141 |
} else {
|
|
| 142 |
## Dummy y params — passed but ignored by the C++ when has_y = FALSE |
|
| 143 | 53x |
mm_y <- matrix(0.0, nrow(V), 1L) |
| 144 | 53x |
y_target_enc <- numeric(nrow(V)) |
| 145 | 53x |
y1_cols <- 0L |
| 146 | 53x |
y_categorical <- FALSE |
| 147 | 53x |
y_n_groups <- 0L |
| 148 |
} |
|
| 149 | ||
| 150 |
## ── Delegate to libena ────────────────────────────────────────────────────── |
|
| 151 | 53x |
result <- generalized_means_rotation( |
| 152 | 53x |
V = V, |
| 153 | 53x |
x_model_matrix = mm_x, |
| 154 | 53x |
x_target = x_target_enc, |
| 155 | 53x |
x1_cols = x1_cols, |
| 156 | 53x |
x_categorical = x_categorical, |
| 157 | 53x |
x_n_groups = x_n_groups, |
| 158 | 53x |
x_subset = x_subset, |
| 159 | 53x |
has_y = has_y, |
| 160 | 53x |
y_model_matrix = mm_y, |
| 161 | 53x |
y_target = y_target_enc, |
| 162 | 53x |
y1_cols = y1_cols, |
| 163 | 53x |
y_categorical = y_categorical, |
| 164 | 53x |
y_n_groups = y_n_groups |
| 165 |
) |
|
| 166 | ||
| 167 |
## ── Assemble rotation matrix ───────────────────────────────────────────────── |
|
| 168 | 53x |
rotation <- result$rotation |
| 169 | 53x |
colnames(rotation) <- result$column_names |
| 170 | 53x |
rownames(rotation) <- colnames(as.matrix(enaset$line.weights)) |
| 171 | ||
| 172 | 53x |
list( |
| 173 | 53x |
node.positions = NULL, |
| 174 | 53x |
rotation = rotation, |
| 175 | 53x |
codes = enaset$rotation$codes, |
| 176 | 53x |
eigenvalues = result$eigenvalues |
| 177 |
) |
|
| 178 |
} |
| 1 |
## |
|
| 2 |
#' Cohen's d calculation |
|
| 3 |
#' |
|
| 4 |
#' @title Cohen's d |
|
| 5 |
#' |
|
| 6 |
#' @description Calculate Conhen's d |
|
| 7 |
#' |
|
| 8 |
#' @details [TBD] |
|
| 9 |
#' |
|
| 10 |
#' @param x [TBD] |
|
| 11 |
#' @param y [TBD] |
|
| 12 |
#' |
|
| 13 |
#' @export |
|
| 14 |
#' @return numeric Cohen's d calculation |
|
| 15 |
fun_cohens.d <- function(x, y) {
|
|
| 16 | 2x |
lx <- length(x)- 1 |
| 17 | 2x |
ly <- length(y)- 1 |
| 18 | 2x |
md <- abs(mean(x) - mean(y)) ## mean difference (numerator) |
| 19 | 2x |
csd <- lx * var(x) + ly * var(y) |
| 20 | 2x |
csd <- csd/(lx + ly) |
| 21 | 2x |
csd <- sqrt(csd) ## common sd computation |
| 22 | ||
| 23 | 2x |
cd <- md/csd |
| 24 | 2x |
return(cd)## cohen's d |
| 25 |
} |
| 1 |
### |
|
| 2 |
#' Calculate the correlations |
|
| 3 |
#' |
|
| 4 |
#' @description Calculate both Spearman and Pearson correlations for the |
|
| 5 |
#' provided ENAset |
|
| 6 |
#' |
|
| 7 |
#' @param enaset ENAset to view methods of |
|
| 8 |
#' @param tool c("rENA","webENA")
|
|
| 9 |
#' @param tool.version as.character(packageVersion(tool)) |
|
| 10 |
#' @param comparison character string representing the comparison used, c(NULL, "parametric", "non-parametric"). Default NULL |
|
| 11 |
#' @param comparison.groups Groups that were used for the comparison |
|
| 12 |
#' @param sig.dig Integer for the number of digits to round to |
|
| 13 |
#' @param output_dir Where to save the output file when \code{type = "file"}.
|
|
| 14 |
#' Default \code{tempdir()}; pass a directory to keep the report.
|
|
| 15 |
#' @param type c("file","stream") File will save to a file in output_dir, Stream returns the contents directly
|
|
| 16 |
#' @param theory Logical indicating whether to include theory in the writeup |
|
| 17 |
#' @param methods Logical indicating whether to include methods in the writeup |
|
| 18 |
#' @param params additional parameters for rmarkdown::render |
|
| 19 |
#' @param output_file character |
|
| 20 |
#' @param output_format character |
|
| 21 |
#' |
|
| 22 |
#' @export |
|
| 23 |
#' |
|
| 24 |
#' @return For \code{type = "file"}, the path of the rendered file; for
|
|
| 25 |
#' \code{type = "stream"}, a string with the methods text.
|
|
| 26 |
ena.writeup <- function( |
|
| 27 |
enaset, |
|
| 28 |
tool = "rENA", tool.version = as.character(packageVersion(tool)), |
|
| 29 |
comparison = NULL, comparison.groups = NULL, sig.dig = 2, |
|
| 30 |
output_dir = tempdir(), type = c("file","stream"), theory = T, methods = T,
|
|
| 31 |
params = NULL, output_file = NULL, output_format = NULL |
|
| 32 |
) {
|
|
| 33 | 1x |
require_writeup_packages() |
| 34 | ||
| 35 | 1x |
if(is.null(enaset$`_function.params`$weight.by)) |
| 36 | ! |
enaset$`_function.params`$weight.by <- enaset$`_function.params`$args$weight.by |
| 37 | ||
| 38 | 1x |
type = match.arg(type, choices = c("file","stream"), several.ok = FALSE)
|
| 39 | ||
| 40 | 1x |
if(type == "file") {
|
| 41 | ! |
output_format = "word_document" |
| 42 |
} |
|
| 43 | ||
| 44 |
# knitr runs and writes its intermediates in a scratch directory, never in |
|
| 45 |
# the working directory; a streamed report is rendered there too. |
|
| 46 | 1x |
work_dir <- tempfile("ena_writeup")
|
| 47 | 1x |
dir.create(work_dir) |
| 48 | 1x |
on.exit(unlink(work_dir, recursive = TRUE), add = TRUE) |
| 49 | 1x |
if (type == "stream") output_dir <- work_dir |
| 50 | ||
| 51 | 1x |
file = rmarkdown::render(system.file("rmd","methods.rmd", package="rENA"), output_dir = output_dir,
|
| 52 | 1x |
knit_root_dir = work_dir, intermediates_dir = work_dir, quiet = TRUE, |
| 53 | 1x |
params = params, output_file = output_file |
| 54 | 1x |
,output_format = output_format |
| 55 |
) |
|
| 56 | ||
| 57 | 1x |
if(type == "file") |
| 58 | ! |
file |
| 59 | 1x |
else if (type == "stream" && endsWith(file, ".plain")) |
| 60 | 1x |
readChar(file, file.info(file)$size) |
| 61 |
} |
|
| 62 | ||
| 63 |
require_writeup_packages <- function() {
|
|
| 64 | 2x |
missing <- c("rmarkdown", "knitr")[!vapply(c("rmarkdown", "knitr"), requireNamespace,
|
| 65 | 2x |
logical(1), quietly = TRUE)] |
| 66 | 2x |
if (length(missing) > 0) |
| 67 | ! |
stop("ENA writeups need the ", paste(missing, collapse = " and "), " package(s): ",
|
| 68 | ! |
"install.packages(c(", paste0('"', missing, '"', collapse = ", "), "))", call. = FALSE)
|
| 69 |
} |
|
| 70 | ||
| 71 |
#' @title methods_report |
|
| 72 |
#' @description Methods report for rmarkdwon |
|
| 73 |
#' @param toc [TBD] |
|
| 74 |
#' @param toc_depth [TBD] |
|
| 75 |
#' @param fig_width [TBD] |
|
| 76 |
#' @param fig_height [TBD] |
|
| 77 |
#' @param keep_md [TBD] |
|
| 78 |
#' @param md_extensions [TBD] |
|
| 79 |
#' @param pandoc_args [TBD] |
|
| 80 |
#' |
|
| 81 |
#' @export |
|
| 82 |
methods_report <- function(toc = FALSE, |
|
| 83 |
toc_depth = 3, |
|
| 84 |
fig_width = 5, |
|
| 85 |
fig_height = 4, |
|
| 86 |
keep_md = FALSE, |
|
| 87 |
md_extensions = NULL, |
|
| 88 |
pandoc_args = NULL) {
|
|
| 89 | ! |
require_writeup_packages() |
| 90 | ||
| 91 |
# knitr options and hooks |
|
| 92 | ! |
knitr <- rmarkdown::knitr_options( |
| 93 | ! |
opts_chunk = list(dev = 'png', |
| 94 | ! |
dpi = 96, |
| 95 | ! |
fig.width = fig_width, |
| 96 | ! |
fig.height = fig_height) |
| 97 |
) |
|
| 98 | ||
| 99 |
# build pandoc args |
|
| 100 | ! |
args <- c("--standalone")
|
| 101 | ||
| 102 |
# table of contents |
|
| 103 | ! |
args <- c(args, rmarkdown::pandoc_toc_args(toc, toc_depth)) |
| 104 | ||
| 105 |
# pandoc args |
|
| 106 | ! |
args <- c(args, pandoc_args) |
| 107 | ||
| 108 | ! |
preserved_chunks <- character() |
| 109 | ||
| 110 |
# pre_processor <- function(metadata, input_file, runtime, knit_meta, |
|
| 111 |
# files_dir, output_dir) {
|
|
| 112 |
# preserved_chunks <<- extract_preserve_chunks(input_file, knitr::extract_raw_output) |
|
| 113 |
# NULL |
|
| 114 |
# } |
|
| 115 | ||
| 116 |
# post_processor <- function(metadata, input_file, output_file, clean, verbose) {
|
|
| 117 |
# output_str <- readLines(output_file, encoding = 'UTF-8') |
|
| 118 |
# output_res <- knitr::restore_raw_output(output_str, preserved_chunks) |
|
| 119 |
# if (!identical(output_str, output_res)) |
|
| 120 |
# writeLines(enc2utf8(output_res), output_file, useBytes = TRUE) |
|
| 121 |
# output_file |
|
| 122 |
# } |
|
| 123 | ||
| 124 |
# return output format |
|
| 125 | ! |
rmarkdown::output_format( |
| 126 | ! |
knitr = knitr, |
| 127 | ! |
pandoc = rmarkdown::pandoc_options(to = "docx", |
| 128 | ! |
from = rmarkdown::from_rmarkdown(extensions = md_extensions), |
| 129 | ! |
args = args), |
| 130 | ! |
keep_md = keep_md |
| 131 |
# ,pre_processor = pre_processor, |
|
| 132 |
# post_processor = post_processor |
|
| 133 |
) |
|
| 134 |
} |
|
| 135 | ||
| 136 |
#' @title methods_report_stream |
|
| 137 |
#' @description Methods report for rmarkdwon |
|
| 138 |
#' @param toc [TBD] |
|
| 139 |
#' @param toc_depth [TBD] |
|
| 140 |
#' @param fig_width [TBD] |
|
| 141 |
#' @param fig_height [TBD] |
|
| 142 |
#' @param keep_md [TBD] |
|
| 143 |
#' @param md_extensions [TBD] |
|
| 144 |
#' @param pandoc_args [TBD] |
|
| 145 |
#' |
|
| 146 |
#' @export |
|
| 147 |
methods_report_stream <- function(toc = FALSE, |
|
| 148 |
toc_depth = 3, |
|
| 149 |
fig_width = 5, |
|
| 150 |
fig_height = 4, |
|
| 151 |
keep_md = FALSE, |
|
| 152 |
md_extensions = NULL, |
|
| 153 |
pandoc_args = NULL) {
|
|
| 154 | 1x |
require_writeup_packages() |
| 155 | ||
| 156 |
# knitr options and hooks |
|
| 157 | 1x |
knitr <- rmarkdown::knitr_options( |
| 158 | 1x |
opts_chunk = list(dev = 'png', |
| 159 | 1x |
dpi = 96, |
| 160 | 1x |
fig.width = fig_width, |
| 161 | 1x |
fig.height = fig_height) |
| 162 |
) |
|
| 163 | ||
| 164 |
# build pandoc args |
|
| 165 | 1x |
args <- c("--standalone")
|
| 166 | ||
| 167 |
# table of contents |
|
| 168 | 1x |
args <- c(args, rmarkdown::pandoc_toc_args(toc, toc_depth)) |
| 169 | ||
| 170 |
# pandoc args |
|
| 171 | 1x |
args <- c(args, pandoc_args) |
| 172 | ||
| 173 | 1x |
preserved_chunks <- character() |
| 174 | ||
| 175 |
# return output format |
|
| 176 | 1x |
rmarkdown::output_format( |
| 177 | 1x |
knitr = knitr, |
| 178 | 1x |
pandoc = rmarkdown::pandoc_options(to = "plain", |
| 179 | 1x |
from = rmarkdown::from_rmarkdown(extensions = md_extensions), |
| 180 | 1x |
args = args), |
| 181 | 1x |
keep_md = keep_md |
| 182 |
) |
|
| 183 |
} |
| 1 |
### |
|
| 2 |
#' @title ENA Rotate by mean |
|
| 3 |
#' |
|
| 4 |
#' @description Computes a dimensional reduction from a matrix of points such |
|
| 5 |
#' that the first dimension of the projected space passes through the means of |
|
| 6 |
#' two groups in the original space. Subsequent dimensions are computed using |
|
| 7 |
#' SVD on the deflated data. Computed in C++ by libena's \code{means_rotation}.
|
|
| 8 |
#' |
|
| 9 |
#' @param enaset An \code{\link{ENAset}} or compatible list with
|
|
| 10 |
#' \code{model$points.for.projection}, \code{connection.counts$ENA_UNIT},
|
|
| 11 |
#' \code{line.weights}, and \code{rotation$codes}.
|
|
| 12 |
#' @param groups A list containing one or more pairs; each pair is a length-2 |
|
| 13 |
#' list \code{list(a, b)} where \code{a} and \code{b} are either logical
|
|
| 14 |
#' vectors (length = number of units) or character vectors of unit IDs. |
|
| 15 |
#' @param params Alias for \code{groups}; used when called from the pipe API.
|
|
| 16 |
#' |
|
| 17 |
#' @export |
|
| 18 |
#' @return A list with \code{rotation}, \code{codes}, \code{eigenvalues}, and
|
|
| 19 |
#' \code{node.positions = NULL}, suitable for use inside \code{rotate()}.
|
|
| 20 |
### |
|
| 21 |
ena.rotate.by.mean <- function(enaset, groups = NULL, params = groups) {
|
|
| 22 | 13x |
if (is.null(groups) && !is.null(params)) {
|
| 23 | 3x |
groups <- params |
| 24 |
} else {
|
|
| 25 | 10x |
groups <- list(groups)[[1]] |
| 26 | 1x |
if (length(groups) < 1) stop("Unable to rotate without 2 groups.")
|
| 27 |
} |
|
| 28 | 12x |
if (!is(groups[[1]], "list")) groups <- list(groups) |
| 29 | ||
| 30 |
# Extract the data matrix (as.matrix strips metadata columns for ena.matrix) |
|
| 31 | 12x |
data <- if (!is.null(enaset$points.normed.centered)) {
|
| 32 | 1x |
as.matrix(enaset$points.normed.centered) |
| 33 |
} else {
|
|
| 34 | 11x |
as.matrix(enaset$model$points.for.projection) |
| 35 |
} |
|
| 36 | ||
| 37 |
# Convert groups (logical or character) to 0-based integer index pairs |
|
| 38 |
# required by means_rotation (libena) |
|
| 39 | 12x |
ena_unit <- enaset$connection.counts$ENA_UNIT |
| 40 | 12x |
group_pairs <- lapply(groups, function(pair) {
|
| 41 | 12x |
a <- pair[[1]] |
| 42 | 12x |
b <- pair[[2]] |
| 43 | ! |
if (!is.logical(a)) a <- ena_unit %in% a |
| 44 | ! |
if (!is.logical(b)) b <- ena_unit %in% b |
| 45 | 12x |
list(as.integer(which(a) - 1L), as.integer(which(b) - 1L)) |
| 46 |
}) |
|
| 47 | ||
| 48 | 12x |
result <- means_rotation(data, group_pairs) |
| 49 | ||
| 50 | 12x |
rotation <- result$rotation |
| 51 | 12x |
colnames(rotation) <- result$column_names |
| 52 | 12x |
rownames(rotation) <- colnames(as.matrix(enaset$line.weights)) |
| 53 | ||
| 54 | 12x |
list( |
| 55 | 12x |
node.positions = NULL, |
| 56 | 12x |
rotation = rotation, |
| 57 | 12x |
codes = enaset$rotation$codes, |
| 58 | 12x |
eigenvalues = result$eigenvalues |
| 59 |
) |
|
| 60 |
} |
| 1 |
## |
|
| 2 |
#' @title Accumulate data from a data frame into a set of adjacency (co-occurrence) vectors |
|
| 3 |
#' |
|
| 4 |
#' @description This function initializes an ENAdata object, processing conversations from coded data to generate adjacency (co-occurrence) vectors |
|
| 5 |
#' |
|
| 6 |
#' @details ENAData objects are created using this function. This accumulation receives |
|
| 7 |
#' separate data frames for units, codes, conversation, and optionally, metadata. It |
|
| 8 |
#' iterates through the data to create an adjacency (co-occurrence) vector corresponding |
|
| 9 |
#' to each unit - or in a trajectory model multiple adjacency (co-occurrence) vectors for |
|
| 10 |
#' each unit. |
|
| 11 |
#' |
|
| 12 |
#' In the default MovingStanzaWindow model, co-occurrences between codes are |
|
| 13 |
#' calculated for each line k in the data between line k and the window.size.back-1 previous |
|
| 14 |
#' lines and window.size.forward-1 subsequent lines in the same conversation as line k. |
|
| 15 |
#' |
|
| 16 |
#' In the Conversation model, co-occurrences between codes are calculated across all lines in |
|
| 17 |
#' each conversation. Adjacency (co-occurrence) vectors are constructed for each unit u by |
|
| 18 |
#' summing the co-occurrences for the lines that correspond to u. |
|
| 19 |
#' |
|
| 20 |
#' Options for how the data is accumulated are endpoint, which produces one adjacency (co-occurrence) |
|
| 21 |
#' vector for each until summing the co-occurrences for all lines, and two trajectory models: |
|
| 22 |
#' AccumulatedTrajectory and SeparateTrajectory. Trajectory models produce an adjacency |
|
| 23 |
#' (co-occurrence) model for each conversation for each unit. In a SeparateTrajectory model, |
|
| 24 |
#' each conversation is modeled as a separate network. In an AccumulatedTrajectory model, the |
|
| 25 |
#' adjacency (co-occurrence) vector for the current conversation includes the co-occurrences |
|
| 26 |
#' from all previous conversations in the data. |
|
| 27 |
#' |
|
| 28 |
#' @export |
|
| 29 |
#' |
|
| 30 |
#' @param units A data frame where the columns are the properties by which units will be identified |
|
| 31 |
#' @param conversation A data frame where the columns are the properties by which conversations will be identified |
|
| 32 |
#' @param codes A data frame where the columns are the codes used to create adjacency (co-occurrence) vectors |
|
| 33 |
#' @param metadata (optional) A data frame with additional columns of metadata to be associated with each unit in the data |
|
| 34 |
#' @param model A character, choices: EndPoint (or E), AccumulatedTrajectory (or A), or SeparateTrajectory (or S); default: EndPoint. Determines the ENA model to be constructed |
|
| 35 |
#' @param weight.by (optional) A function to apply to values after accumulation |
|
| 36 |
#' @param mask (optional) A binary matrix of size ncol(codes) x ncol(codes). 0s in the mask matrix row i column j indicates that co-occurrence will not be modeled between code i and code j |
|
| 37 |
#' @param window A character, choices are Conversation (or C), MovingStanzaWindow (MSW, MS); default MovingStanzaWindow. Determines how stanzas are constructed, which defines how co-occurrences are modeled |
|
| 38 |
#' @param window.size.back A positive integer, Inf, or character (INF or Infinite), default: 1. Determines, for each line in the data frame, the number of previous lines in a conversation to include in the stanza window, which defines how co-occurrences are modeled |
|
| 39 |
#' @param window.size.forward (optional) A positive integer, Inf, or character (INF or Infinite), default: 0. Determines, for each line in the data frame, the number of subsequent lines in a conversation to include in the stanza window, which defines how co-occurrences are modeled |
|
| 40 |
#' @param ... additional parameters addressed in inner function |
|
| 41 |
#' @param include.meta Locigal indicating if unit metadata should be attached to the resulting ENAdata object, default is TRUE |
|
| 42 |
#' @param as.list R6 objects will be deprecated, but if this is TRUE, the original R6 object will be returned, otherwise a list with class `ena.set` |
|
| 43 |
#' |
|
| 44 |
#' @seealso \code{\link{ENAdata}}, \code{\link{ena.make.set}}
|
|
| 45 |
#' |
|
| 46 |
#' @return \code{\link{ENAdata}} object with data [adjacency (co-occurrence) vectors] accumulated from the provided data frames.
|
|
| 47 |
#' |
|
| 48 |
## |
|
| 49 |
ena.accumulate.data <- function( |
|
| 50 |
units = NULL, |
|
| 51 |
conversation = NULL, |
|
| 52 |
codes = NULL, |
|
| 53 |
metadata = NULL, |
|
| 54 |
model = c("EndPoint", "AccumulatedTrajectory", "SeparateTrajectory"),
|
|
| 55 |
weight.by = "binary", |
|
| 56 |
window = c("MovingStanzaWindow", "Conversation"),
|
|
| 57 |
window.size.back = 1, |
|
| 58 |
window.size.forward = 0, |
|
| 59 |
mask = NULL, |
|
| 60 |
include.meta = T, |
|
| 61 |
as.list = T, |
|
| 62 |
... |
|
| 63 |
) {
|
|
| 64 | 99x |
if (is.null(units) || is.null(conversation) || is.null(codes)) {
|
| 65 | 1x |
stop("Accumulation requires: units, conversation, and codes");
|
| 66 |
} |
|
| 67 | 98x |
if (nrow(units) != nrow(conversation) || nrow(conversation) != nrow(codes)) {
|
| 68 | 1x |
stop("Data Frames do not have the same number of rows");
|
| 69 |
} |
|
| 70 | 97x |
check_code_values(codes) |
| 71 | ||
| 72 | 95x |
df <- cbind(units, conversation); |
| 73 | 95x |
df <- cbind(df, codes); |
| 74 | ||
| 75 |
# A metadata table whose rows don't line up with the data used to be dropped |
|
| 76 |
# without a word. |
|
| 77 | 95x |
if (!is.null(metadata) && NROW(metadata) > 0) {
|
| 78 | 9x |
if (NROW(metadata) != nrow(df)) |
| 79 | 1x |
stop("metadata has ", NROW(metadata), " rows but the data has ", nrow(df))
|
| 80 |
} |
|
| 81 | 94x |
metadata <- data.table::as.data.table(metadata) |
| 82 | 94x |
if (nrow(metadata) == nrow(df)) {
|
| 83 | 8x |
df <- cbind(df, metadata); |
| 84 |
} |
|
| 85 | ||
| 86 | 94x |
model <- match.arg(model) |
| 87 | 94x |
window <- match.arg(window) |
| 88 | ||
| 89 | 94x |
units.by <- colnames(units); |
| 90 | 94x |
conversations.by <- colnames(conversation); |
| 91 | 94x |
if (identical(window, "Conversation")) {
|
| 92 | 1x |
conversations.by <- c(conversations.by, units.by); |
| 93 | 1x |
window.size.back <- window; |
| 94 |
} |
|
| 95 | 93x |
else if (identical(window, "MovingStanzaWindow")) {
|
| 96 | 93x |
if( grepl(pattern = "inf", x = window.size.back, ignore.case = T)) {
|
| 97 | 3x |
window.size.back <- Inf |
| 98 |
} |
|
| 99 | 93x |
if( grepl(pattern = "inf", x = window.size.forward, ignore.case = T)) {
|
| 100 | 1x |
window.size.forward <- Inf |
| 101 |
} |
|
| 102 |
} |
|
| 103 | ||
| 104 | 94x |
data <- ENAdata$new( |
| 105 | 94x |
file = df, |
| 106 | 94x |
units = units, |
| 107 | 94x |
units.by = units.by, |
| 108 | 94x |
conversations.by = conversations.by, |
| 109 | 94x |
codes = codes, |
| 110 | 94x |
window.size.back = window.size.back, |
| 111 | 94x |
window.size.forward = window.size.forward, |
| 112 | 94x |
weight.by = weight.by, |
| 113 | 94x |
model = model, |
| 114 | 94x |
mask = mask, |
| 115 | 94x |
include.meta = include.meta, |
| 116 |
... |
|
| 117 |
); |
|
| 118 | 94x |
data$process() |
| 119 | ||
| 120 | 94x |
data$function.call <- sys.call() |
| 121 | ||
| 122 | 94x |
if(as.list) {
|
| 123 | 92x |
data <- ena.set(data) |
| 124 |
} else {
|
|
| 125 | 2x |
warning(paste0("Usage of R6 data objects is deprecated and may be removed ",
|
| 126 | 2x |
"entirely in a future version. Consider upgrading to the new data ", |
| 127 | 2x |
"object.")) |
| 128 |
} |
|
| 129 | ||
| 130 | 94x |
data |
| 131 |
} |
|
| 132 | ||
| 133 |
# Code columns must be complete and numeric (or logical). An NA in a code made |
|
| 134 |
# the unit's whole connection count NA, which then became an all-zero network |
|
| 135 |
# at the origin -- dropping the unit's valid connections without a warning. |
|
| 136 |
check_code_values <- function(codes) {
|
|
| 137 | 142x |
codes <- as.data.frame(codes) |
| 138 | 142x |
bad_type <- names(codes)[!vapply(codes, function(x) is.numeric(x) || is.logical(x), logical(1))] |
| 139 | 142x |
if (length(bad_type)) |
| 140 | 1x |
stop("Code columns must be numeric or logical: ", paste(bad_type, collapse = ", "))
|
| 141 | 141x |
has_na <- names(codes)[vapply(codes, anyNA, logical(1))] |
| 142 | 141x |
if (length(has_na)) |
| 143 | 1x |
stop("Code columns contain missing values (NA): ", paste(has_na, collapse = ", "),
|
| 144 | 1x |
". Recode them (e.g. to 0) before accumulating.") |
| 145 | 140x |
invisible(TRUE) |
| 146 |
} |
| 1 |
#' Apply metadata and code transformations to a data.table |
|
| 2 |
#' |
|
| 3 |
#' This function applies metadata and code transformations to a data.table if provided. |
|
| 4 |
#' It checks if the metadata and codes are supplied as vectors of column names. |
|
| 5 |
#' |
|
| 6 |
#' @param x A data.table. The data.table to be transformed. |
|
| 7 |
#' @param metadata_cols A vector of column names or NULL. A vector specifying the columns for metadata transformations. |
|
| 8 |
#' @param codes_cols A vector of column names or NULL. A vector specifying the columns for code transformations. |
|
| 9 |
#' @param horizon_cols A vector of column names or NULL. A vector specifying the columns for horizon transformations. |
|
| 10 |
#' @param units_cols A vector of column names or NULL. A vector specifying the columns for unit transformations. |
|
| 11 |
#' |
|
| 12 |
#' @return The modified data.table after applying the metadata and code transformations. |
|
| 13 |
#' @examples |
|
| 14 |
#' library(data.table) |
|
| 15 |
#' dt <- data.table(a = 1:5, b = 6:10) |
|
| 16 |
#' dt <- define(dt, metadata = c("a"), codes = c("b"))
|
|
| 17 |
#' @export |
|
| 18 |
define <- function( |
|
| 19 |
x, |
|
| 20 |
metadata_cols = find_meta_cols(x), |
|
| 21 |
codes_cols = find_binary_cols(x), |
|
| 22 |
horizon_cols = NULL, |
|
| 23 |
units_cols = NULL |
|
| 24 |
) {
|
|
| 25 | ! |
x <- as.qe.data(x); |
| 26 | ||
| 27 | ! |
do_call <- function(y, wh) {
|
| 28 | ! |
args <- list(x = x); |
| 29 | ! |
for(u in y) args[[length(args) + 1]] <- u |
| 30 | ! |
x <<- do.call(wh, args); |
| 31 | ||
| 32 | ! |
return(x); |
| 33 |
} |
|
| 34 | ||
| 35 | ! |
if(!is.null(metadata_cols)) {
|
| 36 |
if( |
|
| 37 | ! |
(is.numeric(metadata_cols) || is.character(metadata_cols)) && |
| 38 | ! |
length(metadata_cols) > 0 |
| 39 |
) {
|
|
| 40 | ! |
x <- do_call(metadata_cols, metadata); |
| 41 |
} |
|
| 42 |
else {
|
|
| 43 | ! |
warning(WARNINGS$null_metadata); |
| 44 |
} |
|
| 45 |
} |
|
| 46 | ||
| 47 | ! |
if(!is.null(codes_cols)) {
|
| 48 |
if( |
|
| 49 | ! |
(is.numeric(codes_cols) || is.character(codes_cols)) && |
| 50 | ! |
length(codes_cols) > 0 |
| 51 |
) {
|
|
| 52 | ! |
x <- do_call(codes_cols, codes); |
| 53 |
} |
|
| 54 |
else {
|
|
| 55 | ! |
warning(WARNINGS$null_codes); |
| 56 |
} |
|
| 57 |
} |
|
| 58 | ||
| 59 | ! |
if(!is.null(units_cols)) {
|
| 60 |
if( |
|
| 61 | ! |
(is.numeric(units_cols) || is.character(units_cols)) && |
| 62 | ! |
length(units_cols) > 0 |
| 63 |
) {
|
|
| 64 | ! |
x <- do_call(units_cols, units); |
| 65 |
} |
|
| 66 |
else {
|
|
| 67 | ! |
warning(WARNINGS$null_units); |
| 68 |
} |
|
| 69 |
} |
|
| 70 | ||
| 71 | ! |
if(!is.null(horizon_cols)) {
|
| 72 |
if( |
|
| 73 | ! |
(is.numeric(horizon_cols) || is.character(horizon_cols)) && |
| 74 | ! |
length(horizon_cols) > 0 |
| 75 |
) {
|
|
| 76 | ! |
x <- do_call(horizon_cols, horizon); |
| 77 |
} |
|
| 78 |
else {
|
|
| 79 | ! |
warning(WARNINGS$null_horizon); |
| 80 |
} |
|
| 81 |
} |
|
| 82 | ||
| 83 | ! |
invisible(x); |
| 84 |
} |
|
| 85 | ||
| 86 |
#' Reclassify specified columns as codes or list codes columns in a data.table |
|
| 87 |
#' |
|
| 88 |
#' This function reclassifies specified columns of a data.table to the 'qe.code' format if column names are provided. |
|
| 89 |
#' If no column names are provided, it returns the names of columns that are already classified as 'qe.code'. |
|
| 90 |
#' |
|
| 91 |
#' @param x A data.table. The data.table containing the columns to be reclassified or checked. |
|
| 92 |
#' @param ... Additional arguments specifying the names of the columns to be reclassified. |
|
| 93 |
#' |
|
| 94 |
#' @return The modified data.table with specified columns reclassified as 'qe.code', or a character vector of column names already classified as 'qe.unit'. |
|
| 95 |
#' @examples |
|
| 96 |
#' library(data.table) |
|
| 97 |
#' dt <- data.table(a = 1:5, b = 6:10) |
|
| 98 |
#' # Reclassify columns 'a' and 'b' as 'qe.code' |
|
| 99 |
#' dt <- codes(dt, "a", "b") |
|
| 100 |
#' # List columns classified as 'qe.code' |
|
| 101 |
#' code_columns <- codes(dt) |
|
| 102 |
#' @export |
|
| 103 |
codes <- function(x, ...) {
|
|
| 104 | ! |
x <- as.qe.data(x); |
| 105 | ||
| 106 | ! |
if(...length() > 0) {
|
| 107 | ! |
dot_args <- list(...); |
| 108 | ||
| 109 |
# x <- reclassify(x, dot_args, as.qe.code); |
|
| 110 | ! |
dot_args$x <- x; |
| 111 | ! |
dot_args$v <- as.qe.code; |
| 112 | ! |
x <- do.call(reclassify, dot_args); |
| 113 | ||
| 114 | ! |
return(x); |
| 115 |
} |
|
| 116 |
else {
|
|
| 117 | ! |
return(colnames(x)[sapply(x, is.qe.code)]); |
| 118 |
} |
|
| 119 |
} |
|
| 120 | ||
| 121 |
#' Reclassify specified columns as metadata or list metadata columns in a data.table |
|
| 122 |
#' |
|
| 123 |
#' This function reclassifies specified columns of a data.table to the 'qe.metadata' format if column names are provided. |
|
| 124 |
#' If no column names are provided, it returns the names of columns that are already classified as 'qe.metadata'. |
|
| 125 |
#' |
|
| 126 |
#' @param x A data.table. The data.table containing the columns to be reclassified or checked. |
|
| 127 |
#' @param ... Additional arguments specifying the names of the columns to be reclassified. |
|
| 128 |
#' |
|
| 129 |
#' @return The modified data.table with specified columns reclassified as 'qe.metadata', or a character vector of column names already classified as 'qe.metadata'. |
|
| 130 |
#' @examples |
|
| 131 |
#' library(data.table) |
|
| 132 |
#' dt <- data.table(a = 1:5, b = 6:10) |
|
| 133 |
#' # Reclassify columns 'a' and 'b' as 'qe.metadata' |
|
| 134 |
#' dt <- metadata(dt, "a", "b") |
|
| 135 |
#' # List columns classified as 'qe.metadata' |
|
| 136 |
#' metadata_columns <- metadata(dt) |
|
| 137 |
#' @export |
|
| 138 |
metadata <- function(x, ...) {
|
|
| 139 | ! |
x <- as.qe.data(x); |
| 140 | ||
| 141 | ! |
if(...length() > 0) {
|
| 142 | ! |
dot_args <- list(...); |
| 143 | ||
| 144 | ! |
dot_args$x <- x; |
| 145 | ! |
dot_args$v <- as.qe.metadata; |
| 146 | ! |
x <- do.call(reclassify, dot_args); |
| 147 | ||
| 148 | ! |
return(x); |
| 149 |
} |
|
| 150 |
else {
|
|
| 151 | ! |
return(colnames(x)[sapply(x, is.qe.metadata)]); |
| 152 |
} |
|
| 153 |
} |
|
| 154 | ||
| 155 |
#' Reclassify specified columns as units or list unit columns in a data.table |
|
| 156 |
#' |
|
| 157 |
#' This function reclassifies specified columns of a data.table to the 'qe.unit' format if column names are provided. |
|
| 158 |
#' If no column names are provided, it returns the names of columns that are already classified as 'qe.unit'. |
|
| 159 |
#' |
|
| 160 |
#' @param x A data.table. The data.table containing the columns to be reclassified or checked. |
|
| 161 |
#' @param ... Additional arguments specifying the names of the columns to be reclassified. |
|
| 162 |
#' |
|
| 163 |
#' @return The modified data.table with specified columns reclassified as 'qe.unit', or a character vector of column names already classified as 'qe.unit'. |
|
| 164 |
#' @examples |
|
| 165 |
#' library(data.table) |
|
| 166 |
#' dt <- data.table(a = 1:5, b = 6:10) |
|
| 167 |
#' # Reclassify columns 'a' and 'b' as 'qe.unit' |
|
| 168 |
#' dt <- units(dt, "a", "b") |
|
| 169 |
#' # List columns classified as 'qe.unit' |
|
| 170 |
#' unit_columns <- units(dt) |
|
| 171 |
#' @export |
|
| 172 |
units <- function(x, ...) {
|
|
| 173 | ! |
x <- as.qe.data(x); |
| 174 | ||
| 175 | ! |
if(...length() > 0) {
|
| 176 | ! |
dot_args <- list(...); |
| 177 | ||
| 178 | ! |
dot_args$x <- x; |
| 179 | ! |
dot_args$v <- as.qe.unit; |
| 180 | ! |
x <- do.call(reclassify, dot_args); |
| 181 | ! |
return(x); |
| 182 |
} |
|
| 183 |
else {
|
|
| 184 | ! |
return(colnames(x)[sapply(x, is.qe.unit)]); |
| 185 |
} |
|
| 186 |
} |
|
| 187 | ||
| 188 |
#' Reclassify specified columns as horizon or list horizon columns in a data.table |
|
| 189 |
#' |
|
| 190 |
#' This function reclassifies specified columns of a data.table to the 'qe.horizon' format if column names are provided. |
|
| 191 |
#' If no column names are provided, it returns the names of columns that are already classified as 'qe.horizon'. |
|
| 192 |
#' |
|
| 193 |
#' @param x A data.table. The data.table containing the columns to be reclassified or checked. |
|
| 194 |
#' @param ... Additional arguments specifying the names of the columns to be reclassified. |
|
| 195 |
#' |
|
| 196 |
#' @return The modified data.table with specified columns reclassified as 'qe.horizon', or a character vector of column names already classified as 'qe.horizon'. |
|
| 197 |
#' @examples |
|
| 198 |
#' library(data.table) |
|
| 199 |
#' dt <- data.table(a = 1:5, b = 6:10) |
|
| 200 |
#' # Reclassify columns 'a' and 'b' as 'qe.horizon' |
|
| 201 |
#' dt <- horizon(dt, "a", "b") |
|
| 202 |
#' # List columns classified as 'qe.horizon' |
|
| 203 |
#' horizon_columns <- horizon(dt) |
|
| 204 |
#' @export |
|
| 205 |
horizon <- function(x, ...) {
|
|
| 206 | ! |
x <- as.qe.data(x); |
| 207 | ||
| 208 | ! |
if(...length() > 0) {
|
| 209 | ! |
dot_args <- list(...); |
| 210 | ||
| 211 | ! |
dot_args$x <- x; |
| 212 | ! |
dot_args$v <- as.qe.horizon; |
| 213 | ! |
x <- do.call(reclassify, dot_args); |
| 214 | ||
| 215 | ! |
return(x); |
| 216 |
} |
|
| 217 |
else {
|
|
| 218 | ! |
return(colnames(x)[sapply(x, is.qe.horizon)]); |
| 219 |
} |
|
| 220 |
} |
|
| 221 | ||
| 222 |
#' @export |
|
| 223 |
'@.horizon' <- horizon |
|
| 224 | ||
| 225 |
#' Reclassify specified columns in a data.table |
|
| 226 |
#' |
|
| 227 |
#' This function reclassifies specified columns of a data.table using a provided function. |
|
| 228 |
#' |
|
| 229 |
#' @param x A data.table. The data.table containing the columns to be reclassified. |
|
| 230 |
#' @param ... Additional arguments specifying the names of the columns to be reclassified. |
|
| 231 |
#' @param v A function. The function to apply to each specified column for reclassification. |
|
| 232 |
#' |
|
| 233 |
#' @return The modified data.table with specified columns reclassified. |
|
| 234 |
#' @examples |
|
| 235 |
#' library(data.table) |
|
| 236 |
#' dt <- data.table(a = 1:5, b = 6:10) |
|
| 237 |
#' dt <- reclassify(dt, as.qe.code, "a", "b") |
|
| 238 |
#' @export |
|
| 239 |
reclassify <- function(x, v, ...) {
|
|
| 240 | ! |
wh <- list(...); |
| 241 | ! |
for (i in wh) {
|
| 242 | ! |
data.table::set(x, j = i, value = v(x[[i]])) |
| 243 |
} |
|
| 244 | ||
| 245 | ! |
return(x); |
| 246 |
} |
| 1 |
## |
|
| 2 |
#' @title Generate a plot of an ENAset |
|
| 3 |
#' |
|
| 4 |
#' @description Generates an a plot from a given ENA set object |
|
| 5 |
#' |
|
| 6 |
#' @details This function defines the axes and other features of a plot for displaying an ENAset; generates an ENAplot object that can used to plot points, network graphs, and other information from an ENAset |
|
| 7 |
#' |
|
| 8 |
#' @export |
|
| 9 |
#' |
|
| 10 |
#' @param enaset The \code{\link{ENAset}} that will be used to generate a plot
|
|
| 11 |
#' @param title A character used for the title of the plot, default: ENA Plot |
|
| 12 |
#' @param dimension.labels A character vector containing labels for the axes, default: c(X, Y) |
|
| 13 |
#' @param font.size An integer determining the font size for graph labels, default: 10 |
|
| 14 |
#' @param font.color A character determining the color of label font, default: black |
|
| 15 |
#' @param font.family A character determining the font type, choices: Arial, Courier New, Times New Roman, default: Arial |
|
| 16 |
#' @param scale.to "network" (default), "points", or a list with x and y ranges. Network and points both scale to the c(-max, max) of the corresponding data.frame |
|
| 17 |
#' @param backend \code{"plotly"} (default) or \code{"qeviz"}. Default:
|
|
| 18 |
#' \code{getOption("rENA.plot.backend", "plotly")}. With \code{"qeviz"} the
|
|
| 19 |
#' plot is drawn by the qeviz package (>= 0.5.0) and \code{$plot} is a qeviz
|
|
| 20 |
#' htmlwidget; \code{scale.to} must be \code{"network"}, \code{"points"} or
|
|
| 21 |
#' a number. |
|
| 22 |
#' @param ... additional parameters addressed in inner function |
|
| 23 |
#' |
|
| 24 |
#' |
|
| 25 |
#' @seealso \code{\link{ena.make.set}}, \code{\link{ena.plot.points}},
|
|
| 26 |
#' \link{rENA-plot-backends}
|
|
| 27 |
#' |
|
| 28 |
#' @examples |
|
| 29 |
#' data(RS.data) |
|
| 30 |
#' |
|
| 31 |
#' codeNames = c('Data','Technical.Constraints','Performance.Parameters',
|
|
| 32 |
#' 'Client.and.Consultant.Requests','Design.Reasoning','Collaboration'); |
|
| 33 |
#' |
|
| 34 |
#' accum = ena.accumulate.data( |
|
| 35 |
#' units = RS.data[,c("UserName","Condition")],
|
|
| 36 |
#' conversation = RS.data[,c("Condition","GroupName")],
|
|
| 37 |
#' metadata = RS.data[,c("CONFIDENCE.Change","CONFIDENCE.Pre","CONFIDENCE.Post")],
|
|
| 38 |
#' codes = RS.data[,codeNames], |
|
| 39 |
#' window.size.back = 4 |
|
| 40 |
#' ) |
|
| 41 |
#' |
|
| 42 |
#' set = ena.make.set( |
|
| 43 |
#' enadata = accum |
|
| 44 |
#' ) |
|
| 45 |
#' |
|
| 46 |
#' plot = ena.plot(set) |
|
| 47 |
#' |
|
| 48 |
#' group1.points = set$points.rotated[set$enadata$units$Condition == "FirstGame",] |
|
| 49 |
#' plot = ena.plot.points(plot, points = group1.points); |
|
| 50 |
#' print(plot); |
|
| 51 |
#' |
|
| 52 |
#' @return \code{\link{ENAplot}} used for plotting an ENAset
|
|
| 53 | ||
| 54 |
## |
|
| 55 |
ena.plot <- function( |
|
| 56 |
enaset, |
|
| 57 | ||
| 58 |
title = "ENA Plot", |
|
| 59 | ||
| 60 |
dimension.labels = c("",""),
|
|
| 61 | ||
| 62 |
font.size = 10, |
|
| 63 |
font.color = "#000000", |
|
| 64 |
font.family = c("Arial", "Courier New", "Times New Roman"),
|
|
| 65 |
scale.to = "network", #, "points"), |
|
| 66 |
backend = getOption("rENA.plot.backend", "plotly"),
|
|
| 67 |
... |
|
| 68 |
) {
|
|
| 69 | 73x |
if (is(enaset, "ENAset")) {
|
| 70 | 1x |
warning(paste0("Usage of ENAset objects will be deprecated ",
|
| 71 | 1x |
"and potentially removed altogether in future versions.")) |
| 72 | ||
| 73 | 1x |
enaset <- ena.set(enaset); |
| 74 |
} |
|
| 75 | ||
| 76 | 73x |
font.family = match.arg(font.family); |
| 77 | ||
| 78 | 73x |
plot = ENAplot$new(enaset, |
| 79 | 73x |
title, |
| 80 | 73x |
dimension.labels, |
| 81 | 73x |
font.size, |
| 82 | 73x |
font.color, |
| 83 | 73x |
font.family, |
| 84 | 73x |
scale.to = scale.to, |
| 85 | 73x |
backend = backend, |
| 86 |
... |
|
| 87 |
); |
|
| 88 | ||
| 89 | 71x |
return(plot); |
| 90 |
} |
| 1 |
### centering for projection |
|
| 2 | ||
| 3 |
center.projection = function(lws, rotation) {
|
|
| 4 | 4x |
if(is.null(rotation) || is.null(rotation$center.vec)) {
|
| 5 | 1x |
stop("Supplied value for `rotation` does not have a center vector");
|
| 6 |
} |
|
| 7 | 3x |
mean_ <- rotation$center.vec; |
| 8 | ||
| 9 | 3x |
centered.lws <- t(lws) - mean_; |
| 10 | ||
| 11 | 3x |
return( t(centered.lws) ); |
| 12 |
} |
|
| 13 | ||
| 14 |
# og_lws = as.matrix(set.new$line.weights) |
|
| 15 |
# set.new$rotation$center.vec = colMeans(og_lws) |
|
| 16 |
# |
|
| 17 |
# |
|
| 18 |
# |
|
| 19 |
# test = center.projection(lws = og_lws,set.new) |
|
| 20 |
# centered_og = rENA:::center_data_c(as.matrix(set.new$line.weights)) |
|
| 21 |
# # |
|
| 22 |
# View(test == centered_og) ### DIFFERENCE IN ROUNDING |
|
| 23 |
# View(round(test,3) == round(centered_og,3)) |
| 1 |
## |
|
| 2 |
#' @title Names to Adjacency Key |
|
| 3 |
#' |
|
| 4 |
#' @description Convert a vector of strings, representing names of a square matrix, to an adjacency |
|
| 5 |
#' |
|
| 6 |
#' @details Returns a matrix of 2 rows by choose(length(vector), 2) columns, |
|
| 7 |
#' or by \code{n * (n + 1) / 2} columns when \code{diagonal} is \code{TRUE}
|
|
| 8 |
#' (the self-connection layout: upper triangle with the diagonal, column-major, |
|
| 9 |
#' e.g. A & A, A & B, B & B, A & C, B & C, C & C). |
|
| 10 |
#' |
|
| 11 |
#' @param vector Vector representing the names of a square matrix |
|
| 12 |
#' @param upper_triangle Not Implemented |
|
| 13 |
#' @param diagonal Logical, include the diagonal (self-connections). Default FALSE |
|
| 14 |
#' |
|
| 15 |
#' @export |
|
| 16 |
## |
|
| 17 |
namesToAdjacencyKey <- function(vector, upper_triangle = TRUE, diagonal = FALSE) {
|
|
| 18 | 34x |
if (isTRUE(diagonal)) {
|
| 19 | 5x |
n <- length(vector) |
| 20 | 5x |
j <- rep(seq_len(n), seq_len(n)) |
| 21 | 5x |
i <- sequence(seq_len(n)) |
| 22 | 5x |
return(matrix(vector[rbind(i, j)], nrow = 2)) |
| 23 |
} |
|
| 24 | 29x |
upperTriIndices = triIndices(length(vector)) + 1; |
| 25 | 29x |
matrix(vector[upperTriIndices], nrow=2) |
| 26 |
} |
| 1 |
### |
|
| 2 |
#' Calculate the correlations |
|
| 3 |
#' |
|
| 4 |
#' @description Calculate both Spearman and Pearson correlations for the |
|
| 5 |
#' provided ENAset |
|
| 6 |
#' |
|
| 7 |
#' @param enaset ENAset to run correlations on |
|
| 8 |
#' @param dims The dimensions to calculate the correlations for. Default: c(1,2) |
|
| 9 |
#' |
|
| 10 |
#' @return Matrix of 2 columns, one for each correlation method, with the corresponding |
|
| 11 |
#' correlations per dimension as the rows. |
|
| 12 |
#' |
|
| 13 |
#' @export |
|
| 14 |
### |
|
| 15 |
ena.correlations <- function(enaset, dims = c(1:2)) {
|
|
| 16 | 2x |
pComb = combn(nrow(enaset$points),2) |
| 17 | 2x |
point1 = pComb[1,] |
| 18 | 2x |
point2 = pComb[2,] |
| 19 | ||
| 20 | 2x |
points = as.matrix(enaset$points) |
| 21 | 2x |
centroids = as.matrix(enaset$model$centroids) |
| 22 | 2x |
svdDiff = matrix(points[point1, dims] - points[point2, dims], ncol=length(dims), nrow=length(point1)) |
| 23 | 2x |
optDiff = matrix(centroids[point1, dims] - centroids[point2, dims], ncol=length(dims), nrow=length(point1)) |
| 24 | ||
| 25 |
# svdDiff/optDiff were built with one column per requested dimension, so they |
|
| 26 |
# are indexed by position, not by the caller's dimension number. Indexing by |
|
| 27 |
# `dims` only happens to work for the default c(1,2); any other pair (e.g. |
|
| 28 |
# c(1,3)) reads past the end and errors with "subscript out of bounds". |
|
| 29 | 2x |
correlations = as.data.frame(mapply(function(method) {
|
| 30 | 4x |
lapply(seq_along(dims), function(dim) {
|
| 31 | 8x |
cor(as.numeric(svdDiff[,dim]), as.numeric(optDiff[,dim]), method=method) |
| 32 |
}); |
|
| 33 | 2x |
}, c("pearson","spearman")))
|
| 34 | ||
| 35 | 2x |
return(correlations); |
| 36 |
} |
|
| 37 |
| 1 |
#' Connection counts as square matrix |
|
| 2 |
#' |
|
| 3 |
#' @param x ena.set or ena.connections (i.e. set$connection.counts) |
|
| 4 |
#' |
|
| 5 |
#' @return matrix |
|
| 6 |
#' @export |
|
| 7 |
connection.matrix <- function(x) {
|
|
| 8 | 4x |
if(is(x, "ena.set")) {
|
| 9 | 2x |
connections <- x$connection.counts |
| 10 |
} else {
|
|
| 11 | 2x |
connections <- x |
| 12 |
} |
|
| 13 | 4x |
if(!is(connections, "ena.connections")) {
|
| 14 | 1x |
stop("Unable to find connections. `x` must be connections from an ena.set or an ena.set")
|
| 15 |
} |
|
| 16 | ||
| 17 | 3x |
simplify <- (nrow(connections) == 1) |
| 18 | 3x |
cm <- as.matrix(connections, square = T, simplify = simplify) |
| 19 | 3x |
if(simplify == FALSE && is.list(cm)) |
| 20 | 2x |
names(cm) <- connections$ENA_UNIT |
| 21 | ||
| 22 | 3x |
return(cm); |
| 23 |
} |
| 1 |
.fit_etm_poly_curve <- function( |
|
| 2 |
points, |
|
| 3 |
degree = NULL, |
|
| 4 |
max_degree = 3L, |
|
| 5 |
criterion = c("loocv", "aic")
|
|
| 6 |
) {
|
|
| 7 | 2x |
criterion <- match.arg(criterion) |
| 8 | 2x |
pts <- as.data.frame(points) |
| 9 | ||
| 10 | 2x |
if (ncol(pts) < 2L) {
|
| 11 | ! |
stop("ETM polynomial trajectory fitting requires at least two coordinate columns.")
|
| 12 |
} |
|
| 13 | ||
| 14 | 2x |
tma::tma_fit_poly_curve( |
| 15 | 2x |
pts = data.frame( |
| 16 | 2x |
x = as.numeric(pts[[1L]]), |
| 17 | 2x |
y = as.numeric(pts[[2L]]) |
| 18 |
), |
|
| 19 | 2x |
x_col = "x", |
| 20 | 2x |
y_col = "y", |
| 21 | 2x |
degree = degree, |
| 22 | 2x |
max_degree = as.integer(max_degree), |
| 23 | 2x |
criterion = criterion, |
| 24 | 2x |
fit_curve_with_zero_pts = TRUE |
| 25 |
) |
|
| 26 |
} |
|
| 27 | ||
| 28 |
.eval_etm_poly_curve <- function(curve, n_eval) {
|
|
| 29 | 2x |
t_eval <- seq(0, 1, length.out = as.integer(n_eval)) |
| 30 | 2x |
data.frame( |
| 31 | 2x |
x = curve$pred_x(t_eval), |
| 32 | 2x |
y = curve$pred_y(t_eval) |
| 33 |
) |
|
| 34 |
} |
| 1 |
/** @file ccd.hpp |
|
| 2 |
* @brief Cross-Covariance Decay (CCD) window-size estimation. |
|
| 3 |
* |
|
| 4 |
* Estimates the ENA moving-window size from the "discourse coherence length": |
|
| 5 |
* the half-life decay lag of the noise-corrected Frobenius norm of pooled |
|
| 6 |
* conversation cross-covariance matrices. |
|
| 7 |
* |
|
| 8 |
* Reference: Shaffer, D. W. & Cai, Z. (2026). Discourse Coherence Length: |
|
| 9 |
* Noise-Corrected Covariance Estimation of Window-Size in Epistemic Network |
|
| 10 |
* Analysis. International Conference on Quantitative Ethnography (ICQE26). |
|
| 11 |
* |
|
| 12 |
* This kernel is pure-numeric: it consumes conversation subsets that have |
|
| 13 |
* already been extracted from the raw data (one code matrix per conversation, |
|
| 14 |
* rows in sequence). Column selection and conversation splitting belong in |
|
| 15 |
* each language wrapper (cf. accumulation.hpp), so that the C++ core never |
|
| 16 |
* touches data-frame semantics. |
|
| 17 |
*/ |
|
| 18 |
#ifndef LIBQE_CCD_HPP |
|
| 19 |
#define LIBQE_CCD_HPP |
|
| 20 | ||
| 21 |
#include <armadillo> |
|
| 22 |
#include <cmath> |
|
| 23 |
#include <vector> |
|
| 24 | ||
| 25 |
namespace qe {
|
|
| 26 | ||
| 27 |
/** @brief Result of a CCD window-size estimation. |
|
| 28 |
* |
|
| 29 |
* The `*` vectors are all length `max_window + 1`, indexed by lag `0..max_window`. |
|
| 30 |
* Entries for lags with no contributing conversation are NaN (weight 0). |
|
| 31 |
*/ |
|
| 32 |
struct CCDResult {
|
|
| 33 |
int window_size; ///< Estimated half-life lag window size (>= 1). |
|
| 34 |
int peak_lag; ///< Lag of the peak corrected cross-covariance norm. |
|
| 35 |
arma::vec lag; ///< Evaluated lags, 0..max_window. |
|
| 36 |
arma::vec frob; ///< Uncorrected pooled Frobenius norm per lag. |
|
| 37 |
arma::vec frob_sq_unbiased; ///< Noise-corrected squared Frobenius norm per lag. |
|
| 38 |
arma::vec frob_unbiased_signed; ///< sign * sqrt(|corrected sq|) per lag. |
|
| 39 |
arma::vec total_weight; ///< Pooled overlap weight per lag. |
|
| 40 |
}; |
|
| 41 | ||
| 42 |
/** @brief Estimate the ENA moving-window size via cross-covariance decay. |
|
| 43 |
* |
|
| 44 |
* For each lag @c l in @c 0..max_window, pools window-matched cross-covariance |
|
| 45 |
* matrices across all conversation subsets, subtracts a trace-variance noise |
|
| 46 |
* floor, and locates the first lag strictly after the peak where the corrected |
|
| 47 |
* norm falls to half of its peak value. |
|
| 48 |
* |
|
| 49 |
* @param[in] conversations One code matrix per conversation (N_k rows × C codes, |
|
| 50 |
* rows in sequence). All matrices must share the same |
|
| 51 |
* number of columns. |
|
| 52 |
* @param[in] max_window Maximum lag to evaluate. Default 20. |
|
| 53 |
* @param[in] min_overlap Minimum overlapping rows (N_k - l) required for a |
|
| 54 |
* conversation to contribute at lag l. Default 10. |
|
| 55 |
* |
|
| 56 |
* @returns A @ref CCDResult. When no conversation contributes at any positive |
|
| 57 |
* lag (all-NaN or non-positive corrected covariance), the window size |
|
| 58 |
* defaults to 1 and @c peak_lag to 0. |
|
| 59 |
*/ |
|
| 60 | 10x |
inline CCDResult ccd_window( |
| 61 |
const std::vector<arma::mat>& conversations, |
|
| 62 |
int max_window = 20, |
|
| 63 |
int min_overlap = 10 |
|
| 64 |
) {
|
|
| 65 |
if (max_window < 0) max_window = 0; |
|
| 66 | 10x |
const int n_lags = max_window + 1; |
| 67 | ||
| 68 | 10x |
CCDResult out; |
| 69 | 10x |
out.lag = arma::regspace<arma::vec>(0, max_window); |
| 70 | 10x |
out.frob = arma::vec(n_lags).fill(arma::datum::nan); |
| 71 | 10x |
out.frob_sq_unbiased = arma::vec(n_lags).fill(arma::datum::nan); |
| 72 | 10x |
out.frob_unbiased_signed = arma::vec(n_lags).fill(arma::datum::nan); |
| 73 | 20x |
out.total_weight = arma::zeros<arma::vec>(n_lags); |
| 74 | 10x |
out.window_size = 1; |
| 75 | 10x |
out.peak_lag = 0; |
| 76 | ||
| 77 |
// Determine code count from the first non-empty conversation. |
|
| 78 | 10x |
arma::uword C = 0; |
| 79 | 20x |
for (const arma::mat& X : conversations) {
|
| 80 | 10x |
if (X.n_cols > 0) { C = X.n_cols; break; }
|
| 81 |
} |
|
| 82 |
if (C == 0) return out; // nothing to work with -> window 1 |
|
| 83 | ||
| 84 |
// 1. Cross-covariance curves & noise floor across lags. |
|
| 85 | 189x |
for (int i = 0; i < n_lags; ++i) {
|
| 86 | 179x |
const int l = i; |
| 87 | 179x |
arma::mat Cov_sum = arma::zeros<arma::mat>(C, C); |
| 88 | 179x |
double total_weight = 0.0; |
| 89 | 179x |
double noise_floor_sq_weighted_sum = 0.0; |
| 90 | ||
| 91 | 3938x |
for (const arma::mat& X_k : conversations) {
|
| 92 | 3580x |
const int N_k = static_cast<int>(X_k.n_rows); |
| 93 | 3580x |
const int weight = N_k - l; |
| 94 |
if (weight < min_overlap) continue; |
|
| 95 | ||
| 96 |
// A = rows [l, N_k-1], B = rows [0, N_k-1-l]; both have `weight` rows. |
|
| 97 | 7160x |
const arma::mat A = X_k.rows(l, N_k - 1); |
| 98 | 7160x |
const arma::mat B = X_k.rows(0, N_k - 1 - l); |
| 99 | ||
| 100 | 3580x |
const arma::rowvec p_A = arma::mean(A, 0); |
| 101 | 3580x |
const arma::rowvec p_B = arma::mean(B, 0); |
| 102 | ||
| 103 |
// Window-matched cross-covariance: crossprod(B, A)/w - outer(p_B, p_A). |
|
| 104 | 10740x |
const arma::mat Cov_k = (B.t() * A) / weight - p_B.t() * p_A; |
| 105 | ||
| 106 |
// Trace-variance noise floor (n-1 denominator, matching R's var()). |
|
| 107 |
const double tr_var_A = |
|
| 108 | 3580x |
arma::accu(arma::square(A.each_row() - p_A)) / (weight - 1); |
| 109 |
const double tr_var_B = |
|
| 110 | 3580x |
arma::accu(arma::square(B.each_row() - p_B)) / (weight - 1); |
| 111 | 3580x |
const double var_k_sq = tr_var_A * tr_var_B; |
| 112 | ||
| 113 | 7160x |
Cov_sum += weight * Cov_k; |
| 114 | 3580x |
total_weight += weight; |
| 115 | 3580x |
noise_floor_sq_weighted_sum += weight * var_k_sq; |
| 116 |
} |
|
| 117 | ||
| 118 | 179x |
if (total_weight > 0.0) {
|
| 119 | 179x |
const arma::mat Cov_pooled = Cov_sum / total_weight; |
| 120 | 179x |
const double frob_sq_pooled = arma::accu(arma::square(Cov_pooled)); |
| 121 | 179x |
const double noise_floor_sq_pooled = |
| 122 | 179x |
noise_floor_sq_weighted_sum / (total_weight * total_weight); |
| 123 | 179x |
const double frob_sq_unbiased_pooled = |
| 124 |
frob_sq_pooled - noise_floor_sq_pooled; |
|
| 125 | ||
| 126 |
const double sgn = (frob_sq_unbiased_pooled > 0.0) ? 1.0 |
|
| 127 | ! |
: (frob_sq_unbiased_pooled < 0.0) ? -1.0 : 0.0; |
| 128 | ||
| 129 | 179x |
out.frob[i] = std::sqrt(frob_sq_pooled); |
| 130 | 179x |
out.frob_sq_unbiased[i] = frob_sq_unbiased_pooled; |
| 131 | 179x |
out.frob_unbiased_signed[i] = sgn * std::sqrt(std::abs(frob_sq_unbiased_pooled)); |
| 132 | 179x |
out.total_weight[i] = total_weight; |
| 133 |
} |
|
| 134 |
} |
|
| 135 | ||
| 136 |
// 2. Half-life decay lag detection over the signed corrected norm. |
|
| 137 | 10x |
const arma::vec& f = out.frob_unbiased_signed; |
| 138 | ||
| 139 |
// Peak among lags > 0 with finite, positive corrected norm. |
|
| 140 | 10x |
int peak_lag = -1; |
| 141 | 10x |
double max_f = -arma::datum::inf; |
| 142 | 179x |
for (int i = 1; i < n_lags; ++i) {
|
| 143 | 676x |
if (std::isfinite(f[i]) && f[i] > 0.0 && f[i] > max_f) {
|
| 144 | 10x |
max_f = f[i]; |
| 145 | 10x |
peak_lag = i; // lag == index |
| 146 |
} |
|
| 147 |
} |
|
| 148 | ||
| 149 | 10x |
if (peak_lag < 0) {
|
| 150 |
// No positive corrected covariance anywhere -> default window 1. |
|
| 151 | ! |
out.window_size = 1; |
| 152 | ! |
out.peak_lag = 0; |
| 153 | ! |
return out; |
| 154 |
} |
|
| 155 | 10x |
out.peak_lag = peak_lag; |
| 156 | ||
| 157 |
// First lag at or after the peak whose corrected norm has decayed to <= 50%. |
|
| 158 |
// NaN comparisons are false, so NaN lags are skipped (matching R's which()). |
|
| 159 | 10x |
int decay_lag = -1; |
| 160 | 59x |
for (int i = peak_lag; i < n_lags; ++i) {
|
| 161 | 116x |
if (f[i] / max_f <= 0.5) { decay_lag = i; break; }
|
| 162 |
} |
|
| 163 | ||
| 164 | 10x |
if (decay_lag >= 0) {
|
| 165 | 9x |
out.window_size = decay_lag; |
| 166 |
} else {
|
|
| 167 |
// No decay below half-life within max_window: fall back to the last lag |
|
| 168 |
// (at or after the peak) with a finite corrected norm. |
|
| 169 | 1x |
int last_valid = -1; |
| 170 | 5x |
for (int i = peak_lag; i < n_lags; ++i) {
|
| 171 | 8x |
if (std::isfinite(f[i])) last_valid = i; |
| 172 |
} |
|
| 173 |
out.window_size = (last_valid >= 0) ? last_valid : 1; |
|
| 174 |
} |
|
| 175 | ||
| 176 | 10x |
return out; |
| 177 |
} |
|
| 178 | ||
| 179 |
} // namespace qe |
|
| 180 | ||
| 181 |
#endif // LIBQE_CCD_HPP |
| 1 |
/** |
|
| 2 |
* @file generalized_rotation.hpp |
|
| 3 |
* @brief Generalized Means Rotation (GMR) with Lasso-based covariate adjustment. |
|
| 4 |
* |
|
| 5 |
* @details |
|
| 6 |
* Mirrors rENA's `ena.rotate.by.generalized()` + `gmr()` + `get_x1_main_effect()`. |
|
| 7 |
* Authoritative reference: commit 46776a1981a90b3a3b2861ed1010e9dbb7acf901 |
|
| 8 |
* ("Fixed bugs in rotation functions") by Zhiqiang Cai.
|
|
| 9 |
* |
|
| 10 |
* **Algorithm overview** |
|
| 11 |
* |
|
| 12 |
* 1. Compute x_vector: the rotation axis that best represents the target |
|
| 13 |
* variable's contribution to the ENA point space, after controlling for |
|
| 14 |
* covariates via Lasso (`lasso_x1_contribution`). |
|
| 15 |
* 2. Deflate V by x_vector → defA = V - V*x*x'. |
|
| 16 |
* 3. Optionally compute secondary axis x1: the normalised group-mean |
|
| 17 |
* difference in the *original* V space (not defA) for groups 0 vs 1. |
|
| 18 |
* x1 stays empty when the target is not categorical or there are not |
|
| 19 |
* exactly 2 groups — there is no SVD fallback. |
|
| 20 |
* 4. Compute y_vector from a GMR call on defA, or from SVD of defA. |
|
| 21 |
* Explicitly orthogonalise y_vector against x_vector, and against x1 |
|
| 22 |
* when x1 is available. |
|
| 23 |
* 5. Double-deflate V by x_vector and y_vector, then call `complete_rotation()` |
|
| 24 |
* to fill remaining dimensions from the SVD of the doubly-deflated space. |
|
| 25 |
* |
|
| 26 |
* **Caller responsibilities** |
|
| 27 |
* |
|
| 28 |
* - Build the model matrix (`X_raw`) from the data frame, including dummy |
|
| 29 |
* variables for categorical predictors and any desired interaction terms. |
|
| 30 |
* No formula parsing is done here. |
|
| 31 |
* - Identify `x1_cols`: the 0-based column indices in `X_raw` that correspond to |
|
| 32 |
* the target variable (these are unpenalized in the Lasso). |
|
| 33 |
* - Encode categorical targets as 0-based integer codes in `x_target`. |
|
| 34 |
* - Set `x_n_groups` to the number of distinct groups when `x_categorical=true`. |
|
| 35 |
* |
|
| 36 |
* The R wrapper (`rENA::ena.rotate.by.generalized`) handles these steps and |
|
| 37 |
* calls `libqe::generalized_means_rotation()` with the prepared matrices. |
|
| 38 |
*/ |
|
| 39 | ||
| 40 |
#ifndef LIBQE_GENERALIZED_ROTATION_HPP |
|
| 41 |
#define LIBQE_GENERALIZED_ROTATION_HPP |
|
| 42 | ||
| 43 |
#include <libqe/linalg_fallback.hpp> // qe::linalg::eig_sym (works w/o LAPACK) |
|
| 44 |
#include <libqe/lasso.hpp> // lasso_x1_contribution |
|
| 45 |
#include "rotation.hpp" // ena_svd, complete_rotation, RotationResult |
|
| 46 | ||
| 47 |
#include <armadillo> |
|
| 48 |
#include <stdexcept> |
|
| 49 |
#include <string> |
|
| 50 |
#include <vector> |
|
| 51 | ||
| 52 |
namespace qe {
|
|
| 53 | ||
| 54 |
/// @name Between-group scatter |
|
| 55 |
/// @{
|
|
| 56 | ||
| 57 |
/** |
|
| 58 |
* @brief Compute the between-group scatter matrix S_B. |
|
| 59 |
* |
|
| 60 |
* @details |
|
| 61 |
* Computes S_B = Σ_g n_g * (μ_g − μ)(μ_g − μ)' where μ is the grand mean. |
|
| 62 |
* |
|
| 63 |
* @note Mirrors rENA's `compute_SB()` (commit 46776a1981a90b3a3b2861ed1010e9dbb7acf901). |
|
| 64 |
* |
|
| 65 |
* @param[in] V n×p matrix of ENA points (or covariate-adjusted contribution). |
|
| 66 |
* @param[in] labels n×1 0-based integer group labels. |
|
| 67 |
* @param[in] n_groups Number of distinct groups. |
|
| 68 |
* |
|
| 69 |
* @returns p×p symmetric between-group scatter matrix. |
|
| 70 |
*/ |
|
| 71 | 57x |
inline arma::mat between_group_scatter( |
| 72 |
const arma::mat& V, |
|
| 73 |
const arma::uvec& labels, |
|
| 74 |
arma::uword n_groups |
|
| 75 |
) {
|
|
| 76 | 57x |
const arma::uword p = V.n_cols; |
| 77 | 57x |
arma::rowvec mu = arma::mean(V, 0); |
| 78 | 57x |
arma::mat SB(p, p, arma::fill::zeros); |
| 79 | ||
| 80 | 183x |
for (arma::uword g = 0; g < n_groups; ++g) {
|
| 81 | 126x |
arma::uvec idx = arma::find(labels == g); |
| 82 | 126x |
if (idx.is_empty()) continue; |
| 83 | 375x |
arma::vec diff = (arma::mean(V.rows(idx), 0) - mu).t(); |
| 84 | 250x |
SB += static_cast<double>(idx.n_elem) * diff * diff.t(); |
| 85 |
} |
|
| 86 | 57x |
return SB; |
| 87 |
} |
|
| 88 | ||
| 89 |
/// @} |
|
| 90 | ||
| 91 |
/// @name Rotation direction |
|
| 92 |
/// @{
|
|
| 93 | ||
| 94 |
/** |
|
| 95 |
* @brief Compute the unit rotation direction from a covariate-adjusted contribution matrix. |
|
| 96 |
* |
|
| 97 |
* @details |
|
| 98 |
* **Numeric target:** |
|
| 99 |
* Centres the target, then computes β = (t_c' t_c)^{-1} t_c' Vx (OLS slope from
|
|
| 100 |
* lm(Vx ~ target)). Mirrors R: `model = lm(Vx ~ target); beta = coefficients[2,]`. |
|
| 101 |
* |
|
| 102 |
* **Categorical target:** |
|
| 103 |
* Computes the between-group scatter matrix S_B and returns its leading |
|
| 104 |
* eigenvector (the direction of maximum between-group variance). |
|
| 105 |
* Mirrors R: `sb = compute_SB(Vx, target); r = svd(sb)$v[,1]`. |
|
| 106 |
* |
|
| 107 |
* @note Mirrors rENA's `gmr()` direction computation |
|
| 108 |
* (commit 46776a1981a90b3a3b2861ed1010e9dbb7acf901). |
|
| 109 |
* |
|
| 110 |
* @param[in] Vx n×q covariate-adjusted contribution matrix. |
|
| 111 |
* @param[in] target n×1 target variable (numeric float, or 0-based integer codes). |
|
| 112 |
* @param[in] categorical `true` if the target is categorical (uses between-group scatter); |
|
| 113 |
* `false` for a numeric target (uses OLS slope). |
|
| 114 |
* @param[in] n_groups Number of distinct groups; only used when `categorical` is `true`. |
|
| 115 |
* |
|
| 116 |
* @returns Unit vector (p×1) representing the rotation direction. |
|
| 117 |
* |
|
| 118 |
* @throws std::runtime_error If `categorical` is `true` and `n_groups < 2`. |
|
| 119 |
* @throws std::runtime_error If the numeric target has zero variance after centering. |
|
| 120 |
* @warning Throws `std::runtime_error` if the resulting direction has zero norm, |
|
| 121 |
* which can occur when the Lasso-adjusted contribution collapses to zero. |
|
| 122 |
*/ |
|
| 123 | 68x |
inline arma::vec gmr_direction( |
| 124 |
const arma::mat& Vx, // n×q covariate-adjusted contribution |
|
| 125 |
const arma::vec& target, // n×1 (numeric float, or 0-based int codes) |
|
| 126 |
bool categorical, |
|
| 127 |
arma::uword n_groups = 0 |
|
| 128 |
) {
|
|
| 129 | 68x |
arma::vec r; |
| 130 | ||
| 131 | 68x |
if (categorical) {
|
| 132 | 57x |
if (n_groups < 2) |
| 133 |
throw std::runtime_error( |
|
| 134 | ! |
"gmr_direction: x_n_groups must be >= 2 for categorical target"); |
| 135 | 57x |
arma::uvec labels = arma::conv_to<arma::uvec>::from(target); |
| 136 | 57x |
arma::mat SB = between_group_scatter(Vx, labels, n_groups); |
| 137 | ||
| 138 | 57x |
arma::vec eigval; arma::mat eigvec; |
| 139 | 57x |
qe::linalg::eig_sym(eigval, eigvec, SB); // ascending order |
| 140 | 114x |
r = eigvec.col(eigvec.n_cols - 1); // largest eigenvalue → last col |
| 141 |
} else {
|
|
| 142 |
// Centre target to get the OLS slope (mirrors R's lm() intercept handling) |
|
| 143 | 22x |
arma::vec tc = target - arma::mean(target); |
| 144 | 11x |
double tTt = arma::dot(tc, tc); |
| 145 | 11x |
if (tTt < 1e-12) |
| 146 |
throw std::runtime_error( |
|
| 147 | ! |
"gmr_direction: target has zero variance after centering"); |
| 148 | 11x |
arma::rowvec beta = (tc.t() * Vx) / tTt; // 1×q slope vector |
| 149 | 11x |
r = beta.t(); |
| 150 |
} |
|
| 151 | ||
| 152 | 68x |
double norm = arma::norm(r); |
| 153 | 68x |
if (norm < 1e-12) |
| 154 | ! |
throw std::runtime_error("gmr_direction: resulting direction has zero norm");
|
| 155 | 136x |
return r / norm; |
| 156 |
} |
|
| 157 | ||
| 158 |
/// @} |
|
| 159 | ||
| 160 |
/** |
|
| 161 |
* @brief Result of one GMR axis step. |
|
| 162 |
*/ |
|
| 163 |
struct GMRStepResult {
|
|
| 164 |
arma::vec direction; ///< Unit vector in connection space (p×1). |
|
| 165 |
}; |
|
| 166 | ||
| 167 |
/// @name GMR axis step |
|
| 168 |
/// @{
|
|
| 169 | ||
| 170 |
/** |
|
| 171 |
* @brief Compute the rotation direction for one axis of the Generalized Means Rotation. |
|
| 172 |
* |
|
| 173 |
* @details |
|
| 174 |
* Applies optional row subsetting, performs Lasso-based covariate adjustment |
|
| 175 |
* (or a simple OLS projection when all columns are target columns), then |
|
| 176 |
* delegates to `gmr_direction()` to produce a unit direction vector. |
|
| 177 |
* |
|
| 178 |
* When `x1_cols.n_elem >= p` (all columns are target columns and there are no |
|
| 179 |
* penalized covariates), the function falls back to a simple OLS projection: |
|
| 180 |
* Vx = fitted values from lm(V ~ t). |
|
| 181 |
* |
|
| 182 |
* @note Mirrors rENA's `gmr()` step logic |
|
| 183 |
* (commit 46776a1981a90b3a3b2861ed1010e9dbb7acf901). |
|
| 184 |
* |
|
| 185 |
* @param[in] V n×q matrix of ENA points (full data; subset applied internally). |
|
| 186 |
* @param[in] X_model n×p model matrix (caller-built; target cols + covariate cols). |
|
| 187 |
* @param[in] target n×1 target variable (raw float or 0-based integer codes). |
|
| 188 |
* @param[in] x1_cols 0-based indices of target columns in `X_model` (unpenalized in Lasso). |
|
| 189 |
* @param[in] categorical `true` if the target is categorical. |
|
| 190 |
* @param[in] n_groups Number of distinct groups (only used when `categorical` is `true`). |
|
| 191 |
* @param[in] subset Optional 0-based row indices to use; empty means use all rows. |
|
| 192 |
* @param[in] n_lambda Length of the Lasso lambda path (forwarded to `lasso_x1_contribution`). |
|
| 193 |
* @param[in] k_folds Number of cross-validation folds (forwarded to `lasso_x1_contribution`). |
|
| 194 |
* @param[in] lasso_eps lambda_min = lasso_eps * lambda_max (forwarded to `lasso_x1_contribution`). |
|
| 195 |
* |
|
| 196 |
* @returns `GMRStepResult` containing the unit direction vector. |
|
| 197 |
* |
|
| 198 |
* @throws std::runtime_error Propagated from `gmr_direction()` if the direction |
|
| 199 |
* is degenerate or the target has zero variance. |
|
| 200 |
*/ |
|
| 201 | 68x |
inline GMRStepResult gmr_step( |
| 202 |
const arma::mat& V, |
|
| 203 |
const arma::mat& X_model, |
|
| 204 |
const arma::vec& target, |
|
| 205 |
const arma::uvec& x1_cols, |
|
| 206 |
bool categorical, |
|
| 207 |
arma::uword n_groups, |
|
| 208 |
const arma::uvec& subset, |
|
| 209 |
int n_lambda = 50, |
|
| 210 |
int k_folds = 5, |
|
| 211 |
double lasso_eps = 0.01 |
|
| 212 |
) {
|
|
| 213 | 68x |
const arma::uword n = V.n_rows; |
| 214 | 68x |
const arma::uword p = X_model.n_cols; |
| 215 | ||
| 216 |
// ── Select rows ────────────────────────────────────────────────────────── |
|
| 217 | 68x |
arma::uvec rows = subset.is_empty() |
| 218 | 68x |
? arma::regspace<arma::uvec>(0, n - 1) |
| 219 | 68x |
: subset; |
| 220 | ||
| 221 | 68x |
const arma::mat V_sub = V.rows(rows); |
| 222 | 68x |
const arma::mat X_sub = X_model.rows(rows); |
| 223 | 136x |
const arma::vec t_sub = target.rows(rows); |
| 224 | ||
| 225 |
// ── Lasso-adjusted contribution Vx ─────────────────────────────────────── |
|
| 226 | 68x |
arma::mat Vx_sub; |
| 227 | 68x |
if (x1_cols.n_elem >= p) {
|
| 228 |
// All columns are target columns (no penalized covariates): |
|
| 229 |
// fall back to simple OLS projection (Vx = fitted values from lm(V~t)) |
|
| 230 | 136x |
arma::vec tc = t_sub - arma::mean(t_sub); |
| 231 | 68x |
double tTt = arma::dot(tc, tc); |
| 232 | 68x |
if (tTt > 1e-12) {
|
| 233 | 67x |
arma::rowvec b1 = (tc.t() * V_sub) / tTt; |
| 234 | 67x |
Vx_sub = tc * b1; |
| 235 |
} else {
|
|
| 236 | 1x |
Vx_sub.zeros(V_sub.n_rows, V_sub.n_cols); |
| 237 |
} |
|
| 238 |
} else {
|
|
| 239 | ! |
arma::vec pf(p, arma::fill::ones); |
| 240 | ! |
for (arma::uword j : x1_cols) pf(j) = 0.0; |
| 241 | ||
| 242 | ! |
Vx_sub = lasso_x1_contribution( |
| 243 | ! |
X_sub, V_sub, x1_cols, pf, n_lambda, k_folds, lasso_eps); |
| 244 |
} |
|
| 245 | ||
| 246 |
// ── Rotation direction ─────────────────────────────────────────────────── |
|
| 247 | 68x |
arma::vec direction = gmr_direction(Vx_sub, t_sub, categorical, n_groups); |
| 248 | ||
| 249 | 136x |
return {direction};
|
| 250 |
} |
|
| 251 | ||
| 252 |
/// @} |
|
| 253 | ||
| 254 |
/** |
|
| 255 |
* @brief Parameters for `generalized_means_rotation()`. |
|
| 256 |
* |
|
| 257 |
* @details |
|
| 258 |
* Groups all inputs needed to specify both the X and Y rotation axes, along |
|
| 259 |
* with Lasso tuning knobs. The Y axis is optional: when `has_y` is `false` |
|
| 260 |
* the second axis is taken from the leading SVD of the x-deflated space. |
|
| 261 |
*/ |
|
| 262 |
struct GeneralizedRotationParams {
|
|
| 263 |
/// @name X axis (required) |
|
| 264 |
/// @{
|
|
| 265 |
arma::mat x_model_matrix; ///< n×p model matrix for the X axis. |
|
| 266 |
arma::vec x_target; ///< n×1 target variable (raw float or 0-based integer codes). |
|
| 267 |
arma::uvec x1_cols; ///< 0-based target column indices in `x_model_matrix`. |
|
| 268 |
bool x_categorical = false; ///< `true` if the X target is categorical. |
|
| 269 |
arma::uword x_n_groups = 0; ///< Number of distinct groups (used when `x_categorical` is `true`). |
|
| 270 |
arma::uvec x_subset; ///< Optional row subset (0-based); empty means use all rows. |
|
| 271 |
/// @} |
|
| 272 | ||
| 273 |
/// @name Y axis (optional) |
|
| 274 |
/// @{
|
|
| 275 |
/// When `has_y` is `false`, the Y axis is taken from the leading SVD of the x-deflated space. |
|
| 276 |
bool has_y = false; ///< `true` if a Y-axis GMR target is provided. |
|
| 277 |
arma::mat y_model_matrix; ///< n×p model matrix for the Y axis. |
|
| 278 |
arma::vec y_target; ///< n×1 target variable for the Y axis. |
|
| 279 |
arma::uvec y1_cols; ///< 0-based target column indices in `y_model_matrix`. |
|
| 280 |
bool y_categorical = false; ///< `true` if the Y target is categorical. |
|
| 281 |
arma::uword y_n_groups = 0; ///< Number of distinct groups for the Y axis. |
|
| 282 |
/// The Y axis always uses all rows (no subset parameter). |
|
| 283 |
/// @} |
|
| 284 | ||
| 285 |
/// @name Lasso tuning |
|
| 286 |
/// @{
|
|
| 287 |
int n_lambda = 50; ///< Length of the lambda path. |
|
| 288 |
int k_folds = 5; ///< Number of cross-validation folds. |
|
| 289 |
double lasso_eps = 0.01; ///< lambda_min = lasso_eps * lambda_max. |
|
| 290 |
/// @} |
|
| 291 |
}; |
|
| 292 | ||
| 293 |
/// @name Generalized Means Rotation |
|
| 294 |
/// @{
|
|
| 295 | ||
| 296 |
/** |
|
| 297 |
* @brief Full Generalized Means Rotation (GMR) producing a complete q×q rotation matrix. |
|
| 298 |
* |
|
| 299 |
* @details |
|
| 300 |
* C++/Armadillo implementation of `ena.rotate.by.generalized()`, matching the |
|
| 301 |
* authoritative R implementation in commit 46776a1981a90b3a3b2861ed1010e9dbb7acf901 |
|
| 302 |
* by Zhiqiang Cai. |
|
| 303 |
* |
|
| 304 |
* **Steps performed:** |
|
| 305 |
* |
|
| 306 |
* 1. **X axis via GMR** — calls `gmr_step()` on V with the X-axis parameters. |
|
| 307 |
* 2. **Deflate V** — computes defA = V − V·x·x' for subsequent Y-axis work. |
|
| 308 |
* 3. **Secondary axis x1** — when the X target is categorical with ≥ 2 groups, |
|
| 309 |
* computes x1 = normalize(mean(V[group0]) − mean(V[group1])) using the |
|
| 310 |
* *original* V (not defA) and the full-length target (all n rows). |
|
| 311 |
* x1 is used only to orthogonalise y_vector; it does not deflate defA. |
|
| 312 |
* There is no SVD fallback: x1 stays empty if the condition is not met. |
|
| 313 |
* 4. **Y axis** — either GMR on defA (when `params.has_y` is `true`) or the |
|
| 314 |
* leading SVD direction of defA. |
|
| 315 |
* 5. **Orthogonalise y_vector** — mirrors R: |
|
| 316 |
* `y_vector <- y_vector - (t(y_vector) %*% x_vector) * x_vector` |
|
| 317 |
* and, when x1 is available, |
|
| 318 |
* `y_vector <- y_vector - (t(y_vector) %*% x1) * x1`, |
|
| 319 |
* followed by safe normalisation. |
|
| 320 |
* 6. **Complete with SVD** — double-deflates V by both x_vector and y_vector, |
|
| 321 |
* then calls `complete_rotation()` to fill remaining dimensions. |
|
| 322 |
* |
|
| 323 |
* @note Mirrors rENA's `ena.rotate.by.generalized()` |
|
| 324 |
* (commit 46776a1981a90b3a3b2861ed1010e9dbb7acf901). |
|
| 325 |
* |
|
| 326 |
* @param[in] V n×q matrix of ENA points (line weights; caller normalises/ |
|
| 327 |
* centres upstream). |
|
| 328 |
* @param[in] params Fully populated `GeneralizedRotationParams` struct. |
|
| 329 |
* |
|
| 330 |
* @returns `RotationResult` with: |
|
| 331 |
* - `rotation` q×q full rotation matrix (column j = axis j). |
|
| 332 |
* - `eigenvalues` q×1 variance per SVD-filled axis (named axes carry 0). |
|
| 333 |
* - `column_names` "GMR1", "GMR2" or "SVD2", "SVD3", …, "SVD{q}".
|
|
| 334 |
* |
|
| 335 |
* @throws std::runtime_error If `gmr_direction()` produces a degenerate direction |
|
| 336 |
* for the X or Y axis. |
|
| 337 |
* @throws std::runtime_error If y_vector has zero norm after orthogonalisation |
|
| 338 |
* (can happen when defA is rank-deficient or perfectly aligned with x_vector). |
|
| 339 |
*/ |
|
| 340 | 66x |
inline RotationResult generalized_means_rotation( |
| 341 |
const arma::mat& V, |
|
| 342 |
const GeneralizedRotationParams& params |
|
| 343 |
) {
|
|
| 344 |
// ── Step 1: X axis via GMR ──────────────────────────────────────────── |
|
| 345 |
GMRStepResult x_gmr = gmr_step( |
|
| 346 |
V, |
|
| 347 | 66x |
params.x_model_matrix, |
| 348 | 66x |
params.x_target, |
| 349 | 66x |
params.x1_cols, |
| 350 | 66x |
params.x_categorical, |
| 351 | 66x |
params.x_n_groups, |
| 352 | 66x |
params.x_subset, |
| 353 | 66x |
params.n_lambda, |
| 354 | 66x |
params.k_folds, |
| 355 | 66x |
params.lasso_eps); |
| 356 | ||
| 357 | 66x |
const arma::vec x_vector = x_gmr.direction; |
| 358 | ||
| 359 |
// ── Step 2: Deflate V by x_vector ───────────────────────────────────── |
|
| 360 |
// defA is used only for y_vector computation; x1 uses original V. |
|
| 361 | 132x |
const arma::mat defA = V - V * x_vector * x_vector.t(); |
| 362 | ||
| 363 |
// ── Step 3: Secondary axis x1 ───────────────────────────────────────── |
|
| 364 |
// Mirrors R: x1 = normalize(mean(V[group0]) - mean(V[group1])). |
|
| 365 |
// |
|
| 366 |
// Key points matching the authoritative R: |
|
| 367 |
// - Uses original V (not defA) |
|
| 368 |
// - Uses the full-length target (all n rows), not just the subset |
|
| 369 |
// - Groups are always 0 (first) and 1 (second) in 0-based encoding |
|
| 370 |
// - NO fallback: x1 stays empty when categorical/2-group condition fails |
|
| 371 |
// - x1 does NOT deflate defA; it is only used to orthogonalise y_vector |
|
| 372 | 66x |
arma::vec x1; |
| 373 | ||
| 374 | 66x |
if (params.x_categorical && params.x_n_groups >= 2) {
|
| 375 | 57x |
arma::uvec labels = arma::conv_to<arma::uvec>::from(params.x_target); |
| 376 | 57x |
arma::uvec g0 = arma::find(labels == 0); |
| 377 | 57x |
arma::uvec g1 = arma::find(labels == 1); |
| 378 | ||
| 379 |
if (!g0.is_empty() && !g1.is_empty()) {
|
|
| 380 | 57x |
arma::rowvec m0 = arma::mean(V.rows(g0), 0); // original V, not defA |
| 381 | 57x |
arma::rowvec m1 = arma::mean(V.rows(g1), 0); |
| 382 | 57x |
arma::vec diff = (m0 - m1).t(); |
| 383 | 57x |
double dlen = arma::norm(diff); |
| 384 | 114x |
if (dlen > 1e-10) x1 = diff / dlen; |
| 385 |
} |
|
| 386 |
} |
|
| 387 | ||
| 388 |
// ── Step 4: Y axis ──────────────────────────────────────────────────── |
|
| 389 |
// GMR on defA (deflated by x_vector only), or SVD of defA. |
|
| 390 | 66x |
arma::vec y_vector; |
| 391 | 66x |
std::string y_name = "SVD2"; |
| 392 | ||
| 393 | 66x |
if (params.has_y) {
|
| 394 | 2x |
GMRStepResult y_gmr = gmr_step( |
| 395 |
defA, |
|
| 396 | 2x |
params.y_model_matrix, |
| 397 | 2x |
params.y_target, |
| 398 | 2x |
params.y1_cols, |
| 399 | 2x |
params.y_categorical, |
| 400 | 2x |
params.y_n_groups, |
| 401 |
{}, // no row subset for y axis
|
|
| 402 | 2x |
params.n_lambda, |
| 403 | 2x |
params.k_folds, |
| 404 | 2x |
params.lasso_eps); |
| 405 | 2x |
y_vector = y_gmr.direction; |
| 406 | 2x |
y_name = "GMR2"; |
| 407 |
} else {
|
|
| 408 | 64x |
RotationResult svd_r = ena_svd(defA); |
| 409 | 64x |
y_vector = svd_r.rotation.col(0); |
| 410 |
} |
|
| 411 | ||
| 412 |
// ── Step 5: Orthogonalise y_vector ──────────────────────────────────── |
|
| 413 |
// Mirrors R (authoritative commit): |
|
| 414 |
// y_vector <- y_vector - (t(y_vector) %*% x_vector) * x_vector |
|
| 415 |
// if (!is.null(x1)) y_vector <- y_vector - (t(y_vector) %*% x1) * x1 |
|
| 416 |
// y_vector <- safe_normalize(y_vector) |
|
| 417 | 132x |
y_vector -= arma::dot(y_vector, x_vector) * x_vector; |
| 418 | 66x |
if (!x1.is_empty()) |
| 419 | 114x |
y_vector -= arma::dot(y_vector, x1) * x1; |
| 420 | ||
| 421 | 66x |
double yn = arma::norm(y_vector); |
| 422 | 66x |
if (yn < 1e-12) |
| 423 |
throw std::runtime_error( |
|
| 424 | ! |
"generalized_means_rotation: y_vector has zero norm after orthogonalization"); |
| 425 | 66x |
y_vector /= yn; |
| 426 | ||
| 427 |
// ── Step 6: Combine and complete with SVD ───────────────────────────── |
|
| 428 |
// Double-deflate the original data (mirrors R): |
|
| 429 |
// defA <- A - A %*% x %*% t(x) - A %*% y %*% t(y) |
|
| 430 |
// then pass to prcomp (equivalent: SVD of defA_final). |
|
| 431 |
arma::mat defA_final = V |
|
| 432 | 66x |
- V * x_vector * x_vector.t() |
| 433 | 132x |
- V * y_vector * y_vector.t(); |
| 434 | ||
| 435 | 66x |
arma::mat named_axes = arma::join_rows(x_vector, y_vector); |
| 436 | 330x |
return complete_rotation(defA_final, named_axes, {"GMR1", y_name});
|
| 437 |
} |
|
| 438 | ||
| 439 |
/// @} |
|
| 440 | ||
| 441 |
} // namespace qe |
|
| 442 |
#endif // LIBQE_GENERALIZED_ROTATION_HPP |
| 1 |
/** |
|
| 2 |
* @file positions.hpp |
|
| 3 |
* @brief ENA node-position solvers for undirected and directed networks, the |
|
| 4 |
* points-to-centroids correlation (ena_correlation), and the post-fit |
|
| 5 |
* unit-circle node move with its centroid recomputation. |
|
| 6 |
* |
|
| 7 |
* Split out of the former libqe/modeling.hpp. |
|
| 8 |
*/ |
|
| 9 |
#ifndef LIBENA_POSITIONS_HPP |
|
| 10 |
#define LIBENA_POSITIONS_HPP |
|
| 11 | ||
| 12 |
#include <armadillo> |
|
| 13 |
#include <algorithm> |
|
| 14 |
#include <cmath> |
|
| 15 |
#include <stdexcept> |
|
| 16 |
#include <string> |
|
| 17 |
#include <vector> |
|
| 18 |
#include <libqe/adjacency.hpp> // qe::connection_indices (keys) |
|
| 19 |
#include <libqe/linalg_fallback.hpp> // qe::linalg::solve_spd |
|
| 20 |
#include <libqe/stats.hpp> // normal_quantile |
|
| 21 | ||
| 22 |
namespace qe {
|
|
| 23 | ||
| 24 |
// --------------------------------------------------------------------------- |
|
| 25 |
/// @name Return types |
|
| 26 |
/// @{
|
|
| 27 |
// --------------------------------------------------------------------------- |
|
| 28 | ||
| 29 |
/** |
|
| 30 |
* @brief Aggregated result returned by every node-position solver. |
|
| 31 |
* |
|
| 32 |
* All matrices share the same dimensional conventions: |
|
| 33 |
* rows index units (or nodes), columns index rotated dimensions. |
|
| 34 |
*/ |
|
| 35 |
struct NodePositions {
|
|
| 36 |
arma::mat nodes; ///< Solved node locations, n_codes × n_dims. |
|
| 37 |
arma::mat centroids; ///< Per-unit centroids projected onto the node space, n_units × n_dims. |
|
| 38 |
arma::mat weights; ///< Normalised half-edge weight per unit per node, n_units × n_codes. |
|
| 39 |
arma::mat points; ///< Input rotated points echoed back unchanged, n_units × n_dims. |
|
| 40 |
}; |
|
| 41 | ||
| 42 |
/** |
|
| 43 |
* @brief Result of @ref move_nodes_to_unit_circle. |
|
| 44 |
* |
|
| 45 |
* Unit points are not part of it: the move never changes them. |
|
| 46 |
*/ |
|
| 47 |
struct MovedNodes {
|
|
| 48 |
arma::mat nodes; ///< Moved node locations, n_codes × n_dims. |
|
| 49 |
arma::mat centroids; ///< Centroids recomputed from the moved nodes, n_units × n_dims. |
|
| 50 |
}; |
|
| 51 | ||
| 52 |
/// @} |
|
| 53 | ||
| 54 |
// --------------------------------------------------------------------------- |
|
| 55 |
/// @name Correlation |
|
| 56 |
/// @{
|
|
| 57 |
// --------------------------------------------------------------------------- |
|
| 58 | ||
| 59 |
/** |
|
| 60 |
* @brief Pearson correlation with Fisher-z confidence interval between ENA |
|
| 61 |
* unit points and their centroids. |
|
| 62 |
* |
|
| 63 |
* All unique pairs of units are formed; for each pair the per-dimension |
|
| 64 |
* difference vectors are computed, then Pearson r is obtained between the |
|
| 65 |
* @p points differences and the @p centroids differences. The confidence |
|
| 66 |
* interval is derived via Fisher's z-transform using the pure-C++ |
|
| 67 |
* `normal_quantile` function so the function works outside R without |
|
| 68 |
* `Rcpp::qnorm`. |
|
| 69 |
* |
|
| 70 |
* @param[in] points Rotated ENA unit points, n_units × n_dims. |
|
| 71 |
* @param[in] centroids Corresponding centroid coordinates, n_units × n_dims. |
|
| 72 |
* @param[in] conf_level Confidence level for the interval (default 0.95). |
|
| 73 |
* @returns An n_dims × 3 matrix whose columns are [r, ci_lower, ci_upper]. |
|
| 74 |
* |
|
| 75 |
* @note Equivalent to `ena_correlation()` in rENA/ena.cpp. |
|
| 76 |
*/ |
|
| 77 | 3x |
inline arma::mat ena_correlation(arma::mat points, arma::mat centroids, |
| 78 |
double conf_level = 0.95) {
|
|
| 79 | 3x |
int n = points.n_rows; |
| 80 | 3x |
int n_pairs = (n * (n - 1)) / 2; |
| 81 | ||
| 82 | 3x |
arma::umat idx1(1, n_pairs), idx2(1, n_pairs); |
| 83 | 3x |
int col = 0; |
| 84 | 43x |
for (int i = 0; i < n; i++) |
| 85 | 600x |
for (int j = i + 1; j < n; j++) { idx1[col] = i; idx2[col] = j; col++; }
|
| 86 | ||
| 87 | 6x |
arma::mat pts_diff = points.rows(idx1.row(0)) - points.rows(idx2.row(0)); |
| 88 | 6x |
arma::mat cts_diff = centroids.rows(idx1.row(0)) - centroids.rows(idx2.row(0)); |
| 89 | 3x |
arma::mat cr = arma::cor(pts_diff, cts_diff); |
| 90 | ||
| 91 | 3x |
double qq = normal_quantile((1.0 + conf_level) / 2.0); |
| 92 | 3x |
arma::mat out(points.n_cols, 3); |
| 93 | 9x |
for (arma::uword i = 0; i < points.n_cols; i++) {
|
| 94 | 6x |
double r = cr(i, i); |
| 95 | 6x |
double z = std::atanh(r); |
| 96 | 6x |
double sigma = 1.0 / std::sqrt(static_cast<double>(n_pairs) - 3.0); |
| 97 | 6x |
out(i, 0) = r; |
| 98 | 6x |
out(i, 1) = std::tanh(z - sigma * qq); |
| 99 | 12x |
out(i, 2) = std::tanh(z + sigma * qq); |
| 100 |
} |
|
| 101 | 3x |
return out; |
| 102 |
} |
|
| 103 | ||
| 104 |
/// @} |
|
| 105 | ||
| 106 |
// --------------------------------------------------------------------------- |
|
| 107 |
/// @name Node weights |
|
| 108 |
/// @{
|
|
| 109 |
// --------------------------------------------------------------------------- |
|
| 110 | ||
| 111 |
/** |
|
| 112 |
* @brief Per-unit node weights for undirected ENA. |
|
| 113 |
* |
|
| 114 |
* Half of each line weight goes to each of its two codes; each row is then |
|
| 115 |
* L1-normalised (a row whose L1 norm is below 0.0001 is divided by 0.0001, so |
|
| 116 |
* an all-zero row stays zero). This is the weighting @ref node_positions |
|
| 117 |
* solves with, and the weighting its centroids use. |
|
| 118 |
* |
|
| 119 |
* @param[in] line_weights Upper-triangular adjacency vectors stacked |
|
| 120 |
* row-wise (column order A & B, A & C, B & C, ...), |
|
| 121 |
* n_units × choose(n_codes, 2). |
|
| 122 |
* @returns n_units × n_codes weights. |
|
| 123 |
*/ |
|
| 124 | 96x |
inline arma::mat node_weights(const arma::mat& line_weights) {
|
| 125 | 96x |
int tri_size = line_weights.n_cols; |
| 126 | 96x |
int num_nodes = static_cast<int>( |
| 127 | 96x |
std::pow(std::ceil(std::sqrt(static_cast<double>(2 * tri_size))), 2.0) |
| 128 | 96x |
) - (2 * tri_size); |
| 129 | 96x |
int row_count = line_weights.n_rows; |
| 130 | ||
| 131 | 96x |
arma::mat weights(row_count, num_nodes, arma::fill::zeros); |
| 132 | 6928x |
for (int k = 0; k < row_count; k++) {
|
| 133 | 13664x |
arma::rowvec curr = line_weights.row(k); |
| 134 | 6832x |
int z = 0; |
| 135 | 40294x |
for (int x = 0; x < num_nodes - 1; x++) {
|
| 136 | 132480x |
for (int y = 0; y <= x; y++) {
|
| 137 | 198036x |
weights(k, x + 1) += 0.5 * curr[z]; |
| 138 | 198036x |
weights(k, y) += 0.5 * curr[z]; |
| 139 | 99018x |
z++; |
| 140 |
} |
|
| 141 |
} |
|
| 142 |
} |
|
| 143 | 6928x |
for (int k = 0; k < row_count; k++) {
|
| 144 | 13664x |
double len = arma::accu(arma::abs(weights.row(k))); |
| 145 | 6832x |
if (len < 0.0001) len = 0.0001; |
| 146 | 13664x |
weights.row(k) /= len; |
| 147 |
} |
|
| 148 | 96x |
return weights; |
| 149 |
} |
|
| 150 | ||
| 151 |
/// Default @p self_weight of the directed node weights: a node's weight is |
|
| 152 |
/// the sum of its column plus the sum of its row of the unit's adjacency |
|
| 153 |
/// matrix (in + out), so a self-connection, in both, counts twice |
|
| 154 |
/// (directedENA, Zhiqiang Cai, 2021-10-29). |
|
| 155 |
constexpr double DIRECTED_SELF_WEIGHT = 2.0; |
|
| 156 | ||
| 157 |
/** |
|
| 158 |
* @brief Per-unit node weights for directed (ordered) ENA. |
|
| 159 |
* |
|
| 160 |
* A node's weight is the sum of its column plus the sum of its row of the |
|
| 161 |
* unit's adjacency matrix: every directed connection's weight goes in full to |
|
| 162 |
* both of its codes, and a self-connection (the diagonal, ground i × response |
|
| 163 |
* i) goes to its code @p self_weight times -- twice by default, as it is in |
|
| 164 |
* both the node's row and its column. Each row is then L1-normalised as in |
|
| 165 |
* @ref node_weights. This is the weighting @ref directed_node_positions |
|
| 166 |
* solves with, and the weighting its centroids use. |
|
| 167 |
* |
|
| 168 |
* @param[in] line_weights Directed adjacency vectors, n_units × (n_codes²). |
|
| 169 |
* @param[in] self_weight Multiplier for a self-connection's contribution to |
|
| 170 |
* its code: 2 (default, row + column sum) or e.g. 1 |
|
| 171 |
* (counted once). Must be finite and >= 0. |
|
| 172 |
* @returns n_units × n_codes weights. |
|
| 173 |
*/ |
|
| 174 | 28x |
inline arma::mat directed_node_weights(const arma::mat& line_weights, |
| 175 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 176 |
if (!std::isfinite(self_weight) || self_weight < 0) |
|
| 177 | ! |
throw std::invalid_argument("directed_node_weights: self_weight must be finite and >= 0");
|
| 178 |
int num_nodes = static_cast<int>( |
|
| 179 | 28x |
std::ceil(std::sqrt(static_cast<double>(line_weights.n_cols))) |
| 180 |
); |
|
| 181 | 28x |
int row_count = line_weights.n_rows; |
| 182 | ||
| 183 | 28x |
arma::mat nw(row_count, num_nodes, arma::fill::zeros); |
| 184 | 1242x |
for (int k = 0; k < row_count; k++) {
|
| 185 | 2428x |
arma::mat curr = line_weights.row(k); |
| 186 | 1214x |
int z = 0; |
| 187 | 8432x |
for (int x = 0; x < num_nodes; x++) |
| 188 | 50328x |
for (int y = 0; y < num_nodes; y++) {
|
| 189 | 43110x |
if (x != y) {
|
| 190 | 71784x |
nw(k, x) += curr[z]; |
| 191 | 107676x |
nw(k, y) += curr[z]; |
| 192 |
} else {
|
|
| 193 | 21654x |
nw(k, x) += self_weight * curr[z]; |
| 194 |
} |
|
| 195 | 43110x |
z++; |
| 196 |
} |
|
| 197 |
} |
|
| 198 | 1242x |
for (int k = 0; k < row_count; k++) {
|
| 199 | 2428x |
double len = arma::accu(arma::abs(nw.row(k))); |
| 200 |
if (len < 0.0001) len = 0.0001; |
|
| 201 | 2428x |
nw.row(k) /= len; |
| 202 |
} |
|
| 203 | 28x |
return nw; |
| 204 |
} |
|
| 205 | ||
| 206 |
/** |
|
| 207 |
* @brief Which two codes each connection column connects: the column map the |
|
| 208 |
* layout-agnostic (@c _by_key) node weighting works from. |
|
| 209 |
* |
|
| 210 |
* Column @c k connects codes @c from[k] and @c to[k] (0-based). A |
|
| 211 |
* self-connection has @c from[k] == to[k]. Direction is not used by the node |
|
| 212 |
* weights (every connection counts toward both of its codes), so any column |
|
| 213 |
* layout -- undirected, undirected with the diagonal, ordered -- is described |
|
| 214 |
* by its key alone. |
|
| 215 |
*/ |
|
| 216 |
struct ConnectionKey {
|
|
| 217 |
arma::uvec from; ///< First code of each column (0-based). |
|
| 218 |
arma::uvec to; ///< Second code of each column (0-based). |
|
| 219 |
}; |
|
| 220 | ||
| 221 |
/** |
|
| 222 |
* @brief Key of the undirected layout: the column-major upper triangle (libqe |
|
| 223 |
* @c connection_indices), with the diagonal (A & A, A & B, B & B, ...) |
|
| 224 |
* when @p diagonal. |
|
| 225 |
*/ |
|
| 226 |
inline ConnectionKey undirected_connection_key(int n_codes, bool diagonal = false) {
|
|
| 227 |
arma::umat idx = connection_indices(n_codes, -1, diagonal); |
|
| 228 |
return ConnectionKey{ arma::uvec(idx.row(0).t()), arma::uvec(idx.row(1).t()) };
|
|
| 229 |
} |
|
| 230 | ||
| 231 |
/** |
|
| 232 |
* @brief Key of the ordered (directed) layout: column @c j*n+i is ground i -> |
|
| 233 |
* response j, so @c from = i and @c to = j. |
|
| 234 |
*/ |
|
| 235 |
inline ConnectionKey directed_connection_key(int n_codes) {
|
|
| 236 |
const arma::uword n = static_cast<arma::uword>(n_codes); |
|
| 237 |
ConnectionKey key{ arma::uvec(n * n), arma::uvec(n * n) };
|
|
| 238 |
for (arma::uword j = 0; j < n; ++j) |
|
| 239 |
for (arma::uword i = 0; i < n; ++i) {
|
|
| 240 |
key.from[j * n + i] = i; |
|
| 241 |
key.to[j * n + i] = j; |
|
| 242 |
} |
|
| 243 |
return key; |
|
| 244 |
} |
|
| 245 | ||
| 246 |
/** |
|
| 247 |
* @brief Layout-agnostic node weights: every column counts toward each code it |
|
| 248 |
* connects; a self-connection (from == to) toward its code |
|
| 249 |
* @p self_weight times. Each row is then L1-normalised (as |
|
| 250 |
* @ref node_weights). |
|
| 251 |
* |
|
| 252 |
* One rule for every layout. With the undirected key it equals |
|
| 253 |
* @ref node_weights; with the directed key, @ref directed_node_weights at the |
|
| 254 |
* same @p self_weight; with the undirected-with-diagonal key, the undirected |
|
| 255 |
* weighting plus self-connections that count as they do in ordered models. |
|
| 256 |
* |
|
| 257 |
* @param[in] line_weights n_units × n_columns. |
|
| 258 |
* @param[in] key Column map, @c n_columns long (see @ref ConnectionKey). |
|
| 259 |
* @param[in] n_codes Number of codes (nodes). |
|
| 260 |
* @param[in] self_weight Self-connection multiplier; default 2 (a node's row |
|
| 261 |
* plus column sum, as @ref directed_node_weights). |
|
| 262 |
* @returns n_units × n_codes weights. |
|
| 263 |
* @throws std::invalid_argument if the key's length does not match the |
|
| 264 |
* columns, an index is out of range, or @p self_weight is negative or |
|
| 265 |
* not finite. |
|
| 266 |
*/ |
|
| 267 | 86x |
inline arma::mat node_weights_by_key(const arma::mat& line_weights, |
| 268 |
const ConnectionKey& key, int n_codes, |
|
| 269 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 270 | 86x |
if (key.from.n_elem != line_weights.n_cols || key.to.n_elem != line_weights.n_cols) |
| 271 |
throw std::invalid_argument( |
|
| 272 | 2x |
"node_weights_by_key: the key has " + std::to_string(key.from.n_elem) + |
| 273 | 3x |
" columns; line_weights has " + std::to_string(line_weights.n_cols)); |
| 274 |
if (n_codes < 1 || (key.from.n_elem && (key.from.max() >= static_cast<arma::uword>(n_codes) || |
|
| 275 | 85x |
key.to.max() >= static_cast<arma::uword>(n_codes)))) |
| 276 | ! |
throw std::invalid_argument("node_weights_by_key: key index out of range for n_codes");
|
| 277 |
if (!std::isfinite(self_weight) || self_weight < 0) |
|
| 278 | ! |
throw std::invalid_argument("node_weights_by_key: self_weight must be finite and >= 0");
|
| 279 | ||
| 280 | 85x |
arma::mat w(line_weights.n_rows, n_codes, arma::fill::zeros); |
| 281 | 1484x |
for (arma::uword k = 0; k < line_weights.n_cols; ++k) {
|
| 282 | 2798x |
const arma::uword a = key.from[k], b = key.to[k]; |
| 283 | 1399x |
if (a != b) {
|
| 284 | 1299x |
w.col(a) += line_weights.col(k); |
| 285 | 1299x |
w.col(b) += line_weights.col(k); |
| 286 |
} else {
|
|
| 287 | 100x |
w.col(a) += self_weight * line_weights.col(k); |
| 288 |
} |
|
| 289 |
} |
|
| 290 | 4174x |
for (arma::uword u = 0; u < w.n_rows; ++u) {
|
| 291 | 4089x |
double len = arma::accu(arma::abs(w.row(u))); |
| 292 | 4089x |
if (len < 0.0001) len = 0.0001; |
| 293 | 8178x |
w.row(u) /= len; |
| 294 |
} |
|
| 295 | 85x |
return w; |
| 296 |
} |
|
| 297 | ||
| 298 |
/// @} |
|
| 299 | ||
| 300 |
// --------------------------------------------------------------------------- |
|
| 301 |
/// @name Node-position solvers |
|
| 302 |
/// @{
|
|
| 303 |
// --------------------------------------------------------------------------- |
|
| 304 | ||
| 305 |
/** |
|
| 306 |
* @brief Multiobjective least-squares node positions for undirected ENA. |
|
| 307 |
* |
|
| 308 |
* Half of each adjacency-vector entry (line weight) is distributed to each of |
|
| 309 |
* its two endpoint nodes, building a per-unit weight matrix |
|
| 310 |
* (@ref node_weights). Each row is then L1-normalised. An overdetermined |
|
| 311 |
* system is solved per dimension: |
|
| 312 |
* @code |
|
| 313 |
* (W^T W) X = W^T T |
|
| 314 |
* @endcode |
|
| 315 |
* where W is the normalised weight matrix and T contains the rotated unit |
|
| 316 |
* points. |
|
| 317 |
* |
|
| 318 |
* @param[in] adj_mats Upper-triangular adjacency vectors stacked row-wise, |
|
| 319 |
* n_units × tri_size. |
|
| 320 |
* @param[in] t Rotated unit points, n_units × n_dims. |
|
| 321 |
* @param[in] num_dims Number of dimensions to solve for. |
|
| 322 |
* @returns A @ref NodePositions struct containing `nodes`, `centroids`, |
|
| 323 |
* `weights`, and `points`. |
|
| 324 |
* |
|
| 325 |
* @note Equivalent to `lws_lsq_positions()` in rENA/ena.cpp. |
|
| 326 |
*/ |
|
| 327 | 96x |
inline NodePositions node_positions(arma::mat adj_mats, arma::mat t, |
| 328 |
int num_dims) {
|
|
| 329 |
if (!adj_mats.is_finite() || !t.is_finite()) |
|
| 330 | ! |
throw std::invalid_argument("node_positions: input matrices must not contain NaN or Inf");
|
| 331 | 96x |
arma::mat weights = node_weights(adj_mats); |
| 332 | 96x |
int num_nodes = weights.n_cols; |
| 333 | ||
| 334 | 96x |
arma::mat ssX(num_dims, num_nodes, arma::fill::zeros); |
| 335 | 96x |
arma::mat ssA = weights.t() * weights; |
| 336 | 1477x |
for (int i = 0; i < num_dims; i++) {
|
| 337 | 2762x |
arma::mat ssb = weights.t() * t.col(i); |
| 338 | 4143x |
ssX.row(i) = qe::linalg::solve_spd(ssA, ssb).t(); |
| 339 |
} |
|
| 340 | ||
| 341 | 96x |
NodePositions r; |
| 342 | 96x |
r.nodes = ssX.t(); |
| 343 | 96x |
r.centroids = (ssX * weights.t()).t(); |
| 344 | 96x |
r.weights = weights; |
| 345 | 96x |
r.points = t; |
| 346 | 96x |
return r; |
| 347 |
} |
|
| 348 | ||
| 349 |
/** |
|
| 350 |
* @brief Least-squares node positions for directed (ordered) ENA. |
|
| 351 |
* |
|
| 352 |
* Each row of @p line_weights is an n_nodes × n_nodes directed weight matrix |
|
| 353 |
* stored in row-major order. The per-unit node weight for node x accumulates |
|
| 354 |
* the full row weight plus all column weights directed at x from other nodes. |
|
| 355 |
* Each row is L1-normalised before solving (@ref directed_node_weights). |
|
| 356 |
* |
|
| 357 |
* When @p combine_pairs is `false` (the standard directed case) the system: |
|
| 358 |
* @code |
|
| 359 |
* (W^T W) X = W^T P |
|
| 360 |
* @endcode |
|
| 361 |
* is solved directly with W = normalised weight matrix and P = @p points. |
|
| 362 |
* |
|
| 363 |
* When @p combine_pairs is `true`, adjacent row pairs (ground row k and |
|
| 364 |
* response row k+1) are summed before solving: |
|
| 365 |
* @code |
|
| 366 |
* W_combined[k/2] = W[k] + W[k+1] |
|
| 367 |
* P_combined[k/2] = P[k] + P[k+1] |
|
| 368 |
* @endcode |
|
| 369 |
* The system is then solved on the combined matrices, but centroids are |
|
| 370 |
* projected back using the original (un-combined) weight matrix so that |
|
| 371 |
* every unit retains its own centroid. |
|
| 372 |
* |
|
| 373 |
* @param[in] line_weights Directed adjacency vectors, n_units × (n_nodes²). |
|
| 374 |
* @param[in] points Rotated unit points, n_units × n_dims. |
|
| 375 |
* @param[in] num_dims Number of dimensions to solve for. |
|
| 376 |
* @param[in] combine_pairs If `true`, sum adjacent row pairs before solving |
|
| 377 |
* (ground + response model). Default `false`. |
|
| 378 |
* @param[in] self_weight Self-connection multiplier (see |
|
| 379 |
* @ref directed_node_weights). Default 2. |
|
| 380 |
* @returns A @ref NodePositions struct containing `nodes`, `centroids`, |
|
| 381 |
* `weights`, and `points`. |
|
| 382 |
* |
|
| 383 |
* @note Equivalent to `directed_node_positions()` in rENA/ena.cpp when |
|
| 384 |
* @p combine_pairs is `false`. |
|
| 385 |
* @note Equivalent to `directed_node_positions_with_ground_response_added()` |
|
| 386 |
* in rENA/ena.cpp when @p combine_pairs is `true`. |
|
| 387 |
* |
|
| 388 |
* @warning When @p combine_pairs is `true`, @p line_weights must have an even |
|
| 389 |
* number of rows (row_count must be even); odd row counts result in |
|
| 390 |
* an out-of-bounds access when forming the combined matrices. |
|
| 391 |
*/ |
|
| 392 | 21x |
inline NodePositions directed_node_positions(arma::mat line_weights, |
| 393 |
arma::mat points, int num_dims, |
|
| 394 |
bool combine_pairs = false, |
|
| 395 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 396 |
if (!line_weights.is_finite() || !points.is_finite()) |
|
| 397 | ! |
throw std::invalid_argument("directed_node_positions: input matrices must not contain NaN or Inf");
|
| 398 | 21x |
arma::mat nw = directed_node_weights(line_weights, self_weight); |
| 399 | 21x |
int num_nodes = nw.n_cols; |
| 400 | 21x |
int row_count = line_weights.n_rows; |
| 401 | ||
| 402 | 21x |
if (combine_pairs) {
|
| 403 |
// Combine paired rows (ground at k, response at k+1) |
|
| 404 | 1x |
arma::mat nw_added(row_count / 2, num_nodes, arma::fill::zeros); |
| 405 | 1x |
arma::mat pts_added(row_count / 2, num_dims, arma::fill::zeros); |
| 406 | 4x |
for (int k = 0; k < row_count; k += 2) {
|
| 407 | 12x |
nw_added.row(k / 2) = nw.row(k) + nw.row(k + 1); |
| 408 | 12x |
pts_added.row(k / 2) = points.row(k) + points.row(k + 1); |
| 409 |
} |
|
| 410 | ||
| 411 | 1x |
arma::mat ssX(num_dims, num_nodes, arma::fill::zeros); |
| 412 | 1x |
arma::mat ssA = nw_added.t() * nw_added; |
| 413 | 3x |
for (int i = 0; i < num_dims; i++) {
|
| 414 | 4x |
arma::mat ssb = nw_added.t() * pts_added.col(i); |
| 415 | 6x |
ssX.row(i) = qe::linalg::solve_spd(ssA, ssb).t(); |
| 416 |
} |
|
| 417 | ||
| 418 | 1x |
NodePositions r; |
| 419 | 1x |
r.nodes = ssX.t(); |
| 420 | 1x |
r.centroids = (ssX * nw.t()).t(); |
| 421 | 1x |
r.weights = nw; |
| 422 | 1x |
r.points = points; |
| 423 | 1x |
return r; |
| 424 |
} |
|
| 425 | ||
| 426 | 20x |
arma::mat ssX(num_dims, num_nodes, arma::fill::zeros); |
| 427 | 20x |
arma::mat ssA = nw.t() * nw; |
| 428 | 625x |
for (int i = 0; i < num_dims; i++) {
|
| 429 | 1210x |
arma::mat ssb = nw.t() * points.col(i); |
| 430 | 1815x |
ssX.row(i) = qe::linalg::solve_spd(ssA, ssb).t(); |
| 431 |
} |
|
| 432 | ||
| 433 | 20x |
NodePositions r; |
| 434 | 20x |
r.nodes = ssX.t(); |
| 435 | 20x |
r.centroids = (ssX * nw.t()).t(); |
| 436 | 20x |
r.weights = nw; |
| 437 | 20x |
r.points = points; |
| 438 | 20x |
return r; |
| 439 |
} |
|
| 440 | ||
| 441 |
/** |
|
| 442 |
* @brief Layout-agnostic least-squares node positions: the solve of |
|
| 443 |
* @ref node_positions / @ref directed_node_positions with |
|
| 444 |
* @ref node_weights_by_key. |
|
| 445 |
* |
|
| 446 |
* @param[in] line_weights n_units × n_columns, any layout. |
|
| 447 |
* @param[in] points Rotated unit points, n_units × n_dims. |
|
| 448 |
* @param[in] num_dims Number of dimensions to solve for. |
|
| 449 |
* @param[in] key Column map (see @ref ConnectionKey). |
|
| 450 |
* @param[in] n_codes Number of codes (nodes). |
|
| 451 |
* @param[in] self_weight Self-connection multiplier. Default 2. |
|
| 452 |
* @returns A @ref NodePositions struct. |
|
| 453 |
*/ |
|
| 454 | 50x |
inline NodePositions node_positions_by_key(const arma::mat& line_weights, |
| 455 |
const arma::mat& points, int num_dims, |
|
| 456 |
const ConnectionKey& key, int n_codes, |
|
| 457 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 458 |
if (!line_weights.is_finite() || !points.is_finite()) |
|
| 459 | ! |
throw std::invalid_argument("node_positions_by_key: input matrices must not contain NaN or Inf");
|
| 460 | 50x |
arma::mat w = node_weights_by_key(line_weights, key, n_codes, self_weight); |
| 461 | ||
| 462 | 49x |
arma::mat ssX(num_dims, n_codes, arma::fill::zeros); |
| 463 | 49x |
arma::mat ssA = w.t() * w; |
| 464 | 865x |
for (int i = 0; i < num_dims; i++) {
|
| 465 | 1632x |
arma::mat ssb = w.t() * points.col(i); |
| 466 | 2448x |
ssX.row(i) = qe::linalg::solve_spd(ssA, ssb).t(); |
| 467 |
} |
|
| 468 | ||
| 469 | 49x |
NodePositions r; |
| 470 | 49x |
r.nodes = ssX.t(); |
| 471 | 49x |
r.centroids = (ssX * w.t()).t(); |
| 472 | 49x |
r.weights = w; |
| 473 | 49x |
r.points = points; |
| 474 | 49x |
return r; |
| 475 |
} |
|
| 476 | ||
| 477 |
/// @} |
|
| 478 | ||
| 479 |
// --------------------------------------------------------------------------- |
|
| 480 |
/// @name Post-fit node moves |
|
| 481 |
/// @{
|
|
| 482 |
// --------------------------------------------------------------------------- |
|
| 483 | ||
| 484 |
/** |
|
| 485 |
* @brief How @ref unit_circle_nodes places the nodes on the circle. |
|
| 486 |
*/ |
|
| 487 |
enum class UnitCircleMode {
|
|
| 488 |
Equal, ///< Equally spaced, 2π/n apart, the longest node fixed. |
|
| 489 |
Radial ///< Each node pushed out along its own ray. |
|
| 490 |
}; |
|
| 491 | ||
| 492 |
/** |
|
| 493 |
* @brief Parse a mode name for the bindings: "equal" or "radial". |
|
| 494 |
* |
|
| 495 |
* @throws std::invalid_argument for any other name. |
|
| 496 |
*/ |
|
| 497 | 46x |
inline UnitCircleMode unit_circle_mode(const std::string& name) {
|
| 498 | 46x |
if (name == "equal") return UnitCircleMode::Equal; |
| 499 | 16x |
if (name == "radial") return UnitCircleMode::Radial; |
| 500 |
throw std::invalid_argument( |
|
| 501 | 1x |
"unit_circle_mode: mode must be \"equal\" or \"radial\", got \"" + name + "\""); |
| 502 |
} |
|
| 503 | ||
| 504 |
/** |
|
| 505 |
* @brief Move nodes onto a circle in the plane of two dimensions. |
|
| 506 |
* |
|
| 507 |
* The circle's radius is the length of the longest node in that plane. |
|
| 508 |
* Nodes of length zero there are left where they are, and every other |
|
| 509 |
* dimension is unchanged. |
|
| 510 |
* |
|
| 511 |
* - @ref UnitCircleMode::Radial: each node moves out along its own ray. |
|
| 512 |
* - @ref UnitCircleMode::Equal: the nodes keep their order around the |
|
| 513 |
* origin and are spaced 2π/n apart, counter-clockwise, starting from the |
|
| 514 |
* longest node, which stays where it is (the first one when lengths tie). |
|
| 515 |
* The order is that of rENA's original R implementation: nodes with |
|
| 516 |
* y >= 0 by decreasing x, then nodes with y < 0 by increasing x, ties in |
|
| 517 |
* their original order. |
|
| 518 |
* |
|
| 519 |
* A post-fit transform for display; it does not refit anything. Use |
|
| 520 |
* @ref node_centroids to recompute the centroids for the moved nodes. |
|
| 521 |
* |
|
| 522 |
* @param[in] nodes Node positions, n_codes × n_dims. |
|
| 523 |
* @param[in] dim_x 0-based column of the plane's x axis. |
|
| 524 |
* @param[in] dim_y 0-based column of the plane's y axis. |
|
| 525 |
* @param[in] mode Equal (default) or Radial. |
|
| 526 |
* @returns A copy of @p nodes with columns @p dim_x and @p dim_y moved. |
|
| 527 |
* |
|
| 528 |
* @throws std::invalid_argument if a dimension is out of range, the two are |
|
| 529 |
* the same, or @p nodes contains NaN or Inf. |
|
| 530 |
* |
|
| 531 |
* @note Equivalent to rENA's move_nodes_to_unit_circle() (Radial) and |
|
| 532 |
* move_nodes_to_unit_circle_with_equal_space() (Equal), up to their |
|
| 533 |
* centroid step. |
|
| 534 |
*/ |
|
| 535 | 45x |
inline arma::mat unit_circle_nodes(const arma::mat& nodes, arma::uword dim_x, |
| 536 |
arma::uword dim_y, |
|
| 537 |
UnitCircleMode mode = UnitCircleMode::Equal) {
|
|
| 538 | 45x |
if (dim_x >= nodes.n_cols || dim_y >= nodes.n_cols) |
| 539 |
throw std::invalid_argument( |
|
| 540 | 1x |
"unit_circle_nodes: dimension index out of range (nodes have " + |
| 541 | 2x |
std::to_string(nodes.n_cols) + " columns)"); |
| 542 | 44x |
if (dim_x == dim_y) |
| 543 | 1x |
throw std::invalid_argument("unit_circle_nodes: the two dimensions must differ");
|
| 544 | 43x |
if (!nodes.is_finite()) |
| 545 | 1x |
throw std::invalid_argument("unit_circle_nodes: nodes must not contain NaN or Inf");
|
| 546 | ||
| 547 | 42x |
arma::mat out = nodes; |
| 548 | 42x |
const arma::uword n = nodes.n_rows; |
| 549 | 42x |
arma::vec x = nodes.col(dim_x); |
| 550 | 42x |
arma::vec y = nodes.col(dim_y); |
| 551 | ||
| 552 | 42x |
std::vector<double> len(n); |
| 553 | 42x |
std::vector<arma::uword> non_zero; |
| 554 | 42x |
double max_len = 0.0; |
| 555 | 294x |
for (arma::uword k = 0; k < n; ++k) {
|
| 556 | 1260x |
len[k] = std::sqrt(x[k] * x[k] + y[k] * y[k]); |
| 557 | 252x |
if (len[k] > max_len) max_len = len[k]; |
| 558 | 252x |
if (len[k] != 0.0) non_zero.push_back(k); |
| 559 |
} |
|
| 560 | 42x |
if (non_zero.empty()) return out; |
| 561 | ||
| 562 | 40x |
if (mode == UnitCircleMode::Radial) {
|
| 563 | 104x |
for (arma::uword k : non_zero) {
|
| 564 | 76x |
double scale = max_len / len[k]; |
| 565 | 152x |
out(k, dim_x) = x[k] * scale; |
| 566 | 152x |
out(k, dim_y) = y[k] * scale; |
| 567 |
} |
|
| 568 | 14x |
return out; |
| 569 |
} |
|
| 570 | ||
| 571 |
// Order around the origin: upper half-plane (y >= 0) by decreasing x, |
|
| 572 |
// then lower half-plane by increasing x; stable, as R's order(). |
|
| 573 |
// Deliberate: this is the old R code's order, kept for parity (the |
|
| 574 |
// user's decision, 2026-10-16), not true angular order. The two differ |
|
| 575 |
// only when nodes of different lengths cross between x order and angle. |
|
| 576 | 26x |
std::vector<arma::uword> upper, lower; |
| 577 | 496x |
for (arma::uword k : non_zero) (y[k] >= 0.0 ? upper : lower).push_back(k); |
| 578 | 26x |
std::stable_sort(upper.begin(), upper.end(), |
| 579 | 297x |
[&](arma::uword a, arma::uword b) { return x[a] > x[b]; });
|
| 580 | 26x |
std::stable_sort(lower.begin(), lower.end(), |
| 581 | 183x |
[&](arma::uword a, arma::uword b) { return x[a] < x[b]; });
|
| 582 | 26x |
std::vector<arma::uword> sorted(upper); |
| 583 | 52x |
sorted.insert(sorted.end(), lower.begin(), lower.end()); |
| 584 | ||
| 585 |
// The longest node (first in node order on a tie) stays put; the others |
|
| 586 |
// follow it around the circle. |
|
| 587 | 26x |
arma::uword fixed = non_zero.front(); |
| 588 | 127x |
for (arma::uword k : non_zero) |
| 589 | 101x |
if (len[k] == max_len) { fixed = k; break; }
|
| 590 | 26x |
const std::size_t m = sorted.size(); |
| 591 |
const std::size_t first = |
|
| 592 | 26x |
std::find(sorted.begin(), sorted.end(), fixed) - sorted.begin(); |
| 593 | ||
| 594 | 52x |
const double x1 = x[fixed], y1 = y[fixed]; |
| 595 | 26x |
const double step = 2.0 * arma::datum::pi / static_cast<double>(m); |
| 596 | 174x |
for (std::size_t i = 0; i < m; ++i) {
|
| 597 | 148x |
const arma::uword k = sorted[(first + i) % m]; |
| 598 | 148x |
const double angle = static_cast<double>(i) * step; |
| 599 | 148x |
out(k, dim_x) = x1 * std::cos(angle) - y1 * std::sin(angle); |
| 600 | 296x |
out(k, dim_y) = x1 * std::sin(angle) + y1 * std::cos(angle); |
| 601 |
} |
|
| 602 | 26x |
return out; |
| 603 |
} |
|
| 604 | ||
| 605 |
/** |
|
| 606 |
* @brief Unit centroids for a set of node positions. |
|
| 607 |
* |
|
| 608 |
* The model's node weights (@ref node_weights, or @ref directed_node_weights |
|
| 609 |
* when @p directed) times @p nodes: the centroids @ref node_positions and |
|
| 610 |
* @ref directed_node_positions return, for any nodes. Given a fit's own |
|
| 611 |
* nodes it reproduces the fit's centroids; given moved nodes, it gives the |
|
| 612 |
* centroids that match them. |
|
| 613 |
* |
|
| 614 |
* Line weights rather than raw connection counts: the two give the same |
|
| 615 |
* weights for the default (binary) weight model, since each unit's line |
|
| 616 |
* weights are its counts scaled, and the line weights are what the fit used |
|
| 617 |
* for any weight model. |
|
| 618 |
* |
|
| 619 |
* @param[in] line_weights The model's line weights, n_units × |
|
| 620 |
* choose(n_codes, 2), or n_units × n_codes² when |
|
| 621 |
* @p directed. |
|
| 622 |
* @param[in] nodes Node positions, n_codes × n_dims. |
|
| 623 |
* @param[in] directed Ordered (directed) networks. Default `false`. |
|
| 624 |
* @param[in] self_weight Directed self-connection multiplier (see |
|
| 625 |
* @ref directed_node_weights); use the fit's. Default 2. |
|
| 626 |
* @returns n_units × n_dims centroids. |
|
| 627 |
* |
|
| 628 |
* @throws std::invalid_argument if the line weights' column count does not |
|
| 629 |
* match the number of nodes, or either input contains NaN or Inf. |
|
| 630 |
*/ |
|
| 631 | 9x |
inline arma::mat node_centroids(const arma::mat& line_weights, |
| 632 |
const arma::mat& nodes, bool directed = false, |
|
| 633 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 634 | 9x |
const arma::uword n = nodes.n_rows; |
| 635 | 9x |
if (!directed && n < 2) |
| 636 | ! |
throw std::invalid_argument("node_centroids: undirected networks need at least 2 codes");
|
| 637 | 9x |
const arma::uword expected = directed ? n * n : n * (n - 1) / 2; |
| 638 | 9x |
if (line_weights.n_cols != expected) |
| 639 |
throw std::invalid_argument( |
|
| 640 | 4x |
"node_centroids: line_weights has " + std::to_string(line_weights.n_cols) + |
| 641 | 8x |
" columns; " + std::to_string(n) + " nodes need " + std::to_string(expected) + |
| 642 | 6x |
(directed ? " (n_codes², directed)" : " (choose(n_codes, 2), undirected)")); |
| 643 |
if (!line_weights.is_finite() || !nodes.is_finite()) |
|
| 644 | ! |
throw std::invalid_argument("node_centroids: input matrices must not contain NaN or Inf");
|
| 645 | ||
| 646 | 7x |
const arma::mat weights = directed ? directed_node_weights(line_weights, self_weight) |
| 647 |
: node_weights(line_weights); |
|
| 648 | 14x |
return weights * nodes; |
| 649 |
} |
|
| 650 | ||
| 651 |
/** |
|
| 652 |
* @brief The post-fit unit-circle move of a model: moved nodes and their |
|
| 653 |
* centroids. |
|
| 654 |
* |
|
| 655 |
* @ref unit_circle_nodes on the model's nodes, then @ref node_centroids for |
|
| 656 |
* the moved nodes. Unit points are never changed, so a model keeps its |
|
| 657 |
* points and takes the returned nodes and centroids. |
|
| 658 |
* |
|
| 659 |
* @param[in] line_weights The model's line weights (see @ref node_centroids). |
|
| 660 |
* @param[in] nodes The model's node positions, n_codes × n_dims. |
|
| 661 |
* @param[in] dim_x 0-based column of the plane's x axis. |
|
| 662 |
* @param[in] dim_y 0-based column of the plane's y axis. |
|
| 663 |
* @param[in] mode Equal (default) or Radial. |
|
| 664 |
* @param[in] directed Ordered (directed) networks. Default `false`. |
|
| 665 |
* @param[in] self_weight Directed self-connection multiplier, the fit's. Default 2. |
|
| 666 |
* @returns A @ref MovedNodes with the moved nodes and recomputed centroids. |
|
| 667 |
* |
|
| 668 |
* @throws std::invalid_argument as @ref unit_circle_nodes and |
|
| 669 |
* @ref node_centroids. |
|
| 670 |
*/ |
|
| 671 | 11x |
inline MovedNodes move_nodes_to_unit_circle(const arma::mat& line_weights, |
| 672 |
const arma::mat& nodes, |
|
| 673 |
arma::uword dim_x, arma::uword dim_y, |
|
| 674 |
UnitCircleMode mode = UnitCircleMode::Equal, |
|
| 675 |
bool directed = false, |
|
| 676 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 677 | 11x |
MovedNodes r; |
| 678 | 11x |
r.nodes = unit_circle_nodes(nodes, dim_x, dim_y, mode); |
| 679 | 9x |
r.centroids = node_centroids(line_weights, r.nodes, directed, self_weight); |
| 680 | 7x |
return r; |
| 681 |
} |
|
| 682 | ||
| 683 |
/** |
|
| 684 |
* @brief Layout-agnostic unit centroids: @ref node_weights_by_key times |
|
| 685 |
* @p nodes (as @ref node_centroids). |
|
| 686 |
*/ |
|
| 687 | 34x |
inline arma::mat node_centroids_by_key(const arma::mat& line_weights, const arma::mat& nodes, |
| 688 |
const ConnectionKey& key, |
|
| 689 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 690 |
if (!line_weights.is_finite() || !nodes.is_finite()) |
|
| 691 | ! |
throw std::invalid_argument("node_centroids_by_key: input matrices must not contain NaN or Inf");
|
| 692 | 68x |
return node_weights_by_key(line_weights, key, static_cast<int>(nodes.n_rows), self_weight) * nodes; |
| 693 |
} |
|
| 694 | ||
| 695 |
/** |
|
| 696 |
* @brief Layout-agnostic post-fit unit-circle move (as |
|
| 697 |
* @ref move_nodes_to_unit_circle), centroids from |
|
| 698 |
* @ref node_centroids_by_key. |
|
| 699 |
*/ |
|
| 700 | 34x |
inline MovedNodes move_nodes_to_unit_circle_by_key(const arma::mat& line_weights, |
| 701 |
const arma::mat& nodes, |
|
| 702 |
arma::uword dim_x, arma::uword dim_y, |
|
| 703 |
UnitCircleMode mode, |
|
| 704 |
const ConnectionKey& key, |
|
| 705 |
double self_weight = DIRECTED_SELF_WEIGHT) {
|
|
| 706 | 34x |
MovedNodes r; |
| 707 | 34x |
r.nodes = unit_circle_nodes(nodes, dim_x, dim_y, mode); |
| 708 | 33x |
r.centroids = node_centroids_by_key(line_weights, r.nodes, key, self_weight); |
| 709 | 33x |
return r; |
| 710 |
} |
|
| 711 | ||
| 712 |
/// @} |
|
| 713 | ||
| 714 |
} // namespace qe |
|
| 715 | ||
| 716 |
#endif // LIBENA_POSITIONS_HPP |
| 1 |
/** |
|
| 2 |
* @file rotation.hpp |
|
| 3 |
* @brief Rotation routines for Epistemic Network Analysis (ENA): |
|
| 4 |
* SVD, deflation, orthogonal-SVD, complete (generalized), and |
|
| 5 |
* means rotation. All routines are designed to match the |
|
| 6 |
* numerical output of the reference R implementation in rENA. |
|
| 7 |
*/ |
|
| 8 | ||
| 9 |
#ifndef LIBQE_ROTATION_HPP |
|
| 10 |
#define LIBQE_ROTATION_HPP |
|
| 11 | ||
| 12 |
#include <libqe/linalg_fallback.hpp> |
|
| 13 |
#include <armadillo> |
|
| 14 |
#include <stdexcept> |
|
| 15 |
#include <string> |
|
| 16 |
#include <utility> |
|
| 17 |
#include <vector> |
|
| 18 | ||
| 19 |
namespace qe {
|
|
| 20 | ||
| 21 |
/// @name Return types |
|
| 22 |
/// @{
|
|
| 23 | ||
| 24 |
/** |
|
| 25 |
* @brief Aggregated result returned by every rotation routine. |
|
| 26 |
* |
|
| 27 |
* The layout mirrors the output of R's `prcomp()`: `rotation` holds the |
|
| 28 |
* right-singular vectors (principal axes), `eigenvalues` holds the |
|
| 29 |
* explained-variance values (sdev^2 in R parlance), and `column_names` |
|
| 30 |
* carries the human-readable axis labels used downstream for network plots |
|
| 31 |
* and loadings tables. |
|
| 32 |
*/ |
|
| 33 |
struct RotationResult {
|
|
| 34 |
arma::mat rotation; ///< p x p orthogonal matrix; column j is rotation axis j. |
|
| 35 |
arma::vec eigenvalues; ///< Length-p vector of sdev^2 values, matching rENA's convention. |
|
| 36 |
std::vector<std::string> column_names; ///< Axis labels, e.g. "MR1", "SVD2", "GMR1". |
|
| 37 |
}; |
|
| 38 | ||
| 39 |
/** |
|
| 40 |
* @brief A pair of 0-based row-index vectors identifying two groups within a |
|
| 41 |
* points matrix. |
|
| 42 |
* |
|
| 43 |
* Used by means_rotation() to specify which rows belong to group A and which |
|
| 44 |
* belong to group B for each successive mean-difference axis. |
|
| 45 |
*/ |
|
| 46 |
struct GroupPair {
|
|
| 47 |
arma::uvec a; ///< Row indices of group A (0-based). |
|
| 48 |
arma::uvec b; ///< Row indices of group B (0-based). |
|
| 49 |
}; |
|
| 50 | ||
| 51 |
/// @} |
|
| 52 | ||
| 53 |
/// @name SVD rotation |
|
| 54 |
/// @{
|
|
| 55 | ||
| 56 |
/** |
|
| 57 |
* @brief Compute a full SVD rotation of a (pre-centered) points matrix. |
|
| 58 |
* |
|
| 59 |
* Produces the right-singular vectors V as the rotation matrix and scales |
|
| 60 |
* each singular value to an eigenvalue (sdev^2) compatible with rENA's |
|
| 61 |
* `prcomp()` output. |
|
| 62 |
* |
|
| 63 |
* @param points n x p data matrix. The caller is responsible for |
|
| 64 |
* centering upstream; this function applies no centering. |
|
| 65 |
* |
|
| 66 |
* @returns A RotationResult where: |
|
| 67 |
* - `rotation` = V (p x p full SVD; trailing columns span the null |
|
| 68 |
* space when the matrix is rank-deficient). |
|
| 69 |
* - `eigenvalues[j]` = s[j]^2 / max(1, n - 1) (== sdev^2 in R). |
|
| 70 |
* - `column_names` = {"SVD1", "SVD2", ..., "SVDp"}.
|
|
| 71 |
* |
|
| 72 |
* @note Equivalent to `prcomp(points, retx=FALSE, scale=FALSE, |
|
| 73 |
* center=FALSE, tol=0)` in rENA/R. |
|
| 74 |
* |
|
| 75 |
* @note Sign convention: none. Signs follow the underlying LAPACK SVD, |
|
| 76 |
* matching rENA's long-standing behavior. A deterministic sign rule |
|
| 77 |
* (e.g. svd_flip) may be added later as an opt-in flag. |
|
| 78 |
*/ |
|
| 79 | 162x |
inline RotationResult ena_svd(const arma::mat& points) {
|
| 80 | 162x |
if (!points.is_finite()) |
| 81 | ! |
throw std::invalid_argument("ena_svd: input matrix must not contain NaN or Inf");
|
| 82 | 162x |
arma::mat U, V; |
| 83 | 162x |
arma::vec s; |
| 84 | 162x |
qe::linalg::svd(U, s, V, points); |
| 85 | ||
| 86 | 162x |
const arma::uword p = points.n_cols; |
| 87 | ! |
const double denom = points.n_rows > 1 |
| 88 | 162x |
? static_cast<double>(points.n_rows - 1) |
| 89 |
: 1.0; |
|
| 90 | ||
| 91 | 162x |
arma::vec eigenvalues(p, arma::fill::zeros); |
| 92 | 162x |
const arma::uword k = std::min<arma::uword>(s.n_elem, p); |
| 93 | 2573x |
for (arma::uword j = 0; j < k; ++j) {
|
| 94 | 7233x |
eigenvalues(j) = (s(j) * s(j)) / denom; |
| 95 |
} |
|
| 96 | ||
| 97 | 162x |
std::vector<std::string> labels(p); |
| 98 | 2606x |
for (arma::uword j = 0; j < p; ++j) {
|
| 99 | 2444x |
labels[j] = "SVD" + std::to_string(j + 1); |
| 100 |
} |
|
| 101 |
return {V, eigenvalues, std::move(labels)};
|
|
| 102 |
} |
|
| 103 | ||
| 104 |
/// @} |
|
| 105 | ||
| 106 |
/// @name Deflation |
|
| 107 |
/// @{
|
|
| 108 | ||
| 109 |
/** |
|
| 110 |
* @brief Project a data matrix onto the hyperplane orthogonal to a given axis. |
|
| 111 |
* |
|
| 112 |
* Computes `data - (data * axis) * axis^T`, effectively removing the |
|
| 113 |
* component of every row of `data` that lies along `axis`. |
|
| 114 |
* |
|
| 115 |
* @param data n x p data matrix. |
|
| 116 |
* @param axis Unit-norm column vector of length p. The caller is |
|
| 117 |
* responsible for normalizing `axis` before calling this |
|
| 118 |
* function; no normalization is performed internally. |
|
| 119 |
* |
|
| 120 |
* @returns n x p matrix with the projection onto `axis` subtracted out. |
|
| 121 |
*/ |
|
| 122 | 28x |
inline arma::mat deflate(const arma::mat& data, const arma::vec& axis) {
|
| 123 | 56x |
return data - (data * axis) * axis.t(); |
| 124 |
} |
|
| 125 | ||
| 126 |
/// @} |
|
| 127 | ||
| 128 |
/// @name Orthogonal SVD rotation |
|
| 129 |
/// @{
|
|
| 130 | ||
| 131 |
/** |
|
| 132 |
* @brief Orthonormalize named axes via QR, then complete the rotation with |
|
| 133 |
* an SVD of the data projected onto the orthogonal complement. |
|
| 134 |
* |
|
| 135 |
* The named axes in `weights` are orthonormalized via a full QR |
|
| 136 |
* decomposition. The first k columns of the resulting Q become the |
|
| 137 |
* "fixed" part of the rotation; the remaining p - k columns define a |
|
| 138 |
* complementary subspace onto which `data` is projected, and a further SVD |
|
| 139 |
* fills those trailing rotation columns. |
|
| 140 |
* |
|
| 141 |
* @param data n x p data matrix (caller-centered if required). |
|
| 142 |
* @param weights p x k matrix of named axis directions. Column norms |
|
| 143 |
* need not be 1; QR handles normalization internally. |
|
| 144 |
* @param named_labels Length-k vector of labels for the first k rotation |
|
| 145 |
* axes. Trailing axes receive labels "SVD{k+1}" ..
|
|
| 146 |
* "SVDp". |
|
| 147 |
* |
|
| 148 |
* @returns A RotationResult containing the combined p x p rotation, |
|
| 149 |
* eigenvalues for the SVD-filled trailing axes (indices k..p-1; |
|
| 150 |
* the first k eigenvalues are zero), and axis labels. |
|
| 151 |
* |
|
| 152 |
* @note Equivalent to `orthogonal_svd()` in rENA/R/ena.rotate.by.mean.R: |
|
| 153 |
* @code |
|
| 154 |
* Q = qr.Q(qr(weights), complete = TRUE) # p x p |
|
| 155 |
* X_bar = data %*% Q[, (k+1):p] # n x (p - k) |
|
| 156 |
* V = prcomp(X_bar)$rotation |
|
| 157 |
* out = cbind(Q[, 1:k], Q[, (k+1):p] %*% V) |
|
| 158 |
* @endcode |
|
| 159 |
* |
|
| 160 |
* @note The named axes in the OUTPUT are the orthonormalized Q columns, NOT |
|
| 161 |
* the original `weights` columns. Use complete_rotation() if you need |
|
| 162 |
* to preserve input axes verbatim. |
|
| 163 |
* |
|
| 164 |
* @throws std::runtime_error if `data.n_cols != weights.n_rows` or |
|
| 165 |
* `named_labels.size() != weights.n_cols`. |
|
| 166 |
*/ |
|
| 167 | 23x |
inline RotationResult orthogonal_svd( |
| 168 |
const arma::mat& data, |
|
| 169 |
const arma::mat& weights, |
|
| 170 |
const std::vector<std::string>& named_labels) {
|
|
| 171 | 23x |
const arma::uword p = weights.n_rows; |
| 172 | 23x |
const arma::uword k = weights.n_cols; |
| 173 | 23x |
if (data.n_cols != p) {
|
| 174 |
throw std::runtime_error( |
|
| 175 | 1x |
"orthogonal_svd: data.n_cols must equal weights.n_rows"); |
| 176 |
} |
|
| 177 | 22x |
if (named_labels.size() != k) {
|
| 178 |
throw std::runtime_error( |
|
| 179 | 1x |
"orthogonal_svd: named_labels.size() must equal weights.n_cols"); |
| 180 |
} |
|
| 181 | 21x |
if (k == 0) {
|
| 182 | ! |
return ena_svd(data); |
| 183 |
} |
|
| 184 | ||
| 185 | 21x |
arma::mat Q, R; |
| 186 | 21x |
qe::linalg::qr_full(Q, R, weights); // Q is p x p |
| 187 | ||
| 188 | 21x |
arma::mat rotation(p, p); |
| 189 | 21x |
arma::vec eigenvalues(p, arma::fill::zeros); |
| 190 | 63x |
rotation.cols(0, k - 1) = Q.cols(0, k - 1); |
| 191 | ||
| 192 | 21x |
if (k < p) {
|
| 193 | 42x |
arma::mat Q_comp = Q.cols(k, p - 1); // p x (p - k) |
| 194 | 21x |
arma::mat X_bar = data * Q_comp; // n x (p - k) |
| 195 | 21x |
RotationResult inner = ena_svd(X_bar); |
| 196 | 42x |
rotation.cols(k, p - 1) = Q_comp * inner.rotation; |
| 197 | 217x |
for (arma::uword j = 0; j < (p - k); ++j) {
|
| 198 | 392x |
eigenvalues(k + j) = inner.eigenvalues(j); |
| 199 |
} |
|
| 200 |
} |
|
| 201 | ||
| 202 | 21x |
std::vector<std::string> labels(p); |
| 203 | 48x |
for (arma::uword j = 0; j < k; ++j) labels[j] = named_labels[j]; |
| 204 | 217x |
for (arma::uword j = k; j < p; ++j) {
|
| 205 | 196x |
labels[j] = "SVD" + std::to_string(j + 1); |
| 206 |
} |
|
| 207 |
return {rotation, eigenvalues, std::move(labels)};
|
|
| 208 |
} |
|
| 209 | ||
| 210 |
/// @} |
|
| 211 | ||
| 212 |
/// @name Complete (generalized) rotation |
|
| 213 |
/// @{
|
|
| 214 | ||
| 215 |
/** |
|
| 216 |
* @brief Keep named axes verbatim and fill remaining axes from an SVD of the |
|
| 217 |
* data after parallel deflation by all named axes. |
|
| 218 |
* |
|
| 219 |
* Unlike orthogonal_svd(), this routine preserves the caller-supplied |
|
| 220 |
* `named_axes` exactly (no QR re-orthonormalization). The complementary |
|
| 221 |
* axes are obtained by deflating `data` simultaneously by all named axes and |
|
| 222 |
* then running ena_svd() on the result. |
|
| 223 |
* |
|
| 224 |
* The deflation is **parallel**: every projection is subtracted from the |
|
| 225 |
* original `data`, not from a progressively deflated copy: |
|
| 226 |
* @code |
|
| 227 |
* defA = data - data * (named_axes * named_axes^T) |
|
| 228 |
* @endcode |
|
| 229 |
* This matches `defA <- A - A %*% v1 %*% t(v1) - A %*% v2 %*% t(v2)` in |
|
| 230 |
* rENA line-for-line. For mutually orthogonal axes the result is identical |
|
| 231 |
* to sequential deflation; for non-orthogonal axes it differs. |
|
| 232 |
* |
|
| 233 |
* @param data n x p data matrix. |
|
| 234 |
* @param named_axes p x k matrix of named rotation axes. Each column |
|
| 235 |
* must be unit-norm; orthonormality between columns is |
|
| 236 |
* NOT required. |
|
| 237 |
* @param named_labels Length-k vector of labels for the first k axes. |
|
| 238 |
* Trailing axes receive labels "SVD{k+1}" .. "SVDp".
|
|
| 239 |
* Conventional labels for generalized rotation are |
|
| 240 |
* "GMR1", "GMR2", then "SVD{k+1}" onward, but labels
|
|
| 241 |
* are entirely caller-supplied. |
|
| 242 |
* |
|
| 243 |
* @returns A RotationResult where columns 0..k-1 of `rotation` equal |
|
| 244 |
* `named_axes` verbatim and columns k..p-1 come from the SVD of |
|
| 245 |
* the deflated data. Columns from the deflated data's null space |
|
| 246 |
* (rank-deficient data) are orthogonalised against the named axes |
|
| 247 |
* and each other, so the result is orthonormal whenever the named |
|
| 248 |
* axes are. |
|
| 249 |
* |
|
| 250 |
* @note Equivalent to the tail of `ena.rotate.by.generalized` in rENA |
|
| 251 |
* (canonical version: commit 2c079126 on rENA `origin/main`). |
|
| 252 |
* |
|
| 253 |
* @throws std::runtime_error if `data.n_cols != named_axes.n_rows` or |
|
| 254 |
* `named_labels.size() != named_axes.n_cols`. |
|
| 255 |
*/ |
|
| 256 | 72x |
inline RotationResult complete_rotation( |
| 257 |
const arma::mat& data, |
|
| 258 |
const arma::mat& named_axes, |
|
| 259 |
const std::vector<std::string>& named_labels) {
|
|
| 260 | 72x |
const arma::uword p = named_axes.n_rows; |
| 261 | 72x |
const arma::uword k = named_axes.n_cols; |
| 262 | 72x |
if (data.n_cols != p) {
|
| 263 |
throw std::runtime_error( |
|
| 264 | ! |
"complete_rotation: data.n_cols must equal named_axes.n_rows"); |
| 265 |
} |
|
| 266 | 72x |
if (named_labels.size() != k) {
|
| 267 |
throw std::runtime_error( |
|
| 268 | ! |
"complete_rotation: named_labels.size() must equal named_axes.n_cols"); |
| 269 |
} |
|
| 270 | 72x |
if (k == 0) {
|
| 271 | ! |
return ena_svd(data); |
| 272 |
} |
|
| 273 | ||
| 274 | 144x |
arma::mat defA = data - data * named_axes * named_axes.t(); |
| 275 | 72x |
RotationResult inner = ena_svd(defA); |
| 276 | ||
| 277 | 72x |
arma::mat rotation(p, p); |
| 278 | 72x |
arma::vec eigenvalues(p, arma::fill::zeros); |
| 279 | 144x |
rotation.cols(0, k - 1) = named_axes; |
| 280 | 72x |
if (k < p) {
|
| 281 |
// The SVD columns with non-zero singular values are taken as they are |
|
| 282 |
// (rENA's pattern; with orthonormal named axes they lie in defA's row |
|
| 283 |
// space, orthogonal to the axes). When the data are rank-deficient -- |
|
| 284 |
// e.g. an all-zero connection column from a code mask, or fewer units |
|
| 285 |
// than connections -- the trailing columns come from defA's null |
|
| 286 |
// space, which contains the named axes themselves, so the SVD can |
|
| 287 |
// return one that nearly duplicates a named axis (|cos| ~ 0.99): the |
|
| 288 |
// rotation is no longer orthonormal and that axis's variance is |
|
| 289 |
// counted twice. Null-space columns are therefore orthogonalised |
|
| 290 |
// against the named axes and the axes already chosen (Gram-Schmidt), |
|
| 291 |
// skipping any with nothing left. Full-rank data are unaffected. |
|
| 292 | 72x |
arma::mat basis(p, p, arma::fill::zeros); // orthonormal span so far |
| 293 | 72x |
arma::uword nb = 0; |
| 294 | 1135x |
auto add_to_basis = [&](arma::vec v) {
|
| 295 | 3405x |
for (int pass = 0; pass < 2; ++pass) |
| 296 | 6522x |
if (nb > 0) v -= basis.cols(0, nb - 1) * (basis.cols(0, nb - 1).t() * v); |
| 297 | 1135x |
const double len = arma::norm(v); |
| 298 | 3393x |
if (len > 1e-10 && nb < p) basis.col(nb++) = v / len; |
| 299 |
}; |
|
| 300 | 213x |
for (arma::uword j = 0; j < k; ++j) add_to_basis(named_axes.col(j)); |
| 301 | ||
| 302 |
// A singular value below 1e-7 of the largest is numerically zero. |
|
| 303 |
const double max_eig = inner.eigenvalues.is_empty() ? 0.0 : inner.eigenvalues.max(); |
|
| 304 | 72x |
const double null_eig = 1e-14 * max_eig; |
| 305 | 72x |
arma::uword filled = 0; |
| 306 | 996x |
auto take = [&](arma::vec v, double eigenvalue, bool in_null_space) {
|
| 307 |
if (filled >= p - k) return; |
|
| 308 | 996x |
if (in_null_space) {
|
| 309 | 123x |
for (int pass = 0; pass < 2; ++pass) |
| 310 | 246x |
if (nb > 0) v -= basis.cols(0, nb - 1) * (basis.cols(0, nb - 1).t() * v); |
| 311 | 41x |
const double len = arma::norm(v); |
| 312 | 41x |
if (len < 1e-8) return; // nothing left: skip the candidate |
| 313 | 39x |
v /= len; |
| 314 |
} |
|
| 315 | 1988x |
rotation.col(k + filled) = v; |
| 316 | 994x |
eigenvalues(k + filled) = eigenvalue; |
| 317 | 994x |
++filled; |
| 318 | 994x |
add_to_basis(v); |
| 319 |
}; |
|
| 320 | 1068x |
for (arma::uword j = 0; j < inner.rotation.n_cols && filled < p - k; ++j) |
| 321 | 2988x |
take(inner.rotation.col(j), inner.eigenvalues(j), inner.eigenvalues(j) <= null_eig); |
| 322 |
// The SVD columns span R^p, so this only runs on degenerate input. |
|
| 323 |
for (arma::uword i = 0; i < p && filled < p - k; ++i) {
|
|
| 324 | ! |
arma::vec e(p, arma::fill::zeros); |
| 325 | ! |
e(i) = 1.0; |
| 326 | ! |
take(e, 0.0, true); |
| 327 |
} |
|
| 328 |
} |
|
| 329 | ||
| 330 | 72x |
std::vector<std::string> labels(p); |
| 331 | 213x |
for (arma::uword j = 0; j < k; ++j) labels[j] = named_labels[j]; |
| 332 | 1066x |
for (arma::uword j = k; j < p; ++j) {
|
| 333 | 994x |
labels[j] = "SVD" + std::to_string(j + 1); |
| 334 |
} |
|
| 335 |
return {rotation, eigenvalues, std::move(labels)};
|
|
| 336 |
} |
|
| 337 | ||
| 338 |
/// @} |
|
| 339 | ||
| 340 |
/// @name Means rotation |
|
| 341 |
/// @{
|
|
| 342 | ||
| 343 |
/** |
|
| 344 |
* @brief Compute a means rotation from one or more group pairs. |
|
| 345 |
* |
|
| 346 |
* For each GroupPair, computes the normalized mean-difference vector on the |
|
| 347 |
* progressively deflated data and stacks the resulting unit-norm axes into a |
|
| 348 |
* weights matrix. The final rotation is completed by orthogonal_svd(). |
|
| 349 |
* The input `points` is mean-centered before any axes are computed, matching |
|
| 350 |
* rENA's `scale(data, scale=FALSE, center=TRUE)`. |
|
| 351 |
* |
|
| 352 |
* @param points n x p matrix of network positions (raw, not pre-centered). |
|
| 353 |
* @param pairs Ordered list of GroupPair objects. Each pair contributes |
|
| 354 |
* one "MR" axis in sequence. Must be non-empty. |
|
| 355 |
* |
|
| 356 |
* @returns A RotationResult with `column_names` = {"MR1", ..., "MRm",
|
|
| 357 |
* "SVD{m+1}", ..., "SVDp"} where m = pairs.size().
|
|
| 358 |
* |
|
| 359 |
* @note Equivalent to `ena.rotate.by.mean()` in rENA/R/ena.rotate.by.mean.R. |
|
| 360 |
* The progressive deflation (each axis is computed on the data after |
|
| 361 |
* removing all previous axes) is the key numerical step. |
|
| 362 |
* |
|
| 363 |
* @warning There is NO guard against a zero-norm mean-difference vector. |
|
| 364 |
* If the two group means are identical on the current deflated data, |
|
| 365 |
* the axis will contain NaN values (division by zero). This mirrors |
|
| 366 |
* a latent bug in rENA and is preserved verbatim for numerical |
|
| 367 |
* fidelity; a safe opt-in guard will be added in a future release. |
|
| 368 |
* |
|
| 369 |
* @throws std::runtime_error if `pairs` is empty. |
|
| 370 |
*/ |
|
| 371 | 20x |
inline RotationResult means_rotation(const arma::mat& points, |
| 372 |
const std::vector<GroupPair>& pairs) {
|
|
| 373 | 20x |
if (pairs.empty()) {
|
| 374 | 1x |
throw std::runtime_error("Unable to rotate without 2 groups.");
|
| 375 |
} |
|
| 376 | ||
| 377 | 38x |
arma::mat data = points.each_row() - arma::mean(points, 0); |
| 378 | 19x |
arma::mat deflated = data; |
| 379 | 19x |
arma::mat weights(data.n_cols, pairs.size(), arma::fill::zeros); |
| 380 | 19x |
std::vector<std::string> labels; |
| 381 | 19x |
labels.reserve(pairs.size()); |
| 382 | ||
| 383 | 43x |
for (std::size_t i = 0; i < pairs.size(); ++i) {
|
| 384 | 48x |
const arma::rowvec mean_a = arma::mean(deflated.rows(pairs[i].a), 0); |
| 385 | 48x |
const arma::rowvec mean_b = arma::mean(deflated.rows(pairs[i].b), 0); |
| 386 | 48x |
const arma::vec diff = (mean_a - mean_b).t(); |
| 387 | 24x |
const double norm = std::sqrt(arma::accu(diff % diff)); |
| 388 | 24x |
const arma::vec axis = diff / norm; // no zero-norm guard |
| 389 | ||
| 390 | 24x |
deflated = deflate(deflated, axis); |
| 391 | 48x |
weights.col(i) = axis; |
| 392 | 24x |
labels.push_back("MR" + std::to_string(i + 1));
|
| 393 |
} |
|
| 394 | ||
| 395 | 38x |
return orthogonal_svd(deflated, weights, labels); |
| 396 |
} |
|
| 397 | ||
| 398 |
/// @} |
|
| 399 | ||
| 400 |
} // namespace qe |
|
| 401 | ||
| 402 |
#endif // LIBQE_ROTATION_HPP |
| 1 |
// [[Rcpp::depends(RcppArmadillo)]] |
|
| 2 |
// Rcpp wrappers for libena, the ENA layer of the libqe C++ libraries. The |
|
| 3 |
// headers live in inst/include/libena/ (moved from libqe); they build on |
|
| 4 |
// libqe's generic numerics, which come from LinkingTo: libqe. |
|
| 5 |
// |
|
| 6 |
// These functions are internal: rENA's R code calls them, and its exported |
|
| 7 |
// R functions are the API. ena_correlation_c and directed_node_positions_c |
|
| 8 |
// are suffixed because rENA exports R functions with the plain names. |
|
| 9 |
#include <RcppArmadillo.h> |
|
| 10 |
#include <libena/rotation.hpp> |
|
| 11 |
#include <libena/generalized_rotation.hpp> |
|
| 12 |
#include <libena/positions.hpp> |
|
| 13 |
#include <libena/ccd.hpp> |
|
| 14 | ||
| 15 |
using namespace Rcpp; |
|
| 16 | ||
| 17 |
// ============================================================================= |
|
| 18 |
// Node positions and correlation |
|
| 19 |
// ============================================================================= |
|
| 20 | ||
| 21 |
// Pearson correlation with CI between ENA points and centroids |
|
| 22 |
// @param points Numeric matrix (units x dims) |
|
| 23 |
// @param centroids Numeric matrix (units x dims) |
|
| 24 |
// @param conf_level Confidence level (default 0.95) |
|
| 25 |
// [[Rcpp::export]] |
|
| 26 | 3x |
arma::mat ena_correlation_c(arma::mat points, arma::mat centroids, |
| 27 |
double conf_level = 0.95) {
|
|
| 28 | 3x |
return qe::ena_correlation(points, centroids, conf_level); |
| 29 |
} |
|
| 30 | ||
| 31 |
// Least-squares node positions for undirected ENA |
|
| 32 |
// @param adj_mats Numeric matrix of line weights (units x connections) |
|
| 33 |
// @param t Numeric matrix of rotated points (units x dims) |
|
| 34 |
// @param num_dims Number of dimensions |
|
| 35 |
// @return List with nodes, centroids, weights, points |
|
| 36 |
// [[Rcpp::export]] |
|
| 37 | 98x |
List node_positions(arma::mat adj_mats, arma::mat t, int num_dims) {
|
| 38 | 98x |
if (!adj_mats.is_finite() || !t.is_finite()) |
| 39 | 2x |
stop("node_positions: input matrices must not contain NaN or Inf - "
|
| 40 |
"filter or impute rows with non-finite values before calling"); |
|
| 41 | 96x |
qe::NodePositions r = qe::node_positions(adj_mats, t, num_dims); |
| 42 |
return List::create( |
|
| 43 | 192x |
_("nodes") = r.nodes,
|
| 44 | 192x |
_("centroids") = r.centroids,
|
| 45 | 192x |
_("weights") = r.weights,
|
| 46 | 192x |
_("points") = r.points
|
| 47 |
); |
|
| 48 |
} |
|
| 49 | ||
| 50 |
// Least-squares node positions for directed ENA |
|
| 51 |
// @param line_weights Numeric matrix (units x connections) |
|
| 52 |
// @param points Numeric matrix of rotated points (units x dims) |
|
| 53 |
// @param num_dims Number of dimensions |
|
| 54 |
// @param self_weight Self-connection multiplier in the node weights |
|
| 55 |
// (2 = row + column sum, the default; 1 = once) |
|
| 56 |
// @return List with nodes, centroids, weights, points |
|
| 57 |
// [[Rcpp::export]] |
|
| 58 | 22x |
List directed_node_positions_c(arma::mat line_weights, arma::mat points, |
| 59 |
int num_dims, double self_weight = 2.0) {
|
|
| 60 | 22x |
if (!line_weights.is_finite() || !points.is_finite()) |
| 61 | 2x |
stop("directed_node_positions: input matrices must not contain NaN or Inf - "
|
| 62 |
"filter or impute rows with non-finite values before calling"); |
|
| 63 | 40x |
qe::NodePositions r = qe::directed_node_positions(line_weights, points, num_dims, |
| 64 | 20x |
false, self_weight); |
| 65 |
return List::create( |
|
| 66 | 40x |
_("nodes") = r.nodes,
|
| 67 | 40x |
_("centroids") = r.centroids,
|
| 68 | 40x |
_("weights") = r.weights,
|
| 69 | 40x |
_("points") = r.points
|
| 70 |
); |
|
| 71 |
} |
|
| 72 | ||
| 73 |
// Directed node positions with paired ground+response rows combined |
|
| 74 |
// @param line_weights Numeric matrix (units x connections) |
|
| 75 |
// @param points Numeric matrix of rotated points (units x dims) |
|
| 76 |
// @param num_dims Number of dimensions |
|
| 77 |
// @param self_weight Self-connection multiplier (see directed_node_positions_c) |
|
| 78 |
// @return List with nodes, centroids, weights, points |
|
| 79 |
// [[Rcpp::export]] |
|
| 80 | 1x |
List directed_node_positions_combine_pairs(arma::mat line_weights, |
| 81 |
arma::mat points, |
|
| 82 |
int num_dims, |
|
| 83 |
double self_weight = 2.0) {
|
|
| 84 |
if (!line_weights.is_finite() || !points.is_finite()) |
|
| 85 | ! |
stop("directed_node_positions_combine_pairs: input matrices must not contain NaN or Inf - "
|
| 86 |
"filter or impute rows with non-finite values before calling"); |
|
| 87 | 2x |
qe::NodePositions r = qe::directed_node_positions( |
| 88 | 1x |
line_weights, points, num_dims, true, self_weight); |
| 89 |
return List::create( |
|
| 90 | 2x |
_("nodes") = r.nodes,
|
| 91 | 2x |
_("centroids") = r.centroids,
|
| 92 | 2x |
_("weights") = r.weights,
|
| 93 | 2x |
_("points") = r.points
|
| 94 |
); |
|
| 95 |
} |
|
| 96 | ||
| 97 |
// Layout-agnostic node optimization: column k connects codes from[k] and |
|
| 98 |
// to[k] (0-based; equal for a self-connection). See libena's ConnectionKey. |
|
| 99 | 87x |
static qe::ConnectionKey key_of(const IntegerVector& from, const IntegerVector& to) {
|
| 100 |
if (from.size() != to.size()) stop("from and to must have the same length");
|
|
| 101 |
qe::ConnectionKey key{ arma::uvec(from.size()), arma::uvec(to.size()) };
|
|
| 102 | 1506x |
for (R_xlen_t k = 0; k < from.size(); ++k) {
|
| 103 |
if (from[k] < 0 || to[k] < 0) stop("key indices must be >= 0");
|
|
| 104 | 4257x |
key.from[k] = from[k]; key.to[k] = to[k]; |
| 105 |
} |
|
| 106 | 87x |
return key; |
| 107 |
} |
|
| 108 | ||
| 109 |
// [[Rcpp::export]] |
|
| 110 | 2x |
arma::mat node_weights_by_key_c(arma::mat line_weights, IntegerVector from, IntegerVector to, |
| 111 |
int n_codes, double self_weight = 2.0) {
|
|
| 112 | 2x |
return qe::node_weights_by_key(line_weights, key_of(from, to), n_codes, self_weight); |
| 113 |
} |
|
| 114 | ||
| 115 |
// [[Rcpp::export]] |
|
| 116 | 50x |
List node_positions_by_key_c(arma::mat line_weights, arma::mat points, int num_dims, |
| 117 |
IntegerVector from, IntegerVector to, int n_codes, |
|
| 118 |
double self_weight = 2.0) {
|
|
| 119 |
qe::NodePositions r = qe::node_positions_by_key( |
|
| 120 | 51x |
line_weights, points, num_dims, key_of(from, to), n_codes, self_weight); |
| 121 | 196x |
return List::create(_("nodes") = r.nodes, _("centroids") = r.centroids,
|
| 122 | 294x |
_("weights") = r.weights, _("points") = r.points);
|
| 123 |
} |
|
| 124 | ||
| 125 |
// [[Rcpp::export]] |
|
| 126 | 1x |
arma::mat node_centroids_by_key_c(arma::mat line_weights, arma::mat nodes, |
| 127 |
IntegerVector from, IntegerVector to, double self_weight = 2.0) {
|
|
| 128 | 1x |
return qe::node_centroids_by_key(line_weights, nodes, key_of(from, to), self_weight); |
| 129 |
} |
|
| 130 | ||
| 131 |
// [[Rcpp::export]] |
|
| 132 | 34x |
List move_nodes_to_unit_circle_by_key_c(arma::mat line_weights, arma::mat nodes, |
| 133 |
int dim_x, int dim_y, std::string mode, |
|
| 134 |
IntegerVector from, IntegerVector to, |
|
| 135 |
double self_weight = 2.0) {
|
|
| 136 | 34x |
if (dim_x < 0 || dim_y < 0) |
| 137 | ! |
stop("move_nodes_to_unit_circle: dimension indices must be >= 0");
|
| 138 |
qe::MovedNodes r = qe::move_nodes_to_unit_circle_by_key( |
|
| 139 | 35x |
line_weights, nodes, dim_x, dim_y, qe::unit_circle_mode(mode), key_of(from, to), self_weight); |
| 140 | 132x |
return List::create(_("nodes") = r.nodes, _("centroids") = r.centroids);
|
| 141 |
} |
|
| 142 | ||
| 143 |
// Post-fit unit-circle node move, for move_nodes_to_unit_circle() and |
|
| 144 |
// move_nodes_to_unit_circle_with_equal_space() (R/move_nodes.R) |
|
| 145 |
// @param line_weights Numeric matrix (units x connections) |
|
| 146 |
// @param nodes Numeric matrix (codes x dims) |
|
| 147 |
// @param dim_x 0-based column of the plane's x axis |
|
| 148 |
// @param dim_y 0-based column of the plane's y axis |
|
| 149 |
// @param mode "equal" or "radial" |
|
| 150 |
// @param directed Ordered (directed) networks |
|
| 151 |
// @param self_weight Directed self-connection multiplier, the fit's |
|
| 152 |
// @return List with nodes (all dims; only dim_x, dim_y moved) and centroids |
|
| 153 |
// [[Rcpp::export]] |
|
| 154 | 12x |
List move_nodes_to_unit_circle_c(arma::mat line_weights, arma::mat nodes, |
| 155 |
int dim_x, int dim_y, std::string mode, |
|
| 156 |
bool directed, double self_weight = 2.0) {
|
|
| 157 | 12x |
if (dim_x < 0 || dim_y < 0) |
| 158 | ! |
stop("move_nodes_to_unit_circle: dimension indices must be >= 0");
|
| 159 |
qe::MovedNodes r = qe::move_nodes_to_unit_circle( |
|
| 160 | 12x |
line_weights, nodes, dim_x, dim_y, qe::unit_circle_mode(mode), directed, self_weight); |
| 161 |
return List::create( |
|
| 162 | 14x |
_("nodes") = r.nodes,
|
| 163 | 14x |
_("centroids") = r.centroids
|
| 164 |
); |
|
| 165 |
} |
|
| 166 | ||
| 167 |
// ============================================================================= |
|
| 168 |
// Rotation |
|
| 169 |
// ============================================================================= |
|
| 170 | ||
| 171 |
// Pack a RotationResult into the list shape that matches rENA's ENARotationSet |
|
| 172 |
// payload (rotation + eigenvalues + column names). Column names are attached |
|
| 173 |
// to the rotation matrix as dimnames so downstream R code can index by them. |
|
| 174 |
// Eigenvalues are returned as a plain numeric vector (not an Nx1 matrix) to |
|
| 175 |
// match what rENA's `pcaResults$sdev^2` produces. |
|
| 176 | 98x |
static List pack_rotation_result(const qe::RotationResult& r) {
|
| 177 | 98x |
NumericMatrix rotation = wrap(r.rotation); |
| 178 | 98x |
CharacterVector col_names(r.column_names.begin(), r.column_names.end()); |
| 179 | 294x |
rotation.attr("dimnames") = List::create(R_NilValue, col_names);
|
| 180 | 98x |
NumericVector eigenvalues(r.eigenvalues.begin(), r.eigenvalues.end()); |
| 181 |
return List::create( |
|
| 182 | 196x |
_("rotation") = rotation,
|
| 183 | 196x |
_("eigenvalues") = eigenvalues,
|
| 184 | 196x |
_("column_names") = col_names
|
| 185 |
); |
|
| 186 |
} |
|
| 187 | ||
| 188 |
// SVD rotation (matches prcomp(retx=F, scale=F, center=F, tol=0)) |
|
| 189 |
// |
|
| 190 |
// Caller is responsible for centering upstream. Eigenvalues are stored as |
|
| 191 |
// \code{sdev^2} (variance) to match rENA's \code{ena.svd}.
|
|
| 192 |
// |
|
| 193 |
// @param points Numeric matrix (n_units x n_dims) |
|
| 194 |
// @return List with \code{rotation} (n_dims x n_dims), \code{eigenvalues}
|
|
| 195 |
// (length n_dims, = sdev^2), and \code{column_names} ("SVD1", "SVD2", ...)
|
|
| 196 |
// [[Rcpp::export]] |
|
| 197 | 7x |
List ena_svd(arma::mat points) {
|
| 198 | 7x |
if (!points.is_finite()) |
| 199 | 2x |
stop("ena_svd: input matrix must not contain NaN or Inf - "
|
| 200 |
"filter or impute rows with non-finite values before calling"); |
|
| 201 | 5x |
return pack_rotation_result(qe::ena_svd(points)); |
| 202 |
} |
|
| 203 | ||
| 204 |
// Project a matrix onto the hyperplane orthogonal to a unit-norm axis |
|
| 205 |
// |
|
| 206 |
// Computes \code{data - (data \%*\% axis) \%*\% t(axis)}. The caller is
|
|
| 207 |
// responsible for ensuring \code{axis} is unit-norm.
|
|
| 208 |
// |
|
| 209 |
// @param data Numeric matrix (n_units x n_dims) |
|
| 210 |
// @param axis Numeric vector of length n_dims, unit-norm |
|
| 211 |
// @return Numeric matrix of the same shape as \code{data}
|
|
| 212 |
// [[Rcpp::export]] |
|
| 213 | 4x |
arma::mat deflate(arma::mat data, arma::vec axis) {
|
| 214 | 4x |
return qe::deflate(data, axis); |
| 215 |
} |
|
| 216 | ||
| 217 |
// Orthogonal SVD — orthonormalize named axes via QR, fill the rest from SVD |
|
| 218 |
// |
|
| 219 |
// Mirrors rENA's \code{orthogonal_svd()} in \code{ena.rotate.by.mean.R}:
|
|
| 220 |
// the named axes in the output are the orthonormalized Q columns, not the |
|
| 221 |
// original \code{weights} columns. Use \code{complete_rotation} to keep
|
|
| 222 |
// the named axes verbatim. |
|
| 223 |
// |
|
| 224 |
// @param data Numeric matrix (n_units x n_dims) |
|
| 225 |
// @param weights Numeric matrix (n_dims x k); columns are the named axes |
|
| 226 |
// @param named_labels Character vector of length k |
|
| 227 |
// @return List with \code{rotation}, \code{eigenvalues}, \code{column_names}
|
|
| 228 |
// [[Rcpp::export]] |
|
| 229 | 4x |
List orthogonal_svd(arma::mat data, |
| 230 |
arma::mat weights, |
|
| 231 |
std::vector<std::string> named_labels) {
|
|
| 232 | 4x |
return pack_rotation_result(qe::orthogonal_svd(data, weights, named_labels)); |
| 233 |
} |
|
| 234 | ||
| 235 |
// Complete a rotation — keep named axes verbatim, fill remainder from SVD |
|
| 236 |
// |
|
| 237 |
// Mirrors the tail of \code{ena.rotate.by.generalized}: the named axes
|
|
| 238 |
// appear in the output exactly as provided, and the trailing columns come |
|
| 239 |
// from an SVD of the data deflated by all named axes. |
|
| 240 |
// On rank-deficient data (e.g. an all-zero connection column) the trailing |
|
| 241 |
// axes that come from the deflated data's null space are orthogonalised |
|
| 242 |
// against the named axes, so the rotation is orthonormal whenever the named |
|
| 243 |
// axes are. |
|
| 244 |
// |
|
| 245 |
// @param data Numeric matrix (n_units x n_dims) |
|
| 246 |
// @param named_axes Numeric matrix (n_dims x k); columns must be unit-norm |
|
| 247 |
// @param named_labels Character vector of length k |
|
| 248 |
// @return List with \code{rotation}, \code{eigenvalues}, \code{column_names}
|
|
| 249 |
// [[Rcpp::export]] |
|
| 250 | 6x |
List complete_rotation(arma::mat data, |
| 251 |
arma::mat named_axes, |
|
| 252 |
std::vector<std::string> named_labels) {
|
|
| 253 | 6x |
return pack_rotation_result(qe::complete_rotation(data, named_axes, named_labels)); |
| 254 |
} |
|
| 255 | ||
| 256 |
// Means rotation |
|
| 257 |
// |
|
| 258 |
// For each group pair, computes a normalized mean-difference axis on the |
|
| 259 |
// progressively-deflated data and finishes with \code{orthogonal_svd}.
|
|
| 260 |
// The input is column-centered first, matching rENA's |
|
| 261 |
// \code{scale(data, scale=F, center=T)} at the top of \code{ena.rotate.by.mean}.
|
|
| 262 |
// |
|
| 263 |
// Each element of \code{group_pairs} is a length-2 list \code{list(a, b)}
|
|
| 264 |
// of 0-based row indices into \code{points}.
|
|
| 265 |
// |
|
| 266 |
// @param points Numeric matrix (n_units x n_dims) |
|
| 267 |
// @param group_pairs List of length k; each element is \code{list(a, b)}
|
|
| 268 |
// where \code{a} and \code{b} are 0-based integer index vectors
|
|
| 269 |
// @return List with \code{rotation}, \code{eigenvalues}, \code{column_names}
|
|
| 270 |
// [[Rcpp::export]] |
|
| 271 | 20x |
List means_rotation(arma::mat points, List group_pairs) {
|
| 272 | 20x |
std::vector<qe::GroupPair> pairs; |
| 273 | 20x |
pairs.reserve(group_pairs.size()); |
| 274 | 44x |
for (R_xlen_t i = 0; i < group_pairs.size(); ++i) {
|
| 275 | 24x |
List pair = group_pairs[i]; |
| 276 | 24x |
if (pair.size() != 2) {
|
| 277 | ! |
stop("group_pairs[[%d]] must be a length-2 list(a, b)",
|
| 278 | ! |
static_cast<int>(i + 1)); |
| 279 |
} |
|
| 280 | 24x |
IntegerVector ra = pair[0]; |
| 281 | 24x |
IntegerVector rb = pair[1]; |
| 282 | 24x |
arma::uvec a(ra.size()); |
| 283 | 24x |
arma::uvec b(rb.size()); |
| 284 | 645x |
for (R_xlen_t j = 0; j < ra.size(); ++j) {
|
| 285 | 1242x |
a(j) = static_cast<arma::uword>(ra[j]); |
| 286 |
} |
|
| 287 | 601x |
for (R_xlen_t j = 0; j < rb.size(); ++j) {
|
| 288 | 1154x |
b(j) = static_cast<arma::uword>(rb[j]); |
| 289 |
} |
|
| 290 | 24x |
pairs.push_back({a, b});
|
| 291 |
} |
|
| 292 | 39x |
return pack_rotation_result(qe::means_rotation(points, pairs)); |
| 293 |
} |
|
| 294 | ||
| 295 |
// ============================================================================= |
|
| 296 |
// Generalized Means Rotation (GMR) |
|
| 297 |
// ============================================================================= |
|
| 298 | ||
| 299 |
// Generalized Means Rotation |
|
| 300 |
// |
|
| 301 |
// Computes a rotation axis that best represents a target variable's |
|
| 302 |
// contribution to the ENA point space, after controlling for covariates via |
|
| 303 |
// Lasso (coordinate-descent, k-fold CV). Mirrors |
|
| 304 |
// \code{rENA::ena.rotate.by.generalized()}.
|
|
| 305 |
// |
|
| 306 |
// The caller is responsible for building \code{x_model_matrix} (e.g. via
|
|
| 307 |
// \code{model.matrix()}) and identifying \code{x1_cols} (0-based indices of
|
|
| 308 |
// the target variable's columns, which receive \code{penalty_factor = 0}).
|
|
| 309 |
// Categorical targets should be encoded as 0-based integers. |
|
| 310 |
// |
|
| 311 |
// @param V Numeric matrix (n_units x n_connections) — ENA points. |
|
| 312 |
// @param x_model_matrix Numeric matrix (n_units x p) — model matrix for x axis. |
|
| 313 |
// @param x_target Numeric vector length n — target variable (raw float |
|
| 314 |
// for numeric; 0-based integer codes for categorical). |
|
| 315 |
// @param x1_cols Integer vector of 0-based column indices in |
|
| 316 |
// \code{x_model_matrix} that belong to the target variable (unpenalized).
|
|
| 317 |
// @param x_categorical Logical — TRUE if target is categorical. |
|
| 318 |
// @param x_n_groups Integer — number of distinct groups (categorical only). |
|
| 319 |
// @param x_subset Integer vector of 0-based row indices to subset for |
|
| 320 |
// the x-axis GMR step (pass \code{integer(0)} to use all rows).
|
|
| 321 |
// @param has_y Logical — TRUE to compute a second GMR axis. |
|
| 322 |
// @param y_model_matrix Numeric matrix (n_units x p) — model matrix for y axis |
|
| 323 |
// (ignored when \code{has_y = FALSE}).
|
|
| 324 |
// @param y_target Numeric vector — y-axis target (ignored when |
|
| 325 |
// \code{has_y = FALSE}).
|
|
| 326 |
// @param y1_cols 0-based column indices for y target (ignored when |
|
| 327 |
// \code{has_y = FALSE}).
|
|
| 328 |
// @param y_categorical Logical (ignored when \code{has_y = FALSE}).
|
|
| 329 |
// @param y_n_groups Integer (ignored when \code{has_y = FALSE}).
|
|
| 330 |
// @param n_lambda Length of the Lasso lambda path (default 50). |
|
| 331 |
// @param k_folds Cross-validation folds for lambda selection (default 5). |
|
| 332 |
// @param lasso_eps \code{lambda_min = lasso_eps * lambda_max} (default 0.01).
|
|
| 333 |
// @return List with \code{rotation}, \code{eigenvalues}, \code{column_names}.
|
|
| 334 |
// [[Rcpp::export]] |
|
| 335 | 66x |
List generalized_means_rotation( |
| 336 |
arma::mat V, |
|
| 337 |
arma::mat x_model_matrix, |
|
| 338 |
arma::vec x_target, |
|
| 339 |
IntegerVector x1_cols, |
|
| 340 |
bool x_categorical, |
|
| 341 |
int x_n_groups, |
|
| 342 |
IntegerVector x_subset, |
|
| 343 |
bool has_y, |
|
| 344 |
arma::mat y_model_matrix, |
|
| 345 |
arma::vec y_target, |
|
| 346 |
IntegerVector y1_cols, |
|
| 347 |
bool y_categorical, |
|
| 348 |
int y_n_groups, |
|
| 349 |
int n_lambda = 50, |
|
| 350 |
int k_folds = 5, |
|
| 351 |
double lasso_eps = 0.01 |
|
| 352 |
) {
|
|
| 353 |
// ── Unpack x1_cols ─────────────────────────────────────────────────────── |
|
| 354 | 66x |
arma::uvec x1(x1_cols.size()); |
| 355 | 144x |
for (R_xlen_t i = 0; i < x1_cols.size(); ++i) |
| 356 | 156x |
x1(i) = static_cast<arma::uword>(x1_cols[i]); |
| 357 | ||
| 358 |
// ── Unpack x_subset (empty IntegerVector → use all rows) ───────────────── |
|
| 359 | 66x |
arma::uvec x_sub; |
| 360 | 66x |
if (x_subset.size() > 0) {
|
| 361 | 3x |
x_sub.set_size(x_subset.size()); |
| 362 | 63x |
for (R_xlen_t i = 0; i < x_subset.size(); ++i) |
| 363 | 120x |
x_sub(i) = static_cast<arma::uword>(x_subset[i]); |
| 364 |
} |
|
| 365 | ||
| 366 |
// ── Unpack y1_cols ─────────────────────────────────────────────────────── |
|
| 367 | 66x |
arma::uvec y1(y1_cols.size()); |
| 368 | 121x |
for (R_xlen_t i = 0; i < y1_cols.size(); ++i) |
| 369 | 110x |
y1(i) = static_cast<arma::uword>(y1_cols[i]); |
| 370 | ||
| 371 |
// ── Build params ───────────────────────────────────────────────────────── |
|
| 372 | 66x |
qe::GeneralizedRotationParams p; |
| 373 | 66x |
p.x_model_matrix = x_model_matrix; |
| 374 | 66x |
p.x_target = x_target; |
| 375 | 66x |
p.x1_cols = x1; |
| 376 | 66x |
p.x_categorical = x_categorical; |
| 377 | 66x |
p.x_n_groups = static_cast<arma::uword>(x_n_groups); |
| 378 | 66x |
p.x_subset = x_sub; |
| 379 | 66x |
p.has_y = has_y; |
| 380 | 66x |
p.y_model_matrix = y_model_matrix; |
| 381 | 66x |
p.y_target = y_target; |
| 382 | 66x |
p.y1_cols = y1; |
| 383 | 66x |
p.y_categorical = y_categorical; |
| 384 | 66x |
p.y_n_groups = static_cast<arma::uword>(y_n_groups); |
| 385 | 66x |
p.n_lambda = n_lambda; |
| 386 | 66x |
p.k_folds = k_folds; |
| 387 | 66x |
p.lasso_eps = lasso_eps; |
| 388 | ||
| 389 | 132x |
return pack_rotation_result(qe::generalized_means_rotation(V, p)); |
| 390 |
} |
|
| 391 | ||
| 392 |
// ============================================================================= |
|
| 393 |
// CCD window estimation |
|
| 394 |
// ============================================================================= |
|
| 395 | ||
| 396 |
// Cross-covariance decay (CCD) window-size estimation |
|
| 397 |
// |
|
| 398 |
// Estimates the ENA moving-window size from the half-life decay lag of the |
|
| 399 |
// noise-corrected Frobenius norm of pooled conversation cross-covariance |
|
| 400 |
// matrices. |
|
| 401 |
// |
|
| 402 |
// @param conversations A list of numeric code matrices, one per conversation |
|
| 403 |
// (rows in sequence; all matrices share the same number of columns). |
|
| 404 |
// @param max_window Maximum lag to evaluate (default 20). |
|
| 405 |
// @param min_overlap Minimum overlapping rows (N - lag) required for a |
|
| 406 |
// conversation to contribute at a given lag (default 10). |
|
| 407 |
// |
|
| 408 |
// @return A list with \code{window_size}, \code{peak_lag}, and the per-lag
|
|
| 409 |
// curves \code{lag}, \code{frob}, \code{frob_sq_unbiased},
|
|
| 410 |
// \code{frob_unbiased_signed}, and \code{total_weight}.
|
|
| 411 |
// [[Rcpp::export]] |
|
| 412 | 10x |
Rcpp::List ccd_window( |
| 413 |
Rcpp::List conversations, |
|
| 414 |
int max_window = 20, |
|
| 415 |
int min_overlap = 10 |
|
| 416 |
) {
|
|
| 417 | 10x |
std::vector<arma::mat> convos; |
| 418 | 10x |
convos.reserve(conversations.size()); |
| 419 | 210x |
for (R_xlen_t i = 0; i < conversations.size(); ++i) {
|
| 420 | 200x |
convos.push_back(Rcpp::as<arma::mat>(conversations[i])); |
| 421 |
} |
|
| 422 | ||
| 423 | 10x |
qe::CCDResult res = qe::ccd_window(convos, max_window, min_overlap); |
| 424 | ||
| 425 |
return Rcpp::List::create( |
|
| 426 | 20x |
Named("window_size") = res.window_size,
|
| 427 | 20x |
Named("peak_lag") = res.peak_lag,
|
| 428 | 20x |
Named("lag") = res.lag,
|
| 429 | 20x |
Named("frob") = res.frob,
|
| 430 | 20x |
Named("frob_sq_unbiased") = res.frob_sq_unbiased,
|
| 431 | 20x |
Named("frob_unbiased_signed") = res.frob_unbiased_signed,
|
| 432 | 20x |
Named("total_weight") = res.total_weight
|
| 433 |
); |
|
| 434 |
} |