blob: 7240eca1d5519d8212c2b432657c4f5e86e3b0db [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 Kupietz130a2a22025-10-18 16:09:23 +020063#' \item Optional helper columns such as \code{query}, \code{example}, or \code{url} when example retrieval is requested.
64#' }
Marc Kupietzc4540a22025-10-14 17:39:53 +020065#' @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 +020066#' @importFrom purrr pmap
Marc Kupietzc4540a22025-10-14 17:39:53 +020067#' @importFrom tidyr expand_grid pivot_wider
68#' @importFrom rlang sym
Marc Kupietzdbd431a2021-08-29 12:17:45 +020069#'
70#' @examples
Marc Kupietz6ae76052021-09-21 10:34:00 +020071#' \dontrun{
72#'
Marc Kupietz6dfeed92025-06-03 11:58:06 +020073#' # Find top collocates of "Packung" inside and outside the sports domain.
74#' KorAPConnection(verbose = TRUE) |>
75#' collocationAnalysis("Packung",
76#' vc = c("textClass=sport", "textClass!=sport"),
77#' leftContextSize = 1, rightContextSize = 1, topCollocatesLimit = 20
78#' ) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +020079#' dplyr::filter(logDice >= 5)
80#' }
81#'
Marc Kupietz6ae76052021-09-21 10:34:00 +020082#' \dontrun{
83#'
Marc Kupietzdbd431a2021-08-29 12:17:45 +020084#' # Identify the most prominent light verb construction with "in ... setzen".
85#' # Note that, currently, the use of focus function disallows exactFrequencies.
Marc Kupietz4cd066d2025-02-28 15:48:23 +010086#' KorAPConnection(verbose = TRUE) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +020087#' collocationAnalysis("focus(in [tt/p=NN] {[tt/l=setzen]})",
Marc Kupietz6dfeed92025-06-03 11:58:06 +020088#' leftContextSize = 1, rightContextSize = 0, exactFrequencies = FALSE, topCollocatesLimit = 20
89#' )
Marc Kupietzdbd431a2021-08-29 12:17:45 +020090#' }
91#'
92#' @export
Marc Kupietz6dfeed92025-06-03 11:58:06 +020093setMethod(
94 "collocationAnalysis", "KorAPConnection",
95 function(kco,
96 node,
97 vc = "",
98 lemmatizeNodeQuery = FALSE,
99 minOccur = 5,
100 leftContextSize = 5,
101 rightContextSize = 5,
102 topCollocatesLimit = 200,
103 searchHitsSampleLimit = 20000,
104 ignoreCollocateCase = FALSE,
105 withinSpan = ifelse(exactFrequencies, "base/s=s", ""),
106 exactFrequencies = TRUE,
107 stopwords = append(RKorAPClient::synsemanticStopwords(), node),
108 seed = 7,
109 expand = length(vc) != length(node),
110 maxRecurse = 0,
111 addExamples = FALSE,
112 thresholdScore = "logDice",
113 threshold = 2.0,
114 localStopwords = c(),
115 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzde679ea2025-10-19 13:14:51 +0200116 queryMissingScores = FALSE,
Marc Kupietz9894a372025-10-18 14:51:29 +0200117 missingScoreQuantile = 0.05,
Marc Kupietze34a8be2025-10-17 20:13:42 +0200118 vcLabel = NA_character_,
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200119 cacheAs = NULL,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200120 ...) {
Marc Kupietzb2862d42025-10-18 10:17:49 +0200121 word <- frequency <- O <- NULL
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200122
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200123 if (!is.null(cacheAs) && !grepl("\\.rds$", cacheAs, ignore.case = TRUE)) {
124 cacheAs <- paste0(cacheAs, ".rds")
125 }
126
127 if (!is.null(cacheAs) && file.exists(cacheAs)) {
128 log_info(kco@verbose, sprintf("Loading collocation analysis from cache: %s\n", cacheAs))
129 return(readRDS(cacheAs))
130 }
131
Marc Kupietzb2862d42025-10-18 10:17:49 +0200132 if (!exactFrequencies && (!is.na(withinSpan) && !is.null(withinSpan) && nzchar(withinSpan))) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200133 stop(sprintf("Not empty withinSpan (='%s') requires exactFrequencies=TRUE", withinSpan), call. = FALSE)
134 }
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200135
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200136 warnIfNotAuthorized(kco)
Marc Kupietz581a29b2021-09-04 20:51:04 +0200137
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200138 if (lemmatizeNodeQuery) {
139 node <- lemmatizeWordQuery(node)
140 }
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200141
Marc Kupietze34a8be2025-10-17 20:13:42 +0200142 vcNames <- names(vc)
Marc Kupietze34a8be2025-10-17 20:13:42 +0200143 if (is.null(vcNames)) {
144 vcNames <- rep(NA_character_, length(vc))
Marc Kupietze34a8be2025-10-17 20:13:42 +0200145 }
146
147 label_lookup <- NULL
Marc Kupietzb2862d42025-10-18 10:17:49 +0200148 if (!is.null(names(vc)) && length(vc) > 0) {
149 raw_names <- names(vc)
150 if (any(!is.na(raw_names) & raw_names != "")) {
151 label_lookup <- stats::setNames(raw_names, vc)
152 }
Marc Kupietze34a8be2025-10-17 20:13:42 +0200153 }
154
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200155 result <- if (length(node) > 1 || length(vc) > 1) {
Marc Kupietze34a8be2025-10-17 20:13:42 +0200156 grid <- if (expand) {
Marc Kupietzb2862d42025-10-18 10:17:49 +0200157 tmp_grid <- tidyr::expand_grid(node = node, idx = seq_along(vc))
158 tmp_grid$vc <- vc[tmp_grid$idx]
159 tmp_grid$vcLabel <- vcNames[tmp_grid$idx]
160 tmp_grid[, c("node", "vc", "vcLabel"), drop = FALSE]
Marc Kupietze34a8be2025-10-17 20:13:42 +0200161 } else {
162 tibble(node = node, vc = vc, vcLabel = vcNames)
163 }
164
165 multi_result <- purrr::pmap(grid, function(node, vc, vcLabel, ...) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200166 collocationAnalysis(kco,
167 node = node,
168 vc = vc,
169 minOccur = minOccur,
170 leftContextSize = leftContextSize,
171 rightContextSize = rightContextSize,
172 topCollocatesLimit = topCollocatesLimit,
173 searchHitsSampleLimit = searchHitsSampleLimit,
174 ignoreCollocateCase = ignoreCollocateCase,
175 withinSpan = withinSpan,
176 exactFrequencies = exactFrequencies,
177 stopwords = stopwords,
178 addExamples = TRUE,
179 localStopwords = localStopwords,
180 seed = seed,
181 expand = expand,
Marc Kupietz9894a372025-10-18 14:51:29 +0200182 missingScoreQuantile = missingScoreQuantile,
Marc Kupietzde679ea2025-10-19 13:14:51 +0200183 queryMissingScores = queryMissingScores,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200184 collocateFilterRegex = collocateFilterRegex,
Marc Kupietze34a8be2025-10-17 20:13:42 +0200185 vcLabel = vcLabel,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200186 ...
187 )
188 }) |>
Marc Kupietze31322e2025-10-17 18:55:36 +0200189 bind_rows()
190
191 if (!"vc" %in% names(multi_result) || nrow(multi_result) == 0) {
192 multi_result
193 } else {
Marc Kupietzde679ea2025-10-19 13:14:51 +0200194 if (queryMissingScores) {
195 multi_result <- backfill_missing_scores(
196 multi_result,
197 grid = grid,
198 kco = kco,
199 ignoreCollocateCase = ignoreCollocateCase,
200 ...
201 )
202 }
203
Marc Kupietze34a8be2025-10-17 20:13:42 +0200204 if (!"label" %in% names(multi_result)) {
205 multi_result$label <- NA_character_
206 }
207
208 if (!is.null(label_lookup)) {
209 override <- unname(label_lookup[multi_result$vc])
210 missing_idx <- is.na(multi_result$label) | multi_result$label == ""
211 if (any(missing_idx)) {
212 multi_result$label[missing_idx] <- override[missing_idx]
213 }
214 }
215
216 missing_idx <- is.na(multi_result$label) | multi_result$label == ""
217 if (any(missing_idx)) {
218 multi_result$label[missing_idx] <- queryStringToLabel(multi_result$vc[missing_idx])
219 }
220
Marc Kupietze31322e2025-10-17 18:55:36 +0200221 multi_result |>
Marc Kupietz9894a372025-10-18 14:51:29 +0200222 add_multi_vc_comparisons(
Marc Kupietz9894a372025-10-18 14:51:29 +0200223 missingScoreQuantile = missingScoreQuantile
224 )
Marc Kupietze31322e2025-10-17 18:55:36 +0200225 }
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200226 } else {
Marc Kupietze34a8be2025-10-17 20:13:42 +0200227 if ((is.na(vcLabel) || vcLabel == "") && length(vcNames) >= 1) {
228 vcLabel <- vcNames[1]
229 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200230
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200231 set.seed(seed)
232 candidates <- collocatesQuery(
233 kco,
234 node,
235 vc = vc,
236 minOccur = minOccur,
237 leftContextSize = leftContextSize,
238 rightContextSize = rightContextSize,
239 searchHitsSampleLimit = searchHitsSampleLimit,
240 ignoreCollocateCase = ignoreCollocateCase,
241 stopwords = append(stopwords, localStopwords),
Marc Kupietzb2862d42025-10-18 10:17:49 +0200242 collocateFilterRegex = collocateFilterRegex,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200243 ...
244 )
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200245
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200246 if (nrow(candidates) > 0) {
247 candidates <- candidates |>
248 filter(frequency >= minOccur) |>
249 slice_head(n = topCollocatesLimit)
250 collocationScoreQuery(
251 kco,
252 node = node,
253 collocate = candidates$word,
254 vc = vc,
255 leftContextSize = leftContextSize,
256 rightContextSize = rightContextSize,
257 observed = if (exactFrequencies) NA else candidates$frequency,
258 ignoreCollocateCase = ignoreCollocateCase,
259 withinSpan = withinSpan,
260 ...
261 ) |>
262 filter(O >= minOccur) |>
263 dplyr::arrange(dplyr::desc(logDice))
264 } else {
265 tibble()
266 }
267 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200268
269 if (!is.na(vcLabel) && vcLabel != "" && "label" %in% names(result)) {
270 result$label <- rep(vcLabel, nrow(result))
271 }
272
273 threshold_col <- thresholdScore
274 if (maxRecurse > 0 && nrow(result) > 0 && threshold_col %in% names(result)) {
275 threshold_values <- result[[threshold_col]]
276 eligible_idx <- which(!is.na(threshold_values) & threshold_values >= threshold)
277 if (length(eligible_idx) > 0) {
278 recurseWith <- result[eligible_idx, , drop = FALSE]
279 result <- collocationAnalysis(
280 kco,
281 node = paste0("(", buildCollocationQuery(
282 removeWithinSpan(recurseWith$node, withinSpan),
283 recurseWith$collocate,
284 leftContextSize = leftContextSize,
285 rightContextSize = rightContextSize,
286 withinSpan = ""
287 ), ")"),
288 vc = vc,
289 minOccur = minOccur,
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200290 leftContextSize = leftContextSize,
291 rightContextSize = rightContextSize,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200292 withinSpan = withinSpan,
293 maxRecurse = maxRecurse - 1,
294 stopwords = stopwords,
295 localStopwords = recurseWith$collocate,
296 exactFrequencies = exactFrequencies,
297 searchHitsSampleLimit = searchHitsSampleLimit,
298 topCollocatesLimit = topCollocatesLimit,
299 addExamples = FALSE,
Marc Kupietz9894a372025-10-18 14:51:29 +0200300 missingScoreQuantile = missingScoreQuantile,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200301 collocateFilterRegex = collocateFilterRegex,
Marc Kupietzde679ea2025-10-19 13:14:51 +0200302 queryMissingScores = queryMissingScores,
Marc Kupietz2b0b0a12025-10-19 14:49:14 +0200303 thresholdScore = thresholdScore,
304 threshold = threshold,
Marc Kupietzb2862d42025-10-18 10:17:49 +0200305 vcLabel = vcLabel
306 ) |>
Marc Kupietz2b0b0a12025-10-19 14:49:14 +0200307 bind_rows(result)
308
309 if (threshold_col %in% names(result)) {
310 threshold_values <- result[[threshold_col]]
311 keep_idx <- is.na(threshold_values) | threshold_values >= threshold
312 result <- result[keep_idx, , drop = FALSE]
313 }
314
315 result <- result |>
Marc Kupietzb2862d42025-10-18 10:17:49 +0200316 filter(O >= minOccur) |>
317 dplyr::arrange(dplyr::desc(logDice))
318 }
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200319 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200320
321 if (addExamples && nrow(result) > 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200322 result$query <- buildCollocationQuery(
323 result$node,
324 result$collocate,
325 leftContextSize = leftContextSize,
326 rightContextSize = rightContextSize,
327 withinSpan = withinSpan
328 )
329 result$example <- findExample(
330 kco,
331 query = result$query,
332 vc = result$vc
333 )
334 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200335
Marc Kupietz0a292632025-10-19 14:04:36 +0200336 if (!is.null(withinSpan) && !is.na(withinSpan) && nzchar(withinSpan) &&
337 nrow(result) > 0 &&
338 "webUIRequestUrl" %in% names(result) &&
339 "query" %in% names(result)) {
340 candidate_rows <- which(!is.na(result$node) &
341 !grepl("focus\\(", result$node, perl = TRUE) &
342 !is.na(result$query) & nzchar(result$query))
343
344 if (length(candidate_rows) > 0) {
345 focused_queries <- vapply(
346 result$query[candidate_rows],
347 inject_focus_into_query,
348 character(1)
349 )
350
351 changed <- focused_queries != result$query[candidate_rows]
352 if (any(changed)) {
353 indices <- candidate_rows[changed]
354 vc_values <- as.character(result$vc)
355 vc_values[is.na(vc_values)] <- ""
356
357 result$webUIRequestUrl[indices] <- mapply(
358 function(new_query, vc_value) {
359 buildWebUIRequestUrlFromString(
360 kco@KorAPUrl,
361 new_query,
362 vc = vc_value,
363 ql = "poliqarp"
364 )
365 },
366 focused_queries[changed],
367 vc_values[indices],
368 USE.NAMES = FALSE
369 )
370 }
371 }
372 }
373
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200374 if (!is.null(cacheAs)) {
375 log_info(kco@verbose, sprintf("Saving collocation analysis to cache: %s\n", cacheAs))
376 saveRDS(result, cacheAs)
377 }
378
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200379 result
380 }
Marc Kupietzdbd431a2021-08-29 12:17:45 +0200381)
382
Marc Kupietz76b05592021-12-19 16:26:15 +0100383# #' @export
Marc Kupietz5a336b62021-11-27 17:51:35 +0100384removeWithinSpan <- function(query, withinSpan) {
385 if (withinSpan == "") {
386 return(query)
387 }
388 needle <- sprintf("^\\(contains\\(<%s>, ?(.*)\\){2}$", withinSpan)
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200389 res <- gsub(needle, "\\1", query)
Marc Kupietz5a336b62021-11-27 17:51:35 +0100390 needle <- sprintf("^contains\\(<%s>, ?(.*)\\)$", withinSpan)
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200391 res <- gsub(needle, "\\1", res)
Marc Kupietz5a336b62021-11-27 17:51:35 +0100392 return(res)
393}
394
Marc Kupietzde679ea2025-10-19 13:14:51 +0200395backfill_missing_scores <- function(result,
396 grid,
397 kco,
398 ignoreCollocateCase,
399 ...) {
400 if (!"vc" %in% names(result) || !"node" %in% names(result) || !"collocate" %in% names(result)) {
401 return(result)
402 }
403
404 if (nrow(result) == 0) {
405 return(result)
406 }
407
Marc Kupietz9c53e412026-06-21 12:13:44 +0200408 distinct_pairs <- dplyr::distinct(
409 result,
410 .data$node,
411 .data$collocate
412 )
Marc Kupietzde679ea2025-10-19 13:14:51 +0200413 if (nrow(distinct_pairs) == 0) {
414 return(result)
415 }
416
417 collocates_by_node <- split(as.character(distinct_pairs$collocate), distinct_pairs$node)
418 if (length(collocates_by_node) == 0) {
419 return(result)
420 }
421
422 required_combinations <- unique(as.data.frame(grid[, c("node", "vc", "vcLabel")], drop = FALSE))
423 for (i in seq_len(nrow(required_combinations))) {
424 node_value <- required_combinations$node[i]
425 vc_value <- required_combinations$vc[i]
426
427 collocate_pool <- collocates_by_node[[node_value]]
428 if (is.null(collocate_pool) || length(collocate_pool) == 0) {
429 next
430 }
431
432 existing_idx <- result$node == node_value & result$vc == vc_value
433 existing_collocates <- unique(as.character(result$collocate[existing_idx]))
434 missing_collocates <- setdiff(unique(collocate_pool), existing_collocates)
435 missing_collocates <- missing_collocates[!is.na(missing_collocates) & nzchar(missing_collocates)]
436
437 if (length(missing_collocates) == 0) {
438 next
439 }
440
441 context_rows <- result[result$node == node_value & result$vc == vc_value, , drop = FALSE]
442 if (nrow(context_rows) == 0) {
443 context_rows <- result[result$node == node_value, , drop = FALSE]
444 }
445
446 left_size <- context_rows$leftContextSize[!is.na(context_rows$leftContextSize)][1]
447 if (is.na(left_size) || length(left_size) == 0) {
448 left_size <- result$leftContextSize[!is.na(result$leftContextSize)][1]
449 }
450 if (is.na(left_size) || length(left_size) == 0) {
451 left_size <- 5
452 }
453
454 right_size <- context_rows$rightContextSize[!is.na(context_rows$rightContextSize)][1]
455 if (is.na(right_size) || length(right_size) == 0) {
456 right_size <- result$rightContextSize[!is.na(result$rightContextSize)][1]
457 }
458 if (is.na(right_size) || length(right_size) == 0) {
459 right_size <- 5
460 }
461
462 within_span_value <- ""
463 if ("query" %in% names(context_rows)) {
464 query_candidate <- context_rows$query[!is.na(context_rows$query) & nzchar(context_rows$query)][1]
465 if (!is.na(query_candidate) && nzchar(query_candidate)) {
466 match_one <- regexec("^\\(*contains\\(<([^>]+)>,", query_candidate)
467 matches <- regmatches(query_candidate, match_one)
468 if (length(matches) >= 1 && length(matches[[1]]) >= 2) {
469 within_span_value <- matches[[1]][2]
470 }
471 }
472 }
473
474 new_rows <- collocationScoreQuery(
475 kco,
476 node = node_value,
477 collocate = missing_collocates,
478 vc = vc_value,
479 leftContextSize = left_size,
480 rightContextSize = right_size,
481 ignoreCollocateCase = ignoreCollocateCase,
482 withinSpan = within_span_value,
483 ...
484 )
485
486 if (nrow(new_rows) == 0) {
487 next
488 }
489
490 if (!is.null(required_combinations$vcLabel[i]) && !is.na(required_combinations$vcLabel[i]) && required_combinations$vcLabel[i] != "" && "label" %in% names(new_rows)) {
491 new_rows$label <- required_combinations$vcLabel[i]
492 }
493
494 result <- dplyr::bind_rows(result, new_rows)
495 }
496
497 result
498}
499
Marc Kupietz0a292632025-10-19 14:04:36 +0200500inject_focus_into_query <- function(query) {
501 if (is.null(query) || is.na(query)) {
502 return(query)
503 }
504
505 trimmed <- trimws(query)
506 if (!nzchar(trimmed)) {
507 return(query)
508 }
509
510 if (!grepl("^contains\\(<[^>]+>", trimmed, perl = TRUE)) {
511 return(query)
512 }
513
514 if (grepl("focus\\(", trimmed, perl = TRUE)) {
515 return(query)
516 }
517
518 pattern <- "^contains\\(<([^>]+)>\\s*,\\s*\\((.*)\\)\\)\\s*$"
519 matches <- regexec(pattern, trimmed, perl = TRUE)
520 components <- regmatches(trimmed, matches)
521 if (length(components) == 0 || length(components[[1]]) < 3) {
522 return(query)
523 }
524
525 span <- components[[1]][2]
526 inner <- components[[1]][3]
527 parts <- strsplit(inner, "\\|", perl = TRUE)[[1]]
528 parts <- trimws(parts)
529 parts <- parts[nzchar(parts)]
530
531 if (length(parts) == 0) {
532 return(query)
533 }
534
535 focused <- paste0("focus({", parts, "})")
536 combined <- paste(focused, collapse = " | ")
537
538 sprintf("contains(<%s>, (%s))", span, combined)
539}
540
Marc Kupietz77852b22025-10-19 11:35:34 +0200541add_multi_vc_comparisons <- function(result, missingScoreQuantile = 0.05) {
Marc Kupietz09b1c082026-05-01 14:45:47 +0200542 label <- node <- collocate <- vc <- webUIRequestUrl <- NULL
Marc Kupietzc4540a22025-10-14 17:39:53 +0200543
544 if (!"label" %in% names(result) || dplyr::n_distinct(result$label) < 2) {
545 return(result)
546 }
547
548 numeric_cols <- names(result)[vapply(result, is.numeric, logical(1))]
549 non_score_cols <- c("N", "O", "O1", "O2", "E", "w", "leftContextSize", "rightContextSize", "frequency")
550 score_cols <- setdiff(numeric_cols, non_score_cols)
551
552 if (length(score_cols) == 0) {
553 return(result)
554 }
555
Marc Kupietz9894a372025-10-18 14:51:29 +0200556 compute_score_floor <- function(values) {
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200557 # Estimate a conservative floor so missing scores can be imputed without favoring any label
Marc Kupietz9894a372025-10-18 14:51:29 +0200558 finite_values <- values[is.finite(values)]
559 if (length(finite_values) == 0) {
560 return(0)
561 }
562
563 prob <- min(max(missingScoreQuantile, 0), 0.5)
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200564 # Use a lower quantile as the anchor to stay near the weakest attested scores
Marc Kupietz9894a372025-10-18 14:51:29 +0200565 q_val <- suppressWarnings(stats::quantile(finite_values,
566 probs = prob,
567 names = FALSE,
568 type = 7
569 ))
570
571 if (!is.finite(q_val)) {
572 q_val <- suppressWarnings(min(finite_values, na.rm = TRUE))
573 }
574
575 min_val <- suppressWarnings(min(finite_values, na.rm = TRUE))
576 if (!is.finite(min_val)) {
577 min_val <- 0
578 }
579
580 spread_candidates <- c(
581 suppressWarnings(stats::IQR(finite_values, na.rm = TRUE, type = 7)),
582 stats::sd(finite_values, na.rm = TRUE),
583 abs(q_val) * 0.1,
584 abs(min_val - q_val)
585 )
586 spread_candidates <- spread_candidates[is.finite(spread_candidates)]
587
588 spread <- 0
589 if (length(spread_candidates) > 0) {
590 spread <- max(spread_candidates)
591 }
592 if (!is.finite(spread) || spread == 0) {
593 spread <- max(abs(q_val), abs(min_val), 1e-06)
594 }
595
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200596 # Step away from the anchor by a robust spread estimate to avoid ties with real scores
Marc Kupietz9894a372025-10-18 14:51:29 +0200597 candidate <- q_val - spread
598 if (!is.finite(candidate)) {
599 candidate <- min_val
600 }
601
602 floor_value <- suppressWarnings(min(c(candidate, min_val), na.rm = TRUE))
603 if (!is.finite(floor_value)) {
604 floor_value <- min_val
605 }
606 if (!is.finite(floor_value)) {
607 floor_value <- 0
608 }
609
610 floor_value
611 }
612
613 score_replacements <- stats::setNames(
614 vapply(score_cols, function(col) {
615 compute_score_floor(result[[col]])
616 }, numeric(1)),
617 score_cols
618 )
619
Marc Kupietzc4540a22025-10-14 17:39:53 +0200620 comparison <- result |>
Marc Kupietz28a29842025-10-18 12:25:09 +0200621 dplyr::select(node, collocate, label, dplyr::all_of(score_cols)) |>
622 tidyr::pivot_wider(
Marc Kupietzc4540a22025-10-14 17:39:53 +0200623 names_from = label,
Marc Kupietz28a29842025-10-18 12:25:09 +0200624 values_from = dplyr::all_of(score_cols),
Marc Kupietzc4540a22025-10-14 17:39:53 +0200625 names_glue = "{.value}_{make.names(label)}",
626 values_fn = dplyr::first
627 )
628
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200629 raw_labels <- unique(result$label)
630 labels <- make.names(raw_labels)
631 label_map <- stats::setNames(raw_labels, labels)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200632 vc_map <- result |>
633 dplyr::select(label, vc) |>
634 dplyr::filter(!is.na(label), label != "") |>
635 dplyr::distinct(label, .keep_all = TRUE)
636 vc_map <- stats::setNames(vc_map$vc, make.names(vc_map$label))
637
638 replace_web_ui_cq <- function(url, vc_value) {
639 if (length(url) == 0 || is.na(url) || url == "") {
640 return(NA_character_)
641 }
642 if (length(vc_value) == 0 || is.na(vc_value)) {
643 vc_value <- ""
644 }
645 encoded_vc <- urltools::url_encode(enc2utf8(as.character(vc_value)))
646 if (grepl("([?&]cq=)[^&]*", url, perl = TRUE)) {
647 return(sub("([?&]cq=)[^&]*", paste0("\\1", encoded_vc), url, perl = TRUE))
648 }
649 if (encoded_vc == "") {
650 return(url)
651 }
652 paste0(url, ifelse(grepl("\\?", url), "&", "?"), "cq=", encoded_vc)
653 }
654
655 if ("webUIRequestUrl" %in% names(result)) {
656 url_data <- result |>
657 dplyr::select(node, collocate, label, webUIRequestUrl) |>
658 tidyr::pivot_wider(
659 names_from = label,
660 values_from = webUIRequestUrl,
661 names_glue = "webUIRequestUrl_{make.names(label)}",
662 values_fn = dplyr::first
663 )
664
665 comparison <- dplyr::left_join(comparison, url_data, by = c("node", "collocate"))
666
667 url_cols <- paste0("webUIRequestUrl_", labels)
668 present_url_cols <- intersect(url_cols, names(comparison))
669 fallback_urls <- vapply(seq_len(nrow(comparison)), function(i) {
670 urls <- unlist(comparison[i, present_url_cols, drop = FALSE], use.names = FALSE)
671 urls <- as.character(urls)
672 urls <- urls[!is.na(urls) & urls != ""]
673 if (length(urls) == 0) {
674 NA_character_
675 } else {
676 urls[1]
677 }
678 }, character(1))
679
680 for (safe_label in labels) {
681 url_col <- paste0("webUIRequestUrl_", safe_label)
682 if (!url_col %in% names(comparison)) {
683 comparison[[url_col]] <- NA_character_
684 }
685 missing_urls <- is.na(comparison[[url_col]]) | comparison[[url_col]] == ""
686 if (any(missing_urls)) {
687 comparison[[url_col]][missing_urls] <- vapply(
688 fallback_urls[missing_urls],
689 replace_web_ui_cq,
690 character(1),
691 vc_value = vc_map[[safe_label]]
692 )
693 }
694 }
695 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200696
Marc Kupietz28a29842025-10-18 12:25:09 +0200697 rank_data <- result |>
698 dplyr::distinct(node, collocate)
699
700 for (i in seq_along(raw_labels)) {
701 raw_lab <- raw_labels[i]
702 safe_lab <- labels[i]
703 label_df <- result[result$label == raw_lab, c("node", "collocate", score_cols), drop = FALSE]
704 if (nrow(label_df) == 0) {
705 next
706 }
707 label_df <- dplyr::distinct(label_df)
708 rank_tbl <- label_df[, c("node", "collocate"), drop = FALSE]
709 for (col in score_cols) {
710 rank_col_name <- paste0("rank_", safe_lab, "_", col)
Marc Kupietz130a2a22025-10-18 16:09:23 +0200711 percentile_col_name <- paste0("percentile_rank_", safe_lab, "_", col)
Marc Kupietz28a29842025-10-18 12:25:09 +0200712 values <- label_df[[col]]
713 ranks <- rep(NA_real_, length(values))
Marc Kupietz130a2a22025-10-18 16:09:23 +0200714 percentiles <- rep(NA_real_, length(values))
Marc Kupietz28a29842025-10-18 12:25:09 +0200715 valid_idx <- which(!is.na(values))
716 if (length(valid_idx) > 0) {
717 ranks[valid_idx] <- rank(-values[valid_idx], ties.method = "first")
Marc Kupietz130a2a22025-10-18 16:09:23 +0200718 total <- length(valid_idx)
719 percentiles[valid_idx] <- 1 - (ranks[valid_idx] - 1) / total
Marc Kupietz28a29842025-10-18 12:25:09 +0200720 }
721 rank_tbl[[rank_col_name]] <- ranks
Marc Kupietz130a2a22025-10-18 16:09:23 +0200722 rank_tbl[[percentile_col_name]] <- percentiles
Marc Kupietz28a29842025-10-18 12:25:09 +0200723 }
724 rank_data <- dplyr::left_join(rank_data, rank_tbl, by = c("node", "collocate"))
725 }
726
727 comparison <- dplyr::left_join(comparison, rank_data, by = c("node", "collocate"))
728
729 rank_replacements <- numeric(0)
730 rank_column_names <- grep("^rank_", names(comparison), value = TRUE)
731 if (length(rank_column_names) > 0) {
732 rank_replacements <- stats::setNames(
733 vapply(rank_column_names, function(col) {
734 col_values <- comparison[[col]]
735 valid_values <- col_values[!is.na(col_values)]
736 if (length(valid_values) == 0) {
737 nrow(comparison) + 1
738 } else {
739 suppressWarnings(max(valid_values, na.rm = TRUE)) + 1
740 }
741 }, numeric(1)),
742 rank_column_names
743 )
744 }
745
Marc Kupietz130a2a22025-10-18 16:09:23 +0200746 percentile_replacements <- numeric(0)
747 percentile_column_names <- grep("^percentile_rank_", names(comparison), value = TRUE)
748 if (length(percentile_column_names) > 0) {
749 percentile_replacements <- stats::setNames(
750 rep(0, length(percentile_column_names)),
751 percentile_column_names
752 )
753 }
754
Marc Kupietz28a29842025-10-18 12:25:09 +0200755 collapse_label_values <- function(indices, safe_labels_vec) {
756 if (length(indices) == 0) {
757 return(NA_character_)
758 }
759 labs <- label_map[safe_labels_vec[indices]]
760 fallback <- safe_labels_vec[indices]
761 labs[is.na(labs) | labs == ""] <- fallback[is.na(labs) | labs == ""]
762 labs <- labs[!is.na(labs) & labs != ""]
763 if (length(labs) == 0) {
764 return(NA_character_)
765 }
766 paste(unique(labs), collapse = ", ")
767 }
768
Marc Kupietz09b1c082026-05-01 14:45:47 +0200769 collapse_url_values <- function(indices, url_values) {
770 if (length(indices) == 0 || is.null(url_values)) {
771 return(NA_character_)
772 }
773 urls <- as.character(url_values[indices])
774 urls <- urls[!is.na(urls) & urls != ""]
775 if (length(urls) == 0) {
776 return(NA_character_)
777 }
778 paste(unique(urls), collapse = ", ")
779 }
780
Marc Kupietzc4540a22025-10-14 17:39:53 +0200781 if (length(labels) == 2) {
Marc Kupietz9894a372025-10-18 14:51:29 +0200782 fill_scores <- function(x, y, measure_col) {
783 replacement <- score_replacements[[measure_col]]
784 fallback_min <- suppressWarnings(min(c(x, y), na.rm = TRUE))
785 if (!is.finite(fallback_min)) {
786 fallback_min <- 0
Marc Kupietzc4540a22025-10-14 17:39:53 +0200787 }
Marc Kupietz9894a372025-10-18 14:51:29 +0200788 if (!is.null(replacement) && is.finite(replacement)) {
789 replacement <- min(replacement, fallback_min)
790 } else {
791 replacement <- fallback_min
792 }
793 if (!is.finite(replacement)) {
794 replacement <- 0
795 }
796 if (any(is.na(x))) {
797 x[is.na(x)] <- replacement
798 }
799 if (any(is.na(y))) {
800 y[is.na(y)] <- replacement
801 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200802 list(x = x, y = y)
803 }
804
Marc Kupietz130a2a22025-10-18 16:09:23 +0200805 fill_percentiles <- function(x, y, left_pct_col, right_pct_col) {
806 replacement_left <- percentile_replacements[[left_pct_col]]
807 if (is.null(replacement_left) || !is.finite(replacement_left)) {
808 replacement_left <- 0
809 }
810 replacement_right <- percentile_replacements[[right_pct_col]]
811 if (is.null(replacement_right) || !is.finite(replacement_right)) {
812 replacement_right <- 0
813 }
814 if (any(is.na(x))) {
815 x[is.na(x)] <- replacement_left
816 }
817 if (any(is.na(y))) {
818 y[is.na(y)] <- replacement_right
819 }
820 list(x = x, y = y)
821 }
822
Marc Kupietz28a29842025-10-18 12:25:09 +0200823 fill_ranks <- function(x, y, left_rank_col, right_rank_col) {
824 fallback <- nrow(comparison) + 1
825 replacement_left <- rank_replacements[[left_rank_col]]
826 if (is.null(replacement_left) || !is.finite(replacement_left)) {
827 replacement_left <- fallback
Marc Kupietzc4540a22025-10-14 17:39:53 +0200828 }
Marc Kupietz28a29842025-10-18 12:25:09 +0200829 replacement_right <- rank_replacements[[right_rank_col]]
830 if (is.null(replacement_right) || !is.finite(replacement_right)) {
831 replacement_right <- fallback
832 }
833 if (any(is.na(x))) {
834 x[is.na(x)] <- replacement_left
835 }
836 if (any(is.na(y))) {
837 y[is.na(y)] <- replacement_right
838 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200839 list(x = x, y = y)
840 }
841
842 left_label <- labels[1]
843 right_label <- labels[2]
844
845 for (col in score_cols) {
846 left_col <- paste0(col, "_", left_label)
847 right_col <- paste0(col, "_", right_label)
848 if (!all(c(left_col, right_col) %in% names(comparison))) {
849 next
850 }
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200851 filled <- fill_scores(comparison[[left_col]], comparison[[right_col]], col)
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200852 comparison[[left_col]] <- filled$x
853 comparison[[right_col]] <- filled$y
Marc Kupietzc4540a22025-10-14 17:39:53 +0200854 comparison[[paste0("delta_", col)]] <- filled$x - filled$y
Marc Kupietz28a29842025-10-18 12:25:09 +0200855 rank_left <- paste0("rank_", left_label, "_", col)
856 rank_right <- paste0("rank_", right_label, "_", col)
857 if (all(c(rank_left, rank_right) %in% names(comparison))) {
858 filled_rank <- fill_ranks(
859 comparison[[rank_left]],
860 comparison[[rank_right]],
861 rank_left,
862 rank_right
863 )
864 comparison[[paste0("delta_rank_", col)]] <- filled_rank$x - filled_rank$y
865 }
Marc Kupietz130a2a22025-10-18 16:09:23 +0200866 pct_left <- paste0("percentile_rank_", left_label, "_", col)
867 pct_right <- paste0("percentile_rank_", right_label, "_", col)
868 if (all(c(pct_left, pct_right) %in% names(comparison))) {
869 filled_pct <- fill_percentiles(
870 comparison[[pct_left]],
871 comparison[[pct_right]],
872 pct_left,
873 pct_right
874 )
875 comparison[[paste0("delta_percentile_rank_", col)]] <- filled_pct$x - filled_pct$y
876 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200877 }
878 }
879
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200880 for (col in score_cols) {
881 value_cols <- paste0(col, "_", labels)
882 existing <- value_cols %in% names(comparison)
883 if (!any(existing)) {
884 next
885 }
886 value_cols <- value_cols[existing]
887 safe_labels <- labels[existing]
888
889 score_values <- comparison[, value_cols, drop = FALSE]
890
891 winner_label_col <- paste0("winner_", col)
892 winner_value_col <- paste0("winner_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +0200893 winner_url_col <- paste0("winner_", col, "_webUIRequestUrl")
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200894 runner_label_col <- paste0("runner_up_", col)
895 runner_value_col <- paste0("runner_up_", col, "_value")
Marc Kupietzb2862d42025-10-18 10:17:49 +0200896 loser_label_col <- paste0("loser_", col)
897 loser_value_col <- paste0("loser_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +0200898 loser_url_col <- paste0("loser_", col, "_webUIRequestUrl")
Marc Kupietzb2862d42025-10-18 10:17:49 +0200899 max_delta_col <- paste0("max_delta_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200900 url_cols <- paste0("webUIRequestUrl_", safe_labels)
901 has_urls <- all(url_cols %in% names(comparison))
902 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200903
904 if (nrow(score_values) == 0) {
905 comparison[[winner_label_col]] <- character(0)
906 comparison[[winner_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200907 if (has_urls) {
908 comparison[[winner_url_col]] <- character(0)
909 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200910 comparison[[runner_label_col]] <- character(0)
911 comparison[[runner_value_col]] <- numeric(0)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200912 comparison[[loser_label_col]] <- character(0)
913 comparison[[loser_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200914 if (has_urls) {
915 comparison[[loser_url_col]] <- character(0)
916 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200917 comparison[[max_delta_col]] <- numeric(0)
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200918 next
919 }
920
921 score_matrix <- as.matrix(score_values)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200922 storage.mode(score_matrix) <- "numeric"
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200923
Marc Kupietzb2862d42025-10-18 10:17:49 +0200924 n_rows <- nrow(score_matrix)
925 winner_labels <- rep(NA_character_, n_rows)
926 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200927 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200928 runner_labels <- rep(NA_character_, n_rows)
929 runner_values <- rep(NA_real_, n_rows)
930 loser_labels <- rep(NA_character_, n_rows)
931 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200932 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200933 max_deltas <- rep(NA_real_, n_rows)
934
Marc Kupietzb2862d42025-10-18 10:17:49 +0200935 if (n_rows > 0) {
936 for (i in seq_len(n_rows)) {
937 numeric_row <- as.numeric(score_matrix[i, ])
938 if (all(is.na(numeric_row))) {
939 next
940 }
941
Marc Kupietz9894a372025-10-18 14:51:29 +0200942 replacement <- score_replacements[[col]]
943 fallback_min <- suppressWarnings(min(numeric_row, na.rm = TRUE))
944 if (!is.finite(fallback_min)) {
945 fallback_min <- 0
Marc Kupietzb2862d42025-10-18 10:17:49 +0200946 }
Marc Kupietz9894a372025-10-18 14:51:29 +0200947 if (!is.null(replacement) && is.finite(replacement)) {
948 replacement <- min(replacement, fallback_min)
949 } else {
950 replacement <- fallback_min
951 }
952 if (!is.finite(replacement)) {
953 replacement <- 0
954 }
955 if (any(is.na(numeric_row))) {
956 numeric_row[is.na(numeric_row)] <- replacement
957 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200958 score_matrix[i, ] <- numeric_row
959
960 max_val <- suppressWarnings(max(numeric_row, na.rm = TRUE))
961 max_idx <- which(numeric_row == max_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200962 winner_labels[i] <- collapse_label_values(max_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200963 winner_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +0200964 if (has_urls) {
965 winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ])
966 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200967
968 unique_vals <- sort(unique(numeric_row), decreasing = TRUE)
969 if (length(unique_vals) >= 2) {
970 runner_val <- unique_vals[2]
971 runner_idx <- which(numeric_row == runner_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200972 runner_labels[i] <- collapse_label_values(runner_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200973 runner_values[i] <- runner_val
974 }
975
976 min_val <- suppressWarnings(min(numeric_row, na.rm = TRUE))
977 min_idx <- which(numeric_row == min_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200978 loser_labels[i] <- collapse_label_values(min_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200979 loser_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +0200980 if (has_urls) {
981 loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ])
982 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200983
984 if (is.finite(max_val) && is.finite(min_val)) {
985 max_deltas[i] <- max_val - min_val
986 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200987 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200988 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200989
Marc Kupietzb2862d42025-10-18 10:17:49 +0200990 comparison[, value_cols] <- score_matrix
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200991 comparison[[winner_label_col]] <- winner_labels
992 comparison[[winner_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +0200993 if (has_urls) {
994 comparison[[winner_url_col]] <- winner_urls
995 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200996 comparison[[runner_label_col]] <- runner_labels
997 comparison[[runner_value_col]] <- runner_values
Marc Kupietzb2862d42025-10-18 10:17:49 +0200998 comparison[[loser_label_col]] <- loser_labels
999 comparison[[loser_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001000 if (has_urls) {
1001 comparison[[loser_url_col]] <- loser_urls
1002 }
Marc Kupietzb2862d42025-10-18 10:17:49 +02001003 comparison[[max_delta_col]] <- max_deltas
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001004 }
1005
Marc Kupietz28a29842025-10-18 12:25:09 +02001006 for (col in score_cols) {
1007 rank_cols <- paste0("rank_", labels, "_", col)
1008 existing <- rank_cols %in% names(comparison)
1009 if (!any(existing)) {
1010 next
1011 }
1012 rank_cols <- rank_cols[existing]
1013 safe_labels <- labels[existing]
1014 rank_values <- comparison[, rank_cols, drop = FALSE]
1015
1016 winner_rank_label_col <- paste0("winner_rank_", col)
1017 winner_rank_value_col <- paste0("winner_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001018 winner_rank_url_col <- paste0("winner_rank_", col, "_webUIRequestUrl")
Marc Kupietz28a29842025-10-18 12:25:09 +02001019 runner_rank_label_col <- paste0("runner_up_rank_", col)
1020 runner_rank_value_col <- paste0("runner_up_rank_", col, "_value")
1021 loser_rank_label_col <- paste0("loser_rank_", col)
1022 loser_rank_value_col <- paste0("loser_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001023 loser_rank_url_col <- paste0("loser_rank_", col, "_webUIRequestUrl")
Marc Kupietz28a29842025-10-18 12:25:09 +02001024 max_delta_rank_col <- paste0("max_delta_rank_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001025 url_cols <- paste0("webUIRequestUrl_", safe_labels)
1026 has_urls <- all(url_cols %in% names(comparison))
1027 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz28a29842025-10-18 12:25:09 +02001028
1029 if (nrow(rank_values) == 0) {
1030 comparison[[winner_rank_label_col]] <- character(0)
1031 comparison[[winner_rank_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001032 if (has_urls) {
1033 comparison[[winner_rank_url_col]] <- character(0)
1034 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001035 comparison[[runner_rank_label_col]] <- character(0)
1036 comparison[[runner_rank_value_col]] <- numeric(0)
1037 comparison[[loser_rank_label_col]] <- character(0)
1038 comparison[[loser_rank_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001039 if (has_urls) {
1040 comparison[[loser_rank_url_col]] <- character(0)
1041 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001042 comparison[[max_delta_rank_col]] <- numeric(0)
1043 next
1044 }
1045
Marc Kupietzdb2fabd2026-04-27 15:01:37 +02001046 rank_matrix <- as.matrix(rank_values)
1047 storage.mode(rank_matrix) <- "numeric"
Marc Kupietz28a29842025-10-18 12:25:09 +02001048
1049 n_rows <- nrow(rank_matrix)
1050 winner_labels <- rep(NA_character_, n_rows)
1051 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001052 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietz28a29842025-10-18 12:25:09 +02001053 runner_labels <- rep(NA_character_, n_rows)
1054 runner_values <- rep(NA_real_, n_rows)
1055 loser_labels <- rep(NA_character_, n_rows)
1056 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001057 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietz28a29842025-10-18 12:25:09 +02001058 max_deltas <- rep(NA_real_, n_rows)
1059
1060 for (i in seq_len(n_rows)) {
1061 numeric_row <- as.numeric(rank_matrix[i, ])
1062 if (all(is.na(numeric_row))) {
1063 next
1064 }
1065
1066 if (length(rank_cols) > 0) {
1067 replacement_vec <- rank_replacements[rank_cols]
1068 replacement_vec[is.na(replacement_vec)] <- nrow(comparison) + 1
1069 missing_idx <- which(is.na(numeric_row))
1070 if (length(missing_idx) > 0) {
1071 numeric_row[missing_idx] <- replacement_vec[missing_idx]
1072 }
1073 }
1074
1075 valid_idx <- seq_along(numeric_row)
1076 valid_values <- numeric_row[valid_idx]
1077 min_val <- suppressWarnings(min(valid_values, na.rm = TRUE))
1078 min_positions <- valid_idx[which(valid_values == min_val)]
1079 winner_labels[i] <- collapse_label_values(min_positions, safe_labels)
1080 winner_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001081 if (has_urls) {
1082 winner_urls[i] <- collapse_url_values(min_positions, url_values[i, ])
1083 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001084
1085 ordered_vals <- sort(unique(valid_values), decreasing = FALSE)
1086 if (length(ordered_vals) >= 2) {
1087 runner_val <- ordered_vals[2]
1088 runner_positions <- valid_idx[which(valid_values == runner_val)]
1089 runner_labels[i] <- collapse_label_values(runner_positions, safe_labels)
1090 runner_values[i] <- runner_val
1091 }
1092
1093 max_val <- suppressWarnings(max(valid_values, na.rm = TRUE))
1094 max_positions <- valid_idx[which(valid_values == max_val)]
1095 loser_labels[i] <- collapse_label_values(max_positions, safe_labels)
1096 loser_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001097 if (has_urls) {
1098 loser_urls[i] <- collapse_url_values(max_positions, url_values[i, ])
1099 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001100
1101 if (is.finite(max_val) && is.finite(min_val)) {
1102 max_deltas[i] <- max_val - min_val
1103 }
1104 }
1105
1106 comparison[[winner_rank_label_col]] <- winner_labels
1107 comparison[[winner_rank_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001108 if (has_urls) {
1109 comparison[[winner_rank_url_col]] <- winner_urls
1110 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001111 comparison[[runner_rank_label_col]] <- runner_labels
1112 comparison[[runner_rank_value_col]] <- runner_values
1113 comparison[[loser_rank_label_col]] <- loser_labels
1114 comparison[[loser_rank_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001115 if (has_urls) {
1116 comparison[[loser_rank_url_col]] <- loser_urls
1117 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001118 comparison[[max_delta_rank_col]] <- max_deltas
1119 }
1120
Marc Kupietz130a2a22025-10-18 16:09:23 +02001121 for (col in score_cols) {
1122 pct_cols <- paste0("percentile_rank_", labels, "_", col)
1123 existing <- pct_cols %in% names(comparison)
1124 if (!any(existing)) {
1125 next
1126 }
1127 pct_cols <- pct_cols[existing]
1128 safe_labels <- labels[existing]
1129 pct_values <- comparison[, pct_cols, drop = FALSE]
1130
1131 winner_pct_label_col <- paste0("winner_percentile_rank_", col)
1132 winner_pct_value_col <- paste0("winner_percentile_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001133 winner_pct_url_col <- paste0("winner_percentile_rank_", col, "_webUIRequestUrl")
Marc Kupietz130a2a22025-10-18 16:09:23 +02001134 runner_pct_label_col <- paste0("runner_up_percentile_rank_", col)
1135 runner_pct_value_col <- paste0("runner_up_percentile_rank_", col, "_value")
1136 loser_pct_label_col <- paste0("loser_percentile_rank_", col)
1137 loser_pct_value_col <- paste0("loser_percentile_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001138 loser_pct_url_col <- paste0("loser_percentile_rank_", col, "_webUIRequestUrl")
Marc Kupietz130a2a22025-10-18 16:09:23 +02001139 max_delta_pct_col <- paste0("max_delta_percentile_rank_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001140 url_cols <- paste0("webUIRequestUrl_", safe_labels)
1141 has_urls <- all(url_cols %in% names(comparison))
1142 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz130a2a22025-10-18 16:09:23 +02001143
1144 if (nrow(pct_values) == 0) {
1145 comparison[[winner_pct_label_col]] <- character(0)
1146 comparison[[winner_pct_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001147 if (has_urls) {
1148 comparison[[winner_pct_url_col]] <- character(0)
1149 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001150 comparison[[runner_pct_label_col]] <- character(0)
1151 comparison[[runner_pct_value_col]] <- numeric(0)
1152 comparison[[loser_pct_label_col]] <- character(0)
1153 comparison[[loser_pct_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001154 if (has_urls) {
1155 comparison[[loser_pct_url_col]] <- character(0)
1156 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001157 comparison[[max_delta_pct_col]] <- numeric(0)
1158 next
1159 }
1160
1161 pct_matrix <- as.matrix(pct_values)
1162 storage.mode(pct_matrix) <- "numeric"
1163
1164 n_rows <- nrow(pct_matrix)
1165 winner_labels <- rep(NA_character_, n_rows)
1166 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001167 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietz130a2a22025-10-18 16:09:23 +02001168 runner_labels <- rep(NA_character_, n_rows)
1169 runner_values <- rep(NA_real_, n_rows)
1170 loser_labels <- rep(NA_character_, n_rows)
1171 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001172 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietz130a2a22025-10-18 16:09:23 +02001173 max_deltas <- rep(NA_real_, n_rows)
1174
1175 if (n_rows > 0) {
1176 for (i in seq_len(n_rows)) {
1177 numeric_row <- as.numeric(pct_matrix[i, ])
1178 if (all(is.na(numeric_row))) {
1179 next
1180 }
1181
1182 if (any(is.na(numeric_row))) {
1183 numeric_row[is.na(numeric_row)] <- 0
1184 }
1185 pct_matrix[i, ] <- numeric_row
1186
1187 max_val <- suppressWarnings(max(numeric_row, na.rm = TRUE))
1188 max_idx <- which(numeric_row == max_val)
1189 winner_labels[i] <- collapse_label_values(max_idx, safe_labels)
1190 winner_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001191 if (has_urls) {
1192 winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ])
1193 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001194
1195 unique_vals <- sort(unique(numeric_row), decreasing = TRUE)
1196 if (length(unique_vals) >= 2) {
1197 runner_val <- unique_vals[2]
1198 runner_idx <- which(numeric_row == runner_val)
1199 runner_labels[i] <- collapse_label_values(runner_idx, safe_labels)
1200 runner_values[i] <- runner_val
1201 }
1202
1203 min_val <- suppressWarnings(min(numeric_row, na.rm = TRUE))
1204 min_idx <- which(numeric_row == min_val)
1205 loser_labels[i] <- collapse_label_values(min_idx, safe_labels)
1206 loser_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001207 if (has_urls) {
1208 loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ])
1209 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001210
1211 if (is.finite(max_val) && is.finite(min_val)) {
1212 max_deltas[i] <- max_val - min_val
1213 }
1214 }
1215 }
1216
1217 comparison[, pct_cols] <- pct_matrix
1218 comparison[[winner_pct_label_col]] <- winner_labels
1219 comparison[[winner_pct_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001220 if (has_urls) {
1221 comparison[[winner_pct_url_col]] <- winner_urls
1222 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001223 comparison[[runner_pct_label_col]] <- runner_labels
1224 comparison[[runner_pct_value_col]] <- runner_values
1225 comparison[[loser_pct_label_col]] <- loser_labels
1226 comparison[[loser_pct_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001227 if (has_urls) {
1228 comparison[[loser_pct_url_col]] <- loser_urls
1229 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001230 comparison[[max_delta_pct_col]] <- max_deltas
1231 }
1232
Marc Kupietz09b1c082026-05-01 14:45:47 +02001233 collapse_consensus_url_columns <- function(url_cols) {
1234 if (length(url_cols) == 0) {
1235 return(rep(NA_character_, nrow(comparison)))
1236 }
1237 vapply(seq_len(nrow(comparison)), function(i) {
1238 urls <- unlist(comparison[i, url_cols, drop = FALSE], use.names = FALSE)
1239 urls <- as.character(urls)
1240 urls <- urls[!is.na(urls) & urls != ""]
1241 urls <- unique(urls)
1242 if (length(urls) == 1) {
1243 urls
1244 } else {
1245 NA_character_
1246 }
1247 }, character(1))
1248 }
1249
1250 winner_score_url_cols <- intersect(paste0("winner_", score_cols, "_webUIRequestUrl"), names(comparison))
1251 loser_score_url_cols <- intersect(paste0("loser_", score_cols, "_webUIRequestUrl"), names(comparison))
1252 if (length(winner_score_url_cols) > 0) {
1253 comparison$winner_webUIRequestUrl <- collapse_consensus_url_columns(winner_score_url_cols)
1254 }
1255 if (length(loser_score_url_cols) > 0) {
1256 comparison$loser_webUIRequestUrl <- collapse_consensus_url_columns(loser_score_url_cols)
1257 }
1258
1259 url_helper_cols <- intersect(paste0("webUIRequestUrl_", labels), names(comparison))
1260 if (length(url_helper_cols) > 0) {
1261 comparison <- dplyr::select(comparison, -dplyr::all_of(url_helper_cols))
1262 }
1263
Marc Kupietzc4540a22025-10-14 17:39:53 +02001264 dplyr::left_join(result, comparison, by = c("node", "collocate"))
1265}
1266
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001267#' @importFrom magrittr debug_pipe
Marc Kupietz2b17b212023-08-27 17:47:26 +02001268#' @importFrom stringr str_detect
1269#' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when
1270#'
1271matches2FreqTable <- function(matches,
1272 index = 0,
1273 minOccur = 5,
1274 leftContextSize = 5,
1275 rightContextSize = 5,
1276 ignoreCollocateCase = FALSE,
1277 stopwords = c(),
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001278 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietz2b17b212023-08-27 17:47:26 +02001279 oldTable = data.frame(word = rep(NA, 1), frequency = rep(NA, 1)),
1280 verbose = TRUE) {
1281 word <- NULL # https://stackoverflow.com/questions/8096313/no-visible-binding-for-global-variable-note-in-r-cmd-check
1282 frequency <- NULL
1283
1284 if (nrow(matches) < 1) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001285 dplyr::tibble(word = c(), frequency = c())
Marc Kupietz2b17b212023-08-27 17:47:26 +02001286 } else if (index == 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001287 if (!"tokens" %in% colnames(matches) || !is.list(matches$tokens)) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001288 log_info(verbose, "Outdated KorAP server: Falling back to client side tokenization.\n")
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001289 return(snippet2FreqTable(matches$snippet, minOccur, leftContextSize, rightContextSize,
1290 ignoreCollocateCase = ignoreCollocateCase,
1291 stopwords = stopwords, oldTable = oldTable, verbose = verbose
1292 ))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001293 }
1294 log_info(verbose, paste("Joining", nrow(matches), "kwics\n"))
Marc Kupietza25fbd92025-10-14 17:38:09 +02001295 for (i in seq_len(nrow(matches))) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001296 oldTable <- matches2FreqTable(
1297 matches,
1298 i,
1299 leftContextSize = leftContextSize,
1300 rightContextSize = rightContextSize,
1301 collocateFilterRegex = collocateFilterRegex,
1302 oldTable = oldTable,
1303 stopwords = stopwords
1304 )
1305 }
1306 log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n"))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001307 oldTable |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001308 group_by(word) |>
1309 mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001310 summarise(frequency = sum(frequency), .groups = "drop") |>
Marc Kupietz2b17b212023-08-27 17:47:26 +02001311 arrange(desc(frequency))
1312 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001313 stopwordsTable <- dplyr::tibble(word = stopwords)
Marc Kupietz2b17b212023-08-27 17:47:26 +02001314
1315 left <- tail(unlist(matches$tokens$left[index]), leftContextSize)
1316
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001317 # cat(paste("left:", left, "\n", collapse=" "))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001318
1319 right <- head(unlist(matches$tokens$right[index]), rightContextSize)
1320
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001321 # cat(paste("right:", right, "\n", collapse=" "))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001322
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001323 if (length(left) + length(right) == 0) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001324 oldTable
1325 } else {
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001326 table(c(left, right)) |>
1327 dplyr::as_tibble(.name_repair = "minimal") |>
1328 dplyr::rename(word = 1, frequency = 2) |>
1329 dplyr::filter(str_detect(word, collocateFilterRegex)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001330 dplyr::anti_join(stopwordsTable, by = "word") |>
Marc Kupietz2b17b212023-08-27 17:47:26 +02001331 dplyr::bind_rows(oldTable)
1332 }
1333 }
1334}
1335
1336#' @importFrom magrittr debug_pipe
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001337#' @importFrom stringr str_match str_split str_detect
1338#' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when
1339#'
1340snippet2FreqTable <- function(snippet,
1341 minOccur = 5,
1342 leftContextSize = 5,
1343 rightContextSize = 5,
1344 ignoreCollocateCase = FALSE,
1345 stopwords = c(),
1346 tokenizeRegex = "([! )(\uc2\uab,.:?\u201e\u201c\'\"]+|&quot;)",
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001347 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001348 oldTable = data.frame(word = rep(NA, 1), frequency = rep(NA, 1)),
1349 verbose = TRUE) {
1350 word <- NULL # https://stackoverflow.com/questions/8096313/no-visible-binding-for-global-variable-note-in-r-cmd-check
1351 frequency <- NULL
1352
1353 if (length(snippet) < 1) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001354 dplyr::tibble(word = c(), frequency = c())
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001355 } else if (length(snippet) > 1) {
Marc Kupietza47d1502023-04-18 15:26:47 +02001356 log_info(verbose, paste("Joining", length(snippet), "kwics\n"))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001357 for (s in snippet) {
1358 oldTable <- snippet2FreqTable(
1359 s,
1360 leftContextSize = leftContextSize,
1361 rightContextSize = rightContextSize,
Marc Kupietz47d0d2b2021-12-19 16:38:52 +01001362 collocateFilterRegex = collocateFilterRegex,
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001363 oldTable = oldTable,
1364 stopwords = stopwords
1365 )
1366 }
Marc Kupietza47d1502023-04-18 15:26:47 +02001367 log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n"))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001368 oldTable |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001369 group_by(word) |>
1370 mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001371 summarise(frequency = sum(frequency), .groups = "drop") |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001372 arrange(desc(frequency))
1373 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001374 stopwordsTable <- dplyr::tibble(word = stopwords)
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001375 match <-
1376 str_match(
1377 snippet,
1378 '<span class="context-left">(<span class="more"></span>)?(.*[^ ]) *</span><span class="match"><mark>.*</mark></span><span class="context-right"> *([^<]*)'
1379 )
1380
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001381 left <- if (leftContextSize > 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001382 tail(unlist(str_split(match[1, 3], tokenizeRegex)), leftContextSize)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001383 } else {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001384 ""
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001385 }
1386 # cat(paste("left:", left, "\n", collapse=" "))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001387
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001388 right <- if (rightContextSize > 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001389 head(unlist(str_split(match[1, 4], tokenizeRegex)), rightContextSize)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001390 } else {
1391 ""
1392 }
1393 # cat(paste("right:", right, "\n", collapse=" "))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001394
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001395 if (is.na(left[1]) || is.na(right[1]) || length(left) + length(right) == 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001396 oldTable
1397 } else {
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001398 table(c(left, right)) |>
1399 dplyr::as_tibble(.name_repair = "minimal") |>
1400 dplyr::rename(word = 1, frequency = 2) |>
1401 dplyr::filter(str_detect(word, collocateFilterRegex)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001402 dplyr::anti_join(stopwordsTable, by = "word") |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001403 dplyr::bind_rows(oldTable)
1404 }
1405 }
1406}
1407
1408#' Preliminary synsemantic stopwords function
1409#'
1410#' @description
Marc Kupietz67edcb52021-09-20 21:54:24 +02001411#' `r lifecycle::badge("experimental")`
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001412#'
1413#' Preliminary synsemantic stopwords function to be used in collocation analysis.
1414#'
1415#' @details
1416#' Currently only suitable for German. See stopwords package for other languages.
1417#'
1418#' @param ... future arguments for language detection
1419#'
1420#' @family collocation analysis functions
1421#' @return Vector of synsemantic stopwords.
1422#' @export
1423synsemanticStopwords <- function(...) {
Marc Kupietzc79155b2025-10-19 13:42:55 +02001424 base <- c(
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001425 "der",
1426 "die",
1427 "und",
1428 "in",
1429 "den",
1430 "von",
1431 "mit",
1432 "das",
1433 "zu",
1434 "im",
1435 "ist",
1436 "auf",
1437 "sich",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001438 "des",
1439 "dem",
1440 "nicht",
1441 "ein",
1442 "eine",
1443 "es",
1444 "auch",
1445 "an",
1446 "als",
1447 "am",
1448 "aus",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001449 "bei",
1450 "er",
1451 "dass",
1452 "sie",
1453 "nach",
1454 "um",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001455 "zum",
1456 "noch",
1457 "war",
1458 "einen",
1459 "einer",
1460 "wie",
1461 "einem",
1462 "vor",
1463 "bis",
1464 "\u00fcber",
1465 "so",
1466 "aber",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001467 "diese",
Marc Kupietzc79155b2025-10-19 13:42:55 +02001468 "oder"
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001469 )
Marc Kupietzc79155b2025-10-19 13:42:55 +02001470
1471 lower <- unique(tolower(base))
1472 capitalized <- paste0(toupper(substr(lower, 1, 1)), substring(lower, 2))
1473
1474 unique(c(lower, capitalized))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001475}
1476
Marc Kupietz5a336b62021-11-27 17:51:35 +01001477
Marc Kupietz76b05592021-12-19 16:26:15 +01001478# #' @export
Marc Kupietz5a336b62021-11-27 17:51:35 +01001479findExample <-
1480 function(kco,
1481 query,
1482 vc = "",
1483 matchOnly = TRUE) {
1484 out <- character(length = length(query))
1485
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001486 if (length(vc) < length(query)) {
Marc Kupietz5a336b62021-11-27 17:51:35 +01001487 vc <- rep(vc, length(query))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001488 }
Marc Kupietz5a336b62021-11-27 17:51:35 +01001489
1490 for (i in seq_along(query)) {
1491 q <- corpusQuery(kco, paste0("(", query[i], ")"), vc = vc[i], metadataOnly = FALSE)
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001492 if (q@totalResults > 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001493 q <- fetchNext(q, maxFetch = 50, randomizePageOrder = F)
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001494 example <- as.character((q@collectedMatches)$snippet[1])
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001495 out[i] <- if (matchOnly) {
1496 gsub(".*<mark>(.+)</mark>.*", "\\1", example)
Marc Kupietz5a336b62021-11-27 17:51:35 +01001497 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001498 stringr::str_replace(example, "<[^>]*>", "")
Marc Kupietz5a336b62021-11-27 17:51:35 +01001499 }
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001500 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001501 out[i] <- ""
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001502 }
Marc Kupietz5a336b62021-11-27 17:51:35 +01001503 }
1504 out
1505 }
1506
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001507collocatesQuery <-
1508 function(kco,
1509 query,
1510 vc = "",
1511 minOccur = 5,
1512 leftContextSize = 5,
1513 rightContextSize = 5,
1514 searchHitsSampleLimit = 20000,
1515 ignoreCollocateCase = FALSE,
1516 stopwords = c(),
Marc Kupietzb2862d42025-10-18 10:17:49 +02001517 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001518 ...) {
1519 frequency <- NULL
1520 q <- corpusQuery(kco, query, vc, metadataOnly = F, ...)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001521 if (q@totalResults == 0) {
1522 tibble(word = c(), frequency = c())
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001523 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001524 q <- fetchNext(q, maxFetch = searchHitsSampleLimit, randomizePageOrder = TRUE)
1525 matches2FreqTable(q@collectedMatches,
1526 0,
1527 minOccur = minOccur,
1528 leftContextSize = leftContextSize,
1529 rightContextSize = rightContextSize,
1530 ignoreCollocateCase = ignoreCollocateCase,
1531 stopwords = stopwords,
Marc Kupietzb2862d42025-10-18 10:17:49 +02001532 collocateFilterRegex = collocateFilterRegex,
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001533 ...,
1534 verbose = kco@verbose
1535 ) |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001536 mutate(frequency = frequency * q@totalResults / min(q@totalResults, searchHitsSampleLimit)) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001537 filter(frequency >= minOccur)
1538 }
1539 }