Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
be1d976
Ajout de la vignette ORR
AlexisCochard Apr 29, 2025
d146eae
modification texte suite commentaires livia
AlexisCochard Jul 23, 2025
5bf6a5f
maj du data.R par Dan
AlexisCochard Jul 23, 2025
946a74c
retrait de tout ce qui est lié au patient 129 de icarus
AlexisCochard Jul 23, 2025
48b99b2
Revert "maj du data.R par Dan"
AlexisCochard Jul 23, 2025
15a2cff
suite modif dan
AlexisCochard Jul 23, 2025
c80f642
Fonction pour table ORR v0
AlexisCochard Jul 24, 2025
92fc2d7
mise a jour de l'environnement avec la nouvelle fonction
AlexisCochard Jul 24, 2025
ff14792
survfit_stack de dan
AlexisCochard Jul 24, 2025
fdd50ce
ajout des cli_abort dans ORR_table + optimisation de la confirmation
AlexisCochard Jul 24, 2025
632fb01
modif de dan
AlexisCochard Jul 24, 2025
143d76e
maj des test et exemples
AlexisCochard Jul 24, 2025
32ce6ba
Clean du code + ajout de cli_abort
AlexisCochard Jul 28, 2025
82ee36d
modif des cli_abort pour que ça marche
AlexisCochard Jul 28, 2025
001e307
modif après test
AlexisCochard Jul 28, 2025
95c1633
Revert "modif de dan"
AlexisCochard Jul 29, 2025
254b93d
Revert "suite modif dan"
AlexisCochard Jul 29, 2025
bf6e996
Squashed commit of the following:
AlexisCochard Jul 29, 2025
58e8819
documentation + test
AlexisCochard Aug 4, 2025
88a17a1
maj de calc_best_response avec ma methode
AlexisCochard Aug 4, 2025
a01ec89
ajout de la confirmation ou non à calc_best_response
AlexisCochard Aug 4, 2025
0550e9c
documentation ORR_table
AlexisCochard Aug 4, 2025
88d5aac
suppression code doublons de clac_best_reponse
AlexisCochard Aug 4, 2025
e71b263
optimisation dans ORR_table
AlexisCochard Aug 4, 2025
2b1c24d
modif du as_flextable
AlexisCochard Aug 4, 2025
696b9c9
maj de la doc
AlexisCochard Aug 4, 2025
8377084
ajout d'un distinct() pour supprimer les doublons
AlexisCochard Aug 4, 2025
02138da
suppression de survfit_stack qui était apparu
AlexisCochard Aug 4, 2025
f0891bd
modif pour prendre en compte les valeurs numériques
AlexisCochard Aug 4, 2025
fb3055b
nettoyage
AlexisCochard Aug 5, 2025
7944ed8
inversion des SD et PD dans la légende je sais pas pourquoi
AlexisCochard Aug 5, 2025
de9770c
mofif pour coller à la fonction ORR_table()
AlexisCochard Aug 6, 2025
589ed82
suppr dates NA et patients avec <2 visites
AlexisCochard Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export(as_flextable)
export(butterfly_plot)
export(gr_new_project)
export(grstat_example)
export(survfit_stack)
export(tibble)
export(waterfall_plot)
importFrom(GenBinomApps,clopper.pearson.ci)
importFrom(cli,cli_abort)
importFrom(cli,cli_inform)
importFrom(cli,cli_vec)
Expand All @@ -24,6 +26,7 @@ importFrom(cli,format_inline)
importFrom(cli,qty)
importFrom(dplyr,"%>%")
importFrom(dplyr,across)
importFrom(dplyr,all_of)
importFrom(dplyr,any_of)
importFrom(dplyr,arrange)
importFrom(dplyr,bind_rows)
Expand Down Expand Up @@ -56,19 +59,24 @@ importFrom(dplyr,summarise)
importFrom(dplyr,transmute)
importFrom(flextable,align)
importFrom(flextable,as_flextable)
importFrom(flextable,as_paragraph)
importFrom(flextable,bg)
importFrom(flextable,bold)
importFrom(flextable,delete_rows)
importFrom(flextable,flextable)
importFrom(flextable,fontsize)
importFrom(flextable,footnote)
importFrom(flextable,hline)
importFrom(flextable,hline_bottom)
importFrom(flextable,merge_h)
importFrom(flextable,merge_v)
importFrom(flextable,padding)
importFrom(flextable,set_header_df)
importFrom(flextable,set_table_properties)
importFrom(flextable,surround)
importFrom(flextable,valign)
importFrom(forcats,as_factor)
importFrom(forcats,fct_drop)
importFrom(forcats,fct_infreq)
importFrom(forcats,fct_relevel)
importFrom(forcats,fct_reorder)
Expand Down Expand Up @@ -106,6 +114,7 @@ importFrom(ggplot2,unit)
importFrom(ggplot2,vars)
importFrom(glue,glue)
importFrom(lifecycle,deprecated)
importFrom(officer,fp_border)
importFrom(purrr,discard)
importFrom(purrr,imap)
importFrom(purrr,iwalk)
Expand All @@ -129,6 +138,7 @@ importFrom(rlang,ensym)
importFrom(rlang,int)
importFrom(rlang,is_empty)
importFrom(rlang,is_installed)
importFrom(rlang,is_named)
importFrom(rlang,is_null)
importFrom(rlang,set_names)
importFrom(scales,breaks_width)
Expand Down
248 changes: 248 additions & 0 deletions R/ORR_table.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#' @importFrom dplyr select
#' @importFrom GenBinomApps clopper.pearson.ci
#' @importFrom flextable as_flextable surround delete_rows footnote as_paragraph
#' @importFrom officer fp_border
#' @importFrom cli cli_abort

