blob: 41eb8d23bf0ba82ac8f7c41d2c6d5430c70b367d [file] [log] [blame]
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001#' @include logging.R
2setGeneric("collocationAnalysis", function(kco, ...) standardGeneric("collocationAnalysis"))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02003
4#' Collocation analysis
5#'
Marc Kupietza8c40f42025-06-24 15:49:52 +02006#' @family collocation analysis functions
Marc Kupietzdbd431a2021-08-29 12:17:45 +02007#' @aliases collocationAnalysis
8#'
9#' @description
Marc Kupietzdbd431a2021-08-29 12:17:45 +020010#'
11#' Performs a collocation analysis for the given node (or query)
12#' in the given virtual corpus.
13#'
14#' @details
15#' The collocation analysis is currently implemented on the client side, as some of the
16#' functionality is not yet provided by the KorAP backend. Mainly for this reason
17#' it is very slow (several minutes, up to hours), but on the other hand very flexible.
18#' You can, for example, perform the analysis in arbitrary virtual corpora, use complex node queries,
19#' and look for expression-internal collocates using the focus function (see examples and demo).
20#'
21#' To increase speed at the cost of accuracy and possible false negatives,
22#' you can decrease searchHitsSampleLimit and/or topCollocatesLimit and/or set exactFrequencies to FALSE.
23#'
Marc Kupietze7f0d682025-02-19 10:50:59 +010024#' Note that some outdated non-DeReKo back-ends might not yet support returning tokenized matches (warning issued).
25#' In this case, the client library will fall back to client-side tokenization which might be slightly less accurate.
26#' This might lead to false negatives and to frequencies that differ from corresponding ones acquired via the web
Marc Kupietzdbd431a2021-08-29 12:17:45 +020027#' user interface.
28#'
Marc Kupietzdbd431a2021-08-29 12:17:45 +020029#'
Marc Kupietz67edcb52021-09-20 21:54:24 +020030#' @param lemmatizeNodeQuery if TRUE, node query will be lemmatized, i.e. `x -> [tt/l=x]`
Marc Kupietzdbd431a2021-08-29 12:17:45 +020031#' @param minOccur minimum absolute number of observed co-occurrences to consider a collocate candidate
32#' @param topCollocatesLimit limit analysis to the n most frequent collocates in the search hits sample
33#' @param searchHitsSampleLimit limit the size of the search hits sample
34#' @param stopwords vector of stopwords not to be considered as collocates
Marc Kupietz6bd9cad2024-12-18 15:57:26 +010035#' @param withinSpan KorAP span specification (see <https://korap.ids-mannheim.de/doc/ql/poliqarp-plus?embedded=true#spans>) for collocations to be searched within. Defaults to `base/s=s`.
Marc Kupietzdbd431a2021-08-29 12:17:45 +020036#' @param exactFrequencies if FALSE, extrapolate observed co-occurrence frequencies from frequencies in search hits sample, otherwise retrieve exact co-occurrence frequencies
37#' @param seed seed for random page collecting order
Marc Kupietz67edcb52021-09-20 21:54:24 +020038#' @param expand if TRUE, `node` and `vc` parameters are expanded to all of their combinations
Marc Kupietz7d400e02021-12-19 16:39:36 +010039#' @param maxRecurse apply collocation analysis recursively `maxRecurse` times
40#' @param addExamples If TRUE, examples for instances of collocations will be added in a column `example`. This makes a difference in particular if `node` is given as a lemma query.
Marc Kupietz2b0b0a12025-10-19 14:49:14 +020041#' @param thresholdScore association score function (see \code{\link{association-score-functions}}) to use for computing the threshold that is applied for recursive collocation analysis calls (only applied when \code{maxRecurse > 0})
42#' @param threshold minimum value of `thresholdScore` function call to apply collocation analysis recursively (only applied when \code{maxRecurse > 0})
Marc Kupietz7d400e02021-12-19 16:39:36 +010043#' @param localStopwords vector of stopwords that will not be considered as collocates in the current function call, but that will not be passed to recursive calls
Marc Kupietz47d0d2b2021-12-19 16:38:52 +010044#' @param collocateFilterRegex allow only collocates matching the regular expression
Marc Kupietzde679ea2025-10-19 13:14:51 +020045#' @param queryMissingScores if TRUE, attempt to retrieve corpus-based association scores for vc/collocate combinations that would otherwise be imputed, by re-querying the KorAP backend without applying the collocate frequency threshold
Marc Kupietz4cbb5472025-10-19 12:15:25 +020046#' @param missingScoreQuantile lower quantile (evaluated per association measure) that anchors the adaptive floor used for imputing missing scores between virtual corpora; a robust spread is subtracted from this anchor so the imputed values stay below the weakest observed scores
Marc Kupietze34a8be2025-10-17 20:13:42 +020047#' @param vcLabel optional label override for the current virtual corpus (used internally when named VC collections are expanded)
Marc Kupietzdb2fabd2026-04-27 15:01:37 +020048#' @param cacheAs path to an RDS file for caching the result. If the file already exists, the cached result is loaded and returned immediately without contacting the server. Otherwise the analysis is run normally and the result is saved to the file before returning. Defaults to \code{NULL} (no caching).
Marc Kupietz67edcb52021-09-20 21:54:24 +020049#' @param ... more arguments will be passed to [collocationScoreQuery()]
Marc Kupietzdbd431a2021-08-29 12:17:45 +020050#' @inheritParams collocationScoreQuery,KorAPConnection-method
Marc Kupietz130a2a22025-10-18 16:09:23 +020051#' @return
52#' A tibble where each row represents a candidate collocate for the requested node.
53#' Columns include (depending on the selected association measures):
Marc Kupietzdb2fabd2026-04-27 15:01:37 +020054#'
Marc Kupietz130a2a22025-10-18 16:09:23 +020055#' \itemize{
56#' \item \code{node}, \code{collocate}, \code{vc}, \code{label}: identifiers for the query node, collocate, virtual corpus, and optional label.
57#' \item Frequency and contingency information such as \code{frequency}, \code{O}, \code{O1}, \code{O2}, \code{E}, \code{leftContextSize}, \code{rightContextSize}, and \code{w}.
58#' \item Association measures (e.g. \code{logDice}, \code{ll}, \code{mi}, ...), one column per requested scorer.
59#' \item Per-labelled association scores produced by multi-VC comparisons using the pattern \code{<measure>_<label>}.
60#' \item Ranks per label/measure with the pattern \code{rank_<label>_<measure>} (1 is best) and the corresponding percentile ranks \code{percentile_rank_<label>_<measure>}.
61#' \item Pairwise contrasts for two-label comparisons, e.g. \code{delta_<measure>}, \code{delta_rank_<measure>}, and \code{delta_percentile_rank_<measure>}.
Marc Kupietz09b1c082026-05-01 14:45:47 +020062#' \item Summary columns describing the strongest labels per measure (\code{winner_*}, \code{runner_up_*}, \code{loser_*}, and \code{max_delta_*}), including winner/loser \code{webUIRequestUrl} columns. In multi-VC comparisons, missing per-label concordance URLs are derived from another available row URL for the same \code{node}/\code{collocate} by replacing the \code{cq} parameter with the target label's virtual corpus. Unsuffixed \code{winner_webUIRequestUrl} and \code{loser_webUIRequestUrl} columns are populated only when the score-based URL choices agree.
Marc Kupietzd7bb5cb2026-08-31 10:18:02 +020063#' \item \code{imputed_<label>}, \code{n_imputed}, and \code{imputed}: flags marking rows whose scores were not observed for some label but imputed (see \code{missingScoreQuantile}). Filter with \code{dplyr::filter(!imputed)} to keep only collocates attested in every compared virtual corpus.
Marc Kupietz130a2a22025-10-18 16:09:23 +020064#' \item Optional helper columns such as \code{query}, \code{example}, or \code{url} when example retrieval is requested.
65#' }
Marc Kupietzc4540a22025-10-14 17:39:53 +020066#' @importFrom dplyr arrange desc slice_head bind_rows group_by mutate ungroup left_join select row_number all_of first
Marc Kupietzdbd431a2021-08-29 12:17:45 +020067#' @importFrom purrr pmap
Marc Kupietzc4540a22025-10-14 17:39:53 +020068#' @importFrom tidyr expand_grid pivot_wider
69#' @importFrom rlang sym
Marc Kupietzdbd431a2021-08-29 12:17:45 +020070#'
71#' @examples
Marc Kupietz6ae76052021-09-21 10:34:00 +020072#' \dontrun{
73#'
Marc Kupietz6dfeed92025-06-03 11:58:06 +020074#' # Find top collocates of "Packung" inside and outside the sports domain.
75#' KorAPConnection(verbose = TRUE) |>
76#' collocationAnalysis("Packung",
77#' vc = c("textClass=sport", "textClass!=sport"),
78#' leftContextSize = 1, rightContextSize = 1, topCollocatesLimit = 20
79#' ) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +020080#' dplyr::filter(logDice >= 5)
81#' }
82#'
Marc Kupietz6ae76052021-09-21 10:34:00 +020083#' \dontrun{
84#'
Marc Kupietzdbd431a2021-08-29 12:17:45 +020085#' # Identify the most prominent light verb construction with "in ... setzen".
86#' # Note that, currently, the use of focus function disallows exactFrequencies.
Marc Kupietz4cd066d2025-02-28 15:48:23 +010087#' KorAPConnection(verbose = TRUE) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +020088#' collocationAnalysis("focus(in [tt/p=NN] {[tt/l=setzen]})",
Marc Kupietz6dfeed92025-06-03 11:58:06 +020089#' leftContextSize = 1, rightContextSize = 0, exactFrequencies = FALSE, topCollocatesLimit = 20
90#' )
Marc Kupietzdbd431a2021-08-29 12:17:45 +020091#' }
92#'
93#' @export
Marc Kupietz6dfeed92025-06-03 11:58:06 +020094setMethod(
95 "collocationAnalysis", "KorAPConnection",
96 function(kco,
97 node,
98 vc = "",
99 lemmatizeNodeQuery = FALSE,
100 minOccur = 5,
101 leftContextSize = 5,
102 rightContextSize = 5,
103 topCollocatesLimit = 200,
104 searchHitsSampleLimit = 20000,
105 ignoreCollocateCase = FALSE,
106 withinSpan = ifelse(exactFrequencies, "base/s=s", ""),
107 exactFrequencies = TRUE,
108 stopwords = append(RKorAPClient::synsemanticStopwords(), node),
109 seed = 7,
110 expand = length(vc) != length(node),
111 maxRecurse = 0,
112 addExamples = FALSE,
113 thresholdScore = "logDice",
114 threshold = 2.0,
115 localStopwords = c(),
116 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzde679ea2025-10-19 13:14:51 +0200117 queryMissingScores = FALSE,
Marc Kupietz9894a372025-10-18 14:51:29 +0200118 missingScoreQuantile = 0.05,
Marc Kupietze34a8be2025-10-17 20:13:42 +0200119 vcLabel = NA_character_,
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200120 cacheAs = NULL,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200121 ...) {
Marc Kupietzb2862d42025-10-18 10:17:49 +0200122 word <- frequency <- O <- NULL
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200123
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200124 if (!is.null(cacheAs) && !grepl("\\.rds$", cacheAs, ignore.case = TRUE)) {
125 cacheAs <- paste0(cacheAs, ".rds")
126 }
127
128 if (!is.null(cacheAs) && file.exists(cacheAs)) {
129 log_info(kco@verbose, sprintf("Loading collocation analysis from cache: %s\n", cacheAs))
130 return(readRDS(cacheAs))
131 }
132
Marc Kupietzb2862d42025-10-18 10:17:49 +0200133 if (!exactFrequencies && (!is.na(withinSpan) && !is.null(withinSpan) && nzchar(withinSpan))) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200134 stop(sprintf("Not empty withinSpan (='%s') requires exactFrequencies=TRUE", withinSpan), call. = FALSE)
135 }
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200136
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200137 warnIfNotAuthorized(kco)
Marc Kupietz581a29b2021-09-04 20:51:04 +0200138
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200139 if (lemmatizeNodeQuery) {
140 node <- lemmatizeWordQuery(node)
141 }
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200142
Marc Kupietze34a8be2025-10-17 20:13:42 +0200143 vcNames <- names(vc)
Marc Kupietze34a8be2025-10-17 20:13:42 +0200144 if (is.null(vcNames)) {
145 vcNames <- rep(NA_character_, length(vc))
Marc Kupietze34a8be2025-10-17 20:13:42 +0200146 }
147
148 label_lookup <- NULL
Marc Kupietzb2862d42025-10-18 10:17:49 +0200149 if (!is.null(names(vc)) && length(vc) > 0) {
150 raw_names <- names(vc)
151 if (any(!is.na(raw_names) & raw_names != "")) {
152 label_lookup <- stats::setNames(raw_names, vc)
153 }
Marc Kupietze34a8be2025-10-17 20:13:42 +0200154 }
155
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200156 result <- if (length(node) > 1 || length(vc) > 1) {
Marc Kupietze34a8be2025-10-17 20:13:42 +0200157 grid <- if (expand) {
Marc Kupietzb2862d42025-10-18 10:17:49 +0200158 tmp_grid <- tidyr::expand_grid(node = node, idx = seq_along(vc))
159 tmp_grid$vc <- vc[tmp_grid$idx]
160 tmp_grid$vcLabel <- vcNames[tmp_grid$idx]
161 tmp_grid[, c("node", "vc", "vcLabel"), drop = FALSE]
Marc Kupietze34a8be2025-10-17 20:13:42 +0200162 } else {
163 tibble(node = node, vc = vc, vcLabel = vcNames)
164 }
165
166 multi_result <- purrr::pmap(grid, function(node, vc, vcLabel, ...) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200167 collocationAnalysis(kco,
168 node = node,
169 vc = vc,
170 minOccur = minOccur,
171 leftContextSize = leftContextSize,
172 rightContextSize = rightContextSize,
173 topCollocatesLimit = topCollocatesLimit,
174 searchHitsSampleLimit = searchHitsSampleLimit,
175 ignoreCollocateCase = ignoreCollocateCase,
176 withinSpan = withinSpan,
177 exactFrequencies = exactFrequencies,
178 stopwords = stopwords,
179 addExamples = TRUE,
180 localStopwords = localStopwords,
181 seed = seed,
182 expand = expand,
Marc Kupietz9894a372025-10-18 14:51:29 +0200183 missingScoreQuantile = missingScoreQuantile,
Marc Kupietzde679ea2025-10-19 13:14:51 +0200184 queryMissingScores = queryMissingScores,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200185 collocateFilterRegex = collocateFilterRegex,
Marc Kupietze34a8be2025-10-17 20:13:42 +0200186 vcLabel = vcLabel,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200187 ...
188 )
189 }) |>
Marc Kupietze31322e2025-10-17 18:55:36 +0200190 bind_rows()
191
192 if (!"vc" %in% names(multi_result) || nrow(multi_result) == 0) {
193 multi_result
194 } else {
Marc Kupietzde679ea2025-10-19 13:14:51 +0200195 if (queryMissingScores) {
196 multi_result <- backfill_missing_scores(
197 multi_result,
198 grid = grid,
199 kco = kco,
200 ignoreCollocateCase = ignoreCollocateCase,
201 ...
202 )
203 }
204
Marc Kupietze34a8be2025-10-17 20:13:42 +0200205 if (!"label" %in% names(multi_result)) {
206 multi_result$label <- NA_character_
207 }
208
209 if (!is.null(label_lookup)) {
210 override <- unname(label_lookup[multi_result$vc])
211 missing_idx <- is.na(multi_result$label) | multi_result$label == ""
212 if (any(missing_idx)) {
213 multi_result$label[missing_idx] <- override[missing_idx]
214 }
215 }
216
217 missing_idx <- is.na(multi_result$label) | multi_result$label == ""
218 if (any(missing_idx)) {
219 multi_result$label[missing_idx] <- queryStringToLabel(multi_result$vc[missing_idx])
220 }
221
Marc Kupietze31322e2025-10-17 18:55:36 +0200222 multi_result |>
Marc Kupietz9894a372025-10-18 14:51:29 +0200223 add_multi_vc_comparisons(
Marc Kupietz9894a372025-10-18 14:51:29 +0200224 missingScoreQuantile = missingScoreQuantile
225 )
Marc Kupietze31322e2025-10-17 18:55:36 +0200226 }
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200227 } else {
Marc Kupietze34a8be2025-10-17 20:13:42 +0200228 if ((is.na(vcLabel) || vcLabel == "") && length(vcNames) >= 1) {
229 vcLabel <- vcNames[1]
230 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200231
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200232 set.seed(seed)
233 candidates <- collocatesQuery(
234 kco,
235 node,
236 vc = vc,
237 minOccur = minOccur,
238 leftContextSize = leftContextSize,
239 rightContextSize = rightContextSize,
240 searchHitsSampleLimit = searchHitsSampleLimit,
241 ignoreCollocateCase = ignoreCollocateCase,
242 stopwords = append(stopwords, localStopwords),
Marc Kupietzb2862d42025-10-18 10:17:49 +0200243 collocateFilterRegex = collocateFilterRegex,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200244 ...
245 )
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200246
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200247 if (nrow(candidates) > 0) {
248 candidates <- candidates |>
249 filter(frequency >= minOccur) |>
250 slice_head(n = topCollocatesLimit)
251 collocationScoreQuery(
252 kco,
253 node = node,
254 collocate = candidates$word,
255 vc = vc,
256 leftContextSize = leftContextSize,
257 rightContextSize = rightContextSize,
258 observed = if (exactFrequencies) NA else candidates$frequency,
259 ignoreCollocateCase = ignoreCollocateCase,
260 withinSpan = withinSpan,
261 ...
262 ) |>
263 filter(O >= minOccur) |>
264 dplyr::arrange(dplyr::desc(logDice))
265 } else {
266 tibble()
267 }
268 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200269
270 if (!is.na(vcLabel) && vcLabel != "" && "label" %in% names(result)) {
271 result$label <- rep(vcLabel, nrow(result))
272 }
273
274 threshold_col <- thresholdScore
275 if (maxRecurse > 0 && nrow(result) > 0 && threshold_col %in% names(result)) {
276 threshold_values <- result[[threshold_col]]
277 eligible_idx <- which(!is.na(threshold_values) & threshold_values >= threshold)
278 if (length(eligible_idx) > 0) {
279 recurseWith <- result[eligible_idx, , drop = FALSE]
280 result <- collocationAnalysis(
281 kco,
282 node = paste0("(", buildCollocationQuery(
283 removeWithinSpan(recurseWith$node, withinSpan),
284 recurseWith$collocate,
285 leftContextSize = leftContextSize,
286 rightContextSize = rightContextSize,
287 withinSpan = ""
288 ), ")"),
289 vc = vc,
290 minOccur = minOccur,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200291 leftContextSize = leftContextSize,
292 rightContextSize = rightContextSize,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200293 withinSpan = withinSpan,
294 maxRecurse = maxRecurse - 1,
295 stopwords = stopwords,
296 localStopwords = recurseWith$collocate,
297 exactFrequencies = exactFrequencies,
298 searchHitsSampleLimit = searchHitsSampleLimit,
299 topCollocatesLimit = topCollocatesLimit,
300 addExamples = FALSE,
Marc Kupietz9894a372025-10-18 14:51:29 +0200301 missingScoreQuantile = missingScoreQuantile,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200302 collocateFilterRegex = collocateFilterRegex,
Marc Kupietzde679ea2025-10-19 13:14:51 +0200303 queryMissingScores = queryMissingScores,
Marc Kupietz2b0b0a12025-10-19 14:49:14 +0200304 thresholdScore = thresholdScore,
305 threshold = threshold,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200306 vcLabel = vcLabel
307 ) |>
Marc Kupietz2b0b0a12025-10-19 14:49:14 +0200308 bind_rows(result)
309
310 if (threshold_col %in% names(result)) {
311 threshold_values <- result[[threshold_col]]
312 keep_idx <- is.na(threshold_values) | threshold_values >= threshold
313 result <- result[keep_idx, , drop = FALSE]
314 }
315
316 result <- result |>
Marc Kupietzb2862d42025-10-18 10:17:49 +0200317 filter(O >= minOccur) |>
318 dplyr::arrange(dplyr::desc(logDice))
319 }
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200320 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200321
322 if (addExamples && nrow(result) > 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200323 result$query <- buildCollocationQuery(
324 result$node,
325 result$collocate,
326 leftContextSize = leftContextSize,
327 rightContextSize = rightContextSize,
328 withinSpan = withinSpan
329 )
330 result$example <- findExample(
331 kco,
332 query = result$query,
333 vc = result$vc
334 )
335 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200336
Marc Kupietz0a292632025-10-19 14:04:36 +0200337 if (!is.null(withinSpan) && !is.na(withinSpan) && nzchar(withinSpan) &&
338 nrow(result) > 0 &&
339 "webUIRequestUrl" %in% names(result) &&
340 "query" %in% names(result)) {
341 candidate_rows <- which(!is.na(result$node) &
342 !grepl("focus\\(", result$node, perl = TRUE) &
343 !is.na(result$query) & nzchar(result$query))
344
345 if (length(candidate_rows) > 0) {
346 focused_queries <- vapply(
347 result$query[candidate_rows],
348 inject_focus_into_query,
349 character(1)
350 )
351
352 changed <- focused_queries != result$query[candidate_rows]
353 if (any(changed)) {
354 indices <- candidate_rows[changed]
355 vc_values <- as.character(result$vc)
356 vc_values[is.na(vc_values)] <- ""
357
358 result$webUIRequestUrl[indices] <- mapply(
359 function(new_query, vc_value) {
360 buildWebUIRequestUrlFromString(
361 kco@KorAPUrl,
362 new_query,
363 vc = vc_value,
364 ql = "poliqarp"
365 )
366 },
367 focused_queries[changed],
368 vc_values[indices],
369 USE.NAMES = FALSE
370 )
371 }
372 }
373 }
374
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200375 if (!is.null(cacheAs)) {
376 log_info(kco@verbose, sprintf("Saving collocation analysis to cache: %s\n", cacheAs))
377 saveRDS(result, cacheAs)
378 }
379
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200380 result
381 }
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200382)
383
Marc Kupietz76b05592021-12-19 16:26:15 +0100384# #' @export
Marc Kupietz5a336b62021-11-27 17:51:35 +0100385removeWithinSpan <- function(query, withinSpan) {
386 if (withinSpan == "") {
387 return(query)
388 }
389 needle <- sprintf("^\\(contains\\(<%s>, ?(.*)\\){2}$", withinSpan)
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200390 res <- gsub(needle, "\\1", query)
Marc Kupietz5a336b62021-11-27 17:51:35 +0100391 needle <- sprintf("^contains\\(<%s>, ?(.*)\\)$", withinSpan)
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200392 res <- gsub(needle, "\\1", res)
Marc Kupietz5a336b62021-11-27 17:51:35 +0100393 return(res)
394}
395
Marc Kupietzde679ea2025-10-19 13:14:51 +0200396backfill_missing_scores <- function(result,
397 grid,
398 kco,
399 ignoreCollocateCase,
400 ...) {
401 if (!"vc" %in% names(result) || !"node" %in% names(result) || !"collocate" %in% names(result)) {
402 return(result)
403 }
404
405 if (nrow(result) == 0) {
406 return(result)
407 }
408
Marc Kupietz9c53e412026-06-21 12:13:44 +0200409 distinct_pairs <- dplyr::distinct(
410 result,
411 .data$node,
412 .data$collocate
413 )
Marc Kupietzde679ea2025-10-19 13:14:51 +0200414 if (nrow(distinct_pairs) == 0) {
415 return(result)
416 }
417
418 collocates_by_node <- split(as.character(distinct_pairs$collocate), distinct_pairs$node)
419 if (length(collocates_by_node) == 0) {
420 return(result)
421 }
422
423 required_combinations <- unique(as.data.frame(grid[, c("node", "vc", "vcLabel")], drop = FALSE))
424 for (i in seq_len(nrow(required_combinations))) {
425 node_value <- required_combinations$node[i]
426 vc_value <- required_combinations$vc[i]
427
428 collocate_pool <- collocates_by_node[[node_value]]
429 if (is.null(collocate_pool) || length(collocate_pool) == 0) {
430 next
431 }
432
433 existing_idx <- result$node == node_value & result$vc == vc_value
434 existing_collocates <- unique(as.character(result$collocate[existing_idx]))
435 missing_collocates <- setdiff(unique(collocate_pool), existing_collocates)
436 missing_collocates <- missing_collocates[!is.na(missing_collocates) & nzchar(missing_collocates)]
437
438 if (length(missing_collocates) == 0) {
439 next
440 }
441
442 context_rows <- result[result$node == node_value & result$vc == vc_value, , drop = FALSE]
443 if (nrow(context_rows) == 0) {
444 context_rows <- result[result$node == node_value, , drop = FALSE]
445 }
446
447 left_size <- context_rows$leftContextSize[!is.na(context_rows$leftContextSize)][1]
448 if (is.na(left_size) || length(left_size) == 0) {
449 left_size <- result$leftContextSize[!is.na(result$leftContextSize)][1]
450 }
451 if (is.na(left_size) || length(left_size) == 0) {
452 left_size <- 5
453 }
454
455 right_size <- context_rows$rightContextSize[!is.na(context_rows$rightContextSize)][1]
456 if (is.na(right_size) || length(right_size) == 0) {
457 right_size <- result$rightContextSize[!is.na(result$rightContextSize)][1]
458 }
459 if (is.na(right_size) || length(right_size) == 0) {
460 right_size <- 5
461 }
462
463 within_span_value <- ""
464 if ("query" %in% names(context_rows)) {
465 query_candidate <- context_rows$query[!is.na(context_rows$query) & nzchar(context_rows$query)][1]
466 if (!is.na(query_candidate) && nzchar(query_candidate)) {
467 match_one <- regexec("^\\(*contains\\(<([^>]+)>,", query_candidate)
468 matches <- regmatches(query_candidate, match_one)
469 if (length(matches) >= 1 && length(matches[[1]]) >= 2) {
470 within_span_value <- matches[[1]][2]
471 }
472 }
473 }
474
475 new_rows <- collocationScoreQuery(
476 kco,
477 node = node_value,
478 collocate = missing_collocates,
479 vc = vc_value,
480 leftContextSize = left_size,
481 rightContextSize = right_size,
482 ignoreCollocateCase = ignoreCollocateCase,
483 withinSpan = within_span_value,
484 ...
485 )
486
487 if (nrow(new_rows) == 0) {
488 next
489 }
490
491 if (!is.null(required_combinations$vcLabel[i]) && !is.na(required_combinations$vcLabel[i]) && required_combinations$vcLabel[i] != "" && "label" %in% names(new_rows)) {
492 new_rows$label <- required_combinations$vcLabel[i]
493 }
494
495 result <- dplyr::bind_rows(result, new_rows)
496 }
497
498 result
499}
500
Marc Kupietz0a292632025-10-19 14:04:36 +0200501inject_focus_into_query <- function(query) {
502 if (is.null(query) || is.na(query)) {
503 return(query)
504 }
505
506 trimmed <- trimws(query)
507 if (!nzchar(trimmed)) {
508 return(query)
509 }
510
511 if (!grepl("^contains\\(<[^>]+>", trimmed, perl = TRUE)) {
512 return(query)
513 }
514
515 if (grepl("focus\\(", trimmed, perl = TRUE)) {
516 return(query)
517 }
518
519 pattern <- "^contains\\(<([^>]+)>\\s*,\\s*\\((.*)\\)\\)\\s*$"
520 matches <- regexec(pattern, trimmed, perl = TRUE)
521 components <- regmatches(trimmed, matches)
522 if (length(components) == 0 || length(components[[1]]) < 3) {
523 return(query)
524 }
525
526 span <- components[[1]][2]
527 inner <- components[[1]][3]
528 parts <- strsplit(inner, "\\|", perl = TRUE)[[1]]
529 parts <- trimws(parts)
530 parts <- parts[nzchar(parts)]
531
532 if (length(parts) == 0) {
533 return(query)
534 }
535
536 focused <- paste0("focus({", parts, "})")
537 combined <- paste(focused, collapse = " | ")
538
539 sprintf("contains(<%s>, (%s))", span, combined)
540}
541
Marc Kupietz77852b22025-10-19 11:35:34 +0200542add_multi_vc_comparisons <- function(result, missingScoreQuantile = 0.05) {
Marc Kupietz09b1c082026-05-01 14:45:47 +0200543 label <- node <- collocate <- vc <- webUIRequestUrl <- NULL
Marc Kupietzc4540a22025-10-14 17:39:53 +0200544
545 if (!"label" %in% names(result) || dplyr::n_distinct(result$label) < 2) {
546 return(result)
547 }
548
549 numeric_cols <- names(result)[vapply(result, is.numeric, logical(1))]
550 non_score_cols <- c("N", "O", "O1", "O2", "E", "w", "leftContextSize", "rightContextSize", "frequency")
551 score_cols <- setdiff(numeric_cols, non_score_cols)
552
553 if (length(score_cols) == 0) {
554 return(result)
555 }
556
Marc Kupietz9894a372025-10-18 14:51:29 +0200557 compute_score_floor <- function(values) {
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200558 # Estimate a conservative floor so missing scores can be imputed without favoring any label
Marc Kupietz9894a372025-10-18 14:51:29 +0200559 finite_values <- values[is.finite(values)]
560 if (length(finite_values) == 0) {
561 return(0)
562 }
563
564 prob <- min(max(missingScoreQuantile, 0), 0.5)
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200565 # Use a lower quantile as the anchor to stay near the weakest attested scores
Marc Kupietz9894a372025-10-18 14:51:29 +0200566 q_val <- suppressWarnings(stats::quantile(finite_values,
567 probs = prob,
568 names = FALSE,
569 type = 7
570 ))
571
572 if (!is.finite(q_val)) {
573 q_val <- suppressWarnings(min(finite_values, na.rm = TRUE))
574 }
575
576 min_val <- suppressWarnings(min(finite_values, na.rm = TRUE))
577 if (!is.finite(min_val)) {
578 min_val <- 0
579 }
580
581 spread_candidates <- c(
582 suppressWarnings(stats::IQR(finite_values, na.rm = TRUE, type = 7)),
583 stats::sd(finite_values, na.rm = TRUE),
584 abs(q_val) * 0.1,
585 abs(min_val - q_val)
586 )
587 spread_candidates <- spread_candidates[is.finite(spread_candidates)]
588
589 spread <- 0
590 if (length(spread_candidates) > 0) {
591 spread <- max(spread_candidates)
592 }
593 if (!is.finite(spread) || spread == 0) {
594 spread <- max(abs(q_val), abs(min_val), 1e-06)
595 }
596
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200597 # Step away from the anchor by a robust spread estimate to avoid ties with real scores
Marc Kupietz9894a372025-10-18 14:51:29 +0200598 candidate <- q_val - spread
599 if (!is.finite(candidate)) {
600 candidate <- min_val
601 }
602
603 floor_value <- suppressWarnings(min(c(candidate, min_val), na.rm = TRUE))
604 if (!is.finite(floor_value)) {
605 floor_value <- min_val
606 }
607 if (!is.finite(floor_value)) {
608 floor_value <- 0
609 }
610
611 floor_value
612 }
613
614 score_replacements <- stats::setNames(
615 vapply(score_cols, function(col) {
616 compute_score_floor(result[[col]])
617 }, numeric(1)),
618 score_cols
619 )
620
Marc Kupietzc4540a22025-10-14 17:39:53 +0200621 comparison <- result |>
Marc Kupietz28a29842025-10-18 12:25:09 +0200622 dplyr::select(node, collocate, label, dplyr::all_of(score_cols)) |>
623 tidyr::pivot_wider(
Marc Kupietzc4540a22025-10-14 17:39:53 +0200624 names_from = label,
Marc Kupietz28a29842025-10-18 12:25:09 +0200625 values_from = dplyr::all_of(score_cols),
Marc Kupietzc4540a22025-10-14 17:39:53 +0200626 names_glue = "{.value}_{make.names(label)}",
627 values_fn = dplyr::first
628 )
629
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200630 raw_labels <- unique(result$label)
631 labels <- make.names(raw_labels)
632 label_map <- stats::setNames(raw_labels, labels)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200633 vc_map <- result |>
634 dplyr::select(label, vc) |>
635 dplyr::filter(!is.na(label), label != "") |>
636 dplyr::distinct(label, .keep_all = TRUE)
637 vc_map <- stats::setNames(vc_map$vc, make.names(vc_map$label))
638
639 replace_web_ui_cq <- function(url, vc_value) {
640 if (length(url) == 0 || is.na(url) || url == "") {
641 return(NA_character_)
642 }
643 if (length(vc_value) == 0 || is.na(vc_value)) {
644 vc_value <- ""
645 }
646 encoded_vc <- urltools::url_encode(enc2utf8(as.character(vc_value)))
647 if (grepl("([?&]cq=)[^&]*", url, perl = TRUE)) {
648 return(sub("([?&]cq=)[^&]*", paste0("\\1", encoded_vc), url, perl = TRUE))
649 }
650 if (encoded_vc == "") {
651 return(url)
652 }
653 paste0(url, ifelse(grepl("\\?", url), "&", "?"), "cq=", encoded_vc)
654 }
655
656 if ("webUIRequestUrl" %in% names(result)) {
657 url_data <- result |>
658 dplyr::select(node, collocate, label, webUIRequestUrl) |>
659 tidyr::pivot_wider(
660 names_from = label,
661 values_from = webUIRequestUrl,
662 names_glue = "webUIRequestUrl_{make.names(label)}",
663 values_fn = dplyr::first
664 )
665
666 comparison <- dplyr::left_join(comparison, url_data, by = c("node", "collocate"))
667
668 url_cols <- paste0("webUIRequestUrl_", labels)
669 present_url_cols <- intersect(url_cols, names(comparison))
670 fallback_urls <- vapply(seq_len(nrow(comparison)), function(i) {
671 urls <- unlist(comparison[i, present_url_cols, drop = FALSE], use.names = FALSE)
672 urls <- as.character(urls)
673 urls <- urls[!is.na(urls) & urls != ""]
674 if (length(urls) == 0) {
675 NA_character_
676 } else {
677 urls[1]
678 }
679 }, character(1))
680
681 for (safe_label in labels) {
682 url_col <- paste0("webUIRequestUrl_", safe_label)
683 if (!url_col %in% names(comparison)) {
684 comparison[[url_col]] <- NA_character_
685 }
686 missing_urls <- is.na(comparison[[url_col]]) | comparison[[url_col]] == ""
687 if (any(missing_urls)) {
688 comparison[[url_col]][missing_urls] <- vapply(
689 fallback_urls[missing_urls],
690 replace_web_ui_cq,
691 character(1),
692 vc_value = vc_map[[safe_label]]
693 )
694 }
695 }
696 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200697
Marc Kupietz28a29842025-10-18 12:25:09 +0200698 rank_data <- result |>
699 dplyr::distinct(node, collocate)
700
701 for (i in seq_along(raw_labels)) {
702 raw_lab <- raw_labels[i]
703 safe_lab <- labels[i]
704 label_df <- result[result$label == raw_lab, c("node", "collocate", score_cols), drop = FALSE]
705 if (nrow(label_df) == 0) {
706 next
707 }
708 label_df <- dplyr::distinct(label_df)
709 rank_tbl <- label_df[, c("node", "collocate"), drop = FALSE]
710 for (col in score_cols) {
711 rank_col_name <- paste0("rank_", safe_lab, "_", col)
Marc Kupietz130a2a22025-10-18 16:09:23 +0200712 percentile_col_name <- paste0("percentile_rank_", safe_lab, "_", col)
Marc Kupietz28a29842025-10-18 12:25:09 +0200713 values <- label_df[[col]]
714 ranks <- rep(NA_real_, length(values))
Marc Kupietz130a2a22025-10-18 16:09:23 +0200715 percentiles <- rep(NA_real_, length(values))
Marc Kupietz28a29842025-10-18 12:25:09 +0200716 valid_idx <- which(!is.na(values))
717 if (length(valid_idx) > 0) {
718 ranks[valid_idx] <- rank(-values[valid_idx], ties.method = "first")
Marc Kupietz130a2a22025-10-18 16:09:23 +0200719 total <- length(valid_idx)
720 percentiles[valid_idx] <- 1 - (ranks[valid_idx] - 1) / total
Marc Kupietz28a29842025-10-18 12:25:09 +0200721 }
722 rank_tbl[[rank_col_name]] <- ranks
Marc Kupietz130a2a22025-10-18 16:09:23 +0200723 rank_tbl[[percentile_col_name]] <- percentiles
Marc Kupietz28a29842025-10-18 12:25:09 +0200724 }
725 rank_data <- dplyr::left_join(rank_data, rank_tbl, by = c("node", "collocate"))
726 }
727
728 comparison <- dplyr::left_join(comparison, rank_data, by = c("node", "collocate"))
729
Marc Kupietzd7bb5cb2026-08-31 10:18:02 +0200730 # Record which label/measure cells are absent *before* any imputation happens below.
731 # Deltas computed from imputed cells reflect presence/absence of the collocate in a
732 # virtual corpus, not a measured contrast, so users need to be able to tell them apart.
733 imputed_flags <- lapply(labels, function(safe_label) {
734 label_score_cols <- intersect(paste0(score_cols, "_", safe_label), names(comparison))
735 if (length(label_score_cols) == 0) {
736 return(rep(FALSE, nrow(comparison)))
737 }
738 Reduce(`|`, lapply(label_score_cols, function(col) is.na(comparison[[col]])))
739 })
740 names(imputed_flags) <- paste0("imputed_", labels)
741
Marc Kupietz28a29842025-10-18 12:25:09 +0200742 rank_replacements <- numeric(0)
743 rank_column_names <- grep("^rank_", names(comparison), value = TRUE)
744 if (length(rank_column_names) > 0) {
745 rank_replacements <- stats::setNames(
746 vapply(rank_column_names, function(col) {
747 col_values <- comparison[[col]]
748 valid_values <- col_values[!is.na(col_values)]
749 if (length(valid_values) == 0) {
750 nrow(comparison) + 1
751 } else {
752 suppressWarnings(max(valid_values, na.rm = TRUE)) + 1
753 }
754 }, numeric(1)),
755 rank_column_names
756 )
757 }
758
Marc Kupietz130a2a22025-10-18 16:09:23 +0200759 percentile_replacements <- numeric(0)
760 percentile_column_names <- grep("^percentile_rank_", names(comparison), value = TRUE)
761 if (length(percentile_column_names) > 0) {
762 percentile_replacements <- stats::setNames(
763 rep(0, length(percentile_column_names)),
764 percentile_column_names
765 )
766 }
767
Marc Kupietz28a29842025-10-18 12:25:09 +0200768 collapse_label_values <- function(indices, safe_labels_vec) {
769 if (length(indices) == 0) {
770 return(NA_character_)
771 }
772 labs <- label_map[safe_labels_vec[indices]]
773 fallback <- safe_labels_vec[indices]
774 labs[is.na(labs) | labs == ""] <- fallback[is.na(labs) | labs == ""]
775 labs <- labs[!is.na(labs) & labs != ""]
776 if (length(labs) == 0) {
777 return(NA_character_)
778 }
779 paste(unique(labs), collapse = ", ")
780 }
781
Marc Kupietz09b1c082026-05-01 14:45:47 +0200782 collapse_url_values <- function(indices, url_values) {
783 if (length(indices) == 0 || is.null(url_values)) {
784 return(NA_character_)
785 }
786 urls <- as.character(url_values[indices])
787 urls <- urls[!is.na(urls) & urls != ""]
788 if (length(urls) == 0) {
789 return(NA_character_)
790 }
791 paste(unique(urls), collapse = ", ")
792 }
793
Marc Kupietzc4540a22025-10-14 17:39:53 +0200794 if (length(labels) == 2) {
Marc Kupietz9894a372025-10-18 14:51:29 +0200795 fill_scores <- function(x, y, measure_col) {
796 replacement <- score_replacements[[measure_col]]
797 fallback_min <- suppressWarnings(min(c(x, y), na.rm = TRUE))
798 if (!is.finite(fallback_min)) {
799 fallback_min <- 0
Marc Kupietzc4540a22025-10-14 17:39:53 +0200800 }
Marc Kupietz9894a372025-10-18 14:51:29 +0200801 if (!is.null(replacement) && is.finite(replacement)) {
802 replacement <- min(replacement, fallback_min)
803 } else {
804 replacement <- fallback_min
805 }
806 if (!is.finite(replacement)) {
807 replacement <- 0
808 }
809 if (any(is.na(x))) {
810 x[is.na(x)] <- replacement
811 }
812 if (any(is.na(y))) {
813 y[is.na(y)] <- replacement
814 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200815 list(x = x, y = y)
816 }
817
Marc Kupietz130a2a22025-10-18 16:09:23 +0200818 fill_percentiles <- function(x, y, left_pct_col, right_pct_col) {
819 replacement_left <- percentile_replacements[[left_pct_col]]
820 if (is.null(replacement_left) || !is.finite(replacement_left)) {
821 replacement_left <- 0
822 }
823 replacement_right <- percentile_replacements[[right_pct_col]]
824 if (is.null(replacement_right) || !is.finite(replacement_right)) {
825 replacement_right <- 0
826 }
827 if (any(is.na(x))) {
828 x[is.na(x)] <- replacement_left
829 }
830 if (any(is.na(y))) {
831 y[is.na(y)] <- replacement_right
832 }
833 list(x = x, y = y)
834 }
835
Marc Kupietz28a29842025-10-18 12:25:09 +0200836 fill_ranks <- function(x, y, left_rank_col, right_rank_col) {
837 fallback <- nrow(comparison) + 1
838 replacement_left <- rank_replacements[[left_rank_col]]
839 if (is.null(replacement_left) || !is.finite(replacement_left)) {
840 replacement_left <- fallback
Marc Kupietzc4540a22025-10-14 17:39:53 +0200841 }
Marc Kupietz28a29842025-10-18 12:25:09 +0200842 replacement_right <- rank_replacements[[right_rank_col]]
843 if (is.null(replacement_right) || !is.finite(replacement_right)) {
844 replacement_right <- fallback
845 }
846 if (any(is.na(x))) {
847 x[is.na(x)] <- replacement_left
848 }
849 if (any(is.na(y))) {
850 y[is.na(y)] <- replacement_right
851 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200852 list(x = x, y = y)
853 }
854
855 left_label <- labels[1]
856 right_label <- labels[2]
857
858 for (col in score_cols) {
859 left_col <- paste0(col, "_", left_label)
860 right_col <- paste0(col, "_", right_label)
861 if (!all(c(left_col, right_col) %in% names(comparison))) {
862 next
863 }
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200864 filled <- fill_scores(comparison[[left_col]], comparison[[right_col]], col)
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200865 comparison[[left_col]] <- filled$x
866 comparison[[right_col]] <- filled$y
Marc Kupietzc4540a22025-10-14 17:39:53 +0200867 comparison[[paste0("delta_", col)]] <- filled$x - filled$y
Marc Kupietz28a29842025-10-18 12:25:09 +0200868 rank_left <- paste0("rank_", left_label, "_", col)
869 rank_right <- paste0("rank_", right_label, "_", col)
870 if (all(c(rank_left, rank_right) %in% names(comparison))) {
871 filled_rank <- fill_ranks(
872 comparison[[rank_left]],
873 comparison[[rank_right]],
874 rank_left,
875 rank_right
876 )
877 comparison[[paste0("delta_rank_", col)]] <- filled_rank$x - filled_rank$y
878 }
Marc Kupietz130a2a22025-10-18 16:09:23 +0200879 pct_left <- paste0("percentile_rank_", left_label, "_", col)
880 pct_right <- paste0("percentile_rank_", right_label, "_", col)
881 if (all(c(pct_left, pct_right) %in% names(comparison))) {
882 filled_pct <- fill_percentiles(
883 comparison[[pct_left]],
884 comparison[[pct_right]],
885 pct_left,
886 pct_right
887 )
888 comparison[[paste0("delta_percentile_rank_", col)]] <- filled_pct$x - filled_pct$y
889 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200890 }
891 }
892
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200893 for (col in score_cols) {
894 value_cols <- paste0(col, "_", labels)
895 existing <- value_cols %in% names(comparison)
896 if (!any(existing)) {
897 next
898 }
899 value_cols <- value_cols[existing]
900 safe_labels <- labels[existing]
901
902 score_values <- comparison[, value_cols, drop = FALSE]
903
904 winner_label_col <- paste0("winner_", col)
905 winner_value_col <- paste0("winner_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +0200906 winner_url_col <- paste0("winner_", col, "_webUIRequestUrl")
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200907 runner_label_col <- paste0("runner_up_", col)
908 runner_value_col <- paste0("runner_up_", col, "_value")
Marc Kupietzb2862d42025-10-18 10:17:49 +0200909 loser_label_col <- paste0("loser_", col)
910 loser_value_col <- paste0("loser_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +0200911 loser_url_col <- paste0("loser_", col, "_webUIRequestUrl")
Marc Kupietzb2862d42025-10-18 10:17:49 +0200912 max_delta_col <- paste0("max_delta_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200913 url_cols <- paste0("webUIRequestUrl_", safe_labels)
914 has_urls <- all(url_cols %in% names(comparison))
915 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200916
917 if (nrow(score_values) == 0) {
918 comparison[[winner_label_col]] <- character(0)
919 comparison[[winner_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200920 if (has_urls) {
921 comparison[[winner_url_col]] <- character(0)
922 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200923 comparison[[runner_label_col]] <- character(0)
924 comparison[[runner_value_col]] <- numeric(0)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200925 comparison[[loser_label_col]] <- character(0)
926 comparison[[loser_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200927 if (has_urls) {
928 comparison[[loser_url_col]] <- character(0)
929 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200930 comparison[[max_delta_col]] <- numeric(0)
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200931 next
932 }
933
934 score_matrix <- as.matrix(score_values)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200935 storage.mode(score_matrix) <- "numeric"
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200936
Marc Kupietzb2862d42025-10-18 10:17:49 +0200937 n_rows <- nrow(score_matrix)
938 winner_labels <- rep(NA_character_, n_rows)
939 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200940 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200941 runner_labels <- rep(NA_character_, n_rows)
942 runner_values <- rep(NA_real_, n_rows)
943 loser_labels <- rep(NA_character_, n_rows)
944 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200945 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200946 max_deltas <- rep(NA_real_, n_rows)
947
Marc Kupietzb2862d42025-10-18 10:17:49 +0200948 if (n_rows > 0) {
949 for (i in seq_len(n_rows)) {
950 numeric_row <- as.numeric(score_matrix[i, ])
951 if (all(is.na(numeric_row))) {
952 next
953 }
954
Marc Kupietz9894a372025-10-18 14:51:29 +0200955 replacement <- score_replacements[[col]]
956 fallback_min <- suppressWarnings(min(numeric_row, na.rm = TRUE))
957 if (!is.finite(fallback_min)) {
958 fallback_min <- 0
Marc Kupietzb2862d42025-10-18 10:17:49 +0200959 }
Marc Kupietz9894a372025-10-18 14:51:29 +0200960 if (!is.null(replacement) && is.finite(replacement)) {
961 replacement <- min(replacement, fallback_min)
962 } else {
963 replacement <- fallback_min
964 }
965 if (!is.finite(replacement)) {
966 replacement <- 0
967 }
968 if (any(is.na(numeric_row))) {
969 numeric_row[is.na(numeric_row)] <- replacement
970 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200971 score_matrix[i, ] <- numeric_row
972
973 max_val <- suppressWarnings(max(numeric_row, na.rm = TRUE))
974 max_idx <- which(numeric_row == max_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200975 winner_labels[i] <- collapse_label_values(max_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200976 winner_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +0200977 if (has_urls) {
978 winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ])
979 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200980
981 unique_vals <- sort(unique(numeric_row), decreasing = TRUE)
982 if (length(unique_vals) >= 2) {
983 runner_val <- unique_vals[2]
984 runner_idx <- which(numeric_row == runner_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200985 runner_labels[i] <- collapse_label_values(runner_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200986 runner_values[i] <- runner_val
987 }
988
989 min_val <- suppressWarnings(min(numeric_row, na.rm = TRUE))
990 min_idx <- which(numeric_row == min_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200991 loser_labels[i] <- collapse_label_values(min_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200992 loser_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +0200993 if (has_urls) {
994 loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ])
995 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200996
997 if (is.finite(max_val) && is.finite(min_val)) {
998 max_deltas[i] <- max_val - min_val
999 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001000 }
Marc Kupietzb2862d42025-10-18 10:17:49 +02001001 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001002
Marc Kupietzb2862d42025-10-18 10:17:49 +02001003 comparison[, value_cols] <- score_matrix
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001004 comparison[[winner_label_col]] <- winner_labels
1005 comparison[[winner_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001006 if (has_urls) {
1007 comparison[[winner_url_col]] <- winner_urls
1008 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001009 comparison[[runner_label_col]] <- runner_labels
1010 comparison[[runner_value_col]] <- runner_values
Marc Kupietzb2862d42025-10-18 10:17:49 +02001011 comparison[[loser_label_col]] <- loser_labels
1012 comparison[[loser_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001013 if (has_urls) {
1014 comparison[[loser_url_col]] <- loser_urls
1015 }
Marc Kupietzb2862d42025-10-18 10:17:49 +02001016 comparison[[max_delta_col]] <- max_deltas
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001017 }
1018
Marc Kupietz28a29842025-10-18 12:25:09 +02001019 for (col in score_cols) {
1020 rank_cols <- paste0("rank_", labels, "_", col)
1021 existing <- rank_cols %in% names(comparison)
1022 if (!any(existing)) {
1023 next
1024 }
1025 rank_cols <- rank_cols[existing]
1026 safe_labels <- labels[existing]
1027 rank_values <- comparison[, rank_cols, drop = FALSE]
1028
1029 winner_rank_label_col <- paste0("winner_rank_", col)
1030 winner_rank_value_col <- paste0("winner_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001031 winner_rank_url_col <- paste0("winner_rank_", col, "_webUIRequestUrl")
Marc Kupietz28a29842025-10-18 12:25:09 +02001032 runner_rank_label_col <- paste0("runner_up_rank_", col)
1033 runner_rank_value_col <- paste0("runner_up_rank_", col, "_value")
1034 loser_rank_label_col <- paste0("loser_rank_", col)
1035 loser_rank_value_col <- paste0("loser_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001036 loser_rank_url_col <- paste0("loser_rank_", col, "_webUIRequestUrl")
Marc Kupietz28a29842025-10-18 12:25:09 +02001037 max_delta_rank_col <- paste0("max_delta_rank_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001038 url_cols <- paste0("webUIRequestUrl_", safe_labels)
1039 has_urls <- all(url_cols %in% names(comparison))
1040 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz28a29842025-10-18 12:25:09 +02001041
1042 if (nrow(rank_values) == 0) {
1043 comparison[[winner_rank_label_col]] <- character(0)
1044 comparison[[winner_rank_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001045 if (has_urls) {
1046 comparison[[winner_rank_url_col]] <- character(0)
1047 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001048 comparison[[runner_rank_label_col]] <- character(0)
1049 comparison[[runner_rank_value_col]] <- numeric(0)
1050 comparison[[loser_rank_label_col]] <- character(0)
1051 comparison[[loser_rank_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001052 if (has_urls) {
1053 comparison[[loser_rank_url_col]] <- character(0)
1054 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001055 comparison[[max_delta_rank_col]] <- numeric(0)
1056 next
1057 }
1058
Marc Kupietzdb2fabd2026-04-27 15:01:37 +02001059 rank_matrix <- as.matrix(rank_values)
1060 storage.mode(rank_matrix) <- "numeric"
Marc Kupietz28a29842025-10-18 12:25:09 +02001061
1062 n_rows <- nrow(rank_matrix)
1063 winner_labels <- rep(NA_character_, n_rows)
1064 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001065 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietz28a29842025-10-18 12:25:09 +02001066 runner_labels <- rep(NA_character_, n_rows)
1067 runner_values <- rep(NA_real_, n_rows)
1068 loser_labels <- rep(NA_character_, n_rows)
1069 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001070 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietz28a29842025-10-18 12:25:09 +02001071 max_deltas <- rep(NA_real_, n_rows)
1072
1073 for (i in seq_len(n_rows)) {
1074 numeric_row <- as.numeric(rank_matrix[i, ])
1075 if (all(is.na(numeric_row))) {
1076 next
1077 }
1078
1079 if (length(rank_cols) > 0) {
1080 replacement_vec <- rank_replacements[rank_cols]
1081 replacement_vec[is.na(replacement_vec)] <- nrow(comparison) + 1
1082 missing_idx <- which(is.na(numeric_row))
1083 if (length(missing_idx) > 0) {
1084 numeric_row[missing_idx] <- replacement_vec[missing_idx]
1085 }
1086 }
1087
1088 valid_idx <- seq_along(numeric_row)
1089 valid_values <- numeric_row[valid_idx]
1090 min_val <- suppressWarnings(min(valid_values, na.rm = TRUE))
1091 min_positions <- valid_idx[which(valid_values == min_val)]
1092 winner_labels[i] <- collapse_label_values(min_positions, safe_labels)
1093 winner_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001094 if (has_urls) {
1095 winner_urls[i] <- collapse_url_values(min_positions, url_values[i, ])
1096 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001097
1098 ordered_vals <- sort(unique(valid_values), decreasing = FALSE)
1099 if (length(ordered_vals) >= 2) {
1100 runner_val <- ordered_vals[2]
1101 runner_positions <- valid_idx[which(valid_values == runner_val)]
1102 runner_labels[i] <- collapse_label_values(runner_positions, safe_labels)
1103 runner_values[i] <- runner_val
1104 }
1105
1106 max_val <- suppressWarnings(max(valid_values, na.rm = TRUE))
1107 max_positions <- valid_idx[which(valid_values == max_val)]
1108 loser_labels[i] <- collapse_label_values(max_positions, safe_labels)
1109 loser_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001110 if (has_urls) {
1111 loser_urls[i] <- collapse_url_values(max_positions, url_values[i, ])
1112 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001113
1114 if (is.finite(max_val) && is.finite(min_val)) {
1115 max_deltas[i] <- max_val - min_val
1116 }
1117 }
1118
1119 comparison[[winner_rank_label_col]] <- winner_labels
1120 comparison[[winner_rank_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001121 if (has_urls) {
1122 comparison[[winner_rank_url_col]] <- winner_urls
1123 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001124 comparison[[runner_rank_label_col]] <- runner_labels
1125 comparison[[runner_rank_value_col]] <- runner_values
1126 comparison[[loser_rank_label_col]] <- loser_labels
1127 comparison[[loser_rank_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001128 if (has_urls) {
1129 comparison[[loser_rank_url_col]] <- loser_urls
1130 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001131 comparison[[max_delta_rank_col]] <- max_deltas
1132 }
1133
Marc Kupietz130a2a22025-10-18 16:09:23 +02001134 for (col in score_cols) {
1135 pct_cols <- paste0("percentile_rank_", labels, "_", col)
1136 existing <- pct_cols %in% names(comparison)
1137 if (!any(existing)) {
1138 next
1139 }
1140 pct_cols <- pct_cols[existing]
1141 safe_labels <- labels[existing]
1142 pct_values <- comparison[, pct_cols, drop = FALSE]
1143
1144 winner_pct_label_col <- paste0("winner_percentile_rank_", col)
1145 winner_pct_value_col <- paste0("winner_percentile_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001146 winner_pct_url_col <- paste0("winner_percentile_rank_", col, "_webUIRequestUrl")
Marc Kupietz130a2a22025-10-18 16:09:23 +02001147 runner_pct_label_col <- paste0("runner_up_percentile_rank_", col)
1148 runner_pct_value_col <- paste0("runner_up_percentile_rank_", col, "_value")
1149 loser_pct_label_col <- paste0("loser_percentile_rank_", col)
1150 loser_pct_value_col <- paste0("loser_percentile_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001151 loser_pct_url_col <- paste0("loser_percentile_rank_", col, "_webUIRequestUrl")
Marc Kupietz130a2a22025-10-18 16:09:23 +02001152 max_delta_pct_col <- paste0("max_delta_percentile_rank_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001153 url_cols <- paste0("webUIRequestUrl_", safe_labels)
1154 has_urls <- all(url_cols %in% names(comparison))
1155 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz130a2a22025-10-18 16:09:23 +02001156
1157 if (nrow(pct_values) == 0) {
1158 comparison[[winner_pct_label_col]] <- character(0)
1159 comparison[[winner_pct_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001160 if (has_urls) {
1161 comparison[[winner_pct_url_col]] <- character(0)
1162 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001163 comparison[[runner_pct_label_col]] <- character(0)
1164 comparison[[runner_pct_value_col]] <- numeric(0)
1165 comparison[[loser_pct_label_col]] <- character(0)
1166 comparison[[loser_pct_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001167 if (has_urls) {
1168 comparison[[loser_pct_url_col]] <- character(0)
1169 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001170 comparison[[max_delta_pct_col]] <- numeric(0)
1171 next
1172 }
1173
1174 pct_matrix <- as.matrix(pct_values)
1175 storage.mode(pct_matrix) <- "numeric"
1176
1177 n_rows <- nrow(pct_matrix)
1178 winner_labels <- rep(NA_character_, n_rows)
1179 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001180 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietz130a2a22025-10-18 16:09:23 +02001181 runner_labels <- rep(NA_character_, n_rows)
1182 runner_values <- rep(NA_real_, n_rows)
1183 loser_labels <- rep(NA_character_, n_rows)
1184 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001185 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietz130a2a22025-10-18 16:09:23 +02001186 max_deltas <- rep(NA_real_, n_rows)
1187
1188 if (n_rows > 0) {
1189 for (i in seq_len(n_rows)) {
1190 numeric_row <- as.numeric(pct_matrix[i, ])
1191 if (all(is.na(numeric_row))) {
1192 next
1193 }
1194
1195 if (any(is.na(numeric_row))) {
1196 numeric_row[is.na(numeric_row)] <- 0
1197 }
1198 pct_matrix[i, ] <- numeric_row
1199
1200 max_val <- suppressWarnings(max(numeric_row, na.rm = TRUE))
1201 max_idx <- which(numeric_row == max_val)
1202 winner_labels[i] <- collapse_label_values(max_idx, safe_labels)
1203 winner_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001204 if (has_urls) {
1205 winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ])
1206 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001207
1208 unique_vals <- sort(unique(numeric_row), decreasing = TRUE)
1209 if (length(unique_vals) >= 2) {
1210 runner_val <- unique_vals[2]
1211 runner_idx <- which(numeric_row == runner_val)
1212 runner_labels[i] <- collapse_label_values(runner_idx, safe_labels)
1213 runner_values[i] <- runner_val
1214 }
1215
1216 min_val <- suppressWarnings(min(numeric_row, na.rm = TRUE))
1217 min_idx <- which(numeric_row == min_val)
1218 loser_labels[i] <- collapse_label_values(min_idx, safe_labels)
1219 loser_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001220 if (has_urls) {
1221 loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ])
1222 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001223
1224 if (is.finite(max_val) && is.finite(min_val)) {
1225 max_deltas[i] <- max_val - min_val
1226 }
1227 }
1228 }
1229
1230 comparison[, pct_cols] <- pct_matrix
1231 comparison[[winner_pct_label_col]] <- winner_labels
1232 comparison[[winner_pct_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001233 if (has_urls) {
1234 comparison[[winner_pct_url_col]] <- winner_urls
1235 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001236 comparison[[runner_pct_label_col]] <- runner_labels
1237 comparison[[runner_pct_value_col]] <- runner_values
1238 comparison[[loser_pct_label_col]] <- loser_labels
1239 comparison[[loser_pct_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001240 if (has_urls) {
1241 comparison[[loser_pct_url_col]] <- loser_urls
1242 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001243 comparison[[max_delta_pct_col]] <- max_deltas
1244 }
1245
Marc Kupietzd7bb5cb2026-08-31 10:18:02 +02001246 for (flag_col in names(imputed_flags)) {
1247 comparison[[flag_col]] <- imputed_flags[[flag_col]]
1248 }
1249 if (length(imputed_flags) > 0) {
1250 comparison$n_imputed <- as.integer(Reduce(`+`, lapply(imputed_flags, as.integer)))
1251 } else {
1252 comparison$n_imputed <- rep(0L, nrow(comparison))
1253 }
1254 comparison$imputed <- comparison$n_imputed > 0L
1255
Marc Kupietz09b1c082026-05-01 14:45:47 +02001256 collapse_consensus_url_columns <- function(url_cols) {
1257 if (length(url_cols) == 0) {
1258 return(rep(NA_character_, nrow(comparison)))
1259 }
1260 vapply(seq_len(nrow(comparison)), function(i) {
1261 urls <- unlist(comparison[i, url_cols, drop = FALSE], use.names = FALSE)
1262 urls <- as.character(urls)
1263 urls <- urls[!is.na(urls) & urls != ""]
1264 urls <- unique(urls)
1265 if (length(urls) == 1) {
1266 urls
1267 } else {
1268 NA_character_
1269 }
1270 }, character(1))
1271 }
1272
1273 winner_score_url_cols <- intersect(paste0("winner_", score_cols, "_webUIRequestUrl"), names(comparison))
1274 loser_score_url_cols <- intersect(paste0("loser_", score_cols, "_webUIRequestUrl"), names(comparison))
1275 if (length(winner_score_url_cols) > 0) {
1276 comparison$winner_webUIRequestUrl <- collapse_consensus_url_columns(winner_score_url_cols)
1277 }
1278 if (length(loser_score_url_cols) > 0) {
1279 comparison$loser_webUIRequestUrl <- collapse_consensus_url_columns(loser_score_url_cols)
1280 }
1281
1282 url_helper_cols <- intersect(paste0("webUIRequestUrl_", labels), names(comparison))
1283 if (length(url_helper_cols) > 0) {
1284 comparison <- dplyr::select(comparison, -dplyr::all_of(url_helper_cols))
1285 }
1286
Marc Kupietzc4540a22025-10-14 17:39:53 +02001287 dplyr::left_join(result, comparison, by = c("node", "collocate"))
1288}
1289
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001290#' @importFrom magrittr debug_pipe
Marc Kupietz2b17b212023-08-27 17:47:26 +02001291#' @importFrom stringr str_detect
1292#' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when
1293#'
1294matches2FreqTable <- function(matches,
1295 index = 0,
1296 minOccur = 5,
1297 leftContextSize = 5,
1298 rightContextSize = 5,
1299 ignoreCollocateCase = FALSE,
1300 stopwords = c(),
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001301 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietz2b17b212023-08-27 17:47:26 +02001302 oldTable = data.frame(word = rep(NA, 1), frequency = rep(NA, 1)),
1303 verbose = TRUE) {
1304 word <- NULL # https://stackoverflow.com/questions/8096313/no-visible-binding-for-global-variable-note-in-r-cmd-check
1305 frequency <- NULL
1306
1307 if (nrow(matches) < 1) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001308 dplyr::tibble(word = c(), frequency = c())
Marc Kupietz2b17b212023-08-27 17:47:26 +02001309 } else if (index == 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001310 if (!"tokens" %in% colnames(matches) || !is.list(matches$tokens)) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001311 log_info(verbose, "Outdated KorAP server: Falling back to client side tokenization.\n")
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001312 return(snippet2FreqTable(matches$snippet, minOccur, leftContextSize, rightContextSize,
1313 ignoreCollocateCase = ignoreCollocateCase,
1314 stopwords = stopwords, oldTable = oldTable, verbose = verbose
1315 ))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001316 }
1317 log_info(verbose, paste("Joining", nrow(matches), "kwics\n"))
Marc Kupietza25fbd92025-10-14 17:38:09 +02001318 for (i in seq_len(nrow(matches))) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001319 oldTable <- matches2FreqTable(
1320 matches,
1321 i,
1322 leftContextSize = leftContextSize,
1323 rightContextSize = rightContextSize,
1324 collocateFilterRegex = collocateFilterRegex,
1325 oldTable = oldTable,
1326 stopwords = stopwords
1327 )
1328 }
1329 log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n"))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001330 oldTable |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001331 group_by(word) |>
1332 mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001333 summarise(frequency = sum(frequency), .groups = "drop") |>
Marc Kupietz2b17b212023-08-27 17:47:26 +02001334 arrange(desc(frequency))
1335 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001336 stopwordsTable <- dplyr::tibble(word = stopwords)
Marc Kupietz2b17b212023-08-27 17:47:26 +02001337
1338 left <- tail(unlist(matches$tokens$left[index]), leftContextSize)
1339
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001340 # cat(paste("left:", left, "\n", collapse=" "))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001341
1342 right <- head(unlist(matches$tokens$right[index]), rightContextSize)
1343
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001344 # cat(paste("right:", right, "\n", collapse=" "))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001345
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001346 if (length(left) + length(right) == 0) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001347 oldTable
1348 } else {
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001349 table(c(left, right)) |>
1350 dplyr::as_tibble(.name_repair = "minimal") |>
1351 dplyr::rename(word = 1, frequency = 2) |>
1352 dplyr::filter(str_detect(word, collocateFilterRegex)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001353 dplyr::anti_join(stopwordsTable, by = "word") |>
Marc Kupietz2b17b212023-08-27 17:47:26 +02001354 dplyr::bind_rows(oldTable)
1355 }
1356 }
1357}
1358
1359#' @importFrom magrittr debug_pipe
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001360#' @importFrom stringr str_match str_split str_detect
1361#' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when
1362#'
1363snippet2FreqTable <- function(snippet,
1364 minOccur = 5,
1365 leftContextSize = 5,
1366 rightContextSize = 5,
1367 ignoreCollocateCase = FALSE,
1368 stopwords = c(),
1369 tokenizeRegex = "([! )(\uc2\uab,.:?\u201e\u201c\'\"]+|&quot;)",
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001370 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001371 oldTable = data.frame(word = rep(NA, 1), frequency = rep(NA, 1)),
1372 verbose = TRUE) {
1373 word <- NULL # https://stackoverflow.com/questions/8096313/no-visible-binding-for-global-variable-note-in-r-cmd-check
1374 frequency <- NULL
1375
1376 if (length(snippet) < 1) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001377 dplyr::tibble(word = c(), frequency = c())
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001378 } else if (length(snippet) > 1) {
Marc Kupietza47d1502023-04-18 15:26:47 +02001379 log_info(verbose, paste("Joining", length(snippet), "kwics\n"))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001380 for (s in snippet) {
1381 oldTable <- snippet2FreqTable(
1382 s,
1383 leftContextSize = leftContextSize,
1384 rightContextSize = rightContextSize,
Marc Kupietz47d0d2b2021-12-19 16:38:52 +01001385 collocateFilterRegex = collocateFilterRegex,
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001386 oldTable = oldTable,
1387 stopwords = stopwords
1388 )
1389 }
Marc Kupietza47d1502023-04-18 15:26:47 +02001390 log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n"))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001391 oldTable |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001392 group_by(word) |>
1393 mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001394 summarise(frequency = sum(frequency), .groups = "drop") |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001395 arrange(desc(frequency))
1396 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001397 stopwordsTable <- dplyr::tibble(word = stopwords)
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001398 match <-
1399 str_match(
1400 snippet,
1401 '<span class="context-left">(<span class="more"></span>)?(.*[^ ]) *</span><span class="match"><mark>.*</mark></span><span class="context-right"> *([^<]*)'
1402 )
1403
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001404 left <- if (leftContextSize > 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001405 tail(unlist(str_split(match[1, 3], tokenizeRegex)), leftContextSize)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001406 } else {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001407 ""
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001408 }
1409 # cat(paste("left:", left, "\n", collapse=" "))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001410
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001411 right <- if (rightContextSize > 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001412 head(unlist(str_split(match[1, 4], tokenizeRegex)), rightContextSize)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001413 } else {
1414 ""
1415 }
1416 # cat(paste("right:", right, "\n", collapse=" "))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001417
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001418 if (is.na(left[1]) || is.na(right[1]) || length(left) + length(right) == 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001419 oldTable
1420 } else {
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001421 table(c(left, right)) |>
1422 dplyr::as_tibble(.name_repair = "minimal") |>
1423 dplyr::rename(word = 1, frequency = 2) |>
1424 dplyr::filter(str_detect(word, collocateFilterRegex)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001425 dplyr::anti_join(stopwordsTable, by = "word") |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001426 dplyr::bind_rows(oldTable)
1427 }
1428 }
1429}
1430
1431#' Preliminary synsemantic stopwords function
1432#'
1433#' @description
Marc Kupietz67edcb52021-09-20 21:54:24 +02001434#' `r lifecycle::badge("experimental")`
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001435#'
1436#' Preliminary synsemantic stopwords function to be used in collocation analysis.
1437#'
1438#' @details
1439#' Currently only suitable for German. See stopwords package for other languages.
1440#'
1441#' @param ... future arguments for language detection
1442#'
1443#' @family collocation analysis functions
1444#' @return Vector of synsemantic stopwords.
1445#' @export
1446synsemanticStopwords <- function(...) {
Marc Kupietzc79155b2025-10-19 13:42:55 +02001447 base <- c(
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001448 "der",
1449 "die",
1450 "und",
1451 "in",
1452 "den",
1453 "von",
1454 "mit",
1455 "das",
1456 "zu",
1457 "im",
1458 "ist",
1459 "auf",
1460 "sich",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001461 "des",
1462 "dem",
1463 "nicht",
1464 "ein",
1465 "eine",
1466 "es",
1467 "auch",
1468 "an",
1469 "als",
1470 "am",
1471 "aus",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001472 "bei",
1473 "er",
1474 "dass",
1475 "sie",
1476 "nach",
1477 "um",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001478 "zum",
1479 "noch",
1480 "war",
1481 "einen",
1482 "einer",
1483 "wie",
1484 "einem",
1485 "vor",
1486 "bis",
1487 "\u00fcber",
1488 "so",
1489 "aber",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001490 "diese",
Marc Kupietzc79155b2025-10-19 13:42:55 +02001491 "oder"
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001492 )
Marc Kupietzc79155b2025-10-19 13:42:55 +02001493
1494 lower <- unique(tolower(base))
1495 capitalized <- paste0(toupper(substr(lower, 1, 1)), substring(lower, 2))
1496
1497 unique(c(lower, capitalized))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001498}
1499
Marc Kupietz5a336b62021-11-27 17:51:35 +01001500
Marc Kupietz76b05592021-12-19 16:26:15 +01001501# #' @export
Marc Kupietz5a336b62021-11-27 17:51:35 +01001502findExample <-
1503 function(kco,
1504 query,
1505 vc = "",
1506 matchOnly = TRUE) {
1507 out <- character(length = length(query))
1508
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001509 if (length(vc) < length(query)) {
Marc Kupietz5a336b62021-11-27 17:51:35 +01001510 vc <- rep(vc, length(query))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001511 }
Marc Kupietz5a336b62021-11-27 17:51:35 +01001512
1513 for (i in seq_along(query)) {
1514 q <- corpusQuery(kco, paste0("(", query[i], ")"), vc = vc[i], metadataOnly = FALSE)
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001515 if (q@totalResults > 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001516 q <- fetchNext(q, maxFetch = 50, randomizePageOrder = F)
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001517 example <- as.character((q@collectedMatches)$snippet[1])
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001518 out[i] <- if (matchOnly) {
1519 gsub(".*<mark>(.+)</mark>.*", "\\1", example)
Marc Kupietz5a336b62021-11-27 17:51:35 +01001520 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001521 stringr::str_replace(example, "<[^>]*>", "")
Marc Kupietz5a336b62021-11-27 17:51:35 +01001522 }
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001523 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001524 out[i] <- ""
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001525 }
Marc Kupietz5a336b62021-11-27 17:51:35 +01001526 }
1527 out
1528 }
1529
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001530collocatesQuery <-
1531 function(kco,
1532 query,
1533 vc = "",
1534 minOccur = 5,
1535 leftContextSize = 5,
1536 rightContextSize = 5,
1537 searchHitsSampleLimit = 20000,
1538 ignoreCollocateCase = FALSE,
1539 stopwords = c(),
Marc Kupietzb2862d42025-10-18 10:17:49 +02001540 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001541 ...) {
1542 frequency <- NULL
1543 q <- corpusQuery(kco, query, vc, metadataOnly = F, ...)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001544 if (q@totalResults == 0) {
1545 tibble(word = c(), frequency = c())
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001546 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001547 q <- fetchNext(q, maxFetch = searchHitsSampleLimit, randomizePageOrder = TRUE)
1548 matches2FreqTable(q@collectedMatches,
1549 0,
1550 minOccur = minOccur,
1551 leftContextSize = leftContextSize,
1552 rightContextSize = rightContextSize,
1553 ignoreCollocateCase = ignoreCollocateCase,
1554 stopwords = stopwords,
Marc Kupietzb2862d42025-10-18 10:17:49 +02001555 collocateFilterRegex = collocateFilterRegex,
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001556 ...,
1557 verbose = kco@verbose
1558 ) |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001559 mutate(frequency = frequency * q@totalResults / min(q@totalResults, searchHitsSampleLimit)) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001560 filter(frequency >= minOccur)
1561 }
1562 }