Skip to content

Commit d6086f7

Browse files
chross22claude
andcommitted
Let covariate_columns() report the source columns that travel with the data
Both resamplers were written expecting a <var>_source column to ride along. resolve_methods() says so outright - "a source column riding along should not force the caller to enumerate every column" - and each carries a non-numeric column as a categorical: the commonest value when aggregating, the nearest or a step when interpolating, which is what a source tag needs and an average is not. But covariate_columns() is the default `vars` for both, and it excluded them. So a source column never rode along. Regridding or retiming a gap-filled object dropped the record of which source each value came from, at exactly the point that record matters most - the values have just been combined. The reasoning behind the old exclusion still holds for the other two cases, and they stay out. <var>_depth is a level index, and the mean of two depths is not the depth any value came from. .datamatch_source is internal bookkeeping that matchData() consumes. is_provenance_column() becomes is_bookkeeping_column(), which is what it now describes. Nothing downstream had to change. plot_series() already filtered to numeric columns, and both resamplers already handled categoricals. taupatch's test-prejoin.R has been failing on this since a newer datamatch was installed there - it expected covariate_columns() to return CHL_source, and it was right. It passes now with no change to taupatch. datamatch: 1,440 tests pass, check is OK. taupatch: 1,279 pass, none failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c750caa commit d6086f7

10 files changed

Lines changed: 182 additions & 40 deletions

‎NEWS.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# datamatch 0.2.0
22

3+
## Breaking changes
4+
5+
* **`covariate_columns()` now reports `<var>_source` columns.** They are
6+
provenance, but they travel with the variable they describe rather than being
7+
left behind by it, and both resamplers were already written on that
8+
assumption: `upscale_grid()` and `upscale_time()` carry a non-numeric column
9+
as a categorical - the commonest value when aggregating, the nearest or a step
10+
when interpolating - and `resolve_methods()` says in as many words that "a
11+
source column riding along should not force the caller to enumerate every
12+
column".
13+
14+
Because `covariate_columns()` is the default `vars` for both, a source column
15+
never actually rode along. Regridding or retiming a gap-filled object dropped
16+
the record of which source each value came from, at the point it matters most.
17+
18+
`<var>_depth` is still excluded, since the mean of two depths is not the depth
19+
any value came from, as is the internal per-row source tag.
20+
21+
Callers relying on `covariate_columns()` to return only measurements should
22+
filter on `is.numeric()`, as `plot_series()` does.
23+
324
## New features
425

526
* **A date that has not happened yet is refused before anything is fetched.**

‎R/gapfill.R‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
#' Covariate column names in an environmental data object
22
#'
3-
#' Everything that is not a time column or the geometry.
3+
#' Everything that is not a time column, the geometry, or bookkeeping.
4+
#'
5+
#' `<var>_source` columns are included. They are provenance, but they travel
6+
#' with the variable they describe: [upscale_grid()] and [upscale_time()] carry
7+
#' a non-numeric column as a categorical, so a resampled object keeps the record
8+
#' of which source each value came from. `<var>_depth` is excluded, because the
9+
#' mean of two depths is not the depth any value came from.
410
#'
511
#' @param env_dat an `sf` POINT object from any access function -
612
#' [accessCopernicus()], [accessFVCOM()], [accessHYCOM()], [accessCCMP()] or
@@ -14,9 +20,7 @@
1420
covariate_columns <- function(env_dat) {
1521
candidates <- setdiff(names(env_dat),
1622
c(time_columns(), attr(env_dat, "sf_column")))
17-
# Provenance columns describe the data rather than measuring anything, so they
18-
# must not be aggregated, regridded or plotted as though they were covariates.
19-
candidates[!is_provenance_column(candidates)]
23+
candidates[!is_bookkeeping_column(candidates)]
2024
}
2125

2226
#' Fill satellite gaps with the model equivalent

‎R/matchData.R‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@
6666
#' `<var>_source` column [fill_satellite_gaps()] writes. Pass
6767
#' `record_source = FALSE` to omit them.
6868
#'
69-
#' These are provenance rather than data: [covariate_columns()] excludes them, so
70-
#' they are not aggregated, regridded or plotted as though they were measurements.
69+
#' These are provenance rather than measurements, but they travel with the
70+
#' variable they describe: [covariate_columns()] reports them, and resampling
71+
#' carries them as a categorical - the commonest value when aggregating, the
72+
#' nearest when interpolating - so a regridded or retimed object still says
73+
#' which source each value came from. [plot_series()] skips them, since a source
74+
#' tag has no mean.
7175
#'
7276
#' @param dat <sf object> the points to add columns to: observations, stations,
7377
#' tag positions, anything with coordinates and time. Needs year and month

‎R/provenance.R‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,27 @@ stamp_source <- function(x, source, detail) {
7878
x
7979
}
8080

