Repository: Gilead-BioStats/gsm.reporting
Pull request #32: Add functionality to calculate change from previous snapshot.
Author: login: samussiah
Created: 2025-05-14T21:44:57Z | Merged: 2025-05-28T19:43:03Z

- Resolves #31 

```
load_all()
dfResults <- gsm.core::reportingResults |> dplyr::filter(SnapshotDate  == "2025-04-01")
dfResultsLongitudinal <- gsm.core::reportingResults |> dplyr::filter(SnapshotDate  != "2025-04-01")
dfChChChChanges <- CalculateChange(dfResults = dfResults,
                                   dfResultsLongitudinal = dfResultsLongitudinal)
)
```

Linked issue #31: Feature: Add attribute deltas to `dfResults`
## Feature Details
<!--- What should the new feature add -->
Calculate delta between current and previous snapshot (or provided snapshot date) for all of the following in the `dfResults` table via the `Results.yaml` workflow:​

- Z-score​
- Metric​ (for endpoints primarily)
- Enrollment​
- Numerator​
- Denominator ​


## Example Code
<!--- Not required, what might code using this new feature look like -->

## Possible Implementation
<!--- Not required, suggest an idea for implementing addition or change -->

## Additional Comments
<!--- Not required, anything else import pertaining to this feature -->

Default to previous snapshot if no date is supplied, but `previous_date` argument is important for gismo implementation.  Always include `previous_snapshot` as a field in the `dfResults` table for clarity.


Diff:
diff --git a/.github/ISSUE_TEMPLATE/business_requirement.md b/.github/ISSUE_TEMPLATE/business_requirement.md
new file mode 100644
index 0000000..51e4013
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/business_requirement.md
@@ -0,0 +1,17 @@
+---
+name: "Business Requirement Issue Template"  
+about: "Implement a New Business Requirement"
+title: "Business Requirement: (fill in)"
+---
+<!--- Make sure to add this issue to the RBQM Release Project and attach this to the appropriate release milestone -->
+<!--- All relevant features/bug fixes/qc improvements should be sub-issues of this project -->
+
+## Business Requirement Details
+<!--- What should the new feature add -->
+
+## Possible Implementation
+<!--- Not required, suggest an idea for implementing addition or change -->
+
+## Additional Comments
+<!--- Not required, anything else import pertaining to this feature -->
+
diff --git a/NAMESPACE b/NAMESPACE
index d4a157d..97a00d5 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,6 +1,7 @@
 # Generated by roxygen2: do not edit by hand
 
 export(BindResults)
+export(CalculateChange)
 export(MakeBounds)
 export(MakeMetric)
 import(dplyr)
diff --git a/R/CalculateChange.R b/R/CalculateChange.R
new file mode 100644
index 0000000..a614622
--- /dev/null
+++ b/R/CalculateChange.R
@@ -0,0 +1,179 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [gsm.core::Summarize()] to
+#'   [gsm.reporting::BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param dfResultsLongitudinal `data.frame` A stacked summary of analysis pipeline output
+#'   from the previous snapshot.
+#'   Created by passing a list of results returned by [gsm.core::Summarize()] to
+#'   [gsm.reporting::BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param strMetricColumns `character` A vector of numeric column names with which calculate change from
+#'   previous snapshot. Default:
+#'   - Numerator
+#'   - Denominator
+#'   - Metric
+#'   - Score
+#'   - Flag
+#'
+#' @return `data.frame` A transposed table of results with a column for each attribute, its value,
+#'   and the change from the previous snapshot.
+#'
+#' @examples
+#' dfResults <- gsm.core::reportingResults |> dplyr::filter(SnapshotDate  == "2025-04-01")
+#' dfResultsLongitudinal <- gsm.core::reportingResults |> dplyr::filter(SnapshotDate  != "2025-04-01")
+#' dfChChChChanges <- CalculateChange(dfResults = dfResults,
+#'                                    dfResultsLongitudinal = dfResultsLongitudinal)
+#'
+#' @export
+
+CalculateChange <- function(
+    dfResults,
+    dfResultsLongitudinal = NULL,
+    strIDColumns = c(
+        "StudyID",
+        "GroupLevel",
+        "GroupID",
+        "MetricID"
+    ),
+    strSnapshotDateColumn = "SnapshotDate",
+    dPrevSnapshotDate = NULL,
+    strMetricColumns = c(
+        "Numerator",
+        "Denominator",
+        "Metric",
+        "Score",
+        "Flag"
+    )
+) {
+    stop_if(
+        cnd = !is.data.frame(dfResults),
+        message = "`dfResults` must be a data frame."
+    )
+
+    # If dfResultsLongitudinal is not a data.frame, just return the dfResults
+    if (!is.data.frame(dfResultsLongitudinal) || nrow(dfResultsLongitudinal) == 0) {
+      cli::cli_alert_info("`dfResultsLongitudinal` not supplied properly, so dfResults will be returned unchanged.")
+      return(dfResults)
+    }
+
+    # Check that all columns exist in [ dfResults ] and [ dfResultsLongitudinal ].
+    for (strColumn in c(
+        strIDColumns,
+        strSnapshotDateColumn,
+        strMetricColumns
+    )) {
+        stop_if(
+            cnd = !strColumn %in% colnames(dfResults),
+            message = glue::glue("`{strColumn} not found in `dfResults`.")
+        )
+      stop_if(
+        cnd = !strColumn %in% colnames(dfResultsLongitudinal),
+        message = glue::glue("`{strColumn} not found in `dfResultsLongitudinal`.")
+      )
+    }
+
+    # Determine current snapshot date and ensure formatted as date
+    dCurrentSnapshotDate <- max(dfResults[[strSnapshotDateColumn]], na.rm = T)  %>% as.Date()
+
+    # Filter out current snapshot from longitudinal data if present
+    if (!is.null(dfResultsLongitudinal) && nrow(dfResultsLongitudinal) > 0) {
+      if (dCurrentSnapshotDate %in% dfResultsLongitudinal[[strSnapshotDateColumn]]) {
+        cli::cli_alert_info("Current snapshot date ({dCurrentSnapshotDate}) found in longitudinal data - removing to avoid duplication.")
+      }
+
+      # Remove current snapshot from longitudinal data
+      dfResultsLongitudinal <- dfResultsLongitudinal %>%
+        dplyr::filter(.data[[strSnapshotDateColumn]] != dCurrentSnapshotDate)
+
+      # Get latest previous snapshot date
+      available_dates <- sort(unique(dfResultsLongitudinal[[strSnapshotDateColumn]]))
+      if (length(available_dates) == 0) {
+        cli::cli_alert_info("No valid previous snapshots in longitudinal data - returning current results unchanged.")
+        return(dfResults)
+      }
+    }
+
+    # Bind dfResults and dfResultsLongitudinal and keep appropriate columns
+    dfResults_combined <- dplyr::bind_rows(dfResultsLongitudinal, dfResults) %>%
+      select(all_of(c(strIDColumns,
+                      strSnapshotDateColumn,
+                      strMetricColumns)))
+
+
+    # If dPrevSnapshotDate is not null, then filter the data to only the current and previous snapshot dates.
+    # This is primarily for gismo implementation when intermediary snapshots are taken but not reported.
+    if (!is.null(dPrevSnapshotDate)) {
+      dPrevSnapshotDate <- dPrevSnapshotDate %>% as.Date()
+      # ensure dPrevSnapshotDate is a date in dfResults$SnapshotDate
+      # and that it is not equal to the current snapshot date.
+      stop_if(
+        cnd = !dPrevSnapshotDate %in% dfResults_combined$SnapshotDate,
+        message = glue::glue("`{dPrevSnapshotDate} not found in `dfResults$SnapshotDate`.")
+      )
+      stop_if(
+        cnd = dPrevSnapshotDate == dCurrentSnapshotDate,
+        message = glue::glue("`dPrevSnapshotDate cannot be equal to the current snapshot date in dfResults`.")
+      )
+
+      # filter dfResults based on SnapshotDate
+      dfResults <- dfResults %>%
+        dplyr::filter(.data[[strSnapshotDateColumn]] %in% c(dPrevSnapshotDate, dCurrentSnapshotDate))
+
+    } else {
+      dPrevSnapshotDate <- sort(dfResults_combined[[strSnapshotDateColumn]] %>% unique())[-2]
+    }
+
+    # Calculate change from previous snapshot.
+    dfChanges <- dfResults_combined %>%
+        tidyr::pivot_longer(
+            cols = all_of(strMetricColumns),
+            names_to = "Param",
+            values_to = "Value"
+        ) %>%
+        dplyr::mutate(
+            Param = factor(.data$Param, levels = strMetricColumns)
+        ) %>%
+        dplyr::group_by(across(all_of(c(
+            strIDColumns,
+            'Param'
+        )))) %>%
+        dplyr::arrange(
+            strSnapshotDateColumn,
+            .by_group = TRUE
+        ) %>%
+        dplyr::mutate(
+            Previous = dplyr::lag(.data$Value),
+            Change = Value - dplyr::lag(.data$Value),
+            PercentChange = .data$Change / dplyr::lag(.data$Value) * 100,
+            PrevSnapshotDate = dplyr::lag(.data[[strSnapshotDateColumn]])
+        ) %>%
+        dplyr::ungroup() %>%
+        dplyr::arrange(across(all_of(c(
+            strIDColumns,
+            'PrevSnapshotDate',
+            'Param',
+            strSnapshotDateColumn
+        )))) %>%
+      tidyr::pivot_wider(names_from = Param,
+                         values_from = c(Value, Previous, Change, PercentChange),
+                         names_glue = "{Param}_{.value}",) %>%
+      dplyr::rename_with(~ gsub("_Value", "", .x), tidyr::ends_with("_Value"))
+
+    return(dfChanges)
+}
diff --git a/gsm.reporting.Rproj b/gsm.reporting.Rproj
index cedeed2..69fafd4 100644
--- a/gsm.reporting.Rproj
+++ b/gsm.reporting.Rproj
@@ -1,5 +1,4 @@
 Version: 1.0
