Skip to content

Commit ea1a931

Browse files
committed
zoi_from_curve: implemented zoi from curve for bag and data.frame, with examples
1 parent d0df7f6 commit ea1a931

4 files changed

Lines changed: 987 additions & 173 deletions

File tree

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ S3method(threshold_decay,numeric)
1414
S3method(weirdness,bag)
1515
S3method(weirdness,data.frame)
1616
S3method(weirdness,numeric)
17+
S3method(zoi_from_curve,bag)
18+
S3method(zoi_from_curve,data.frame)
1719
export(AUC)
1820
export(Cindex)
1921
export(add_zoi_formula)

R/zoi_from_curve.R

Lines changed: 247 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,73 @@
1-
#' Get estimates of ZOI from response curves
1+
#' Get estimates of zone of influence (ZOI) from response curves
2+
#'
3+
#' This generic function computes ZOI metrics—**maximum effect size**,
4+
#' **ZOI radius**, and **impact**—
5+
#' for ZOI predictor variables based on response curves.
6+
#' The ZOI radius is estimated as the
7+
#' It supports two types of input: a data.frame of predictions
8+
#' or a bag of models.
9+
#'
10+
#' @param x Either a `data.frame` containing response curve predictions for a single variable,
11+
#' or a `bag` object containing an ensemble of models.
12+
#' @param ... Additional arguments passed to the appropriate method.
13+
#'
14+
#' @return A `data.frame` or a `list` containing ZOI metrics:
15+
#' - `max_effect_size`: Maximum effect size on the relative selection strength (y) axis.
16+
#' - `zoi_radius`: Distance at which the effect drops below a threshold,
17+
#' defined by the parameter `percentage`.
18+
#' - `effect_zoi_radius`: Relative selection strength (y axis) valye in which the ZOI
19+
#' is reached.
20+
#' - `impact`: Area under the curve up to the ZOI radius,
21+
#' combining the varying effect size with distance.
22+
#' Each ZOI measure presents mean, median, CI lower, and CI upper.
223
#'
324
#' @example examples/zoi_from_curve_example.R
25+
#' @seealso [oneimpact::predict], [oneimpact::plot_response], [oneimpact::weirdness]
426
#'
27+
#' @name zoi_from_curve
528
#' @export
6-
zoi_from_curve <- function(x,
7-
percentage = 0.95,
8-
curve = c("median", "mean"),
9-
ci = TRUE,
10-
type = c("linear", "exp")[1],
11-
# n_features = 1,
12-
mean_col_name = "mean",
13-
median_col_name = "quantile:0.5",
14-
ci_col_name = c("quantile:0.025", "quantile:0.975")) {
29+
zoi_from_curve <- function(x, ...) {
1530
UseMethod("zoi_from_curve")
1631
}
1732

33+
#' @param percentage `[numeric(1)=0.95]` \cr Numeric between 0 and 1. Defines the
34+
#' threshold for ZOI radius as a proportion of the maximum effect size.
35+
#' Default is `0.95`.
36+
#' @param curve `[character(1)=c("mean", "median")]` \cr Character vector.
37+
#' Which central tendency curves to use: `"median"`, `"mean"`, or both.
38+
#' @param ci `[logical(1)=TRUE]` \cr Logical. Whether to compute ZOI estimates for
39+
#' the upper and lower limits of the confidence interval. Default is TRUE.
40+
#' @param type `[character(1)="linear"]{"linear", "exp"}` \cr Character. Defines whether
41+
#' the calculation of ZOI should be based on the prediction of at linear
42+
#' or response (exponential) scale: `"linear"` or `"exp"`, respectively.
43+
#' @param mean_col_name `[character="mean"]` \cr Name of the column containing
44+
#' the mean response curve.
45+
#' @param median_col_name `[character="quantile:0.5"]` \cr Name of the column
46+
#' containing the median response curve.
47+
#' @param ci_col_name `[character=c("quantile:0.255", "quantile:0.975")]` \cr Character
48+
#' vector of length 2. Names of columns for lower and upper confidence intervals.
49+
#'
50+
#' @rdname zoi_from_curve
1851
#' @export
19-
zoi_from_curve <- function(x,
20-
percentage = 0.95,
21-
curve = c("median", "mean"),
22-
ci = TRUE,
23-
type = c("linear", "exp")[1],
24-
# n_features = 1,
25-
mean_col_name = "mean",
26-
median_col_name = "quantile:0.5",
27-
ci_col_name = c("quantile:0.025", "quantile:0.975")) {
52+
zoi_from_curve.data.frame <- function(x,
53+
percentage = 0.95,
54+
curve = c("median", "mean"),
55+
ci = TRUE,
56+
type = c("linear", "exp")[1],
57+
# n_features = 1,
58+
mean_col_name = "mean",
59+
median_col_name = "quantile:0.5",
60+
ci_col_name = c("quantile:0.025", "quantile:0.975")) {
2861

2962
# get predictor / ZOI variable
3063
xvar <- colnames(x)[1]
3164

65+
if(type == "linear") {
66+
ref <- 0
67+
} else {
68+
ref <- 1
69+
}
70+
3271
# initialize output
3372
# mean, median, 0.025, 0.975
3473
max_effect_size <- rep(NA, 4)
@@ -49,75 +88,239 @@ zoi_from_curve <- function(x,
4988
main_response <- x[[mean_col_name]]
5089
}
5190

52-
# max effect size
53-
max_effect_size_main <- main_response[which.max(abs(main_response))]
91+
# # max effect size
92+
# if(type == "linear") {
93+
# max_effect_size_main <- main_response[which.max(abs(main_response))]
94+
# } else {
95+
max_effect_size_main <- main_response[which.max(abs(main_response - ref))]
96+
# }
5497

5598
# zoi radius main
56-
y_value_percentage <- (1 - percentage) * (max_effect_size_main)
57-
x_radius_index <- which(abs(main_response) < abs(y_value_percentage))[1] - 1
99+
# y_value_percentage <- (1 - percentage) * (max_effect_size_main)
100+
# x_radius_index <- which(abs(main_response) < abs(y_value_percentage))[1] - 1
101+
# zoi_radius_main <- x[[xvar]][x_radius_index]
102+
103+
y_value_percentage <- (1 - percentage) * (max_effect_size_main - ref)
104+
if(type == "linear") {
105+
x_radius_index <- which(abs(main_response) < abs(y_value_percentage + ref))[1] - 1
106+
} else {
107+
x_radius_index <- which(abs(main_response) > abs(y_value_percentage + ref))[1] - 1
108+
}
109+
if(is.na(x_radius_index)) {
110+
x_radius_index <- length(main_response)
111+
}
58112
zoi_radius_main <- x[[xvar]][x_radius_index]
59113

60114
# impact
61115
x_vals <- x[[xvar]][1:x_radius_index]
62-
y_vals <- main_response[1:x_radius_index]
63-
if(type == "exp") y_vals <- exp(y_vals) - 1
116+
y_vals <- main_response[1:x_radius_index] - ref
117+
signal <- ifelse(y_vals[1] < 0, -1, 1)
64118
y_vals <- abs(y_vals) - min(abs(y_vals)) # get positive and discount are above y(ZOI)
65119

66-
impact_main <- DescTools::AUC(x_vals, y_vals)
120+
impact_main <- signal * DescTools::AUC(x_vals, y_vals)
67121

68-
max_effect_size[id] <- ifelse(type == "exp", exp(max_effect_size_main), max_effect_size_main)
122+
max_effect_size[id] <- max_effect_size_main
69123
zoi_radius[id] <- zoi_radius_main
70-
effect_zoi_radius[id] <- ifelse(type == "exp", exp(y_value_percentage), y_value_percentage)
124+
effect_zoi_radius[id] <- y_value_percentage + ref
71125
impact[id] <- impact_main
72126
}
73127

74128
# repeat that for the CI
75129
if(ci) {
76130

77-
ci_id <- 1
78-
131+
ci_id <- 2
79132
for(ci_id in seq(ci_col_name)) {
80133

81134
ci_response <- x[[ci_col_name[ci_id]]]
82135

83136
# max effect size
84-
ci_max <- ci_response[which.max(abs(main_response))]
137+
ci_max <- ci_response[which.max(abs(main_response - ref))]
85138

86139
# zoi radius main
87140
# y_value_percentage <- (1 - percentage) * max_effect_size
88-
x_radius_ci_index <- which(abs(ci_response) < abs(y_value_percentage))[1] - 1
141+
if(type == "linear") {
142+
x_radius_ci_index <- which(abs(ci_response) < abs(y_value_percentage + ref))[1] - 1
143+
} else {
144+
x_radius_ci_index <- which(abs(ci_response) > abs(y_value_percentage + ref))[1] - 1
145+
}
146+
if(is.na(x_radius_ci_index)) {
147+
x_radius_ci_index <- length(main_response)
148+
}
89149
zoi_radius_ci <- x[[xvar]][x_radius_ci_index]
90150

151+
if(length(zoi_radius_ci) == 0) {
152+
zoi_radius_ci <- NA
153+
}
154+
91155
# impact
92156
x_vals <- x[[xvar]][1:x_radius_ci_index]
93-
y_vals <- ci_response[1:x_radius_ci_index]
94-
if(type == "exp") y_vals <- exp(y_vals) - 1
157+
y_vals <- ci_response[1:x_radius_ci_index] - ref
158+
signal <- ifelse(y_vals[1] < 0, -1, 1)
95159
y_vals <- abs(y_vals) - min(abs(y_vals)) # get positive and discount are above y(ZOI)
96160

97-
impact_ci <- DescTools::AUC(x_vals, y_vals)
161+
impact_ci <- signal * DescTools::AUC(x_vals, y_vals)
98162

99-
max_effect_size[ci_id + 2] <- ifelse(type == "exp", exp(ci_max), ci_max)
163+
max_effect_size[ci_id + 2] <- ci_max
100164
zoi_radius[ci_id + 2] <- zoi_radius_ci
101-
effect_zoi_radius[ci_id + 2] <- ifelse(type == "exp", exp(y_value_percentage), y_value_percentage)
165+
effect_zoi_radius[ci_id + 2] <- y_value_percentage + ref
102166
impact[ci_id + 2] <- impact_ci
103167
}
104168
}
105169

106-
out <- list(max_effect_size = max_effect_size,
170+
out <- data.frame(max_effect_size = max_effect_size,
107171
zoi_radius = zoi_radius,
108172
effect_zoi_radius = effect_zoi_radius,
109173
impact = impact) |>
110-
dplyr::bind_rows() |>
111-
t()
174+
# dplyr::bind_rows() |>
175+
t() |>
176+
as.data.frame() |>
177+
tibble::rownames_to_column(var = "zoi_measure")
112178

113-
colnames(out) <- c("mean", "median", ci_col_name[1], ci_col_name[2])
179+
colnames(out)[-1] <- c(mean_col_name, median_col_name, ci_col_name[1], ci_col_name[2])
114180
out
115181
}
116182

183+
#' @param data `[data.frame]` \cr The original dataset used for model fitting.
184+
#' @param include `[character="all"]` \cr Character. Either `"all"` or a
185+
#' regex pattern to filter selected ZOI variables.
186+
#' @param return_predictions `[logical=FALSE]` \cr Logical. Whether to return
187+
#' the prediction curves alongside ZOI metrics. If `TRUE`, the output is necessarily
188+
#' a `list` with predictions and the ZOI parameters.
189+
#' @param return_format `[character="df"]{"list", "df"}` \cr
190+
#' Format of the returned ZOI metrics. Either a list of data.frames (if `return_format = "list"`),
191+
#' one for each variable, or a single `data.frame` (default, if `return_format = "df"`).
192+
#' @param wq_probs `[numeric,vector=c(0.025, 0.975)]` \cr Numeric vector of quantiles
193+
#' used for prediction summaries.
194+
#' @param n_features `[numeric=1]` \cr Number of features used in ZOI prediction.
195+
#' It can a single number (considered the same for all ZOI variables) or a vector
196+
#' with the same number of elements as ZOI variables in the model.
197+
#' @param radius_max `[numeric=NULL]` \cr Numeric. Maximum distance/radius to use for
198+
#' prediction curves. If `NULL` (default), the maximum value present in the bag's
199+
#' predictor table is used.
200+
#' @param baseline `[character="zero"]` \cr Character. Baseline used in `predict()` (e.g., `"zero"`).
201+
#' @param type_feature `[character="point"]` \cr Character or vector. Type of spatial feature used in
202+
#' `predict()`.
203+
#' @param type_feature_recompute `[logical=FALSE]` \cr Logical. Whether to recompute spatial
204+
#' features within `predict()`, for linear features.
205+
#' @param resolution `[numeric=200]` \cr Integer. Resolution used in the recomuptation
206+
#' of ZOIs for linear features.
207+
#' @param radii `[vector]` \cr Numeric vector. Radii used for ZOI modeling.
208+
#' @param zoi_shape `[character]` \cr Character. Shape of the ZOI used in the model
209+
#' (e.g., `"circle"`, `"Gauss"`, `"exp_decay"`).
210+
#'
211+
#' @return If `x` is a bag object, the function returns wither a `list` or
212+
#' `data.frame` of ZOI measures for each ZOI variable in the bag.
213+
#' If `return_predictions = TRUE`, also returns the prediction curves.
214+
#'
215+
#' @rdname zoi_from_curve
216+
#' @export
217+
zoi_from_curve.bag <- function(x,
218+
data,
219+
include = "all",
220+
percentage = 0.95,
221+
curve = c("median", "mean"),
222+
type = c("linear", "exp")[1],
223+
return_predictions = FALSE,
224+
return_format = c("list", "df")[2],
225+
ci = TRUE,
226+
wq_probs = c(0.025, 0.5, 0.975),
227+
n_features = 1,
228+
mean_col_name = "mean",
229+
median_col_name = "quantile:0.5",
230+
ci_col_name = c("quantile:0.025", "quantile:0.975"),
231+
radius_max = NULL,
232+
baseline = "zero",
233+
type_feature = "line",
234+
type_feature_recompute = TRUE,
235+
resolution = 200,
236+
radii = c(100, 250, 500, 1000, 2500, 5000, 10000),
237+
zoi_shape = c("circle", "Gauss", "rectangle", "exp_decay", "bartlett", "threshold",
238+
"mfilter")[1],
239+
...) {
240+
241+
# get ZOI variables and terms from predictor table
242+
pred_table <- x$parms$predictor_table
243+
zoi_vars <- pred_table$variable[pred_table$is_zoi == 1]
244+
zoi_terms <- pred_table$term_zoi[pred_table$is_zoi == 1]
245+
zoi_radii <- pred_table$zoi_radius[pred_table$is_zoi == 1]
117246

118-
# test n_features
119-
# test exp vs linear
120-
# implement function.df this one
247+
# set radius from predictor table, if null
248+
if(is.null(radius_max)) radius_max <- max(zoi_radii)
249+
250+
# unique variables
251+
zoi_vars_unique <- unique(zoi_vars)
252+
if(include != "all") {
253+
vars <- grep(include, zoi_vars_unique, value = TRUE)
254+
if(length(vars) < 1) {
255+
stop(paste0("Variable(s) ", paste0(include, collapse = ","), " not present in the bag."))
256+
}
257+
zoi_vars_unique <- vars
258+
}
259+
260+
# check parameters
261+
if(length(type_feature) == 1) {
262+
type_feature <- rep(type_feature, times = length(zoi_vars_unique))
263+
}
264+
265+
if(length(n_features) == 1) {
266+
n_features <- rep(n_features, times = length(zoi_vars_unique))
267+
}
268+
269+
# compute predictions
270+
i <- 3
271+
dfs <- lapply(seq_along(zoi_vars_unique), function(i) {
272+
dfvar <- data.frame(var = seq(0, radius_max, length.out = 10001))
273+
names(dfvar) <- zoi_vars_unique[i]
274+
type_feat <- type_feature[i]
275+
zoi_r <- zoi_radii[zoi_vars == zoi_vars_unique[i]]
276+
pred <- oneimpact::predict(x,
277+
newdata = dfvar,
278+
data = data,
279+
type = type,
280+
wq_probs = wq_probs,
281+
zoi = TRUE,
282+
n_features = n_features[i],
283+
baseline = baseline,
284+
type_feature = type_feat,
285+
type_feature_recompute = type_feature_recompute,
286+
resolution = resolution,
287+
radii = zoi_r,
288+
zoi_shape = zoi_shape, ...)
289+
cbind(dfvar, pred)
290+
})
291+
names(dfs) <- zoi_vars_unique
292+
293+
# compute zoi
294+
i <- 1
295+
zois <- lapply(seq_along(dfs), function(i) {
296+
zoi_from_curve(dfs[[i]], type = type,
297+
percentage = percentage,
298+
curve = curve,
299+
ci = ci,
300+
mean_col_name = mean_col_name,
301+
median_col_name = median_col_name,
302+
ci_col_name = ci_col_name)
303+
})
304+
names(zois) <- zoi_vars_unique
305+
306+
if(return_format == "df") {
307+
# i <- 1
308+
zois <- lapply(seq_along(zois), function(i) {
309+
zois[[i]] |>
310+
dplyr::mutate(variable = zoi_vars_unique[i])
311+
}) |>
312+
dplyr::bind_rows() |>
313+
dplyr::relocate(variable, .before = 1) |>
314+
tibble::as_tibble()
315+
}
316+
317+
# return tables with zois
318+
if(return_predictions) {
319+
return(list(predictions = df, zoi = zois))
320+
} else {
321+
return(zois)
322+
}
323+
}
121324
# implement function var
122325
# implement function bag - all vars
123326

0 commit comments

Comments
 (0)