A decision maker rarely accepts a clustering as-is.
simOutrank lets them inject domain knowledge through
pairwise constraints and remove uninformative cases by
trimming. This vignette follows Runs 2 and 3 of Delias
et al. (2023) on the bundled illustrative_log.
traces <- as_traces(illustrative_log, "case_id", "activity", "timestamp")
criteria <- list(
crit_activity_profile(weight = 0.2, indifference = 0.7, similarity = 0.8,
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)
)
sim <- outrank_similarity(traces, criteria)
run1 <- cluster_traces(sim, k = 4, seed = 42)$membershipsEach Run-1 cluster already contains a single tier: with these
criteria and the corrected normalisation, simOutrank
separates GOLD from NORMAL without any
constraint. (The paper’s original Run 1, under a different
normalisation, mixed some long-path GOLD customers with NORMAL
ones.)
tier <- illustrative_log$status[match(names(run1), illustrative_log$case_id)]
table(cluster = run1, tier = tier)
#> tier
#> cluster GOLD NORMAL
#> 1 0 6
#> 2 0 8
#> 3 5 0
#> 4 6 0Even when the base clustering separates the tiers, a decision maker
may want to guarantee it. cannot_link() severs the
credibility of every mixed-tier pair (S <- S * (1 - M));
passing an attribute name applies the constraint to all pairs whose
values differ.
sim$S["1", "11"] # a GOLD-NORMAL pair, before
#> [1] 0
sim2 <- cannot_link(sim, "status")
sim2$S["1", "11"] # severed to 0
#> [1] 0
run2 <- cluster_traces(sim2, k = 4, seed = 42)$memberships
table(cluster = run2,
tier = illustrative_log$status[match(names(run2), illustrative_log$case_id)])
#> tier
#> cluster GOLD NORMAL
#> 1 0 8
#> 2 6 0
#> 3 0 6
#> 4 5 0The complementary operation, must_link(), rewards pairs
that should stay together (S <- min(S + reward * M, 1)),
and likewise accepts either a matrix of case-id pairs or an attribute
name.
The two outliers dilute the clustering. trim_outliers()
scores each case by its total similarity to the others and removes the
least-connected ones. The genuine low-similarity case is flagged
first:
trimmed <- trim_outliers(sim, prop = 0.04) # remove floor(0.04 * 25) = 1 case
attr(trimmed, "trimmed")
#> [1] "24"Case 24, whose A, B, E flow matches neither
tier, has the lowest connectivity and is removed. (The paper
additionally trims case 25 by domain judgement; under this
weighting 25 shares its tier and satisfaction with many
cases and is not flagged automatically – a good illustration of why
trimming is offered as a tool, not an oracle.) For small logs the exact
integer program is also available:
identical(attr(trim_outliers(sim, prop = 0.04, method = "lp"), "trimmed"),
attr(trim_outliers(sim, prop = 0.04, method = "greedy"), "trimmed"))
#> [1] TRUEClustering the trimmed matrix gives cleaner groups:
Constraints and trimming compose: trim first, then constrain (or the reverse), and cluster the adjusted matrix.
Delias, P., Doumpos, M., Grigoroudis, E. and Matsatsinis, N. (2023). Improving the non-compensatory trace-clustering decision process. International Transactions in Operational Research, 30(3), 1387–1406. doi:10.1111/itor.13062 ```