| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1 | #' @include logging.R |
| 2 | setGeneric("collocationAnalysis", function(kco, ...) standardGeneric("collocationAnalysis")) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 3 | |
| 4 | #' Collocation analysis |
| 5 | #' |
| Marc Kupietz | a8c40f4 | 2025-06-24 15:49:52 +0200 | [diff] [blame] | 6 | #' @family collocation analysis functions |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 7 | #' @aliases collocationAnalysis |
| 8 | #' |
| 9 | #' @description |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 10 | #' |
| 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 Kupietz | e7f0d68 | 2025-02-19 10:50:59 +0100 | [diff] [blame] | 24 | #' 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 Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 27 | #' user interface. |
| 28 | #' |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 29 | #' |
| Marc Kupietz | 67edcb5 | 2021-09-20 21:54:24 +0200 | [diff] [blame] | 30 | #' @param lemmatizeNodeQuery if TRUE, node query will be lemmatized, i.e. `x -> [tt/l=x]` |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 31 | #' @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 Kupietz | 6bd9cad | 2024-12-18 15:57:26 +0100 | [diff] [blame] | 35 | #' @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 Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 36 | #' @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 Kupietz | 67edcb5 | 2021-09-20 21:54:24 +0200 | [diff] [blame] | 38 | #' @param expand if TRUE, `node` and `vc` parameters are expanded to all of their combinations |
| Marc Kupietz | 7d400e0 | 2021-12-19 16:39:36 +0100 | [diff] [blame] | 39 | #' @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 Kupietz | 2b0b0a1 | 2025-10-19 14:49:14 +0200 | [diff] [blame] | 41 | #' @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 Kupietz | 7d400e0 | 2021-12-19 16:39:36 +0100 | [diff] [blame] | 43 | #' @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 Kupietz | 47d0d2b | 2021-12-19 16:38:52 +0100 | [diff] [blame] | 44 | #' @param collocateFilterRegex allow only collocates matching the regular expression |
| Marc Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 45 | #' @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 Kupietz | 4cbb547 | 2025-10-19 12:15:25 +0200 | [diff] [blame] | 46 | #' @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 Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 47 | #' @param vcLabel optional label override for the current virtual corpus (used internally when named VC collections are expanded) |
| Marc Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 48 | #' @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 Kupietz | 67edcb5 | 2021-09-20 21:54:24 +0200 | [diff] [blame] | 49 | #' @param ... more arguments will be passed to [collocationScoreQuery()] |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 50 | #' @inheritParams collocationScoreQuery,KorAPConnection-method |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 51 | #' @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 Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 54 | #' |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 55 | #' \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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 62 | #' \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 Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 63 | #' \item Optional helper columns such as \code{query}, \code{example}, or \code{url} when example retrieval is requested. |
| 64 | #' } |
| Marc Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 65 | #' @importFrom dplyr arrange desc slice_head bind_rows group_by mutate ungroup left_join select row_number all_of first |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 66 | #' @importFrom purrr pmap |
| Marc Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 67 | #' @importFrom tidyr expand_grid pivot_wider |
| 68 | #' @importFrom rlang sym |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 69 | #' |
| 70 | #' @examples |
| Marc Kupietz | 6ae7605 | 2021-09-21 10:34:00 +0200 | [diff] [blame] | 71 | #' \dontrun{ |
| 72 | #' |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 73 | #' # 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 Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 79 | #' dplyr::filter(logDice >= 5) |
| 80 | #' } |
| 81 | #' |
| Marc Kupietz | 6ae7605 | 2021-09-21 10:34:00 +0200 | [diff] [blame] | 82 | #' \dontrun{ |
| 83 | #' |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 84 | #' # Identify the most prominent light verb construction with "in ... setzen". |
| 85 | #' # Note that, currently, the use of focus function disallows exactFrequencies. |
| Marc Kupietz | 4cd066d | 2025-02-28 15:48:23 +0100 | [diff] [blame] | 86 | #' KorAPConnection(verbose = TRUE) |> |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 87 | #' collocationAnalysis("focus(in [tt/p=NN] {[tt/l=setzen]})", |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 88 | #' leftContextSize = 1, rightContextSize = 0, exactFrequencies = FALSE, topCollocatesLimit = 20 |
| 89 | #' ) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 90 | #' } |
| 91 | #' |
| 92 | #' @export |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 93 | setMethod( |
| 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 Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 116 | queryMissingScores = FALSE, |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 117 | missingScoreQuantile = 0.05, |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 118 | vcLabel = NA_character_, |
| Marc Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 119 | cacheAs = NULL, |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 120 | ...) { |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 121 | word <- frequency <- O <- NULL |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 122 | |
| Marc Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 123 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 132 | if (!exactFrequencies && (!is.na(withinSpan) && !is.null(withinSpan) && nzchar(withinSpan))) { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 133 | stop(sprintf("Not empty withinSpan (='%s') requires exactFrequencies=TRUE", withinSpan), call. = FALSE) |
| 134 | } |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 135 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 136 | warnIfNotAuthorized(kco) |
| Marc Kupietz | 581a29b | 2021-09-04 20:51:04 +0200 | [diff] [blame] | 137 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 138 | if (lemmatizeNodeQuery) { |
| 139 | node <- lemmatizeWordQuery(node) |
| 140 | } |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 141 | |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 142 | vcNames <- names(vc) |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 143 | if (is.null(vcNames)) { |
| 144 | vcNames <- rep(NA_character_, length(vc)) |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 145 | } |
| 146 | |
| 147 | label_lookup <- NULL |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 148 | 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 Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 153 | } |
| 154 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 155 | result <- if (length(node) > 1 || length(vc) > 1) { |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 156 | grid <- if (expand) { |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 157 | 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 Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 161 | } else { |
| 162 | tibble(node = node, vc = vc, vcLabel = vcNames) |
| 163 | } |
| 164 | |
| 165 | multi_result <- purrr::pmap(grid, function(node, vc, vcLabel, ...) { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 166 | 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 Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 182 | missingScoreQuantile = missingScoreQuantile, |
| Marc Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 183 | queryMissingScores = queryMissingScores, |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 184 | collocateFilterRegex = collocateFilterRegex, |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 185 | vcLabel = vcLabel, |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 186 | ... |
| 187 | ) |
| 188 | }) |> |
| Marc Kupietz | e31322e | 2025-10-17 18:55:36 +0200 | [diff] [blame] | 189 | bind_rows() |
| 190 | |
| 191 | if (!"vc" %in% names(multi_result) || nrow(multi_result) == 0) { |
| 192 | multi_result |
| 193 | } else { |
| Marc Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 194 | 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 Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 204 | 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 Kupietz | e31322e | 2025-10-17 18:55:36 +0200 | [diff] [blame] | 221 | multi_result |> |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 222 | add_multi_vc_comparisons( |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 223 | missingScoreQuantile = missingScoreQuantile |
| 224 | ) |
| Marc Kupietz | e31322e | 2025-10-17 18:55:36 +0200 | [diff] [blame] | 225 | } |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 226 | } else { |
| Marc Kupietz | e34a8be | 2025-10-17 20:13:42 +0200 | [diff] [blame] | 227 | if ((is.na(vcLabel) || vcLabel == "") && length(vcNames) >= 1) { |
| 228 | vcLabel <- vcNames[1] |
| 229 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 230 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 231 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 242 | collocateFilterRegex = collocateFilterRegex, |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 243 | ... |
| 244 | ) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 245 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 246 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 268 | |
| 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 290 | leftContextSize = leftContextSize, |
| 291 | rightContextSize = rightContextSize, |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 292 | 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 Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 300 | missingScoreQuantile = missingScoreQuantile, |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 301 | collocateFilterRegex = collocateFilterRegex, |
| Marc Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 302 | queryMissingScores = queryMissingScores, |
| Marc Kupietz | 2b0b0a1 | 2025-10-19 14:49:14 +0200 | [diff] [blame] | 303 | thresholdScore = thresholdScore, |
| 304 | threshold = threshold, |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 305 | vcLabel = vcLabel |
| 306 | ) |> |
| Marc Kupietz | 2b0b0a1 | 2025-10-19 14:49:14 +0200 | [diff] [blame] | 307 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 316 | filter(O >= minOccur) |> |
| 317 | dplyr::arrange(dplyr::desc(logDice)) |
| 318 | } |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 319 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 320 | |
| 321 | if (addExamples && nrow(result) > 0) { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 322 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 335 | |
| Marc Kupietz | 0a29263 | 2025-10-19 14:04:36 +0200 | [diff] [blame] | 336 | 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 Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 374 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 379 | result |
| 380 | } |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 381 | ) |
| 382 | |
| Marc Kupietz | 76b0559 | 2021-12-19 16:26:15 +0100 | [diff] [blame] | 383 | # #' @export |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 384 | removeWithinSpan <- function(query, withinSpan) { |
| 385 | if (withinSpan == "") { |
| 386 | return(query) |
| 387 | } |
| 388 | needle <- sprintf("^\\(contains\\(<%s>, ?(.*)\\){2}$", withinSpan) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 389 | res <- gsub(needle, "\\1", query) |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 390 | needle <- sprintf("^contains\\(<%s>, ?(.*)\\)$", withinSpan) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 391 | res <- gsub(needle, "\\1", res) |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 392 | return(res) |
| 393 | } |
| 394 | |
| Marc Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 395 | backfill_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 Kupietz | 9c53e41 | 2026-06-21 12:13:44 +0200 | [diff] [blame^] | 408 | distinct_pairs <- dplyr::distinct( |
| 409 | result, |
| 410 | .data$node, |
| 411 | .data$collocate |
| 412 | ) |
| Marc Kupietz | de679ea | 2025-10-19 13:14:51 +0200 | [diff] [blame] | 413 | 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 Kupietz | 0a29263 | 2025-10-19 14:04:36 +0200 | [diff] [blame] | 500 | inject_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 Kupietz | 77852b2 | 2025-10-19 11:35:34 +0200 | [diff] [blame] | 541 | add_multi_vc_comparisons <- function(result, missingScoreQuantile = 0.05) { |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 542 | label <- node <- collocate <- vc <- webUIRequestUrl <- NULL |
| Marc Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 543 | |
| 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 Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 556 | compute_score_floor <- function(values) { |
| Marc Kupietz | 4cbb547 | 2025-10-19 12:15:25 +0200 | [diff] [blame] | 557 | # Estimate a conservative floor so missing scores can be imputed without favoring any label |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 558 | 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 Kupietz | 4cbb547 | 2025-10-19 12:15:25 +0200 | [diff] [blame] | 564 | # Use a lower quantile as the anchor to stay near the weakest attested scores |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 565 | 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 Kupietz | 4cbb547 | 2025-10-19 12:15:25 +0200 | [diff] [blame] | 596 | # Step away from the anchor by a robust spread estimate to avoid ties with real scores |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 597 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 620 | comparison <- result |> |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 621 | dplyr::select(node, collocate, label, dplyr::all_of(score_cols)) |> |
| 622 | tidyr::pivot_wider( |
| Marc Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 623 | names_from = label, |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 624 | values_from = dplyr::all_of(score_cols), |
| Marc Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 625 | names_glue = "{.value}_{make.names(label)}", |
| 626 | values_fn = dplyr::first |
| 627 | ) |
| 628 | |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 629 | raw_labels <- unique(result$label) |
| 630 | labels <- make.names(raw_labels) |
| 631 | label_map <- stats::setNames(raw_labels, labels) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 632 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 696 | |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 697 | 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 Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 711 | percentile_col_name <- paste0("percentile_rank_", safe_lab, "_", col) |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 712 | values <- label_df[[col]] |
| 713 | ranks <- rep(NA_real_, length(values)) |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 714 | percentiles <- rep(NA_real_, length(values)) |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 715 | valid_idx <- which(!is.na(values)) |
| 716 | if (length(valid_idx) > 0) { |
| 717 | ranks[valid_idx] <- rank(-values[valid_idx], ties.method = "first") |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 718 | total <- length(valid_idx) |
| 719 | percentiles[valid_idx] <- 1 - (ranks[valid_idx] - 1) / total |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 720 | } |
| 721 | rank_tbl[[rank_col_name]] <- ranks |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 722 | rank_tbl[[percentile_col_name]] <- percentiles |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 723 | } |
| 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 Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 746 | 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 Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 755 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 769 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 781 | if (length(labels) == 2) { |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 782 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 787 | } |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 788 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 802 | list(x = x, y = y) |
| 803 | } |
| 804 | |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 805 | 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 Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 823 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 828 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 829 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 839 | 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 Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 851 | filled <- fill_scores(comparison[[left_col]], comparison[[right_col]], col) |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 852 | comparison[[left_col]] <- filled$x |
| 853 | comparison[[right_col]] <- filled$y |
| Marc Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 854 | comparison[[paste0("delta_", col)]] <- filled$x - filled$y |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 855 | 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 Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 866 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 877 | } |
| 878 | } |
| 879 | |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 880 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 893 | winner_url_col <- paste0("winner_", col, "_webUIRequestUrl") |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 894 | runner_label_col <- paste0("runner_up_", col) |
| 895 | runner_value_col <- paste0("runner_up_", col, "_value") |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 896 | loser_label_col <- paste0("loser_", col) |
| 897 | loser_value_col <- paste0("loser_", col, "_value") |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 898 | loser_url_col <- paste0("loser_", col, "_webUIRequestUrl") |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 899 | max_delta_col <- paste0("max_delta_", col) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 900 | 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 Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 903 | |
| 904 | if (nrow(score_values) == 0) { |
| 905 | comparison[[winner_label_col]] <- character(0) |
| 906 | comparison[[winner_value_col]] <- numeric(0) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 907 | if (has_urls) { |
| 908 | comparison[[winner_url_col]] <- character(0) |
| 909 | } |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 910 | comparison[[runner_label_col]] <- character(0) |
| 911 | comparison[[runner_value_col]] <- numeric(0) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 912 | comparison[[loser_label_col]] <- character(0) |
| 913 | comparison[[loser_value_col]] <- numeric(0) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 914 | if (has_urls) { |
| 915 | comparison[[loser_url_col]] <- character(0) |
| 916 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 917 | comparison[[max_delta_col]] <- numeric(0) |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 918 | next |
| 919 | } |
| 920 | |
| 921 | score_matrix <- as.matrix(score_values) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 922 | storage.mode(score_matrix) <- "numeric" |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 923 | |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 924 | n_rows <- nrow(score_matrix) |
| 925 | winner_labels <- rep(NA_character_, n_rows) |
| 926 | winner_values <- rep(NA_real_, n_rows) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 927 | winner_urls <- rep(NA_character_, n_rows) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 928 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 932 | loser_urls <- rep(NA_character_, n_rows) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 933 | max_deltas <- rep(NA_real_, n_rows) |
| 934 | |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 935 | 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 Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 942 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 946 | } |
| Marc Kupietz | 9894a37 | 2025-10-18 14:51:29 +0200 | [diff] [blame] | 947 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 958 | 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 Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 962 | winner_labels[i] <- collapse_label_values(max_idx, safe_labels) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 963 | winner_values[i] <- max_val |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 964 | if (has_urls) { |
| 965 | winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ]) |
| 966 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 967 | |
| 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 Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 972 | runner_labels[i] <- collapse_label_values(runner_idx, safe_labels) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 973 | 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 Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 978 | loser_labels[i] <- collapse_label_values(min_idx, safe_labels) |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 979 | loser_values[i] <- min_val |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 980 | if (has_urls) { |
| 981 | loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ]) |
| 982 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 983 | |
| 984 | if (is.finite(max_val) && is.finite(min_val)) { |
| 985 | max_deltas[i] <- max_val - min_val |
| 986 | } |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 987 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 988 | } |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 989 | |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 990 | comparison[, value_cols] <- score_matrix |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 991 | comparison[[winner_label_col]] <- winner_labels |
| 992 | comparison[[winner_value_col]] <- winner_values |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 993 | if (has_urls) { |
| 994 | comparison[[winner_url_col]] <- winner_urls |
| 995 | } |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 996 | comparison[[runner_label_col]] <- runner_labels |
| 997 | comparison[[runner_value_col]] <- runner_values |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 998 | comparison[[loser_label_col]] <- loser_labels |
| 999 | comparison[[loser_value_col]] <- loser_values |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1000 | if (has_urls) { |
| 1001 | comparison[[loser_url_col]] <- loser_urls |
| 1002 | } |
| Marc Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 1003 | comparison[[max_delta_col]] <- max_deltas |
| Marc Kupietz | 5e35d7a | 2025-10-17 21:21:22 +0200 | [diff] [blame] | 1004 | } |
| 1005 | |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1006 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1018 | winner_rank_url_col <- paste0("winner_rank_", col, "_webUIRequestUrl") |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1019 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1023 | loser_rank_url_col <- paste0("loser_rank_", col, "_webUIRequestUrl") |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1024 | max_delta_rank_col <- paste0("max_delta_rank_", col) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1025 | 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 Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1028 | |
| 1029 | if (nrow(rank_values) == 0) { |
| 1030 | comparison[[winner_rank_label_col]] <- character(0) |
| 1031 | comparison[[winner_rank_value_col]] <- numeric(0) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1032 | if (has_urls) { |
| 1033 | comparison[[winner_rank_url_col]] <- character(0) |
| 1034 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1035 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1039 | if (has_urls) { |
| 1040 | comparison[[loser_rank_url_col]] <- character(0) |
| 1041 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1042 | comparison[[max_delta_rank_col]] <- numeric(0) |
| 1043 | next |
| 1044 | } |
| 1045 | |
| Marc Kupietz | db2fabd | 2026-04-27 15:01:37 +0200 | [diff] [blame] | 1046 | rank_matrix <- as.matrix(rank_values) |
| 1047 | storage.mode(rank_matrix) <- "numeric" |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1048 | |
| 1049 | n_rows <- nrow(rank_matrix) |
| 1050 | winner_labels <- rep(NA_character_, n_rows) |
| 1051 | winner_values <- rep(NA_real_, n_rows) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1052 | winner_urls <- rep(NA_character_, n_rows) |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1053 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1057 | loser_urls <- rep(NA_character_, n_rows) |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1058 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1081 | if (has_urls) { |
| 1082 | winner_urls[i] <- collapse_url_values(min_positions, url_values[i, ]) |
| 1083 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1084 | |
| 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1097 | if (has_urls) { |
| 1098 | loser_urls[i] <- collapse_url_values(max_positions, url_values[i, ]) |
| 1099 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1100 | |
| 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1108 | if (has_urls) { |
| 1109 | comparison[[winner_rank_url_col]] <- winner_urls |
| 1110 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1111 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1115 | if (has_urls) { |
| 1116 | comparison[[loser_rank_url_col]] <- loser_urls |
| 1117 | } |
| Marc Kupietz | 28a2984 | 2025-10-18 12:25:09 +0200 | [diff] [blame] | 1118 | comparison[[max_delta_rank_col]] <- max_deltas |
| 1119 | } |
| 1120 | |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1121 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1133 | winner_pct_url_col <- paste0("winner_percentile_rank_", col, "_webUIRequestUrl") |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1134 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1138 | loser_pct_url_col <- paste0("loser_percentile_rank_", col, "_webUIRequestUrl") |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1139 | max_delta_pct_col <- paste0("max_delta_percentile_rank_", col) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1140 | 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 Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1143 | |
| 1144 | if (nrow(pct_values) == 0) { |
| 1145 | comparison[[winner_pct_label_col]] <- character(0) |
| 1146 | comparison[[winner_pct_value_col]] <- numeric(0) |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1147 | if (has_urls) { |
| 1148 | comparison[[winner_pct_url_col]] <- character(0) |
| 1149 | } |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1150 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1154 | if (has_urls) { |
| 1155 | comparison[[loser_pct_url_col]] <- character(0) |
| 1156 | } |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1157 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1167 | winner_urls <- rep(NA_character_, n_rows) |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1168 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1172 | loser_urls <- rep(NA_character_, n_rows) |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1173 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1191 | if (has_urls) { |
| 1192 | winner_urls[i] <- collapse_url_values(max_idx, url_values[i, ]) |
| 1193 | } |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1194 | |
| 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1207 | if (has_urls) { |
| 1208 | loser_urls[i] <- collapse_url_values(min_idx, url_values[i, ]) |
| 1209 | } |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1210 | |
| 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1220 | if (has_urls) { |
| 1221 | comparison[[winner_pct_url_col]] <- winner_urls |
| 1222 | } |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1223 | 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 Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1227 | if (has_urls) { |
| 1228 | comparison[[loser_pct_url_col]] <- loser_urls |
| 1229 | } |
| Marc Kupietz | 130a2a2 | 2025-10-18 16:09:23 +0200 | [diff] [blame] | 1230 | comparison[[max_delta_pct_col]] <- max_deltas |
| 1231 | } |
| 1232 | |
| Marc Kupietz | 09b1c08 | 2026-05-01 14:45:47 +0200 | [diff] [blame] | 1233 | 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 Kupietz | c4540a2 | 2025-10-14 17:39:53 +0200 | [diff] [blame] | 1264 | dplyr::left_join(result, comparison, by = c("node", "collocate")) |
| 1265 | } |
| 1266 | |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1267 | #' @importFrom magrittr debug_pipe |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1268 | #' @importFrom stringr str_detect |
| 1269 | #' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when |
| 1270 | #' |
| 1271 | matches2FreqTable <- function(matches, |
| 1272 | index = 0, |
| 1273 | minOccur = 5, |
| 1274 | leftContextSize = 5, |
| 1275 | rightContextSize = 5, |
| 1276 | ignoreCollocateCase = FALSE, |
| 1277 | stopwords = c(), |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1278 | collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$", |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1279 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1285 | dplyr::tibble(word = c(), frequency = c()) |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1286 | } else if (index == 0) { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1287 | if (!"tokens" %in% colnames(matches) || !is.list(matches$tokens)) { |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1288 | log_info(verbose, "Outdated KorAP server: Falling back to client side tokenization.\n") |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1289 | return(snippet2FreqTable(matches$snippet, minOccur, leftContextSize, rightContextSize, |
| 1290 | ignoreCollocateCase = ignoreCollocateCase, |
| 1291 | stopwords = stopwords, oldTable = oldTable, verbose = verbose |
| 1292 | )) |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1293 | } |
| 1294 | log_info(verbose, paste("Joining", nrow(matches), "kwics\n")) |
| Marc Kupietz | a25fbd9 | 2025-10-14 17:38:09 +0200 | [diff] [blame] | 1295 | for (i in seq_len(nrow(matches))) { |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1296 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1307 | oldTable |> |
| Marc Kupietz | 4cd066d | 2025-02-28 15:48:23 +0100 | [diff] [blame] | 1308 | group_by(word) |> |
| 1309 | mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |> |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1310 | summarise(frequency = sum(frequency), .groups = "drop") |> |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1311 | arrange(desc(frequency)) |
| 1312 | } else { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1313 | stopwordsTable <- dplyr::tibble(word = stopwords) |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1314 | |
| 1315 | left <- tail(unlist(matches$tokens$left[index]), leftContextSize) |
| 1316 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1317 | # cat(paste("left:", left, "\n", collapse=" ")) |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1318 | |
| 1319 | right <- head(unlist(matches$tokens$right[index]), rightContextSize) |
| 1320 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1321 | # cat(paste("right:", right, "\n", collapse=" ")) |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1322 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1323 | if (length(left) + length(right) == 0) { |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1324 | oldTable |
| 1325 | } else { |
| Marc Kupietz | 4cd066d | 2025-02-28 15:48:23 +0100 | [diff] [blame] | 1326 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1330 | dplyr::anti_join(stopwordsTable, by = "word") |> |
| Marc Kupietz | 2b17b21 | 2023-08-27 17:47:26 +0200 | [diff] [blame] | 1331 | dplyr::bind_rows(oldTable) |
| 1332 | } |
| 1333 | } |
| 1334 | } |
| 1335 | |
| 1336 | #' @importFrom magrittr debug_pipe |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1337 | #' @importFrom stringr str_match str_split str_detect |
| 1338 | #' @importFrom dplyr as_tibble tibble rename filter anti_join tibble bind_rows case_when |
| 1339 | #' |
| 1340 | snippet2FreqTable <- function(snippet, |
| 1341 | minOccur = 5, |
| 1342 | leftContextSize = 5, |
| 1343 | rightContextSize = 5, |
| 1344 | ignoreCollocateCase = FALSE, |
| 1345 | stopwords = c(), |
| 1346 | tokenizeRegex = "([! )(\uc2\uab,.:?\u201e\u201c\'\"]+|")", |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1347 | collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$", |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1348 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1354 | dplyr::tibble(word = c(), frequency = c()) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1355 | } else if (length(snippet) > 1) { |
| Marc Kupietz | a47d150 | 2023-04-18 15:26:47 +0200 | [diff] [blame] | 1356 | log_info(verbose, paste("Joining", length(snippet), "kwics\n")) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1357 | for (s in snippet) { |
| 1358 | oldTable <- snippet2FreqTable( |
| 1359 | s, |
| 1360 | leftContextSize = leftContextSize, |
| 1361 | rightContextSize = rightContextSize, |
| Marc Kupietz | 47d0d2b | 2021-12-19 16:38:52 +0100 | [diff] [blame] | 1362 | collocateFilterRegex = collocateFilterRegex, |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1363 | oldTable = oldTable, |
| 1364 | stopwords = stopwords |
| 1365 | ) |
| 1366 | } |
| Marc Kupietz | a47d150 | 2023-04-18 15:26:47 +0200 | [diff] [blame] | 1367 | log_info(verbose, paste("Aggregating", length(oldTable$word), "tokens\n")) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1368 | oldTable |> |
| Marc Kupietz | 4cd066d | 2025-02-28 15:48:23 +0100 | [diff] [blame] | 1369 | group_by(word) |> |
| 1370 | mutate(word = dplyr::case_when(ignoreCollocateCase ~ tolower(word), TRUE ~ word)) |> |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1371 | summarise(frequency = sum(frequency), .groups = "drop") |> |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1372 | arrange(desc(frequency)) |
| 1373 | } else { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1374 | stopwordsTable <- dplyr::tibble(word = stopwords) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1375 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1381 | left <- if (leftContextSize > 0) { |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1382 | tail(unlist(str_split(match[1, 3], tokenizeRegex)), leftContextSize) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1383 | } else { |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1384 | "" |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1385 | } |
| 1386 | # cat(paste("left:", left, "\n", collapse=" ")) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1387 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1388 | right <- if (rightContextSize > 0) { |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1389 | head(unlist(str_split(match[1, 4], tokenizeRegex)), rightContextSize) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1390 | } else { |
| 1391 | "" |
| 1392 | } |
| 1393 | # cat(paste("right:", right, "\n", collapse=" ")) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1394 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1395 | if (is.na(left[1]) || is.na(right[1]) || length(left) + length(right) == 0) { |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1396 | oldTable |
| 1397 | } else { |
| Marc Kupietz | 4cd066d | 2025-02-28 15:48:23 +0100 | [diff] [blame] | 1398 | 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 Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1402 | dplyr::anti_join(stopwordsTable, by = "word") |> |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1403 | dplyr::bind_rows(oldTable) |
| 1404 | } |
| 1405 | } |
| 1406 | } |
| 1407 | |
| 1408 | #' Preliminary synsemantic stopwords function |
| 1409 | #' |
| 1410 | #' @description |
| Marc Kupietz | 67edcb5 | 2021-09-20 21:54:24 +0200 | [diff] [blame] | 1411 | #' `r lifecycle::badge("experimental")` |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1412 | #' |
| 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 |
| 1423 | synsemanticStopwords <- function(...) { |
| Marc Kupietz | c79155b | 2025-10-19 13:42:55 +0200 | [diff] [blame] | 1424 | base <- c( |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1425 | "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 Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1438 | "des", |
| 1439 | "dem", |
| 1440 | "nicht", |
| 1441 | "ein", |
| 1442 | "eine", |
| 1443 | "es", |
| 1444 | "auch", |
| 1445 | "an", |
| 1446 | "als", |
| 1447 | "am", |
| 1448 | "aus", |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1449 | "bei", |
| 1450 | "er", |
| 1451 | "dass", |
| 1452 | "sie", |
| 1453 | "nach", |
| 1454 | "um", |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1455 | "zum", |
| 1456 | "noch", |
| 1457 | "war", |
| 1458 | "einen", |
| 1459 | "einer", |
| 1460 | "wie", |
| 1461 | "einem", |
| 1462 | "vor", |
| 1463 | "bis", |
| 1464 | "\u00fcber", |
| 1465 | "so", |
| 1466 | "aber", |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1467 | "diese", |
| Marc Kupietz | c79155b | 2025-10-19 13:42:55 +0200 | [diff] [blame] | 1468 | "oder" |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1469 | ) |
| Marc Kupietz | c79155b | 2025-10-19 13:42:55 +0200 | [diff] [blame] | 1470 | |
| 1471 | lower <- unique(tolower(base)) |
| 1472 | capitalized <- paste0(toupper(substr(lower, 1, 1)), substring(lower, 2)) |
| 1473 | |
| 1474 | unique(c(lower, capitalized)) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1475 | } |
| 1476 | |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1477 | |
| Marc Kupietz | 76b0559 | 2021-12-19 16:26:15 +0100 | [diff] [blame] | 1478 | # #' @export |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1479 | findExample <- |
| 1480 | function(kco, |
| 1481 | query, |
| 1482 | vc = "", |
| 1483 | matchOnly = TRUE) { |
| 1484 | out <- character(length = length(query)) |
| 1485 | |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1486 | if (length(vc) < length(query)) { |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1487 | vc <- rep(vc, length(query)) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1488 | } |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1489 | |
| 1490 | for (i in seq_along(query)) { |
| 1491 | q <- corpusQuery(kco, paste0("(", query[i], ")"), vc = vc[i], metadataOnly = FALSE) |
| Marc Kupietz | b811ffb | 2021-12-07 10:34:10 +0100 | [diff] [blame] | 1492 | if (q@totalResults > 0) { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1493 | q <- fetchNext(q, maxFetch = 50, randomizePageOrder = F) |
| Marc Kupietz | b811ffb | 2021-12-07 10:34:10 +0100 | [diff] [blame] | 1494 | example <- as.character((q@collectedMatches)$snippet[1]) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1495 | out[i] <- if (matchOnly) { |
| 1496 | gsub(".*<mark>(.+)</mark>.*", "\\1", example) |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1497 | } else { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1498 | stringr::str_replace(example, "<[^>]*>", "") |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1499 | } |
| Marc Kupietz | b811ffb | 2021-12-07 10:34:10 +0100 | [diff] [blame] | 1500 | } else { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1501 | out[i] <- "" |
| Marc Kupietz | b811ffb | 2021-12-07 10:34:10 +0100 | [diff] [blame] | 1502 | } |
| Marc Kupietz | 5a336b6 | 2021-11-27 17:51:35 +0100 | [diff] [blame] | 1503 | } |
| 1504 | out |
| 1505 | } |
| 1506 | |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1507 | collocatesQuery <- |
| 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 1517 | collocateFilterRegex = "^[:alnum:]+-?[:alnum:]*$", |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1518 | ...) { |
| 1519 | frequency <- NULL |
| 1520 | q <- corpusQuery(kco, query, vc, metadataOnly = F, ...) |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1521 | if (q@totalResults == 0) { |
| 1522 | tibble(word = c(), frequency = c()) |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1523 | } else { |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1524 | 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 Kupietz | b2862d4 | 2025-10-18 10:17:49 +0200 | [diff] [blame] | 1532 | collocateFilterRegex = collocateFilterRegex, |
| Marc Kupietz | 6dfeed9 | 2025-06-03 11:58:06 +0200 | [diff] [blame] | 1533 | ..., |
| 1534 | verbose = kco@verbose |
| 1535 | ) |> |
| Marc Kupietz | 4cd066d | 2025-02-28 15:48:23 +0100 | [diff] [blame] | 1536 | mutate(frequency = frequency * q@totalResults / min(q@totalResults, searchHitsSampleLimit)) |> |
| Marc Kupietz | dbd431a | 2021-08-29 12:17:45 +0200 | [diff] [blame] | 1537 | filter(frequency >= minOccur) |
| 1538 | } |
| 1539 | } |