From 843d35c729c3c7dedd4084cfa39a96a5bf741c5d Mon Sep 17 00:00:00 2001 From: Don van den Bergh Date: Thu, 22 May 2025 14:52:16 +0200 Subject: [PATCH 1/5] properly compute BF_M for non uniform priors --- R/commonAnovaBayesian.R | 41 +++++++++++++++++++++----- tests/testthat/BANOVA_modelPriors.rds | Bin 1565 -> 1788 bytes tests/testthat/test-anovabayesian.R | 6 ++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/R/commonAnovaBayesian.R b/R/commonAnovaBayesian.R index 1550d660..cf9ea71e 100644 --- a/R/commonAnovaBayesian.R +++ b/R/commonAnovaBayesian.R @@ -782,11 +782,15 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio # no errors, proceed normally and complete the table logsumbfs <- logSumExp(logbfs + logprior) - internalTable[["P(M|data)"]] <- exp(logbfs + logprior - logsumbfs) - - nmodels <- nrow(internalTable) - for (i in seq_len(nmodels)) { - internalTable[i, "BFM"] <- logbfs[i] - logSumExp(logbfs[-i]) + log(nmodels - 1L) + logPostProbModel <- logbfs + logprior - logsumbfs + internalTable[["P(M|data)"]] <- exp(logPostProbModel) + + for (i in seq_len(nrow(internalTable))) { + logNumPostOdds <- logPostProbModel[i] + logDenPostOdds <- log1mexp(logNumPostOdds) + logNumPriorOdds <- logprior[i] + logDenPriorOdds <- log1mexp(logNumPriorOdds) + internalTable[i, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds } } else { @@ -795,12 +799,22 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio idxGood <- !is.na(logbfs) widxGood <- which(idxGood) logsumbfs <- logSumExp(logbfs[idxGood]) - internalTable[["P(M|data)"]] <- exp(logbfs - logsumbfs) + + # normalize the prior w.r.t the non-failed models + logpriorSubset <- logpriorSubset[idxGood] + logpriorSubset <- logpriorSubset - logSumExp(logpriorSubset) + + logPostProbModel <- logbfs[idxGood] + logpriorSubset - logsumbfs + internalTable[idxGood, "P(M|data)"] <- exp(logPostProbModel) nmodels <- sum(idxGood) widxBad <- which(!idxGood) for (i in widxGood) { - internalTable[["BFM"]][i] <- logbfs[i] - logSumExp(logbfs[-c(i, widxBad)]) + log(nmodels - 1L) + logNumPostOdds <- logPostProbModel[i] + logDenPostOdds <- log1mexp(logNumPostOdds) + logNumPriorOdds <- logprior[i] + logDenPriorOdds <- log1mexp(logNumPriorOdds) + internalTable[i, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds } internalTable[widxBad, "P(M|data)"] <- NaN @@ -2832,6 +2846,17 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio return(interactions.matrix) } +#' Accurately compute log(1 - exp(x)) +#' +#' @param x numeric value of vector +#' +#' @details See https://cran.r-project.org/web/packages/Rmpfr/vignettes/log1mexp-note.pdf +#' +log1mexp <- function(x) { + a0 <- -0.69314718055994528623 # log(1 / 2) + ifelse(x < a0, log1p(-exp(x)), log(-expm1(x))) +} + # Model prior ---- .BANOVAcomputePriorModelProbs <- function(models, nuisance, options) { @@ -3714,6 +3739,8 @@ dBernoulliModelPrior <- function(k, n, prob = 0.5, log = FALSE) { } + + # Citations ---- .BANOVAcitations <- c( "MoreyEtal2015" = "Morey, R. D. & Rouder, J. N. (2015). BayesFactor (Version 0.9.10-2)[Computer software].", diff --git a/tests/testthat/BANOVA_modelPriors.rds b/tests/testthat/BANOVA_modelPriors.rds index 76cc934cf7c0ec9b0efd5b2d4805bd32b7a371fb..6531b50b59b299c5fc26a0fda359a45e84578f11 100644 GIT binary patch literal 1788 zcmVR0s+0MXZH<~Roc2m{n$pm`^#_pbNh>fKbm_n|%l zZ?|ryksV?@#)l@TKa;x{=mR75yU|M43U@X?kS&BiEQ;&e;vv@$9T;I2IRp1YfSoT; zu${(QLVodJ-^!(_TXEkfF~wXig(nDPbG1e)t6ywR?#P9_$!m2QXN&@}Y{rEcM;q*X z!;_})lhL8LNT0zWQEVwI1bh*l!x!+h2F+iWB=fR!gmtpDwoXXjd>WoF2umu{vvh-8 zX=;#rGw&$Aj_5#V;*9H^C3ML9TWK#Q)H`73A5Zowt4xRdn=If_UEF!)a7jEYtw}PD zf2wPe)aSNf?&MAL&uqIRWUo#|LWN z=4ND}O3@mqr1a_SJw$aU>l6NUVIrT!V~1#>5i|$;ci%&90$wmn$O#efHHXQ;f)Ei~ z$Poy=#XKHMxK@2krL!DNzGH6BH)WwKn0uH-kldnRcSD60*O0Uy=qd+QRKcr6uSEbY zhst|?iEo%AA=l+@a|^ODgp86y^N_ow`C%2{z2tx&UdCcZze61xVVzKNs4;g4ty)lx zZV-H+W2C4+$_N@{q#T@PrP*E;fsZN&{1iw6q?>lxN5X1ol|zro^->PaJ8pcnagh>o zd*x^)FFQonB&jVqw)J~nrK4N>&YTeW@yj__>&p52p6?wDx#8AKvh6VJ97qr4Up?pw&-m&9emM^*|@O3;)|FZmdqlJ)BJ}BVYETJkg zz(@6gbeVOs%S2DeJGR?@T~!QAuGI%UC)Z0KD4to)shK8)tlWHke8#b2U6Z6fu`_P8 zp7+lB?cG2v%c4a^s(z50*WU?mG{n|#6}0w&T)j5PRR->)Y0CAGOK%(y2c0)V&pken zLe*p%Krhi6NNOnGZqkwb6kzQeX%OInCaH&JQ?+b7IL)ZuQfq+;st9l3-vTOK{^y$+3 zuo~KY&~tLV^ntRzb8Oa_2N=Ij_E)Z%r)!eb*TT${?-%z)w|2YZ8lM?0zK|b8G52{E zehWEsl-R$fBp4gzy(=k&b>BcPFJ-nqE!JUeh!3P3mwU8*dzRKf@w4U-n6o`Tr4NX7 z`nipre(0hVHDrlj)L;)fwU;jTKjPm3boIgG1$9<&!(;&MgSv?z!y~O7vON5H*jf8! zkdZz(QDu?6=rnjQA7oCL5Pgae4*AHgSieDmt6-f_K9E^$wX}>eM)w51j=$#EgnH%q zLq@I-XrmbJzZigz>Vs50d3-Fv0&-~3)PNKp7g!Cg>w_MX>!lAK$_fk?B;CgDo(j92 z^b0<^CP{s5#uN>5rlDKAc_)8rs?iUS8{$S=<;4z!TzS_jCR@=7`A*3xX=DYqfR!;) zl0V-|M9)1wkRp+BdF*Ukt${9>l-DI>9{H3$Ffv}w+0z+@E?P!-gZoMIb&#(I`c*0N zRX|4{&Oz>Vl*p_Z+XVbP8 za%4_$K5wD~rjPPL)fJY+)3pTM6W2)2!KpV%kR?SYf`)Fvo{CXED4Mb>;qw}6@KJpb zwp*05Y6I4o-r}J@=|$whYH0I8&&l<6eNb9EFv^yI@e9V9^BY`rPE!Ad{7>HMKIqm~ z6nAadvc2_&Q?_edFyw69e}ctXCKj*aX^^Y!%e+t7uSU;3{_oOu;c(Yl2kc20|95GI zz~u3i=~q685B_rr1I>5)s;j?JAmnM#csX0d>Q0%%;%bf=EaY&piyOMJLPQ)cSMYX_ ebheN$P~XFxw_jf&4iR+^S@kz{PNTg582|u+GHp)) literal 1565 zcmV+&2IBc2iwFP!000001I?IuP!o3)$Cq#j5j2Aoty+yDR11g)Dk2&PB8qZU@G1qH zLZB&07IQF&j#jE77A>NcLjeIn8Lbwj5GsgL4=DEyiX?zgD1mrXiWhC5@ol>_4Sy^% z*_oZ)&+cZw@AuyOeRs3l$q0gIB3c?GL{nS7i)e50T4!m4AfzdB&{1qj@^yGSOTJE$ zuNt%`$h&2P?I||VE(=30%J(V5jA$Wx@~vI~!_S$?^=I1ybP3wKt+y znF)d0o;?n`66uXodJl#~HK+QaTmhZMML9}E3-=^fx-WVLc``ROO}u=l8_pMnCRb_B za0ITKq*Ks%^8!APs)eq_>q}N=Z2*qzwjZ#P*kI?ME_1J{I|BT3rSFs0_@Y63Arpkd7jsd-ccQ7nqkP~dwW}HZMYd7O=RDbyFCMuHx+_7|I0q(TRYL=CUVuAd6IU+CLR(0+1 zLfui0deet4DHSTOc>eH|_GSdpBK_mW+FS;Q$ycb66bC1d%ppgV6U^YT_$XI#m=cWg z1xy|b<#`G@90qT<{MZoj=1_2+LCShS#b!gT30s#OhY)WLb?x3sZ{6ksw-!V>2Ab&t zkBd3rhnKy>Y2vv-f5;PI=71NvNRIHQ&anqp3v&iEmoO+q2y0u$2^2M2V^0C$xb7Nn8Hw|m32kR*2 zAEaUp=`gp-sJ;rgSlBgWvSNR0jq@{h<2K$tXbj;eEgX8=K3-yfl@yxVw-(JlSU(i_<{8I85^5c zihyN@N*88dEE{W(v}cR;J4`)Bm1ML~*Um^m$yuU|zrK+T(?!Lysr<(#w?!iFC!s40_I~r5|MVIiU?GHUVx^rg+a67b3*_ z!EbMiGhC*zFzyjmn{2rcJT86^IY>@?6f_k$)a;L5(LQ)M5$6Z^Jf6HJy6fgy39J@= zfNIySZ+M>q>=1T@ZY!#QY$)@C+k?W`4?P1ALi|7`d1aBipcmuUDL%4Y9%BuX_L`TI z`qhpJs$08n)h@5KU0Z;ABiv$Li@yNQnJ4sVDi6j+*?u>*xb-vO>I%2+=VcA5YmXO_ zSr%OAJC>_dC}I6ZlG_Fswe*9Mwp`&r6ZS`^`Sz{J={|!9@qW;{1j%;E=mQpqm4;ro z-U&P|evo5j6?v5u2Am#q+Iy-$JbVcJn1IXU_xWZa?l*ma)xrmbqt;r9u*;Bjt1pS8BSc9a!HgpqAT|Gs0Yj>XDu1M4S z8f&2V`6f3{8)LP7WD=bxeFfZ7epM7+i@m^BI;Ez}KGmkW_IM#_lG7cF4dzOPZWvUz zCg!B8r5|L6wK-og+5=qb?_Dn~972fq0}1ucne+~tN0FR3wL|xWhY}T_I zI6ObNkhA1l$P;0HaG$(7IIWQkoK!MmP4~mCYC2lkg2|0>((lECyR~OlKR5jon<=k--Q02u%Py@33Z diff --git a/tests/testthat/test-anovabayesian.R b/tests/testthat/test-anovabayesian.R index d76fdc25..325a76bf 100644 --- a/tests/testthat/test-anovabayesian.R +++ b/tests/testthat/test-anovabayesian.R @@ -223,6 +223,12 @@ test_that("Model prior changes posterior model probabilities", { } else { jaspTools::expect_equal_tables(tableModelComparison, reference[[modelPrior]][["modelComparison"]]) jaspTools::expect_equal_tables(tableEffects, reference[[modelPrior]][["posteriorSummary"]]) + + # internal consistency check + BFM <- vapply(tableModelComparison, `[[`, "BFM", FUN.VALUE = numeric(1)) + prior_model_prob <- vapply(tableModelComparison, `[[`, "P(M)", FUN.VALUE = numeric(1)) + post_model_prob <- vapply(tableModelComparison, `[[`, "P(M|data)", FUN.VALUE = numeric(1)) + testthat::expect_equal(BFM, post_model_prob / (1 - post_model_prob) * (1 - prior_model_prob) / prior_model_prob) } } if (createReference) saveRDS(reference, referencePath) From 0798f47100068d3884a468a1e181e6b3bda59662 Mon Sep 17 00:00:00 2001 From: Don van den Bergh Date: Thu, 22 May 2025 14:55:11 +0200 Subject: [PATCH 2/5] cleanup --- R/commonAnovaBayesian.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R/commonAnovaBayesian.R b/R/commonAnovaBayesian.R index cf9ea71e..e06bd9f6 100644 --- a/R/commonAnovaBayesian.R +++ b/R/commonAnovaBayesian.R @@ -798,12 +798,12 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio idxGood <- !is.na(logbfs) widxGood <- which(idxGood) - logsumbfs <- logSumExp(logbfs[idxGood]) # normalize the prior w.r.t the non-failed models logpriorSubset <- logpriorSubset[idxGood] logpriorSubset <- logpriorSubset - logSumExp(logpriorSubset) + logsumbfs <- logSumExp(logbfs[idxGood] + logpriorSubset) logPostProbModel <- logbfs[idxGood] + logpriorSubset - logsumbfs internalTable[idxGood, "P(M|data)"] <- exp(logPostProbModel) @@ -2848,7 +2848,7 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio #' Accurately compute log(1 - exp(x)) #' -#' @param x numeric value of vector +#' @param x numeric value or vector #' #' @details See https://cran.r-project.org/web/packages/Rmpfr/vignettes/log1mexp-note.pdf #' @@ -3739,8 +3739,6 @@ dBernoulliModelPrior <- function(k, n, prob = 0.5, log = FALSE) { } - - # Citations ---- .BANOVAcitations <- c( "MoreyEtal2015" = "Morey, R. D. & Rouder, J. N. (2015). BayesFactor (Version 0.9.10-2)[Computer software].", From 001b08e4b92127193f03ef637035f359a5932dc6 Mon Sep 17 00:00:00 2001 From: Don van den Bergh Date: Thu, 22 May 2025 15:00:11 +0200 Subject: [PATCH 3/5] vectorize instead of loop --- R/commonAnovaBayesian.R | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/R/commonAnovaBayesian.R b/R/commonAnovaBayesian.R index e06bd9f6..6958d70c 100644 --- a/R/commonAnovaBayesian.R +++ b/R/commonAnovaBayesian.R @@ -785,13 +785,11 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio logPostProbModel <- logbfs + logprior - logsumbfs internalTable[["P(M|data)"]] <- exp(logPostProbModel) - for (i in seq_len(nrow(internalTable))) { - logNumPostOdds <- logPostProbModel[i] - logDenPostOdds <- log1mexp(logNumPostOdds) - logNumPriorOdds <- logprior[i] - logDenPriorOdds <- log1mexp(logNumPriorOdds) - internalTable[i, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds - } + logNumPostOdds <- logPostProbModel + logDenPostOdds <- log1mexp(logNumPostOdds) + logNumPriorOdds <- logprior + logDenPriorOdds <- log1mexp(logNumPriorOdds) + internalTable[["BFM"]] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds } else { # create table excluding failed models @@ -800,22 +798,20 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio widxGood <- which(idxGood) # normalize the prior w.r.t the non-failed models - logpriorSubset <- logpriorSubset[idxGood] + logpriorSubset <- logprior[idxGood] logpriorSubset <- logpriorSubset - logSumExp(logpriorSubset) logsumbfs <- logSumExp(logbfs[idxGood] + logpriorSubset) logPostProbModel <- logbfs[idxGood] + logpriorSubset - logsumbfs internalTable[idxGood, "P(M|data)"] <- exp(logPostProbModel) - nmodels <- sum(idxGood) + logNumPostOdds <- logPostProbModel + logDenPostOdds <- log1mexp(logNumPostOdds) + logNumPriorOdds <- logprior + logDenPriorOdds <- log1mexp(logNumPriorOdds) + internalTable[idxGood, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds + widxBad <- which(!idxGood) - for (i in widxGood) { - logNumPostOdds <- logPostProbModel[i] - logDenPostOdds <- log1mexp(logNumPostOdds) - logNumPriorOdds <- logprior[i] - logDenPriorOdds <- log1mexp(logNumPriorOdds) - internalTable[i, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds - } internalTable[widxBad, "P(M|data)"] <- NaN internalTable[widxBad, "BFM"] <- NaN From 703fa62c1198a4fcb956d5442b2c01cb99f25166 Mon Sep 17 00:00:00 2001 From: Don van den Bergh Date: Thu, 22 May 2025 15:16:38 +0200 Subject: [PATCH 4/5] fixes to make tests pass --- R/commonAnovaBayesian.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/commonAnovaBayesian.R b/R/commonAnovaBayesian.R index 6958d70c..6c57d1d0 100644 --- a/R/commonAnovaBayesian.R +++ b/R/commonAnovaBayesian.R @@ -803,7 +803,7 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio logsumbfs <- logSumExp(logbfs[idxGood] + logpriorSubset) logPostProbModel <- logbfs[idxGood] + logpriorSubset - logsumbfs - internalTable[idxGood, "P(M|data)"] <- exp(logPostProbModel) + internalTable[widxGood, "P(M|data)"] <- exp(logPostProbModel) logNumPostOdds <- logPostProbModel logDenPostOdds <- log1mexp(logNumPostOdds) From a3133c1b11c5049aa6ddb274e6188f171e01c731 Mon Sep 17 00:00:00 2001 From: Don van den Bergh Date: Thu, 22 May 2025 15:29:30 +0200 Subject: [PATCH 5/5] some more fixes --- R/commonAnovaBayesian.R | 4 +- .../anova/raincloud-plots-horizontal.svg | 136 +++---- .../_snaps/anova/raincloud-plots-vertical.svg | 136 +++---- .../raincloud-plots-between-subjects.svg | 208 +++++------ .../anovarepeatedmeasures/raincloud-plots.svg | 344 +++++++++--------- 5 files changed, 414 insertions(+), 414 deletions(-) diff --git a/R/commonAnovaBayesian.R b/R/commonAnovaBayesian.R index 6c57d1d0..095654c1 100644 --- a/R/commonAnovaBayesian.R +++ b/R/commonAnovaBayesian.R @@ -807,9 +807,9 @@ BANOVAcomputMatchedInclusion <- function(effectNames, effects.matrix, interactio logNumPostOdds <- logPostProbModel logDenPostOdds <- log1mexp(logNumPostOdds) - logNumPriorOdds <- logprior + logNumPriorOdds <- logpriorSubset logDenPriorOdds <- log1mexp(logNumPriorOdds) - internalTable[idxGood, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds + internalTable[widxGood, "BFM"] <- logNumPostOdds - logDenPostOdds + logDenPriorOdds - logNumPriorOdds widxBad <- which(!idxGood) diff --git a/tests/testthat/_snaps/anova/raincloud-plots-horizontal.svg b/tests/testthat/_snaps/anova/raincloud-plots-horizontal.svg index 2206b22b..21990290 100644 --- a/tests/testthat/_snaps/anova/raincloud-plots-horizontal.svg +++ b/tests/testthat/_snaps/anova/raincloud-plots-horizontal.svg @@ -21,77 +21,77 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -1 -0 - - - - - - - - - - - --4 --3 --2 --1 -0 -1 -2 -3 -contNormal -contBinom -raincloud-plots-horizontal + +1 +0 + + + + + + + + + + + +-4 +-3 +-2 +-1 +0 +1 +2 +3 +contNormal +contBinom +raincloud-plots-horizontal diff --git a/tests/testthat/_snaps/anova/raincloud-plots-vertical.svg b/tests/testthat/_snaps/anova/raincloud-plots-vertical.svg index 63f66743..679848cd 100644 --- a/tests/testthat/_snaps/anova/raincloud-plots-vertical.svg +++ b/tests/testthat/_snaps/anova/raincloud-plots-vertical.svg @@ -21,77 +21,77 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - --4 --3 --2 --1 -0 -1 -2 -3 - - - - - - - - - - - -1 -0 -contBinom -contNormal -raincloud-plots-vertical + +-4 +-3 +-2 +-1 +0 +1 +2 +3 + + + + + + + + + + + +1 +0 +contBinom +contNormal +raincloud-plots-vertical diff --git a/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots-between-subjects.svg b/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots-between-subjects.svg index 594103ac..271abe51 100644 --- a/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots-between-subjects.svg +++ b/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots-between-subjects.svg @@ -21,114 +21,114 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -20 -40 -60 -80 -100 -120 -140 - - - - - - - - - - -Male -Female -gender + +20 +40 +60 +80 +100 +120 +140 + + + + + + + + + + +Male +Female +gender raincloud-plots-between-subjects diff --git a/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots.svg b/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots.svg index ff198058..3ab58155 100644 --- a/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots.svg +++ b/tests/testthat/_snaps/anovarepeatedmeasures/raincloud-plots.svg @@ -21,182 +21,182 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -20 -40 -60 -80 -100 -120 - - - - - - - - - - -High -Some -None -Charisma + +20 +40 +60 +80 +100 +120 + + + + + + + + + + +High +Some +None +Charisma raincloud-plots