25  Move a legacy HAZARD analysis into R

25.1 When to use it

The destination is a maintained R analysis. TemporalHazard (Ehrlinger 2026) can read the structure of a historical PROC HAZARD job, translate the parts it recognizes, and import a legacy OUTHAZ= artifact for a bounded comparison. Once we have checked the mapping, the R code, fitted object, diagnostics, and predictions become the source we keep. A SAS license or live SAS job is not part of the new workflow.

Think of the old program as a set of labeled boxes. TIME, EVENT, PARMS, and the phase statements tell us what each box contained. The migration job is to unpack those boxes once into named R arguments, inspect the result, and then work in the current API.

The example below is synthetic and contains no patient data.

25.2 Translate a short historical input

hzr_translate_sas() reads a file path, so we write the small text block to a session temporary file. No legacy software is called. The parser reads the statements and returns an hzr_sas_job containing the R expressions it would place in a translated Quarto document.

sas_text <- c(
  "PROC HAZARD DATA=AVC CONSERVE CONDITION=14;",
  "  TIME INT_DEAD;",
  "  EVENT DEAD;",
  "  PARMS MUE=0.20 NU=1;",
  "RUN;"
)

sas_path <- tempfile(fileext = ".sas")
writeLines(sas_text, sas_path)
job <- hzr_translate_sas(sas_path)

cat(paste(sas_text, collapse = "\n"))
PROC HAZARD DATA=AVC CONSERVE CONDITION=14;
  TIME INT_DEAD;
  EVENT DEAD;
  PARMS MUE=0.20 NU=1;
RUN;
data.frame(
  tokens_seen = job$coverage$tokens_seen,
  tokens_mapped = job$coverage$tokens_mapped,
  untranslated_constructs = nrow(job$untranslated),
  emitted_expressions = length(job$calls)
)
  tokens_seen tokens_mapped untranslated_constructs emitted_expressions
1           6             6                       0                   3

Coverage reports what the parser recognized, not whether a model has been validated. For this bounded input all six tokens are mapped and no construct is left untranslated. Now inspect the emitted fit call itself.

cat(paste(deparse(job$calls$fit, width.cutoff = 80), collapse = "\n"))
fit <- hazard(data = AVC, time = INT_DEAD, status = .hzr_status, fit = TRUE, dist = "multiphase", 
    phases = list(hzr_phase("cdf", nu = 1)), theta = c(log(0.2), log(1), 1, 0), weights = ifelse(DEAD > 
        0, DEAD, 1), control = list(condition = 14, conserve = TRUE))

The generated call carries TIME to time, derives a 0/1 status from the historical event count, preserves event counts as weights, turns the early parameters into hzr_phase("cdf", ...), and carries CONSERVE and CONDITION=14 into control. The returned job also contains a data guard and status expression. Translation does not recreate a SAS DATA step, so you must bind the reviewed R data frame before running the generated fit.

WarningRead the generated code before you run it

The translator deliberately refuses some constructs, including a SELECTION statement and censoring combinations that cannot be expressed by one current R argument. An unresolved external fit or prediction grid can also stop the generated document. Never replace that stop with guessed code. Rebuild the unsupported step directly in R and document the choice.

25.3 Map the old names once

hzr_argument_mapping() exposes the package’s formal crosswalk. Keep planned rows visible during migration: the status column distinguishes a maintained mapping from a broader legacy concept that still needs review.

argument_map <- hzr_argument_mapping(include_planned = TRUE)

argument_map[
  argument_map$sas_statement %in% c("TIME", "EVENT", "PARMS", "DIST", "G1", "G2"),
  c("sas_statement", "legacy_input", "r_parameter",
    "transform_rule", "implementation_status")
]
   sas_statement        legacy_input            r_parameter
8           TIME                   t                   time
9          EVENT              status                 status
10         PARMS              theta0                  theta
11          DIST                dist                   dist
14            G1 THALF / RHO (early)     hzr_phase(t_half=)
15            G1          NU (early)         hzr_phase(nu=)
16            G1           M (early)          hzr_phase(m=)
17            G1       DELTA (early) (absorbed by decompos)
18            G2   G2 constant phase  hzr_phase('constant')
                                                              transform_rule
