---
title: "FPScausal: Functional propensity score weighting for causal inference with functional treatments, covariates, and outcomes"
author: "Nicole Fontana, Simone Ciardulli"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
    toc_depth: 3
    number_sections: true
vignette: >
  %\VignetteIndexEntry{FPScausal: Functional propensity score weighting for causal inference with functional treatments, covariates, and outcomes}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment  = "#>",
  fig.width  = 7,
  fig.height = 4.5,
  warning  = FALSE,
  message  = FALSE
)
```

# Introduction

**FPScausal** implements the *Functional Propensity Score (FPS) weighting*
methodology for causal inference with functional treatments
(Ciardulli, S. and Fontana, N., 2026).

The core idea is to represent a functional treatment $X(s)$ through its
Functional Principal Component (FPC) scores $\mathbf{A} \in \mathbb{R}^L$,
and then to estimate covariate-balancing weights $\{w_i\}$ by maximising the
empirical likelihood subject to the balancing constraints
$$\frac{1}{n}\sum_{i=1}^n w_i \mathbf{g}_i = \mathbf{0}, \quad
  \sum_{i=1}^n w_i = 1,$$
where $\mathbf{g}_i = [\mathbf{A}_i^\top, \mathbf{C}_i^\top,
\mathrm{vec}(\mathbf{A}_i\mathbf{C}_i^\top)^\top]^\top$ stacks the
balancing moments for unit $i$.  Here $\mathbf{C}_i$ denotes the vector
of **confounders** observed for unit $i$ (e.g.\ demographic variables
or baseline measurements).
The dual of this empirical-likelihood problem reduces to the smooth,
unconstrained minimisation
$$\min_{\boldsymbol\theta} \log\!\Bigl(\sum_{i=1}^n
  e^{-\boldsymbol\theta^\top \mathbf{g}_i}\Bigr),$$
solved via the BFGS quasi-Newton algorithm.  The weights are recovered as
the softmax transformation
$w_i = e^{-\boldsymbol\theta^{*\top}\mathbf{g}_i}/
       \sum_j e^{-\boldsymbol\theta^{*\top}\mathbf{g}_j}$.

Once the weights are obtained, the causal estimand is

- the **causal effect function** $\mu(s)$ (scalar or binary outcome), or
- the **causal effect surface** $\mu(s,t)$ (functional outcome)

estimated via weighted least squares.

This vignette walks through the full workflow on simulated data for two
outcome types.

```{r load-package}
library(FPScausal)
```

---

# Simulation settings

`simulate_fps_data()` implements the data-generating process from the
simulation study in the paper.  The four settings ("LL", "LN", "NL", "NN")
control whether the treatment-confounder and the confounder-outcome
relationships are **L**inear or **N**onlinear:

| Setting | Treatment–Confounder | Confounder–Outcome |
|---------|---------------------|--------------------|
| LL      | Linear              | Linear             |
| LN      | Linear              | Nonlinear          |
| NL      | Nonlinear           | Linear             |
| NN      | Nonlinear           | Nonlinear          |

The treatment $X(s)$ is built from six Fourier eigenfunctions; the scalar
confounders $\mathbf{C}$ are 3-dimensional; one functional covariate $D(s)$
(4 Fourier components) is optionally included.

---

# Part 1: Scalar continuous outcome

## Data generation

We simulate $n = 200$ subjects under setting **"LL"** with **scalar
covariates only** (no functional covariate) and a scalar continuous outcome.

```{r sim-scalar}
set.seed(42)
dat <- simulate_fps_data(
  n                      = 200,
  setting                = "LL",
  outcome_type           = "scalar",
  include_functional_cov = FALSE,
  seed                   = 42
)

cat("Treatment X:", nrow(dat$X), "x", ncol(dat$X), "\n")
cat("Outcome Y:   length", length(dat$Y), "\n")
cat("Scalar C:   ", nrow(dat$C), "x", ncol(dat$C), "\n")
```

The true causal effect function is:

$$\mu(s) = 2\sqrt{2}\sin(2\pi s) + \sqrt{2}\cos(2\pi s)
           + \tfrac{\sqrt{2}}{2}\sin(4\pi s)
           + \tfrac{\sqrt{2}}{2}\cos(4\pi s)$$

```{r plot-true-mu, fig.cap="True causal effect function."}
plot(dat$t_grid, dat$true_beta, type = "l", lwd = 2, col = "black",
     xlab = "s", ylab = expression(mu(s)), main = "True causal effect")