81-
#' Columns that record where a value came from rather than what it is
81+
#' Columns that are bookkeeping rather than data
8282
#'
83-
#' `<var>_source` columns written by [matchData()] and [fill_satellite_gaps()],
84-
#' and the `<var>_depth` column a derived bottom variable returns. They describe
85-
#' the data rather than measuring anything, so aggregating or interpolating them
86-
#' is meaningless — a mean of two source tags is not a source, and the mean of
87-
#' two depths is not the depth any value came from.
83+
#' `<var>_depth` records which model level a derived bottom value was taken
84+
#' from, and `.datamatch_source` is the per-row source tag a fetch spanning
85+
#' several archives carries. Neither measures anything: the mean of two depths
86+
#' is not the depth any value came from, and the internal tag is consumed by
87+
#' [matchData()] rather than kept. So [covariate_columns()] leaves both out, and
88+
#' nothing resamples or plots them as though they were covariates.
89+
#'
90+
#' @section Why `<var>_source` is not here:
91+
#' It is provenance too, but it travels with the variable it describes rather
92+
#' than being left behind by it. [upscale_grid()] and [upscale_time()] carry a
93+
#' non-numeric column as a categorical - the commonest value when aggregating,
94+
#' the nearest when interpolating - which is exactly what a source tag needs,
95+
#' and both were written expecting one to ride along. Excluding it here meant it
96+
#' never did, so the record of which source a value came from was lost at the
97+
#' first resample, which is the point at which it matters most.
8898
#'
8999
#' @param names <char> column names to inspect
90100
#' @return <logical> one per name
91101
#' @keywords internal
92-
is_provenance_column <- function(names) {
93-
grepl("_source$", names) | grepl("_depth$", names) |
94-
names == ".datamatch_source"
102+
is_bookkeeping_column <- function(names) {
103+
grepl("_depth$", names) | names == ".datamatch_source"
95104
}

‎man/covariate_columns.Rd‎

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎man/is_bookkeeping_column.Rd‎

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎man/is_provenance_column.Rd‎

Lines changed: 0 additions & 22 deletions
This file was deleted.

‎man/matchData.Rd‎

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎man/stop_if_future.Rd‎

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/testthat/test-gapfill.R‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,51 @@ test_that("missing columns are reported with what is available", {
134134
expect_error(fill_satellite_gaps(sources$satellite, sources$model,
135135
c(CHL = "MISSING")), "Model has")
136136
})
137+
138+
# `<var>_source` is provenance, but it travels with the variable it describes
139+
# rather than being left behind by it. Both resamplers were written expecting a
140+
# source column to ride along - they carry a non-numeric column as a categorical
141+
# - but covariate_columns() used to strip it, so it never arrived and the record
142+
# of which source a value came from was lost at the first resample.
143+
144+
test_that("covariate_columns reports <var>_source but not bookkeeping", {
145+
d <- sf::st_as_sf(
146+
data.frame(x = c(1, 2), y = c(1, 2), SST = c(4, 5),
147+
SST_source = c("satellite", "model"), BOTS_depth = c(80, 90),
148+
YEAR = 2010L, MONTH = 6L, DAY = 1L),
149+
coords = c("x", "y"), crs = 4326)
150+
151+
cols <- covariate_columns(d)
152+
expect_true("SST_source" %in% cols)
153+
expect_true("SST" %in% cols)
154+
# The mean of two depths is not the depth any value came from.
155+
expect_false("BOTS_depth" %in% cols)
156+
# Time and geometry are never covariates.
157+
expect_false(any(c("YEAR", "MONTH", "DAY", "geometry") %in% cols))
158+
})
159+
160+
test_that("the internal per-row source tag stays out of covariate_columns", {
161+
d <- sf::st_as_sf(
162+
data.frame(x = c(1, 2), y = c(1, 2), SST = c(4, 5),
163+
YEAR = 2010L, MONTH = 6L, DAY = 1L),
164+
coords = c("x", "y"), crs = 4326)
165+
d <- stamp_source(d, "hycom", c("GLBv53X", "GLBy930"))
166+
167+
expect_true(".datamatch_source" %in% names(d))
168+
expect_false(".datamatch_source" %in% covariate_columns(d))
169+
})
170+
171+
test_that("a source column survives upscaling, as the commonest value", {
172+
sources <- gappy_sources(gap_fraction = 0.3)
173+
filled <- fill_satellite_gaps(sources$satellite, sources$model,
174+
c(CHL = "CHL_MODEL"))
175+
176+
# No `vars`: the default is covariate_columns(), which is the whole point -
177+
# a caller should not have to enumerate columns to keep provenance.
178+
coarser <- upscale_grid(filled, to = 0.5, min_coverage = 0)
179+
180+
expect_true("CHL_source" %in% names(coarser))
181+
expect_true(all(coarser$CHL_source %in% c("satellite", "model")))
182+
# Carried as a category, not averaged into something that is neither.
183+
expect_false(any(is.na(coarser$CHL_source) & !is.na(coarser$CHL)))
184+
})

0 commit comments

Comments
 (0)