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             5                       1                   3

Coverage reports what the parser recognized, not whether a model has been validated. For this bounded input five of the six tokens are mapped. The sixth, CONDITION=14, comes back as a row in job$untranslated, and the reason it gives is worth reading: in PROC HAZARD it stops the optimizer once the Hessian turns ill-conditioned, and hazard() has no such stop. It fits, then warns if the final Hessian is ill-conditioned. 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", t_half = 1, nu = 1, m = 1)), theta = c(log(0.2), 
        log(1), 1, 1), weights = ifelse(DEAD > 0, DEAD, 1), control = list(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 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, because 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)     (not implemented)
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; drops out at DELTA = 0, non-zero DELTA not implemented
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               planned
18           implemented

Two rows remain marked as planned. The general PARMS row is one, even though the translator can parse the small early-phase subset above. The early-phase DELTA time transform is the other: it drops out at DELTA = 0, and any other value is not implemented. Treat both as limits 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 what the old file cannot: how the maintained analysis was built and how we will rerun it next time.