Skip to contents

Reads a SAS program containing PROC HAZARD and/or PROC HAZPRED blocks and emits a Quarto document of the equivalent hazard() and predict.hazard() calls.

Usage

hzr_translate_sas(path, out_dir = NULL, librefs = NULL)

Arguments

path

Path to a .sas file.

out_dir

Directory to write the .qmd into. NULL (default) parses without writing.

librefs

Optional named character vector mapping SAS librefs to directories, e.g. c(EX = "estimates"), used to resolve INHAZ=. The resolved member is read as <member>.sas7bdat; give a librefs value that already ends in .rds or .sas7bdat (a specific file, not a directory) to name an already-converted fit directly, e.g. c(EX = "estimates/hzdeath.rds").

Value

An hzr_sas_job object, invisibly. $calls holds the emitted calls keyed by chunk label, $grid the last prediction grid seen and $inhaz the first unresolved INHAZ= (not all of each, when a job has several), $untranslated the recorded gaps, $coverage the token counts, and $notes any callout attached to a chunk by label, emitted immediately above that chunk in the rendered document.

Details

Experimental: a job that translates does render (the emitted hazard() chunk binds its fit and asks for an actual fit), but this is a translation aid, not a turnkey reproduction, and some SAS constructs are refused rather than translated. See the Experimental section below.

Constructs the translator does not cover are recorded on the returned object and rendered as visible callouts, never dropped. A PROC HAZPRED job whose INHAZ= fitted model cannot be located emits a stop(), so the document fails to render rather than reporting over a model it did not load.

A phase-statement variable that PROC HAZARD cannot read as a name, such as AGE*SEX, LOG(AGE) or B SEX, is left out of the model. (LOG() is read as LOG, as PROC HAZARD reads it: its lexer treats ) as whitespace and ( as a change of state, hazard_l.l:32 and :56.) A phase variable must be a NAME, [_A-Z][_A-Z0-9]* (hazard_l.l:39; phasevar : NAME, hazard_y.y:213), so PROC HAZARD rejects such a job at parse. The document raises a warning() above the fit, and the operand is recorded in $untranslated.

Two more syntax errors take the same route. A value on a PROC HAZARD option that takes none, such as NOCOV=1 (hazard_y.y:65-76), is ignored in the fit. A TIME, EVENT, RCENSOR, LCENSOR or WEIGHT statement with more than one operand, which takes exactly one name (hazard_y.y:106-127), is fitted on the first. Both are warned about above the fit and recorded in $untranslated. TIME or EVENT with no operand, and no other statement supplying the variable, leaves nothing to fit, so that job's fit chunk is a stop().

A job may contain more than one PROC HAZARD and/or PROC HAZPRED block. Every block is preserved: the first of a kind keeps the bare chunk name (fit, pred, pred_haz), later ones get fit_2, fit_3, pred_2, and so on, so the emitted document contains every model, not just the last one seen. job$outhaz is therefore a character vector, one element per PROC HAZARD block that set OUTHAZ= (in the order the blocks appear). Each PROC HAZPRED block's INHAZ= is resolved independently against that vector, matching the most recently written OUTHAZ= at that point in the file, mirroring SAS itself, where a later OUTHAZ= write overwrites the dataset an earlier one wrote under the same name. If a job's own OUTHAZ= values don't cover it, librefs is tried next; distinct external INHAZ= values each get their own loaded-fit chunk. When neither resolves and the job holds more than one local fit, which fit a predict() call belongs to is genuinely unknown; the emitted call falls back to referencing fit and the ambiguity is recorded in untranslated, never guessed at silently.

What the emitted document needs to run

Base R at the version this package declares in DESCRIPTION (Depends), plus TemporalHazard itself. The emitted chunks never reach into this package's internals, so the document renders in your session, and in a colleague's, without anything further installed, with one exception. A translated SELECTION screen on a multiphase job with interval-censored rows (an ICENSOR job; LCENSOR is left truncation and does not need it) needs the suggested package numDeriv. Without it a screen that tests an entry stops and says so. A screen that completes but cannot test a removal (a BACKWARD screen, whose base has no usable variance) is caught by the emitted check, which warns and names the variables.

Experimental

