# Clinical decision tree using R

I'm trying to code this decision tree using R. I was wondering if there was a better way to go about this rather than using a ton of conditionals?

# ops
rm(list=ls(all=TRUE))
options(warn=-1)
saveFiles = FALSE
setwd("~/Documents/Education/UCLA/Spring 2016/MGMT271B/proj/")

# data vars
n = 10
T = c("X", seq(0,4), "is")
N = c("X", seq(0,3))
M = c("X", seq(0,1))
S = c("1A", "1B", "2A", "2B", "3A", "3B", "3C", "4")
h = c("ductal", "lobular", "mixed", "metaplastic", "tubular", "mucinous")

# necessary functions
create_data <- function(n) {
return (data.frame(PatientID       = sample(10^12, n, FALSE),
Histology       = sample(h, n, TRUE),
ER              = factor(sample(c("TRUE", "FALSE"), n, TRUE)),
PR              = factor(sample(c("TRUE", "FALSE"), n, TRUE)),
HER2            = factor(sample(c("TRUE", "FALSE"), n, TRUE)),
PrimaryTumor    = factor(paste0("T", sample(T, n, TRUE))),
TumorSize       = round(runif(n, 0, 6),2),
NearbyLymphNode = paste0("N", sample(N, n, TRUE)),
Metastasis      = paste0("M", sample(M, n, TRUE)),
Stage           = sample(S, n, TRUE), row.names=NULL))
}

get_patient_data <- function(df, id) {
return (df[df$PatientID == id,]) } get_treatment_rec <- function(Histology, ER, PR, HER2, PrimaryTumor, TumorSize, NearbyLymphNode, Metastasis, Stage) { if (Histology %in% c("ductal", "lobular", "mixed", "metaplastic")) { if (ER == TRUE || PR == TRUE) { if (HER2 == TRUE) { if (NearbyLymphNode %in% c("N0", "N1") & PrimaryTumor %in% c("T1", "T2", "T3")){ if (TumorSize <= 0.5) { if (NearbyLymphNode == "N0") { message(cat("Consider adjuvant endocrine therapy and/or adjuvant chemotherapy with Trastuzumab")) } if (NearbyLymphNode == "N1") { message(cat("Adjuvant endocrine therapy or adjuvant chemotherapy with Trastuzumab followed by endocrine therapy")) } } else if (TumorSize > 0.06 & TumorSize < 1.0) { message(cat("Adjuvant endocrine therapy and/or adjuvant chemotherapy with Trastuzumab")) } else { message(cat("Adjuvant endocrine therapy and adjuvant chemotherapy with Trastuzumab")) } } } else if (HER2 == FALSE) { if (NearbyLymphNode %in% c("N0", "N1") & PrimaryTumor %in% c("T1", "T2", "T3")) { if (TumorSize <= 0.5){ if (NearbyLymphNode == "N0") { message(cat("Consider adjuvant endocrine therapy")) } if (NearbyLymphNode == "N1") { message(cat("Adjuvant endocrine therapy and/or adjuvant chemotherapy")) } } else { message(cat("Consider 21-gene RT-PCR assay")) } } } } else if (ER == FALSE & PR == FALSE) { if (HER2 == TRUE) { if (NearbyLymphNode %in% c("N0", "N1") & PrimaryTumor %in% c("T1", "T2", "T3")) { if (TumorSize <= 0.5) { if (NearbyLymphNode == "N0") { message(cat("Consider adjuvant chemotherapy with Trastuzumab")) } if (NearbyLymphNode == "N1") { message(cat("Consider adjuvant chemotherapy with Trastuzumab")) } } else if (TumorSize > 0.06 & TumorSize < 1.0) { message(cat("Consider adjuvant chemotherapy with Trastuzumab")) } else { message(cat("Adjuvant chemotherapy with Trastuzumab")) } } } else if (HER2 == FALSE) { if (NearbyLymphNode %in% c("N0", "N1") & PrimaryTumor %in% c("T1", "T2", "T3")) { if (TumorSize <= 0.5) { if (NearbyLymphNode == "N0") { message(cat("No adjuvant therapy")) } if (NearbyLymphNode == "N1") { message(cat("Consider adjuvant chemotherapy")) } } else if (TumorSize > 0.06 & TumorSize < 1.0) { message(cat("Consider adjuvant chemotherapy")) } else { message(cat("Adjuvant chemotherapy")) } } } else { message(cat("Invalid HER2 Status. Exiting.")) } } } else if (Histology %in% c("tubular", "mucinous")) { if (ER == TRUE || PR == TRUE) { if (NearbyLymphNode %in% c("N0", "N1") & PrimaryTumor %in% c("T1", "T2", "T3")) { if (TumorSize <= 1) { message(cat("No adjuvant therapy")) } else if (TumorSize > 1 & TumorSize < 3) { message(cat("Consider adjuvant endocrine therapy")) } else { message(cat("Adjuvant endocrine therapy")) } } } else if (ER == FALSE || PR == FALSE) { message(cat("Repeat determination of ER/PR status")) } } else { message(cat("Invalid Histology Status. Exiting")) } } # tests n = 20 data = create_data(n) if(saveFiles){ write.table(data, "data.tsv", sep="\t", col.names=NA) system("gzip data.tsv") } get_patient_data(data, data$PatientID[1])
get_patient_data(data, "12857252013")
get_treatment_rec(Histology="ductal", ER=TRUE, PR=FALSE, HER2=TRUE, PrimaryTumor="T1", TumorSize=1.02, NearbyLymphNode="N1", Metastasis=NULL, Stage=NULL)
get_treatment_rec(Histology="mucinous", ER=FALSE, PR=TRUE, HER2=TRUE, PrimaryTumor="T1", TumorSize=4.22, NearbyLymphNode="N0", Metastasis=NULL, Stage=NULL)

