# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/ml/transformers_inference.R
# Python-backed transformer inference via reticulate.
library(reticulate)
library(tidyverse)
library(purrr)
use_virtualenv("~/.venv/healthcare-ai", required = TRUE)
transformers <- import("transformers")
torch <- import("torch")
# ── Clinical Risk Classifier ─────────────────────────────────
ClinicalRiskClassifier <- R6::R6Class("ClinicalRiskClassifier",
private = list(
pipe = NULL,
model_id = "HealthcareBERT/clinical-risk-classifier-v2",
threshold = 0.80
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"text-classification",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
score = function(age, records_df) {
prompt <- private$build_prompt(age, records_df)
result <- private$pipe(prompt, top_k = 1L)[[1]]
if (result$label == "HIGH_RISK") result$score else 1 - result$score
}
),
private = list(
build_prompt = function(age, records_df) {
codes <- records_df |>
pull(icd10_code) |>
na.omit() |>
paste(collapse = ", ")
glue("Patient age {age}. Diagnoses: {if (nchar(codes) > 0) codes else 'none'}.")
}
)
)
# ── Clinical NER ─────────────────────────────────────────────
ClinicalNER <- R6::R6Class("ClinicalNER",
private = list(
pipe = NULL,
model_id = "blaze999/Medical-NER"
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"ner",
model = private$model_id,
aggregation_strategy = "simple",
device = if (device == "cuda") 0L else -1L
)
},
extract = function(note, min_score = 0.80) {
raw <- private$pipe(note)
raw |>
purrr::keep(\(e) e$score >= min_score) |>
purrr::map_dfr(\(e) tibble(
text = e$word,
label = e$entity_group,
score = round(e$score, 4),
start = e$start,
end = e$end
))
},
problems = function(note) self$extract(note) |> filter(label == "PROBLEM"),
treatments = function(note) self$extract(note) |> filter(label == "TREATMENT"),
tests = function(note) self$extract(note) |> filter(label == "TEST")
)
)
# ── Summariser ───────────────────────────────────────────────
ClinicalSummariser <- R6::R6Class("ClinicalSummariser",
private = list(
pipe = NULL,
model_id = "Falconsai/medical_summarization",
max_input = 1024L,
max_out = 256L
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"summarization",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
summarise = function(note) {
chunks <- private$chunk(note)
summaries <- purrr::map_chr(chunks, \(chunk) {
private$pipe(
chunk,
max_length = private$max_out,
min_length = 40L,
do_sample = FALSE
)[[1]]$summary_text
})
paste(summaries, collapse = " ")
}
),
private = list(
chunk = function(text) {
words <- strsplit(text, "\\s+")[[1]]
n <- length(words)
starts <- seq(1, n, by = private$max_input)
purrr::map_chr(starts, \(s) {
paste(words[s:min(s + private$max_input - 1, n)], collapse = " ")
})
}
)
)
# healthcare/ml/transformers_inference.R
# Python-backed transformer inference via reticulate.
library(reticulate)
library(tidyverse)
library(purrr)
use_virtualenv("~/.venv/healthcare-ai", required = TRUE)
transformers <- import("transformers")
torch <- import("torch")
# ── Clinical Risk Classifier ─────────────────────────────────
ClinicalRiskClassifier <- R6::R6Class("ClinicalRiskClassifier",
private = list(
pipe = NULL,
model_id = "HealthcareBERT/clinical-risk-classifier-v2",
threshold = 0.80
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"text-classification",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
score = function(age, records_df) {
prompt <- private$build_prompt(age, records_df)
result <- private$pipe(prompt, top_k = 1L)[[1]]
if (result$label == "HIGH_RISK") result$score else 1 - result$score
}
),
private = list(
build_prompt = function(age, records_df) {
codes <- records_df |>
pull(icd10_code) |>
na.omit() |>
paste(collapse = ", ")
glue("Patient age {age}. Diagnoses: {if (nchar(codes) > 0) codes else 'none'}.")
}
)
)
# ── Clinical NER ─────────────────────────────────────────────
ClinicalNER <- R6::R6Class("ClinicalNER",
private = list(
pipe = NULL,
model_id = "blaze999/Medical-NER"
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"ner",
model = private$model_id,
aggregation_strategy = "simple",
device = if (device == "cuda") 0L else -1L
)
},
extract = function(note, min_score = 0.80) {
raw <- private$pipe(note)
raw |>
purrr::keep(\(e) e$score >= min_score) |>
purrr::map_dfr(\(e) tibble(
text = e$word,
label = e$entity_group,
score = round(e$score, 4),
start = e$start,
end = e$end
))
},
problems = function(note) self$extract(note) |> filter(label == "PROBLEM"),
treatments = function(note) self$extract(note) |> filter(label == "TREATMENT"),
tests = function(note) self$extract(note) |> filter(label == "TEST")
)
)
# ── Summariser ───────────────────────────────────────────────
ClinicalSummariser <- R6::R6Class("ClinicalSummariser",
private = list(
pipe = NULL,
model_id = "Falconsai/medical_summarization",
max_input = 1024L,
max_out = 256L
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"summarization",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
summarise = function(note) {
chunks <- private$chunk(note)
summaries <- purrr::map_chr(chunks, \(chunk) {
private$pipe(
chunk,
max_length = private$max_out,
min_length = 40L,
do_sample = FALSE
)[[1]]$summary_text
})
paste(summaries, collapse = " ")
}
),
private = list(
chunk = function(text) {
words <- strsplit(text, "\\s+")[[1]]
n <- length(words)
starts <- seq(1, n, by = private$max_input)
purrr::map_chr(starts, \(s) {
paste(words[s:min(s + private$max_input - 1, n)], collapse = " ")
})
}
)
)
# healthcare/ml/transformers_inference.R
# Python-backed transformer inference via reticulate.
library(reticulate)
library(tidyverse)
library(purrr)
use_virtualenv("~/.venv/healthcare-ai", required = TRUE)
transformers <- import("transformers")
torch <- import("torch")
# ── Clinical Risk Classifier ─────────────────────────────────
ClinicalRiskClassifier <- R6::R6Class("ClinicalRiskClassifier",
private = list(
pipe = NULL,
model_id = "HealthcareBERT/clinical-risk-classifier-v2",
threshold = 0.80
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"text-classification",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
score = function(age, records_df) {
prompt <- private$build_prompt(age, records_df)
result <- private$pipe(prompt, top_k = 1L)[[1]]
if (result$label == "HIGH_RISK") result$score else 1 - result$score
}
),
private = list(
build_prompt = function(age, records_df) {
codes <- records_df |>
pull(icd10_code) |>
na.omit() |>
paste(collapse = ", ")
glue("Patient age {age}. Diagnoses: {if (nchar(codes) > 0) codes else 'none'}.")
}
)
)
# ── Clinical NER ─────────────────────────────────────────────
ClinicalNER <- R6::R6Class("ClinicalNER",
private = list(
pipe = NULL,
model_id = "blaze999/Medical-NER"
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"ner",
model = private$model_id,
aggregation_strategy = "simple",
device = if (device == "cuda") 0L else -1L
)
},
extract = function(note, min_score = 0.80) {
raw <- private$pipe(note)
raw |>
purrr::keep(\(e) e$score >= min_score) |>
purrr::map_dfr(\(e) tibble(
text = e$word,
label = e$entity_group,
score = round(e$score, 4),
start = e$start,
end = e$end
))
},
problems = function(note) self$extract(note) |> filter(label == "PROBLEM"),
treatments = function(note) self$extract(note) |> filter(label == "TREATMENT"),
tests = function(note) self$extract(note) |> filter(label == "TEST")
)
)
# ── Summariser ───────────────────────────────────────────────
ClinicalSummariser <- R6::R6Class("ClinicalSummariser",
private = list(
pipe = NULL,
model_id = "Falconsai/medical_summarization",
max_input = 1024L,
max_out = 256L
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"summarization",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
summarise = function(note) {
chunks <- private$chunk(note)
summaries <- purrr::map_chr(chunks, \(chunk) {
private$pipe(
chunk,
max_length = private$max_out,
min_length = 40L,
do_sample = FALSE
)[[1]]$summary_text
})
paste(summaries, collapse = " ")
}
),
private = list(
chunk = function(text) {
words <- strsplit(text, "\\s+")[[1]]
n <- length(words)
starts <- seq(1, n, by = private$max_input)
purrr::map_chr(starts, \(s) {
paste(words[s:min(s + private$max_input - 1, n)], collapse = " ")
})
}
)
)
# healthcare/ml/transformers_inference.R
# Python-backed transformer inference via reticulate.
library(reticulate)
library(tidyverse)
library(purrr)
use_virtualenv("~/.venv/healthcare-ai", required = TRUE)
transformers <- import("transformers")
torch <- import("torch")
# ── Clinical Risk Classifier ─────────────────────────────────
ClinicalRiskClassifier <- R6::R6Class("ClinicalRiskClassifier",
private = list(
pipe = NULL,
model_id = "HealthcareBERT/clinical-risk-classifier-v2",
threshold = 0.80
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"text-classification",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
score = function(age, records_df) {
prompt <- private$build_prompt(age, records_df)
result <- private$pipe(prompt, top_k = 1L)[[1]]
if (result$label == "HIGH_RISK") result$score else 1 - result$score
}
),
private = list(
build_prompt = function(age, records_df) {
codes <- records_df |>
pull(icd10_code) |>
na.omit() |>
paste(collapse = ", ")
glue("Patient age {age}. Diagnoses: {if (nchar(codes) > 0) codes else 'none'}.")
}
)
)
# ── Clinical NER ─────────────────────────────────────────────
ClinicalNER <- R6::R6Class("ClinicalNER",
private = list(
pipe = NULL,
model_id = "blaze999/Medical-NER"
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"ner",
model = private$model_id,
aggregation_strategy = "simple",
device = if (device == "cuda") 0L else -1L
)
},
extract = function(note, min_score = 0.80) {
raw <- private$pipe(note)
raw |>
purrr::keep(\(e) e$score >= min_score) |>
purrr::map_dfr(\(e) tibble(
text = e$word,
label = e$entity_group,
score = round(e$score, 4),
start = e$start,
end = e$end
))
},
problems = function(note) self$extract(note) |> filter(label == "PROBLEM"),
treatments = function(note) self$extract(note) |> filter(label == "TREATMENT"),
tests = function(note) self$extract(note) |> filter(label == "TEST")
)
)
# ── Summariser ───────────────────────────────────────────────
ClinicalSummariser <- R6::R6Class("ClinicalSummariser",
private = list(
pipe = NULL,
model_id = "Falconsai/medical_summarization",
max_input = 1024L,
max_out = 256L
),
public = list(
initialize = function(device = "cpu") {
private$pipe <- transformers$pipeline(
"summarization",
model = private$model_id,
device = if (device == "cuda") 0L else -1L
)
},
summarise = function(note) {
chunks <- private$chunk(note)
summaries <- purrr::map_chr(chunks, \(chunk) {
private$pipe(
chunk,
max_length = private$max_out,
min_length = 40L,
do_sample = FALSE
)[[1]]$summary_text
})
paste(summaries, collapse = " ")
}
),
private = list(
chunk = function(text) {
words <- strsplit(text, "\\s+")[[1]]
n <- length(words)
starts <- seq(1, n, by = private$max_input)
purrr::map_chr(starts, \(s) {
paste(words[s:min(s + private$max_input - 1, n)], collapse = " ")
})
}
)
)
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/domain/patient_analysis.R
# Single Responsibility: each function does exactly one thing.
library(tidyverse)
library(lubridate)
library(glue)
# ── Constants ────────────────────────────────────────────────
NORMAL_RANGES <- list(
heart_rate = c(60, 100),
systolic_bp = c(90, 120),
diastolic_bp = c(60, 80),
spo2 = c(95.0, 100.0),
temperature = c(36.1, 37.2)
)
RISK_WEIGHTS <- c(
age = 0.20,
comorbidity = 0.40,
vitals = 0.30,
history = 0.10
)
SEVERITY_RANK <- c(LOW = 0L, MODERATE = 1L, HIGH = 2L, CRITICAL = 3L)
# ── Vitals helpers ───────────────────────────────────────────
#' Compute the proportion of vital fields outside normal range.
#' @param vitals A named list of numeric vital values.
#' @return Numeric scalar in [0, 1].
abnormality_index <- function(vitals) {
checks <- purrr::imap_lgl(NORMAL_RANGES, \(range, field) {
val <- vitals[[field]]
!between(val, range[1], range[2])
})
mean(checks, na.rm = TRUE)
}
#' @return Logical; TRUE when vitals indicate a critical state.
vitals_is_critical <- function(vitals) {
abnormality_index(vitals) >= 0.6 || vitals$spo2 < 90.0
}
#' @return Logical; TRUE when vitals are older than 6 hours.
vitals_is_stale <- function(recorded_at) {
difftime(now("UTC"), recorded_at, units = "hours") > 6
}
# ── Scoring components ───────────────────────────────────────
score_age <- function(age) {
dplyr::case_when(
age < 18 ~ 0.1,
age < 45 ~ 0.2,
age < 65 ~ 0.5,
age < 80 ~ 0.7,
TRUE ~ 1.0
)
}
score_comorbidity <- function(records_df) {
n <- records_df |> pull(diagnosis_count) |> sum(na.rm = TRUE)
min(1.0, n / 10)
}
score_vitals <- function(records_df) {
indices <- records_df |>
filter(!is.na(vitals)) |>
pull(vitals) |>
purrr::map_dbl(abnormality_index)
if (length(indices) == 0L) return(0.0)
mean(indices)
}
score_history <- function(records_df) {
if (any(records_df$hospitalization, na.rm = TRUE)) 0.8 else 0.1
}
#' Compute weighted risk score for a patient.
#' @param age Integer patient age.
#' @param records_df A data frame of medical records.
#' @return Numeric scalar in [0, 1].
compute_risk_score <- function(age, records_df) {
components <- c(
age = score_age(age),
comorbidity = score_comorbidity(records_df),
vitals = score_vitals(records_df),
history = score_history(records_df)
)
raw <- sum(components * RISK_WEIGHTS)
clamp(raw, 0.0, 1.0)
}
clamp <- function(x, lo, hi) pmax(lo, pmin(hi, x))
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}
# healthcare/application/patient_pipeline.R
library(tidyverse)
library(purrr)
library(rlang)
# ── Context builder ──────────────────────────────────────────
#' Build a structured patient context from raw data frames.
#' Open/Closed: extend via additional normaliser functions,
#' not by modifying this function.
#'
#' @param patient A named list with id, age, dob.
#' @param records A data frame of medical records.
#' @param normalise A function: icd10_code -> Diagnosis list.
#' @return A named list representing PatientContext.
build_patient_context <- function(patient, records, normalise) {
diagnoses <- records |>
pull(icd10_code) |>
purrr::map(normalise) |>
purrr::list_rbind() |>
dplyr::distinct(code, .keep_all = TRUE) |>
dplyr::arrange(desc(SEVERITY_RANK[severity]))
latest_vitals <- records |>
filter(!is.na(vitals_json)) |>
arrange(desc(recorded_at)) |>
slice_head(n = 1) |>
pull(vitals_json) |>
purrr::pluck(1)
# Drop stale vitals
if (!is.null(latest_vitals) &&
vitals_is_stale(latest_vitals$recorded_at)) {
latest_vitals <- NULL
}
risk <- compute_risk_score(patient$age, records)
flags <- derive_flags(patient, records, latest_vitals, risk)
list(
id = patient$id,
age = patient$age,
diagnoses = diagnoses,
vitals = latest_vitals,
risk_score = risk,
flags = flags
)
}
derive_flags <- function(patient, records, vitals, risk) {
flags <- character(0)
if (!is.null(vitals) && vitals_is_critical(vitals))
flags <- c(flags, "icu_candidate")
if (patient$age >= 65)
flags <- c(flags, "elderly")
if (any(records$chronic, na.rm = TRUE))
flags <- c(flags, "chronic")
if (risk > 0.75)
flags <- c(flags, "high_risk")
flags
}
# ── Pipeline steps ───────────────────────────────────────────
#' Compose a list of step functions into a single pipeline.
#' Interface Segregation: each step is just function(ctx) -> ctx.
make_pipeline <- function(...) {
steps <- list(...)
function(ctx) purrr::reduce(steps, \(acc, step) step(acc), .init = ctx)
}
flagging_step <- function(threshold = 0.75) {
function(ctx) {
if (ctx$risk_score < threshold) return(ctx)
ctx$flags <- union(ctx$flags, "auto_flagged")
ctx
}
}
enrichment_step <- function(model_fn) {
function(ctx) {
ctx$recommendations <- model_fn(ctx)
ctx
}
}
# ── Notifiers ─────────────────────────────────────────────────
#' Dispatch an event to a list of notifier functions.
#' Dependency Inversion: notifiers are injected, not hardcoded.
broadcast <- function(event, ctx, notifiers) {
purrr::walk(notifiers, \(notify) {
tryCatch(
notify(event = event, ctx = ctx),
error = \(e) warning(glue("Notifier failed: {conditionMessage(e)}"))
)
})
}
slack_notifier <- function(webhook_url, channel) {
function(event, ctx) {
payload <- list(
channel = channel,
text = glue(
"{emoji(event)} *{event}* — ",
"Patient `{ctx$id}` | ",
"Risk: *{round(ctx$risk_score * 100)}%* | ",
"Flags: {paste(ctx$flags, collapse = ', ')}"
)
)
httr2::request(webhook_url) |>
httr2::req_body_json(payload) |>
httr2::req_perform()
}
}
emoji <- function(event) {
switch(event,
analysis_complete = "\u2705",
analysis_failed = "\u274c",
high_risk = "\U0001f6a8",
"\U0001f514"
)
}