abline(h = 0, lty = 2, col = "grey")
```

## Weight estimation

The treatment domain `treat_domain` is inferred automatically from
`treat_grid` when omitted:

```{r weight-scalar}
w_obj <- fps_weighting(
  treatment  = dat$X,
  treat_grid = dat$t_grid,
  domain_name = "s",
  pve        = 0.95,
  covariates = dat$C
)
print(w_obj)
```

### Diagnostic plots

**Weight distribution:**

```{r plot-weights-scalar, fig.cap="Distribution of FPS weights."}
plot(w_obj, type = "weights")
```

**Treatment FPCA:** scree and eigenfunctions:

```{r plot-fpca-treatment, fig.cap="Treatment FPCA.", fig.width=9}
plot(w_obj, type = "fpca_treatment")
```

**Covariate balance:** absolute Pearson correlations before (red) and after
(blue) weighting. Dashed line at 0.1:

```{r plot-balance-scalar, fig.cap="Covariate balance for scalar outcome."}
plot(w_obj, type = "balance")
```

## Effect estimation (analytical CI)

```{r effect-scalar}
eff <- fps_effect_estimation(
  outcome    = dat$Y,
  fps_object = w_obj,
  true_beta  = dat$true_beta
)
print(eff)
```

**Weighted vs unweighted comparison (with analytical CI):**

```{r plot-comparison-scalar, fig.cap="Weighted vs unweighted causal effect."}
plot(eff, type = "comparison")
```

## Effect estimation with bootstrap CI

```{r effect-scalar-boot}
eff_boot <- fps_effect_estimation(
  outcome    = dat$Y,
  fps_object = w_obj,
  bootstrap  = TRUE,
  B          = 200,
  alpha      = 0.05,
  true_beta  = dat$true_beta,
  seed       = 123
)
```

**Effect with 95% bootstrap CI:**

```{r plot-effect-scalar, fig.cap="Causal effect with 95% bootstrap CI."}
plot(eff_boot, type = "effect")
```

**Significant time points** (CI excludes 0):

```{r plot-sig-scalar, fig.cap="Significant regions at alpha = 0.05."}
plot(eff_boot, type = "significance")
```

## Binary outcome

When the outcome is binary (0/1), `fps_effect_estimation` automatically
detects it and fits a **linear probability model** (weighted least squares),
returning the average treatment effect on the probability scale.

```{r effect-binary}
Y_bin   <- as.integer(dat$Y > median(dat$Y))
eff_bin <- fps_effect_estimation(Y_bin, w_obj)
print(eff_bin)
```

**Weighted vs unweighted comparison (with analytical CI):**

```{r plot-comparison-binary, fig.cap="Binary outcome: weighted vs unweighted."}
plot(eff_bin, type = "comparison")
```

---

## Scalar outcome with a functional covariate

When a functional covariate $D(s)$ is available, it enters the balancing step
through its own FPC scores.  We use $n = 2000$ to ensure a stable weight
solution (the constraint dimension grows with the number of FPCs).

```{r sim-scalar-funcov}
dat_fc <- simulate_fps_data(
  n                      = 2000,
  setting                = "LL",
  outcome_type           = "scalar",
  include_functional_cov = TRUE,
  seed                   = 7
)
```

```{r weight-scalar-funcov}
w_fc <- fps_weighting(
  treatment   = dat_fc$X,
  treat_grid  = dat_fc$t_grid,
  domain_name = "s",
  pve         = 0.95,
  covariates  = list(
    scalar     = dat_fc$C,
    functional = list(dat_fc$D)
  ),
  cov_grids   = list(dat_fc$t_grid),
  cov_pve     = 0.95
)
print(w_fc)
```

The FPC scores of the functional covariate are automatically named
`Func_Cov1_FPC1`, `Func_Cov1_FPC2`, ... in the balance plot:

```{r plot-balance-funcov, fig.cap="Balance with functional covariate."}
plot(w_fc, type = "balance")
```

```{r effect-scalar-funcov}
eff_fc <- fps_effect_estimation(
  outcome    = dat_fc$Y,
  fps_object = w_fc,
  true_beta  = dat_fc$true_beta
)
plot(eff_fc, type = "comparison")
```

---

## All four simulation settings

The table below shows the Integrated Squared Error (ISE) and Integrated
Squared Bias (ISB) of the weighted vs unweighted estimate across settings.

```{r all-settings, results='asis'}
settings    <- c("LL", "LN", "NL", "NN")
results_tbl <- lapply(settings, function(s) {
  d   <- simulate_fps_data(200, setting = s, outcome_type = "scalar",
                            include_functional_cov = FALSE, seed = 1)
  w   <- fps_weighting(d$X, treat_grid = d$t_grid,
                        covariates = d$C)
  eff <- fps_effect_estimation(d$Y, w, true_beta = d$true_beta)
  data.frame(
    Setting        = s,
    ISE_weighted   = round(mean((eff$beta - d$true_beta)^2),  4),
    ISE_unweighted = round(mean((eff$beta_unweighted - d$true_beta)^2), 4),
    ISB_weighted   = round(mean(eff$beta - d$true_beta)^2,   6),
    ISB_unweighted = round(mean(eff$beta_unweighted - d$true_beta)^2, 6)
  )
})

knitr::kable(
  do.call(rbind, results_tbl),
  caption = "ISE and ISB for weighted vs unweighted estimate across settings"
)
```

---

# Part 2: Functional outcome

## Data generation

Now we simulate with a **functional outcome** $Y(t)$, so the causal estimand
is the bivariate effect surface $\mu(s,t)$.
We use $n = 200$ with scalar covariates only for this illustration.

```{r sim-functional}
dat_fn <- simulate_fps_data(
  n                      = 200,
  setting                = "LL",
  outcome_type           = "functional",
  include_functional_cov = FALSE,
  seed                   = 99
)

