Skip to content

Commit f3aea90

Browse files
committed
ch15 survival DGP example; setup/CI dependency reconciliation
Content: - 15-dgp-dqcp.qmd: add a worked DGP section on stochastically ordered survival curves (Lim, Kim & Wang 2009) -- the right-censored, simple-ordering case reproducing their Figure 1 on the oropharynx data, with a Kaplan-Meier sanity check. Points to the cvxr.rbind.io survival-ordering example for partial/uniform ordering and interval censoring. - data/oropharynx.rds: dataset for that example (from cvxr_docs). - references.bib: add LimKimWang:2009, AgrawalDiamondBoyd:2019. - _quarto.yml: authors are now Anqi Fu and Balasubramanian Narasimhan. Preface (index.qmd): - Mark RStudio as recommended; make clone URLs and the RStudio/Positron tool names clickable; reword so opening the .Rproj is framed as reopening after a quit, not a redundant step. - Add guidance to edit in Source (not Visual) mode and to render once before the session to warm the freeze cache. - Reorder Installation so setup_check.R runs first (diagnose, then install what it flags). Dependency reconciliation (chapters <-> setup_check <-> install <-> CI): - setup_check.R: print installed vs. needed vs. version-built-with for each package; add RColorBrewer (a real CRAN dep, ch09); drop base and recommended packages that ship with R (survival, boot) as they are never the thing that is missing. - index.qmd install line: add RColorBrewer, drop boot. - publish.yml: drop dead any::expm (only referenced in a comment).
1 parent 6690861 commit f3aea90

7 files changed

Lines changed: 252 additions & 49 deletions

File tree

.github/workflows/publish.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ jobs:
7676
any::bench
7777
any::png
7878
any::RColorBrewer
79-
any::expm
8079
any::MASS
8180
any::Matrix
8281