ORR_table = function(id = recist$SUBJID, global_response = recist$RCRESP, date = recist$RCDT, confirmed = FALSE, show_CBR = FALSE){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans grstat, on utilise des interfaces tidyverse, pas base R comme ici
ça veut dire que le premier argument est quasi toujours une dataframe
donc il vaut mieux avoir dans tes arguments d'abord data, puis un moyen d'identifier les colonnes, par exemple subjid="SUBJID", resp="RCRESP".

Attention, tes valeurs par défaut attendent qu'un objet recist existe, ce qui fera planter la fonction dans quasi tous les cas de figures réels.

`%notin%` <- Negate(`%in%`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il y a vraiment besoin d'un opérateur en plus ?

if(x %notin% b) ...
if(! x %in% b) ...

if(length(id) ==0){
cli_abort("id must be defined")
}
if(length(date) ==0){
cli_abort("date must be defined")
}
if(length(global_response) ==0){
cli_abort("global_response must be defined")
}
if(length(id) != length(global_response) | length(id) != length(date) | length(date) != length(global_response)){
cli_abort("id, global_reponse and date should have the same length")
}
if(length(confirmed) > 1){
cli_abort("confirmed shoulb be TRUE or FALSE (default = FALSE)")
}
if(length(show_CBR) > 1){
cli_abort("show_CBR shoulb be TRUE or FALSE (default = FALSE)")
}
if(confirmed %notin% c(TRUE,FALSE,NA)){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tu peux utiliser les fonctions du fichier assertion.R
Par exemple ici assert_class(confirmed, "numeric")

cli_abort("confirmed shoulb be TRUE or FALSE (default = FALSE)")
}
if(show_CBR %notin% c(TRUE,FALSE,NA)){
cli_abort("show_CBR shoulb be TRUE or FALSE (default = FALSE)")
}
if(is.Date(date)!= TRUE){
cli_abort("date shoul be in as.Date format")
}

data = data.frame(subjid = id, rcresp = global_response, rcdt = date)

#Si un patient a une seule visite recist (la premiere) puis aucune autre, on le modifie en Not evaluable
data = data %>%
distinct(subjid,rcresp,rcdt) %>%
arrange(as.numeric(subjid), rcdt) %>%
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cette ligne plantera dans toutes les études où SUBJID n'est pas numérique

mutate(n = n(), .by = subjid) %>%
mutate(rcresp = ifelse(n==1, "Not evaluable",as.character(rcresp)))

data$rcresp <- factor(data$rcresp, levels = c("Complete response", "Partial response", "Stable disease", "Progressive disease", "Not evaluable"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Au secours, du base R 😱😱


recist_2 = data %>%
mutate(rcresp_num=as.numeric(as.factor(rcresp))) %>%
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attention, tu fais des hypothèses fortes sur le contenu de rcresp, et la fonction va exploser à chaque fois qu'une hypothèse sera fausse.
Si rcresp est en majuscules, a des préfixes numéros, etc, ça ne marchera pas.
J'ai fait beaucoup de programmation défensive sur les réponses recist, c'est dans le fichier recist_utils.R. Par exemple tu pourrais utiliser .recist_to_num() par exemple.

filter(!is.na(rcresp_num) & !is.na(rcdt) & !is.na(subjid)) %>%
mutate(previous_rcresp_num=lag(rcresp_num),
previous_date=lag(rcdt),
delta_date=as.numeric(rcdt - previous_date),
delta_date= ifelse(is.na(delta_date),0,delta_date),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

essaie d'espacer un peu ton code : espaces avant et après les = (sauf pour les arguments mineurs genre na.rm=TRUE), espaces après les virgules

delta_date_before_PD_or_end = cumsum(delta_date),
delta_date_before_PD_or_end = ifelse(rcresp=="Progressive disease" ,0,delta_date_before_PD_or_end),
delta_date_before_PD_or_end= replace_na(delta_date_before_PD_or_end,0),
duree_suivi_max = max(delta_date_before_PD_or_end),
bestresponse_withinprotocole=ifelse(previous_rcresp_num==rcresp_num, 1, 0 ),
.by = subjid)

if (is.na(confirmed) | confirmed == FALSE){
final_best_response=recist_2 %>%
mutate(bestresponse=min(rcresp_num),
.by = subjid) %>%
filter(bestresponse==rcresp_num) %>%
slice_head(by=subjid) %>%
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

je crois que tu peux remplacer ces 4 lignes par slice_min(rcresp_num, by=subjid)

mutate(Overall_ORR= ifelse(rcresp=="Complete response" | rcresp=="Partial response",1,0),
CBR = ifelse(duree_suivi_max >= 152 | rcresp=="Complete response" | rcresp=="Partial response",1,0))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pourquoi 152 ?
C'est une valeur fixe pour toutes les études ?
Sinon il faudrait l'ajouter en argument non ?

}
else if(confirmed == TRUE){
recist_2 <- recist_2 %>%
mutate(
meilleur_reponse = case_when(
rcresp_num == 1 & previous_rcresp_num == 1 & delta_date >= 28 ~ 1,
rcresp_num == 1 & previous_rcresp_num == 1 & delta_date < 28 ~ 3,
rcresp_num == 1 & previous_rcresp_num == 2 & delta_date >= 28 ~ 2,
rcresp_num == 1 & previous_rcresp_num == 2 & delta_date < 28 ~ 3,
rcresp_num == 1 & previous_rcresp_num == 3 ~ 3,
rcresp_num == 1 & previous_rcresp_num == 4 ~ 4,
rcresp_num == 1 & previous_rcresp_num == 5 ~ 5,

rcresp_num == 2 & previous_rcresp_num <= 2 & delta_date >= 28 ~ 2,
rcresp_num == 2 & previous_rcresp_num <= 2 & delta_date < 28 ~ 3,
rcresp_num == 2 & previous_rcresp_num == 3 ~ 3,
rcresp_num == 2 & previous_rcresp_num == 4 ~ 4,
rcresp_num == 2 & previous_rcresp_num == 5 ~ 5,

is.na(previous_rcresp_num) & rcresp_num == 4 ~ 4,

TRUE ~ rcresp_num
)
)

final_best_response=recist_2 %>%
mutate(bestresponse=min(meilleur_reponse),
.by=subjid
) %>%
filter(bestresponse==meilleur_reponse) %>%
slice_head(by=subjid) %>%
mutate(Overall_ORR= ifelse(rcresp=="Complete response" | rcresp=="Partial response",1,0),
CBR = ifelse(duree_suivi_max >= 152 | rcresp=="Complete response" | rcresp=="Partial response",1,0))
}

total <- length(unique(final_best_response$subjid))
CR <- length(final_best_response$rcresp[final_best_response$rcresp == "Complete response"])
PR <- length(final_best_response$rcresp[final_best_response$rcresp == "Partial response"])
SD <- length(final_best_response$rcresp[final_best_response$rcresp == "Stable disease"])
PD <- length(final_best_response$rcresp[final_best_response$rcresp == "Progressive disease"])
NE <- length(final_best_response$rcresp[final_best_response$rcresp == "Not evaluable"])
Overall_ORR <- CR + PR
CBR <- length(final_best_response$CBR[final_best_response$CBR==1])

CR_CP <- clopper.pearson.ci(CR,total,CI="two.sided", alpha = 0.05)
PR_CP <- clopper.pearson.ci(PR,total,CI="two.sided", alpha = 0.05)
SD_CP <- clopper.pearson.ci(SD,total,CI="two.sided", alpha = 0.05)
PD_CP <- clopper.pearson.ci(PD,total,CI="two.sided", alpha = 0.05)
NE_CP <- clopper.pearson.ci(NE,total,CI="two.sided", alpha = 0.05)
Overall_ORR_CP <- clopper.pearson.ci(Overall_ORR,total,CI="two.sided", alpha = 0.05)
CBR_CP <- clopper.pearson.ci(CBR,total,CI="two.sided", alpha = 0.05)

CR_CP_IC <- paste0("[",round(CR_CP$Lower.limit*100,1),";",round(CR_CP$Upper.limit*100,1),"]")
PR_CP_IC <- paste0("[",round(PR_CP$Lower.limit*100,1),";",round(PR_CP$Upper.limit*100,1),"]")
SD_CP_IC <- paste0("[",round(SD_CP$Lower.limit*100,1),";",round(SD_CP$Upper.limit*100,1),"]")
PD_CP_IC <- paste0("[",round(PD_CP$Lower.limit*100,1),";",round(PD_CP$Upper.limit*100,1),"]")
NE_CP_IC <- paste0("[",round(NE_CP$Lower.limit*100,1),";",round(NE_CP$Upper.limit*100,1),"]")
Overall_ORR_CP_IC <- paste0("[",round(Overall_ORR_CP$Lower.limit*100,1),";",round(Overall_ORR_CP$Upper.limit*100,1),"]")
CBR_CP_IC <- paste0("[",round(CBR_CP$Lower.limit*100,1),";",round(CBR_CP$Upper.limit*100,1),"]")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attention, beaucoup de baseR et beaucoup de répétitions (DRY), donc risque d'erreur et mauvaise maintenabilité.

final_best_response %>% mutate(total=n()) %>% count(rcresp) devrait être un début, faudrait que je teste pour le reste. On voit ensemble si besoin évidemment :-)


if (is.na(show_CBR) | show_CBR == FALSE){
IC_95 <- c(Overall_ORR_CP_IC,
CR_CP_IC,
PR_CP_IC,
SD_CP_IC,
PD_CP_IC,
NE_CP_IC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bon alors là je vais dire le contraire, c'est un peu trop aéré 😂
Si ça prend trop de lignes c'est aussi difficile à lire.


Name <- c("Overall ORR",
"Complete response (CR)",
"Partial response (PR)",
"Stable disease (SD)",
"Progressive disease (PD)",
"Not evaluable (NE)")

N <- c(Overall_ORR,
CR,
PR,
SD,
PD,
NE)

Percentage <- c(round(Overall_ORR/total*100,1),
round(CR/total*100,1),
round(PR/total*100,1),
round(SD/total*100,1),
round(PD/total*100,1),
round(NE/total*100,1))
}
else if(show_CBR == TRUE){IC_95 <- c(Overall_ORR_CP_IC,
CR_CP_IC,
PR_CP_IC,
SD_CP_IC,
PD_CP_IC,
NE_CP_IC,
CBR_CP_IC)

Name <- c("Overall ORR",
"Complete response (CR)",
"Partial response (PR)",
"Stable disease (SD)",
"Progressive disease (PD)",
"Not evaluable (NE)",
"Clinical Benefit Rate (CBR)")

N <- c(Overall_ORR,
CR,
PR,
SD,
PD,
NE,
CBR)

Percentage <- c(round(Overall_ORR/total*100,1),
round(CR/total*100,1),
round(PR/total*100,1),
round(SD/total*100,1),
round(PD/total*100,1),
round(NE/total*100,1),
round(CBR/total*100,1))
}

Best_Response_during_treatment <- as.data.frame(cbind(Name,N,Percentage,IC_95))
if (is.na(confirmed) | confirmed == FALSE){
nom_col = c("Unconfirmed Best Response during treatment",paste0("N=",total),"%","IC 95%")
}
else if(confirmed == TRUE){
nom_col = c("Confirmed Best Response during treatment",paste0("N=",total),"%","IC 95%")
}

colnames(Best_Response_during_treatment) <- nom_col
Best_Response_during_treatment = Best_Response_during_treatment %>%
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il faut toujours séparer le fond et la forme.
Ca veut dire qu'il faut toujours une fonction qui fabrique les données (dataframe) et une fonction qui les affiche (flextable).
Ca implique de faire un peu de programmation orientée objet, on voit ça ensemble.

as_flextable(show_coltype = F, include.row_percent = FALSE, include.column_percent = FALSE, include.table_percent = FALSE) %>%
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best_Response_during_treatment est une dataframe, donc à mon avis as_flextable() est plus galère que simplement flextable().

delete_rows(i=1, part = "footer") %>%
bold(bold = TRUE, part = "header") %>%
surround(i = c(1,6), j = 1:4, border.bottom = fp_border(color = "black", style = "solid", width = 1), part = "body")

if (is.na(confirmed) | confirmed == FALSE){
Best_Response_during_treatment = Best_Response_during_treatment %>%
footnote( i = 1, j = c(4),
value = as_paragraph(
c("Clopper-Pearson (Exact) method was used for confidence interval")),
ref_symbols =c("*"), part = "header") %>%
valign(valign = "bottom", part = "header")
}
else if(confirmed == TRUE){
Best_Response_during_treatment = Best_Response_during_treatment %>%
footnote( i = 1, j = c(4,1),
value = as_paragraph(
c("Clopper-Pearson (Exact) method was used for confidence interval",
"For CR & PR confirmation of response had to be be demonstrated with an assessment 4 weeks or later from the initial response for response. *As stated in the protocol: «For equivocal findings of progression (e.g., very small and uncertain new lesions; cystic changes or necrosis in existing lesions), treatment may continue until the next scheduled assessment». Therefore, some patients had a response after a progressive disease response, in that case best response was examine before and after PD. Scans conducted after initiating new anti-cancer therapy were not included in the ORR analyses")),
ref_symbols =c("*","1"), part = "header") %>%
valign(valign = "bottom", part = "header")
}

if (is.na(show_CBR) | show_CBR == FALSE){
Best_Response_during_treatment = Best_Response_during_treatment %>%
bold(i = c(1), j = 1, bold = TRUE, part = "body")
}
else if(show_CBR == TRUE & confirmed == TRUE){
Best_Response_during_treatment = Best_Response_during_treatment %>%
footnote( i = c(7), j = 1,
value = as_paragraph(
c("CBR was defined as the presence of at least a partial response (PR), complete response (CR), or stable disease (SD) lasting at least six months (using a window of +/-1 month for the RECIST date).")),
ref_symbols = c("2"), part = "body") %>%
valign(valign = "bottom", part = "header") %>%
bold(i = c(1,7), j = 1, bold = TRUE, part = "body")
}
else if(show_CBR == TRUE & (is.na(confirmed) | confirmed == FALSE)){
Best_Response_during_treatment = Best_Response_during_treatment %>%
footnote( i = c(7), j = 1,
value = as_paragraph(
c("CBR was defined as the presence of at least a partial response (PR), complete response (CR), or stable disease (SD) lasting at least six months (using a window of +/-1 month for the RECIST date).")),
ref_symbols = c("1"), part = "body") %>%
valign(valign = "bottom", part = "header") %>%
bold(i = c(1,7), j = 1, bold = TRUE, part = "body")
}
Best_Response_during_treatment
}
Loading