| Title: | Risk Quantification Engine for Clinical Submission Readiness |
|---|---|
| Description: | Quantifies submission risk using a Failure Modes and Effects Analysis (FMEA)-inspired framework (probability, impact, detectability). Builds risk registers from evidence, computes Risk Priority Numbers (RPN), classifies risk levels, and emits standardized R4SUB (Ready for Submission) evidence table rows via 'r4subcore'. Supports risk mitigation tracking and trend analysis across submission milestones. |
| Authors: | Pawan Rama Mali [aut, cre, cph] (ORCID: <https://orcid.org/0000-0001-7864-5819>) |
| Maintainer: | Pawan Rama Mali <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-10-11 10:37:17 UTC |
| Source: | https://github.com/r4sub/r4subrisk |
Applies mitigation updates to a risk register. Allows updating probability, impact, detectability, status, and mitigation notes for specific risks.
apply_mitigations(risk_register, updates, config = risk_config_default())apply_mitigations(risk_register, updates, config = risk_config_default())
risk_register |
A |
updates |
A data.frame with at minimum column |
config |
A |
An updated risk_register with recomputed RPN and risk levels.
risks <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 3), impact = c(5, 4), detectability = c(2, 3) ) rr <- create_risk_register(risks) updates <- data.frame( risk_id = "R001", probability = 2, mitigation = "Added validation check", status = "mitigated" ) rr2 <- apply_mitigations(rr, updates) rr2risks <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 3), impact = c(5, 4), detectability = c(2, 3) ) rr <- create_risk_register(risks) updates <- data.frame( risk_id = "R001", probability = 2, mitigation = "Added validation check", status = "mitigated" ) rr2 <- apply_mitigations(rr, updates) rr2
Applies a risk_model() to a set of risks and returns per-risk raw scores,
normalized 0–100 risk scores, and qualitative levels. Any model that
produces a 0–100 risk score plugs into the same Risk pillar computation.
apply_risk_model(risks, model = risk_model_fmea())apply_risk_model(risks, model = risk_model_fmea())
risks |
A data.frame or |
model |
A |
A tibble with columns risk_id, raw_score, risk_score (0–100,
higher is more risk), risk_level, and model.
risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) apply_risk_model(risks, risk_model_fmea())risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) apply_risk_model(risks, risk_model_fmea())
Classify RPN Value into Risk Level
classify_rpn(rpn, bands = risk_config_default()$rpn_bands)classify_rpn(rpn, bands = risk_config_default()$rpn_bands)
rpn |
Numeric RPN score (1–125). |
bands |
Named list of band boundaries from |
Character risk level name.
classify_rpn(90) classify_rpn(25) classify_rpn(5)classify_rpn(90) classify_rpn(25) classify_rpn(5)
Compares two risk register snapshots and reports changes in RPN, new risks, resolved risks, and risk level transitions.
compare_risk_registers(before, after)compare_risk_registers(before, after)
before |
A |
after |
A |
A list with:
rpn_changes: tibble of risks with changed RPN
new_risks: risk_ids present in after but not before
resolved_risks: risk_ids present in before but not after
level_transitions: tibble of risk level changes
delta_mean_rpn: change in mean RPN
r1 <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 3), impact = c(5, 4), detectability = c(2, 3) ) r2 <- data.frame( risk_id = c("R001", "R003"), description = c("Missing vars", "New issue"), probability = c(2, 3), impact = c(5, 3), detectability = c(2, 2) ) rr1 <- create_risk_register(r1) rr2 <- create_risk_register(r2) compare_risk_registers(rr1, rr2)r1 <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 3), impact = c(5, 4), detectability = c(2, 3) ) r2 <- data.frame( risk_id = c("R001", "R003"), description = c("Missing vars", "New issue"), probability = c(2, 3), impact = c(5, 3), detectability = c(2, 2) ) rr1 <- create_risk_register(r1) rr2 <- create_risk_register(r2) compare_risk_registers(rr1, rr2)
Computes aggregate risk metrics from a risk register, including mean RPN, risk distribution, and overall risk score normalized to 0–1.
compute_risk_scores(risk_register, config = risk_config_default())compute_risk_scores(risk_register, config = risk_config_default())
risk_register |
A |
config |
A |
A list of class "risk_scores" with:
overall_risk_score: 0–1 (0 = no risk, 1 = maximum risk)
mean_rpn: average RPN across all risks
max_rpn: highest individual RPN
n_risks: total risk count
risk_distribution: tibble of counts by risk_level
category_summary: tibble of mean RPN by category
risks <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 2), impact = c(5, 3), detectability = c(2, 3) ) rr <- create_risk_register(risks) compute_risk_scores(rr)risks <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 2), impact = c(5, 3), detectability = c(2, 3) ) rr <- create_risk_register(risks) compute_risk_scores(rr)
Builds a risk register from a user-supplied data.frame of identified risks. Validates required columns and fills defaults.
create_risk_register(risks, config = risk_config_default())create_risk_register(risks, config = risk_config_default())
risks |
A data.frame with at minimum columns |
config |
A |
A tibble of class "risk_register" with standardized columns and
computed RPN values.
risks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing SDTM variables", "Unmapped ADaM derivations", "Inconsistent define.xml"), category = c("data_quality", "traceability", "documentation"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) rr <- create_risk_register(risks) rrrisks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing SDTM variables", "Unmapped ADaM derivations", "Inconsistent define.xml"), category = c("data_quality", "traceability", "documentation"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) rr <- create_risk_register(risks) rr
Automatically generates risk items from an R4SUB evidence table. Each failing or warning indicator becomes a potential risk, with probability and impact inferred from evidence severity.
evidence_to_risks( evidence, config = risk_config_default(), include_pass = FALSE )evidence_to_risks( evidence, config = risk_config_default(), include_pass = FALSE )
evidence |
A validated evidence data.frame (from |
config |
A |
include_pass |
Logical; if |
The mapping from evidence to risk uses:
risk_id: derived from indicator_id + asset_id via
r4subcore::hash_id()
category: mapped from indicator_domain
probability: mapped from evidence severity via config
impact: mapped from evidence severity via config
detectability: uses config$default_detectability
Multiple evidence rows for the same indicator + asset are aggregated: probability and impact use the maximum across rows.
A tibble suitable for create_risk_register().
## Not run: risk_items <- evidence_to_risks(evidence) rr <- create_risk_register(risk_items) ## End(Not run)## Not run: risk_items <- evidence_to_risks(evidence) rr <- create_risk_register(risk_items) ## End(Not run)
Draws the standard FMEA probability by impact heatmap. Cells are coloured by the chosen metric and labelled with the number of risks that fall in them.
plot_risk_heatmap( risk_register, metric = c("mean_rpn", "max_rpn", "n"), config = risk_config_default(), interactive = FALSE )plot_risk_heatmap( risk_register, metric = c("mean_rpn", "max_rpn", "n"), config = risk_config_default(), interactive = FALSE )
risk_register |
A |
metric |
The cell fill, one of |
config |
A |
interactive |
If |
A ggplot object, or a plotly object when interactive = TRUE.
## Not run: risks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing SDTM variables", "Unmapped ADaM derivations", "Inconsistent define.xml"), category = c("data_quality", "traceability", "documentation"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) rr <- create_risk_register(risks) plot_risk_heatmap(rr) ## End(Not run)## Not run: risks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing SDTM variables", "Unmapped ADaM derivations", "Inconsistent define.xml"), category = c("data_quality", "traceability", "documentation"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) rr <- create_risk_register(risks) plot_risk_heatmap(rr) ## End(Not run)
Print a Risk Model
## S3 method for class 'risk_model' print(x, ...)## S3 method for class 'risk_model' print(x, ...)
x |
A |
... |
Ignored. |
Print Monte Carlo Risk Result
## S3 method for class 'risk_monte_carlo' print(x, ...)## S3 method for class 'risk_monte_carlo' print(x, ...)
x |
A |
... |
Ignored. |
Print a Risk Reduction Summary
## S3 method for class 'risk_reduction' print(x, ...)## S3 method for class 'risk_reduction' print(x, ...)
x |
A |
... |
Ignored. |
Print Risk Register
## S3 method for class 'risk_register' print(x, ...)## S3 method for class 'risk_register' print(x, ...)
x |
A |
... |
Ignored. |
Print Risk Scores
## S3 method for class 'risk_scores' print(x, ...)## S3 method for class 'risk_scores' print(x, ...)
x |
A |
... |
Ignored. |
Returns configuration for risk assessment including FMEA scale definitions, RPN thresholds, and risk level classification bands.
risk_config_default( rpn_bands = list(critical = c(80, 125), high = c(40, 79), medium = c(15, 39), low = c(1, 14)), evidence_severity_to_probability = c(info = 1, low = 2, medium = 3, high = 4, critical = 5), evidence_severity_to_impact = c(info = 1, low = 2, medium = 3, high = 4, critical = 5), default_detectability = 3 )risk_config_default( rpn_bands = list(critical = c(80, 125), high = c(40, 79), medium = c(15, 39), low = c(1, 14)), evidence_severity_to_probability = c(info = 1, low = 2, medium = 3, high = 4, critical = 5), evidence_severity_to_impact = c(info = 1, low = 2, medium = 3, high = 4, critical = 5), default_detectability = 3 )
rpn_bands |
Named list of RPN band boundaries |
evidence_severity_to_probability |
Named numeric vector mapping evidence severity to probability scores (1–5 scale). |
evidence_severity_to_impact |
Named numeric vector mapping evidence severity to impact scores (1–5 scale). |
default_detectability |
Default detectability score (1–5) when not explicitly provided. Lower = more detectable. |
The FMEA-inspired risk model uses three dimensions:
Probability (1–5): likelihood of the issue occurring/persisting
Impact (1–5): severity of consequence if unresolved
Detectability (1–5): difficulty of detecting the issue (1 = easy, 5 = hard)
RPN = Probability x Impact x Detectability (range 1–125)
A list of class "risk_config" with elements:
rpn_bands, evidence_severity_to_probability,
evidence_severity_to_impact, default_detectability.
cfg <- risk_config_default() cfg$rpn_bandscfg <- risk_config_default() cfg$rpn_bands
Aggregates a risk register into the probability by impact grid that an FMEA heatmap is drawn from. Every cell of the 5 by 5 grid is returned, including empty ones, so the plotted heatmap has no gaps.
risk_heatmap_data(risk_register, config = risk_config_default())risk_heatmap_data(risk_register, config = risk_config_default())
risk_register |
A |
config |
A |
A tibble with one row per grid cell: probability, impact, n
(risks in the cell), mean_rpn, max_rpn, and zone (the risk level of
the cell's mean RPN, NA for empty cells).
rr <- create_risk_register(data.frame( risk_id = c("R001", "R002"), description = c("a", "b"), probability = c(4, 2), impact = c(5, 3), detectability = c(2, 3) )) risk_heatmap_data(rr)rr <- create_risk_register(data.frame( risk_id = c("R001", "R002"), description = c("a", "b"), probability = c(4, 2), impact = c(5, 3), detectability = c(2, 3) )) risk_heatmap_data(rr)
Computes summary risk indicators from a risk register, similar to
r4subtrace::trace_indicator_scores().
risk_indicator_summary(risk_register)risk_indicator_summary(risk_register)
risk_register |
A |
A tibble with columns: indicator, value, description.
risks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing vars", "Bad derivation", "Label mismatch"), probability = c(4, 2, 1), impact = c(5, 3, 2), detectability = c(2, 3, 1) ) rr <- create_risk_register(risks) risk_indicator_summary(rr)risks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing vars", "Bad derivation", "Label mismatch"), probability = c(4, 2, 1), impact = c(5, 3, 2), detectability = c(2, 3, 1) ) rr <- create_risk_register(risks) risk_indicator_summary(rr)
Wraps a scoring rule so the engine can score risks with something other than
the default FMEA product. A model turns a data.frame of risks into a raw
score, a normalized 0–100 risk score, and a qualitative level. Use the
pre-built models or supply your own score function.
risk_model(name, score, max_raw, bands, required = character())risk_model(name, score, max_raw, bands, required = character())
name |
A short model name. |
score |
A function taking a data.frame of risks and returning a numeric vector of raw scores, one per row. |
max_raw |
The raw score at maximum risk, used to normalize to 0–100. |
bands |
A named list of |
required |
Character vector of columns the |
An object of class "risk_model".
risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) m <- risk_model( name = "impact_only", score = function(df) df$impact, max_raw = 5, bands = list(high = c(4, 5), medium = c(3, 3), low = c(1, 2)), required = "impact" ) apply_risk_model(risks, m)risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) m <- risk_model( name = "impact_only", score = function(df) df$impact, max_raw = 5, bands = list(high = c(4, 5), medium = c(3, 3), low = c(1, 2)), required = "impact" ) apply_risk_model(risks, m)
Reduces the per-risk output of apply_risk_model() to a single normalized
0–100 risk figure (the mean risk score). This is the value the Risk pillar
consumes, and it is model-agnostic by construction.
risk_model_overall(scored)risk_model_overall(scored)
scored |
A tibble from |
A single number in [0, 100].
risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) scored <- apply_risk_model(risks, risk_model_ich_q9()) risk_model_overall(scored)risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) scored <- apply_risk_model(risks, risk_model_ich_q9()) risk_model_overall(scored)
Ready-made risk_model() objects for the common frameworks. risk_model_fmea()
is the default three-factor FMEA product. risk_model_probability_impact() is
a two-factor probability times impact matrix. risk_model_ich_q9() is a
qualitative ICH Q9 style ranking that collapses probability and impact into
acceptable, tolerable, and unacceptable levels.
risk_model_fmea(config = risk_config_default()) risk_model_probability_impact() risk_model_ich_q9()risk_model_fmea(config = risk_config_default()) risk_model_probability_impact() risk_model_ich_q9()
config |
A |
A risk_model object.
risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) apply_risk_model(risks, risk_model_probability_impact()) apply_risk_model(risks, risk_model_ich_q9())risks <- data.frame( risk_id = c("R001", "R002", "R003"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) apply_risk_model(risks, risk_model_probability_impact()) apply_risk_model(risks, risk_model_ich_q9())
An FMEA scores each risk with single integer values for probability, impact, and detectability, but those assessments are judgements with uncertainty. This simulation treats each score as uncertain, draws many possible registers, and reports the resulting distribution of Risk Priority Numbers, both per risk and for the register as a whole. It turns a single RPN into a range and a probability of being critical, which is what a risk review actually wants to know.
risk_monte_carlo( register, n = 10000, spread = 1, config = risk_config_default(), seed = NULL )risk_monte_carlo( register, n = 10000, spread = 1, config = risk_config_default(), seed = NULL )
register |
A |
n |
Number of Monte Carlo iterations. Default |
spread |
Standard deviation of the per-component uncertainty, in scale
points. Default |
config |
A |
seed |
Optional integer seed. When supplied the random state is set for the call and restored on exit, so the result is reproducible without disturbing the caller's RNG stream. |
For each draw, every FMEA component is sampled from a normal distribution
centred on its assessed value with standard deviation spread (in scale
points), then rounded and clamped to the 1–5 scale. spread = 0 reproduces
the deterministic RPN. The per-draw RPN is probability * impact * detectability, and the register total is their sum. The critical threshold is
the lower bound of the top RPN band in config.
A list of class "risk_monte_carlo" with n, spread,
crit_threshold, point_total (the deterministic register RPN),
total (named numeric summary of the total RPN distribution),
total_draws (the simulated totals, for plotting), and per_risk
(a data.frame with risk_id, rpn, mc_mean, mc_sd, p05, p50,
p95, and prob_critical).
risks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing SDTM vars", "Unmapped derivations", "Define drift"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) rr <- suppressMessages(create_risk_register(risks)) mc <- risk_monte_carlo(rr, n = 2000, seed = 1) mc$total mc$per_riskrisks <- data.frame( risk_id = c("R001", "R002", "R003"), description = c("Missing SDTM vars", "Unmapped derivations", "Define drift"), probability = c(4, 3, 2), impact = c(5, 4, 3), detectability = c(2, 3, 4) ) rr <- suppressMessages(create_risk_register(risks)) mc <- risk_monte_carlo(rr, n = 2000, seed = 1) mc$total mc$per_risk
Quantifies how much risk was removed between an earlier and a later risk register, so a mitigation effort can be reported as a single before/after story. Regulatory reviewers expect to see that identified risks were acted on, not just listed.
risk_reduction_summary(before, after, config = risk_config_default())risk_reduction_summary(before, after, config = risk_config_default())
before |
A |
after |
A |
config |
A |
Reduction is measured on the risks common to both registers. Total RPN is the
sum of Risk Priority Numbers, mean RPN is their average, and the percentage
reduction is relative to the before total. A risk is counted as downgraded
when its risk level moves to a lower band (for example critical to high).
Risks that appear only in after are reported as n_added, and risks that
disappear are reported as n_resolved.
A list of class "risk_reduction" with overall figures and a
per_risk tibble of the common risks. Fields include total_rpn_before,
total_rpn_after, total_rpn_reduction, pct_reduction,
mean_rpn_before, mean_rpn_after, n_reduced, n_increased,
n_unchanged, n_downgraded, n_added, n_resolved, and per_risk.
risks <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 3), impact = c(5, 4), detectability = c(2, 3) ) rr <- create_risk_register(risks) rr2 <- apply_mitigations(rr, data.frame(risk_id = "R001", probability = 1)) risk_reduction_summary(rr, rr2)risks <- data.frame( risk_id = c("R001", "R002"), description = c("Missing vars", "Bad derivation"), probability = c(4, 3), impact = c(5, 4), detectability = c(2, 3) ) rr <- create_risk_register(risks) rr2 <- apply_mitigations(rr, data.frame(risk_id = "R001", probability = 1)) risk_reduction_summary(rr, rr2)
Emits evidence rows compatible with r4subcore::validate_evidence() for
each risk item in the register, plus aggregate risk metric rows.
risk_register_to_evidence( risk_register, ctx, source_name = "r4subrisk", source_version = NULL )risk_register_to_evidence( risk_register, ctx, source_name = "r4subrisk", source_version = NULL )
risk_register |
A |
ctx |
An |
source_name |
Character; the name of the evidence source. |
source_version |
Character or |
A data.frame of evidence rows passing r4subcore::validate_evidence().
## Not run: library(r4subcore) ctx <- r4sub_run_context(study_id = "TEST001", environment = "DEV") risks <- data.frame( risk_id = "R001", description = "Missing vars", probability = 4, impact = 5, detectability = 2 ) rr <- create_risk_register(risks) ev <- risk_register_to_evidence(rr, ctx = ctx) ## End(Not run)## Not run: library(r4subcore) ctx <- r4sub_run_context(study_id = "TEST001", environment = "DEV") risks <- data.frame( risk_id = "R001", description = "Missing vars", probability = 4, impact = 5, detectability = 2 ) rr <- create_risk_register(risks) ev <- risk_register_to_evidence(rr, ctx = ctx) ## End(Not run)