diff --git a/jenner-check/.gitignore b/jenner-check/.gitignore new file mode 100644 index 0000000..75fb4a3 --- /dev/null +++ b/jenner-check/.gitignore @@ -0,0 +1,2 @@ +*_response.json +response.json diff --git a/jenner-check/README.md b/jenner-check/README.md new file mode 100644 index 0000000..58459d6 --- /dev/null +++ b/jenner-check/README.md @@ -0,0 +1,76 @@ +# jenner-check: compatibility test bundles + +Each `tNNN_/` subdirectory is a self-contained test bundle built from SAS +code that already lives in this repository. A bundle pins the output of a +captured passing run so the same script can be re-run later and compared +against that snapshot. + +This directory is fully self-contained: nothing outside `jenner-check/` is +referenced or modified, nothing runs on merge or checkout, and deleting the +directory removes every trace of it. + +## What the runner sends — read before running + +`run_jenner.sh` uploads only a bundle's SAS **source text** — `autoexec.sas` +plus `script.sas`, and nothing else — over HTTPS to `api.jenneranalytics.com`, +where it runs and returns the log and listing. It does not read or upload any +data files, so anything sitting next to a script stays on your machine; the +only thing transmitted is the code you run, as when pasting a snippet into any +hosted tool. Nothing is sent unless you run a command yourself. To point the +runner at a different endpoint, set `JENNER_HOST`. + +## What's in here + +``` +jenner-check/ +├── README.md # this file +├── run_jenner.sh # runner (bash + curl; python3 used if present) +└── tNNN_/ + ├── script.sas # the SAS under test (derived from this repo) + ├── autoexec.sas # run options prepended to script.sas at run time + ├── expected.json # fields pinned from the captured passing run + ├── meta.json # provenance: source file, blob sha, commit + └── expected/ # human-readable snapshot of that run + ├── log.txt + └── output.txt +``` + +`meta.json` records the source path, blob sha, and commit of the script each +bundle was built from, so you can verify the copy matches your code. + +## How to run + +From inside this directory: + +```bash +./run_jenner.sh --list # show the bundles in this directory +./run_jenner.sh --all # run every bundle, verify pinned fields +./run_jenner.sh tNNN_ # run just one (use a name from --list) +``` + +For each bundle the runner submits the SAS, writes the JSON response to +`_response.json`, and compares the response against the bundle's +`expected.json` — status, exit code, and the pinned log lines. A bundle +passes only when every pinned field matches, so an `N pass, 0 fail` summary +from `--all` means the captured results still hold. + +You can also compare a saved response offline, with no network call: + +```bash +./run_jenner.sh --compare tNNN__response.json tNNN_/expected.json +``` + +Requirements: `bash` 4+ and `curl` (both ship with mainstream Linux and +macOS); `python3` for the pinned-field comparison. On Windows, run it under +WSL. + +## Removing this directory + +`git rm -r jenner-check/` (or just delete the folder). Nothing else in the +repository references it. + +--- + +`run_jenner.sh` and this README are provided under the MIT license. Each +bundle's `script.sas` is a copy of code that already lives in this repository +and remains under this repository's own license — nothing here relicenses it. diff --git a/jenner-check/run_jenner.sh b/jenner-check/run_jenner.sh new file mode 100755 index 0000000..ad7825a --- /dev/null +++ b/jenner-check/run_jenner.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# run_jenner.sh - mac/linux runner for Jenner compatibility checks. +# +# Quick start: +# cd jenner-check/ +# ./run_jenner.sh # lists bundles in the current dir +# ./run_jenner.sh t001_something # run that one +# ./run_jenner.sh --all # run every bundle in the current dir +# +# Usage: ./run_jenner.sh [bundle-dir | script.sas | --all | --list] [response.json] +# ./run_jenner.sh --compare RESPONSE.json EXPECTED.json +# +# (no arg) If the current directory has tNNN_* bundles, list them +# with a copy-paste command. Otherwise show this help. +# +# --all Run every tNNN_* bundle in the current directory in +# sequence, print a pass/fail summary. +# +# --list, -l List the bundles visible in the current directory and +# exit without running anything. +# +# --compare Purely local, no network: check RESPONSE.json against the +# pinned EXPECTED.json and print one line per check. Only +# keys present in EXPECTED.json are checked (keys starting +# with "_" are provenance and ignored): status / exit_code +# must match exactly, every log_contains entry must appear +# in the response log, no log_does_not_contain entry may +# appear, output_contains entries must appear in the output, +# and pinned diagnostics lists must match exactly. Unknown +# keys print a warning and do not fail. +# Exit 0 all-match, 1 any mismatch, 2 usage/unreadable file/ +# invalid JSON, 5 python3 unavailable. +# +# bundle-dir A directory containing script.sas and (optionally) +# autoexec.sas. The two are concatenated (autoexec first, +# then a blank line, then script) and submitted together. +# This is the normal case. +# +# script.sas A single .sas file. Submitted as-is — no autoexec. +# +# The API response is written to (or response.json in +# the current directory if omitted) and the most useful fields are also +# printed to stdout for a quick sanity check. +# +# What "pass" means: a bundle run passes only if the POST returns HTTP 200 +# AND — when the bundle carries an expected.json and python3 is available — +# the fresh response matches every pinned field (the --compare semantics +# above). Without python3 the pinned comparison is skipped with a notice +# and the run falls back to the old HTTP/status-only behavior. +# +# Requires: bash 4+, curl. Both ship with every mainstream Linux distro +# and macOS 12+. python3 (present on virtually every modern system) is +# optional but needed for the pinned-result comparison and the pretty +# summary. Windows: use run_jenner.bat (single-file mode) or WSL. +# +# IMPORTANT: execute this script, don't source it. Running with `. ./...` +# or `source ./...` will short-circuit error handling and can close your +# terminal if an error path fires. + +# --- refuse to be sourced ------------------------------------------------ +# `return` only works inside a sourced script. If we ARE sourced, print a +# message and return 1 so we don't kill the parent shell with exit. If +# we're running directly, (return 0) fails and we fall through. +(return 0 2>/dev/null) && { + printf 'run_jenner.sh: execute this script, do not source it.\n ./run_jenner.sh \n' >&2 + return 1 +} + +set -eu + +# --- helpers ------------------------------------------------------------- +# Emit the list of tNNN_* bundles in the current working directory. A +# "bundle" is a directory matching t[0-9]*_* whose name contains a +# script.sas file. Writes one path per line (no prefix); empty output +# if nothing found. +list_bundles_here() { + local d + for d in ./t[0-9]*_*/ ; do + [[ -d "$d" && -f "$d/script.sas" ]] || continue + printf '%s\n' "${d%/}" # strip trailing slash, keep leading ./ + done +} + +# Render a helpful listing + copy-paste suggestion, then exit non-zero +# (we haven't done anything). Used when the user runs with no args. +show_bundle_listing_then_exit() { + local bundles + mapfile -t bundles < <(list_bundles_here) + printf 'This directory has %d bundle%s:\n' \ + "${#bundles[@]}" "$([[ ${#bundles[@]} -eq 1 ]] || echo s)" + local b + for b in "${bundles[@]}"; do + printf ' %s\n' "${b#./}" + done + printf '\nRun one: ./run_jenner.sh %s\n' "${bundles[0]#./}" + printf 'Run them all: ./run_jenner.sh --all\n' + printf 'Just list: ./run_jenner.sh --list\n' + exit 2 +} + +# Show the usage block when we have nothing better to offer. +show_usage_then_exit() { + local status=${1:-2} + { + printf 'Usage: %s [bundle-dir | script.sas | --all | --list] [response.json]\n' "$(basename "$0")" + printf ' %s --compare RESPONSE.json EXPECTED.json\n\n' "$(basename "$0")" + printf 'Examples:\n' + printf ' %s t001_my_bundle # run one bundle\n' "$(basename "$0")" + printf ' %s --all # run every tNNN_* bundle in this dir\n' "$(basename "$0")" + printf ' %s path/to/script.sas # run a single file, no autoexec\n' "$(basename "$0")" + printf ' %s --compare r.json e.json # check a response against pinned fields\n' "$(basename "$0")" + } >&2 + exit "$status" +} + +# Compare a response.json against a pinned expected.json. Purely local — +# no network. Only keys present in expected.json are checked; keys whose +# name starts with "_" are capture provenance and are ignored. Prints one +# line per check (ok/FAIL/warn). Return codes: 0 all checks match, +# 1 any mismatch, 2 unreadable file or invalid JSON, 5 python3 missing. +compare_response_to_expected() { + local resp_file=$1 expected_file=$2 + if ! command -v python3 >/dev/null 2>&1; then + printf 'error: the pinned comparison needs python3, which was not found\n' >&2 + return 5 + fi + python3 - "$resp_file" "$expected_file" <<'PY' +import json, sys + +def load(path, label): + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError) as e: + print(f"compare: cannot read {label} file {path}: {e}", file=sys.stderr) + sys.exit(2) + +resp = load(sys.argv[1], "response") +exp = load(sys.argv[2], "expected") +if not isinstance(resp, dict) or not isinstance(exp, dict): + print("compare: both files must contain a JSON object", file=sys.stderr) + sys.exit(2) + +failures = 0 + +def ok(msg): + print(f" ok {msg}") + +def bad(msg): + global failures + failures += 1 + print(f" FAIL {msg}") + +def as_list(v): + return v if isinstance(v, list) else [v] + +log = resp.get("log") or "" +output = resp.get("output") or "" + +for key, want in exp.items(): + if key.startswith("_"): + continue # provenance (_captured_at, ...), not a check + if key in ("status", "exit_code"): + got = resp.get(key) + if got == want: + ok(f"{key} == {want!r}") + else: + bad(f"{key}: expected {want!r}, got {got!r}") + elif key == "log_contains": + for s in as_list(want): + if s in log: + ok(f"log contains {s!r}") + else: + bad(f"log_contains: log is missing {s!r}") + elif key == "log_does_not_contain": + for s in as_list(want): + if s not in log: + ok(f"log does not contain {s!r}") + else: + bad(f"log_does_not_contain: log unexpectedly contains {s!r}") + elif key == "output_contains": + for s in as_list(want): + if s in output: + ok(f"output contains {s!r}") + else: + bad(f"output_contains: output is missing {s!r}") + elif key == "diagnostics": + diag = resp.get("diagnostics") or {} + for dk, dv in (want or {}).items(): + if dk.startswith("_"): + continue + got = diag.get(dk) + if got == dv: + ok(f"diagnostics.{dk} == {dv!r}") + else: + bad(f"diagnostics.{dk}: expected {dv!r}, got {got!r}") + else: + print(f" warn unknown expected.json key {key!r} — not checked") + +if failures: + print(f"pinned comparison: {failures} check(s) FAILED") + sys.exit(1) +print("pinned comparison: all pinned checks passed") +PY +} + +# --- arg parsing --------------------------------------------------------- +if [[ $# -lt 1 ]]; then + # No args: if the cwd contains bundles, list them; otherwise show help. + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -gt 0 ]]; then + show_bundle_listing_then_exit + fi + show_usage_then_exit 2 +fi + +HOST=${JENNER_HOST:-api.jenneranalytics.com} + +case "$1" in + -h|--help) + show_usage_then_exit 0 + ;; + -l|--list) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" + exit 0 + fi + printf 'Bundles in %s:\n' "$(pwd)" + for b in "${_found[@]}"; do + printf ' %s\n' "${b#./}" + done + exit 0 + ;; + --compare) + # Purely local: diff a response.json against a pinned expected.json. + if [[ $# -ne 3 ]]; then + printf 'usage: %s --compare RESPONSE.json EXPECTED.json\n' "$(basename "$0")" >&2 + exit 2 + fi + _rc=0 + compare_response_to_expected "$2" "$3" || _rc=$? + exit "$_rc" + ;; + --all) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" >&2 + exit 3 + fi + _pass=0; _fail=0 + for b in "${_found[@]}"; do + printf '\n── %s ──\n' "${b#./}" + if "$0" "$b" "${b#./}_response.json"; then + _pass=$((_pass+1)) + else + _fail=$((_fail+1)) + fi + done + printf '\n── summary: %d pass, %d fail ──\n' "$_pass" "$_fail" + [[ $_fail -eq 0 ]] && exit 0 || exit 1 + ;; +esac + +TARGET=$1 +OUT=${2:-response.json} + +# --- assemble the submission body --------------------------------------- +# If TARGET is a directory, treat it as a bundle. If it's a file, submit +# it directly. +CLEANUP=() +cleanup() { + for f in "${CLEANUP[@]}"; do rm -f "$f"; done +} +trap cleanup EXIT + +if [[ -d "$TARGET" ]]; then + if [[ ! -f "$TARGET/script.sas" ]]; then + printf 'error: %s is a directory but has no script.sas\n' "$TARGET" >&2 + exit 3 + fi + SUBMIT=$(mktemp -t jc_submit.XXXXXX.sas) + CLEANUP+=("$SUBMIT") + if [[ -f "$TARGET/autoexec.sas" ]]; then + cat "$TARGET/autoexec.sas" > "$SUBMIT" + printf '\n' >> "$SUBMIT" + fi + cat "$TARGET/script.sas" >> "$SUBMIT" + printf 'Submitting bundle: %s\n' "$TARGET" + if [[ -f "$TARGET/autoexec.sas" ]]; then + printf ' autoexec.sas (%d bytes) + script.sas (%d bytes)\n' \ + "$(wc -c < "$TARGET/autoexec.sas")" "$(wc -c < "$TARGET/script.sas")" + else + printf ' script.sas (%d bytes), no autoexec\n' "$(wc -c < "$TARGET/script.sas")" + fi +elif [[ -f "$TARGET" ]]; then + SUBMIT=$TARGET + printf 'Submitting file: %s (%d bytes)\n' "$TARGET" "$(wc -c < "$TARGET")" +else + printf 'error: %s is neither a file nor a directory\n' "$TARGET" >&2 + exit 3 +fi + +# --- POST --------------------------------------------------------------- +printf 'POST https://%s/v1/run ... ' "$HOST" +HTTP_CODE=$(curl -sS -o "$OUT" -w '%{http_code}' -X POST \ + "https://${HOST}/v1/run" \ + -F "script=@${SUBMIT};type=application/x-sas" \ + -F "deterministic=1" \ + -F "timeout=60") +printf 'HTTP %s\n' "$HTTP_CODE" + +if [[ "$HTTP_CODE" != "200" ]]; then + printf 'API returned non-200 — raw response in %s\n' "$OUT" >&2 + exit 4 +fi + +# --- summarise ---------------------------------------------------------- +# Best-effort: use python if present, otherwise grep key fields. +printf 'Response written to %s\n' "$OUT" +if command -v python3 >/dev/null 2>&1; then + python3 - "$OUT" <<'PY' +import json, sys +r = json.load(open(sys.argv[1])) +print(f" status : {r.get('status')}") +print(f" exit_code : {r.get('exit_code')}") +print(f" duration_ms: {r.get('duration_ms')}") +print(f" run_id : {r.get('run_id')}") +print(f" jenner_ver : {r.get('jenner_version')}") +log = r.get('log', '') +if log: + print(' log (first 10 lines):') + for line in log.splitlines()[:10]: + print(f' {line}') +PY +else + printf ' (install python3 for a pretty summary; raw JSON in %s)\n' "$OUT" +fi + +# --- pinned-result comparison --------------------------------------------- +# A bundle that ships an expected.json only passes if the fresh response +# matches every pinned field — HTTP 200 alone is not a pass. Without +# python3 we can't parse JSON portably, so fall back to the old behavior +# and say so. +if [[ -d "$TARGET" && -f "$TARGET/expected.json" ]]; then + if command -v python3 >/dev/null 2>&1; then + printf 'Comparing response against pinned %s/expected.json\n' "${TARGET%/}" + _rc=0 + compare_response_to_expected "$OUT" "$TARGET/expected.json" || _rc=$? + if [[ $_rc -ne 0 ]]; then + printf 'Bundle FAILED the pinned comparison (see lines above)\n' >&2 + exit "$_rc" + fi + else + printf 'pinned comparison skipped (python3 not found) — HTTP/status only\n' + fi +fi diff --git a/jenner-check/t001_merge2sets/autoexec.sas b/jenner-check/t001_merge2sets/autoexec.sas new file mode 100644 index 0000000..89310d3 --- /dev/null +++ b/jenner-check/t001_merge2sets/autoexec.sas @@ -0,0 +1,3 @@ +/* cap input rows for the captured run */ +options obs=100; +options nocenter nodate; diff --git a/jenner-check/t001_merge2sets/expected.json b/jenner-check/t001_merge2sets/expected.json new file mode 100644 index 0000000..37d8a6e --- /dev/null +++ b/jenner-check/t001_merge2sets/expected.json @@ -0,0 +1,18 @@ +{ + "_captured_at": "2026-07-14T11:25:44Z", + "_captured_run_id": "r_dae8ed006a19477c8c051ca75d81abe7", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote merged (5 rows, 5 columns).", + "NOTE: PROC PRINT completed: 5 observations printed, 5 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t001_merge2sets/expected/log.txt b/jenner-check/t001_merge2sets/expected/log.txt new file mode 100644 index 0000000..09e5aed --- /dev/null +++ b/jenner-check/t001_merge2sets/expected/log.txt @@ -0,0 +1,49 @@ +NOTE: Jenner v1.5.30 (build v1.5.30+unknown.20260714T020019Z.x86_64-unknown-linux-gnu) + +NOTE: Option OBS changed to 100. +NOTE: DATA demog + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote demog (5 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA cog + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote cog (5 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SORT data=demog + +NOTE: Read 5 rows from demog. +NOTE: Wrote demog (5 rows, 3 columns). +NOTE: PROC SORT statement used. +NOTE: PROC SORT data=cog + +NOTE: Read 5 rows from cog. +NOTE: Wrote cog (5 rows, 3 columns). +NOTE: PROC SORT statement used. +NOTE: DATA merged + +NOTE: Stream 1 processed 5 rows, max BY-group size: 1 (O(1) memory verified) +NOTE: Stream 2 processed 5 rows, max BY-group size: 1 (O(1) memory verified) + +NOTE: Wrote merged (5 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=merged + +NOTE: PROC PRINT completed: 5 observations printed, 5 variables +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. +NOTE: PROC FREQ +NOTE: ODS plot written: freq_hacohort.spec.json +NOTE: ODS plot written: freq_r3proxy.spec.json +NOTE: PROC FREQ statement used. diff --git a/jenner-check/t001_merge2sets/expected/output.txt b/jenner-check/t001_merge2sets/expected/output.txt new file mode 100644 index 0000000..541ba70 --- /dev/null +++ b/jenner-check/t001_merge2sets/expected/output.txt @@ -0,0 +1,31 @@ + + Obs hhidpn hacohort racohbyr r3cogtot r3proxy +----- ------ -------- -------- -------- ------- + 1 1001 1 1930 22 0 + 2 1002 3 1942 15 1 + 3 1003 0 1925 30 0 + 4 1004 1 1938 8 1 + 5 1005 3 1951 . . + + The MEANS Procedure + + Variable N N Miss Minimum Maximum Mean + ----------------------------------------------------------------------- + r3cogtot 4 1 8.0000000 30.0000000 18.7500000 + ----------------------------------------------------------------------- + + The FREQ Procedure + + Cumulative Cumulative +hacohort Frequency Percent Frequency Percent +----------------------------------------------------------------- +0 1 20.00 1 20.00 +1 2 40.00 3 60.00 +3 2 40.00 5 100.00 + + Cumulative Cumulative +r3proxy Frequency Percent Frequency Percent +---------------------------------------------------------------- + 1 20.00 1 20.00 +0 2 40.00 3 60.00 +1 2 40.00 5 100.00 diff --git a/jenner-check/t001_merge2sets/meta.json b/jenner-check/t001_merge2sets/meta.json new file mode 100644 index 0000000..c0471d1 --- /dev/null +++ b/jenner-check/t001_merge2sets/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t001_merge2sets", + "source_file": "cogvars_gdr_20251107.sas", + "source_blob_sha": "261859d16d1ff9756f22705ebd4b7e841e83ceef", + "source_commit": "003ce8910307caae253eb3700df98f7b0270354b", + "notes": "%merge2sets macro (Irena Cenzer section of cogvars_gdr_20251107.sas) reproduced verbatim; exercised with two small mock datasets keyed by hhidpn to run the sort/keeplist/merge/ifstmt logic. PROC PRINT/MEANS/FREQ confirm the merge." +} diff --git a/jenner-check/t001_merge2sets/script.sas b/jenner-check/t001_merge2sets/script.sas new file mode 100644 index 0000000..85fcdb1 --- /dev/null +++ b/jenner-check/t001_merge2sets/script.sas @@ -0,0 +1,60 @@ +/* + %merge2sets macro, reproduced verbatim from cogvars_gdr_20251107.sas + (macro section from Irena Cenzer). It sorts two datasets by a set of + by-variables, optionally applies a keep-list to each, merges them, and + optionally subsets with an if-statement. + + This bundle exercises the macro against two small mock datasets so the + macro logic (keeplist expansion, sort, merge, ifstmt) runs unchanged. + The two mock sources are defined inline below so this script is + self-contained and can be posted on its own. +*/ + +/* Two small mock source datasets keyed by hhidpn, standing in for the + RAND and cognition-imputation extracts the upstream program merges. */ +data demog; + input hhidpn hacohort racohbyr; + datalines; +1001 1 1930 +1002 3 1942 +1003 0 1925 +1004 1 1938 +1005 3 1951 +; +run; + +data cog; + input hhidpn r3cogtot r3proxy; + datalines; +1001 22 0 +1002 15 1 +1003 30 0 +1004 8 1 +1006 27 0 +; +run; + +%macro merge2sets(destdata, srcdata1, keeplist1, srcdata2, keeplist2, byvars, ifstmt); +proc sort data = &srcdata1; + by &byvars; +run; +proc sort data = &srcdata2; + by &byvars; +run; +%if %length(&keeplist1) > 0 %then %let srcdata1 = %str(&srcdata1%str((keep = &keeplist1))); +%if %length(&keeplist2) > 0 %then %let srcdata2 = %str(&srcdata2%str((keep = &keeplist2))); +data &destdata; +merge &srcdata1 &srcdata2; + by &byvars; + %if %length(&ifstmt) > 0 %then %do; + if &ifstmt; + %end; +run; +%mend merge2sets; + +/* merge2sets(destdata, srcdata1, keeplist1, srcdata2, keeplist2, byvars, ifstmt) */ +%merge2sets(merged, demog, hhidpn hacohort racohbyr, cog, hhidpn r3cogtot r3proxy, hhidpn, %str(hacohort ne .)); + +proc print data=merged; run; +proc means data=merged n nmiss min max mean; var r3cogtot; run; +proc freq data=merged; tables hacohort r3proxy / missing; run; diff --git a/jenner-check/t002_proxy_iqcode/autoexec.sas b/jenner-check/t002_proxy_iqcode/autoexec.sas new file mode 100644 index 0000000..38d9d1c --- /dev/null +++ b/jenner-check/t002_proxy_iqcode/autoexec.sas @@ -0,0 +1,41 @@ +/* cap input rows for the captured run */ +options obs=100; +options nocenter nodate; + +/* + Mock proxyvars dataset standing in for the merged fat-file extract the + upstream program feeds into %proxy. It carries the wave-3 (1995) driver + columns R3IWSTAT / R3PROXY and the 48 Jorm-IQCODE item columns + D1072..D1143 that %proxy(w=3, year=95, ...) reads. + + Each row seeds the 16 "landscape" items (D1072, D1077, ...) with a + base response 1/2/3, and fills the paired "improved"/"declined" follow-up + items so the macro's improve/decline branches both get exercised. +*/ +data proxyvars; + /* driver + the 16 landscape items */ + input hhidpn r3iwstat r3proxy + d1072 d1077 d1082 d1087 d1092 d1097 d1102 d1107 + d1112 d1117 d1122 d1127 d1132 d1135 d1138 d1141; + + array ls[16] d1072 d1077 d1082 d1087 d1092 d1097 d1102 d1107 + d1112 d1117 d1122 d1127 d1132 d1135 d1138 d1141; + array im[16] d1073 d1078 d1083 d1088 d1093 d1098 d1103 d1108 + d1113 d1118 d1123 d1128 d1133 d1136 d1139 d1142; + array de[16] d1074 d1079 d1084 d1089 d1094 d1099 d1104 d1109 + d1114 d1119 d1124 d1129 d1134 d1137 d1140 d1143; + do j = 1 to 16; + /* "improved" follow-up: 1.much improved, 2.a bit improved */ + im[j] = 1 + mod(j, 2); + /* "declined" follow-up: 4.a bit worse, 5.much worse */ + de[j] = 4 + mod(j, 2); + end; + drop j; + datalines; +1001 1 1 1 2 3 2 3 1 2 3 2 1 3 2 1 3 2 3 +1002 1 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +1003 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1004 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 +1005 1 1 1 3 1 3 1 3 1 3 1 3 1 3 1 3 1 3 +; +run; diff --git a/jenner-check/t002_proxy_iqcode/expected.json b/jenner-check/t002_proxy_iqcode/expected.json new file mode 100644 index 0000000..93c43bc --- /dev/null +++ b/jenner-check/t002_proxy_iqcode/expected.json @@ -0,0 +1,18 @@ +{ + "_captured_at": "2026-07-14T11:07:56Z", + "_captured_run_id": "r_32bbe120de6d4e52a293cdcc392e4c34", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote proxyvars (5 rows, 56 columns).", + "NOTE: PROC PRINT completed: 5 observations printed, 4 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t002_proxy_iqcode/expected/log.txt b/jenner-check/t002_proxy_iqcode/expected/log.txt new file mode 100644 index 0000000..56e5e31 --- /dev/null +++ b/jenner-check/t002_proxy_iqcode/expected/log.txt @@ -0,0 +1,25 @@ +NOTE: Jenner v1.5.30 (build v1.5.30+unknown.20260714T020019Z.x86_64-unknown-linux-gnu) + +NOTE: Option OBS changed to 100. +NOTE: DATA proxyvars + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote proxyvars (5 rows, 51 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA proxyvars + + +NOTE: Read 5 rows from proxyvars. +NOTE: Wrote proxyvars (5 rows, 56 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. +NOTE: PROC PRINT data=proxyvars + +NOTE: PROC PRINT completed: 5 observations printed, 4 variables diff --git a/jenner-check/t002_proxy_iqcode/expected/output.txt b/jenner-check/t002_proxy_iqcode/expected/output.txt new file mode 100644 index 0000000..7383048 --- /dev/null +++ b/jenner-check/t002_proxy_iqcode/expected/output.txt @@ -0,0 +1,15 @@ + The MEANS Procedure + + Variable Label N Mean Std Dev Minimum Maximum + --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + iqcode95 JORM IQCODE: AVERAGE SCORE - year 95: 1.much improved, 2. a bit improved, 3.not much changed, 4. a bit worse, 5.much worse 5 3.0375000 1.0639696 1.5000000 4.5000000 + --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + Obs hhidpn r3proxy iqcode95 IQCodeD95 +----- ------ ------- -------- --------- + 1 1001 1 3.1875 0 + 2 1002 1 4.5 1 + 3 1003 1 1.5 0 + 4 1004 1 3 0 + 5 1005 1 3 0 diff --git a/jenner-check/t002_proxy_iqcode/meta.json b/jenner-check/t002_proxy_iqcode/meta.json new file mode 100644 index 0000000..ac3c13f --- /dev/null +++ b/jenner-check/t002_proxy_iqcode/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t002_proxy_iqcode", + "source_file": "cogvars_gdr_20251107.sas", + "source_blob_sha": "261859d16d1ff9756f22705ebd4b7e841e83ceef", + "source_commit": "003ce8910307caae253eb3700df98f7b0270354b", + "notes": "%proxy macro (Jorm IQCODE derivation, adapted from Maria Glymour code) reproduced verbatim from cogvars_gdr_20251107.sas; called with w=3/year=95 as upstream does, against a mock proxyvars carrying R3IWSTAT/R3PROXY and the 48 D-item columns. PROC MEANS/PRINT confirm the 16-item averaged IQCODE and the >=3.5 dementia flag." +} diff --git a/jenner-check/t002_proxy_iqcode/script.sas b/jenner-check/t002_proxy_iqcode/script.sas new file mode 100644 index 0000000..a6e91ef --- /dev/null +++ b/jenner-check/t002_proxy_iqcode/script.sas @@ -0,0 +1,70 @@ +/* + %proxy macro, reproduced verbatim from cogvars_gdr_20251107.sas. + It derives the Jorm IQCODE (Informant Questionnaire for Cognitive Decline) + from proxy-interview items for one HRS wave: for each of 16 items it takes + the "landscape" response, follows the improved/declined branch, treats + refusals/DKs as "closest to unchanged", averages the 16 items, and flags + IQCODE >= 3.5 as probable dementia. + + This bundle calls the macro exactly as the upstream program does for + wave 3 / year 1995, against the mock proxyvars dataset in autoexec.sas. +*/ + +%macro proxy (w=, year=, iqcodels=, iqcodeim=, iqcodede=) ; + +data proxyvars (drop=tempsum); + set proxyvars; + + /* Jorm IQCode for proxy interviews + treat item missings as 3 "unchanged" or if proxy has already indicated things have gotten better or worse, interpret as value closest to staying the same + */ + + if R&w.IWSTAT=1 and R&w.PROXY=1 then do; /*RwIWSTAT=1.Resp, alive RwPROXY=1.proxy */ + + /* Question is:" Compared with two years ago, how is R at: Remembering things about family and friends, such as occupations, birthdays, and + addresses. Has this 1.improved, 2.not much changed, 3.gotten worse, 4. R doesn't do this/ DOES NOT APPLY/R DOESN'T DO ACTIVITY, 7. other, 8.dk, 9.rf" + */ + array iqcodels [16] &iqcodels; + + * If improved, then asked "Is it much improved or a bit improved?": 1.much improved, 2.a bit improved, 7. other, 8.dk, 9.rf; + array iqcodeim [16] &iqcodeim; + + * If gotten worse, then asked "Is it much worse or a bit worse?": 4.a bit worse,5.much worse 7. other, 8.dk, 9.rf; + array iqcodede [16] &iqcodede; + + IQcode&year=0; + iqcode&year.ms=0; + do i=1 to 16; + tempsum=iqcodels[i]; + if iqcodeim[i] in (7,8,9) then iqcodeim[i]=2; /*GDR: 2.a bit improved: interpret as value closest to staying the same, i.e "a bit improved" instead of "much improved"*/ + if iqcodede[i] in (7,8,9) then iqcodede[i]=4; /*GDR: 4.a bit worse: interpret as value closest to staying the same, i.e. "a bit worse" instead of "much worse"*/ + if iqcodels[i]=1 then tempsum=iqcodeim[i]; *improved; /*GDR: 1.much improved, 2.a bit improved*/ + else if iqcodels[i]=3 then tempsum=iqcodede[i]; *declined; /*GDR: 4.a bit worse,5.much worse*/ + else if iqcodels[i]=2 then tempsum=3; *stayed same; + if tempsum in (7,8,9) then do; + iqcode&year.ms=iqcode&year.ms+1; + tempsum=0; + end; + iqcode&year=sum(iqcode&year,tempsum); + end; drop i; + + if iqcode&year.ms > 3 or iqcode&year=0 then iqcode&year=.; /*if the number of missing is >3 or the iqcode&year.=0 because every question was missing then iqcode&year.=.*/ + else iqcode&year=iqcode&year/(16-iqcode&year.ms); + + if iqcode&year >= 3.5 then IQCodeD&year=1; /*GDR: IQCODE range:1.excellent to 5.poor. So >=3.5 means that participants had dementia=1*/ + else if 0<=iqcode&year<3.5 then IQCodeD&year=0; + label iqcode&year="JORM IQCODE: AVERAGE SCORE - year &year: 1.much improved, 2. a bit improved, 3.not much changed, 4. a bit worse, 5.much worse"; + + end; + +run; + +%mend proxy; + +%proxy(w=3, year=95, + iqcodels=D1072 D1077 D1082 D1087 D1092 D1097 D1102 D1107 D1112 D1117 D1122 D1127 D1132 D1135 D1138 D1141, + iqcodeim=D1073 D1078 D1083 D1088 D1093 D1098 D1103 D1108 D1113 D1118 D1123 D1128 D1133 D1136 D1139 D1142, + iqcodede=D1074 D1079 D1084 D1089 D1094 D1099 D1104 D1109 D1114 D1119 D1124 D1129 D1134 D1137 D1140 D1143) +proc means data=proxyvars; var IQCode95; run; + +proc print data=proxyvars; var hhidpn r3proxy IQCode95 IQCodeD95; run; diff --git a/jenner-check/t003_frailtyscore/autoexec.sas b/jenner-check/t003_frailtyscore/autoexec.sas new file mode 100644 index 0000000..cb8e733 --- /dev/null +++ b/jenner-check/t003_frailtyscore/autoexec.sas @@ -0,0 +1,26 @@ +/* cap input rows for the captured run */ +options obs=100; +options nocenter nodate; + +/* + Mock temp13 standing in for the HRS extract the frailty program consumes. + The upstream DATA step first maps wave-specific HRS columns into the + derived pre_/post_ frailty inputs (dizz, falls, lifting, weight, cognition, + sensory, proxy). Here we supply those derived inputs directly and set + pre_wavenum / post_wavenum outside the mapped wave range (0), so the + wave-mapping loops are inert and the frailty-domain and frailty-score + logic runs unchanged on the values below. +*/ +data temp13; + input pre_wavenum post_wavenum HACOHORT core_within3yrafter + dizz0 fallsnum0 falls0 lifting0 weight0 cogtot0 proxymem0 iwrate0 sight0 hearing0 proxy0 + dizz1 fallsnum1 falls1 lifting1 weight1 cogtot1 proxymem1 iwrate1 sight1 hearing1 proxy1 + bmi0; + datalines; +0 0 1 1 1 3 1 1 80 8 4 3 5 4 0 0 1 0 0 74 20 2 1 2 2 0 17.2 +0 0 1 1 0 0 0 0 70 22 1 1 1 1 0 0 0 0 0 69 24 1 1 1 1 0 24.5 +0 0 1 1 0 2 1 0 90 15 3 2 3 3 0 1 3 1 1 82 12 4 3 4 3 0 27.0 +0 0 3 1 1 0 0 1 65 30 1 1 2 2 1 0 0 0 0 66 30 1 1 2 2 1 19.1 +0 0 1 0 0 1 1 0 78 11 2 2 4 5 0 . . . . . . . . . . . 22.3 +; +run; diff --git a/jenner-check/t003_frailtyscore/expected.json b/jenner-check/t003_frailtyscore/expected.json new file mode 100644 index 0000000..951bbd6 --- /dev/null +++ b/jenner-check/t003_frailtyscore/expected.json @@ -0,0 +1,18 @@ +{ + "_captured_at": "2026-07-14T11:10:02Z", + "_captured_run_id": "r_638de93e8a9a4e41bbb3db741fb7dca8", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote temp14 (5 rows, 164 columns).", + "NOTE: PROC PRINT completed: 5 observations printed, 8 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t003_frailtyscore/expected/log.txt b/jenner-check/t003_frailtyscore/expected/log.txt new file mode 100644 index 0000000..f1cf37f --- /dev/null +++ b/jenner-check/t003_frailtyscore/expected/log.txt @@ -0,0 +1,29 @@ +NOTE: Jenner v1.5.30 (build v1.5.30+unknown.20260714T020019Z.x86_64-unknown-linux-gnu) + +NOTE: Option OBS changed to 100. +NOTE: DATA temp13 + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote temp13 (5 rows, 27 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA temp14 + + +NOTE: Read 5 rows from temp13. +NOTE: Wrote temp14 (5 rows, 164 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=temp14 + +NOTE: PROC PRINT completed: 5 observations printed, 8 variables +NOTE: PROC FREQ +NOTE: ODS plot written: freq_frailstatus0.spec.json +NOTE: ODS plot written: freq_frailscore0.spec.json +NOTE: PROC FREQ statement used. +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t003_frailtyscore/expected/output.txt b/jenner-check/t003_frailtyscore/expected/output.txt new file mode 100644 index 0000000..6cbfcbf --- /dev/null +++ b/jenner-check/t003_frailtyscore/expected/output.txt @@ -0,0 +1,29 @@ + + Obs physical0 nutritive0 cognitive0 sensory0 domainmiss0 frailscore0 frailstatus0 frailscore4g0 +----- --------- ---------- ---------- -------- ----------- ----------- ------------ ------------- + 1 1 1 1 1 0 4 1 3 + 2 0 0 0 0 0 0 0 0 + 3 1 0 0 0 0 1 0 1 + 4 1 0 0 0 0 1 0 1 + 5 0 . 0 1 1 1 0 1 + + The FREQ Procedure + + Cumulative Cumulative +R frailty status pre procedure. 0.no frail, 1.frail Frequency Percent Frequency Percent +------------------------------------------------------------------------------------------------------------ +0 4 80.00 4 80.00 +1 1 20.00 5 100.00 + + Cumulative Cumulative +R frailty score pre procedure:0-4 Frequency Percent Frequency Percent +------------------------------------------------------------------------------------------ +0 1 20.00 1 20.00 +1 3 60.00 4 80.00 +4 1 20.00 5 100.00 + The MEANS Procedure + + Variable Label N N Miss Minimum Maximum Mean + ------------------------------------------------------------------------------------------------------------- + frailscore0 R frailty score pre procedure:0-4 5 0 0.0000000 4.0000000 1.4000000 + ------------------------------------------------------------------------------------------------------------- diff --git a/jenner-check/t003_frailtyscore/meta.json b/jenner-check/t003_frailtyscore/meta.json new file mode 100644 index 0000000..3573be4 --- /dev/null +++ b/jenner-check/t003_frailtyscore/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t003_frailtyscore", + "source_file": "frailtyscore_gdr_20200729.sas", + "source_blob_sha": "a375e10c84787d34bad090d329b153a256f6d750", + "source_commit": "003ce8910307caae253eb3700df98f7b0270354b", + "notes": "Frailty-score DATA step (Cigolle et al. 2009 definition) reproduced verbatim from frailtyscore_gdr_20200729.sas. temp13 mock supplies the derived pre-/post-procedure frailty inputs directly with wave numbers set outside the mapped range so the domain (physical/nutritive/cognitive/sensory) and 0-4 frailty score / status logic runs unchanged. PROC PRINT/FREQ/MEANS confirm the scoring." +} diff --git a/jenner-check/t003_frailtyscore/script.sas b/jenner-check/t003_frailtyscore/script.sas new file mode 100644 index 0000000..8e43d76 --- /dev/null +++ b/jenner-check/t003_frailtyscore/script.sas @@ -0,0 +1,150 @@ +/* + Frailty-score derivation, reproduced from frailtyscore_gdr_20200729.sas + (Cigolle et al. 2009 definition). The DATA step maps wave-specific HRS + columns into pre-/post-procedure frailty inputs, recodes special missing + values, builds the physical / nutritive / cognitive / sensory domains, and + sums them into a 0-4 frailty score with a frail/not-frail status flag. + + temp13 (in autoexec.sas) supplies the derived inputs directly with the + wave numbers set outside the mapped range, so the domain and scoring logic + below runs unchanged. +*/ + +data temp14 (drop=filler); + set temp13; + filler=.; + + array pre_vars [11] dizz0 fallsnum0 falls0 lifting0 weight0 cogtot0 proxymem0 iwrate0 sight0 hearing0 proxy0; + array post_vars [11] dizz1 fallsnum1 falls1 lifting1 weight1 cogtot1 proxymem1 iwrate1 sight1 hearing1 proxy1; + + array wave3a [11] D967 D879 D878 R3LIFTA R3WEIGHT R3COGTOT D1056 filler D900 D908 R3PROXY; + array wave3h [11] E969 E879 E878 R3LIFTA R3WEIGHT R3COGTOT E1056 filler E900 E908 R3PROXY; + array wave4 [11] F1306 F1207 F1206 R4LIFTA R4WEIGHT R4COGTOT F1373 filler F1228 F1236 R4PROXY; + array wave5 [11] G1439 G1340 G1339 R5LIFTA R5WEIGHT R5COGTOT G1527 g517 G1361 G1369 R5PROXY; + array wave6 [11] HC145 HC080 HC079 R6LIFTA R6WEIGHT R6COGTOT HD501 ha011 HC095 HC103 R6PROXY; + array wave7 [11] JC145 JC080 JC079 R7LIFTA R7WEIGHT R7COGTOT JD501 ja011 JC095 JC103 R7PROXY; + array wave8 [11] KC145 KC080 KC079 R8LIFTA R8WEIGHT R8COGTOT KD501 ka011 KC095 KC103 R8PROXY; + array wave9 [11] LC145 LC080 LC079 R9LIFTA R9WEIGHT R9COGTOT LD501 la011 LC095 LC103 R9PROXY; + array wave10 [11] MC145 MC080 MC079 R10LIFTA R10WEIGHT R10COGTOT MD501 ma011 MC095 MC103 R10PROXY; + array wave11 [11] NC145 NC080 NC079 R11LIFTA R11WEIGHT R11COGTOT ND501 na011 NC095 NC103 R11PROXY; + array wave12 [11] OC145 OC080 OC079 R12LIFTA R12WEIGHT R12COGTOT OD501 oa011 OC095 OC103 R12PROXY; + array wave13 [11] PC145 PC080 PC079 R13LIFTA R13WEIGHT filler PD501 pa011 PC095 PC103 R13PROXY; + + do i = 1 to 11; + if pre_wavenum = 3 and HACOHORT in (0,1) then pre_vars{i} = wave3a{i}; + else if pre_wavenum = 3 and HACOHORT=3 then pre_vars{i} = wave3h{i}; + else if pre_wavenum = 4 then pre_vars{i} = wave4{i}; + else if pre_wavenum = 5 then pre_vars{i} = wave5{i}; + else if pre_wavenum = 6 then pre_vars{i} = wave6{i}; + else if pre_wavenum = 7 then pre_vars{i} = wave7{i}; + else if pre_wavenum = 8 then pre_vars{i} = wave8{i}; + else if pre_wavenum = 9 then pre_vars{i} = wave9{i}; + else if pre_wavenum = 10 then pre_vars{i} = wave10{i}; + else if pre_wavenum = 11 then pre_vars{i} = wave11{i}; + else if pre_wavenum = 12 then pre_vars{i} = wave12{i}; + end; drop i; + + if core_within3yrafter=1 then do i = 1 to 11; + if post_wavenum = 4 then post_vars{i} = wave4{i}; + else if post_wavenum = 5 then post_vars{i} = wave5{i}; + else if post_wavenum = 6 then post_vars{i} = wave6{i}; + else if post_wavenum = 7 then post_vars{i} = wave7{i}; + else if post_wavenum = 8 then post_vars{i} = wave8{i}; + else if post_wavenum = 9 then post_vars{i} = wave9{i}; + else if post_wavenum = 10 then post_vars{i} = wave10{i}; + else if post_wavenum = 11 then post_vars{i} = wave11{i}; + else if post_wavenum = 12 then post_vars{i} = wave12{i}; + else if post_wavenum = 13 then post_vars{i} = wave13{i}; + end; drop i; + + array olvar[6] dizz0 dizz1 falls0 falls1 lifting0 lifting1; + do i=1 to 6; + if olvar[i]=5 then olvar[i]=0; + else if olvar[i] in (7,8,9, .R, .S, .X, .D) then olvar[i]=.; + end; drop i; + + array olvar2[8] proxymem0 proxymem1 iwrate0 iwrate1 sight0 sight1 hearing0 hearing1; + do i=1 to 8; + if olvar2[i] in (7,8,9, .R, .D) then olvar2[i]=.; + end;drop i; + + *Physical domain; + if dizz0=. then do; + if pre_wavenum = 4 then dizz0=E969; + else if pre_wavenum = 5 then dizz0=F1306; + else if pre_wavenum = 6 then dizz0=G1439; + else if pre_wavenum = 7 then dizz0=HC145; + else if pre_wavenum = 8 then dizz0=JC145; + else if pre_wavenum = 9 then dizz0=KC145; + else if pre_wavenum = 10 then dizz0=LC145; + else if pre_wavenum = 11 then dizz0=MC145; + else if pre_wavenum = 12 then dizz0=NC145; + end; + + if dizz0=5 then dizz0=0; + else if dizz0 in (7,8,9, .R, .S, .X, .D) then dizz0=.; + + if falls0=0 then fallsnum0=0; + if fallsnum0 in (997, 998, 98, 999, 99) then fallsnum0=.; + if pre_wavenum = 3 and HACOHORT in (0,1) and fallsnum0>20 then fallsnum0=.; + + if falls1=0 then fallsnum1=0; + if fallsnum1 in (997, 998, 98, 999, 99) then fallsnum1=.; + if pre_wavenum = 3 and HACOHORT in (0,1) and fallsnum1>20 then fallsnum1=.; + + if dizz0=1 or fallsnum0>=2 or lifting0=1 then physical0=1; + else if dizz0=0 and 0<=fallsnum0<2 and lifting0=0 then physical0=0; + + if dizz1=1 or fallsnum1>=2 or lifting1=1 then physical1=1; + else if dizz1=0 and 0<=fallsnum1<2 and lifting1=0 then physical1=0; + label physical0='R had problem in physical dom pre procedure. 0.no, 1.yes' + physical1='R had problem in physical dom post procedure. 0.no, 1.yes'; + + *Nutritive domain; + if (((weight0-weight1)/weight0)*100) >=10 then weightdom=1; + else if ((weight0-weight1)/weight0) ne . and (((weight0-weight1)/weight0)*100)<10 then weightdom=0; + label weightdom='R % decrease in weight from pre to post procedure is >=10%. 0.no, 1.yes'; + if weightdom=1 or 0<=bmi0<18.5 then nutritive0=1; + else if weightdom=0 and bmi0>=18.5 then nutritive0=0; + label nutritive0='R had problem in nutritive dom pre procedure. 0.no, 1.yes'; + + *Cognitive domain; + if cogtot0>10 then cognitive0=0; + else if 0<=cogtot0<=10 or proxymem0 in (4,5) or iwrate0 in (3,4) then cognitive0=1; + else if proxymem0 in (1,2,3) or iwrate0 in (1,2) then cognitive0=0; + + if cogtot1>10 then cognitive1=0; + else if 0<=cogtot1<=10 or proxymem1 in (4,5) or iwrate1 in (3,4) then cognitive1=1; + else if proxymem1 in (1,2,3) or iwrate1 in (1,2) then cognitive1=0; + label cognitive0='R had problem in cognitive dom pre procedure. 0.no, 1.yes' + cognitive1='R had problem in cognitive dom post procedure. 0.no, 1.yes'; + + *Sensory domain; + if sight0 in (4,5,6) or hearing0 in (4,5) then sensory0=1; + else if sight0 in (1,2,3) and hearing0 in (1,2,3) then sensory0=0; + + if sight1 in (4,5,6) or hearing1 in (4,5) then sensory1=1; + else if sight1 in (1,2,3) and hearing1 in (1,2,3) then sensory1=0; + label sensory0='R had problem in sensory dom pre procedure. 0.no, 1.yes' + sensory1='R had problem in sensory dom post procedure. 0.no, 1.yes'; + + *Frailty score; + domainmiss0=nmiss(physical0, nutritive0, cognitive0,sensory0); + frailscore0=sum(physical0, nutritive0, cognitive0,sensory0); + if frailscore0 in (0,1) and domainmiss0 in (2,3) then frailscore0=.; + label frailscore0='R frailty score pre procedure:0-4' + domainmiss0='R number of frailty domains with missing value:0-4'; + if frailscore0>=2 then frailstatus0=1; + else if 0<=frailscore0<2 then frailstatus0=0; + label frailstatus0='R frailty status pre procedure. 0.no frail, 1.frail'; + + frailscore4g0=frailscore0; + if frailscore0=4 then frailscore4g0=3; + label frailscore4g0='R frailty score pre procedure:0-3+'; +run; + +proc print data=temp14; + var physical0 nutritive0 cognitive0 sensory0 domainmiss0 frailscore0 frailstatus0 frailscore4g0; +run; +proc freq data=temp14; tables frailstatus0 frailscore0 / missing; run; +proc means data=temp14 n nmiss min max mean; var frailscore0; run;