Diff of /R/spatial.R [000000] .. [413088]

Switch to unified view

a b/R/spatial.R
1
#' @include allgenerics.R
2
#'
3
NULL
4
5
####
6
# Spatial Neighbor graphs ####
7
####
8
9
#' Get spatial neighbors
10
#'
11
#' get neighbors in an assay given spatial coordinates
12
#'
13
#' @param object a VoltRon object
14
#' @param assay assay name (exp: Assay1) or assay class (exp: Visium, Xenium), see \link{SampleMetadata}. 
15
#' if NULL, the default assay will be used, see \link{vrMainAssay}.
16
#' @param group.by a column of metadata from \link{Metadata} used as grouping label for the spatial entities.
17
#' @param group.ids a subset of categories defined in metadata column from \code{group.by}.
18
#' @param method the method spatial connectivity: "delaunay", "spatialkNN", "radius".
19
#' @param k number of neighbors for kNN.
20
#' @param radius When \code{method = "radius"} selected, determines the radius of a neighborhood ball around each spatial point.
21
#' @param graph.key the name of the graph.
22
#' @param verbose verbose
23
#'
24
#' @importFrom igraph add_edges simplify make_empty_graph vertices
25
#' @importFrom RCDT delaunay
26
#' @importFrom RANN nn2
27
#' @importFrom data.table data.table melt
28
#' @importFrom stats dist
29
#'
30
#' @export
31
getSpatialNeighbors <- function(object, 
32
                                assay = NULL, 
33
                                group.by = NULL,
34
                                group.ids = NULL,
35
                                method = "delaunay", 
36
                                k = 10, 
37
                                radius = numeric(0), 
38
                                graph.key = method, 
39
                                verbose = TRUE){
40
41
  # get coordinates
42
  spatialpoints <- vrSpatialPoints(object, assay = assay)
43
44
  # get spatial graph per assay
45
  assay_names <- vrAssayNames(object, assay = assay)
46
  
47
  # get assay connectivity 
48
  assay_names_connected <- getBlockConnectivity(object, assay = assay_names)
49
  
50
  # get spatial edges
51
  spatialedges_list <- list()
52
  for(assy in assay_names_connected){
53
    
54
    # get coordinates
55
    cur_coords <- as.matrix(vrCoordinates(object, assay = assy))
56
    
57
    # get groups
58
    if(!is.null(group.by) && !is.null(group.ids)){
59
      
60
      # metadata
61
      if(verbose)
62
        message("Calculating Spatial Neighbors with group.by='", group.by, "' and group.ids='", paste(group.ids, collapse = ","), "'")
63
      metadata = Metadata(object, assay = assy)
64
      if(!group.by %in% colnames(metadata))
65
        stop("The column '", group.by, "' was not found in the metadata!")
66
      if(inherits(metadata, "data.table")){
67
        cur_group.by <- metadata[,get(names(metadata)[which(colnames(metadata) == group.by)])]
68
        names(cur_group.by) <- metadata$id
69
      } else {
70
        cur_group.by <- metadata[,group.by]
71
        if(!is.null(rownames(metadata))){
72
          names(cur_group.by) <- rownames(metadata)
73
        } else {
74
          names(cur_group.by) <- as.vector(metadata$id)
75
        }
76
      }
77
      if(!is.null(group.ids)){
78
        len_set_diff <- length(setdiff(group.ids,  cur_group.by))
79
        if(len_set_diff > 0){
80
        } else if(len_set_diff == length(group.ids)){ 
81
          stop("None of the groups defined in group.ids exist in group.by!")
82
        } 
83
        cur_group.by <- cur_group.by[cur_group.by %in% group.ids]
84
        cur_coords <- cur_coords[names(cur_group.by),]
85
      }
86
      
87
    } else if(sum(is.null(group.by),is.null(group.ids)) == 2) {
88
      
89
    } else {
90
      stop("Either both 'group.by' and 'group.ids' should be specified or both should be null")
91
    }
92
    
93
    # get edges
94
    spatialedges <-
95
      switch(method,
96
             delaunay = {
97
               nnedges <- RCDT::delaunay(cur_coords)
98
               nnedges <- as.vector(t(nnedges$edges[,seq_len(2)]))
99
               nnedges <- rownames(cur_coords)[nnedges]
100
               nnedges
101
             },
102
             spatialkNN = {
103
               # nnedges <- RANN::nn2(cur_coords, k = k + 1)
104
               nnedges <- knn_annoy(cur_coords, k = k + 1)
105
               names(nnedges) <- c("nn.index", "nn.dist")
106
               nnedges <- data.table::melt(data.table::data.table(nnedges$nn.index), id.vars = "V1")
107
               nnedges <- nnedges[,c("V1", "value")][V1 > 0 & value > 0]
108
               nnedges <- as.vector(t(as.matrix(nnedges)))
109
               nnedges <- rownames(cur_coords)[nnedges]
110
               nnedges
111
             },
112
             radius = {
113
               if(length(radius) == 0){
114
                 spot.radius <- vrAssayParams(object[[assy]], param = "nearestpost.distance")
115
                 radius <- ifelse(is.null(spot.radius), 1, spot.radius)
116
               }
117
               nnedges <- suppressWarnings({RANN::nn2(cur_coords, searchtype = "radius", radius = radius, k = min(300, sqrt(nrow(cur_coords))/2))})
118
               nnedges <- data.table::melt(data.table::data.table(nnedges$nn.idx), id.vars = "V1")
119
               nnedges <- nnedges[,c("V1", "value")][V1 > 0 & value > 0]
120
               nnedges <- as.vector(t(as.matrix(nnedges)))
121
               nnedges <- rownames(cur_coords)[nnedges]
122
               nnedges
123
             })
124
    spatialedges_list <- c(spatialedges_list, list(spatialedges))
125
  }
126
  spatialedges <- unlist(spatialedges_list)
127
128
  # make graph and add edges
129
  graph <- make_empty_graph(directed = FALSE) + vertices(spatialpoints)
130
  graph <- add_edges(graph, edges = spatialedges)
131
  graph <- simplify(graph, remove.multiple = TRUE, remove.loops = FALSE)
132
  vrGraph(object, assay = assay_names, graph.type = graph.key) <- graph
133
134
  # return
135
  return(object)
136
}
137
138
####
139
# Neighbor Enrichment test ####
140
####
141
142
#' vrNeighbourhoodEnrichment
143
#'
144
#' Conduct Neighborhood enrichment test for pairs of clusters for all assays
145
#'
146
#' @param object a VoltRon object
147
#' @param assay assay name (exp: Assay1) or assay class (exp: Visium, Xenium), see \link{SampleMetadata}. 
148
#' if NULL, the default assay will be used, see \link{vrMainAssay}.
149
#' @param group.by a column from metadata to seperate spatial points
150
#' @param graph.type the type of graph to determine spatial neighborhood
151
#' @param num.sim the number of simulations
152
#' @param seed seed
153
#' @param verbose verbose
154
#'
155
#' @export
156
#'
157
vrNeighbourhoodEnrichment <- function(object, assay = NULL, group.by = NULL, graph.type = "delaunay", num.sim = 1000, seed = 1, verbose = TRUE){
158
159
  # set the seed
160
  set.seed(seed)
161
162
  # check object
163
  if(!inherits(object, "VoltRon"))
164
    stop("Please provide a VoltRon object!")
165
166
  # sample metadata
167
  sample.metadata <- SampleMetadata(object)
168
169
  # get assay names
170
  assay_names <- vrAssayNames(object, assay = assay)
171
172
  # test for each assay
173
  neigh_results <- list()
174
  for(assy in assay_names){
175
    if(verbose)
176
      message("Testing Neighborhood Enrichment of '", group.by ,"' for '", assy, "'")
177
    object_subset <- subsetVoltRon(object, assays = assy)
178
    neigh_results[[assy]] <- vrNeighbourhoodEnrichmentSingle(object_subset, group.by = group.by, graph.type = graph.type,
179
                                                             num.sim = num.sim, seed = seed)
180
    neigh_results[[assy]] <- data.frame(neigh_results[[assy]], AssayID = assy, SampleMetadata(object_subset))
181
  }
182
  all_neigh_results <- do.call(rbind, neigh_results)
183
184
  # return
185
  return(all_neigh_results)
186
}
187
188
#' vrNeighbourhoodEnrichmentSingle
189
#'
190
#' Conduct Neighborhood enrichment test for pairs of clusters for a vrAssay
191
#'
192
#' @param object a VoltRon object
193
#' @param group.by a column from metadata to seperate spatial points
194
#' @param graph.type the type of graph to determine spatial neighborhood
195
#' @param num.sim the number of simulations
196
#' @param seed seed
197
#'
198
#' @importFrom dplyr group_by bind_rows filter summarize mutate n
199
#' @importFrom igraph neighborhood
200
#'
201
#' @noRd
202
vrNeighbourhoodEnrichmentSingle <- function(object, group.by = NULL, graph.type = "delaunay", num.sim = 1000, seed = 1) {
203
  
204
  # set the seed
205
  set.seed(seed)
206
  
207
  # main object
208
  metadata <- Metadata(object)
209
  if(group.by %in% colnames(metadata)){
210
    grp <- metadata[[group.by]]
211
    names(grp) <- rownames(metadata) 
212
  } else {
213
    stop("'", group.by, "' is not available in metadata!")
214
  }
215
216
  # get graph and neighborhood
217
  graph <- vrGraph(object, graph.type = graph.type)
218
  neighbors_graph <- igraph::neighborhood(graph)
219
  neighbors_graph_data <- lapply(neighbors_graph, function(x) {
220
    # cbind(x$name[1],x$name[-1])
221
    cbind(x$name[1],x$name)[-1,]
222
    # if(dat)
223
  })
224
  neighbors_graph_data <- do.call(rbind, neighbors_graph_data)
225
  colnames(neighbors_graph_data) <- c("from", "to")
226
  
227
  # get simulations
228
  grp_sim <- vapply(seq_len(1000), function(x) sample(grp), grp)
229
  rownames(grp_sim) <- names(grp)
230
  
231
  # get adjacency for observed and simulated pairs
232
  neighbors_graph_data_list <- list(data.frame(neighbors_graph_data, from_value = grp[neighbors_graph_data[,1]], to_value = grp[neighbors_graph_data[,2]], type = "obs"))
233
  for(i in 2:(ncol(grp_sim)+1))
234
    neighbors_graph_data_list[[i]] <- data.frame(neighbors_graph_data, from_value = grp_sim[,i-1][neighbors_graph_data[,1]], to_value = grp_sim[,i-1][neighbors_graph_data[,2]], type = paste0("sim", i))
235
  neighbors_graph_data <- dplyr::bind_rows(neighbors_graph_data_list)
236
237
  # get adjacency for observed and simulated pairs
238
  neigh_results <- neighbors_graph_data %>%
239
    dplyr::group_by(from_value, to_value, type) %>%
240
    dplyr::summarize(mean_value = dplyr::n()) %>%
241
    dplyr::group_by(from_value, to_value) %>%
242
    dplyr::mutate(assoc_test = mean_value > ifelse("obs" %in% type, mean_value[type == "obs"], 0),
243
                  segreg_test = mean_value < ifelse("obs" %in% type, mean_value[type == "obs"], 0)) %>%
244
    dplyr::mutate(majortype = ifelse(type == "obs", "obs", "sim")) %>% 
245
    dplyr::group_by(from_value, to_value) %>%
246
    dplyr::mutate(value = ifelse(sum(majortype == "obs") > 0, log(mean_value[majortype == "obs"]/mean(mean_value[majortype == "sim"])), 0)) %>% 
247
    dplyr::filter(type != "obs") %>%
248
    dplyr::group_by(from_value, to_value) %>%
249
    dplyr::summarize(p_assoc = mean(assoc_test), p_segreg = mean(segreg_test), value = value[1]) %>%
250
    dplyr::mutate(p_assoc_adj = p.adjust(p_assoc, method = "fdr"),
251
                  p_segreg_adj = p.adjust(p_segreg, method = "fdr"))
252
  
253
  # number of samples
254
  grp_table <- table(grp)
255
  neigh_results$n_from <- grp_table[neigh_results$from_value]
256
  neigh_results$n_to <- grp_table[neigh_results$to_value]
257
  
258
  # return
259
  neigh_results
260
}
261
262
####
263
# Hot Spot Analysis ####
264
####
265
266
#' getHotSpotAnalysis
267
#'
268
#' Conduct hot spot detection
269
#'
270
#' @param object a VoltRon object
271
#' @param assay assay name (exp: Assay1) or assay class (exp: Visium, Xenium), see \link{SampleMetadata}. 
272
#' if NULL, the default assay will be used, see \link{vrMainAssay}.
273
#' @param method the statistical method of conducting hot spot analysis. Default is "Getis-Ord"
274
#' @param features a set of features to be visualized, either from \link{vrFeatures} of raw or normalized data or columns of the \link{Metadata}.
275
#' @param graph.type the type of graph to determine spatial neighborhood
276
#' @param alpha.value the alpha value for the hot spot analysis test. Default is 0.01
277
#' @param norm if TRUE, the normalized data is used
278
#' @param verbose verbose
279
#' 
280
#' @importFrom Matrix rowSums
281
#' @importFrom igraph as_adjacency_matrix
282
#' @importFrom stats pnorm
283
#'
284
#' @export
285
getHotSpotAnalysis <- function(object, assay = NULL, method = "Getis-Ord", features, graph.type = "delaunay", alpha.value = 0.01, norm = TRUE, verbose = TRUE){
286
  
287
  # check object
288
  if(!inherits(object, "VoltRon"))
289
    stop("Please provide a VoltRon object!")
290
  
291
  # metadata
292
  sample.metadata <- SampleMetadata(object)
293
  metadata <- Metadata(object, assay = assay)
294
  
295
  # initiate metadata columns
296
  for(feat in features){
297
    metadata[[paste0(feat,"_hotspot_flag")]] <- 
298
      metadata[[paste0(feat,"_hotspot_pvalue")]] <- 
299
        metadata[[paste0(feat,"_hotspot_stat")]] <- rep(NA, nrow(metadata))
300
  }
301
  
302
  # get assay names
303
  assay_names <- vrAssayNames(object, assay = assay)
304
  
305
  # test for each assay
306
  neigh_results <- list()
307
  for(assy in assay_names){
308
    
309
    # verbose
310
    if(verbose)
311
      message("Running Hot Spot Analysis with '", method, "' for '", assy, "'")
312
    
313
    # get related data 
314
    graph <- vrGraph(object, assay = assy, graph.type = graph.type)
315
    adj_matrix <- igraph::as_adjacency_matrix(graph)
316
    cur_metadata <- subset_metadata(metadata, assays = assy)
317
    
318
    # get features
319
    data_features <- features[features %in% vrFeatures(object, assay = assy)]
320
    if(length(data_features) > 0){
321
      normdata <- vrData(object, assay = assy, features = data_features, norm = norm)
322
    }
323
    
324
    # for each feature
325
    for(feat in features){
326
      
327
      # Getis-Ord 
328
      if(method == "Getis-Ord"){
329
330
        # get feature
331
        if(feat %in% data_features){
332
          if(inherits(normdata, "IterableMatrix")){
333
            statistic <- as.matrix(normdata[feat,])[1,]
334
          } else {
335
            statistic <- normdata[feat,]
336
          }
337
        } else if(feat %in% colnames(cur_metadata)){
338
          if(inherits(cur_metadata, "data.table")){
339
            statistic <- cur_metadata[,get(names(cur_metadata)[which(colnames(cur_metadata) == feat)])]
340
          } else {
341
            statistic <- cur_metadata[,feat]
342
          }
343
        } else {
344
          stop("'", feat, "' is not found in either the data matrix or the metadata!")
345
        }
346
        
347
        # update statistics if not numeric
348
        if(!is.numeric(statistic)){
349
          statistic <- Matrix::rowSums(adj_matrix)
350
        }
351
        
352
        # initiate getis ord
353
        getisord <- list()
354
        length(getisord) <- 3
355
        names(getisord) <- c("hotspot_stat", "hotspot_pvalue", "hotspot_flag")
356
        
357
        # calculate getis ord
358
        n <- length(statistic)
359
        statistic <- statistic - (min(statistic)) ### correct for negative scores, correct for this later
360
        getisord_stat <- adj_matrix %*% statistic
361
        getisord_stat <- getisord_stat/(sum(statistic) - statistic)
362
        getisord[[1]] <- getisord_stat[,1]
363
          
364
        # calculate z score expectation and variance
365
        weight_sum <- Matrix::rowSums(adj_matrix)
366
        getisord_exp <- weight_sum/(n - 1)
367
        getisord_moment_1 <- (sum(statistic) - statistic)/(n - 1)
368
        getisord_moment_2 <- (sum(statistic^2) - statistic^2)/(n - 1) - getisord_moment_1^2
369
        getisord_var <- weight_sum*(n - 1 - weight_sum)*getisord_moment_2
370
        getisord_var <- getisord_var/((n-1)^2 *(n-2)*getisord_moment_1^2)
371
        
372
        # calculate z score 
373
        getisord_zscore <- (getisord[[1]] - getisord_exp)/sqrt(getisord_var)
374
        getisord_zscore[is.nan(getisord_zscore)] <- NA
375
        # getisord[[2]] <- 1-stats::pnorm(getisord_zscore)
376
        getisord[[2]] <- p.adjust(1-stats::pnorm(getisord_zscore), method = "bonferroni")
377
        getisord[[3]] <- ifelse(getisord[[2]] < alpha.value, "hot", "cold") 
378
        
379
        # get graph based hot spot filtering
380
        # at least some number of common neighbors should be hotspots
381
        
382
        # update metadata for features
383
        for(label in names(getisord))
384
          cur_metadata[[paste(feat, label, sep = "_")]] <-  getisord[[label]]
385
      }
386
    }
387
    
388
    # update metadata for assays
389
    if("id" %in% colnames(metadata)){
390
      ind <- match(as.vector(cur_metadata$id), as.vector(metadata$id))
391
      for(feat in features){
392
        for(label in names(getisord)){
393
          metadata_label <- paste(feat, label, sep = "_")
394
          metadata[[metadata_label]][ind] <- cur_metadata[[metadata_label]]
395
          object <- addMetadata(object, assay = assay, value = metadata[[metadata_label]], label = metadata_label)
396
        }
397
      }
398
    } else {
399
      for(feat in features){
400
        for(label in names(getisord)){
401
          metadata_label <- paste(feat, label, sep = "_")
402
          object <- addMetadata(object, assay = assay, value = metadata[[metadata_label]], label = metadata_label)
403
        }
404
      }
405
    }
406
  }
407
  
408
  # update metadata
409
  # Metadata(object, assay = assay) <- metadata
410
  
411
  # return
412
  return(object)
413
}
414
415
####
416
# Niche Analysis ####
417
####
418
419
#' getNicheAssay
420
#'
421
#' Create Niche Assays
422
#'
423
#' @param object a VoltRon object
424
#' @param assay assay name (exp: Assay1) or assay class (exp: Visium, Xenium), see \link{SampleMetadata}. 
425
#' if NULL, the default assay will be used, see \link{vrMainAssay}.
426
#' @param label grouping label for Niche definition
427
#' @param graph.type the type of graph to determine spatial neighborhood
428
#' @param new_feature_name the name of the new feature set created for the niche assay. Default: "Niche"
429
#'
430
#' @importFrom igraph V V<- neighborhood
431
#' @export
432
getNicheAssay <- function(object, assay = NULL, label = NULL, graph.type = "delaunay", new_feature_name = "Niche"){
433
  
434
  # get metadata
435
  sample.metadata <- SampleMetadata(object)
436
  metadata <- Metadata(object)
437
  
438
  # get assay names
439
  assay_names <- vrAssayNames(object, assay = assay)
440
  
441
  # get graph 
442
  graph <- vrGraph(object, assay = assay_names, graph.type = graph.type)
443
  
444
  # get label
445
  cur_metadata <- subset_metadata(metadata, assays = assay_names)
446
  if(label %in% colnames(cur_metadata)){
447
    label <- as.vector(cur_metadata[,label])
448
    if(!is.null(rownames(cur_metadata))){
449
      names(label) <- rownames(cur_metadata)
450
    } else {
451
      names(label) <- as.vector(cur_metadata$id)
452
    }
453
  } else {
454
    stop("'", label, "' is not found in the metadata!")
455
  }
456
  
457
  # get niche assay
458
  adj_matrix <- igraph::neighborhood(graph)
459
  unique_label <- unique(label)
460
  niche_counts <- vapply(adj_matrix, function(x){
461
    table(factor(label[x], levels = unique_label))
462
  }, numeric(length(na.omit(unique_label))))
463
  colnames(niche_counts) <- igraph::V(graph)$name
464
  
465
  # add cell type mixtures as new feature set
466
  for(assy in assay_names){
467
    cur_niche_counts <- niche_counts[,stringr::str_extract(colnames(niche_counts), "Assay[0-9]+") %in% assy]
468
    object <- addFeature(object, assay = assy, data = cur_niche_counts, feature_name = new_feature_name)
469
  }
470
471
  # return
472
  return(object)
473
}