-ProjectId: e605176b-ba1b-4eb6-b4d7-7f87042f0e73
 
 RestoreWorkspace: No
 SaveWorkspace: No
diff --git a/inst/workflow/3_reporting/Results.yaml b/inst/workflow/3_reporting/Results.yaml
index 390dfaf..c501633 100644
--- a/inst/workflow/3_reporting/Results.yaml
+++ b/inst/workflow/3_reporting/Results.yaml
@@ -26,4 +26,15 @@ steps:
       strName: "Analysis_Summary"
       strStudyID: strStudyID
       dSnapshotDate: dSnapshotDate
+  # Stack latest snapshot with previous snapshots.
+  - output: Reporting_Results_With_Changes
+    name: gsm.reporting::CalculateChange
+    params:
+      dfResults: Results_Summary
+      dfResultsLongitudinal: Reporting_Results_Longitudinal
+  # Subset on latest snapshot.
+  - output: Reporting_Results
+    name: gsm.kri::FilterByLatestSnapshot
+    params:
+      df: Reporting_Results_With_Changes
 
diff --git a/man/CalculateChange.Rd b/man/CalculateChange.Rd
new file mode 100644
index 0000000..254f714
--- /dev/null
+++ b/man/CalculateChange.Rd
@@ -0,0 +1,68 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/CalculateChange.R
+\name{CalculateChange}
+\alias{CalculateChange}
+\title{Calculate Change}
+\usage{
+CalculateChange(
+  dfResults,
+  dfResultsLongitudinal = NULL,
+  strIDColumns = c("StudyID", "GroupLevel", "GroupID", "MetricID"),
+  strSnapshotDateColumn = "SnapshotDate",
+  dPrevSnapshotDate = NULL,
+  strMetricColumns = c("Numerator", "Denominator", "Metric", "Score", "Flag")
+)
+}
+\arguments{
+\item{dfResults}{\code{data.frame} A stacked summary of analysis pipeline output.
+Created by passing a list of results returned by \code{\link[gsm.core:Summarize]{gsm.core::Summarize()}} to
+\code{\link[=BindResults]{BindResults()}}. Expected columns: \code{GroupID}, \code{GroupLevel}, \code{Numerator},
+\code{Denominator}, \code{Metric}, \code{Score}, \code{Flag}, \code{MetricID}, \code{StudyID},
+\code{SnapshotDate}.}
+
+\item{dfResultsLongitudinal}{\code{data.frame} A stacked summary of analysis pipeline output
+from the previous snapshot.
+Created by passing a list of results returned by \code{\link[gsm.core:Summarize]{gsm.core::Summarize()}} to
+\code{\link[=BindResults]{BindResults()}}. Expected columns: \code{GroupID}, \code{GroupLevel}, \code{Numerator},
+\code{Denominator}, \code{Metric}, \code{Score}, \code{Flag}, \code{MetricID}, \code{StudyID},
+\code{SnapshotDate}.}
+
+\item{strIDColumns}{\code{character} A vector of column names with which to group
+results. Default:
+\itemize{
+\item StudyID
+\item GroupLevel
+\item GroupID
+\item MetricID
+}}
+
+\item{strSnapshotDateColumn}{\code{character} The name of the column containing the
+snapshot date. Default: \code{SnapshotDate}.}
+
+\item{dPrevSnapshotDate}{\code{Date} The date of the previous snapshot to be compared.
+Optional. Default = \code{NULL}.}
+
+\item{strMetricColumns}{\code{character} A vector of numeric column names with which calculate change from
+previous snapshot. Default:
+\itemize{
+\item Numerator
+\item Denominator
+\item Metric
+\item Score
+\item Flag
+}}
+}
+\value{
+\code{data.frame} A transposed table of results with a column for each attribute, its value,
+and the change from the previous snapshot.
+}
+\description{
+Calculate the change from the previous snapshot date.
+}
+\examples{
+dfResults <- gsm.core::reportingResults |> dplyr::filter(SnapshotDate  == "2025-04-01")
+dfResultsLongitudinal <- gsm.core::reportingResults |> dplyr::filter(SnapshotDate  != "2025-04-01")
+dfChChChChanges <- CalculateChange(dfResults = dfResults,
+                                   dfResultsLongitudinal = dfResultsLongitudinal)
+
+}
diff --git a/tests/testthat/test-CalculateChange.R b/tests/testthat/test-CalculateChange.R
new file mode 100644
index 0000000..9e99677
--- /dev/null
+++ b/tests/testthat/test-CalculateChange.R
@@ -0,0 +1,228 @@
+## set up test data and params
+
+dfResults <- data.frame(
+  StudyID = c(1, 1, 1, 1, 1, 1),
+  GroupLevel = c("A", "A", "A", "A", "A", "A"),
+  GroupID = c(1, 2, 3, 4, 5, 6),
+  MetricID = c("Metric A", "Metric B", "Metric C", "Metric D", "Metric E", "Metric F"),
+  SnapshotDate = as.Date(c("2023-03-01", "2023-03-01", "2023-03-01", "2023-03-01", "2023-03-01", "2023-03-01")),
+  Numerator = c(10, 25, 40, 5, 5, 15),
+  Denominator = c(100, 250, 400, 50, 50, 150),
+  Metric = c(.1, .1, .1, .1, .1, .1),
+  Score = c(0.1, 0.2, 0.3, 0.05, 0.1, 0.15),
+  Flag = c(-2, 0, 1, 0, 0, 2)
+)
+
+dfPrevious <- data.frame(
+  StudyID = c(1, 1, 1, 1, 1, 1),
+  GroupLevel = c("A", "A", "A", "A", "A", "A"),
+  GroupID = c(1, 2, 3, 4, 5, 6),
+  MetricID = c("Metric A", "Metric B", "Metric C", "Metric D", "Metric E", "Metric F"),
+  SnapshotDate = as.Date(c("2023-02-01", "2023-02-01", "2023-02-01", "2023-02-01", "2023-02-01", "2023-02-01")),
+  Numerator = c(10, 20, 30, 5, 10, 15),
+  Denominator = c(100, 200, 300, 50, 100, 150),
+  Metric = c(.1, .1, .1, .1, .1, .1),
+  Score = c(0.1, 0.2, 0.3, 0.05, 0.1, 0.15),
+  Flag = c(-2, -1, 0, 0, 1, 2)
+)
+
+strIDColumns <- c(
+  "StudyID",
+  "GroupLevel",
+  "GroupID",
+  "MetricID"
+)
+
+strSnapshotDateColumn <- "SnapshotDate"
+
+strMetricColumns <- c(
+  "Numerator",
+  "Denominator",
+  "Metric",
+  "Score",
+  "Flag"
+)
+
+test_that(
+    '[ CalculateChange ] correctly calculates change from the previous snapshot.',
+    {
+
+        # [ CalculateChange ] returns a data frame with one record per combination of the
+        # following columns
+        # - StudyID
+        # - GroupLevel
+        # - GroupID
+        # - MetricID
+        # - SnapshotDate
+        dfChanges <- CalculateChange(
+            dfResults = dfResults,
+            dfResultsLongitudinal = dfPrevious
+        )
+
+        # Check that the output is a data frame.
+        expect_true(is.data.frame(dfChanges))
+
+        # Check that the output has the expected number of rows.
+        expect_equal(
+            nrow(dfChanges),
+            sum(nrow(dfResults), nrow(dfPrevious))
+        )
+
+        # Check that the output has the expected columns.
+        expect_true(all(c(
+            strIDColumns,
+            strSnapshotDateColumn,
+            "PrevSnapshotDate",
+            paste(strMetricColumns,"Previous", sep = "_"),
+            paste(strMetricColumns,"Change", sep = "_"),
+            paste(strMetricColumns,"PercentChange", sep = "_")
+        ) %in% colnames(dfChanges)))
+
+
+        # Check that the output has the expected values.
+        expect_equal(
+            dfChanges$Numerator,
+            c(10, 10, 25, 20, 40, 30,  5,  5,  5, 10, 15, 15)
+        )
+        expect_equal(
+            dfChanges$Denominator,
+            c(100, 100, 250, 200, 400, 300,  50,  50,  50, 100, 150, 150)
+        )
+        expect_equal(
+            dfChanges$Metric,
+            c(.1, .1, .1, .1, .1, .1, .1, .1, .1, .1, .1, .1)
+        )
+        expect_equal(
+            dfChanges$Score,
+            c(0.10, 0.10, 0.20, 0.20, 0.30, 0.30, 0.05, 0.05, 0.10, 0.10, 0.15, 0.15)
+        )
+        expect_equal(
+            dfChanges$Flag,
+            c(-2, -2,  0, -1,  1,  0,  0,  0,  0,  1,  2,  2)
+        )
+
+        # Check that the output has the expected change values.
+        expect_equal(
+            dfChanges$Numerator_Change,
+            c(0, NA,  5, NA, 10, NA,  0, NA, -5, NA,  0, NA)
+        )
+        expect_equal(
+            dfChanges$Denominator_Change,
+            c(0,  NA,  50,  NA, 100,  NA,   0,  NA, -50,  NA,   0,  NA)
+        )
+        expect_equal(
+            dfChanges$Metric_Change,
+            c( 0, NA,  0, NA,  0, NA,  0, NA, 0, NA, 0, NA)
+        )
+        expect_equal(
+            dfChanges$Score_Change,
+            c( 0, NA,  0, NA,  0, NA,  0, NA, 0, NA, 0, NA)
+        )
+        expect_equal(
+            dfChanges$Flag_Change,
+            c( 0, NA,  1, NA,  1, NA,  0, NA, -1, NA,  0, NA)
+        )
+
+        # Check that the output has the expected percent change values.
+        expect_equal(
+            dfChanges$Numerator_PercentChange,
+            c(0,NA, 25, NA, 100/3, NA, 0, NA, -50, NA, 0, NA)
+        )
+        expect_equal(
+            dfChanges$Denominator_PercentChange,
+            c(0,NA, 25, NA, 100/3, NA, 0, NA, -50, NA, 0, NA)
+        )
+        expect_equal(
+            dfChanges$Metric_PercentChange,
+            c( 0, NA,  0, NA,  0, NA,  0, NA, 0, NA, 0, NA)
+        )
+        expect_equal(
+            dfChanges$Score_PercentChange,
+            c( 0, NA,  0, NA,  0, NA,  0, NA, 0, NA, 0, NA)
+        )
+        expect_equal(
+            dfChanges$Flag_PercentChange,
+            c(0,   NA, -100,   NA,  Inf,   NA,  NaN,   NA, -100,   NA,    0,  NA)
+        )
+    }
+)
+
+# test_that(
+#     '[ CalculateChange ] correctly calculates change from two snapshots ago.',
+#     {
+#         # Create a sample data frame with three snapshots.
+#         dfResults <- data.frame(
+#             StudyID = c(1, 1, 1, 2, 2, 2),
+#             GroupLevel = c("A", "A", "A", "B", "B", "B"),
+#             GroupID = c(1, 1, 1, 2, 2, 2),
+#             MetricID = c("Metric A", "Metric A", "Metric A", "Metric B", "Metric B", "Metric B"),
+#             SnapshotDate = as.Date(c("2023-01-01", "2023-02-01", "2023-03-01", "2023-01-01", "2023-02-01", "2023-03-01")),
+#             Numerator = c(10, 20, 30, 5, 10, 15),
+#             Denominator = c(100, 200, 300, 50, 100, 150),
+#             Metric = c(.1, .1, .1, .1, .1, .1),
+#             Score = c(0.1, 0.2, 0.3, 0.05, 0.1, 0.15),
+#             Flag = c(-2, -1, 0, 0, 1, 2)
+#         )
+#
+#         dfChanges <- CalculateChange(
+#             dfResults = dfResults,
+#             intSnapshots = 2
+#         )
+#
+#         # Check that the output has the expected change values.
+#         expect_equal(
+#             dfChanges$Change[dfChanges$Param == "Numerator"],
+#             c(NA, NA, 20, NA, NA, 10)
+#         )
+#         expect_equal(
+#             dfChanges$Change[dfChanges$Param == "Denominator"],
+#             c(NA, NA, 200, NA, NA, 100)
+#         )
+#         expect_equal(
+#             dfChanges$Change[dfChanges$Param == "Metric"],
+#             c(NA, NA, 0, NA, NA, 0)
+#         )
+#         expect_equal(
+#             dfChanges$Change[dfChanges$Param == "Score"],
+#             c(NA, NA, 0.2, NA, NA, 0.1)
+#         )
+#         expect_equal(
+#             dfChanges$Change[dfChanges$Param == "Flag"],
+#             c(NA, NA, 2, NA, NA, 2)
+#         )
+#
+#         # Check that the output has the expected percent change values.
+#         expect_equal(
+#             dfChanges$PercentChange[dfChanges$Param == "Numerator"],
+#             c(NA, NA, 200, NA, NA, 200)
+#         )
+#         expect_equal(
+#             dfChanges$PercentChange[dfChanges$Param == "Denominator"],
+#             c(NA, NA, 200, NA, NA, 200)
+#         )
+#         expect_equal(
+#             dfChanges$PercentChange[dfChanges$Param == "Metric"],
+#             c(NA, NA, 0, NA, NA, 0)
+#         )
+#         expect_equal(
+#             dfChanges$PercentChange[dfChanges$Param == "Score"],
+#             c(NA, NA, 200, NA, NA, 200)
+#         )
+#         expect_equal(
+#             dfChanges$PercentChange[dfChanges$Param == "Flag"],
+#             c(NA, NA, -100, NA, NA, Inf)
+#         )
+#     }
+# )
+
+test_that(
+  '[ CalculateChange ] returns message and dfResults unchanged if dfPrevious not supplied.',
+  {
+    dfChanges <- CalculateChange(
+      dfResults = dfResults
+    )
+    expect_equal(dfChanges, dfResults)
+
+    expect_message(CalculateChange(
+      dfResults = dfResults), regexp = "dfResults will be returned unchanged")
+})


