---
title: "Parameter tuning and sensitivity"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Parameter tuning and sensitivity}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6,
                      fig.height = 4)
library(simOutrank)
```

An outranking model has three tuning surfaces: the **thresholds** of each
criterion, the **weights** between criteria, and the **number of clusters**
`k`. A good analysis checks that its conclusions are not an artefact of a single
arbitrary choice. This vignette shows how, using `illustrative_log`.

```{r base}
traces <- as_traces(illustrative_log, "case_id", "activity", "timestamp")

criteria <- function(activity_sim = 0.8, activity_indiff = 0.7,
                     w_activity = 0.2) {
  list(
    crit_activity_profile(weight = w_activity, indifference = activity_indiff,
                          similarity = activity_sim, veto = 0.4),
    crit_edit_distance(weight = 0.2, similarity = 2, indifference = 3,
                       veto = 6),
    crit_nominal("status", weight = 0.3),
    crit_nominal("satisfaction", weight = 0.3)
  )
}
```

We compare clusterings with the Rand index (the fraction of case pairs that two
partitions agree to keep together or apart):

```{r rand}
rand_index <- function(a, b) {
  agree <- 0L; n <- length(a)
  for (i in seq_len(n - 1L)) for (j in (i + 1L):n) {
    agree <- agree + ((a[i] == a[j]) == (b[i] == b[j]))
  }
  agree / choose(n, 2)
}

baseline <- cluster_traces(outrank_similarity(traces, criteria()),
                           k = 4, seed = 42)$memberships
```

## Fixed vs quantile thresholds

A threshold can be a fixed number or a quantile of the criterion's own value
distribution, written `as_quantile(p)`. Quantiles adapt to the data, which is
useful when you do not have a natural scale.

```{r quantile}
crit_fixed    <- crit_activity_profile(0.2, indifference = 0.7, similarity = 0.8)
crit_quantile <- crit_activity_profile(0.2, indifference = as_quantile(0.5),
                                       similarity = as_quantile(0.8))
crit_quantile
```

## Threshold sensitivity

Sweep the activity-similarity threshold and measure both the average credibility
and the agreement of the resulting clustering with the baseline.

```{r threshold-sweep}
grid <- seq(0.75, 0.95, by = 0.05)  # kept above the indifference of 0.70
sens <- t(vapply(grid, function(s) {
  sim <- outrank_similarity(traces, criteria(activity_sim = s))
  memb <- cluster_traces(sim, k = 4, seed = 42)$memberships
  c(mean_S = mean(sim$S[upper.tri(sim$S)]),
    rand   = rand_index(memb, baseline))
}, numeric(2)))
data.frame(similarity = grid, sens)
```

The Rand index stays high across the sweep: the four-cluster solution is not an
artefact of one threshold choice.

## Weight sensitivity

Do the same for the weight of the activities criterion, from negligible to
dominant (weights are renormalised internally).

```{r weight-sweep}
weights <- c(0.05, 0.2, 0.5, 1)
w_sens <- vapply(weights, function(w) {
  sim <- suppressMessages(outrank_similarity(traces,
                                             criteria(w_activity = w)))
  rand_index(cluster_traces(sim, k = 4, seed = 42)$memberships, baseline)
}, numeric(1))
data.frame(w_activity = weights, rand = w_sens)
```

## Choosing k

`eigengap()` guides `k` from the spectrum of the normalized Laplacian; the gap
after the k-th eigenvalue marks a natural cut.

```{r eigengap}
sim <- outrank_similarity(traces, criteria())
gaps <- eigengap(sim, k_max = 8)
gaps[which.max(gaps$gap), ]
```

Pair the diagnostic with a validity index across candidate `k` when
\pkg{clValid} is available:

```{r kvalidity}
if (requireNamespace("clValid", quietly = TRUE)) {
  t(vapply(2:6, function(k) {
    cl <- cluster_traces(sim, k = k, seed = 42)
    c(k = k, validate_clusters(cl))
  }, numeric(3)))
}
```

## Takeaway

Report the sweep, not a point estimate. When the Rand index stays high and a
validity index agrees with the eigengap, the clustering is robust to the
inevitable arbitrariness of the parameters.
```
