blob: 81ca66119d11890db8fa9add3a2736110e0ee113 [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
408 distinct_pairs <- dplyr::distinct(result, node, collocate)
409 if (nrow(distinct_pairs) == 0) {
410 return(result)
411 }
412
413 collocates_by_node <- split(as.character(distinct_pairs$collocate), distinct_pairs$node)
414 if (length(collocates_by_node) == 0) {
415 return(result)
416 }
417
418 required_combinations <- unique(as.data.frame(grid[, c("node", "vc", "vcLabel")], drop = FALSE))
419 for (i in seq_len(nrow(required_combinations))) {
420 node_value <- required_combinations$node[i]
421 vc_value <- required_combinations$vc[i]
422
423 collocate_pool <- collocates_by_node[[node_value]]
424 if (is.null(collocate_pool) || length(collocate_pool) == 0) {
425 next
426 }
427
428 existing_idx <- result$node == node_value & result$vc == vc_value
429 existing_collocates <- unique(as.character(result$collocate[existing_idx]))
430 missing_collocates <- setdiff(unique(collocate_pool), existing_collocates)
431 missing_collocates <- missing_collocates[!is.na(missing_collocates) & nzchar(missing_collocates)]
432
433 if (length(missing_collocates) == 0) {
434 next
435 }
436
437 context_rows <- result[result$node == node_value & result$vc == vc_value, , drop = FALSE]
438 if (nrow(context_rows) == 0) {
439 context_rows <- result[result$node == node_value, , drop = FALSE]
440 }
441
442 left_size <- context_rows$leftContextSize[!is.na(context_rows$leftContextSize)][1]
443 if (is.na(left_size) || length(left_size) == 0) {
444 left_size <- result$leftContextSize[!is.na(result$leftContextSize)][1]
445 }
446 if (is.na(left_size) || length(left_size) == 0) {
447 left_size <- 5
448 }
449
450 right_size <- context_rows$rightContextSize[!is.na(context_rows$rightContextSize)][1]
451 if (is.na(right_size) || length(right_size) == 0) {
452 right_size <- result$rightContextSize[!is.na(result$rightContextSize)][1]
453 }
454 if (is.na(right_size) || length(right_size) == 0) {
455 right_size <- 5
456 }
457
458 within_span_value <- ""
459 if ("query" %in% names(context_rows)) {
460 query_candidate <- context_rows$query[!is.na(context_rows$query) & nzchar(context_rows$query)][1]
461 if (!is.na(query_candidate) && nzchar(query_candidate)) {
462 match_one <- regexec("^\\(*contains\\(<([^>]+)>,", query_candidate)
463 matches <- regmatches(query_candidate, match_one)
464 if (length(matches) >= 1 && length(matches[[1]]) >= 2) {
465 within_span_value <- matches[[1]][2]
466 }
467 }
468 }
469
470 new_rows <- collocationScoreQuery(
471 kco,
472 node = node_value,
473 collocate = missing_collocates,
474 vc = vc_value,
475 leftContextSize = left_size,
476 rightContextSize = right_size,
477 ignoreCollocateCase = ignoreCollocateCase,
478 withinSpan = within_span_value,
479 ...
480 )
481
482 if (nrow(new_rows) == 0) {
483 next
484 }
485
486 if (!is.null(required_combinations$vcLabel[i]) && !is.na(required_combinations$vcLabel[i]) && required_combinations$vcLabel[i] != "" && "label" %in% names(new_rows)) {
487 new_rows$label <- required_combinations$vcLabel[i]
488 }
489
490 result <- dplyr::bind_rows(result, new_rows)
491 }
492
493 result
494}
495
Marc Kupietz0a292632025-10-19 14:04:36 +0200496inject_focus_into_query <- function(query) {
497 if (is.null(query) || is.na(query)) {
498 return(query)
499 }
500
501 trimmed <- trimws(query)
502 if (!nzchar(trimmed)) {
503 return(query)
504 }
505
506 if (!grepl("^contains\\(<[^>]+>", trimmed, perl = TRUE)) {
507 return(query)
508 }
509
510 if (grepl("focus\\(", trimmed, perl = TRUE)) {
511 return(query)
512 }
513
514 pattern <- "^contains\\(<([^>]+)>\\s*,\\s*\\((.*)\\)\\)\\s*$"
515 matches <- regexec(pattern, trimmed, perl = TRUE)
516 components <- regmatches(trimmed, matches)
517 if (length(components) == 0 || length(components[[1]]) < 3) {
518 return(query)
519 }
520
521 span <- components[[1]][2]
522 inner <- components[[1]][3]
523 parts <- strsplit(inner, "\\|", perl = TRUE)[[1]]
524 parts <- trimws(parts)
525 parts <- parts[nzchar(parts)]
526
527 if (length(parts) == 0) {
528 return(query)
529 }
530
531 focused <- paste0("focus({", parts, "})")
532 combined <- paste(focused, collapse = " | ")
533
534 sprintf("contains(<%s>, (%s))", span, combined)
535}
536
Marc Kupietz77852b22025-10-19 11:35:34 +0200537add_multi_vc_comparisons <- function(result, missingScoreQuantile = 0.05) {
Marc Kupietz09b1c082026-05-01 14:45:47 +0200538 label <- node <- collocate <- vc <- webUIRequestUrl <- NULL
Marc Kupietzc4540a22025-10-14 17:39:53 +0200539
540 if (!"label" %in% names(result) || dplyr::n_distinct(result$label) < 2) {
541 return(result)
542 }
543
544 numeric_cols <- names(result)[vapply(result, is.numeric, logical(1))]
545 non_score_cols <- c("N", "O", "O1", "O2", "E", "w", "leftContextSize", "rightContextSize", "frequency")
546 score_cols <- setdiff(numeric_cols, non_score_cols)
547
548 if (length(score_cols) == 0) {
549 return(result)
550 }
551
Marc Kupietz9894a372025-10-18 14:51:29 +0200552 compute_score_floor <- function(values) {
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200553 # Estimate a conservative floor so missing scores can be imputed without favoring any label
Marc Kupietz9894a372025-10-18 14:51:29 +0200554 finite_values <- values[is.finite(values)]
555 if (length(finite_values) == 0) {
556 return(0)
557 }
558
559 prob <- min(max(missingScoreQuantile, 0), 0.5)
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200560 # Use a lower quantile as the anchor to stay near the weakest attested scores
Marc Kupietz9894a372025-10-18 14:51:29 +0200561 q_val <- suppressWarnings(stats::quantile(finite_values,
562 probs = prob,
563 names = FALSE,
564 type = 7
565 ))
566
567 if (!is.finite(q_val)) {
568 q_val <- suppressWarnings(min(finite_values, na.rm = TRUE))
569 }
570
571 min_val <- suppressWarnings(min(finite_values, na.rm = TRUE))
572 if (!is.finite(min_val)) {
573 min_val <- 0
574 }
575
576 spread_candidates <- c(
577 suppressWarnings(stats::IQR(finite_values, na.rm = TRUE, type = 7)),
578 stats::sd(finite_values, na.rm = TRUE),
579 abs(q_val) * 0.1,
580 abs(min_val - q_val)
581 )
582 spread_candidates <- spread_candidates[is.finite(spread_candidates)]
583
584 spread <- 0
585 if (length(spread_candidates) > 0) {
586 spread <- max(spread_candidates)
587 }
588 if (!is.finite(spread) || spread == 0) {
589 spread <- max(abs(q_val), abs(min_val), 1e-06)
590 }
591
Marc Kupietz4cbb5472025-10-19 12:15:25 +0200592 # Step away from the anchor by a robust spread estimate to avoid ties with real scores
Marc Kupietz9894a372025-10-18 14:51:29 +0200593 candidate <- q_val - spread
594 if (!is.finite(candidate)) {
595 candidate <- min_val
596 }
597
598 floor_value <- suppressWarnings(min(c(candidate, min_val), na.rm = TRUE))
599 if (!is.finite(floor_value)) {
600 floor_value <- min_val
601 }
602 if (!is.finite(floor_value)) {
603 floor_value <- 0
604 }
605
606 floor_value
607 }
608
609 score_replacements <- stats::setNames(
610 vapply(score_cols, function(col) {
611 compute_score_floor(result[[col]])
612 }, numeric(1)),
613 score_cols
614 )
615
Marc Kupietzc4540a22025-10-14 17:39:53 +0200616 comparison <- result |>
Marc Kupietz28a29842025-10-18 12:25:09 +0200617 dplyr::select(node, collocate, label, dplyr::all_of(score_cols)) |>
618 tidyr::pivot_wider(
Marc Kupietzc4540a22025-10-14 17:39:53 +0200619 names_from = label,
Marc Kupietz28a29842025-10-18 12:25:09 +0200620 values_from = dplyr::all_of(score_cols),
Marc Kupietzc4540a22025-10-14 17:39:53 +0200621 names_glue = "{.value}_{make.names(label)}",
622 values_fn = dplyr::first
623 )
624
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200625 raw_labels <- unique(result$label)
626 labels <- make.names(raw_labels)
627 label_map <- stats::setNames(raw_labels, labels)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200628 vc_map <- result |>
629 dplyr::select(label, vc) |>
630 dplyr::filter(!is.na(label), label != "") |>
631 dplyr::distinct(label, .keep_all = TRUE)
632 vc_map <- stats::setNames(vc_map$vc, make.names(vc_map$label))
633
634 replace_web_ui_cq <- function(url, vc_value) {
635 if (length(url) == 0 || is.na(url) || url == "") {
636 return(NA_character_)
637 }
638 if (length(vc_value) == 0 || is.na(vc_value)) {
639 vc_value <- ""
640 }
641 encoded_vc <- urltools::url_encode(enc2utf8(as.character(vc_value)))
642 if (grepl("([?&]cq=)[^&]*", url, perl = TRUE)) {
643 return(sub("([?&]cq=)[^&]*", paste0("\\1", encoded_vc), url, perl = TRUE))
644 }
645 if (encoded_vc == "") {
646 return(url)
647 }
648 paste0(url, ifelse(grepl("\\?", url), "&", "?"), "cq=", encoded_vc)
649 }
650
651 if ("webUIRequestUrl" %in% names(result)) {
652 url_data <- result |>
653 dplyr::select(node, collocate, label, webUIRequestUrl) |>
654 tidyr::pivot_wider(
655 names_from = label,
656 values_from = webUIRequestUrl,
657 names_glue = "webUIRequestUrl_{make.names(label)}",
658 values_fn = dplyr::first
659 )
660
661 comparison <- dplyr::left_join(comparison, url_data, by = c("node", "collocate"))
662
663 url_cols <- paste0("webUIRequestUrl_", labels)
664 present_url_cols <- intersect(url_cols, names(comparison))
665 fallback_urls <- vapply(seq_len(nrow(comparison)), function(i) {
666 urls <- unlist(comparison[i, present_url_cols, drop = FALSE], use.names = FALSE)
667 urls <- as.character(urls)
668 urls <- urls[!is.na(urls) & urls != ""]
669 if (length(urls) == 0) {
670 NA_character_
671 } else {
672 urls[1]
673 }
674 }, character(1))
675
676 for (safe_label in labels) {
677 url_col <- paste0("webUIRequestUrl_", safe_label)
678 if (!url_col %in% names(comparison)) {
679 comparison[[url_col]] <- NA_character_
680 }
681 missing_urls <- is.na(comparison[[url_col]]) | comparison[[url_col]] == ""
682 if (any(missing_urls)) {
683 comparison[[url_col]][missing_urls] <- vapply(
684 fallback_urls[missing_urls],
685 replace_web_ui_cq,
686 character(1),
687 vc_value = vc_map[[safe_label]]
688 )
689 }
690 }
691 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200692
Marc Kupietz28a29842025-10-18 12:25:09 +0200693 rank_data <- result |>
694 dplyr::distinct(node, collocate)
695
696 for (i in seq_along(raw_labels)) {
697 raw_lab <- raw_labels[i]
698 safe_lab <- labels[i]
699 label_df <- result[result$label == raw_lab, c("node", "collocate", score_cols), drop = FALSE]
700 if (nrow(label_df) == 0) {
701 next
702 }
703 label_df <- dplyr::distinct(label_df)
704 rank_tbl <- label_df[, c("node", "collocate"), drop = FALSE]
705 for (col in score_cols) {
706 rank_col_name <- paste0("rank_", safe_lab, "_", col)
Marc Kupietz130a2a22025-10-18 16:09:23 +0200707 percentile_col_name <- paste0("percentile_rank_", safe_lab, "_", col)
Marc Kupietz28a29842025-10-18 12:25:09 +0200708 values <- label_df[[col]]
709 ranks <- rep(NA_real_, length(values))
Marc Kupietz130a2a22025-10-18 16:09:23 +0200710 percentiles <- rep(NA_real_, length(values))
Marc Kupietz28a29842025-10-18 12:25:09 +0200711 valid_idx <- which(!is.na(values))
712 if (length(valid_idx) > 0) {
713 ranks[valid_idx] <- rank(-values[valid_idx], ties.method = "first")
Marc Kupietz130a2a22025-10-18 16:09:23 +0200714 total <- length(valid_idx)
715 percentiles[valid_idx] <- 1 - (ranks[valid_idx] - 1) / total
Marc Kupietz28a29842025-10-18 12:25:09 +0200716 }
717 rank_tbl[[rank_col_name]] <- ranks
Marc Kupietz130a2a22025-10-18 16:09:23 +0200718 rank_tbl[[percentile_col_name]] <- percentiles
Marc Kupietz28a29842025-10-18 12:25:09 +0200719 }
720 rank_data <- dplyr::left_join(rank_data, rank_tbl, by = c("node", "collocate"))
721 }
722
723 comparison <- dplyr::left_join(comparison, rank_data, by = c("node", "collocate"))
724
725 rank_replacements <- numeric(0)
726 rank_column_names <- grep("^rank_", names(comparison), value = TRUE)
727 if (length(rank_column_names) > 0) {
728 rank_replacements <- stats::setNames(
729 vapply(rank_column_names, function(col) {
730 col_values <- comparison[[col]]
731 valid_values <- col_values[!is.na(col_values)]
732 if (length(valid_values) == 0) {
733 nrow(comparison) + 1
734 } else {
735 suppressWarnings(max(valid_values, na.rm = TRUE)) + 1
736 }
737 }, numeric(1)),
738 rank_column_names
739 )
740 }
741
Marc Kupietz130a2a22025-10-18 16:09:23 +0200742 percentile_replacements <- numeric(0)
743 percentile_column_names <- grep("^percentile_rank_", names(comparison), value = TRUE)
744 if (length(percentile_column_names) > 0) {
745 percentile_replacements <- stats::setNames(
746 rep(0, length(percentile_column_names)),
747 percentile_column_names
748 )
749 }
750
Marc Kupietz28a29842025-10-18 12:25:09 +0200751 collapse_label_values <- function(indices, safe_labels_vec) {
752 if (length(indices) == 0) {
753 return(NA_character_)
754 }
755 labs <- label_map[safe_labels_vec[indices]]
756 fallback <- safe_labels_vec[indices]
757 labs[is.na(labs) | labs == ""] <- fallback[is.na(labs) | labs == ""]
758 labs <- labs[!is.na(labs) & labs != ""]
759 if (length(labs) == 0) {
760 return(NA_character_)
761 }
762 paste(unique(labs), collapse = ", ")
763 }
764
Marc Kupietz09b1c082026-05-01 14:45:47 +0200765 collapse_url_values <- function(indices, url_values) {
766 if (length(indices) == 0 || is.null(url_values)) {
767 return(NA_character_)
768 }
769 urls <- as.character(url_values[indices])
770 urls <- urls[!is.na(urls) & urls != ""]
771 if (length(urls) == 0) {
772 return(NA_character_)
773 }
774 paste(unique(urls), collapse = ", ")
775 }
776
Marc Kupietzc4540a22025-10-14 17:39:53 +0200777 if (length(labels) == 2) {
Marc Kupietz9894a372025-10-18 14:51:29 +0200778 fill_scores <- function(x, y, measure_col) {
779 replacement <- score_replacements[[measure_col]]
780 fallback_min <- suppressWarnings(min(c(x, y), na.rm = TRUE))
781 if (!is.finite(fallback_min)) {
782 fallback_min <- 0
Marc Kupietzc4540a22025-10-14 17:39:53 +0200783 }
Marc Kupietz9894a372025-10-18 14:51:29 +0200784 if (!is.null(replacement) && is.finite(replacement)) {
785 replacement <- min(replacement, fallback_min)
786 } else {
787 replacement <- fallback_min
788 }
789 if (!is.finite(replacement)) {
790 replacement <- 0
791 }
792 if (any(is.na(x))) {
793 x[is.na(x)] <- replacement
794 }
795 if (any(is.na(y))) {
796 y[is.na(y)] <- replacement
797 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200798 list(x = x, y = y)
799 }
800
Marc Kupietz130a2a22025-10-18 16:09:23 +0200801 fill_percentiles <- function(x, y, left_pct_col, right_pct_col) {
802 replacement_left <- percentile_replacements[[left_pct_col]]
803 if (is.null(replacement_left) || !is.finite(replacement_left)) {
804 replacement_left <- 0
805 }
806 replacement_right <- percentile_replacements[[right_pct_col]]
807 if (is.null(replacement_right) || !is.finite(replacement_right)) {
808 replacement_right <- 0
809 }
810 if (any(is.na(x))) {
811 x[is.na(x)] <- replacement_left
812 }
813 if (any(is.na(y))) {
814 y[is.na(y)] <- replacement_right
815 }
816 list(x = x, y = y)
817 }
818
Marc Kupietz28a29842025-10-18 12:25:09 +0200819 fill_ranks <- function(x, y, left_rank_col, right_rank_col) {
820 fallback <- nrow(comparison) + 1
821 replacement_left <- rank_replacements[[left_rank_col]]
822 if (is.null(replacement_left) || !is.finite(replacement_left)) {
823 replacement_left <- fallback
Marc Kupietzc4540a22025-10-14 17:39:53 +0200824 }
Marc Kupietz28a29842025-10-18 12:25:09 +0200825 replacement_right <- rank_replacements[[right_rank_col]]
826 if (is.null(replacement_right) || !is.finite(replacement_right)) {
827 replacement_right <- fallback
828 }
829 if (any(is.na(x))) {
830 x[is.na(x)] <- replacement_left
831 }
832 if (any(is.na(y))) {
833 y[is.na(y)] <- replacement_right
834 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200835 list(x = x, y = y)
836 }
837
838 left_label <- labels[1]
839 right_label <- labels[2]
840
841 for (col in score_cols) {
842 left_col <- paste0(col, "_", left_label)
843 right_col <- paste0(col, "_", right_label)
844 if (!all(c(left_col, right_col) %in% names(comparison))) {
845 next
846 }
Marc Kupietzdb2fabd2026-04-27 15:01:37 +0200847 filled <- fill_scores(comparison[[left_col]], comparison[[right_col]], col)
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200848 comparison[[left_col]] <- filled$x
849 comparison[[right_col]] <- filled$y
Marc Kupietzc4540a22025-10-14 17:39:53 +0200850 comparison[[paste0("delta_", col)]] <- filled$x - filled$y
Marc Kupietz28a29842025-10-18 12:25:09 +0200851 rank_left <- paste0("rank_", left_label, "_", col)
852 rank_right <- paste0("rank_", right_label, "_", col)
853 if (all(c(rank_left, rank_right) %in% names(comparison))) {
854 filled_rank <- fill_ranks(
855 comparison[[rank_left]],
856 comparison[[rank_right]],
857 rank_left,
858 rank_right
859 )
860 comparison[[paste0("delta_rank_", col)]] <- filled_rank$x - filled_rank$y
861 }
Marc Kupietz130a2a22025-10-18 16:09:23 +0200862 pct_left <- paste0("percentile_rank_", left_label, "_", col)
863 pct_right <- paste0("percentile_rank_", right_label, "_", col)
864 if (all(c(pct_left, pct_right) %in% names(comparison))) {
865 filled_pct <- fill_percentiles(
866 comparison[[pct_left]],
867 comparison[[pct_right]],
868 pct_left,
869 pct_right
870 )
871 comparison[[paste0("delta_percentile_rank_", col)]] <- filled_pct$x - filled_pct$y
872 }
Marc Kupietzc4540a22025-10-14 17:39:53 +0200873 }
874 }
875
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200876 for (col in score_cols) {
877 value_cols <- paste0(col, "_", labels)
878 existing <- value_cols %in% names(comparison)
879 if (!any(existing)) {
880 next
881 }
882 value_cols <- value_cols[existing]
883 safe_labels <- labels[existing]
884
885 score_values <- comparison[, value_cols, drop = FALSE]
886
887 winner_label_col <- paste0("winner_", col)
888 winner_value_col <- paste0("winner_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +0200889 winner_url_col <- paste0("winner_", col, "_webUIRequestUrl")
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200890 runner_label_col <- paste0("runner_up_", col)
891 runner_value_col <- paste0("runner_up_", col, "_value")
Marc Kupietzb2862d42025-10-18 10:17:49 +0200892 loser_label_col <- paste0("loser_", col)
893 loser_value_col <- paste0("loser_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +0200894 loser_url_col <- paste0("loser_", col, "_webUIRequestUrl")
Marc Kupietzb2862d42025-10-18 10:17:49 +0200895 max_delta_col <- paste0("max_delta_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200896 url_cols <- paste0("webUIRequestUrl_", safe_labels)
897 has_urls <- all(url_cols %in% names(comparison))
898 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200899
900 if (nrow(score_values) == 0) {
901 comparison[[winner_label_col]] <- character(0)
902 comparison[[winner_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200903 if (has_urls) {
904 comparison[[winner_url_col]] <- character(0)
905 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200906 comparison[[runner_label_col]] <- character(0)
907 comparison[[runner_value_col]] <- numeric(0)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200908 comparison[[loser_label_col]] <- character(0)
909 comparison[[loser_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200910 if (has_urls) {
911 comparison[[loser_url_col]] <- character(0)
912 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200913 comparison[[max_delta_col]] <- numeric(0)
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200914 next
915 }
916
917 score_matrix <- as.matrix(score_values)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200918 storage.mode(score_matrix) <- "numeric"
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200919
Marc Kupietzb2862d42025-10-18 10:17:49 +0200920 n_rows <- nrow(score_matrix)
921 winner_labels <- rep(NA_character_, n_rows)
922 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200923 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200924 runner_labels <- rep(NA_character_, n_rows)
925 runner_values <- rep(NA_real_, n_rows)
926 loser_labels <- rep(NA_character_, n_rows)
927 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +0200928 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200929 max_deltas <- rep(NA_real_, n_rows)
930
Marc Kupietzb2862d42025-10-18 10:17:49 +0200931 if (n_rows > 0) {
932 for (i in seq_len(n_rows)) {
933 numeric_row <- as.numeric(score_matrix[i, ])
934 if (all(is.na(numeric_row))) {
935 next
936 }
937
Marc Kupietz9894a372025-10-18 14:51:29 +0200938 replacement <- score_replacements[[col]]
939 fallback_min <- suppressWarnings(min(numeric_row, na.rm = TRUE))
940 if (!is.finite(fallback_min)) {
941 fallback_min <- 0
Marc Kupietzb2862d42025-10-18 10:17:49 +0200942 }
Marc Kupietz9894a372025-10-18 14:51:29 +0200943 if (!is.null(replacement) && is.finite(replacement)) {
944 replacement <- min(replacement, fallback_min)
945 } else {
946 replacement <- fallback_min
947 }
948 if (!is.finite(replacement)) {
949 replacement <- 0
950 }
951 if (any(is.na(numeric_row))) {
952 numeric_row[is.na(numeric_row)] <- replacement
953 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200954 score_matrix[i, ] <- numeric_row
955
956 max_val <- suppressWarnings(max(numeric_row, na.rm = TRUE))
957 max_idx <- which(numeric_row == max_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200958 winner_labels[i] <- collapse_label_values(max_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200959 winner_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +0200960 if (has_urls) {
961 winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ])
962 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200963
964 unique_vals <- sort(unique(numeric_row), decreasing = TRUE)
965 if (length(unique_vals) >= 2) {
966 runner_val <- unique_vals[2]
967 runner_idx <- which(numeric_row == runner_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200968 runner_labels[i] <- collapse_label_values(runner_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200969 runner_values[i] <- runner_val
970 }
971
972 min_val <- suppressWarnings(min(numeric_row, na.rm = TRUE))
973 min_idx <- which(numeric_row == min_val)
Marc Kupietz28a29842025-10-18 12:25:09 +0200974 loser_labels[i] <- collapse_label_values(min_idx, safe_labels)
Marc Kupietzb2862d42025-10-18 10:17:49 +0200975 loser_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +0200976 if (has_urls) {
977 loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ])
978 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200979
980 if (is.finite(max_val) && is.finite(min_val)) {
981 max_deltas[i] <- max_val - min_val
982 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200983 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200984 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200985
Marc Kupietzb2862d42025-10-18 10:17:49 +0200986 comparison[, value_cols] <- score_matrix
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200987 comparison[[winner_label_col]] <- winner_labels
988 comparison[[winner_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +0200989 if (has_urls) {
990 comparison[[winner_url_col]] <- winner_urls
991 }
Marc Kupietz5e35d7a2025-10-17 21:21:22 +0200992 comparison[[runner_label_col]] <- runner_labels
993 comparison[[runner_value_col]] <- runner_values
Marc Kupietzb2862d42025-10-18 10:17:49 +0200994 comparison[[loser_label_col]] <- loser_labels
995 comparison[[loser_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +0200996 if (has_urls) {
997 comparison[[loser_url_col]] <- loser_urls
998 }
Marc Kupietzb2862d42025-10-18 10:17:49 +0200999 comparison[[max_delta_col]] <- max_deltas
Marc Kupietz5e35d7a2025-10-17 21:21:22 +02001000 }
1001
Marc Kupietz28a29842025-10-18 12:25:09 +02001002 for (col in score_cols) {
1003 rank_cols <- paste0("rank_", labels, "_", col)
1004 existing <- rank_cols %in% names(comparison)
1005 if (!any(existing)) {
1006 next
1007 }
1008 rank_cols <- rank_cols[existing]
1009 safe_labels <- labels[existing]
1010 rank_values <- comparison[, rank_cols, drop = FALSE]
1011
1012 winner_rank_label_col <- paste0("winner_rank_", col)
1013 winner_rank_value_col <- paste0("winner_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001014 winner_rank_url_col <- paste0("winner_rank_", col, "_webUIRequestUrl")
Marc Kupietz28a29842025-10-18 12:25:09 +02001015 runner_rank_label_col <- paste0("runner_up_rank_", col)
1016 runner_rank_value_col <- paste0("runner_up_rank_", col, "_value")
1017 loser_rank_label_col <- paste0("loser_rank_", col)
1018 loser_rank_value_col <- paste0("loser_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001019 loser_rank_url_col <- paste0("loser_rank_", col, "_webUIRequestUrl")
Marc Kupietz28a29842025-10-18 12:25:09 +02001020 max_delta_rank_col <- paste0("max_delta_rank_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001021 url_cols <- paste0("webUIRequestUrl_", safe_labels)
1022 has_urls <- all(url_cols %in% names(comparison))
1023 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz28a29842025-10-18 12:25:09 +02001024
1025 if (nrow(rank_values) == 0) {
1026 comparison[[winner_rank_label_col]] <- character(0)
1027 comparison[[winner_rank_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001028 if (has_urls) {
1029 comparison[[winner_rank_url_col]] <- character(0)
1030 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001031 comparison[[runner_rank_label_col]] <- character(0)
1032 comparison[[runner_rank_value_col]] <- numeric(0)
1033 comparison[[loser_rank_label_col]] <- character(0)
1034 comparison[[loser_rank_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001035 if (has_urls) {
1036 comparison[[loser_rank_url_col]] <- character(0)
1037 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001038 comparison[[max_delta_rank_col]] <- numeric(0)
1039 next
1040 }
1041
Marc Kupietzdb2fabd2026-04-27 15:01:37 +02001042 rank_matrix <- as.matrix(rank_values)
1043 storage.mode(rank_matrix) <- "numeric"
Marc Kupietz28a29842025-10-18 12:25:09 +02001044
1045 n_rows <- nrow(rank_matrix)
1046 winner_labels <- rep(NA_character_, n_rows)
1047 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001048 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietz28a29842025-10-18 12:25:09 +02001049 runner_labels <- rep(NA_character_, n_rows)
1050 runner_values <- rep(NA_real_, n_rows)
1051 loser_labels <- rep(NA_character_, n_rows)
1052 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001053 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietz28a29842025-10-18 12:25:09 +02001054 max_deltas <- rep(NA_real_, n_rows)
1055
1056 for (i in seq_len(n_rows)) {
1057 numeric_row <- as.numeric(rank_matrix[i, ])
1058 if (all(is.na(numeric_row))) {
1059 next
1060 }
1061
1062 if (length(rank_cols) > 0) {
1063 replacement_vec <- rank_replacements[rank_cols]
1064 replacement_vec[is.na(replacement_vec)] <- nrow(comparison) + 1
1065 missing_idx <- which(is.na(numeric_row))
1066 if (length(missing_idx) > 0) {
1067 numeric_row[missing_idx] <- replacement_vec[missing_idx]
1068 }
1069 }
1070
1071 valid_idx <- seq_along(numeric_row)
1072 valid_values <- numeric_row[valid_idx]
1073 min_val <- suppressWarnings(min(valid_values, na.rm = TRUE))
1074 min_positions <- valid_idx[which(valid_values == min_val)]
1075 winner_labels[i] <- collapse_label_values(min_positions, safe_labels)
1076 winner_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001077 if (has_urls) {
1078 winner_urls[i] <- collapse_url_values(min_positions, url_values[i, ])
1079 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001080
1081 ordered_vals <- sort(unique(valid_values), decreasing = FALSE)
1082 if (length(ordered_vals) >= 2) {
1083 runner_val <- ordered_vals[2]
1084 runner_positions <- valid_idx[which(valid_values == runner_val)]
1085 runner_labels[i] <- collapse_label_values(runner_positions, safe_labels)
1086 runner_values[i] <- runner_val
1087 }
1088
1089 max_val <- suppressWarnings(max(valid_values, na.rm = TRUE))
1090 max_positions <- valid_idx[which(valid_values == max_val)]
1091 loser_labels[i] <- collapse_label_values(max_positions, safe_labels)
1092 loser_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001093 if (has_urls) {
1094 loser_urls[i] <- collapse_url_values(max_positions, url_values[i, ])
1095 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001096
1097 if (is.finite(max_val) && is.finite(min_val)) {
1098 max_deltas[i] <- max_val - min_val
1099 }
1100 }
1101
1102 comparison[[winner_rank_label_col]] <- winner_labels
1103 comparison[[winner_rank_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001104 if (has_urls) {
1105 comparison[[winner_rank_url_col]] <- winner_urls
1106 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001107 comparison[[runner_rank_label_col]] <- runner_labels
1108 comparison[[runner_rank_value_col]] <- runner_values
1109 comparison[[loser_rank_label_col]] <- loser_labels
1110 comparison[[loser_rank_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001111 if (has_urls) {
1112 comparison[[loser_rank_url_col]] <- loser_urls
1113 }
Marc Kupietz28a29842025-10-18 12:25:09 +02001114 comparison[[max_delta_rank_col]] <- max_deltas
1115 }
1116
Marc Kupietz130a2a22025-10-18 16:09:23 +02001117 for (col in score_cols) {
1118 pct_cols <- paste0("percentile_rank_", labels, "_", col)
1119 existing <- pct_cols %in% names(comparison)
1120 if (!any(existing)) {
1121 next
1122 }
1123 pct_cols <- pct_cols[existing]
1124 safe_labels <- labels[existing]
1125 pct_values <- comparison[, pct_cols, drop = FALSE]
1126
1127 winner_pct_label_col <- paste0("winner_percentile_rank_", col)
1128 winner_pct_value_col <- paste0("winner_percentile_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001129 winner_pct_url_col <- paste0("winner_percentile_rank_", col, "_webUIRequestUrl")
Marc Kupietz130a2a22025-10-18 16:09:23 +02001130 runner_pct_label_col <- paste0("runner_up_percentile_rank_", col)
1131 runner_pct_value_col <- paste0("runner_up_percentile_rank_", col, "_value")
1132 loser_pct_label_col <- paste0("loser_percentile_rank_", col)
1133 loser_pct_value_col <- paste0("loser_percentile_rank_", col, "_value")
Marc Kupietz09b1c082026-05-01 14:45:47 +02001134 loser_pct_url_col <- paste0("loser_percentile_rank_", col, "_webUIRequestUrl")
Marc Kupietz130a2a22025-10-18 16:09:23 +02001135 max_delta_pct_col <- paste0("max_delta_percentile_rank_", col)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001136 url_cols <- paste0("webUIRequestUrl_", safe_labels)
1137 has_urls <- all(url_cols %in% names(comparison))
1138 url_values <- if (has_urls) comparison[, url_cols, drop = FALSE] else NULL
Marc Kupietz130a2a22025-10-18 16:09:23 +02001139
1140 if (nrow(pct_values) == 0) {
1141 comparison[[winner_pct_label_col]] <- character(0)
1142 comparison[[winner_pct_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001143 if (has_urls) {
1144 comparison[[winner_pct_url_col]] <- character(0)
1145 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001146 comparison[[runner_pct_label_col]] <- character(0)
1147 comparison[[runner_pct_value_col]] <- numeric(0)
1148 comparison[[loser_pct_label_col]] <- character(0)
1149 comparison[[loser_pct_value_col]] <- numeric(0)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001150 if (has_urls) {
1151 comparison[[loser_pct_url_col]] <- character(0)
1152 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001153 comparison[[max_delta_pct_col]] <- numeric(0)
1154 next
1155 }
1156
1157 pct_matrix <- as.matrix(pct_values)
1158 storage.mode(pct_matrix) <- "numeric"
1159
1160 n_rows <- nrow(pct_matrix)
1161 winner_labels <- rep(NA_character_, n_rows)
1162 winner_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001163 winner_urls <- rep(NA_character_, n_rows)
Marc Kupietz130a2a22025-10-18 16:09:23 +02001164 runner_labels <- rep(NA_character_, n_rows)
1165 runner_values <- rep(NA_real_, n_rows)
1166 loser_labels <- rep(NA_character_, n_rows)
1167 loser_values <- rep(NA_real_, n_rows)
Marc Kupietz09b1c082026-05-01 14:45:47 +02001168 loser_urls <- rep(NA_character_, n_rows)
Marc Kupietz130a2a22025-10-18 16:09:23 +02001169 max_deltas <- rep(NA_real_, n_rows)
1170
1171 if (n_rows > 0) {
1172 for (i in seq_len(n_rows)) {
1173 numeric_row <- as.numeric(pct_matrix[i, ])
1174 if (all(is.na(numeric_row))) {
1175 next
1176 }
1177
1178 if (any(is.na(numeric_row))) {
1179 numeric_row[is.na(numeric_row)] <- 0
1180 }
1181 pct_matrix[i, ] <- numeric_row
1182
1183 max_val <- suppressWarnings(max(numeric_row, na.rm = TRUE))
1184 max_idx <- which(numeric_row == max_val)
1185 winner_labels[i] <- collapse_label_values(max_idx, safe_labels)
1186 winner_values[i] <- max_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001187 if (has_urls) {
1188 winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ])
1189 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001190
1191 unique_vals <- sort(unique(numeric_row), decreasing = TRUE)
1192 if (length(unique_vals) >= 2) {
1193 runner_val <- unique_vals[2]
1194 runner_idx <- which(numeric_row == runner_val)
1195 runner_labels[i] <- collapse_label_values(runner_idx, safe_labels)
1196 runner_values[i] <- runner_val
1197 }
1198
1199 min_val <- suppressWarnings(min(numeric_row, na.rm = TRUE))
1200 min_idx <- which(numeric_row == min_val)
1201 loser_labels[i] <- collapse_label_values(min_idx, safe_labels)
1202 loser_values[i] <- min_val
Marc Kupietz09b1c082026-05-01 14:45:47 +02001203 if (has_urls) {
1204 loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ])
1205 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001206
1207 if (is.finite(max_val) && is.finite(min_val)) {
1208 max_deltas[i] <- max_val - min_val
1209 }
1210 }
1211 }
1212
1213 comparison[, pct_cols] <- pct_matrix
1214 comparison[[winner_pct_label_col]] <- winner_labels
1215 comparison[[winner_pct_value_col]] <- winner_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001216 if (has_urls) {
1217 comparison[[winner_pct_url_col]] <- winner_urls
1218 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001219 comparison[[runner_pct_label_col]] <- runner_labels
1220 comparison[[runner_pct_value_col]] <- runner_values
1221 comparison[[loser_pct_label_col]] <- loser_labels
1222 comparison[[loser_pct_value_col]] <- loser_values
Marc Kupietz09b1c082026-05-01 14:45:47 +02001223 if (has_urls) {
1224 comparison[[loser_pct_url_col]] <- loser_urls
1225 }
Marc Kupietz130a2a22025-10-18 16:09:23 +02001226 comparison[[max_delta_pct_col]] <- max_deltas
1227 }
1228
Marc Kupietz09b1c082026-05-01 14:45:47 +02001229 collapse_consensus_url_columns <- function(url_cols) {
1230 if (length(url_cols) == 0) {
1231 return(rep(NA_character_, nrow(comparison)))
1232 }
1233 vapply(seq_len(nrow(comparison)), function(i) {
1234 urls <- unlist(comparison[i, url_cols, drop = FALSE], use.names = FALSE)
1235 urls <- as.character(urls)
1236 urls <- urls[!is.na(urls) & urls != ""]
1237 urls <- unique(urls)
1238 if (length(urls) == 1) {
1239 urls
1240 } else {
1241 NA_character_
1242 }
1243 }, character(1))
1244 }
1245
1246 winner_score_url_cols <- intersect(paste0("winner_", score_cols, "_webUIRequestUrl"), names(comparison))
1247 loser_score_url_cols <- intersect(paste0("loser_", score_cols, "_webUIRequestUrl"), names(comparison))
1248 if (length(winner_score_url_cols) > 0) {
1249 comparison$winner_webUIRequestUrl <- collapse_consensus_url_columns(winner_score_url_cols)
1250 }
1251 if (length(loser_score_url_cols) > 0) {
1252 comparison$loser_webUIRequestUrl <- collapse_consensus_url_columns(loser_score_url_cols)
1253 }
1254
1255 url_helper_cols <- intersect(paste0("webUIRequestUrl_", labels), names(comparison))
1256 if (length(url_helper_cols) > 0) {
1257 comparison <- dplyr::select(comparison, -dplyr::all_of(url_helper_cols))
1258 }
1259
Marc Kupietzc4540a22025-10-14 17:39:53 +02001260 dplyr::left_join(result, comparison, by = c("node", "collocate"))
1261}
1262
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001263#' @importFrom magrittr debug_pipe
Marc Kupietz2b17b212023-08-27 17:47:26 +02001264#' @importFrom stringr str_detect
1265#' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when
1266#'
1267matches2FreqTable <- function(matches,
1268 index = 0,
1269 minOccur = 5,
1270 leftContextSize = 5,
1271 rightContextSize = 5,
1272 ignoreCollocateCase = FALSE,
1273 stopwords = c(),
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001274 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietz2b17b212023-08-27 17:47:26 +02001275 oldTable = data.frame(word = rep(NA, 1), frequency = rep(NA, 1)),
1276 verbose = TRUE) {
1277 word <- NULL # https://stackoverflow.com/questions/8096313/no-visible-binding-for-global-variable-note-in-r-cmd-check
1278 frequency <- NULL
1279
1280 if (nrow(matches) < 1) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001281 dplyr::tibble(word = c(), frequency = c())
Marc Kupietz2b17b212023-08-27 17:47:26 +02001282 } else if (index == 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001283 if (!"tokens" %in% colnames(matches) || !is.list(matches$tokens)) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001284 log_info(verbose, "Outdated KorAP server: Falling back to client side tokenization.\n")
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001285 return(snippet2FreqTable(matches$snippet, minOccur, leftContextSize, rightContextSize,
1286 ignoreCollocateCase = ignoreCollocateCase,
1287 stopwords = stopwords, oldTable = oldTable, verbose = verbose
1288 ))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001289 }
1290 log_info(verbose, paste("Joining", nrow(matches), "kwics\n"))
Marc Kupietza25fbd92025-10-14 17:38:09 +02001291 for (i in seq_len(nrow(matches))) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001292 oldTable <- matches2FreqTable(
1293 matches,
1294 i,
1295 leftContextSize = leftContextSize,
1296 rightContextSize = rightContextSize,
1297 collocateFilterRegex = collocateFilterRegex,
1298 oldTable = oldTable,
1299 stopwords = stopwords
1300 )
1301 }
1302 log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n"))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001303 oldTable |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001304 group_by(word) |>
1305 mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001306 summarise(frequency = sum(frequency), .groups = "drop") |>
Marc Kupietz2b17b212023-08-27 17:47:26 +02001307 arrange(desc(frequency))
1308 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001309 stopwordsTable <- dplyr::tibble(word = stopwords)
Marc Kupietz2b17b212023-08-27 17:47:26 +02001310
1311 left <- tail(unlist(matches$tokens$left[index]), leftContextSize)
1312
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001313 # cat(paste("left:", left, "\n", collapse=" "))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001314
1315 right <- head(unlist(matches$tokens$right[index]), rightContextSize)
1316
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001317 # cat(paste("right:", right, "\n", collapse=" "))
Marc Kupietz2b17b212023-08-27 17:47:26 +02001318
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001319 if (length(left) + length(right) == 0) {
Marc Kupietz2b17b212023-08-27 17:47:26 +02001320 oldTable
1321 } else {
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001322 table(c(left, right)) |>
1323 dplyr::as_tibble(.name_repair = "minimal") |>
1324 dplyr::rename(word = 1, frequency = 2) |>
1325 dplyr::filter(str_detect(word, collocateFilterRegex)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001326 dplyr::anti_join(stopwordsTable, by = "word") |>
Marc Kupietz2b17b212023-08-27 17:47:26 +02001327 dplyr::bind_rows(oldTable)
1328 }
1329 }
1330}
1331
1332#' @importFrom magrittr debug_pipe
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001333#' @importFrom stringr str_match str_split str_detect
1334#' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when
1335#'
1336snippet2FreqTable <- function(snippet,
1337 minOccur = 5,
1338 leftContextSize = 5,
1339 rightContextSize = 5,
1340 ignoreCollocateCase = FALSE,
1341 stopwords = c(),
1342 tokenizeRegex = "([! )(\uc2\uab,.:?\u201e\u201c\'\"]+|&quot;)",
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001343 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001344 oldTable = data.frame(word = rep(NA, 1), frequency = rep(NA, 1)),
1345 verbose = TRUE) {
1346 word <- NULL # https://stackoverflow.com/questions/8096313/no-visible-binding-for-global-variable-note-in-r-cmd-check
1347 frequency <- NULL
1348
1349 if (length(snippet) < 1) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001350 dplyr::tibble(word = c(), frequency = c())
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001351 } else if (length(snippet) > 1) {
Marc Kupietza47d1502023-04-18 15:26:47 +02001352 log_info(verbose, paste("Joining", length(snippet), "kwics\n"))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001353 for (s in snippet) {
1354 oldTable <- snippet2FreqTable(
1355 s,
1356 leftContextSize = leftContextSize,
1357 rightContextSize = rightContextSize,
Marc Kupietz47d0d2b2021-12-19 16:38:52 +01001358 collocateFilterRegex = collocateFilterRegex,
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001359 oldTable = oldTable,
1360 stopwords = stopwords
1361 )
1362 }
Marc Kupietza47d1502023-04-18 15:26:47 +02001363 log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n"))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001364 oldTable |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001365 group_by(word) |>
1366 mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001367 summarise(frequency = sum(frequency), .groups = "drop") |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001368 arrange(desc(frequency))
1369 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001370 stopwordsTable <- dplyr::tibble(word = stopwords)
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001371 match <-
1372 str_match(
1373 snippet,
1374 '<span class="context-left">(<span class="more"></span>)?(.*[^ ]) *</span><span class="match"><mark>.*</mark></span><span class="context-right"> *([^<]*)'
1375 )
1376
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001377 left <- if (leftContextSize > 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001378 tail(unlist(str_split(match[1, 3], tokenizeRegex)), leftContextSize)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001379 } else {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001380 ""
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001381 }
1382 # cat(paste("left:", left, "\n", collapse=" "))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001383
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001384 right <- if (rightContextSize > 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001385 head(unlist(str_split(match[1, 4], tokenizeRegex)), rightContextSize)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001386 } else {
1387 ""
1388 }
1389 # cat(paste("right:", right, "\n", collapse=" "))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001390
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001391 if (is.na(left[1]) || is.na(right[1]) || length(left) + length(right) == 0) {
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001392 oldTable
1393 } else {
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001394 table(c(left, right)) |>
1395 dplyr::as_tibble(.name_repair = "minimal") |>
1396 dplyr::rename(word = 1, frequency = 2) |>
1397 dplyr::filter(str_detect(word, collocateFilterRegex)) |>
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001398 dplyr::anti_join(stopwordsTable, by = "word") |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001399 dplyr::bind_rows(oldTable)
1400 }
1401 }
1402}
1403
1404#' Preliminary synsemantic stopwords function
1405#'
1406#' @description
Marc Kupietz67edcb52021-09-20 21:54:24 +02001407#' `r lifecycle::badge("experimental")`
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001408#'
1409#' Preliminary synsemantic stopwords function to be used in collocation analysis.
1410#'
1411#' @details
1412#' Currently only suitable for German. See stopwords package for other languages.
1413#'
1414#' @param ... future arguments for language detection
1415#'
1416#' @family collocation analysis functions
1417#' @return Vector of synsemantic stopwords.
1418#' @export
1419synsemanticStopwords <- function(...) {
Marc Kupietzc79155b2025-10-19 13:42:55 +02001420 base <- c(
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001421 "der",
1422 "die",
1423 "und",
1424 "in",
1425 "den",
1426 "von",
1427 "mit",
1428 "das",
1429 "zu",
1430 "im",
1431 "ist",
1432 "auf",
1433 "sich",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001434 "des",
1435 "dem",
1436 "nicht",
1437 "ein",
1438 "eine",
1439 "es",
1440 "auch",
1441 "an",
1442 "als",
1443 "am",
1444 "aus",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001445 "bei",
1446 "er",
1447 "dass",
1448 "sie",
1449 "nach",
1450 "um",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001451 "zum",
1452 "noch",
1453 "war",
1454 "einen",
1455 "einer",
1456 "wie",
1457 "einem",
1458 "vor",
1459 "bis",
1460 "\u00fcber",
1461 "so",
1462 "aber",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001463 "diese",
Marc Kupietzc79155b2025-10-19 13:42:55 +02001464 "oder"
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001465 )
Marc Kupietzc79155b2025-10-19 13:42:55 +02001466
1467 lower <- unique(tolower(base))
1468 capitalized <- paste0(toupper(substr(lower, 1, 1)), substring(lower, 2))
1469
1470 unique(c(lower, capitalized))
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001471}
1472
Marc Kupietz5a336b62021-11-27 17:51:35 +01001473
Marc Kupietz76b05592021-12-19 16:26:15 +01001474# #' @export
Marc Kupietz5a336b62021-11-27 17:51:35 +01001475findExample <-
1476 function(kco,
1477 query,
1478 vc = "",
1479 matchOnly = TRUE) {
1480 out <- character(length = length(query))
1481
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001482 if (length(vc) < length(query)) {
Marc Kupietz5a336b62021-11-27 17:51:35 +01001483 vc <- rep(vc, length(query))
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001484 }
Marc Kupietz5a336b62021-11-27 17:51:35 +01001485
1486 for (i in seq_along(query)) {
1487 q <- corpusQuery(kco, paste0("(", query[i], ")"), vc = vc[i], metadataOnly = FALSE)
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001488 if (q@totalResults > 0) {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001489 q <- fetchNext(q, maxFetch = 50, randomizePageOrder = F)
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001490 example <- as.character((q@collectedMatches)$snippet[1])
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001491 out[i] <- if (matchOnly) {
1492 gsub(".*<mark>(.+)</mark>.*", "\\1", example)
Marc Kupietz5a336b62021-11-27 17:51:35 +01001493 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001494 stringr::str_replace(example, "<[^>]*>", "")
Marc Kupietz5a336b62021-11-27 17:51:35 +01001495 }
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001496 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001497 out[i] <- ""
Marc Kupietzb811ffb2021-12-07 10:34:10 +01001498 }
Marc Kupietz5a336b62021-11-27 17:51:35 +01001499 }
1500 out
1501 }
1502
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001503collocatesQuery <-
1504 function(kco,
1505 query,
1506 vc = "",
1507 minOccur = 5,
1508 leftContextSize = 5,
1509 rightContextSize = 5,
1510 searchHitsSampleLimit = 20000,
1511 ignoreCollocateCase = FALSE,
1512 stopwords = c(),
Marc Kupietzb2862d42025-10-18 10:17:49 +02001513 collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$",
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001514 ...) {
1515 frequency <- NULL
1516 q <- corpusQuery(kco, query, vc, metadataOnly = F, ...)
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001517 if (q@totalResults == 0) {
1518 tibble(word = c(), frequency = c())
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001519 } else {
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001520 q <- fetchNext(q, maxFetch = searchHitsSampleLimit, randomizePageOrder = TRUE)
1521 matches2FreqTable(q@collectedMatches,
1522 0,
1523 minOccur = minOccur,
1524 leftContextSize = leftContextSize,
1525 rightContextSize = rightContextSize,
1526 ignoreCollocateCase = ignoreCollocateCase,
1527 stopwords = stopwords,
Marc Kupietzb2862d42025-10-18 10:17:49 +02001528 collocateFilterRegex = collocateFilterRegex,
Marc Kupietz6dfeed92025-06-03 11:58:06 +02001529 ...,
1530 verbose = kco@verbose
1531 ) |>
Marc Kupietz4cd066d2025-02-28 15:48:23 +01001532 mutate(frequency = frequency * q@totalResults / min(q@totalResults, searchHitsSampleLimit)) |>
Marc Kupietzdbd431a2021-08-29 12:17:45 +02001533 filter(frequency >= minOccur)
1534 }
1535 }