diff --git a/CITATION.cff b/CITATION.cff index 7c81352..eb22df6 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,7 +8,7 @@ message: 'To cite package "wowi" in publications use:' type: software license: GPL-3.0-or-later title: 'wowi: Detect Spatial Clusters of High Acute Malnutrition Rates' -version: 1.0.1 +version: 1.0.2 abstract: Utilities for detecting statistically significant spatial clusters of high acute malnutrition rates using SaTScan's Bernoulli spatial-scan model. authors: @@ -23,7 +23,7 @@ preferred-citation: authors: - name: Tomás Zaba year: '2025' - notes: R package version 1.0.1 + notes: R package version 1.0.2 url: https://tiwowi.github.io/wowi/ repository-code: https://github.com/tiwowi/wowi url: https://tiwowi.github.io/wowi/ diff --git a/DESCRIPTION b/DESCRIPTION index 5cadeea..39bf72a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: wowi Title: Detect Spatial Clusters of High Acute Malnutrition Rates -Version: 1.0.1 +Version: 1.0.2 Authors@R: person(given = "Tomás", family = "Zaba", diff --git a/NEWS.md b/NEWS.md index 0108aff..e5564cd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# wowi 1.0.2 + +## Bug fixes + +* Resolved issue [#61](https://github.com/tiwowi/wowi/issues/61). +In the previous version, the regular expressions used to match text patterns into tables would break whenever the list of location IDs in a cluster wrapped onto a new line, altering the expected match order. This caused several columns to remain empty. The parser now handles these cases programmatically. + # wowi 1.0.1 ## General updates diff --git a/R/satscan-runner.R b/R/satscan-runner.R index 1bb5ce7..fbd3fd9 100644 --- a/R/satscan-runner.R +++ b/R/satscan-runner.R @@ -127,7 +127,8 @@ #' #' -# no# nocov start +# nocov start + ww_run_satscan <- function( .data, filename = NULL, diff --git a/R/utils.R b/R/utils.R index 6265f24..534e2e9 100644 --- a/R/utils.R +++ b/R/utils.R @@ -2,58 +2,91 @@ #' #' @keywords internal #' +#' skip_if_no_satscan <- function(ss_path = "/Applications/SaTScan.app/Contents/app/satscan") { testthat::skip_if_not(file.exists(ss_path), message = "SaTScan is not installed or not found") } #' -#' +#' Extract results from SaTScan-text-based output +#' +#' @param file SaTScan-text-based output result given as "main" to be parsed +#' #' @keywords internal -#' -#' +#' +#' + + +# nocov start + parse_clusters <- function(file) { - ## Access the txt-based results ---- + + ## Subset SaTScan-text-based output file ---- txt <- file$main - ## Find line indices with "Location IDs included" ---- + ## Find line indices for cluster starts and coordinates cluster_start <- stringr::str_which(txt, "^\\d+\\.Location IDs included\\.:") + coords <- stringr::str_which(txt, "^\\s+Coordinates / radius") + + ## Extract the name of the survey area ---- area_name <- stringr::str_which(txt, "^[Case File]+\\:") - ## Define line offsets to grab per cluster ---- - cluster_blocks <- lapply(cluster_start, function(start_idx) { - txt[start_idx:(start_idx + 10)] - }) - - ## Parse each cluster block ---- - parsed_clusters <- lapply(cluster_blocks, function(block) { - tibble::tibble( - survey_area = stringr::str_extract_all(basename(txt[[area_name]]), "^[^.]+") |> - as.character(), - nr_EAs = stringr::str_extract(txt[[18]], "\\d+") |> as.integer(), - total_children = stringr::str_extract(txt[[19]], "\\d+") |> as.integer(), - total_cases = stringr::str_extract(txt[[20]], "\\d+") |> as.integer(), - `%_cases` = stringr::str_extract(txt[[21]], "\\d+") |> as.double(), - location_ids = stringr::str_extract_all(block[1], "[0-9]+")[[1]][-1] |> - paste(collapse = ","), - geo = stringr::str_extract(block[2], "\\d+\\.\\d+\\s+\\w\\,\\s+\\d+\\.\\d+\\s+\\w"), - radius = stringr::str_extract(block[2], "\\d+\\.\\d+\\s*\\w+$"), - span = stringr::str_extract(block[3], "[0-9]+[.]+[0-9]+\\s+\\w+$"), - children = stringr::str_extract(block[4], "[0-9]+") |> as.integer(), - n_cases = stringr::str_extract(block[5], "[0-9]+") |> as.integer(), - expected_cases = stringr::str_extract(block[6], "[0-9]+[.]+[0-9]+") |> as.double(), - observedExpected = stringr::str_extract(block[7], "[0-9]+[.]+[0-9]+") |> as.double(), - relative_risk = stringr::str_extract(block[8], "[0-9]+\\.\\d+") |> as.double(), - `%_cases_in_area` = stringr::str_extract(block[9], "[0-9]+\\.\\d+") |> as.double(), - log_lik_ratio = stringr::str_extract(block[10], "[0-9]+\\.\\d+") |> as.double(), - pvalue = stringr::str_extract(block[11], "[0-9]+[.]+[0-9]+") |> as.double(), + ## Guard: align pairs if counts differ + n <- min(length(cluster_start), length(coords)) + if (n == 0) return(tibble::tibble()) + + out <- vector("list", n) + + for (j in seq_len(n)) { + idx <- cluster_start[j] + coord <- coords[j] + + ## Collect all lines containing IDs up to the coordinates line + id_block <- txt[idx:(coord - 1)] + ids_vec <- stringr::str_extract_all(id_block, "[0-9]+") |> unlist() + + ## Drop the leading cluster number (e.g., "1", "2") if present + location_ids <- if (length(ids_vec) > 1) { + paste(ids_vec[-1], collapse = ",") + } else { + NA_character_ + } + + ## Build tibble for this cluster (offsets are stable in SaTScan output) + out[[j]] <- tibble::tibble( + + ### Summary metadata (from fixed lines in your example) ---- + survey_area = as.character(stringr::str_extract_all(basename(txt[[area_name]]), "^[^.]+")), + nr_EAs = as.integer(stringr::str_extract(txt[18], "\\d+")), + total_children = as.integer(stringr::str_extract(txt[19], "\\d+")), + total_cases = as.integer(stringr::str_extract(txt[20], "\\d+")), + `%_cases` = as.double(stringr::str_extract(txt[21], "\\d+\\.?\\d*")), + + ### Cluster-specific ---- + location_ids = location_ids, + geo = stringr::str_extract(txt[coord], + "\\d+\\.\\d+\\s+\\w\\,\\s+\\d+\\.\\d+\\s+\\w"), + radius = stringr::str_extract(txt[coord],"[0-9]+\\.[0-9]+\\s*km"), + span = stringr::str_extract(txt[coord + 1], "[0-9]+\\.[0-9]+\\s*km"), + children = as.integer(stringr::str_extract(txt[coord + 2], "\\d+")), + n_cases = as.integer(stringr::str_extract(txt[coord + 3], "\\d+")), + expected_cases = as.double(stringr::str_extract(txt[coord + 4], "[0-9]+\\.[0-9]+")), + observedExpected = as.double(stringr::str_extract(txt[coord + 5], "[0-9]+\\.[0-9]+")), + relative_risk = as.double(stringr::str_extract(txt[coord + 6], "[0-9]+\\.[0-9]+")), + `%_cases_in_area` = as.double(stringr::str_extract(txt[coord + 7], "[0-9]+\\.?[0-9]*")), + log_lik_ratio = as.double(stringr::str_extract(txt[coord + 8], "[0-9]+\\.[0-9]+")), + pvalue = as.double(stringr::str_extract(txt[coord + 9], "[0-9]+\\.?[0-9]*")), + + ### Check if IPC AMN reqs for survey disaggregation is met ---- ipc_amn = ifelse( - test = length(strsplit(.data$location_ids, ",\\s*")[[1]]) >= 5 & as.numeric(.data$children) > 100, - yes = "yes", - no = "no" + length(strsplit(location_ids, ",\\s*")[[1]]) >= 5 & !is.na(children) & children >= 100, + "yes", "no" ) - ) - }) + ) + } - ## Combine all into one data frame ---- - dplyr::bind_rows(parsed_clusters) + ## Return binded results ---- + dplyr::bind_rows(out) } + +# nocov end \ No newline at end of file diff --git a/README.md b/README.md index 029ff80..e14bc89 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostat [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![R-CMD-check](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml) +[![Test +app](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/tiwowi/wowi/graph/badge.svg)](https://app.codecov.io/gh/tiwowi/wowi) @@ -107,7 +109,7 @@ citation("wowi") Tomás Zaba (2025). _wowi: Utilities for detecting statistically significant spatial clusters of high acute malnutrition rates using - SaTScan's Bernoulli spatial-scan model_. R package version 1.0.1, + SaTScan's Bernoulli spatial-scan model_. R package version 1.0.2, . A BibTeX entry for LaTeX users is @@ -116,7 +118,7 @@ citation("wowi") title = {wowi: Utilities for detecting statistically significant spatial clusters of high acute malnutrition rates using SaTScan's Bernoulli spatial-scan model}, author = {{Tomás Zaba}}, year = {2025}, - note = {R package version 1.0.1}, + note = {R package version 1.0.2}, url = {https://tiwowi.github.io/wowi/}, } diff --git a/README.qmd b/README.qmd index d22dfb0..2da606b 100644 --- a/README.qmd +++ b/README.qmd @@ -10,6 +10,7 @@ format: gfm [![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip) [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![R-CMD-check](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml) +[![Test app](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tiwowi/wowi/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/tiwowi/wowi/graph/badge.svg)](https://app.codecov.io/gh/tiwowi/wowi) diff --git a/inst/CITATION b/inst/CITATION index ea63582..a088d47 100644 --- a/inst/CITATION +++ b/inst/CITATION @@ -4,6 +4,6 @@ bibentry( title = "wowi: Utilities for detecting statistically significant spatial clusters of high acute malnutrition rates using SaTScan's Bernoulli spatial-scan model", author = person("Tomás Zaba"), year = 2025, - note = "R package version 1.0.1", + note = "R package version 1.0.2", url = "https://tiwowi.github.io/wowi/" ) diff --git a/inst/app/ui.R b/inst/app/ui.R index c9faf0b..7d5d294 100644 --- a/inst/app/ui.R +++ b/inst/app/ui.R @@ -43,7 +43,7 @@ ui <- tagList( ), ### Right side of the page navigation bar ---- - tags$span("v1.0.1", + tags$span("v1.0.2", id = "app-version", style = "font-size: 12.5px; color: rgba(31, 42, 68, 0.58); position: fixed; top: 40px; right: 20px;" diff --git a/man/parse_clusters.Rd b/man/parse_clusters.Rd new file mode 100644 index 0000000..2542a3e --- /dev/null +++ b/man/parse_clusters.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{parse_clusters} +\alias{parse_clusters} +\title{Extract results from SaTScan-text-based output} +\usage{ +parse_clusters(file) +} +\arguments{ +\item{file}{SaTScan-text-based output result given as "main" to be parsed} +} +\description{ +Extract results from SaTScan-text-based output +} +\keyword{internal} diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index a1f4a6c..c736a40 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -25,6 +25,7 @@ testthat::test_that( .by = "zscores", oedema = oedema ) + ### Create a temporary directory ---- tmp <- withr::local_tempdir() # ensures cleanup after test @@ -80,5 +81,23 @@ testthat::test_that( testthat::expect_true(is.double(dplyr::pull(r[[1]][16]))) testthat::expect_true(is.double(dplyr::pull(r[[1]][17]))) testthat::expect_true(is.character(dplyr::pull(r[[1]][18]))) + + # ## Check if results are in the tibble are correct ---- + df <- r$.df + testthat::expect_equal(df$nr_EAs[1], 36) + testthat::expect_equal(df$total_cases[1], 26) + testthat::expect_equal(df$"%_cases"[1], 7.8) + testthat::expect_equal(df$location_ids[1], "10,9") + testthat::expect_equal(df$geo[1], "34.113909 N, 3.087933 E") + testthat::expect_equal(df$radius[1], "1.20 km") + testthat::expect_equal(df$span[1], "1.20 km") + testthat::expect_equal(df$children[1], 25) + testthat::expect_equal(df$n_cases[1], 6) + testthat::expect_equal(df$expected_cases[1], 1.95) + testthat::expect_equal(df$observedExpected[1], 3.07) + testthat::expect_equal(df$relative_risk[1], 3.70) + testthat::expect_equal(df$"%_cases_in_area"[1], 24.0) + testthat::expect_equal(df$log_lik_ratio[1], 3.458213) + testthat::expect_equal(df$pvalue[1], 0.55) } )