15-dgp-dqcp.qmd

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ of a box subject to area limits:
2626
#| label: setup
2727
#| include: false
2828
library(CVXR)
29+
library(ggplot2)
30+
library(survival)
31+
source("_check_status.R")
32+
theme_set(theme_bw())
2933
```
3034

3135
```{r}
@@ -41,6 +45,147 @@ The objective `x * y * z` multiplies three variables --- not DCP at all --- yet
4145
solves it to global optimality. See the
4246
[DGP Tutorial](https://cvxr.rbind.io/examples/dgp/tutorial.html).
4347

48+
### A worked example: ordering survival curves
49+
50+
DGP is not only for engineering design; it appears in statistics wherever a model is
51+
built from products and ratios of positive quantities. Here is one such problem, from
52+
@LimKimWang:2009.
53+
54+
Suppose we estimate survival curves $S_1, \ldots, S_N$ for several populations and
55+
know on scientific grounds that they are **stochastically ordered**: population $a$
56+
dies earlier than $b$, meaning $S_a(t) \le S_b(t)$ at every $t$ (written $S_a \preceq
57+
S_b$). The unconstrained nonparametric estimates --- the Kaplan--Meier curves --- often
58+
*cross* where they should not. @LimKimWang:2009 observed that imposing the ordering
59+
turns the estimation into a geometric program, which `CVXR` solves directly.
60+
61+
Take **right-censored** data. For population $i$, at each distinct failure time
62+
$t_{ij}$ let $n_{ij}$ subjects be at risk and $d_{ij}$ of them fail. Write the one-step
63+
conditional survival probability $p_{ij} = S_i(t_{ij}) / S_i(t_{i,j-1})$ and $q_{ij} =
64+
1 - p_{ij}$, so survival is the running product $S_i(t_{ij}) = \prod_{r \le j} p_{ir}$
65+
(the Kaplan--Meier form). The likelihood is the **monomial**
66+
$$
67+
\mathcal L = \prod_{i} \prod_{j} p_{ij}^{\,n_{ij} - d_{ij}}\, q_{ij}^{\,d_{ij}},
68+
$$
69+
which is log-log affine and so drops straight into a GP. Three ingredients complete the
70+
model:
71+
72+
- **Sum-to-one**, relaxed from $p_{ij} + q_{ij} = 1$ to the posynomial inequality
73+
$p_{ij} + q_{ij} \le 1$ (a GP admits only monomial equalities). It is tight at the
74+
optimum, since both exponents are nonnegative.
75+
- **Monotonicity** of each survival curve, which is *automatic* here: $S_i$ is a
76+
product of factors $p \le 1$, so it is nonincreasing with no extra constraint.
77+
- **Ordering** $S_a \preceq S_b$: at each pooled failure time $t$, the monomial
78+
$\bigl(\prod_{t_{ar} \le t} p_{ar}\bigr)\bigl(\prod_{t_{br} \le t} p_{br}\bigr)^{-1}
79+
\le 1$.
80+
81+
Everything is a monomial except the sum-to-one posynomial, so `is_dgp()` holds. We use
82+
the oropharyngeal-carcinoma trial of Kalbfleisch and Prentice, grouping patients by
83+
tumor stage `T` and nodal stage `N`:
84+
85+
```{r}
86+
#| label: surv-data
87+
oropharynx <- readRDS("data/oropharynx.rds")
88+
89+
grp_counts <- function(T, N) { # discrete-hazard counts
90+
s <- subset(oropharynx, tstage == T & nstage == N)
91+
dt <- sort(unique(s$days[s$status == 1])) # distinct failure times
92+
data.frame(t = dt,
93+
d = sapply(dt, function(x) sum(s$days == x & s$status == 1)), # failures
94+
n = sapply(dt, function(x) sum(s$days >= x))) # at risk
95+
}
96+
```
97+
98+
`fit_rc()` assembles the monomial likelihood, the relaxed sum-to-one constraints, and
99+
one ordering monomial per pooled failure time for each requested pair `c(a, b)`
100+
(meaning $S_a \preceq S_b$):
101+
102+
```{r}
103+
#| label: surv-fit
104+
fit_rc <- function(groups, order = list()) {
105+
G <- length(groups)
106+
p <- lapply(groups, function(g) Variable(nrow(g), pos = TRUE))
107+
q <- lapply(groups, function(g) Variable(nrow(g), pos = TRUE))
108+
mono <- function(v, a) Reduce(`*`, lapply(which(a != 0), function(j) v[j]^a[j]))
109+
Scum <- function(i, ix) if (length(ix)) prod_entries(p[[i]][ix]) else 1 # empty prod = 1
110+
111+
objective <- Reduce(`*`, lapply(seq_len(G), function(i)
112+
mono(p[[i]], groups[[i]]$n - groups[[i]]$d) * mono(q[[i]], groups[[i]]$d)))
113+
constraints <- lapply(seq_len(G), function(i) p[[i]] + q[[i]] <= 1)
114+
115+
for (o in order) { # S_a(t) <= S_b(t) at every pooled failure time
116+
a <- o[1]; b <- o[2]
117+
for (tk in sort(unique(c(groups[[a]]$t, groups[[b]]$t)))) {
118+
ia <- which(groups[[a]]$t <= tk); ib <- which(groups[[b]]$t <= tk)
119+
constraints <- c(constraints, Scum(a, ia) * Scum(b, ib)^(-1) <= 1)
120+
}
121+
}
122+
prob <- Problem(Maximize(objective), constraints)
123+
val <- psolve(prob, gp = TRUE)
124+
check_solver_status(prob)
125+
list(is_dgp = is_dgp(prob), logLik = log(val),
126+
S = lapply(seq_len(G), function(i) cumprod(value(p[[i]])))) # S = running product
127+
}
128+
```
129+
130+
With no ordering the GP must return the ordinary Kaplan--Meier estimator --- a
131+
reassuring check:
132+
133+
```{r}
134+
#| label: surv-km-check
135+
g <- grp_counts(3, 3)
136+
fit <- fit_rc(list(g))
137+
km <- survfit(Surv(days, status) ~ 1,
138+
data = subset(oropharynx, tstage == 3 & nstage == 3))
139+
max(abs(fit$S[[1]] - summary(km, times = g$t)$surv)) # ~ 0: GP == Kaplan-Meier
140+
```
141+
142+
Now the ordering. Take the three T = 3 groups differing in nodal involvement,
143+
$(T,N) = (3,1), (3,2), (3,3)$, and impose $S_3 \preceq S_2 \preceq S_1$ (more nodal
144+
involvement, worse survival):
145+
146+
```{r}
147+
#| label: surv-fig
148+
#| fig-width: 8
149+
#| fig-height: 3.3
150+
g1 <- list("(3,1)" = grp_counts(3, 1),
151+
"(3,2)" = grp_counts(3, 2),
152+
"(3,3)" = grp_counts(3, 3))
153+
154+
rc_free <- fit_rc(g1)
155+
rc_order <- fit_rc(g1, list(c(3, 2), c(2, 1))) # S3 <= S2 <= S1
156+
157+
step_rc <- function(fit, groups, panel)
158+
do.call(rbind, lapply(seq_along(groups), function(i) {
159+
g <- groups[[i]]; S <- fit$S[[i]]
160+
data.frame(t = c(0, g$t), S = c(1, S),
161+
grp = paste0("(T,N)=", names(groups)[i]), panel = panel)
162+
}))
163+
164+
df <- rbind(step_rc(rc_free, g1, "(a) Kaplan-Meier (unconstrained)"),
165+
step_rc(rc_order, g1, "(b) ordered: S3 <= S2 <= S1"))
166+
167+
ggplot(df, aes(t, S, colour = grp)) + geom_step(linewidth = 0.6) +
168+
facet_wrap(~panel) + coord_cartesian(xlim = c(0, 1850), ylim = c(0, 1)) +
169+
labs(x = "time (days)", y = "survival probability", colour = NULL) +
170+
theme(legend.position = "inside", legend.position.inside = c(0.9, 0.8),
171+
legend.background = element_blank())
172+
```
173+
174+
The unconstrained Kaplan--Meier curves cross --- $(3,2)$ sits above $(3,1)$ for much of
175+
the range, contradicting $S_2 \le S_1$ --- while the ordered fit removes the crossings,
176+
reproducing Figure 1 of @LimKimWang:2009. The ordering costs only a little likelihood:
177+
178+
```{r}
179+
#| label: surv-ll
180+
c(unconstrained = round(rc_free$logLik, 2), ordered = round(rc_order$logLik, 2))
181+
```
182+
183+
The website's
184+
[survival-ordering example](https://cvxr.rbind.io/examples/dgp/survival-ordering.html)
185+
carries this further --- partial (non-total) orders, a stronger **uniform** ordering
186+
that constrains the hazard ratio, and the **interval-censored** case, where the same GP
187+
recipe applies with a different parameterization.
188+
44189
## Disciplined Quasiconvex Programming (DQCP)
45190

46191
A **quasiconvex** function has convex sub-level sets but need not be convex itself

_quarto.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ book:
88
author:
99
- "Anqi Fu"
1010
- "Balasubramanian Narasimhan"
11-
- "Stephen Boyd"
1211
date: today
1312
description: >
1413
A hands-on tutorial on disciplined convex optimization in R with CVXR,

data/oropharynx.rds

871 Bytes
Binary file not shown.

index.qmd

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,67 @@ live in one Git repository:
4747
[github.com/bnaras/cvxr_tutorial](https://github.com/bnaras/cvxr_tutorial). Get a copy
4848
in whichever way suits you:
4949

50-
- **RStudio:** *File → New Project → Version Control → Git*, paste
51-
`https://github.com/bnaras/cvxr_tutorial.git` as the repository URL, and click
52-
*Create Project*. Then open `cvxr-tutorial-2026.Rproj`.
53-
- **Positron:** open the Command Palette (`Ctrl/Cmd+Shift+P`), run *Git: Clone*, paste
54-
the same URL, and open the cloned folder.
50+
- **[RStudio](https://posit.co/download/rstudio-desktop/) (recommended):** *File → New
51+
Project → Version Control → Git*, paste
52+
[`https://github.com/bnaras/cvxr_tutorial.git`](https://github.com/bnaras/cvxr_tutorial.git)
53+
as the repository URL, and click *Create Project* --- RStudio opens the project for
54+
you. (If you quit RStudio and come back later, reopen it via the
55+
`cvxr-tutorial-2026.Rproj` file.)
56+
- **[Positron](https://positron.posit.co/):** open the Command Palette
57+
(`Ctrl/Cmd+Shift+P`), run *Git: Clone*, paste the same URL, and open the cloned
58+
folder.
5559
- **Terminal:** `git clone https://github.com/bnaras/cvxr_tutorial.git`, then open the
5660
folder in your editor.
5761
- **No Git?** Use the green *Code → Download ZIP* button on the repository page and
5862
unzip it.
5963

64+
**We recommend RStudio** for the tutorial: it bundles Quarto and needs no extra setup,
65+
so your time goes to optimization rather than tooling. Positron and other-editor users
66+
are of course welcome --- you already know your environment. Whichever you use, make your
67+
**first step**, the moment the project is open, the setup check in *Installation* below:
68+
it tells you exactly which packages you need and which you already have.
69+
6070
You will also need **[Quarto](https://quarto.org/docs/get-started/)** (≥ 1.4) to render
6171
the book or preview chapters. **RStudio bundles Quarto**, so there is nothing extra to
6272
install there. **Positron** and plain-terminal users should install the Quarto CLI
6373
once from the link above. You can still *run individual code chunks* without rendering
6474
anything --- Quarto is only required when you render a whole chapter or the full book
6575
(`quarto render`).
6676

77+
Two small things before you start. When you open a chapter such as `01-intro.qmd`, edit
78+
it in **Source** mode, not **Visual** mode: the visual editor can silently rewrite a
79+
file's options (for instance, adding a word-wrap setting to the header), which you don't
80+
want in the shared materials. And we recommend rendering the book **once, ahead of the
81+
session**, with `quarto render` (or, in RStudio, the **Render** button): this caches
82+
every chunk's results via Quarto's *freeze*, so nothing has to recompute live during the
83+
tutorial, and it confirms your whole setup --- packages and solvers --- in one shot.
84+
6785
## Installation
6886

69-
Everything in this tutorial runs on **CRAN packages only**. Two lines install all
70-
you need --- the **solvers** and a handful of **helper packages** (for plots,
71-
data, and comparisons against familiar R routines):
87+
Everything in this tutorial runs on **CRAN packages only**. Your first step, once the
88+
project is open, is to run the included **`setup_check.R`** --- it tells you what you
89+
need before you install anything:
90+
91+
```r
92+
source("setup_check.R") # or: Rscript setup_check.R
93+
```
94+
95+
For every package it prints a table of **what you have**, your **installed version**,
96+
the **minimum needed**, and the **version the tutorial was built with** --- so if
97+
anything misbehaves later, you can tell at a glance whether a version difference is to
98+
blame. It also checks your R version (**R ≥ 4.3.0**) and runs a tiny solve on each
99+
solver, reporting `PASS`/`WARN`/`FAIL`.
100+
101+
Then install whatever it flags. It is all on CRAN --- the **solvers** and a handful of
102+
**helper packages** (for plots, data, and comparisons against familiar R routines):
72103

73104
```r
74105
# Solvers (CVXR auto-pulls clarabel, osqp, scs, highs)
75106
install.packages(c("CVXR", "scip", "Uno", "sparsediff"))
76107

77108
# Helpers used by the examples
78-
install.packages(c("ggplot2", "tidyr", "nnls", "glmnet",
79-
"boot", "png", "bench"))
109+
install.packages(c("ggplot2", "RColorBrewer", "tidyr", "nnls",
110+
"glmnet", "png", "bench"))
80111
```
81112

82113
`CVXR` automatically brings in the four open-source solvers it uses ---
@@ -86,16 +117,6 @@ install.packages(c("ggplot2", "tidyr", "nnls", "glmnet",
86117
you ever can't install one of those three, you can still do every other chapter with
87118
just `CVXR` plus the helpers.
88119

89-
Before the tutorial, please run the included **`setup_check.R`** to confirm your
90-
machine is ready:
91-
92-
```r
93-
source("setup_check.R") # or: Rscript setup_check.R
94-
```
95-
96-
It checks your R version, the packages, and runs a tiny solve on each solver,
97-
reporting `PASS`/`FAIL`. Requires **R ≥ 4.3.0**.
98-
99120
## A note on solvers
100121

101122
We deliberately restrict ourselves to solvers available on CRAN. CVXR also

references.bib

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -598,20 +598,42 @@ @Article{tibsjhf:2017
598598
journal = {arXiv preprint arxiv:1712.00484},
599599
url = {https://arxiv.org/abs/1712.00484}
600600
}
601-
602-
@misc{riskfolio,
603-
author = {Dany Cajas},
604-
title = {{Riskfolio-Lib}: Portfolio Optimization in {Python}},
605-
year = {2026},
606-
url = {https://github.com/dcajasn/Riskfolio-Lib}
607-
}
608-
609-
@article{RockafellarUryasev:2000,
610-
author = {R. Tyrrell Rockafellar and Stanislav Uryasev},
611-
title = {Optimization of Conditional Value-at-Risk},
612-
journal = {Journal of Risk},
613-
volume = {2},
614-
number = {3},
615-
pages = {21--41},
616-
year = {2000}
617-
}
601+
602+
@misc{riskfolio,
603+
author = {Dany Cajas},
604+
title = {{Riskfolio-Lib}: Portfolio Optimization in {Python}},
605+
year = {2026},
606+
url = {https://github.com/dcajasn/Riskfolio-Lib}
607+
}
608+
609+
@article{RockafellarUryasev:2000,
610+
author = {R. Tyrrell Rockafellar and Stanislav Uryasev},
611+
title = {Optimization of Conditional Value-at-Risk},
612+
journal = {Journal of Risk},
613+
volume = {2},
614+
number = {3},
615+
pages = {21--41},
616+
year = {2000}
617+
}
618+
619+
@article{LimKimWang:2009,
620+
author = {Lim, Johan and Kim, Seung Jean and Wang, Xinlei},
621+
title = {Estimating Stochastically Ordered Survival Functions via Geometric Programming},
622+
journal = {Journal of Computational and Graphical Statistics},
623+
year = {2009},
624+
volume = {18},
625+
number = {4},
626+
pages = {978--994},
627+
doi = {10.1198/jcgs.2009.06140}
628+
}
629+
630+
@article{AgrawalDiamondBoyd:2019,
631+
author = {Agrawal, Akshay and Diamond, Steven and Boyd, Stephen},
632+
title = {Disciplined Geometric Programming},
633+
journal = {Optimization Letters},
634+
year = {2019},
635+
volume = {13},
636+
number = {5},
637+
pages = {961--976},
638+
doi = {10.1007/s11590-019-01422-z}
639+
}

setup_check.R

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
##
99
## One-line install for everything (run once if anything is missing):
1010
## install.packages(c("CVXR", "scip", "Uno", "sparsediff")) # solvers
11-
## install.packages(c("ggplot2","tidyr","nnls","glmnet","boot","png","bench"))
11+
## install.packages(c("ggplot2","RColorBrewer","tidyr","nnls","glmnet","png","bench"))
1212
##
1313
## Base R only -- no external dependencies -- so it runs even if a package
1414
## failed to install. CORE = the four built-in solvers used by most chapters;
@@ -29,14 +29,29 @@ rv_ok <- getRversion() >= "4.3.0"
2929
line(mark(rv_ok), "R >= 4.3.0", R.version.string)
3030
if (!rv_ok) core_ok <- FALSE
3131

32-
## --- 2. Packages present (+ version floors) ------------------------------
33-
hdr("Packages")
32+
## --- 2. Packages: installed vs. needed vs. version used to build ---------
33+
## REF = the versions this tutorial was developed and tested against. A
34+
## mismatch is usually harmless; it is shown so that if something misbehaves
35+
## during the session you can see at a glance whether your versions differ
36+
## from the author's and factor that in.
37+
REF <- c(CVXR = "1.9.1", clarabel = "0.11.2", osqp = "1.0.0", scs = "3.2.7",
38+
highs = "1.14.0.2", ggplot2 = "4.0.3", RColorBrewer = "1.1.3",
39+
tidyr = "1.3.2", nnls = "1.6", glmnet = "5.0", png = "0.1.9",
40+
bench = "1.1.4", scip = "1.10.0.3", Uno = "2.7.4",
41+
sparsediff = "0.4.0")
42+
43+
hdr("Packages (installed = you have | needed = minimum | used = built with)")
44+
cat(sprintf(" %-6s %-12s %-14s %-11s %s\n", "", "package", "installed", "needed", "used"))
3445
check_pkg <- function(pkg, min = NULL) {
35-
v <- tryCatch(packageVersion(pkg), error = function(e) NULL)
36-
if (is.null(v)) { line("FAIL", pkg, "not installed"); return(FALSE) }
46+
v <- tryCatch(packageVersion(pkg), error = function(e) NULL)
47+
ref <- if (pkg %in% names(REF)) REF[[pkg]] else "--"
48+
need <- if (is.null(min)) "any" else paste(">=", min)
49+
if (is.null(v)) {
50+
cat(sprintf(" [%s] %-12s %-14s %-11s %s\n", "FAIL", pkg, "not installed", need, ref))
51+
return(FALSE)
52+
}
3753
ok <- is.null(min) || v >= min
38-
line(mark(ok), pkg, sprintf("%s%s", v,
39-
if (!ok) sprintf(" (need >= %s)", min) else ""))
54+
cat(sprintf(" [%s] %-12s %-14s %-11s %s\n", mark(ok), pkg, as.character(v), need, ref))
4055
ok
4156
}
4257
## CVXR + the four solvers it imports (auto-installed with CVXR)
@@ -45,8 +60,10 @@ core_ok <- check_pkg("clarabel") && core_ok
4560
core_ok <- check_pkg("osqp") && core_ok
4661
core_ok <- check_pkg("scs") && core_ok
4762
core_ok <- check_pkg("highs", "1.14") && core_ok # CVXR requires highs >= 1.14
48-
## Helper packages used by the example chapters (CRAN)
49-
for (h in c("ggplot2", "tidyr", "nnls", "glmnet", "boot", "png", "bench"))
63+
## Helper packages the example chapters library() (CRAN installs only; base and
64+
## recommended packages that ship with R -- Matrix, MASS, survival -- are not
65+
## listed here because they are always present).
66+
for (h in c("ggplot2", "RColorBrewer", "tidyr", "nnls", "glmnet", "png", "bench"))
5067
core_ok <- check_pkg(h) && core_ok
5168
## Extra solvers for chapters 11 and 14 (CRAN, in CVXR Enhances -> separate installs)
5269
extra_ok <- check_pkg("scip", "1.10") && extra_ok # ch.11 MI-SOCP
@@ -107,7 +124,7 @@ cat(sprintf(" CORE (most chapters) : %s\n", if (core_ok) "READY" else "
107124
cat(sprintf(" EXTRAS (chapters 11 and 14) : %s\n", if (extra_ok) "READY" else "incomplete"))
108125
if (!core_ok) {
109126
cat("\n -> Fix CORE first:\n")
110-
cat(" install.packages(c(\"CVXR\",\"ggplot2\",\"tidyr\",\"nnls\",\"glmnet\",\"boot\",\"png\",\"bench\"))\n")
127+
cat(" install.packages(c(\"CVXR\",\"ggplot2\",\"RColorBrewer\",\"tidyr\",\"nnls\",\"glmnet\",\"png\",\"bench\"))\n")
111128
} else if (!extra_ok) {
112129
cat("\n -> Core is ready (most chapters work). For chapters 11 and 14 also:\n")
113130
cat(" install.packages(c(\"scip\", \"Uno\", \"sparsediff\"))\n")

0 commit comments

Comments
 (0)