Reviews:
user: login: zdz2101
body: In terms of features added/implemented it makes sense to me
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T22:34:22Z
user: login: jwildfire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:46:14Z
user: login: jwildfire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:47:49Z
user: login: jwildfire
body: Looks great in general! 

Added a few nitty-gritty comments for yall to consider. My one main question is about whether it makes sense to just add columns in `dfResults` or create a whole new `dfChanges` reporting table. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:15:20Z
user: login: lauramaxwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T19:45:24Z
user: login: samussiah
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T13:46:01Z
user: login: samussiah
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T13:47:46Z
user: login: annezheng2
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T17:31:09Z

Review comments:
diff_hunk: @@ -0,0 +1,25 @@
+meta:
+  Type: Reporting
+  ID: Changes
+  Description: Calculate change from previous snapshot.
+  Priority: 2
+spec:
+  Reporting_Results:
path: inst/workflow/3_reporting/Changes.yaml
user: login: zdz2101
body: This all certainly makes sense to me, I think the only thing I'd have to say is when we run `BindResults()` via `Results.yaml` we are generally grabbing the `Sys.Date()` so singular results dataset tend to only have one snapshot at a time. The features/code from this PR seem aimed directly for that `Reporting_Results_Longitudinal` object we may have in our snapshotting process(?)
created_at: 2025-05-14T22:33:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
path: R/CalculateChange.R
user: login: jwildfire
body: Not sure I understand the need for this ... 
created_at: 2025-05-21T14:46:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
+#' @param strMetricColumns `character` A vector of numeric column names with which calculate change from
+#'   previous snapshot. Default:
+#'   - Numerator
+#'   - Denominator
+#'   - Metric
+#'   - Score
+#'   - Flag
+#' @param strDfOutput `character` The desired output format of dfChanges. Options are "wide" and "long".
+#'    Default = `"long"`
+#'
+#' @return `data.frame` A transposed table of results with a column for each attribute, its value,
+#'   and the change from the previous snapshot.
+#'
+#' @examples
+#' dfChChChChanges <- CalculateChange(gsm.core::reportingResults)
path: R/CalculateChange.R
user: login: jwildfire
body: 😆 
created_at: 2025-05-21T14:47:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
path: R/CalculateChange.R
user: login: jwildfire
body: Ah, ok. I see now. it's just the number of positions to lag.  dPrevSnapshotDate handles this, so I think we should just remove this parameter altogether and force a 1 record lag.
created_at: 2025-05-21T14:54:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
+#' @param strMetricColumns `character` A vector of numeric column names with which calculate change from
+#'   previous snapshot. Default:
+#'   - Numerator
+#'   - Denominator
+#'   - Metric
+#'   - Score
+#'   - Flag
+#' @param strDfOutput `character` The desired output format of dfChanges. Options are "wide" and "long".
+#'    Default = `"long"`
+#'
+#' @return `data.frame` A transposed table of results with a column for each attribute, its value,
+#'   and the change from the previous snapshot.
+#'
+#' @examples
+#' dfChChChChanges <- CalculateChange(gsm.core::reportingResults)
+#'
+#' @export
+
+CalculateChange <- function(
+    dfResults,
+    strIDColumns = c(
+        "StudyID",
+        "GroupLevel",
+        "GroupID",
+        "MetricID"
+    ),
+    strSnapshotDateColumn = "SnapshotDate",
+    intSnapshots = 1,
+    dPrevSnapshotDate = NULL,
+    strMetricColumns = c(
+        "Numerator",
+        "Denominator",
+        "Metric",
+        "Score",
+        "Flag"
+    ),
+    strDfOutput = "long"
+) {
+    stop_if(
+        cnd = !is.data.frame(dfResults),
+        message = "`dfResults` must be a data frame."
+    )
+
+    # Check that all columns exist in [ dfResults ].
+    for (strColumn in c(
+        strIDColumns,
+        strSnapshotDateColumn,
+        strMetricColumns
+    )) {
+        stop_if(
+            cnd = !strColumn %in% colnames(dfResults),
+            message = glue::glue("`{strColumn} not found in `dfResults`.")
+        )
+    }
+
+    # If dPrevSnapshotDate is not null, then filter the data to only the current and previous snapshot dates.
+    # This is primarily for gismo implementation when intermediary snapshots are taken but not reported.
+    if (!is.null(dPrevSnapshotDate)) {
+
+      # determine current snapshot date and ensure both are formatted as dates
+      dCurrentSnapshotDate <- max(dfResults$SnapshotDate)  %>% as.Date()
+      dPrevSnapshotDate <- dPrevSnapshotDate %>% as.Date()
+
+      # ensure dPrevSnapshotDate is a date in dfResults$SnapshotDate
+      # and that it is not equal to the current snapshot date.
+      stop_if(
+        cnd = !dPrevSnapshotDate %in% dfResults$SnapshotDate,
+        message = glue::glue("`{dPrevSnapshotDate} not found in `dfResults$SnapshotDate`.")
+      )
+      stop_if(
+        cnd = dPrevSnapshotDate == dCurrentSnapshotDate,
+        message = glue::glue("`dPrevSnapshotDate cannot be equal to the current snapshot date in dfResults`.")
+      )
+
+      # filter dfResults based on SnapshotDate
+      dfResults <- dfResults %>%
+        dplyr::filter(SnapshotDate %in% c(dPrevSnapshotDate, dCurrentSnapshotDate))
+
+    } else {
+      dCurrentSnapshotDate <- max(dfResults$SnapshotDate)  %>% as.Date()
+      dPrevSnapshotDate <- sort(dfResults$SnapshotDate %>% unique())[-2]
+    }
+
+    # Calculate change from previous snapshot.
+    dfChanges <- dfResults %>%
+        tidyr::pivot_longer(
+            cols = all_of(strMetricColumns),
+            names_to = "Param",
+            values_to = "Value"
+        ) %>%
+        dplyr::mutate(
+            Param = factor(.data$Param, levels = strMetricColumns)
+        ) %>%
+        dplyr::group_by(across(all_of(c(
+            strIDColumns,
+            'Param'
+        )))) %>%
+        dplyr::arrange(
+            strSnapshotDateColumn,
+            .by_group = TRUE
+        ) %>%
+        dplyr::mutate(
+            Change = Value - dplyr::lag(.data$Value, n = intSnapshots),
+            PercentChange = .data$Change / dplyr::lag(.data$Value, n = intSnapshots) * 100
path: R/CalculateChange.R
user: login: jwildfire
body: Remove the `n=intSnapshot` parameters and just use 1 (which is the default). 
created_at: 2025-05-21T14:55:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
+#' @param strMetricColumns `character` A vector of numeric column names with which calculate change from
+#'   previous snapshot. Default:
+#'   - Numerator
+#'   - Denominator
+#'   - Metric
+#'   - Score
+#'   - Flag
+#' @param strDfOutput `character` The desired output format of dfChanges. Options are "wide" and "long".
path: R/CalculateChange.R
user: login: jwildfire
body: I think we should default to `wide`, but need to look at workflows to confirm ... 
created_at: 2025-05-21T14:56:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,240 @@
+## set up test data and params
+
+dfResults <- data.frame(
+  StudyID = c(1, 1, 1, 2, 2, 2),
+  GroupLevel = c("A", "A", "A", "B", "B", "B"),
+  GroupID = c(1, 1, 1, 2, 2, 2),
+  MetricID = c("Metric A", "Metric A", "Metric A", "Metric B", "Metric B", "Metric B"),
+  SnapshotDate = as.Date(c("2023-01-01", "2023-02-01", "2023-03-01", "2023-01-01", "2023-02-01", "2023-03-01")),
+  Numerator = c(10, 20, 30, 5, 10, 15),
+  Denominator = c(100, 200, 300, 50, 100, 150),
+  Metric = c(.1, .1, .1, .1, .1, .1),
+  Score = c(0.1, 0.2, 0.3, 0.05, 0.1, 0.15),
+  Flag = c(-2, -1, 0, 0, 1, 2)
+)
+
+strIDColumns <- c(
+  "StudyID",
+  "GroupLevel",
+  "GroupID",
+  "MetricID"
+)
+
+strSnapshotDateColumn <- "SnapshotDate"
+
+strMetricColumns <- c(
+  "Numerator",
+  "Denominator",
+  "Metric",
+  "Score",
+  "Flag"
+)
+
+test_that(
+    '[ CalculateChange ] correctly calculates change from the previous snapshot.',
+    {
+
+        # [ CalculateChange ] returns a transposed data frame with one record per combination of the
+        # following columns, where [ Param ] is the name of the column and [ Value ] is the value of
+        # that column:
+        # - StudyID
+        # - GroupLevel
+        # - GroupID
+        # - MetricID
+        # - SnapshotDate
+        # - Param
+        dfChanges <- CalculateChange(
+            dfResults = dfResults
+        )
+
+        # Check that the output is a data frame.
+        expect_true(is.data.frame(dfChanges))
+
+        # Check that the output has the expected number of rows.
+        expect_equal(
+            nrow(dfChanges),
+            nrow(dfResults) * length(strMetricColumns)
+        )
+
+        # Check that the output has the expected columns.
+        expect_true(all(c(
+            strIDColumns,
+            strSnapshotDateColumn,
+            "Param",
+            "Value",
+            "Change",
+            "PercentChange"
+        ) %in% colnames(dfChanges)))
+
+        # Check that the output has the expected column names.
+        expect_equal(
+            colnames(dfChanges),
+            c(
+                strIDColumns,
+                strSnapshotDateColumn,
+                "Param",
+                "Value",
+                "Change",
+                "PercentChange"
+            )
+        )
+
+        # Check that the output has the expected values.
+        expect_equal(
+            dfChanges$Value[dfChanges$Param == "Numerator"],
+            c(10, 20, 30, 5, 10, 15)
+        )
+        expect_equal(
+            dfChanges$Value[dfChanges$Param == "Denominator"],
+            c(100, 200, 300, 50, 100, 150)
+        )
+        expect_equal(
+            dfChanges$Value[dfChanges$Param == "Metric"],
+            c(.1, .1, .1, .1, .1, .1)
+        )
+        expect_equal(
+            dfChanges$Value[dfChanges$Param == "Score"],
+            c(0.1, 0.2, 0.3, 0.05, 0.1, 0.15)
+        )
+        expect_equal(
+            dfChanges$Value[dfChanges$Param == "Flag"],
+            c(-2, -1, 0, 0, 1, 2)
+        )
+
+        # Check that the output has the expected change values.
+        expect_equal(
+            dfChanges$Change[dfChanges$Param == "Numerator"],
+            c(NA, 10, 10, NA, 5, 5)
+        )
+        expect_equal(
+            dfChanges$Change[dfChanges$Param == "Denominator"],
+            c(NA, 100, 100, NA, 50, 50)
+        )
+        expect_equal(
+            dfChanges$Change[dfChanges$Param == "Metric"],
+            c(NA, 0, 0, NA, 0, 0)
+        )
+        expect_equal(
+            dfChanges$Change[dfChanges$Param == "Score"],
+            c(NA, 0.1, 0.1, NA, 0.05, 0.05)
+        )
+        expect_equal(
+            dfChanges$Change[dfChanges$Param == "Flag"],
+            c(NA, 1, 1, NA, 1, 1)
+        )
+
+        # Check that the output has the expected percent change values.
+        expect_equal(
+            dfChanges$PercentChange[dfChanges$Param == "Numerator"],
+            c(NA, 100, 50, NA, 100, 50)
+        )
+        expect_equal(
+            dfChanges$PercentChange[dfChanges$Param == "Denominator"],
+            c(NA, 100, 50, NA, 100, 50)
+        )
+        expect_equal(
+            dfChanges$PercentChange[dfChanges$Param == "Metric"],
+            c(NA, 0, 0, NA, 0, 0)
+        )
+        expect_equal(
+            dfChanges$PercentChange[dfChanges$Param == "Score"],
+            c(NA, 100, 50, NA, 100, 50)
+        )
+        expect_equal(
+            dfChanges$PercentChange[dfChanges$Param == "Flag"],
+            c(NA, -50, -100, NA, Inf, 100)
+        )
+    }
+)
+
+test_that(
+    '[ CalculateChange ] correctly calculates change from two snapshots ago.',
+    {
+        # Create a sample data frame with three snapshots.
+        dfResults <- data.frame(
+            StudyID = c(1, 1, 1, 2, 2, 2),
+            GroupLevel = c("A", "A", "A", "B", "B", "B"),
+            GroupID = c(1, 1, 1, 2, 2, 2),
+            MetricID = c("Metric A", "Metric A", "Metric A", "Metric B", "Metric B", "Metric B"),
+            SnapshotDate = as.Date(c("2023-01-01", "2023-02-01", "2023-03-01", "2023-01-01", "2023-02-01", "2023-03-01")),
+            Numerator = c(10, 20, 30, 5, 10, 15),
+            Denominator = c(100, 200, 300, 50, 100, 150),
+            Metric = c(.1, .1, .1, .1, .1, .1),
+            Score = c(0.1, 0.2, 0.3, 0.05, 0.1, 0.15),
+            Flag = c(-2, -1, 0, 0, 1, 2)
+        )
+
+        dfChanges <- CalculateChange(
+            dfResults = dfResults,
+            intSnapshots = 2
+        )
path: tests/testthat/test-CalculateChange.R
user: login: jwildfire
body: update this to use the snapshot date
created_at: 2025-05-21T14:58:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
path: R/CalculateChange.R
user: login: jwildfire
body: Or if we're going to keep both `intSnapshots` and `dPreviousSnapshotDate` we should throw and error when a date is provided and `intSnapshots > 1`. 
created_at: 2025-05-21T15:00:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
path: R/CalculateChange.R
user: login: jwildfire
body: Let's add some more text here about expected behavior when there is only a single snapshot or a value is missing in a previous snapshot. 
created_at: 2025-05-21T15:02:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
path: R/CalculateChange.R
user: login: jwildfire
body: (Looks like NA is being returned, which is fine)
created_at: 2025-05-21T15:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
+#' @param strMetricColumns `character` A vector of numeric column names with which calculate change from
+#'   previous snapshot. Default:
+#'   - Numerator
+#'   - Denominator
+#'   - Metric
+#'   - Score
+#'   - Flag
+#' @param strDfOutput `character` The desired output format of dfChanges. Options are "wide" and "long".
+#'    Default = `"long"`
+#'
+#' @return `data.frame` A transposed table of results with a column for each attribute, its value,
+#'   and the change from the previous snapshot.
+#'
+#' @examples
+#' dfChChChChanges <- CalculateChange(gsm.core::reportingResults)
+#'
+#' @export
+
+CalculateChange <- function(
+    dfResults,
+    strIDColumns = c(
+        "StudyID",
+        "GroupLevel",
+        "GroupID",
+        "MetricID"
+    ),
+    strSnapshotDateColumn = "SnapshotDate",
+    intSnapshots = 1,
+    dPrevSnapshotDate = NULL,
+    strMetricColumns = c(
+        "Numerator",
+        "Denominator",
+        "Metric",
+        "Score",
+        "Flag"
+    ),
+    strDfOutput = "long"
+) {
+    stop_if(
+        cnd = !is.data.frame(dfResults),
+        message = "`dfResults` must be a data frame."
+    )
+
+    # Check that all columns exist in [ dfResults ].
+    for (strColumn in c(
+        strIDColumns,
+        strSnapshotDateColumn,
+        strMetricColumns
+    )) {
+        stop_if(
+            cnd = !strColumn %in% colnames(dfResults),
+            message = glue::glue("`{strColumn} not found in `dfResults`.")
+        )
+    }
+
+    # If dPrevSnapshotDate is not null, then filter the data to only the current and previous snapshot dates.
+    # This is primarily for gismo implementation when intermediary snapshots are taken but not reported.
+    if (!is.null(dPrevSnapshotDate)) {
+
+      # determine current snapshot date and ensure both are formatted as dates
+      dCurrentSnapshotDate <- max(dfResults$SnapshotDate)  %>% as.Date()
+      dPrevSnapshotDate <- dPrevSnapshotDate %>% as.Date()
+
+      # ensure dPrevSnapshotDate is a date in dfResults$SnapshotDate
+      # and that it is not equal to the current snapshot date.
+      stop_if(
+        cnd = !dPrevSnapshotDate %in% dfResults$SnapshotDate,
+        message = glue::glue("`{dPrevSnapshotDate} not found in `dfResults$SnapshotDate`.")
+      )
+      stop_if(
+        cnd = dPrevSnapshotDate == dCurrentSnapshotDate,
+        message = glue::glue("`dPrevSnapshotDate cannot be equal to the current snapshot date in dfResults`.")
+      )
+
+      # filter dfResults based on SnapshotDate
+      dfResults <- dfResults %>%
+        dplyr::filter(SnapshotDate %in% c(dPrevSnapshotDate, dCurrentSnapshotDate))
+
+    } else {
+      dCurrentSnapshotDate <- max(dfResults$SnapshotDate)  %>% as.Date()
+      dPrevSnapshotDate <- sort(dfResults$SnapshotDate %>% unique())[-2]
+    }
+
+    # Calculate change from previous snapshot.
+    dfChanges <- dfResults %>%
+        tidyr::pivot_longer(
+            cols = all_of(strMetricColumns),
+            names_to = "Param",
+            values_to = "Value"
+        ) %>%
+        dplyr::mutate(
+            Param = factor(.data$Param, levels = strMetricColumns)
+        ) %>%
+        dplyr::group_by(across(all_of(c(
+            strIDColumns,
+            'Param'
+        )))) %>%
+        dplyr::arrange(
+            strSnapshotDateColumn,
+            .by_group = TRUE
+        ) %>%
+        dplyr::mutate(
+            Change = Value - dplyr::lag(.data$Value, n = intSnapshots),
path: R/CalculateChange.R
user: login: jwildfire
body: I think we should also add `Previous = lag(.data$Value)`
created_at: 2025-05-21T15:04:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+meta:
path: inst/workflow/3_reporting/Changes.yaml
user: login: jwildfire
body: I'd originally thought we'd just add columns to `dfResults` instead of adding a new `dfChanges` table, but I'm not 100% tied to that approach. I guess it's easy enough to just merge this on ... 
created_at: 2025-05-21T15:10:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
path: R/CalculateChange.R
user: login: lauramaxwell
body: i'm leaning on the side of removing the `intSnapshots` param and if multiple snapshot lags are requested we can provide the date via the `dPrevSnapshotDate` argument (or add the argument in down the line when that requirement is needed). Does that sound ok to you, @samussiah?
created_at: 2025-05-21T19:45:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+meta:
+  Type: Reporting
+  ID: Changes
+  Description: Calculate change from previous snapshot.
+  Priority: 2
+spec:
+  Reporting_Results:
path: inst/workflow/3_reporting/Changes.yaml
user: login: samussiah
body: Yeah it's a valid point... But our workflows are all set up in the context of a single snapshot, so it's up to the end user to implement in the context of multiple snapshots.  And I'm not sure we want to introduce the complexity of a longitudinal implementation here...
created_at: 2025-05-22T13:46:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+#' Calculate Change
+#'
+#' Calculate the change from the previous snapshot date.
+#'
+#' @param dfResults `data.frame` A stacked summary of analysis pipeline output.
+#'   Created by passing a list of results returned by [Summarize()] to
+#'   [BindResults()]. Expected columns: `GroupID`, `GroupLevel`, `Numerator`,
+#'   `Denominator`, `Metric`, `Score`, `Flag`, `MetricID`, `StudyID`,
+#'   `SnapshotDate`.
+#' @param strIDColumns `character` A vector of column names with which to group
+#'   results. Default:
+#'   - StudyID
+#'   - GroupLevel
+#'   - GroupID
+#'   - MetricID
+#' @param strSnapshotDateColumn `character` The name of the column containing the
+#'   snapshot date. Default: `SnapshotDate`.
+#' @param dPrevSnapshotDate `Date` The date of the previous snapshot to be compared.
+#'   Optional. Default = `NULL`.
+#' @param intSnapshots `integer` The number of snapshots to calculate changes for.
path: R/CalculateChange.R
user: login: samussiah
body: That works with me.  Easy enough to grab the snapshot date for comparison if it's ever not the previous one.
created_at: 2025-05-22T13:47:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: lauramaxwell
created_at: 2025-05-20T13:13:32Z
body: @samussiah and @jwildfire - I've added the optional argument to specify which date is considered the "previous snapshot" (`dPrevSnapshot`) in which case only the current and previous snapshots are kept and returned. We could make some minor modifications to allow this to accept a vector of dates- which right now would only cause a minor issue in creating the `PrevSnapshotDate` column at the end.

I've also added an option to return the data in "wide" format, since i believe we discussed having this data just be like `dfResults` with additional columns- in which case we could potentially just append `CalculateChange()` to the `Results.yaml` instead of creating a new df. 

Let me know what you think about these two approaches and we can go from there.
author_association: CONTRIBUTOR
user: login: jwildfire
created_at: 2025-05-21T15:18:33Z
body: One other comment is that we should probably also add a few new requirements and QC tests over in `gsm.qc`. @lauramaxwell  can you file issue and link to business requirement so we don't lose track of that. 
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-21T20:44:45Z
body: @samussiah- One question that @annezheng2 raised in chat: How will multiple snapshots' worth of reporting_results data get generated in the pipeline? If it's being fed in separately, then `CalculateChange()` will need to be in a separate workflow, like we had originally specified.
author_association: CONTRIBUTOR
user: login: zdz2101
created_at: 2025-05-21T20:52:05Z
body: > @samussiah- One question that @annezheng2 raised in chat: How will multiple snapshots' worth of reporting_results data get generated in the pipeline? If it's being fed in separately, then `CalculateChange()` will need to be in a separate workflow, like we had originally specified.

This is the `Reporting_Results_Longitudinal` object right @samussiah ?
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-22T12:58:26Z
body: > > @samussiah- One question that @annezheng2 raised in chat: How will multiple snapshots' worth of reporting_results data get generated in the pipeline? If it's being fed in separately, then `CalculateChange()` will need to be in a separate workflow, like we had originally specified.
> 
> This is the `Reporting_Results_Longitudinal` object right @samussiah ?

Right, but that object is currently just created via gsm.template data.config after the Results table is written, so i think we need to re-think where in the pipeline this delta step can work.
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-22T13:59:37Z
body: For future reference here's an implementation that assumes a longitudinal results table is available:
```
meta:
  Type: Reporting
  ID: Results
  Description: Reporting Results Data
  Priority: 1
spec:
  Analysis_Summary:
    _all:
      required: true
  Reporting_Results_Longitudinal:
    _all:
      required: true
steps:
  # Stack all analysis results data into a single data frame.
  - output: Reporting_Results_Snapshot
    name: RunQuery
    params:
      df: Analysis_Summary
      strQuery: 'SELECT * FROM df ORDER BY MetricID'
  # Stack latest snapshot with previous snapshots.
  - output: Reporting_Results_Longitudinal_Latest
    name: dplyr::bind_rows
    params:
      Reporting_Results_Longitudinal: Reporting_Results_Longitudinal
      Reporting_Results_Snapshot: Reporting_Results_Snapshot
  # Calculate change from previous snapshot.
  - output: Reporting_Results_With_Changes
    name: gsm.reporting::CalculateChange
    params:
      dfResults: Reporting_Results_Longitudinal_Latest
  # Subset on latest snapshot.
  - output: Reporting_Results
    name: gsm.kri::FilterByLatestSnapshot
    params:
      df: Reporting_Results_With_Changes
```
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-22T18:07:26Z
body: @lauramaxwell would it make more sense to pass in a df of the Longitudinal results that will contain all kri-related data rather than just the previous snapshot's? That way we can also keep that sorting mechanism to find current + previous snapshot within the df from before
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-22T23:39:01Z
body: some questions/changes i had when i was trying to get this to work with gismo:

1. This is kind of what I was imagining when passing in the longitudinal data, we just have two dataframes to pass in:
* dfResults: the current snapshot's results.
* dfResultsLongitudinal: all previously available results.
This way, I think we can nicely handle the three possible scenarios:
1. First run – no longitudinal data yet (NULL or empty df).
2. Subsequent run – longitudinal data is available and used for comparison.
3. Rerun – longitudinal data includes the current snapshot date, which gets filtered out to avoid duplication.

```
CalculateChange <- function(
    dfResults,
    dfResultsLongitudinal = NULL,
    strIDColumns = c(
      "StudyID",
      "GroupLevel",
      "GroupID",
      "MetricID"
    ),
    strSnapshotDateColumn = "SnapshotDate",
    strMetricColumns = c(
      "Numerator",
      "Denominator",
      "Metric",
      "Score",
      "Flag"
    )
) {
  stop_if(
    cnd = !is.data.frame(dfResults),
    message = "`dfResults` must be a data frame."
  )

  # Extract current snapshot date
  current_date <- max(dfResults[[strSnapshotDateColumn]], na.rm = TRUE)

  # Filter out current snapshot from longitudinal data if present
  if (!is.null(dfResultsLongitudinal) && nrow(dfResultsLongitudinal) > 0) {
    if (current_date %in% dfResultsLongitudinal[[strSnapshotDateColumn]]) {
      cli::cli_alert_info("Current snapshot date ({current_date}) found in longitudinal data — removing to avoid duplication.")
    }

    # Remove current snapshot from longitudinal data
    dfResultsLongitudinal <- dfResultsLongitudinal %>%
      dplyr::filter(.data[[strSnapshotDateColumn]] != current_date)

    # Get latest previous snapshot date
    available_dates <- sort(unique(dfResultsLongitudinal[[strSnapshotDateColumn]]))
    if (length(available_dates) == 0) {
      cli::cli_alert_info("No valid previous snapshots in longitudinal data — returning current results unchanged.")
      return(dfResults)
    }

    prev_date <- tail(available_dates, 1)

    cli::cli_alert_info("Comparing current snapshot {current_date} to previous snapshot {prev_date}.")

    dfPrevious <- dfResultsLongitudinal %>%
      dplyr::filter(.data[[strSnapshotDateColumn]] == prev_date)
  } else {
    cli::cli_alert_info("Longitudinal data missing or empty — returning current results unchanged.")
    return(dfResults)
  }

  # Check that all columns exist in [ dfResults ] and [ dfPrevious ].
  for (strColumn in c(
    strIDColumns,
    strSnapshotDateColumn,
    strMetricColumns
  )) {
    stop_if(
      cnd = !strColumn %in% colnames(dfResults),
      message = glue::glue("`{strColumn} not found in `dfResults`.")
    )
    stop_if(
      cnd = !strColumn %in% colnames(dfPrevious),
      message = glue::glue("`{strColumn} not found in `dfPrevious`.")
    )
  }

  # Combine previous and current
  dfResults_combined <- dplyr::bind_rows(dfPrevious, dfResults) %>%
    dplyr::select(all_of(c(strIDColumns, strSnapshotDateColumn, strMetricColumns)))

  # Calculate change from previous snapshot.
  dfChanges <- dfResults_combined %>%
    tidyr::pivot_longer(
      cols = all_of(strMetricColumns),
      names_to = "Param",
      values_to = "Value"
    ) %>%
    dplyr::mutate(
      Param = factor(.data$Param, levels = strMetricColumns)
    ) %>%
    dplyr::group_by(across(all_of(c(
      strIDColumns,
      'Param'
    )))) %>%
    dplyr::arrange(
      strSnapshotDateColumn,
      .by_group = TRUE
    ) %>%
    dplyr::mutate(
      Change = Value - dplyr::lag(.data$Value),
      PercentChange = .data$Change / dplyr::lag(.data$Value) * 100,
      PrevSnapshotDate = dplyr::lag(.data[[strSnapshotDateColumn]])
    ) %>%
    dplyr::ungroup() %>%
    dplyr::arrange(dplyr::across(all_of(c(
      strIDColumns,
      'PrevSnapshotDate',
      'Param',
      strSnapshotDateColumn
    )))) %>%
    tidyr::pivot_wider(names_from = Param,
                       values_from = c(Value, Change, PercentChange),
                       names_glue = "{Param}_{.value}",)

  return(dfChanges)
}
```


and the wf yaml

```
meta:
  Type: test
  ID: test
  Description: Reporting Results Data
  Priority: 1
spec:
  Mapped_STUDY:
    GroupID:
      type: character
  Reporting_Results_Longitudinal:
    _all:
      required: true
steps:
  # Capture study ID from the GroupID column in the study metadata.
  - output: GroupID
    name: dplyr::pull
    params:
      .data: Mapped_STUDY
      var: GroupID
  - output: strStudyID
    name: unique
    params:
      x: GroupID
  # Stack all analysis results data into a single data frame.
  - output: Results_Summary
    name: gsm.reporting::BindResults
    params:
      lAnalysis: lAnalyzed
      strName: "Analysis_Summary"
      strStudyID: strStudyID
      dSnapshotDate: dSnapshotDate
  # Stack latest snapshot with previous snapshots.
  - output: Reporting_Results_With_Changes
    name: gsm.reporting::CalculateChange
    params:
      dfResults: Reporting_Summary
      dfResultsLongitudinal: Reporting_Results_Longitudinal
 ```
 
2.  The output I got seemed to be matching what was outputting from the tests, but let me know if it looks off: 
<img width="1267" alt="Pasted Graphic" src="https://github.com/user-attachments/assets/c0bbb53c-be36-4589-8a00-c505950e35c4" />

3. Another question that came up was how we are going to insert a wf yaml that requires the longitudinal data? Is it going to live in grail or gismo.template only and be a separate wf that will save over Results.parquet, or will it just be the “new” Results.yaml when we create the study repos?
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-23T13:15:05Z
body: @annezheng2 - I think the edits to the function look good, and cover all the features I added, but make it more flexible for longitudinal data frames to be included. 

Two bits on the wf yaml:
1. at the moment we don't want to require `Reporting_Results_Longitudinal` via a spec because we want to ensure that for users outside of gilead, they can run the `Results.yaml` as usual and just get the standard result back without error, as we've built that into `CalculateChanges()`
2. I believe that we want to return a a single snapshot `dfResults` from the `Results.yaml`, so adding the step to `FilterToLatestSnapshot()` at the end would accomplish this, as seen in the current version of this PR.

@jwildfire and @samussiah- let me know what you think and/or if I missed anything with this review.
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-23T13:48:14Z
body: We need to be explicit with which snapshot date to compare to, typically just the previous but if we're running snapshots at a higher cadence than the risk signal review process we need the snapshot date of the previous review. So we need an explicit step that identifies that snapshot date.

@annezheng2, I don't see an easy way to avoid using modified workflows short of building this new step into the `gismo.runner` scripts.  They're meant to be easily customized for different use cases though, and I could see them living in `gsm.template` or `gismo.runner`.
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-23T13:51:44Z
body: Ah yes, I missed that the `dPrevSnapshotDate` argument had been removed. If we are going to feed longitudinal data in, then we need to retain that feature for sure.
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T13:53:15Z
body: > 2. I believe that we want to return a a single snapshot `dfResults` from the `Results.yaml`, so adding the step to `FilterToLatestSnapshot()` at the end would accomplish this, as seen in the current version of this PR.

Do the columns need to be renamed back for the reports layer?


author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T14:15:29Z
body: > We need to be explicit with which snapshot date to compare to, typically just the previous but if we're running snapshots at a higher cadence than the risk signal review process we need the snapshot date of the previous review. So we need an explicit step that identifies that snapshot date.

Is the expectation to run the monthly (or whatever cadence) snapshots without the risk signals and then run the risk signal generations workflow manually when needed?
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-23T14:23:33Z
body: > Ah yes, I missed that the `dPrevSnapshotDate` argument had been removed. If we are going to feed longitudinal data in, then we need to retain that feature for sure.

I think it's a nice feature although it makes the function/testing more complex.  But dynamic subsetting on the workflow side is kind of a pain so, again, useful and relevant.

Either way, the comparison snapshot date needs to be exposed prior to this step.
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-23T14:25:22Z
body: > > 2. I believe that we want to return a a single snapshot `dfResults` from the `Results.yaml`, so adding the step to `FilterToLatestSnapshot()` at the end would accomplish this, as seen in the current version of this PR.
> 
> Do the columns need to be renamed back for the reports layer?

Which columns?
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T14:29:04Z
body: > > > 2. I believe that we want to return a a single snapshot `dfResults` from the `Results.yaml`, so adding the step to `FilterToLatestSnapshot()` at the end would accomplish this, as seen in the current version of this PR.
> > 
> > 
> > Do the columns need to be renamed back for the reports layer?
> 
> Which columns?

Wouldn't Results.yaml need to output a df with these? https://github.com/Gilead-BioStats/gsm.kri/blob/5d68a91dd442fd38c2d4cda74611abd25e2936eb/inst/workflow/4_modules/report_kri_country.yaml#L8-L20

```
  Reporting_Results:
    _all:
      required: true
    Numerator:
      type: numeric
    Denominator:
      type: numeric
    Metric:
      type: numeric
    Score:
      type: numeric
    Flag:
      type: numeric
```
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-23T14:29:38Z
body: > > We need to be explicit with which snapshot date to compare to, typically just the previous but if we're running snapshots at a higher cadence than the risk signal review process we need the snapshot date of the previous review. So we need an explicit step that identifies that snapshot date.
> 
> Is the expectation to run the monthly (or whatever cadence) snapshots without the risk signals and then run the risk signal generations workflow manually when needed?

My understanding is the KRI reports and ADO workflows will generally adhere to a monthly cadence, but everything up to `Priority=1` reporting tables will potentially run more often, perhaps weekly. So when we need to calculate change it should compare with the previous monthly snapshot.  Depending on the study that would be one of 1st/2nd/3rd/4th Monday of the month.
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-23T14:32:33Z
body: > > > > 2. I believe that we want to return a a single snapshot `dfResults` from the `Results.yaml`, so adding the step to `FilterToLatestSnapshot()` at the end would accomplish this, as seen in the current version of this PR.
> > > 
> > > 
> > > Do the columns need to be renamed back for the reports layer?
> > 
> > 
> > Which columns?
> 
> Wouldn't Results.yaml need to output a df with these? https://github.com/Gilead-BioStats/gsm.kri/blob/5d68a91dd442fd38c2d4cda74611abd25e2936eb/inst/workflow/4_modules/report_kri_country.yaml#L8-L20
> 
> ```
>   Reporting_Results:
>     _all:
>       required: true
>     Numerator:
>       type: numeric
>     Denominator:
>       type: numeric
>     Metric:
>       type: numeric
>     Score:
>       type: numeric
>     Flag:
>       type: numeric
> ```

It should include all preexisting `Results` columns plus the change columns, so `SnapshotDatePrevious`, `NumeratorPrevious`, `NumeratorChange`, `NumeratorPercentChange`, etc.
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T14:34:30Z
body: > > > We need to be explicit with which snapshot date to compare to, typically just the previous but if we're running snapshots at a higher cadence than the risk signal review process we need the snapshot date of the previous review. So we need an explicit step that identifies that snapshot date.
> > 
> > 
> > Is the expectation to run the monthly (or whatever cadence) snapshots without the risk signals and then run the risk signal generations workflow manually when needed?
> 
> My understanding is the KRI reports and ADO workflows will generally adhere to a monthly cadence, but everything up to `Priority=1` reporting tables will potentially run more often, perhaps weekly. So when we need to calculate change it should compare with the previous monthly snapshot. Depending on the study that would be one of 1st/2nd/3rd/4th Monday of the month.

if this is the case, would it be sufficient to just only give back longitudinal data that collects from snapshots that have kri reports? Or did you still want the ability to input an arg that specifies the date to pick from the longitudinal data? 
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T14:40:51Z
body: > > > > > 2. I believe that we want to return a a single snapshot `dfResults` from the `Results.yaml`, so adding the step to `FilterToLatestSnapshot()` at the end would accomplish this, as seen in the current version of this PR.
> > > > 
> > > > 
> > > > Do the columns need to be renamed back for the reports layer?
> > > 
> > > 
> > > Which columns?
> > 
> > 
> > Wouldn't Results.yaml need to output a df with these? https://github.com/Gilead-BioStats/gsm.kri/blob/5d68a91dd442fd38c2d4cda74611abd25e2936eb/inst/workflow/4_modules/report_kri_country.yaml#L8-L20
> > ```
> >   Reporting_Results:
> >     _all:
> >       required: true
> >     Numerator:
> >       type: numeric
> >     Denominator:
> >       type: numeric
> >     Metric:
> >       type: numeric
> >     Score:
> >       type: numeric
> >     Flag:
> >       type: numeric
> > ```
> 
> It should include all preexisting `Results` columns plus the change columns, so `SnapshotDatePrevious`, `NumeratorPrevious`, `NumeratorChange`, `NumeratorPercentChange`, etc.

oh, i think this must have got changed somewhere earlier along the way because the outputs i'm getting from the change portion have these columns 
```
> colnames(calc1)
 [1] "StudyID"                   "GroupLevel"                "GroupID"                  
 [4] "MetricID"                  "SnapshotDate"              "PrevSnapshotDate"         
 [7] "Numerator_Value"           "Denominator_Value"         "Metric_Value"             
[10] "Score_Value"               "Flag_Value"                "Numerator_Change"         
[13] "Denominator_Change"        "Metric_Change"             "Score_Change"             
[16] "Flag_Change"               "Numerator_PercentChange"   "Denominator_PercentChange"
[19] "Metric_PercentChange"      "Score_PercentChange"       "Flag_PercentChange"    
```
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-23T14:43:43Z
body: > oh, i think this must have got changed somewhere earlier along the way because the outputs i'm getting from the change portion have these columns
> 
> ```
> > colnames(calc1)
>  [1] "StudyID"                   "GroupLevel"                "GroupID"                  
>  [4] "MetricID"                  "SnapshotDate"              "PrevSnapshotDate"         
>  [7] "Numerator_Value"           "Denominator_Value"         "Metric_Value"             
> [10] "Score_Value"               "Flag_Value"                "Numerator_Change"         
> [13] "Denominator_Change"        "Metric_Change"             "Score_Change"             
> [16] "Flag_Change"               "Numerator_PercentChange"   "Denominator_PercentChange"
> [19] "Metric_PercentChange"      "Score_PercentChange"       "Flag_PercentChange"    
> ```

Yeah, looks like with all the iterating on this, we missed this. the `*_Value` columns just need to be edited/renamed to remove that suffix and it will be back to `dfResults` standards
author_association: CONTRIBUTOR
user: login: samussiah
created_at: 2025-05-23T14:53:12Z
body: > > > > We need to be explicit with which snapshot date to compare to, typically just the previous but if we're running snapshots at a higher cadence than the risk signal review process we need the snapshot date of the previous review. So we need an explicit step that identifies that snapshot date.
> > > 
> > > 
> > > Is the expectation to run the monthly (or whatever cadence) snapshots without the risk signals and then run the risk signal generations workflow manually when needed?
> > 
> > 
> > My understanding is the KRI reports and ADO workflows will generally adhere to a monthly cadence, but everything up to `Priority=1` reporting tables will potentially run more often, perhaps weekly. So when we need to calculate change it should compare with the previous monthly snapshot. Depending on the study that would be one of 1st/2nd/3rd/4th Monday of the month.
> 
> if this is the case, would it be sufficient to just only give back longitudinal data that collects from snapshots that have kri reports? Or did you still want the ability to input an arg that specifies the date to pick from the longitudinal data?

That should cover our needs, a longitudinal dataset without all those intermediate snapshots.  The date parameter is nice though because we can look back however many snapshots, like to when a risk signal was first actioned.
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-23T15:40:42Z
body: @samussiah and @annezheng2 i _think_ i've implemented the changes we've decided on for `CalculateChange()` in these two commits:
https://github.com/Gilead-BioStats/gsm.reporting/pull/32/commits/88c18a317b455b1f74514e43f671abb4e1f34fa5 
https://github.com/Gilead-BioStats/gsm.reporting/pull/32/commits/11e34766d3dec88c314a4ff8564d5980106c2f97
let me know if there is anything i missed, or add comments to the code as you see fit.
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-23T15:43:13Z
body: > @samussiah and @annezheng2 i _think_ i've implemented the changes we've decided on for `CalculateChange()` in these two commits: [88c18a3](https://github.com/Gilead-BioStats/gsm.reporting/commit/88c18a317b455b1f74514e43f671abb4e1f34fa5) [11e3476](https://github.com/Gilead-BioStats/gsm.reporting/commit/11e34766d3dec88c314a4ff8564d5980106c2f97) let me know if there is anything i missed, or add comments to the code as you see fit.

missed a reference to `current_date`, but should be working now
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T17:12:40Z
body: https://github.com/Gilead-BioStats/gsm.reporting/blob/4f9f9197292be8aaf9f5650ed994b82a81773d7d/R/CalculateChange.R#L67 

can you add a check to not allow empty dataframes here? If the data is specified in the specs, it still has to apply the checks to proceed 

```
── Checking data against spec 
Error in `layout()`:
! `lData` must contain all data.frames in `lSpec`. Missing data.frames: Reporting_Results_Longitudinal
```

so I can make gismo.runner pass in an empty df with the specs to bypass this check, but then we need it to return dfResults at this point 
author_association: CONTRIBUTOR
user: login: lauramaxwell
created_at: 2025-05-23T17:22:09Z
body: > https://github.com/Gilead-BioStats/gsm.reporting/blob/4f9f9197292be8aaf9f5650ed994b82a81773d7d/R/CalculateChange.R#L67
> 
> can you add a check to not allow empty dataframes here? If the data is specified in the specs, it still has to apply the checks to proceed
> 
> ```
> ── Checking data against spec 
> Error in `layout()`:
> ! `lData` must contain all data.frames in `lSpec`. Missing data.frames: Reporting_Results_Longitudinal
> ```
> 
> so I can make gismo.runner pass in an empty df with the specs to bypass this check, but then we need it to return dfResults at this point

Sure, I can make that check, but I don't think that we are _requiring_ Reporting_Results_Longitudinal for this workflow, so it won't be in the spec.
author_association: CONTRIBUTOR
user: login: annezheng2
created_at: 2025-05-23T17:24:36Z
body: > > https://github.com/Gilead-BioStats/gsm.reporting/blob/4f9f9197292be8aaf9f5650ed994b82a81773d7d/R/CalculateChange.R#L67
> > 
> > can you add a check to not allow empty dataframes here? If the data is specified in the specs, it still has to apply the checks to proceed
> > ```
> > ── Checking data against spec 
> > Error in `layout()`:
> > ! `lData` must contain all data.frames in `lSpec`. Missing data.frames: Reporting_Results_Longitudinal
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > so I can make gismo.runner pass in an empty df with the specs to bypass this check, but then we need it to return dfResults at this point
> 
> Sure, I can make that check, but I don't think that we are _requiring_ Reporting_Results_Longitudinal for this workflow, so it won't be in the spec.

right, it won't be for this workflow, but for whatever workflow implementation we want in gismo.runner/template that will pass in Reporting_Results_Longitudinal. thanks!
author_association: CONTRIBUTOR