The emitted document renders: the hazard() chunk binds its fit to a name and passes fit = TRUE, so the predict() chunks have something to predict from. A SELECTION statement is translated into an hzr_stepwise() call carrying the job's own candidates, per-variable flags and thresholds (#160); the screen is real, and may select a different model than PROC HAZARD did, which the emitted document says in a callout above the chunk. SELECTION options with no faithful translation are refused outright rather than mistranslated, emitting a stop() in place of the fit: FAST, MAXVARS, RESTRICT, a per-variable MOVE= or ORDER=, and a variable held by /I in one phase but movable in another. So is LCENSOR combined with ICENSOR, which one time_lower argument cannot express (#155). A job with ICENSOR is fitted with objective = "sas", the interval term PROC HAZARD accumulates, so it reproduces PROC HAZARD's estimates; the value it reports is then PROC HAZARD's objective, not a log-likelihood, and a callout above the chunk says so (#543). Degenerate intervals are resolved as PROC HAZARD does before its fit: a lower bound equal to the time makes the row an exact event, and one that is missing, negative or after the time drops the row from the fit (not from your data frame); the status chunk warns with both counts. Prediction grids the parser cannot resolve are refused whole, and the predict() chunks that would have read such a grid become a stop() naming it, rather than a predict(newdata = ) over a name no chunk builds. An unresolved INHAZ= stops the render on purpose.

On a fit loaded from an external INHAZ= dataset, point predictions work but se.fit = TRUE is refused when PROC HAZARD estimated a late shape parameter on a composite scale (the generic unconstrained three-phase case, not an exotic one). A translated PROC HAZPRED block asks for confidence limits unless the SAS job says NOCL, so such a job stops at its predict() chunks.

Repeated events

A job that builds its fit input with the SAS macro %repeat has that call translated to hzr_repeated_events(). The emitted chunk renames the function's output columns to the names the job gives them, upper-cased like every name the translator emits, so the fit reads them. An input column named like one of those outputs is dropped first, with a warning, because the macro overwrites it. The macro's input is built by the job's own DATA steps, which are not translated, so the document stops until that input is assigned. Any step between the macro and the fit that names the macro's output, or uses a macro variable that might, is not translated either; a plain PROC SORT that only reorders rows is the one step let through. Its chunk stops and quotes the step, for the reader to replace with R code or to delete if the step leaves the output unchanged. The same holds between two %repeat calls when the second reads the first's output. A step that changes the output without naming it, such as a macro that writes it internally, is not detected.

Comparing a translated fit against a SAS listing

The emitted hzr_phase("g3", ...) call carries the TAU, GAMMA, ALPHA and ETA that PARMS specified. PROC HAZARD does not always report the values it was given: when ALPHA is fixed at 1 it rewrites the late phase before fitting, pinning TAU at 1 and folding the two shape parameters into one exponent, because at alpha = 1, tau = 1 the G3 form collapses to \(t^{\gamma\eta}\) and only that product is identified. With ETA fixed it reports GAMMA as \(\gamma\eta\) and ETA as 1; otherwise it reports ETA as \(\gamma\eta\) and GAMMA as 1.

So a listing can print a GAMMA/ETA pair that differs from the job's own PARMS text, and from the emitted call, while describing the same fitted model. Compare the product \(\gamma\eta\), not the two parameters separately, and expect TAU to read 1 and fixed. The log-likelihood, MUL and every prediction agree regardless. Most jobs are unaffected in practice: GAMMA = 1 FIXGAMMA is the usual companion to ALPHA = 1 FIXALPHA, and it makes the rewrite an identity.

The one case that is a genuine model difference (ALPHA fixed at 1 with GAMMA and ETA both estimated, where PROC HAZARD fixes ETA and fits one parameter fewer than hazard() would) is recorded in $untranslated rather than left to be discovered in the comparison.

$coverage counts tokens the parser recognised; it is not evidence that the emitted calls execute, and a job can report full coverage with an empty $untranslated while its document still errors on render. See the 1.2.2 NEWS.md entry. The function's API, the hzr_sas_job field layout and the emitted document format are all expected to change.

Examples

# \donttest{
job <- hzr_translate_sas(
  system.file("extdata", "hz-example.sas", package = "TemporalHazard")
)
#> Warning: 2 untranslated construct(s) in hz-example.sas: QUASINEWTON, CONDITION
# }