Continuous daily msPAF time series from interpolated grab chemistry
Source:R/mspaf_daily.R
mspaf_daily.RdInterpolates per-analyte grab chemistry onto a daily date grid and computes
msPAF for every day within the requested date range. The result is a tidy
tibble with one row per (site \(\times\) day), suitable for trend
analysis, visualisation, and downstream time_weighted_aggregate() calls.
Usage
mspaf_daily(
df,
temperature = NULL,
reference = NULL,
reference_model = NULL,
imputation_model = NULL,
start = NULL,
end = NULL,
by = "day",
interpolation = c("forward_fill", "linear", "model"),
leading_edge = c("drop", "backfill"),
analyte_metadata = NULL,
method = c("multi", "anzecc"),
guideline_dir = getOption("hydroSense.guideline_dir"),
min_analytes = 3L,
conc_units = NULL,
require_temperature = TRUE,
ndraws = NULL,
seed = NULL,
return = c("summary", "draws"),
interval = 0.9,
central = c("median", "mean"),
grab_cv = NULL,
ou_scale = 1,
kappa = 0.5,
parallel = FALSE,
couple_residuals = TRUE,
gap_uncertainty = c("bracket", "ignorable", "informative"),
transform = c("pseudo_log", "additive")
)Arguments
- df
Long-format grab chemistry data frame. Required columns:
sample_id,site_id,datetime(Date or POSIXct),analyte,value,detected. Optional but propagated:units.analyte,imputed. Chemistry for multiple sites may be stacked; interpolation and msPAF are computed per site.- temperature
Optional daily water temperature data frame for days without a grab temperature measurement. Required columns:
datetime(Date or POSIXct) andvalue(temperature in \({}^\circ\)C). The output ofestimate_water_temp()is accepted directly (extra columns are ignored). When both this argument and grab-sample temperature rows indfare present for the same day, the grab measurement takes priority.NULL(default) means temperature must come fromdfrows alone.- reference
Background reference chemistry for ARA adjustment. Accepts the same four forms as
add_mspaf(). Controls only whether background is subtracted; it is independent ofinterpolation. Withinterpolation = "model", pass the samereference_modelhere to assess the leachate-attributable increment, orNULLto assess total concentration.- reference_model
A
reference_modelfromfit_reference_model(). Required wheninterpolation = "model"— it supplies the background and catchment hydrology used by the season-blind target impact model (fit_target_model()) that interpolates toxicants between grabs. Ignored for the"forward_fill"and"linear"paths.- imputation_model
Optional
imputation_modelfromfit_imputation_model(), passed tofit_target_model()for tier-2 enrichment underinterpolation = "model". Requires brms.- start, end
Date boundaries for the daily grid. Default: earliest and latest
datetimevalues indf. Coerced toDate.- by
Temporal resolution string passed to
seq.Date(). Default"day".- interpolation
How to fill gaps between grab samples. One of
"forward_fill"(default) or"linear". See the Interpolation section.- leading_edge
What to do with days before the first grab sample for an analyte.
"drop"(default) or"backfill". See the Leading edge section.- analyte_metadata
Data frame of analyte metadata, or
NULLto use the bundled metadata. Passed toadd_mspaf().- method
SSD method. One of
"multi"(default, model-averaged) or"anzecc". Passed toadd_mspaf().- guideline_dir
Path to the ANZG guideline data folder. Falls back to
getOption("hydroSense.guideline_dir").- min_analytes
Minimum number of SSD-eligible analytes per day for msPAF to be computed. Default
3L.- conc_units
Character. Concentration units for SSD-eligible rows when
dflacks aunits.analytecolumn. Passed toadd_mspaf().- require_temperature
Logical (default
TRUE). WhenTRUE, any daily sample withNH3-Nmust also carry atemperaturevalue. Passed toadd_mspaf(). SetFALSEfor datasets without ammonia.- ndraws
Positive integer or
NULL(default).NULLreturns the deterministic daily msPAF: the fast, grabs-exact point estimate (the default and recommended best guess). When non-NULL, runs the full OU/GAM uncertainty propagation forndrawsdraws and returns a draws product instead (see Details for the distinction). Requiresinterpolation = "model".- seed
Integer or
NULL. RNG seed for reproducibility of draws.- return
"summary"(default) or"draws"; relevant only whenndrawsis supplied."summary"collapses the draws to per-envelope central estimates (median_*, the draws' own central tendency — seecentral) plus credible-interval bounds (lo_*,hi_*) for the chosengap_uncertainty, alongside thedeterministiccentre line."draws"returns one row per (site \(\times\) day \(\times\) draw) with adraw_idcolumn and the per-draw envelope value(s). See Value. Ignored in point mode (ndraws = NULL).- interval
Credible interval width for
return = "summary"(default0.9). The lower bound is the(1 - interval)/2quantile, the upper is the1 - (1 - interval)/2quantile.- central
Central tendency for the
median_*envelope columns whenreturn = "summary":"median"(default) or"mean"of the per-day draws. Because it is a summary of the draws, eachmedian_*is coherent with its band (it lies inside[lo_*, hi_*]by construction) and depends onndraws/seed. It is NOT the deterministic point estimate, which is reported separately in thedeterministiccolumn (and is the sole estimate when called withndraws = NULL).- grab_cv
Numeric scalar or named numeric vector of coefficients of variation for grab-sample measurement error. A scalar applies the same CV to all analytes; a named vector (e.g.
c(Cu = 0.1, pH = 0.02)) applies analyte-specific CVs. Controls two uncertainty sources: S6 (anchor S-value spread at measured dates) and S7 (co-analyte normalisation spread).NULL(default) disables S6 and S7.- ou_scale
Scale factor for the OU bridge envelope (default
1). Multiplies \(\sigma^2\) and \(\gamma\) (marginal variance) without changing \(\theta\) (correlation length); values > 1 widen the between-grab uncertainty bands. Either a single number applied to every analyte, or a numeric vector named by impact tier to scale the tiers independently, e.g.c(model = 1, bridge = 2.5)(a tier not named falls back to1). Measure each tier's calibration withloo_anchor_coverage()and raise the scale of an under-covered tier — the sparse-anchor bridge tier is typically the under-covered one, and a per-tier scale widens it without inflating the well-calibrated model tier. Seevignette("daily-uncertainty").- kappa
Non-negative numeric (default
0.5). Hydrological modulation exponent: the smoother's process variance is multiplied by \(\exp(\kappa \cdot z_h)\) where \(z_h\) is the standardised daily flow (capped at ±4 SD).kappa = 0disables hydrological modulation; larger values widen the credible band more aggressively across high-flow gaps. Requires a hydrology column indf.- parallel
Logical (default
FALSE). WhenTRUE, the per-site loop (interpolation + impact-model fit + residual-draw generation for each site) is parallelised viafuture.apply::future_lapply(), which honours whateverfuture::plan()the caller has established. This is the embarrassingly-parallel level; the cross-site toxicity combine downstream is a single vectorised pass and runs once. Only speeds up multi-sitedf(one site = one task). Requires the future.apply package and a parallel plan, e.g.future::plan(future::multisession, workers = 4); note workers need the installed package (usefuture::multicoreto parallelise adevtools::load_all()session). In parallel modefuture.applymanages a reproducible per-site L'Ecuyer-CMRG stream fromseed, so draws differ from the sequential stream (point mode is deterministic and identical either way), but are themselves reproducible.- couple_residuals
Logical (default
TRUE). WhenTRUEand >= 2 analytes have fitted residual smoothers, daily residual draws are correlated across analytes using the empirical anchor-residual correlation (see.anchor_residual_cor()). This widens the combined msPAF interval to reflect co-movement of co-toxicants on breach events while leaving per-analyte marginals unchanged. Set toFALSEto reproduce the pre-#32 independent-draw path exactly.- gap_uncertainty
One of
"bracket"(default),"ignorable", or"informative"; relevant only in draws mode. In observation gaps the latent residual reverts to its marginal variance, widening the band — the honest posterior only under ignorable (MAR) missingness. Field sampling is often informative (MNAR): gaps exist because the system was judged quiescent, so that band over-states gap uncertainty. The model cannot identify the mechanism from grabs, so the default brackets both: the ignorable (upper) envelope keeps the simulation-smoother draw; the informative (lower) envelope freezes the residual at its posterior mean on in-gap days (the fully-informative extreme — gaps perfectly predictable). The two are nested and coincide at observation days."ignorable"/"informative"return only that envelope. See Interpreting gap uncertainty below. Reference: Rubin (1976) doi:10.1093/biomet/63.3.581 .- transform
"pseudo_log"(default) or"additive". Controls the variance-stabilising transform for the daily impact residual smoother."pseudo_log"appliesg = asinh(I / c)with per-analyte scalec = HC5(issue #15);"additive"keepsg = I(pre-#15 behaviour, no HC5 scaling). Forwarded tofit_target_model(); ignored unlessinterpolation = "model".
Value
Point mode (ndraws = NULL): a tibble with one row per (site
\(\times\) day) for days with sufficient analyte coverage; mspaf is
the deterministic daily estimate.
Draws mode (ndraws > 0, return = "summary"): one row per (site
\(\times\) day) with the deterministic centre line and the envelope
columns for the chosen gap_uncertainty: median_*, lo_*, hi_* per
envelope (informative and/or ignorable, the central tendency per
central and the interval credible bounds), plus
precautionary_lo/precautionary_hi in "bracket" mode.
Draws mode (ndraws > 0, return = "draws"): one row per (site
\(\times\) day \(\times\) draw), with a draw_id integer column and the
per-draw msPAF value(s) mspaf_ignorable and/or mspaf_informative for
the chosen gap_uncertainty.
Common columns:
dateDate of this daily estimate.
site_idSite identifier.
n_analytes_usedSSD-eligible analytes contributing to msPAF.
dominant_analyteAnalyte with the highest individual PAF.
max_pafPAF of the dominant analyte (proportion 0–1).
n_measured_analytesSSD-eligible analytes with a direct grab sample on this day (not interpolated).
days_since_last_sampleDays since the most recent grab sample for any SSD-eligible analyte.
"analyte_pafs" (per-analyte PAF breakdown, re-keyed by date) and
"ara_summary" attributes are attached; retrieve them with analyte_pafs()
and ara_summary(). (analyte_pafs is now a flat attribute, not a
list-column — issue #30.)
Details
mspaf_daily() returns one of two distinct products, chosen by
ndraws; they answer different questions and should not be expected to
coincide:
Deterministic (
ndraws = NULL, default). A single grabs-exact daily line: the residual smoother is pinned to the measured grab samples, so the curve threads the reported lab values. Pro: fast, reproducible, the best single-number estimate; ideal as a sanity check. Con: carries no uncertainty and over-fits noisy grabs.Draws (
ndraws > 0). A Monte-Carlo posterior propagating trend (GAM) and between-grab (OU bridge) uncertainty, plus grab measurement error whengrab_cvis set.return = "summary"reports the central tendency plus a credible band;return = "draws"returns per-draw paths. Pro: honest uncertainty quantification. Con: the central estimate is seed/ndraws-dependent and, by Jensen's inequality on the bounded msPAF index, generally differs from the deterministic line.
The summary centre is a summary of the draws (issue #42), so it always lies within its own credible band; it is not the deterministic line overlaid on a band built from a different posterior.
Interpolation
Each analyte is interpolated independently:
"forward_fill"carries the last directly observed value forward until the next grab. Produces a step function."linear"linearly interpolates between consecutive observations. For SSD-eligible toxicants (metals, ammonia), interpolation is performed in log-concentration space, which is more appropriate for log-normally distributed data and avoids negative intermediate values. Co-analytes (pH, temperature, DOC, hardness) are interpolated linearly."model"fits a season-blindfit_target_model()on the grab chemistry and the suppliedreference_model, and predicts each toxicant's concentration between grabs asreference + impact, where the impact (the leachate-attributable increment) is modelled from hydrology and a persistent latent state but never from day-of-year. Co-analytes are forward-filled. Requiresreference_model; seefit_target_model()for the method.
Below-detection values are treated as their detection-limit value for
interpolation purposes, matching the treatment in add_mspaf().
Leading edge
Days before the first grab sample for an analyte are outside the observation record:
"drop"(default) excludes such days from the output."backfill"assigns the first observed value to all earlier days. Use cautiously – it assumes the analyte was at its first observed level before sampling commenced.
Temperature for ammonia
When NH3-N is in df, water temperature is required per sample for the
un-ionised fraction normalisation. Two sources are accepted:
Temperature rows already in
df(measured on grab-sample days) are interpolated along with other analytes.An external daily temperature series supplied via the
temperatureargument (e.g. fromestimate_water_temp()) fills in days without a grab temperature. Grab-sample-day measurements take priority.
Set require_temperature = FALSE only for datasets that do not contain
ammonia.
Interpreting gap uncertainty
In draws mode the width of the band across an observation gap is the honest
posterior only under ignorable (MAR) missingness. Where you have
external grounds that a gap was quiescent (informative/MNAR sampling), the
ignorable band over-states uncertainty there. The "bracket" output gives
both extremes: read the informative (lower) envelope where you vouch the
gap was quiet, the ignorable (upper) envelope otherwise. The
precautionary_lo/precautionary_hi columns are the composite
[lo_informative, hi_ignorable] — a decision bound, not a calibrated
credible interval (its coverage exceeds nominal and is undefined). Applied
blanket, the informative envelope under-covers genuinely eventful gaps, so use
it per-gap; automatic per-gap conditioning on a continuous proxy is future
work (#18). The deterministic line is not a safe blanket alternative —
it under-states risk by Jensen's inequality (#39/#42).
Examples
# \donttest{
demo <- leachate_demo()
ds <- subset(demo, site_id == "downstream")
out <- mspaf_daily(ds, require_temperature = FALSE)
head(out[, c(
"date", "site_id", "mspaf", "n_measured_analytes",
"days_since_last_sample"
)])
#> # A tibble: 6 × 5
#> date site_id mspaf n_measured_analytes days_since_last_sample
#> <date> <chr> <dbl> <int> <int>
#> 1 2024-01-15 downstream 93.3 3 0
#> 2 2024-01-16 downstream 93.3 0 1
#> 3 2024-01-17 downstream 93.3 0 2
#> 4 2024-01-18 downstream 93.3 0 3
#> 5 2024-01-19 downstream 93.3 0 4
#> 6 2024-01-20 downstream 93.3 0 5
# }