8                                                               pass through
9                                                          coerce to numeric
10                                                map PARMS/INITIAL to theta
11                                                         map DIST= to dist
14                        maps directly to hzr_phase(t_half=) starting value
15                            maps directly to hzr_phase(nu=) starting value
16                             maps directly to hzr_phase(m=) starting value
17 time transform B(t) = (exp(delta*t)-1)/delta absorbed into decompos shape
18                            hzr_phase('constant') with no shape parameters
   implementation_status
8            implemented
9            implemented
10               planned
11           implemented
14           implemented
15           implemented
16           implemented
17           implemented
18           implemented

The general PARMS row remains marked as planned even though the translator can parse the small early-phase subset above. Treat that as a limit on the claim: the successful example is evidence for those statements, not blanket support for every historical parameter combination. For a new analysis, write the reviewed hazard() and hzr_phase() call directly rather than carrying legacy names forward.

25.4 Import an old OUTHAZ= artifact for a bounded check

The package ships a public synthetic fixture with the row layout of an OUTHAZ= dataset. hzr_read_outhaz() reads that artifact into estimates, free/fixed status, a covariance block for free parameters, and model-structure flags. The same function can read a real .sas7bdat file when haven is installed, but the fixture keeps this recipe public and reproducible. The reader is experimental, and its return shape may change. Its _STATUS_ coding is asserted only for this synthetic fixture. A historical file that uses a different convention may populate the estimates while returning an empty covariance matrix.

outhaz_path <- system.file(
  "extdata", "outhaz-fixture.rds",
  package = "TemporalHazard"
)
legacy_fit <- hzr_read_outhaz(outhaz_path)

data.frame(
  fixture = basename(outhaz_path),
  parameter_estimates = length(legacy_fit$estimates),
  status_values = length(legacy_fit$status),
  free_parameters = nrow(legacy_fit$vcov),
  covariance_columns = ncol(legacy_fit$vcov),
  structure_flags = length(legacy_fit$flags)
)
             fixture parameter_estimates status_values free_parameters
1 outhaz-fixture.rds                  11            11               4
  covariance_columns structure_flags
1                  4               6
data.frame(
  parameter = names(legacy_fit$estimates),
  estimate = unname(legacy_fit$estimates),
  fixture_status = unname(legacy_fit$status),
  free_in_fixture = unname(legacy_fit$status == 1L)
)
   parameter    estimate fixture_status free_in_fixture
1      DELTA  0.00000000              0           FALSE
2      THALF  0.03141593              1            TRUE
3         NU  1.41421356              1            TRUE
4          M  0.00000000              0           FALSE
5        TAU  0.00000000              0           FALSE
6      GAMMA  0.00000000              0           FALSE
7      ALPHA  0.00000000              0           FALSE
8        ETA  0.00000000              0           FALSE
9         E0 -2.71828183              1            TRUE
10        C0 -3.32192809              1            TRUE
11        L0  0.00000000              0           FALSE
data.frame(
  flag = names(legacy_fit$flags),
  fixture_value = unname(legacy_fit$flags)
)
     flag fixture_value
1  G1FLAG             2
2 FIXDEL0             1
3 FIXMNU1             0
4  G3FLAG             0
5  FIXGE2             0
6 FIXGAE2             0

For this fixture, the output inventories parameter names, stored estimates, fixture-specific status codes, covariance dimensions, and imported phase flags. It does not validate the coding conventions of another OUTHAZ= artifact. Nor can it recover the original analysis rows, the original log-likelihood, deletion tallies, event-time coding, or every data transformation. It is not a refit and cannot establish that a new model is correct merely because selected numbers match.

Use the artifact once as a migration checksum. Then refit from the reviewed R analysis data, run the diagnostics in Check a hazard model, save the R model object and code under version control, and retire the legacy artifact from the active workflow.

25.5 What the maintained record should contain

Before calling the migration complete, keep these pieces together:

  • the reviewed R data-building code and a public or synthetic test example;
  • the explicit hazard() model, phase definitions, starting values, and time units;
  • any translator warnings and every manual resolution;
  • the comparison quantities chosen in advance, with tolerances;
  • the fitted R object, diagnostics, predictions, and session/package versions.

That record answers the question the old file cannot: not only whether a few stored estimates matched, but exactly how the maintained analysis was built and how we will rerun it next time.