cat("Treatment X:", nrow(dat_fn$X), "x", ncol(dat_fn$X), "\n")
cat("Outcome Y:  ", nrow(dat_fn$Y), "x", ncol(dat_fn$Y), "\n")
```

The true surface is:

$$\mu(s,t) = 2\sqrt{2}\sin(2\pi s)\cos(2\pi t)
            + 2\sqrt{2}\sin(2\pi t)\cos(2\pi s)
            + \sqrt{2}\cos(4\pi t)\sin(4\pi s)
            + \sqrt{2}\cos(4\pi s)\sin(4\pi t)$$

```{r plot-true-surface, fig.cap="True causal effect surface mu(s,t)."}
image(dat_fn$t_grid, dat_fn$t_grid, dat_fn$true_beta,
      xlab = "s (treatment)", ylab = "t (outcome)",
      main = expression(paste("True  ", mu, "(s,t)")),
      col  = hcl.colors(50, "Blue-Red 3"))
```

## Weight estimation

```{r weight-functional}
w_fn <- fps_weighting(
  treatment   = dat_fn$X,
  treat_grid  = dat_fn$t_grid,
  treat_domain = c(0, 1),
  domain_name = "s",
  pve         = 0.95,
  covariates  = dat_fn$C
)
print(w_fn)
```

```{r plot-balance-functional, fig.cap="Covariate balance for functional outcome."}
plot(w_fn, type = "balance")
```

## Effect estimation (no bootstrap)

```{r effect-functional}
eff_fn <- fps_effect_estimation(
  outcome             = dat_fn$Y,
  fps_object          = w_fn,
  outcome_t_grid      = dat_fn$t_grid,
  outcome_domain      = c(0, 1),
  outcome_domain_name = "t",
  outcome_pve         = 0.95,
  true_beta           = dat_fn$true_beta
)
print(eff_fn)
```

**Outcome FPCA:**

```{r plot-fpca-outcome, fig.cap="Outcome FPCA.", fig.width=9}
plot(eff_fn, type = "fpca_outcome")
```

**Estimated effect surface (weighted):**

The dashed black contour lines overlay the true surface $\mu(s,t)$ for
reference — they appear because `true_beta` was passed to
`fps_effect_estimation()`.

```{r plot-surface, fig.cap="Estimated causal effect surface."}
plot(eff_fn, type = "effect")
```

**Weighted vs unweighted comparison:**

```{r plot-comparison-fn, fig.cap="Weighted vs unweighted surface.", fig.width=9}
plot(eff_fn, type = "comparison")
```

## Effect estimation with bootstrap

```{r effect-functional-boot}
eff_fn_boot <- fps_effect_estimation(
  outcome             = dat_fn$Y,
  fps_object          = w_fn,
  outcome_t_grid      = dat_fn$t_grid,
  outcome_domain      = c(0, 1),
  outcome_domain_name = "t",
  outcome_pve         = 0.95,
  bootstrap           = TRUE,
  B                   = 200,
  alpha               = 0.05,
  true_beta           = dat_fn$true_beta,
  seed                = 42
)
```

**1-D slice of the effect surface — fixing outcome time t = 0.5:**

```{r plot-slice-outcome}
plot(eff_fn_boot, type = "bootstrap_slice",
     point = 0.5, which_domain = "outcome")
```

**1-D slice — fixing exposure time s = 0.5:**

```{r plot-slice-treatment}
plot(eff_fn_boot, type = "bootstrap_slice",
     point = 0.5, which_domain = "treatment")
```

**Significance map:**

```{r plot-sig-fn}
plot(eff_fn_boot, type = "significance")
```

## All four simulation settings

```{r all-settings-fn, results='asis'}
settings   <- c("LL", "LN", "NL", "NN")
results_fn <- lapply(settings, function(s) {
  d   <- simulate_fps_data(200, setting = s, outcome_type = "functional",
                            include_functional_cov = FALSE, seed = 2)
  w   <- fps_weighting(d$X, treat_grid = d$t_grid,
                        domain_name = "s",
                        covariates  = d$C)
  eff <- fps_effect_estimation(d$Y, w,
                                outcome_t_grid      = d$t_grid,
                                outcome_domain      = c(0, 1),
                                outcome_domain_name = "t",
                                true_beta           = d$true_beta)
  data.frame(
    Setting        = s,
    ISE_weighted   = round(mean((eff$beta - d$true_beta)^2),  4),
    ISE_unweighted = round(mean((eff$beta_unweighted - d$true_beta)^2), 4),
    ISB_weighted   = round(mean(eff$beta - d$true_beta)^2,   6),
    ISB_unweighted = round(mean(eff$beta_unweighted - d$true_beta)^2, 6)
  )
})

knitr::kable(
  do.call(rbind, results_fn),
  caption = "Surface ISE and ISB for weighted vs unweighted estimate"
)
```

---

# Session info

```{r session-info}
sessionInfo()
```