• It looks pretty compact and straightforward to me. However, I think you have a typo here: TumorSize > 0.06. Should be TumorSize > 0.6. – hackR Apr 13 '16 at 15:41
• Ah! Good catch! – user2117258 Apr 13 '16 at 17:32
• Another choice for such an unbalanced tree would be a nested list. – hackR Apr 15 '16 at 17:47

I think the common wisdom is to go data driven instead of hard coding the tree. Then again, it's not clear if that helps too much here.

Let me suggest a few other things first to make the code a bit more compact.

• message(cat(...)) is used very often, but it seems the cat isn't actually necessary here? Also, since all the function get_treatment_rec does is to print/message the result, I'd say that just returning the string and calling message outside of it is far better in terms of composability, i.e. message(get_treatment_rec(...)). Making use of the fact that R will just return the last value in the function, it becomes much more simple. There are also quite a number of "empty" cases - shouldn't those also do something or at least return an error response?
• Use && instead of & unless you really want to evaluate all arguments. && and || are much more common since they can do less work if the first parameter is already FALSE / TRUE respectively.
• x == FALSE is usually written as !x unless you really only want to match FALSE (not all "falsy" things). Same goes for x == TRUE vs. just x.
• Don't write something like if (foo) {} else if (!foo) {} - that's unnecessary information as there's only two cases anyway, just use a regular else branch instead, i.e. if (foo) {} else {}.
• Now, there are recurring cases. I'd put those either into a variable and only calculate them once (and name them), or put them into a function and reuse that. Note that I have no idea what to name these, so I'm using the usual foo, bar, baz, ... here.

For posterity I'm going to paste the resulting function, but it's really just for illustration purposes:

get_treatment_rec <- function(Histology, ER, PR, HER2, PrimaryTumor, TumorSize, NearbyLymphNode, Metastasis, Stage) {
foo <- NearbyLymphNode %in% c("N0", "N1") && PrimaryTumor %in% c("T1", "T2", "T3")

if (Histology %in% c("ductal", "lobular", "mixed", "metaplastic")) {
if (ER || PR) {
if (HER2) {
if (foo) {
if (TumorSize <= 0.5) {
if (NearbyLymphNode == "N0") {
} else if (NearbyLymphNode == "N1") {
"Adjuvant endocrine therapy or adjuvant chemotherapy with Trastuzumab followed by endocrine therapy"
}
} else if (TumorSize > 0.06 & TumorSize < 1.0) {
} else {
}
}
} else {
if (foo) {
if (TumorSize <= 0.5) {
if (NearbyLymphNode == "N0") {
}
if (NearbyLymphNode == "N1") {
}
} else {
"Consider 21-gene RT-PCR assay"
}
}
}
} else if (!ER && !PR) {
if (HER2) {
if (foo) {
if (TumorSize <= 0.5) {
if (NearbyLymphNode == "N0" || NearbyLymphNode == "N1") {
}
} else if (TumorSize > 0.06 & TumorSize < 1.0) {
} else {
}
}
} else if (!HER2) {
if (foo) {
if (TumorSize <= 0.5) {
if (NearbyLymphNode == "N0") {
} else if (NearbyLymphNode == "N1") {
}
} else if (TumorSize > 0.06 & TumorSize < 1.0) {
} else {
}
}
} else {
"Invalid HER2 Status. Exiting."
}
}
} else if (Histology %in% c("tubular", "mucinous")) {
if (ER || PR) {
if (foo) {
if (TumorSize <= 1) {
} else if (TumorSize > 1 && TumorSize < 3) {
} else {
}
}
} else if (!ER || !PR) {
"Repeat determination of ER/PR status"
}
} else {
"Invalid Histology Status. Exiting"
}
}


According to the best practices in R coding the maximum numbers of characters in line is 80. Then people who read your code don't have to scroll horizontally if you stick to the rule.

In R studio your can turn on the margin line.

Also you shouldn't write return at the end of the function.