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.
Arguments
- path
Path to a
.sasfile.- out_dir
Directory to write the
.qmdinto.NULL(default) parses without writing.- librefs
Optional named character vector mapping SAS librefs to directories, e.g.
c(EX = "estimates"), used to resolveINHAZ=. The resolved member is read as<member>.sas7bdat; give a librefs value that already ends in.rdsor.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
# }