\ No newline at end of file
diff --git a/radiant.basics/_pkgdown.yml b/radiant.basics/_pkgdown.yml
new file mode 100644
index 0000000..36f5023
--- /dev/null
+++ b/radiant.basics/_pkgdown.yml
@@ -0,0 +1,136 @@
+url: https://radiant-rstats.github.io/radiant.basics
+
+template:
+ params:
+ docsearch:
+ api_key: 0629d253426ce7046f92e2bc5bb11b03
+ index_name: radiant_basics
+
+navbar:
+ title: "radiant.basics"
+ left:
+ - icon: fa-home fa-lg
+ href: index.html
+ - text: "Reference"
+ href: reference/index.html
+ - text: "Articles"
+ href: articles/index.html
+ - text: "Changelog"
+ href: news/index.html
+ - text: "Other Packages"
+ menu:
+ - text: "radiant"
+ href: https://radiant-rstats.github.io/radiant/
+ - text: "radiant.data"
+ href: https://radiant-rstats.github.io/radiant.data/
+ - text: "radiant.design"
+ href: https://radiant-rstats.github.io/radiant.design/
+ - text: "radiant.basics"
+ href: https://radiant-rstats.github.io/radiant.basics/
+ - text: "radiant.model"
+ href: https://radiant-rstats.github.io/radiant.model/
+ - text: "radiant.multivariate"
+ href: https://radiant-rstats.github.io/radiant.multivariate/
+ - text: "docker"
+ href: https://github.com/radiant-rstats/docker
+ right:
+ - icon: fa-twitter fa-lg
+ href: https://twitter.com/vrnijs
+ - icon: fa-github fa-lg
+ href: https://github.com/radiant-rstats
+
+reference:
+- title: Basics > Probability
+ desc: Functions used with the Probability Calculator and the Central Limit Theorem simulator
+ contents:
+ - clt
+ - plot.clt
+ - prob_binom
+ - summary.prob_binom
+ - plot.prob_binom
+ - prob_chisq
+ - summary.prob_chisq
+ - plot.prob_chisq
+ - prob_disc
+ - summary.prob_disc
+ - plot.prob_disc
+ - prob_expo
+ - summary.prob_expo
+ - plot.prob_expo
+ - prob_fdist
+ - summary.prob_fdist
+ - plot.prob_fdist
+ - prob_lnorm
+ - summary.prob_lnorm
+ - plot.prob_lnorm
+ - prob_norm
+ - summary.prob_norm
+ - plot.prob_norm
+ - prob_pois
+ - summary.prob_pois
+ - plot.prob_pois
+ - prob_tdist
+ - summary.prob_tdist
+ - plot.prob_tdist
+ - prob_unif
+ - summary.prob_unif
+ - plot.prob_unif
+- title: Basics > Means
+ desc: Functions used with Basics > Means
+ contents:
+ - single_mean
+ - summary.single_mean
+ - plot.single_mean
+ - compare_means
+ - summary.compare_means
+ - plot.compare_means
+- title: Basics > Proportions
+ desc: Functions used with Basics > Proportions
+ contents:
+ - single_prop
+ - summary.single_prop
+ - plot.single_prop
+ - compare_props
+ - summary.compare_props
+ - plot.compare_props
+- title: Basics > Tables
+ desc: Functions used with Basics > Tables
+ contents:
+ - goodness
+ - summary.goodness
+ - plot.goodness
+ - cross_tabs
+ - summary.cross_tabs
+ - plot.cross_tabs
+ - correlation
+ - summary.correlation
+ - plot.correlation
+ - print.rcorr
+ - cor2df
+- title: Data sets
+ desc: Data sets bundled with radiant.basics
+ contents:
+ - consider
+ - demand_uk
+ - newspaper
+ - salary
+- title: Starting radiant.basics
+ desc: Functions used to start the radiant.basics shiny app
+ contents:
+ - radiant.basics
+ - radiant.basics_viewer
+ - radiant.basics_window
+articles:
+- title: Basics Menu
+ desc: >
+ These vignettes provide an introduction to the Basics menu in radiant
+ contents:
+ - pkgdown/clt
+ - pkgdown/prob_calc
+ - pkgdown/single_mean
+ - pkgdown/compare_means
+ - pkgdown/single_prop
+ - pkgdown/compare_props
+ - pkgdown/goodness
+ - pkgdown/cross_tabs
+ - pkgdown/correlation
diff --git a/radiant.basics/build/build.R b/radiant.basics/build/build.R
new file mode 100644
index 0000000..c901d8c
--- /dev/null
+++ b/radiant.basics/build/build.R
@@ -0,0 +1,87 @@
+setwd(rstudioapi::getActiveProject())
+curr <- getwd()
+pkg <- basename(curr)
+
+## building package for mac and windows
+rv <- R.Version()
+rv <- paste(rv$major, substr(rv$minor, 1, 1), sep = ".")
+
+rvprompt <- readline(prompt = paste0("Running for R version: ", rv, ". Is that what you wanted y/n: "))
+if (grepl("[nN]", rvprompt)) stop("Change R-version")
+
+dirsrc <- "../minicran/src/contrib"
+
+if (rv < "3.4") {
+ dirmac <- fs::path("../minicran/bin/macosx/mavericks/contrib", rv)
+} else if (rv > "3.6") {
+ dirmac <- c(
+ fs::path("../minicran/bin/macosx/big-sur-arm64/contrib", rv),
+ fs::path("../minicran/bin/macosx/contrib", rv)
+ )
+} else {
+ dirmac <- fs::path("../minicran/bin/macosx/el-capitan/contrib", rv)
+}
+
+dirwin <- fs::path("../minicran/bin/windows/contrib", rv)
+
+if (!fs::file_exists(dirsrc)) fs::dir_create(dirsrc, recursive = TRUE)
+for (d in dirmac) {
+ if (!fs::file_exists(d)) fs::dir_create(d, recursive = TRUE)
+}
+if (!fs::file_exists(dirwin)) fs::dir_create(dirwin, recursive = TRUE)
+
+# delete older version of radiant
+rem_old <- function(pkg) {
+ unlink(paste0(dirsrc, "/", pkg, "*"))
+ for (d in dirmac) {
+ unlink(paste0(d, "/", pkg, "*"))
+ }
+ unlink(paste0(dirwin, "/", pkg, "*"))
+}
+
+sapply(pkg, rem_old)
+
+## avoid 'loaded namespace' stuff when building for mac
+system(paste0(Sys.which("R"), " -e \"setwd('", getwd(), "'); app <- '", pkg, "'; source('build/build_mac.R')\""))
+
+
+win <- readline(prompt = "Did you build on Windows? y/n: ")
+if (grepl("[yY]", win)) {
+
+ fl <- list.files(pattern = "*.zip", path = "~/Dropbox/r-packages/", full.names = TRUE)
+ for (f in fl) {
+ file.copy(f, "~/gh/")
+ }
+
+ ## move packages to radiant_miniCRAN. must package in Windows first
+ # path <- normalizePath("../")
+ pth <- fs::path_abs("../")
+
+ sapply(list.files(pth, pattern = "*.tar.gz", full.names = TRUE), file.copy, dirsrc)
+ unlink("../*.tar.gz")
+ for (d in dirmac) {
+ sapply(list.files(pth, pattern = "*.tgz", full.names = TRUE), file.copy, d)
+ }
+ unlink("../*.tgz")
+ sapply(list.files(pth, pattern = "*.zip", full.names = TRUE), file.copy, dirwin)
+ unlink("../*.zip")
+
+ tools::write_PACKAGES(dirwin, type = "win.binary")
+ for (d in dirmac) {
+ tools::write_PACKAGES(d, type = "mac.binary")
+ }
+ tools::write_PACKAGES(dirsrc, type = "source")
+
+ # commit to repo
+ setwd("../minicran")
+ system("git add --all .")
+ mess <- paste0(pkg, " package update: ", format(Sys.Date(), format = "%m-%d-%Y"))
+ system(paste0("git commit -m '", mess, "'"))
+ system("git push")
+}
+
+setwd(curr)
+
+# remove.packages(c("radiant.model", "radiant.data"))
+# radiant.update::radiant.update()
+# install.packages("radiant.update")
diff --git a/radiant.basics/build/build_mac.R b/radiant.basics/build/build_mac.R
new file mode 100644
index 0000000..1452bac
--- /dev/null
+++ b/radiant.basics/build/build_mac.R
@@ -0,0 +1,6 @@
+## build for mac
+app <- basename(getwd())
+curr <- setwd("../")
+f <- devtools::build(app)
+system(paste0("R CMD INSTALL --build ", f))
+setwd(curr)
diff --git a/radiant.basics/build/build_win.R b/radiant.basics/build/build_win.R
new file mode 100644
index 0000000..e6861ce
--- /dev/null
+++ b/radiant.basics/build/build_win.R
@@ -0,0 +1,26 @@
+## build for windows
+rv <- R.Version()
+rv <- paste(rv$major, substr(rv$minor, 1, 1), sep = ".")
+
+rvprompt <- readline(prompt = paste0("Running for R version: ", rv, ". Is that what you wanted y/n: "))
+if (grepl("[nN]", rvprompt))
+ stop("Change R-version using Rstudio > Tools > Global Options > Rversion")
+
+## build for windows
+setwd(rstudioapi::getActiveProject())
+f <- devtools::build(binary = TRUE)
+devtools::install(upgrade = "never")
+
+fl <- list.files(pattern = "*.zip", path = "../", full.names = TRUE)
+
+for (f in fl) {
+ print(glue::glue("Copying: {f}"))
+ file.copy(f, "C:/Users/vnijs/Dropbox/r-packages/", overwrite = TRUE)
+ unlink(f)
+}
+
+#options(repos = c(RSM = "https://radiant-rstats.github.io/minicran"))
+#install.packages("radiant.data", type = "binary")
+# remove.packages(c("radiant.data", "radiant.model"))
+#install.packages("radiant.update")
+# radiant.update::radiant.update()
diff --git a/radiant.basics/data/consider.rda b/radiant.basics/data/consider.rda
new file mode 100644
index 0000000000000000000000000000000000000000..d00b31838f8bf9cbe39ca333d1fba82a47346ba9
GIT binary patch
literal 3248
zcmV;h3{UePiwFP!000001I?Ltd?i&C$0wWH@0}U2Vjo>mz%}fG4HXeV1r-Y*U}YzH
zliepT@5T3CHpGg(_ulKr-h1!8_uhNQUXkT{@-_>s2-%S2vwz&o%)RHH`a9))a^kV4
z9TrXtS(a_c#`RdXVVqaBfj?uJ$~;du)$0!0t)`!4HRA#a>h9 zFYjR-YtLWFTG+4rUoPx3W3MH9nSC_x-p+;YQtXpHoPN(^O>92z8T*gS#rp)kB_r)O z)AseO`4QYB+w2>7<|?1T`+w&7vGm7d(*y3s2hr2wLzwf6=>G-u;Y#t-*>}?Zb&Q+Q zhX2jKmHVG$57#mNZ|Q$O^Il6I@AJ4eY OGp9OLy4e!Jc zMMumzdkO3R8|(Z8&%e#Q#6W~UMSsUL^G9G=gsxI#oFj9|{tLNo;3C%27x11qh>4Gc zxZ4o?`y~3lns)Fh`y<-WNr^wijG5tIDfY`H#)+>P`_6xWF+Wd#{Hr8ZQgoTTl?yD4 zL~k#m5572mA{RPJ1P|ZF{ks@ @-cKjkDeoK6@WRLL&^DKMjN(9Hiy5f=aB_`$>@7VW@m@mjd znItu^D7%Vv@vAxhlp )ASY0d=hgAo{)29*fnNvv#;O+gA)42;7ayB#*oKyVr)( >2Z%3+94(#mBJ@dYB_?#=aJ;G5>YeV6K?9;^TS$TE-)P ziXImK#eC$f9D6JzmRYaFZ%Qy><1FI@_lT>U{1Ssrg~T~y7W`(|Y$2F}ZX?OJf@kcr z1i$8Y9a4eGoEV5@kLW29tS7EY;$(qr1#y~T&r d1P(%#O zB+sIUl( CkJDGMn7!oKFZ*xqiKzv+mVnJU{*|%+ zRBVXf#o*6^+&jl-WArm8cQ4=`$-mPVK2adcf;DsEq4*iba9<+V24kZKPk7IQ*qcf& z0iO%vIXWGDcq+Of{l7EGpl;0S9JSEnryvwk6N*v^m z3^)*pZjiY|*7Pkw{yBL+#TU{d_)>HebAokw7C3=mNk(kU%YcD`9n(ki9z06mXF0L5 zjxT4#TKckpPZ_x_`X}0ny*YlohV3#q#r$H(hY@}dVaF5<$jOzt_%V9VkSQbXGJGXx zjRbC%g4yfnDI-p%;O_Jdfqw} V%vFwRBiolqR*qBJnJP@!l7C!dGa4$Gg zFkdO}v0;hcX7IEW8Dq&)G6uXT+mJh_FQp%{rgsOOOkK#~k>CJWxdD!(FAg|TN-iUA zrsVN8;j&^s{3(OmX5_r&g@J#@ #GICgf&*b=8hJTgc&pYJG@S9ll zOzf?rgA@$T;bTyvNrLWX;updX1p|n)nD|-~jC@DPMG11G &ox!(ai8Jzc^5}p& zspuBpkMQF);Uwgln7o$@-jE-&*M&7x!Bp}@PVR`W3Y>TyeZ}BviCq$Om5Oh({{pN_ z(NRKt%#kqx;|lB_A^!pnHJ5nb4}3qxRvGfG;rsD9`r cP_?8awed_88~ zb@W#XZ$1++U ~`D6Wb7@OY|xJ4`& zitkRCW5S-N=zjg~kgunLfA@u)84ItyC)6;LTLQlq--{&{ D<@!G_NL&!2{^R=kRvQ*okZ#j zczXt~OYzA9JcwQ!@_q^CWg?UK0P;l-9JQh6l-yJZ4&EDlI)#s{N$g I{9 z@q6mqsbI`Q17=TN9c+>b*P#Z?uL;~CeNL#6OL#_hG1N>6^USbuj{YL}&gA(=*d#c9 zHt@3K*&%k;;8YQNo+9Iv{jAad3dy_ohdN~Tm{5yK46dW=5 e`K(aarc%?Or{tcX_kz96#2&<0BDnv~P!mM=1r8b`a|GX= 1a%0GA!b7t=1BcE; zu5(BJDx93XUn%?pZd>AWnT)}9bM%&y6Xuft@#*z92R<(ORO*y>@a6=ZpZZYS3g?eyuD1t{B5^Hsa(Y*&zp_V!T5# 6qwf#?SiUOM266@ d43zvo?LNAh{B6<|KP&j_OcnqzPYr?d)K;4o9e0_D%I5WL#yWMD>oc96`^Su z>spQTgZGy GNNzlN;SE$ z?N@5fS5qmgHshf7*S2U>dqdNz&9q}R^xBPlqqN{VTto}iYY(Z1p;H6hfDtbh-Fq!n z%?2;sO<%Wae|~KAYE?UGV>#<-ZD$5Hb+0ts40Wv?xvd8!DBf`#g)6a!);LVnX@yqq zGGe*zQoYnVx@ufqyB?0#8l_jtG}zrM|8J{W`@hFQmD*jeU9TN{qo?XyjYY0=jnb8` z#zu=(*Q1u9uDz0>tDNoxhBex)nq$N|c92nfD8B9Wc52;J4^6A_u<3ghr+qtUyc)5p zwsSvfgLT!==mqOh2Y5XiOCy4Y5ByJuPSqK?oU7Dat{=R%7_O{!+SH?(o}kcR&W4 *;hbkFVr
cI$QOF zwmn%#=QQ6o-jE=taiFfoPOH8#HmloC16$MBxE}z0oh!yp(*&>a*1N9OSsQ(+CO5Q| znra+PSk$e~V2+^E>7yGoldXwg?cj|`VI=9QzElR+YBJc2C%j}8J d)&u(3#N3(pt%* z&V_YtXiw8th6%OCgoK5O;MO=z+jb`LfNnau52KDJv9#WF@6B9ouj@HSD$oN5XB=>F z1{NV58W!O+%80(xZrTb`YTp`l5rMLFjpC}xYpfxHbt lo^;+{3_qz5)4H~21{;jrH+nC@q)VOh`O84~ANCtWv zQei3=0;M&lD4oX6#x** F}YG4r@P{Py~Q99=KMQ7 ftE!&8| mscGl!ag{SC8w8WqF`Por zKKe%OuNw@63@1ElrG(y;INsKgII{LWxPn34lgjS6em7?kV5!D|!DJrFcLPS_P$?H3 zEA1UE3RM}n9!v%XAyrK}x?Xqc%;df`EJx1zOjE5Xny&^; )KWy(Lt;%i?TH-310&+7^%sA=QTO0yILz1-Do8B-KbreLVr}G&}pff zs@K4Si?$jphF+iNG;mPeG#ZEm@eM%-{S4HZ<8(Q`H0i3Zl@ZB~I(8`64O{7~BYAT~ zM-DYO-RhXek<3>N46Ab=#f1(rLqnYRrrzjHw8|$Ko!i?{%ixF$4f%Mp{bFU0@V!1$ zmlgF%Q)d{u_`FFw243l;( zMSFYO3naz$;}ct2W>Q5n~*&6k( Sc?QN?$25HXzp@8{b z*HTF8?{$?4DEOE8_|s^`x4k8uP<#7z&Yp!kQ{zC}%pbED^I&N6mpPK4lmdHQm()O@ z`Lh6T{!FTCowh}zRnyidABR^a^yUl@L^5aG=96Jx8;vrz{YYR=az}6K@Xk7^Yc#L` z)ut9}ty#UP>+ocAtlstJEZlkhCjj8GIbdvb;UF=m;b(L@rYT&_A2Rio`BMnO+?*zQ z{S`;Zmx{y2YX~Ho^g%Awvpy6-bp8FLG3A+1AswwyYE$&I=2KbKHTp2cr1RG6zdE2$ zHebqmtI^t=#<%42m9CEHm??rF9p?O;z|?kbt$)Q6??t?m6&dpj2ob}sBr z4*zI$^=j> CS9Y@fNnTwAy8?d+bR%l7j8h24X*@_Kvo z)Y-5|=p1e@ZtiZMyX>O-+Xu^24Bp(?-9Mb^_RYPW&0EiHU$eaW_}^JyeeE)6;HIOO z`$h5i)qXe6Z!hoIVRcgN^1`kA+k3YkiZ0VF>*M)~;!(?;t>t5v*9YOYws#M9PVa2* z(SE!TuWz_;uzBuekH`BKE$lBZZtsOHDSO86_vgQp^`pl1ovow0B&?tZojSL 8T3NDQH(mLL%%4foF8ncbOP+rRG2J^!3J=X1_?&i9z- z>+Ag%+p0!D_f4;wbuH<1wz)C*y K3QsN~tux-WRn0#8E5j8#C&A$9dd7)*+sfGq%9( <8d6yW4r7F$a@)ydfqU-UH?B=^YR zzBB`l;|Q@BkEpy$((acA93KSIK%A0dSu0uk^;7;6x(PJ^#V65?sQ!wol-B~ST<3s? zeP#a0P>_v0(*LlC?}ZExEowpHNd91c7JW}VoXmer|13VhywW<(Pn_5EI9@KJTlZJY z$EEvrMuWt+(GSaz#^Sp$50+rTp-5d0McR@O25|}EY;{#8Jx5)so7K}C#^rtd2rrM^ z8E3b0HrMlAmBo !wespk6Lgfi>1T z!c9a*x% 3JKfs_L zVE7L(lfPX4yB$Ws-x)C2bSe3JwuR#@-|+u!qOeK|uA^ n126+$O*u9yR(a|DaoNE2YAJw1W|E?T`D6(5RrN)fL9YtNRg!m@f9nom~-~zTc^#S@Ia%M^^ohJwu z#LI-(cs0+JQrpO_kfc`6moz0vwRCbX-HuD5u~947O?Ja_I0C7BfR c= zx}I@~hD^Q_+Yx)vb*Y#s#0kHV$^t}z!o>d!PL5-4-nj8yf4bCd{W^sKb0r2-uuJP> zJ)!x>mqUdqm@DnE{!G>p%8ZMycJ`&GP^@&ujAQ-4Mf-`0fX=$XrP(R_+Wxfytzp%% z=^s8nVYw+giCY_fOM4}Gg1=tsi(t5$Vq?TyL2;|_TbZHV^J>$fOJ1q6W{wA<(!@9` z!xJY9xHeh+;wACY?fv}jcdt#L;|P5Ug6?(~D=Nb*AVnn>ClMH-cB&8~U74;)vnLG- z&y&h)l|7;4vVy{*E90yEq%QVqFImZ6?;VBBt*}$dQs5N3#fG08g;*n8mo%2c8FA*4 zDFN@V5_`cjY^D0*r!Yks dwFPm3gsvO;%ekyro& zjZCLGZmeb|raWWqRb_YUE-q_fmx8=E4r8Karyd0DoP#EwN1Yz-S*slFw2x`VQX^bj zoOby}g(>pCe>J)5GSRgqqQMCgx4jNhr)ISe<2OQ`0nYH7_{Q~X!Nk<<#ut3gA^15~ zeq)glLxYf*k~0xb4(zhIdLh`<&g~j91bHWWtna%MCfy-UAG1#$EtT9lW13Ycyx*VX zQFX`k^E}BShx$~#9Mp|_N+ym82{+u&D$pIP^!_ed3%Qh*@QK%p$)pflqnYMFWzqo{ z Y6zTZXL~AvG7?EZ zao@bL%Sro;o(k|<%cX7}r@&Cfb!K!j&*Sn!RE|khw>8xgq#u?Ml6Ncbsi^1N7tYMv ze*QLijComy+Oqxr^H9?j&=P;G=0JK-_oE?bOw=S#pXzx3L~Y$|OKU5J(PA^j@i7;y tvsi6jk%|+2qZ8N50~N0;Z4NSiJzOe=x<&i%$`oA&We2A0J8!P1_iwGGo815a literal 0 HcmV?d00001 diff --git a/radiant.basics/data/salary.rda b/radiant.basics/data/salary.rda new file mode 100644 index 0000000000000000000000000000000000000000..811a3223f58c686fb16683300103e95d6bad34b6 GIT binary patch literal 3304 zcmV WhsiwFP!000001Lax^a8%V9{?FOXYd7zALP%~%AOVt?1bI17jg3_Cl>$W? zTj8>M6IM36>Fy%NM`aY1f%+%~Md||)wOSml+Np}x$2i*hs1$9jt@Tl>s6|UVKG3$% z@9zB%_vTJQ5=qO9X6EOf```ckkMIA^Id^xN*S@ILx5!6Ck~}Gbqz6N<#6N<(l!0q% zTo3E9J|a(nc?ZeD!z)Nuze94{)g-20M&j`qB k{wIBGSC=6gt7w}iy6ZzQpH4T%lqBzB)hvZS8GcmGV{>ibDVc9M8-JBixUNZd4y zWcJ%6+MXrR@GBC}zd+J^D~TBmB%VnpF;lk$&*YLW65ljPyq5{zb4ku>ClP+1#Jc;) zv!;(kFJeE|N#dd(lI-{c$(rLx6!wz%@CFihoJ^wTNfKMGBC)Z9#5YjOvxv8(o5baH zB*xcb{1~o(BeDGz D~Y9v;iPvY|*khtnh b%stOWMe2>K28xj9QqK~(c=mWkj^GGfZ zTQ(1UpTt (Kklwvaq& zZ?dn_k6bs-A#uwdaB&vNMc0sgR3qsFw_g1Sy#&7&V-4&;UT0TWe%^YM#G~!j{fiq& z#;ztg|9+CI(YwBCl6~OGORx`aBYEK}5`lA3D>(8o{OrM6c>#4ze~RQW$mPdaTifT6 z*!3LAP2kM*W^m$4^c~nNgzak@(P!6_$U~lsqR2PX@>jWl ?-xEoMkY^`2x2=-IE68OJ^1S!~tX >0+O4(z_kH+-D}}qgZtN3TfWwz2i?HuHsst3 zoG(9swTyb^0*|8WNTls1nFS8y#8DG;D>fs}>m*m4ioV_jJFKDIlfbv7Brdrd^`A%b z!~c+YdIDm;PjcEbBsQHz@^o YNrypZqD8;^Y T{1tex) zN}h4(-CFFcPhbzb{X+CJg8ar|O=Df(jXh%fI$-`1i61Qo?%;4O;y0FIpF&R8JV>Jc zRgyQO5A&ne8hQ`=(l0lYta_2eo!Gy2Vf_2sNv?gK ylV7(Y9KX)uA-f{?^?mPy=-R;-y(~Y@XKUmI#=?BZlUF)H^ _tAhu;Z2VFyGjX! z^| $K-X>7g>a-@6sRgxp!>n{l(%%esg zAnSCIe7cA>H8|d~@F dE>>u9EMIAo-9?NSdRMg{(8@Pk@Yv z)Iq8swUBX;QY%i @SOBd--`W>nB3YtV>wxIgS~xjPFZ$Z01zKe*Ijv zInHJq(~tRmWIB({)4!T+y{s$d9P`*$9@jsX^%Xqd)R(gl6Q{|nE9QH1oWpoX&QHZx zGJ!GivV9igZPq-AW92eN`HZQb=jL$jg^Y!VV`Z|>O74Tg)5JjIUX<~58pn|Q%**~~ za10OET*!IkvA&4? WmT<4LSYN^Zin(_MEX~{}#l>;lCokizI8i`0pG|*hWUrq(uBn`C z9-8-K84tA F-oGQ*$ zt(Ri9HDhaJaj=o?su=HD#>%YWD9(Kni wo%+yJa1vU3dTY0VU-+zGD{BURzo(wOSryD z>c1{iIhLQ}jNxkoj}=G9vRyS}U&-St?sXZ@Q)fp$sn1GQ&J#IS3HM6jl}`5AO04tn z^Koon%UD!#+yb(BRl@ef_g?mwLH4$zFrzIEDQ= z&kR*ZmBU`~-qhuB9g^!WaMY(hbrz6)hC26o$+(#PcAgz+oJ$&u+AoOgGdY{FE8|#d z-}jMyZYilEoBM)e5!X1D^B0_}Iy;IT(!#Nb`=t6Xll=?ML)DVaIVv9b9N4+f4t1`o z^Fp1QMD}}#`Ik!S-N(W+jr#K=m-VSUN1Yv|j9sb&+YHW8ogFFcqnPY o(h=W3p--lNp|Nh6ydeirpU xi#vZB%ey6c)AY$Io-?SeHxoJ7)v+KM&2n9I|;)z%?kWGPu56 zwo~txe!f?})cc=0J7$s1WyMQ%UT1P&a@dda?5N>6V;EZx=h@=GM)kjlaW&_tcQ2J! zCHtMsHa?zH$#K%jejhLA9Lvex2PUwMhwS~Ofn#QneTFE^74OtOuIlrS@O!V}er6oV zZX|O@`q^}#IV16LIQfngt1serv}+j-R)_l?!T$o&(ZK6SfA4WP+#V^%hx2^2@i@}> z{=YafSWiY93o*+6Mo|X*-Ya#E(W8eA^<&Jb=6D$2fA#;lCZhKk>gSqN*MCxD$1y#! zoX5UkC>{v)hC>k*HmffdZ;FQ^0i&sRS&)_fq|%6;6ACChPuw`y6+0sqUD>4Kn=dAE z75+JJV1L|Y=noq4KrGao2t_0PEAvdPsnO<}*@ld`))kFu=1)>aJRXh3eZCqzY`3TS zd<&KtT5EIjjK=1fS_CyT_CzCzWttw)gGNs%pjmY%{j|m7p?E@%Bpe fAw6L@ zw3;3XI&VV}P1gd^aM Iq$2X~Yc6B9JhGnjY7B^;klSb|r12 zk;d+5DAKJNT}b4hnmyJbUa%(=L2@zOtV&BnHIxxTe^vg$P*)d1peJ!{sgYP|7!l26 zN;1=~Bxy7-VnsO7r}Y>;OAT~OYqN6eilHIe$`D|gGIP2XU23*587Qtrtzx>6Npz)| z;WX_WAg?bC8}TL!?uF{!=WEnV;%KvhhDATitMyZ(rhZ#wOs%0p5_4^$4eCLwuR}V~ zn2`tt^l+UPG)x$Kj7TCrQG@aEFwlG9!wB|&2>1P1iwJ#&j@BhLTF0_>WnoeH;B&Ns zRWJad2TRRbn8|X?RcRVqotf>Vouz9qIT8e G!5Ow`&U+C1&+(VkvC!Zc{Jiq+;B-7%06TG$(bC>)#+3?<1jU+*@We6DRm m{otFLSQ=g&RKM Pm literal 0 HcmV?d00001 diff --git a/radiant.basics/inst/app/global.R b/radiant.basics/inst/app/global.R new file mode 100644 index 0000000..f83f097 --- /dev/null +++ b/radiant.basics/inst/app/global.R @@ -0,0 +1,32 @@ +library(shiny.i18n) +# file with translations +i18n <- Translator$new(translation_csvs_path = "../translations") + +# change this to zh +i18n$set_translation_language("zh") + +## sourcing from radiant.data +options(radiant.path.data = system.file(package = "radiant.data")) +source(file.path(getOption("radiant.path.data"), "app/global.R"), encoding = getOption("radiant.encoding", default = "UTF-8"), local = TRUE) + +ifelse(grepl("radiant.basics", getwd()) && file.exists("../../inst"), "..", system.file(package = "radiant.basics")) %>% + options(radiant.path.basics = .) + +## setting path for figures in help files +addResourcePath("figures_basics", "tools/help/figures/") + +## setting path for www resources +addResourcePath("www_basics", file.path(getOption("radiant.path.basics"), "app/www/")) + +## loading urls and ui +source("init.R", encoding = getOption("radiant.encoding", "UTF-8"), local = TRUE) +options(radiant.url.patterns = make_url_patterns()) + +## if radiant.data is not in search main function from dplyr etc. won't be available +if (!"package:radiant.basics" %in% search() && + isTRUE(getOption("radiant.development")) && + getOption("radiant.path.basics") == "..") { + options(radiant.from.package = FALSE) +} else { + options(radiant.from.package = TRUE) +} diff --git a/radiant.basics/inst/app/help.R b/radiant.basics/inst/app/help.R new file mode 100644 index 0000000..040ebd7 --- /dev/null +++ b/radiant.basics/inst/app/help.R @@ -0,0 +1,27 @@ +help_basics <- c( + "Probability calculator" = "prob_calc.md", "Central limit theorem" = "clt.md", + "Single mean" = "single_mean.md", "Compare means" = "compare_means.md", + "Single proportion" = "single_prop.md", "Compare proportions" = "compare_props.md", + "Goodness of fit" = "goodness.md", "Cross-tabs" = "cross_tabs.md", + "Correlation" = "correlation.md" +) + +output$help_basics <- reactive(append_help("help_basics", file.path(getOption("radiant.path.basics"), "app/tools/help"), Rmd = TRUE)) + +observeEvent(input$help_basics_all, { + help_switch(input$help_basics_all, "help_basics") +}) +observeEvent(input$help_basics_none, { + help_switch(input$help_basics_none, "help_basics", help_on = FALSE) +}) + +help_basics_panel <- tagList( + wellPanel( + HTML(""), + checkboxGroupInput( + "help_basics", NULL, help_basics, + selected = state_group("help_basics"), inline = TRUE + ) + ) +) diff --git a/radiant.basics/inst/app/init.R b/radiant.basics/inst/app/init.R new file mode 100644 index 0000000..5f95133 --- /dev/null +++ b/radiant.basics/inst/app/init.R @@ -0,0 +1,49 @@ +## urls for menu +r_url_list <- getOption("radiant.url.list") +r_url_list[["Single mean"]] <- + list("tabs_single_mean" = list("Summary" = "basics/single-mean/", "Plot" = "basics/single-mean/plot/")) +r_url_list[["Compare means"]] <- + list("tabs_compare_means" = list("Summary" = "basics/compare-means/", "Plot" = "basics/compare-means/plot/")) +r_url_list[["Single proportion"]] <- + list("tabs_single_prop" = list("Summary" = "basics/single-prop/", "Plot" = "basics/single-prop/plot/")) +r_url_list[["Compare proportions"]] <- + list("tabs_compare_props" = list("Summary" = "basics/compare-props/", "Plot" = "basics/compare-props/plot/")) +r_url_list[["Goodness of fit"]] <- + list("tabs_goodness" = list("Summary" = "basics/goodness/", "Plot" = "basics/goodness/plot/")) +r_url_list[["Cross-tabs"]] <- + list("tabs_cross_tabs" = list("Summary" = "basics/cross-tabs/", "Plot" = "basics/cross-tabs/plot/")) +r_url_list[["Correlation"]] <- + list("tabs_correlation" = list("Summary" = "basics/correlation/", "Plot" = "basics`/correlation/plot/")) +options(radiant.url.list = r_url_list) +rm(r_url_list) + +## try http://127.0.0.1:3174/?url=basics/goodness/plot/&SSUID=local +## http://127.0.0.1:7407/?url=basics/compare-means/plot/&SSUID=local-a82049 + +## design menu +options( + radiant.basics_ui = + tagList( + navbarMenu( + i18n$t("Basics"), + tags$head( + tags$script(src = "www_basics/js/run_return.js") + ), + i18n$t("Probability"), + tabPanel(i18n$t("Probability calculator"), uiOutput("prob_calc")), + tabPanel(i18n$t("Central Limit Theorem"), uiOutput("clt")), + "----", i18n$t("Means"), + tabPanel(i18n$t("Single mean"), uiOutput("single_mean")), + tabPanel(i18n$t("Compare means"), uiOutput("compare_means")), + tabPanel(i18n$t("Normality test"),uiOutput("normality_test")), + tabPanel(i18n$t("Homogeneity of variance test"),uiOutput("homo_variance_test")), + "----", i18n$t("Proportions"), + tabPanel(i18n$t("Single proportion"), uiOutput("single_prop")), + tabPanel(i18n$t("Compare proportions"), uiOutput("compare_props")), + "----", i18n$t("Tables"), + tabPanel(i18n$t("Goodness of fit"), uiOutput("goodness")), + tabPanel(i18n$t("Cross-tabs"), uiOutput("cross_tabs")), + tabPanel(i18n$t("Correlation"), uiOutput("correlation")) + ) + ) +) diff --git a/radiant.basics/inst/app/server.R b/radiant.basics/inst/app/server.R new file mode 100644 index 0000000..dd26d78 --- /dev/null +++ b/radiant.basics/inst/app/server.R @@ -0,0 +1,59 @@ +if (isTRUE(getOption("radiant.from.package"))) { + library(radiant.basics) +} + +shinyServer(function(input, output, session) { + + ## source shared functions + source(file.path(getOption("radiant.path.data"), "app/init.R"), encoding = getOption("radiant.encoding"), local = TRUE) + source(file.path(getOption("radiant.path.data"), "app/radiant.R"), encoding = getOption("radiant.encoding"), local = TRUE) + source("help.R", encoding = getOption("radiant.encoding"), local = TRUE) + + ## help ui + output$help_basics_ui <- renderUI({ + sidebarLayout( + sidebarPanel( + help_data_panel, + help_basics_panel, + uiOutput("help_text"), + width = 3 + ), + mainPanel( + HTML(paste0(" Select help files to show and search
")), + htmlOutput("help_data"), + htmlOutput("help_basics") + ) + ) + }) + + ## packages to use for example data + options(radiant.example.data = c("radiant.data", "radiant.basics")) + + ## source data & app tools from radiant.data + for (file in list.files( + c( + file.path(getOption("radiant.path.data"), "app/tools/app"), + file.path(getOption("radiant.path.data"), "app/tools/data") + ), + pattern = "\\.(r|R)$", full.names = TRUE + )) + source(file, encoding = getOption("radiant.encoding"), local = TRUE) + + ## 'sourcing' package functions in the server.R environment for development + if (!isTRUE(getOption("radiant.from.package"))) { + for (file in list.files("../../R", pattern = "\\.(r|R)$", full.names = TRUE)) { + source(file, encoding = getOption("radiant.encoding"), local = TRUE) + } + cat("\nGetting radiant.basics from source ...\n") + } else { + ## weired but required + summary.correlation <- radiant.basics:::summary.correlation + } + + ## source analysis tools for basic app + for (file in list.files(c("tools/analysis"), pattern = "\\.(r|R)$", full.names = TRUE)) + source(file, encoding = getOption("radiant.encoding"), local = TRUE) + + ## save state on refresh or browser close + saveStateOnRefresh(session) +}) diff --git a/radiant.basics/inst/app/tools/analysis/clt_ui.R b/radiant.basics/inst/app/tools/analysis/clt_ui.R new file mode 100644 index 0000000..fd2b94b --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/clt_ui.R @@ -0,0 +1,252 @@ +############################### +# Central Limit Theorem +############################### +clt_dist <- c("Normal", "Binomial", "Uniform", "Exponential") %>% + setNames(c( + i18n$t("Normal"), + i18n$t("Binomial"), + i18n$t("Uniform"), + i18n$t("Exponential") + )) + +clt_stat <- c("sum", "mean") %>% + setNames(c( + i18n$t("Sum"), + i18n$t("Mean") + )) +clt_args <- as.list(formals(clt)) + +clt_inputs <- reactive({ + for (i in names(clt_args)) { + clt_args[[i]] <- input[[paste0("clt_", i)]] + } + clt_args +}) + +## add a spinning refresh icon if the tabel needs to be (re)calculated +run_refresh(clt_args, "clt", init = "dist", label = i18n$t("Run simulation"), relabel = i18n$t("Re-run simulation"), data = FALSE) + +output$ui_clt <- renderUI({ + tagList( + wellPanel( + actionButton("clt_run", i18n$t("Run simulation"), width = "100%", icon = icon("play", verify_fa = FALSE), class = "btn-success") + ), + wellPanel( + selectInput( + "clt_dist", i18n$t("Distribution:"), + choices = clt_dist, + selected = state_single("clt_dist", clt_dist), + multiple = FALSE + ), + conditionalPanel( + condition = "input.clt_dist == 'Uniform'", + make_side_by_side( + numericInput( + "clt_unif_min", i18n$t("Min:"), + value = state_init("clt_unif_min", 0) + ), + numericInput( + "clt_unif_max", i18n$t("Max:"), + value = state_init("clt_unif_max", 1) + ) + ) + ), + conditionalPanel( + condition = "input.clt_dist == 'Normal'", + make_side_by_side( + numericInput( + "clt_norm_mean", i18n$t("Mean:"), + value = state_init("clt_norm_mean", 0) + ), + numericInput( + "clt_norm_sd", i18n$t("SD:"), + value = state_init("clt_norm_sd", 1), + min = 0.1, step = 0.1 + ) + ) + ), + conditionalPanel( + condition = "input.clt_dist == 'Exponential'", + numericInput( + "clt_expo_rate", i18n$t("Rate:"), + value = state_init("clt_expo_rate", 1), + min = 1, step = 1 + ) + ), + conditionalPanel( + condition = "input.clt_dist == 'Binomial'", + make_side_by_side( + numericInput( + "clt_binom_size", i18n$t("Size:"), + value = state_init("clt_binom_size", 10), + min = 1, step = 1 + ), + numericInput( + "clt_binom_prob", i18n$t("Prob:"), + value = state_init("clt_binom_prob", 0.2), + min = 0, max = 1, step = .1 + ) + ) + ), + make_side_by_side( + numericInput( + "clt_n", i18n$t("Sample size:"), + value = state_init("clt_n", 100), + min = 2, step = 1 + ), + numericInput( + "clt_m", i18n$t("# of samples:"), + value = state_init("clt_m", 100), + min = 2, step = 1 + ) + ), + sliderInput( + "clt_bins", + label = i18n$t("Number of bins:"), + min = 1, max = 50, step = 1, + value = state_init("clt_bins", 15), + ), + radioButtons( + "clt_stat", NULL, + choices = clt_stat, + selected = state_init("clt_stat", "sum"), + inline = TRUE + ) + ), + help_and_report( + modal_title = i18n$t("Central Limit Theorem"), fun_name = "clt", + help_file = inclRmd(file.path(getOption("radiant.path.basics"), "app/tools/help/clt.md")) + ) + ) +}) + +clt_plot_width <- function() 700 +clt_plot_height <- function() 700 + +## output is called from the main radiant ui.R +output$clt <- renderUI({ + register_plot_output( + "plot_clt", ".plot_clt", + height_fun = "clt_plot_height", + width_fun = "clt_plot_width" + ) + + ## two separate tabs + clt_output_panels <- tagList( + tabPanel( + "Plot", + download_link("dlp_clt"), + plotOutput("plot_clt", width = "100%", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Probability"), + tool = i18n$t("Central Limit Theorem"), + data = NULL, + tool_ui = "ui_clt", + output_panels = clt_output_panels + ) +}) + +.clt <- eventReactive(input$clt_run, { + ## avoiding input errors + ret <- "" + if (is.na(input$clt_n) || input$clt_n < 2) { + ret <- i18n$t("Please choose a sample size larger than 2") + } else if (is.na(input$clt_m) || input$clt_m < 2) { + ret <- i18n$t("Please choose 2 or more samples") + } else if (input$clt_dist == "Uniform") { + if (is.na(input$clt_unif_min)) { + ret <- i18n$t("Please choose a minimum value for the uniform distribution") + } else if (is.na(input$clt_unif_max)) { + ret <- i18n$t("Please choose a maximum value for the uniform distribution") + } else if (input$clt_unif_max <= input$clt_unif_min) { + ret <- i18n$t("The maximum value for the uniform distribution\nmust be larger than the minimum value") + } + } else if (input$clt_dist == "Normal") { + if (is.na(input$clt_norm_mean)) { + ret <- i18n$t("Please choose a mean value for the normal distribution") + } else if (is.na(input$clt_norm_sd) || input$clt_norm_sd < .001) { + ret <- i18n$t("Please choose a non-zero standard deviation for the normal distribution") + } + } else if (input$clt_dist == "Exponential") { + if (is.na(input$clt_expo_rate) || input$clt_expo_rate < 1) { + ret <- i18n$t("Please choose a rate larger than 1 for the exponential distribution") + } + } else if (input$clt_dist == "Binomial") { + if (is.na(input$clt_binom_size) || input$clt_binom_size < 1) { + ret <- i18n$t("Please choose a size parameter larger than 1 for the binomial distribution") + } else if (is.na(input$clt_binom_prob) || input$clt_binom_prob < 0.01) { + ret <- i18n$t("Please choose a probability between 0 and 1 for the binomial distribution") + } + } + if (is.empty(ret)) { + do.call(clt, clt_inputs()) + } else { + ret + } +}) + +.plot_clt <- reactive({ + if (not_pressed(input$clt_run)) { + return(i18n$t("** Press the Run simulation button to simulate data **")) + } + clt <- .clt() + validate(need(!is.character(clt), paste0("\n\n\n ", clt))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(clt, stat = input$clt_stat, bins = input$clt_bins) + }) +}) + +clt_report <- function() { + outputs <- c("plot") + inp_out <- list(list(stat = input$clt_stat, bins = input$clt_bins)) + inp <- clt_inputs() + inp3 <- inp[!grepl("_", names(inp))] + if (input$clt_dist == "Normal") { + inp <- c(inp3, inp[grepl("norm_", names(inp))]) + } else if (input$clt_dist == "Uniform") { + inp <- c(inp3, inp[grepl("unif", names(inp))]) + } else if (input$clt_dist == "Binomial") { + inp <- c(inp3, inp[grepl("binom_", names(inp))]) + } else if (input$clt_dist == "Exponential") { + inp <- c(inp3, inp[grepl("expo_", names(inp))]) + } + + update_report( + inp_main = clean_args(inp, clt_args), + fun_name = "clt", + inp_out = inp_out, + outputs = outputs, + figs = TRUE, + fig.width = clt_plot_width(), + fig.height = clt_plot_height() + ) +} + +download_handler( + id = "dlp_clt", + fun = download_handler_plot, + fn = function() paste0(tolower(input$clt_dist), "_clt"), + type = "png", + caption = i18n$t("Save central limit theorem plot"), + plot = .plot_clt, + width = clt_plot_width, + height = clt_plot_height +) + +observeEvent(input$clt_report, { + r_info[["latest_screenshot"]] <- NULL + clt_report() +}) + +observeEvent(input$clt_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_clt_screenshot") +}) + +observeEvent(input$modal_clt_screenshot, { + clt_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/compare_means_ui.R b/radiant.basics/inst/app/tools/analysis/compare_means_ui.R new file mode 100644 index 0000000..4e0de2e --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/compare_means_ui.R @@ -0,0 +1,317 @@ +## choice lists for compare means +cm_alt <- c( + "two.sided", + "less", + "greater" +) %>% setNames(c( + i18n$t("Two sided"), + i18n$t("Less than"), + i18n$t("Greater than") +)) + +cm_samples <- c( + "independent", + "paired" +) %>% setNames(c( + i18n$t("independent"), + i18n$t("paired") +)) + +cm_adjust <- c( + "none", + "bonf" +) %>% setNames(c( + i18n$t("None"), + i18n$t("Bonferroni") +)) + +cm_plots <- c( + "scatter", + "box", + "density", + "bar" +) %>% setNames(c( + i18n$t("Scatter"), + i18n$t("Box"), + i18n$t("Density"), + i18n$t("Bar") +)) +## list of function arguments +cm_args <- as.list(formals(compare_means)) + +## list of function inputs selected by user +cm_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + cm_args$data_filter <- if (input$show_filter) input$data_filter else "" + cm_args$dataset <- input$dataset + for (i in r_drop(names(cm_args))) { + cm_args[[i]] <- input[[paste0("cm_", i)]] + } + cm_args +}) + +############################### +# Compare means +############################### +output$ui_cm_var1 <- renderUI({ + vars <- c("None" = "", groupable_vars()) + isNum <- .get_class() %in% c("integer", "numeric", "ts") + + ## can't use unique here - removes variable type information + vars <- c(vars, varnames()[isNum]) %>% .[!duplicated(.)] + + selectInput( + inputId = "cm_var1", + label = i18n$t("Select a factor or numeric variable:"), + choices = vars, + selected = state_single("cm_var1", vars), + multiple = FALSE + ) +}) + +output$ui_cm_var2 <- renderUI({ + if (not_available(input$cm_var1)) { + return() + } + isNum <- .get_class() %in% c("integer", "numeric", "ts") + vars <- varnames()[isNum] + + if (input$cm_var1 %in% vars) { + ## when cm_var1 is numeric comparisons for multiple variables are possible + vars <- vars[-which(vars == input$cm_var1)] + if (length(vars) == 0) { + return() + } + + selectizeInput( + inputId = "cm_var2", label = i18n$t("Numeric variable(s):"), + selected = state_multiple("cm_var2", vars, isolate(input$cm_var2)), + choices = vars, multiple = TRUE, + options = list(placeholder = "None", plugins = list("remove_button", "drag_drop")) + ) + } else { + ## when cm_var1 is not numeric comparisons are across levels/groups + vars <- c("None" = "", vars) + selectInput( + "cm_var2", i18n$t("Numeric variable:"), + selected = state_single("cm_var2", vars), + choices = vars, + multiple = FALSE + ) + } +}) + +output$ui_cm_comb <- renderUI({ + if (not_available(input$cm_var1)) { + return() + } + + if (.get_class()[[input$cm_var1]] == "factor") { + levs <- .get_data()[[input$cm_var1]] %>% levels() + } else { + levs <- c(input$cm_var1, input$cm_var2) + } + + if (length(levs) > 2) { + cmb <- combn(levs, 2) %>% apply(2, paste, collapse = ":") + } else { + return() + } + + selectizeInput( + "cm_comb", + label = i18n$t("Choose combinations:"), + choices = cmb, + selected = state_multiple("cm_comb", cmb, cmb[1]), + multiple = TRUE, + options = list(placeholder = i18n$t("Evaluate all combinations"), plugins = list("remove_button", "drag_drop")) + ) +}) + + +output$ui_compare_means <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + uiOutput("ui_cm_var1"), + uiOutput("ui_cm_var2"), + condition = "input.tabs_compare_means == 'Summary'", + uiOutput("ui_cm_comb"), + selectInput( + inputId = "cm_alternative", label = i18n$t("Alternative hypothesis:"), + choices = cm_alt, + selected = state_single("cm_alternative", cm_alt, cm_args$alternative) + ), + sliderInput( + "cm_conf_lev", i18n$t("Confidence level:"), + min = 0.85, max = 0.99, step = 0.01, + value = state_init("cm_conf_lev", cm_args$conf_lev) + ), + checkboxInput("cm_show", i18n$t("Show additional statistics"), value = state_init("cm_show", FALSE)), + radioButtons( + inputId = "cm_samples", label = i18n$t("Sample type:"), cm_samples, + selected = state_init("cm_samples", cm_args$samples), + inline = TRUE + ), + radioButtons( + inputId = "cm_adjust", label = i18n$t("Multiple comp. adjustment:"), cm_adjust, + selected = state_init("cm_adjust", cm_args$adjust), + inline = TRUE + ), + radioButtons( + inputId = "cm_test", label = i18n$t("Test type:"), + choices = c( + "t", + "wilcox" + ) %>% setNames(c( + i18n$t("t-test"), + i18n$t("Wilcox") + )), + selected = state_init("cm_test", cm_args$test), + inline = TRUE + ) + ), + conditionalPanel( + condition = "input.tabs_compare_means == 'Plot'", + selectizeInput( + inputId = "cm_plots", label = i18n$t("Select plots:"), + choices = cm_plots, + selected = state_multiple("cm_plots", cm_plots, "scatter"), + multiple = TRUE, + options = list(placeholder = i18n$t("Select plots"), plugins = list("remove_button", "drag_drop")) + ) + ) + ), + help_and_report( + modal_title = i18n$t("Compare means"), + fun_name = "compare_means", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/compare_means.md")) + ) + ) +}) + +cm_plot <- reactive({ + list(plot_width = 650, plot_height = 400 * max(length(input$cm_plots), 1)) +}) + +cm_plot_width <- function() { + cm_plot() %>% + (function(x) if (is.list(x)) x$plot_width else 650) +} + +cm_plot_height <- function() { + cm_plot() %>% + (function(x) if (is.list(x)) x$plot_height else 400) +} + +# output is called from the main radiant ui.R +output$compare_means <- renderUI({ + register_print_output("summary_compare_means", ".summary_compare_means", ) + register_plot_output( + "plot_compare_means", ".plot_compare_means", + height_fun = "cm_plot_height" + ) + + # two separate tabs + cm_output_panels <- tabsetPanel( + id = "tabs_compare_means", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_compare_means")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_compare_means"), + plotOutput("plot_compare_means", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Means"), + tool = i18n$t("Compare means"), + tool_ui = "ui_compare_means", + output_panels = cm_output_panels + ) +}) + +cm_available <- reactive({ + if (not_available(input$cm_var1) || not_available(input$cm_var2)) { + return(i18n$t("This analysis requires at least two variables. The first can be of type\nfactor, numeric, or interval. The second must be of type numeric or interval.\nIf these variable types are not available please select another dataset.\n\n") %>% suggest_data("salary")) + } else if (length(input$cm_var2) > 1 && .get_class()[input$cm_var1] == "factor") { + " " + } else if (input$cm_var1 %in% input$cm_var2) { + " " + } else { + "available" + } +}) + +.compare_means <- reactive({ + cmi <- cm_inputs() + cmi$envir <- r_data + do.call(compare_means, cmi) +}) + +.summary_compare_means <- reactive({ + if (cm_available() != "available") { + return(cm_available()) + } + if (input$cm_show) summary(.compare_means(), show = TRUE) else summary(.compare_means()) +}) + +.plot_compare_means <- reactive({ + if (cm_available() != "available") { + return(cm_available()) + } + validate(need(input$cm_plots, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(.compare_means(), plots = input$cm_plots, shiny = TRUE) + }) +}) + +compare_means_report <- function() { + if (is.empty(input$cm_var1) || is.empty(input$cm_var2)) { + return(invisible()) + } + figs <- FALSE + outputs <- c("summary") + inp_out <- list(list(show = input$cm_show), "") + if (length(input$cm_plots) > 0) { + outputs <- c("summary", "plot") + inp_out[[2]] <- list(plots = input$cm_plots, custom = FALSE) + figs <- TRUE + } + update_report( + inp_main = clean_args(cm_inputs(), cm_args), + fun_name = "compare_means", + inp_out = inp_out, + outputs = outputs, + figs = figs, + fig.width = cm_plot_width(), + fig.height = cm_plot_height() + ) +} + +download_handler( + id = "dlp_compare_means", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_compare_means"), + type = "png", + caption = i18n$t("Save compare means plot"), + plot = .plot_compare_means, + width = cm_plot_width, + height = cm_plot_height +) + +observeEvent(input$compare_means_report, { + r_info[["latest_screenshot"]] <- NULL + compare_means_report() +}) + +observeEvent(input$compare_means_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_compare_means_screenshot") +}) + +observeEvent(input$modal_compare_means_screenshot, { + compare_means_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/compare_props_ui.R b/radiant.basics/inst/app/tools/analysis/compare_props_ui.R new file mode 100644 index 0000000..8e814a8 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/compare_props_ui.R @@ -0,0 +1,281 @@ +## choice lists for compare proportions(不使用等号命名) +cp_alt <- c("two.sided", "less", "greater") %>% + setNames(c( + i18n$t("Two sided"), + i18n$t("Less than"), + i18n$t("Greater than") + )) + +cp_adjust <- c("none", "bonf") %>% + setNames(c( + i18n$t("None"), + i18n$t("Bonferroni") + )) + +# cp_plots <- c("props", "counts") %>% setNames(c(i18n$t("Proportions"), i18n$t("Relative"))) +cp_plots <- c("bar", "dodge") %>% + setNames(c( + i18n$t("Bar"), + i18n$t("Dodge") + )) + +## list of function arguments +cp_args <- as.list(formals(compare_props)) + +## list of function inputs selected by user +cp_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + cp_args$data_filter <- if (input$show_filter) input$data_filter else "" + cp_args$dataset <- input$dataset + for (i in r_drop(names(cp_args))) { + cp_args[[i]] <- input[[paste0("cp_", i)]] + } + cp_args +}) + +############################### +# Compare proportions +############################### +output$ui_cp_var1 <- renderUI({ + vars <- c("None" = "", groupable_vars()) + selectInput( + "cp_var1", i18n$t("Select a grouping variable:"), + choices = vars, + selected = state_single("cp_var1", vars), + multiple = FALSE + ) +}) + +output$ui_cp_var2 <- renderUI({ + vars <- two_level_vars() + if (not_available(input$cp_var1)) { + return() + } + if (input$cp_var1 %in% vars) vars <- vars[-which(vars == input$cp_var1)] + + vars <- c("None" = "", vars) + selectInput( + inputId = "cp_var2", i18n$t("Variable (select one):"), + selected = state_single("cp_var2", vars), + choices = vars, + multiple = FALSE + ) +}) + +output$ui_cp_levs <- renderUI({ + if (not_available(input$cp_var2)) { + return() + } else { + levs <- .get_data()[[input$cp_var2]] %>% + as.factor() %>% + levels() + } + + selectInput( + inputId = "cp_levs", i18n$t("Choose level:"), + choices = levs, + selected = state_single("cp_levs", levs), + multiple = FALSE + ) +}) + +output$ui_cp_comb <- renderUI({ + if (not_available(input$cp_var1)) { + return() + } + + dat <- .get_data()[[input$cp_var1]] %>% as.factor() + levs <- levels(dat) + alevs <- unique(dat) + len <- length(dat) + levs <- levs[levs %in% alevs] + + if (length(levs) > 2 && length(levs) < len) { + cmb <- combn(levs, 2) %>% apply(2, paste, collapse = ":") + } else { + return() + } + + selectizeInput( + "cp_comb", i18n$t("Choose combinations:"), + choices = cmb, + selected = state_multiple("cp_comb", cmb, cmb[1]), + multiple = TRUE, + options = list(placeholder = i18n$t("Evaluate all combinations"), plugins = list("remove_button", "drag_drop")) + ) +}) + + +output$ui_compare_props <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_compare_props == 'Summary'", + uiOutput("ui_cp_var1"), + uiOutput("ui_cp_var2"), + uiOutput("ui_cp_levs"), + uiOutput("ui_cp_comb"), + selectInput( + inputId = "cp_alternative", i18n$t("Alternative hypothesis:"), + choices = cp_alt, + selected = state_single("cp_alternative", cp_alt, cp_args$alternative) + ), + checkboxInput( + "cp_show", i18n$t("Show additional statistics"), + value = state_init("cp_show", FALSE) + ), + sliderInput( + "cp_conf_lev", i18n$t("Confidence level:"), + min = 0.85, max = 0.99, step = 0.01, + value = state_init("cp_conf_lev", cp_args$conf_lev) + ), + radioButtons( + inputId = "cp_adjust", i18n$t("Multiple comp. adjustment:"), + cp_adjust, + selected = state_init("cp_adjust", cp_args$adjust), + inline = TRUE + ) + ), + conditionalPanel( + condition = "input.tabs_compare_props == 'Plot'", + selectizeInput( + inputId = "cp_plots", label = i18n$t("Select plots:"), + choices = cp_plots, + selected = state_multiple("cp_plots", cp_plots, "bar"), + multiple = TRUE, + options = list(placeholder = i18n$t("Select plots"), plugins = list("remove_button", "drag_drop")) + ) + ) + ), + help_and_report( + modal_title = i18n$t("Compare proportions"), + fun_name = "compare_props", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/compare_props.md")) + ) + ) +}) + +cp_plot <- reactive({ + list(plot_width = 650, plot_height = 400 * max(length(input$cp_plots), 1)) +}) + +cp_plot_width <- function() { + cp_plot() %>% + (function(x) if (is.list(x)) x$plot_width else 650) +} + +cp_plot_height <- function() { + cp_plot() %>% + (function(x) if (is.list(x)) x$plot_height else 400) +} + +# output is called from the main radiant ui.R +output$compare_props <- renderUI({ + register_print_output("summary_compare_props", ".summary_compare_props", ) + register_plot_output( + "plot_compare_props", ".plot_compare_props", + height_fun = "cp_plot_height" + ) + + # two separate tabs + cp_output_panels <- tabsetPanel( + id = "tabs_compare_props", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_compare_props")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_compare_props"), + plotOutput("plot_compare_props", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Proportions"), + tool = i18n$t("Compare proportions"), + tool_ui = "ui_compare_props", + output_panels = cp_output_panels + ) +}) + +cp_available <- reactive({ + if (not_available(input$cp_var1) || not_available(input$cp_var2)) { + i18n$t("This analysis requires two categorical variables. The first must have\ntwo or more levels. The second can have only two levels. If these\nvariable types are not available please select another dataset.\n\n") %>% suggest_data("titanic") + } else if (input$cp_var1 %in% input$cp_var2) { + " " + } else { + "available" + } +}) + +.compare_props <- reactive({ + cpi <- cp_inputs() + cpi$envir <- r_data + do.call(compare_props, cpi) +}) + +.summary_compare_props <- reactive({ + if (cp_available() != "available") { + return(cp_available()) + } + if (input$cp_show) summary(.compare_props(), show = TRUE) else summary(.compare_props()) +}) + +.plot_compare_props <- reactive({ + if (cp_available() != "available") { + return(cp_available()) + } + validate(need(input$cp_plots, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(.compare_props(), plots = input$cp_plots, shiny = TRUE) + }) +}) + +compare_props_report <- function() { + if (is.empty(input$cp_var1) || is.empty(input$cp_var2)) { + return(invisible()) + } + figs <- FALSE + outputs <- c("summary") + inp_out <- list(list(show = input$cp_show), "") + if (length(input$cp_plots) > 0) { + outputs <- c("summary", "plot") + inp_out[[2]] <- list(plots = input$cp_plots, custom = FALSE) + figs <- TRUE + } + + update_report( + inp_main = clean_args(cp_inputs(), cp_args), + fun_name = "compare_props", + inp_out = inp_out, + outputs = outputs, + figs = figs, + fig.width = cp_plot_width(), + fig.height = cp_plot_height() + ) +} + +download_handler( + id = "dlp_compare_props", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_compare_props"), + type = "png", + caption = i18n$t("Save compare proportions plot"), + plot = .plot_compare_props, + width = cp_plot_width, + height = cp_plot_height +) + +observeEvent(input$compare_props_report, { + r_info[["latest_screenshot"]] <- NULL + compare_props_report() +}) + +observeEvent(input$compare_props_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_compare_props_screenshot") +}) + +observeEvent(input$modal_compare_props_screenshot, { + compare_props_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/correlation_ui.R b/radiant.basics/inst/app/tools/analysis/correlation_ui.R new file mode 100644 index 0000000..89417d7 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/correlation_ui.R @@ -0,0 +1,330 @@ +############################### +## Correlation +############################### +cor_method <- c( + "pearson", + "spearman", + "kendall" +) %>% setNames(c( + i18n$t("Pearson"), + i18n$t("Spearman"), + i18n$t("Kendall") +)) +## list of function arguments +cor_args <- as.list(formals(correlation)) + +## list of function inputs selected by user +cor_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + cor_args$data_filter <- if (input$show_filter) input$data_filter else "" + cor_args$dataset <- input$dataset + for (i in r_drop(names(cor_args))) { + cor_args[[i]] <- input[[paste0("cor_", i)]] + } + cor_args +}) + +output$ui_cor_method <- renderUI({ + if (isTRUE(input$cor_hcor)) { + cor_method <- c("pearson") %>% + setNames(c(i18n$t("Pearson"))) + } + selectInput( + "cor_method", i18n$t("Method:"), + choices = cor_method, + selected = state_single("cor_method", cor_method, "pearson"), + multiple = FALSE + ) +}) + +cor_sum_args <- as.list(if (exists("summary.correlation")) { + formals(summary.correlation) +} else { + formals(radiant.basics::summary.correlation) +}) + +## list of function inputs selected by user +cor_sum_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + for (i in names(cor_sum_args)) { + cor_sum_args[[i]] <- input[[paste0("cor_", i)]] + } + cor_sum_args +}) + +output$ui_cor_vars <- renderUI({ + withProgress(message = i18n$t("Acquiring variable information"), value = 1, { + vars <- varnames() + toSelect <- .get_class() %in% c("numeric", "integer", "date", "factor") + vars <- vars[toSelect] + }) + if (length(vars) == 0) { + return() + } + selectInput( + inputId = "cor_vars", label = i18n$t("Select variables:"), + choices = vars, + selected = state_multiple("cor_vars", vars, isolate(input$cor_vars)), + multiple = TRUE, + size = min(10, length(vars)), + selectize = FALSE + ) +}) + +output$ui_cor_nrobs <- renderUI({ + nrobs <- nrow(.get_data()) + choices <- c("1,000" = 1000, "5,000" = 5000, "10,000" = 10000, "All" = -1) %>% + .[. < nrobs] + selectInput( + "cor_nrobs", i18n$t("Number of data points plotted:"), + choices = choices, + selected = state_single("cor_nrobs", choices, 1000) + ) +}) + +output$ui_cor_name <- renderUI({ + req(input$dataset) + textInput("cor_name", i18n$t("Store as data.frame:"), "", placeholder = "Provide a name") +}) + +## add a spinning refresh icon if correlations need to be (re)calculated +run_refresh(cor_args, "cor", init = "vars", tabs = "tabs_correlation", label = i18n$t("Calculate correlation"), relabel = i18n$t("Re-calculate correlations")) + +output$ui_correlation <- renderUI({ + req(input$dataset) + tagList( + conditionalPanel( + condition = "input.tabs_correlation == 'Summary'", + wellPanel( + actionButton("cor_run", i18n$t("Calculate correlation"), width = "100%", icon = icon("play", verify_fa = FALSE), class = "btn-success") + ) + ), + wellPanel( + conditionalPanel( + condition = "input.tabs_correlation == 'Summary'", + uiOutput("ui_cor_vars"), + uiOutput("ui_cor_method"), + checkboxInput("cor_hcor", i18n$t("Adjust for {factor} variables"), value = state_init("cor_hcor", FALSE)), + conditionalPanel( + condition = "input.cor_hcor == true", + checkboxInput("cor_hcor_se", i18n$t("Calculate adjusted p.values"), value = state_init("cor_hcor_se", FALSE)) + ), + numericInput( + "cor_cutoff", i18n$t("Correlation cutoff:"), + min = 0, max = 1, step = 0.05, + value = state_init("cor_cutoff", 0) + ), + conditionalPanel( + condition = "input.cor_hcor == false", + checkboxInput( + "cor_covar", i18n$t("Show covariance matrix"), + value = state_init("cor_covar", FALSE) + ) + ), + ), + conditionalPanel( + condition = "input.tabs_correlation == 'Plot'", + uiOutput("ui_cor_nrobs") + ) + ), + conditionalPanel( + condition = "input.tabs_correlation == 'Summary'", + wellPanel( + tags$table( + tags$td(uiOutput("ui_cor_name")), + tags$td(actionButton("cor_store", i18n$t("Store"), icon = icon("plus", verify_fa = FALSE)), class = "top") + ) + ) + ), + help_and_report( + modal_title = i18n$t("Correlation"), + fun_name = "correlation", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/correlation.md")) + ) + ) +}) + +observeEvent(input$cor_hcor, { + if (input$cor_hcor == FALSE) { + updateCheckboxInput(session, "cor_hcor_se", value = FALSE) + } else { + updateCheckboxInput(session, "cor_covar", value = FALSE) + } +}) + +cor_plot <- reactive({ + max(2, length(input$cor_vars)) %>% + (function(x) list(plot_width = 400 + 75 * x, plot_height = 400 + 75 * x)) +}) + +cor_plot_width <- function() { + cor_plot() %>% + (function(x) if (is.list(x)) x$plot_width else 650) +} + +cor_plot_height <- function() { + cor_plot() %>% + (function(x) if (is.list(x)) x$plot_height else 650) +} + +## output is called from the main radiant ui.R +output$correlation <- renderUI({ + register_print_output("summary_correlation", ".summary_correlation") + register_plot_output( + "plot_correlation", ".plot_correlation", + height_fun = "cor_plot_height", + width_fun = "cor_plot_width" + ) + + ## two separate tabs + cor_output_panels <- tabsetPanel( + id = "tabs_correlation", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_correlation")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_correlation"), + plotOutput( + "plot_correlation", + width = "100%", + height = "100%" + ) + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Tables"), + tool = i18n$t("Correlation"), + tool_ui = "ui_correlation", + output_panels = cor_output_panels + ) +}) + +cor_available <- reactive({ + if (not_available(input$cor_vars) || length(input$cor_vars) < 2) { + return(i18n$t("This analysis requires two or more variables or type numeric,\ninteger,or date. If these variable types are not available\nplease select another dataset.\n\n") %>% suggest_data("salary")) + } + "available" +}) + +# .correlation <- reactive({ +.correlation <- eventReactive(input$cor_run, { + cori <- cor_inputs() + cori$envir <- r_data + do.call(correlation, cori) +}) + +.summary_correlation <- reactive({ + if (cor_available() != "available") { + return(cor_available()) + } + if (not_pressed(input$cor_run)) { + return(i18n$t("** Press the Calculate correlation button to generate output **")) + } + validate( + need( + input$cor_cutoff >= 0 && input$cor_cutoff <= 1, + i18n$t("Provide a correlation cutoff value in the range from 0 to 1") + ) + ) + withProgress(message = i18n$t("Calculating correlations"), value = 0.5, { + do.call(summary, c(list(object = .correlation()), cor_sum_inputs())) + }) +}) + +.plot_correlation <- reactive({ + if (cor_available() != "available") { + return(cor_available()) + } + if (not_pressed(input$cor_run)) { + return(i18n$t("** Press the Calculate correlation button to generate output **")) + } + req(input$cor_nrobs) + withProgress(message = i18n$t("Generating correlation plot"), value = 0.5, { + capture_plot(plot(.correlation(), nrobs = input$cor_nrobs)) + }) +}) + +correlation_report <- function() { + if (length(input$cor_vars) < 2) { + return(invisible()) + } + inp_out <- list("", "") + nrobs <- ifelse(is.empty(input$cor_nrobs), 1000, as_integer(input$cor_nrobs)) + inp_out[[1]] <- clean_args(cor_sum_inputs(), cor_sum_args[-1]) + inp_out[[2]] <- list(nrobs = nrobs) + + if (!is.empty(input$cor_name)) { + dataset <- fix_names(input$cor_name) + if (input$cor_name != dataset) { + updateTextInput(session, inputId = "cor_name", value = dataset) + } + xcmd <- paste0(dataset, " <- cor2df(result)\nregister(\"", dataset, "\", descr = result$descr)") + } else { + xcmd <- "" + } + + update_report( + inp_main = clean_args(cor_inputs(), cor_args), + fun_name = "correlation", + inp_out = inp_out, + fig.width = cor_plot_width(), + fig.height = cor_plot_height(), + xcmd = xcmd + ) +} + +download_handler( + id = "dlp_correlation", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_correlation"), + type = "png", + caption = i18n$t("Save correlation plot"), + plot = .plot_correlation, + width = cor_plot_width, + height = cor_plot_height +) + +observeEvent(input$cor_store, { + req(input$cor_name) + cmat <- try(.correlation(), silent = TRUE) + if (inherits(cmat, "try-error") || is.null(cmat)) { + return() + } + + dataset <- fix_names(input$cor_name) + updateTextInput(session, inputId = "cor_name", value = dataset) + r_data[[dataset]] <- cor2df(cmat) + register(dataset, descr = cmat$descr) + updateSelectInput(session, "dataset", selected = input$dataset) + + ## See https://shiny.posit.co//reference/shiny/latest/modalDialog.html + showModal( + modalDialog( + title = i18n$t("Data Stored"), + span( + i18n$t( + "Dataset '{dataset}' was successfully added to the datasets dropdown. Add code to Report > Rmd or Report > R to (re)create the results by clicking the report icon on the bottom left of your screen.", + dataset = dataset + ) + ), + footer = modalButton(i18n$t("OK")), + size = "s", + easyClose = TRUE + ) + ) +}) + +observeEvent(input$correlation_report, { + r_info[["latest_screenshot"]] <- NULL + correlation_report() +}) + +observeEvent(input$correlation_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_correlation_screenshot") +}) + +observeEvent(input$modal_correlation_screenshot, { + correlation_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/cross_tabs_ui.R b/radiant.basics/inst/app/tools/analysis/cross_tabs_ui.R new file mode 100644 index 0000000..b349d23 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/cross_tabs_ui.R @@ -0,0 +1,211 @@ +## alternative hypothesis options +ct_check <- c( + "observed", + "expected", + "chi_sq", + "dev_std", + "row_perc", + "col_perc", + "perc" +) + +names(ct_check) <- c( + i18n$t("Observed"), + i18n$t("Expected"), + i18n$t("Chi-squared"), + i18n$t("Deviation std."), + i18n$t("Row percentages"), + i18n$t("Column percentages"), + i18n$t("Table percentages") +) + +## list of function arguments +ct_args <- as.list(formals(cross_tabs)) + +## list of function inputs selected by user +ct_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + ct_args$data_filter <- if (input$show_filter) input$data_filter else "" + ct_args$dataset <- input$dataset + for (i in r_drop(names(ct_args))) { + ct_args[[i]] <- input[[paste0("ct_", i)]] + } + ct_args +}) + +############################### +# Cross-tabs +############################### +output$ui_ct_var1 <- renderUI({ + vars <- c("None" = "", groupable_vars()) + selectInput( + inputId = "ct_var1", label = i18n$t("Select a categorical variable:"), + choices = vars, selected = state_single("ct_var1", vars), multiple = FALSE + ) +}) + +output$ui_ct_var2 <- renderUI({ + if (not_available(input$ct_var1)) { + return() + } + vars <- c("None" = "", groupable_vars()) + + if (length(vars) > 0) vars <- vars[-which(vars == input$ct_var1)] + selectInput( + inputId = "ct_var2", label = i18n$t("Select a categorical variable:"), + selected = state_single("ct_var2", vars), + choices = vars, multiple = FALSE + ) +}) + +output$ui_cross_tabs <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_cross_tabs == 'Summary'", + uiOutput("ui_ct_var1"), + uiOutput("ui_ct_var2") + ), + br(), + checkboxGroupInput( + "ct_check", NULL, + choices = ct_check, + selected = state_group("ct_check"), + inline = FALSE + ) + ), + help_and_report( + modal_title = i18n$t("Cross-tabs"), + fun_name = "cross_tabs", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/cross_tabs.md")) + ) + ) +}) + +ct_plot <- reactive({ + list(plot_width = 650, plot_height = 400 * max(length(input$ct_check), 1)) +}) + +ct_plot_width <- function() { + ct_plot() %>% + (function(x) if (is.list(x)) x$plot_width else 650) +} + +ct_plot_height <- function() { + ct_plot() %>% + (function(x) if (is.list(x)) x$plot_height else 400) +} + +## output is called from the main radiant ui.R +output$cross_tabs <- renderUI({ + register_print_output("summary_cross_tabs", ".summary_cross_tabs") + register_plot_output( + "plot_cross_tabs", ".plot_cross_tabs", + height_fun = "ct_plot_height", + width_fun = "ct_plot_width" + ) + + ## two separate tabs + ct_output_panels <- tabsetPanel( + id = "tabs_cross_tabs", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_cross_tabs")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_cross_tabs"), + plotOutput("plot_cross_tabs", width = "100%", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Tables"), + tool = i18n$t("Cross-tabs"), + tool_ui = "ui_cross_tabs", + output_panels = ct_output_panels + ) +}) + +ct_available <- reactive({ + if (not_available(input$ct_var1) || not_available(input$ct_var2)) { + i18n$t("This analysis requires two categorical variables. Both must have two or more levels.\nIf these variable types are not available please select another dataset.\n\n") %>% + suggest_data("newspaper") + } else { + "available" + } +}) + +.cross_tabs <- reactive({ + cti <- ct_inputs() + cti$envir <- r_data + do.call(cross_tabs, cti) +}) + +.summary_cross_tabs <- reactive({ + if (ct_available() != "available") { + return(ct_available()) + } + summary(.cross_tabs(), check = input$ct_check) +}) + +.plot_cross_tabs <- reactive({ + if (ct_available() != "available") { + return(ct_available()) + } + validate(need(input$ct_check, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(.cross_tabs(), check = input$ct_check, shiny = TRUE) + }) +}) + +cross_tabs_report <- function() { + if (is.empty(input$ct_var1) || is.empty(input$ct_var2)) { + return(invisible()) + } + inp_out <- list("", "") + if (length(input$ct_check) > 0) { + outputs <- c("summary", "plot") + inp_out[[1]] <- list(check = input$ct_check) + inp_out[[2]] <- list(check = input$ct_check, custom = FALSE) + figs <- TRUE + } else { + outputs <- "summary" + inp_out[[1]] <- list(check = "") + figs <- FALSE + } + + update_report( + inp_main = clean_args(ct_inputs(), ct_args), + inp_out = inp_out, + fun_name = "cross_tabs", + outputs = outputs, + figs = figs, + fig.width = ct_plot_width(), + fig.height = ct_plot_height() + ) +} + +download_handler( + id = "dlp_cross_tabs", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_cross_tabs"), + type = "png", + caption = i18n$t("Save cross-tabs plot"), + plot = .plot_cross_tabs, + width = ct_plot_width, + height = ct_plot_height +) + +observeEvent(input$cross_tabs_report, { + r_info[["latest_screenshot"]] <- NULL + cross_tabs_report() +}) + +observeEvent(input$cross_tabs_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_cross_tabs_screenshot") +}) + +observeEvent(input$modal_cross_tabs_screenshot, { + cross_tabs_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/goodness_ui.R b/radiant.basics/inst/app/tools/analysis/goodness_ui.R new file mode 100644 index 0000000..66723f0 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/goodness_ui.R @@ -0,0 +1,197 @@ +## alternative hypothesis options +gd_check <- c("observed", "expected", "chi_sq", "dev_std") +names(gd_check) <- c( + i18n$t("Observed"), + i18n$t("Expected"), + i18n$t("Chi-squared"), + i18n$t("Deviation std.") +) + +## list of function arguments +gd_args <- as.list(formals(goodness)) + +## list of function inputs selected by user +gd_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + gd_args$data_filter <- if (input$show_filter) input$data_filter else "" + gd_args$dataset <- input$dataset + for (i in r_drop(names(gd_args))) { + gd_args[[i]] <- input[[paste0("gd_", i)]] + } + gd_args +}) + +############################### +# Goodness of fit test +############################### +output$ui_gd_var <- renderUI({ + vars <- c("None" = "", groupable_vars()) + selectInput( + "gd_var", i18n$t("Select a categorical variable:"), + choices = vars, + selected = state_single("gd_var", vars), + multiple = FALSE + ) +}) + +output$ui_gd_p <- renderUI({ + req(input$gd_var) + returnTextInput( + "gd_p", i18n$t("Probabilities:"), + value = state_init("gd_p", ""), + placeholder = i18n$t("Enter probabilities (e.g., 1/2 1/2)") + ) +}) + +output$ui_goodness <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_goodness == 'Summary'", + uiOutput("ui_gd_var"), + uiOutput("ui_gd_p"), + br() + ), + checkboxGroupInput( + "gd_check", NULL, + choices = gd_check, + selected = state_group("gd_check"), + inline = FALSE + ) + ), + help_and_report( + modal_title = i18n$t("Goodness of fit"), + fun_name = "goodness", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/goodness.md")) + ) + ) +}) + +gd_plot <- reactive({ + list(plot_width = 650, plot_height = 400 * max(length(input$gd_check), 1)) +}) + +gd_plot_width <- function() { + gd_plot() %>% + (function(x) if (is.list(x)) x$plot_width else 650) +} + +gd_plot_height <- function() { + gd_plot() %>% + (function(x) if (is.list(x)) x$plot_height else 400) +} + +## output is called from the main radiant ui.R +output$goodness <- renderUI({ + register_print_output("summary_goodness", ".summary_goodness") + register_plot_output( + "plot_goodness", ".plot_goodness", + height_fun = "gd_plot_height", + width_fun = "gd_plot_width" + ) + + ## two separate tabs + gd_output_panels <- tabsetPanel( + id = "tabs_goodness", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_goodness")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_goodness"), + plotOutput("plot_goodness", width = "100%", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Tables"), + tool = i18n$t("Goodness of fit"), + tool_ui = "ui_goodness", + output_panels = gd_output_panels + ) +}) + +gd_available <- reactive({ + if (not_available(input$gd_var)) { + i18n$t("This analysis requires a categorical variables with two or more levels.\nIf such a variable type is not available please select another dataset.\n\n") %>% suggest_data("newspaper") + } else { + "available" + } +}) + +.goodness <- reactive({ + gdi <- gd_inputs() + gdi$envir <- r_data + do.call(goodness, gdi) +}) + +.summary_goodness <- reactive({ + if (gd_available() != "available") { + return(gd_available()) + } + summary(.goodness(), check = input$gd_check) +}) + +.plot_goodness <- reactive({ + if (gd_available() != "available") { + return(gd_available()) + } + validate(need(input$gd_check, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(.goodness(), check = input$gd_check, shiny = TRUE) + }) +}) + +goodness_report <- function() { + if (is.empty(input$gd_var)) { + return(invisible()) + } + inp_out <- list("", "") + if (length(input$gd_check) > 0) { + outputs <- c("summary", "plot") + inp_out[[1]] <- list(check = input$gd_check) + inp_out[[2]] <- list(check = input$gd_check, custom = FALSE) + figs <- TRUE + } else { + outputs <- "summary" + figs <- FALSE + } + + gdi <- gd_inputs() + gdi$p <- radiant.data::make_vec(gdi$p) + + update_report( + inp_main = clean_args(gdi, gd_args), + inp_out = inp_out, + fun_name = "goodness", + outputs = outputs, + figs = figs, + fig.width = gd_plot_width(), + fig.height = gd_plot_height() + ) +} + +download_handler( + id = "dlp_goodness", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_goodness"), + type = "png", + caption = i18n$t("Save goodness of fit plot"), + plot = .plot_goodness, + width = gd_plot_width, + height = gd_plot_height +) + +observeEvent(input$goodness_report, { + r_info[["latest_screenshot"]] <- NULL + goodness_report() +}) + +observeEvent(input$goodness_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_goodness_screenshot") +}) + +observeEvent(input$modal_goodness_screenshot, { + goodness_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/homo_variance_test_ui.R b/radiant.basics/inst/app/tools/analysis/homo_variance_test_ui.R new file mode 100644 index 0000000..7a400fd --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/homo_variance_test_ui.R @@ -0,0 +1,190 @@ +############################################ +## Homogeneity of variance test - ui +############################################ + +## 1. 翻译标签 +hv_method <- c("levene", "bartlett", "fligner") +names(hv_method) <- c(i18n$t("Levene"), + i18n$t("Bartlett"), + i18n$t("Fligner")) + +hv_plots <- c("hist", "density", "boxplot") +names(hv_plots) <- c(i18n$t("Histogram"), + i18n$t("Density"), + i18n$t("Boxplot")) + +## 2. 函数形参 +hv_args <- as.list(formals(homo_variance_test)) + +## 3. 收集输入 +hv_inputs <- reactive({ + hv_args$data_filter <- if (input$show_filter) input$data_filter else "" + hv_args$dataset <- input$dataset + for (i in r_drop(names(hv_args))) { + hv_args[[i]] <- input[[paste0("hv_", i)]] + } + hv_args +}) + +## 4. 变量选择(numeric + grouping) +output$ui_hv_var <- renderUI({ + isNum <- .get_class() %in% c("integer", "numeric", "ts") + vars <- c("None" = "", varnames()[isNum]) + selectInput( + inputId = "hv_var", label = i18n$t("Variable (select one):"), + choices = vars, selected = state_single("hv_var", vars), multiple = FALSE + ) +}) + +output$ui_hv_group <- renderUI({ + vars <- groupable_vars() + selectInput( + inputId = "hv_group", label = i18n$t("Grouping variable:"), + choices = vars, selected = state_single("hv_group", vars), multiple = FALSE + ) +}) + +## 5. 主 UI +output$ui_homo_variance_test <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_homo_variance_test == 'Summary'", + uiOutput("ui_hv_var"), + uiOutput("ui_hv_group"), + selectInput( + inputId = "hv_method", label = i18n$t("Test method:"), + choices = hv_method, + selected = state_single("hv_method", hv_method, "levene"), + multiple = FALSE + ), + sliderInput( + "hv_conf_lev", i18n$t("Confidence level:"), + min = 0.85, max = 0.99, + value = state_init("hv_conf_lev", 0.95), step = 0.01 + ) + ), + conditionalPanel( + condition = "input.tabs_homo_variance_test == 'Plot'", + selectizeInput( + inputId = "hv_plots", label = i18n$t("Select plots:"), + choices = hv_plots, + selected = state_multiple("hv_plots", hv_plots, "boxplot"), + multiple = TRUE, + options = list(placeholder = i18n$t("Select plots"), + plugins = list("remove_button", "drag_drop")) + ) + ) + ), + help_and_report( + modal_title = i18n$t("Homogeneity of variance test"), + fun_name = "homo_variance_test", + help_file = inclMD(file.path(getOption("radiant.path.basics"), + "app/tools/help/homo_variance_test.md")) + ) + ) +}) + +## 6. 画图尺寸 +hv_plot <- reactive({ + list(plot_width = 650, + plot_height = 400 * max(length(input$hv_plots), 1)) +}) +hv_plot_width <- function() hv_plot()$plot_width +hv_plot_height <- function() hv_plot()$plot_height + +## 7. 输出面板 +output$homo_variance_test <- renderUI({ + register_print_output("summary_homo_variance_test", ".summary_homo_variance_test") + register_plot_output("plot_homo_variance_test", ".plot_homo_variance_test", + height_fun = "hv_plot_height") + + hv_output_panels <- tabsetPanel( + id = "tabs_homo_variance_test", + tabPanel(title = i18n$t("Summary"), + value = "Summary", + verbatimTextOutput("summary_homo_variance_test")), + tabPanel(title = i18n$t("Plot"), + value = "Plot", + download_link("dlp_homo_variance_test"), + plotOutput("plot_homo_variance_test", height = "100%")) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Homogeneity"), + tool = i18n$t("Homogeneity of variance test"), + tool_ui = "ui_homo_variance_test", + output_panels = hv_output_panels + ) +}) + +## 8. 可用性检查 +hv_available <- reactive({ + if (not_available(input$hv_var)) + return(i18n$t("This analysis requires a numeric variable. If none are\navailable please select another dataset.") %>% suggest_data("demand_uk")) + if (not_available(input$hv_group)) + return(i18n$t("Please select a grouping variable.")) + "available" +}) + +## 9. 计算核心 +.homo_variance_test <- reactive({ + hvi <- hv_inputs() + hvi$envir <- r_data + do.call(homo_variance_test, hvi) +}) + +.summary_homo_variance_test <- reactive({ + if (hv_available() != "available") return(hv_available()) + summary(.homo_variance_test()) +}) + +.plot_homo_variance_test <- reactive({ + if (hv_available() != "available") return(hv_available()) + validate(need(input$hv_plots, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, + plot(.homo_variance_test(), plots = input$hv_plots, shiny = TRUE)) +}) + +## 10. Report +homo_variance_test_report <- function() { + if (is.empty(input$hv_var)) return(invisible()) + figs <- length(input$hv_plots) > 0 + outputs <- if (figs) c("summary", "plot") else "summary" + inp_out <- if (figs) list("", list(plots = input$hv_plots, custom = FALSE)) else list("", "") + update_report(inp_main = clean_args(hv_inputs(), hv_args), + fun_name = "homo_variance_test", + inp_out = inp_out, + outputs = outputs, + figs = figs, + fig.width = hv_plot_width(), + fig.height = hv_plot_height()) +} + +## 11. 下载 & 截图 +download_handler( + id = "dlp_homo_variance_test", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_homo_variance_test"), + type = "png", + caption = i18n$t("Save homogeneity of variance plot"), + plot = .plot_homo_variance_test, + width = hv_plot_width, + height = hv_plot_height +) + +observeEvent(input$homo_variance_test_report, { + r_info[["latest_screenshot"]] <- NULL + homo_variance_test_report() +}) + +observeEvent(input$homo_variance_test_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_homo_variance_test_screenshot") +}) + +observeEvent(input$modal_homo_variance_test_screenshot, { + homo_variance_test_report() + removeModal() +}) diff --git a/radiant.basics/inst/app/tools/analysis/normality_test_ui.R b/radiant.basics/inst/app/tools/analysis/normality_test_ui.R new file mode 100644 index 0000000..b22ad9a --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/normality_test_ui.R @@ -0,0 +1,181 @@ +############################################ +## Normality test - ui +############################################ + +## 1. 翻译标签 +nt_method <- c("shapiro", "ks", "ad") # 先给 3 个常用方法 +names(nt_method) <- c(i18n$t("Shapiro-Wilk"), + i18n$t("Kolmogorov-Smirnov"), + i18n$t("Anderson-Darling")) + +nt_plots <- c("qq", "hist", "pp", "density") +names(nt_plots) <- c(i18n$t("Q-Q plot"), + i18n$t("Histogram"), + i18n$t("P-P plot"), + i18n$t("Density")) + +## 2. 函数形参 +nt_args <- as.list(formals(normality_test)) + +## 3. 收集输入 +nt_inputs <- reactive({ + nt_args$data_filter <- if (input$show_filter) input$data_filter else "" + nt_args$dataset <- input$dataset + for (i in r_drop(names(nt_args))) { + nt_args[[i]] <- input[[paste0("nt_", i)]] + } + nt_args +}) + +## 4. 变量选择(仅 numeric) +output$ui_nt_var <- renderUI({ + isNum <- .get_class() %in% c("integer", "numeric", "ts") + vars <- c("None" = "", varnames()[isNum]) + selectInput( + inputId = "nt_var", label = i18n$t("Variable (select one):"), + choices = vars, selected = state_single("nt_var", vars), multiple = FALSE + ) +}) + +## 5. 主 UI +output$ui_normality_test <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_normality_test == 'Summary'", + uiOutput("ui_nt_var"), + selectInput( + inputId = "nt_method", label = i18n$t("Test method:"), + choices = nt_method, + selected = state_single("nt_method", nt_method, "shapiro"), + multiple = FALSE + ), + sliderInput( + "nt_conf_lev", i18n$t("Confidence level:"), + min = 0.85, max = 0.99, + value = state_init("nt_conf_lev", 0.95), step = 0.01 + ) + ), + conditionalPanel( + condition = "input.tabs_normality_test == 'Plot'", + selectizeInput( + inputId = "nt_plots", label = i18n$t("Select plots:"), + choices = nt_plots, + selected = state_multiple("nt_plots", nt_plots, "qq"), + multiple = TRUE, + options = list(placeholder = i18n$t("Select plots"), + plugins = list("remove_button", "drag_drop")) + ) + ) + ), + help_and_report( + modal_title = i18n$t("Normality test"), + fun_name = "normality_test", + help_file = inclMD(file.path(getOption("radiant.path.basics"), + "app/tools/help/normality_test.md")) + ) + ) +}) + +## 6. 画图尺寸(直接抄) +nt_plot <- reactive({ + list(plot_width = 650, + plot_height = 400 * max(length(input$nt_plots), 1)) +}) +nt_plot_width <- function() nt_plot()$plot_width +nt_plot_height <- function() nt_plot()$plot_height + + +## 7. 输出面板 +output$normality_test <- renderUI({ + register_print_output("summary_normality_test", ".summary_normality_test") + register_plot_output("plot_normality_test", ".plot_normality_test", + height_fun = "nt_plot_height") + + nt_output_panels <- tabsetPanel( + id = "tabs_normality_test", + tabPanel(title = i18n$t("Summary"), + value = "Summary", + verbatimTextOutput("summary_normality_test")), + tabPanel(title = i18n$t("Plot"), + value = "Plot", + download_link("dlp_normality_test"), + plotOutput("plot_normality_test", height = "100%")) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Normality"), + tool = i18n$t("Normality test"), + tool_ui = "ui_normality_test", + output_panels = nt_output_panels + ) +}) + +## 8. 可用性检查 +nt_available <- reactive({ + if (not_available(input$nt_var)) + return(i18n$t("This analysis requires a numeric variable. If none are\navailable please select another dataset.") %>% suggest_data("demand_uk")) + "available" +}) + +## 9. 计算核心 +.normality_test <- reactive({ + nti <- nt_inputs() + nti$envir <- r_data + do.call(normality_test, nti) +}) + +.summary_normality_test <- reactive({ + if (nt_available() != "available") return(nt_available()) + summary(.normality_test()) +}) + +.plot_normality_test <- reactive({ + if (nt_available() != "available") return(nt_available()) + validate(need(input$nt_plots, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, + plot(.normality_test(), plots = input$nt_plots, shiny = TRUE)) +}) + +## 10. Report +normality_test_report <- function() { + if (is.empty(input$nt_var)) return(invisible()) + figs <- length(input$nt_plots) > 0 + outputs <- if (figs) c("summary", "plot") else "summary" + inp_out <- if (figs) list("", list(plots = input$nt_plots, custom = FALSE)) else list("", "") + update_report(inp_main = clean_args(nt_inputs(), nt_args), + fun_name = "normality_test", + inp_out = inp_out, + outputs = outputs, + figs = figs, + fig.width = nt_plot_width(), + fig.height = nt_plot_height()) +} + +## 11. 下载 & 截图 +download_handler( + id = "dlp_normality_test", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_normality_test"), + type = "png", + caption = i18n$t("Save normality test plot"), + plot = .plot_normality_test, + width = nt_plot_width, + height = nt_plot_height +) + +observeEvent(input$normality_test_report, { + r_info[["latest_screenshot"]] <- NULL + normality_test_report() +}) + +observeEvent(input$normality_test_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_normality_test_screenshot") +}) + +observeEvent(input$modal_normality_test_screenshot, { + normality_test_report() + removeModal() +}) \ No newline at end of file diff --git a/radiant.basics/inst/app/tools/analysis/prob_calc_ui.R b/radiant.basics/inst/app/tools/analysis/prob_calc_ui.R new file mode 100644 index 0000000..a842298 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/prob_calc_ui.R @@ -0,0 +1,572 @@ +pc_dist <- c("binom", "chisq", "disc", "expo", "fdist", "lnorm", "norm", "pois", "tdist", "unif") +names(pc_dist) <- c( + i18n$t("Binomial"), i18n$t("Chi-squared"), i18n$t("Discrete"), + i18n$t("Exponential"), i18n$t("F"), i18n$t("Log normal"), + i18n$t("Normal"), i18n$t("Poisson"), i18n$t("t"), i18n$t("Uniform") +) + +pc_type <- c("values", "probs") +names(pc_type) <- c(i18n$t("Values"), i18n$t("Probabilities")) + + +make_pc_values_input <- function(lb, lb_init = NA, ub, ub_init = 0) { + if(!is.empty(r_state[[lb]])) ub_init <- NA + if(!is.empty(r_state[[ub]])) lb_init <- NA + tags$table( + tags$td(numericInput(lb, i18n$t("Lower bound:"), value = state_init(lb, lb_init))), + tags$td(numericInput(ub, i18n$t("Upper bound:"), value = state_init(ub, ub_init))) + ) +} + +make_side_by_side <- function(a, b) { + tags$table( + tags$td(a, width="50%"), + tags$td(b, width="50%"), + width="100%" + ) +} + +make_pc_prob_input <- function(lb, lb_init = NA, ub, ub_init = 0.95) { + if(!is.empty(r_state[[lb]])) ub_init <- NA + if(!is.empty(r_state[[ub]])) lb_init <- NA + make_side_by_side( + numericInput( + lb, i18n$t("Lower bound:"), value = state_init(lb, lb_init), + min = 0, max = 1, step = .005 + ), + numericInput( + ub, i18n$t("Upper bound:"), value = state_init(ub, ub_init), + min = 0, max = 1, step = .005 + ) + ) +} + +output$ui_pc_pois <- renderUI({ + numericInput( + "pcp_lambda", i18n$t("Lambda:"), + value = state_init("pcp_lambda", 1), + min = 1 + ) +}) + +output$ui_pc_input_pois <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcp_lb", lb_init = NA, "pcp_ub", ub_init = 3) + } else { + make_pc_prob_input("pcp_plb", lb_init = NA, "pcp_pub", ub_init = 0.95) + } +}) + +output$ui_pc_expo <- renderUI({ + numericInput( + "pce_rate", i18n$t("Rate:"), + value = state_init("pce_rate", 1), + min = 0 + ) +}) + +output$ui_pc_input_expo <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pce_lb", lb_init = NA, "pce_ub", ub_init = 2.996) + } else { + make_pc_prob_input("pce_plb", lb_init = NA, "pce_pub", ub_init = 0.95) + } +}) + +output$ui_pc_disc <- renderUI({ + tagList( + returnTextInput( + "pcd_v", i18n$t("Values:"), + value = state_init("pcd_v", "1 2 3 4 5 6") + ), + returnTextInput( + "pcd_p", i18n$t("Probabilities:"), + value = state_init("pcd_p", "1/6") + ) + ) +}) + +output$ui_pc_input_disc <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcd_lb", lb_init = NA, "pcd_ub", ub_init = 3) + } else { + make_pc_prob_input("pcd_plb", lb_init = NA, "pcd_pub", ub_init = 0.95) + } +}) + +output$ui_pc_fdist <- renderUI({ + tagList( + numericInput( + "pcf_df1", i18n$t("Degrees of freedom 1:"), + value = state_init("pcf_df1", 10), + min = 1 + ), + numericInput( + "pcf_df2", i18n$t("Degrees of freedom 2:"), + value = state_init("pcf_df2", 10), + min = 5 + ) + ) +}) + +output$ui_pc_input_fdist <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcf_lb", lb_init = NA, "pcf_ub", ub_init = 2.978) + } else { + make_pc_prob_input("pcf_plb", lb_init = NA, "pcf_pub", ub_init = 0.95) + } +}) + +output$ui_pc_chisq <- renderUI({ + numericInput( + "pcc_df", i18n$t("Degrees of freedom:"), + value = state_init("pcc_df", 1), + min = 1 + ) +}) + +output$ui_pc_input_chisq <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcc_lb", lb_init = NA, "pcc_ub", ub_init = 3.841) + } else { + make_pc_prob_input("pcc_plb", lb_init = NA, "pcc_pub", ub_init = 0.95) + } +}) + +output$ui_pc_tdist <- renderUI({ + numericInput( + "pct_df", i18n$t("Degrees of freedom:"), + value = state_init("pct_df", 10), + min = 3 + ) +}) + +output$ui_pc_input_tdist <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pct_lb", lb_init = -Inf, "pct_ub", ub_init = 2.228) + } else { + make_pc_prob_input("pct_plb", lb_init = 0.025, "pct_pub", ub_init = 0.975) + } +}) + +output$ui_pc_norm <- renderUI({ + make_side_by_side( + numericInput( + "pc_mean", i18n$t("Mean:"), + value = state_init("pc_mean", 0) + ), + numericInput( + "pc_stdev", i18n$t("St. dev:"), + min = 0, + value = state_init("pc_stdev", 1) + ) + ) +}) + +output$ui_pc_input_norm <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pc_lb", lb_init = -Inf, "pc_ub", ub_init = 0) + } else { + make_pc_prob_input("pc_plb", lb_init = 0.025, "pc_pub", ub_init = 0.975) + } +}) + +output$ui_pc_lnorm <- renderUI({ + make_side_by_side( + numericInput( + "pcln_meanlog", i18n$t("Mean log:"), + value = state_init("pcln_meanlog", 0) + ), + numericInput( + "pcln_sdlog", i18n$t("St. dev log:"), + min = 0, + value = state_init("pcln_sdlog", 1) + ) + ) +}) + +output$ui_pc_input_lnorm <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcln_lb", lb_init = 0, "pcln_ub", ub_init = 1) + } else { + make_pc_prob_input("pcln_plb", lb_init = 0.025, "pcln_pub", ub_init = 0.975) + } +}) + +output$ui_pc_binom <- renderUI({ + make_side_by_side( + numericInput( + "pcb_n", label = i18n$t("n:"), + value = state_init("pcb_n", 10), min = 0 + ), + numericInput( + "pcb_p", i18n$t("p:"), + min = 0, max = 1, step = .005, + value = state_init("pcb_p", .2) + ) + ) +}) + +output$ui_pc_input_binom <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcb_lb", lb_init = NA, "pcb_ub", ub_init = 3) + } else { + make_pc_prob_input("pcb_plb", lb_init = NA, "pcb_pub", ub_init = 0.3) + } +}) + +output$ui_pc_unif <- renderUI({ + make_side_by_side( + numericInput( + "pcu_min", i18n$t("Min:"), + value = state_init("pcu_min", 0) + ), + numericInput( + "pcu_max", i18n$t("Max:"), + value = state_init("pcu_max", 1) + ) + ) +}) + +output$ui_pc_input_unif <- renderUI({ + if (input$pc_type == "values") { + make_pc_values_input("pcu_lb", lb_init = NA, "pcu_ub", ub_init = 0.3) + } else { + make_pc_prob_input("pcu_plb", lb_init = NA, "pcu_pub", ub_init = 0.3) + } +}) + +output$ui_prob_calc <- renderUI({ + tagList( + wellPanel( + selectInput( + "pc_dist", label = i18n$t("Distribution:"), + choices = pc_dist, + selected = state_init("pc_dist", "norm"), + multiple = FALSE + ), + conditionalPanel( + "input.pc_dist == 'norm'", + uiOutput("ui_pc_norm") + ), + conditionalPanel( + "input.pc_dist == 'lnorm'", + uiOutput("ui_pc_lnorm") + ), + conditionalPanel( + "input.pc_dist == 'binom'", + uiOutput("ui_pc_binom") + ), + conditionalPanel( + "input.pc_dist == 'unif'", + uiOutput("ui_pc_unif") + ), + conditionalPanel( + "input.pc_dist == 'tdist'", + uiOutput("ui_pc_tdist") + ), + conditionalPanel( + "input.pc_dist == 'fdist'", + uiOutput("ui_pc_fdist") + ), + conditionalPanel( + "input.pc_dist == 'chisq'", + uiOutput("ui_pc_chisq") + ), + conditionalPanel( + "input.pc_dist == 'disc'", + uiOutput("ui_pc_disc") + ), + conditionalPanel( + "input.pc_dist == 'expo'", + uiOutput("ui_pc_expo") + ), + conditionalPanel( + "input.pc_dist == 'pois'", + uiOutput("ui_pc_pois") + ) + ), + wellPanel( + radioButtons( + "pc_type", i18n$t("Input type:"), + choices = pc_type, + selected = state_init("pc_type", "values"), + inline = TRUE + ), + conditionalPanel( + "input.pc_dist == 'norm'", + uiOutput("ui_pc_input_norm") + ), + conditionalPanel( + "input.pc_dist == 'lnorm'", + uiOutput("ui_pc_input_lnorm") + ), + conditionalPanel( + "input.pc_dist == 'binom'", + uiOutput("ui_pc_input_binom") + ), + conditionalPanel( + "input.pc_dist == 'unif'", + uiOutput("ui_pc_input_unif") + ), + conditionalPanel( + "input.pc_dist == 'tdist'", + uiOutput("ui_pc_input_tdist") + ), + conditionalPanel( + "input.pc_dist == 'fdist'", + uiOutput("ui_pc_input_fdist") + ), + conditionalPanel( + "input.pc_dist == 'chisq'", + uiOutput("ui_pc_input_chisq") + ), + conditionalPanel( + "input.pc_dist == 'disc'", + uiOutput("ui_pc_input_disc") + ), + conditionalPanel( + "input.pc_dist == 'expo'", + uiOutput("ui_pc_input_expo") + ), + conditionalPanel( + "input.pc_dist == 'pois'", + uiOutput("ui_pc_input_pois") + ), + numericInput( + "pc_dec", i18n$t("Decimals:"), + value = state_init("pc_dec", 3), + min = 0 + ) + ), + help_and_report( + modal_title = i18n$t("Probability calculator"), + fun_name = "prob_calc", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/prob_calc.md")) + ) + ) +}) + +pc_plot_width <- function() + if (!is.null(input$viz_plot_width)) input$viz_plot_width else 650 + +pc_plot_height <- function() 400 + +pc_args <- reactive({ + pc_dist <- input$pc_dist + if (is.empty(pc_dist) || pc_dist == "norm") { + as.list(formals(prob_norm)) + } else if (pc_dist == "lnorm") { + as.list(formals(prob_lnorm)) + } else if (pc_dist == "binom") { + as.list(formals(prob_binom)) + } else if (pc_dist == "unif") { + as.list(formals(prob_unif)) + } else if (pc_dist == "tdist") { + as.list(formals(prob_tdist)) + } else if (pc_dist == "fdist") { + as.list(formals(prob_fdist)) + } else if (pc_dist == "chisq") { + as.list(formals(prob_chisq)) + } else if (pc_dist == "disc") { + as.list(formals(prob_disc)) + } else if (pc_dist == "expo") { + as.list(formals(prob_expo)) + } else if (pc_dist == "pois") { + as.list(formals(prob_pois)) + } +}) + +## list of function inputs selected by user +pc_inputs <- reactive({ + pc_dist <- input$pc_dist + if (is.empty(pc_dist) || pc_dist == "norm") { + pre <- "pc_" + } else if (pc_dist == "lnorm") { + pre <- "pcln_" + } else if (pc_dist == "binom") { + pre <- "pcb_" + } else if (pc_dist == "unif") { + pre <- "pcu_" + } else if (pc_dist == "tdist") { + pre <- "pct_" + } else if (pc_dist == "fdist") { + pre <- "pcf_" + } else if (pc_dist == "chisq") { + pre <- "pcc_" + } else if (pc_dist == "disc") { + pre <- "pcd_" + } else if (pc_dist == "expo") { + pre <- "pce_" + } else if (pc_dist == "pois") { + pre <- "pcp_" + } + + # loop needed because reactive values don't allow single bracket indexing + args <- pc_args() + for (i in names(args)) + args[[i]] <- input[[paste0(pre, i)]] + + validate( + need( + input$pc_dec, + i18n$t("Provide an integer value for the number of decimal places") + ) + ) + + args[["dec"]] <- input$pc_dec + args +}) + +## output is called from the main radiant ui.R +output$prob_calc <- renderUI({ + register_print_output("summary_prob_calc", ".summary_prob_calc") + register_plot_output( + "plot_prob_calc", ".plot_prob_calc", + height_fun = "pc_plot_height", + width_fun = "pc_plot_width" + ) + + ## two separate tabs + pc_output_panels <- tagList( + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_prob_calc")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_prob_calc"), + plotOutput("plot_prob_calc", width = "100%", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Probability"), + tool = i18n$t("Probability calculator"), + data = NULL, + tool_ui = "ui_prob_calc", + output_panels = pc_output_panels + ) +}) + +pc_available <- reactive({ + if (is.empty(input$pc_dist) || is.empty(input$pc_type)) { + "" + } else { + a <- "available" + if (input$pc_dist == "norm") { + if (is_not(input$pc_mean) || is_not(input$pc_stdev) || input$pc_stdev <= 0) { + a <- i18n$t("Please provide a mean and standard deviation (> 0)") + } + } else if (input$pc_dist == "lnorm") { + if (is_not(input$pcln_meanlog) || is_not(input$pcln_sdlog) || input$pcln_sdlog <= 0) { + a <- i18n$t("Please provide a mean and standard deviation (> 0)") + } + } else if (input$pc_dist == "binom") { + if (is_not(input$pcb_n) || input$pcb_n < 0 || is_not(input$pcb_p) || input$pcb_p < 0) { + a <- i18n$t("Please provide a value for n (number of trials) and p (probability of success)") + } + } else if (input$pc_dist == "unif") { + if (is_not(input$pcu_min) || is_not(input$pcu_max)) { + a <- i18n$t("Please provide a minimum and a maximum value") + } + } else if (input$pc_dist == "tdist") { + if (is_not(input$pct_df)) { + a <- i18n$t("Please provide a value for the degrees of freedom (> 0)") + } + } else if (input$pc_dist == "fdist") { + if (is_not(input$pcf_df1) || is_not(input$pcf_df2) || input$pcf_df1 < 1 || input$pcf_df2 < 5) { + a <- i18n$t("Please provide a value for Degrees of freedom 1 (> 0)\nand for Degrees of freedom 2 (> 4)") + } + } else if (input$pc_dist == "chisq") { + if (is_not(input$pcc_df)) { + a <- i18n$t("Please provide a value for the degrees of freedom (> 0)") + } + } else if (input$pc_dist == "disc") { + if (is.empty(input$pcd_v) || is.empty(input$pcd_p)) { + a <- i18n$t("Please provide a set of values and probabilities.\nSeparate numbers using spaces (e.g., 1/2 1/2)") + } + } else if (input$pc_dist == "expo") { + if (is_not(input$pce_rate) || input$pce_rate <= 0) { + a <- i18n$t("Please provide a value for the rate (> 0)") + } + } else if (input$pc_dist == "pois") { + if (is_not(input$pcp_lambda) || input$pcp_lambda <= 0) { + a <- i18n$t("Please provide a value for lambda (> 0)") + } + } else { + a <- "" + } + a + } +}) + +.prob_calc <- reactive({ + validate( + need(pc_available() == "available", pc_available()) + ) + do.call(get(paste0("prob_", input$pc_dist)), pc_inputs()) +}) + +.summary_prob_calc <- reactive({ + type <- if (is.empty(input$pc_type)) "values" else input$pc_type + summary(.prob_calc(), type = type) +}) + +.plot_prob_calc <- reactive({ + req(pc_available() == "available") + type <- if (is.empty(input$pc_type)) "values" else input$pc_type + plot(.prob_calc(), type = type) +}) + +prob_calc_report <- function() { + req(input$pc_dist) + type <- input$pc_type + inp <- pc_inputs() + if (!is.null(type) && type == "probs") { + inp_out <- list(type = type) %>% list(., .) + inp[["ub"]] <- inp[["lb"]] <- NA + } else { + inp_out <- list("", "") + inp[["pub"]] <- inp[["plb"]] <- NA + } + + if (input$pc_dist == "disc") { + inp$v <- radiant.data::make_vec(inp$v) + inp$p <- radiant.data::make_vec(inp$p) + } + + outputs <- c("summary", "plot") + update_report( + inp_main = clean_args(inp, pc_args()), + fun_name = paste0("prob_", input$pc_dist), + inp_out = inp_out, + outputs = outputs, + figs = TRUE, + fig.width = pc_plot_width(), + fig.height = pc_plot_height() + ) +} + +download_handler( + id = "dlp_prob_calc", + fun = download_handler_plot, + fn = function() paste0(input$pc_dist, "_prob_calc"), + type = "png", + caption = i18n$t("Save probability calculator plot"), + plot = .plot_prob_calc, + width = pc_plot_width, + height = pc_plot_height +) + +observeEvent(input$prob_calc_report, { + r_info[["latest_screenshot"]] <- NULL + prob_calc_report() +}) + +observeEvent(input$prob_calc_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_prob_calc_screenshot") +}) + +observeEvent(input$modal_prob_calc_screenshot, { + prob_calc_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/single_mean_ui.R b/radiant.basics/inst/app/tools/analysis/single_mean_ui.R new file mode 100644 index 0000000..7dd1259 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/single_mean_ui.R @@ -0,0 +1,201 @@ +############################### +## Single mean - ui +############################### + +## alternative hypothesis options +sm_alt <- c("two.sided", "less", "greater") +names(sm_alt) <- c(i18n$t("Two sided"), i18n$t("Less than"), i18n$t("Greater than")) + +sm_plots <- c("hist", "simulate") +names(sm_plots) <- c(i18n$t("Histogram"), i18n$t("Simulate")) + +## list of function arguments +sm_args <- as.list(formals(single_mean)) + +## list of function inputs selected by user +sm_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + sm_args$data_filter <- if (input$show_filter) input$data_filter else "" + sm_args$dataset <- input$dataset + for (i in r_drop(names(sm_args))) { + sm_args[[i]] <- input[[paste0("sm_", i)]] + } + sm_args +}) + +output$ui_sm_var <- renderUI({ + isNum <- .get_class() %in% c("integer", "numeric", "ts") + vars <- c("None" = "", varnames()[isNum]) + selectInput( + inputId = "sm_var", label = i18n$t("Variable (select one):"), + choices = vars, selected = state_single("sm_var", vars), multiple = FALSE + ) +}) + +output$ui_single_mean <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_single_mean == 'Summary'", + uiOutput("ui_sm_var"), + selectInput( + inputId = "sm_alternative", label = i18n$t("Alternative hypothesis:"), + choices = sm_alt, + selected = state_single("sm_alternative", sm_alt, sm_args$alternative), + multiple = FALSE + ), + sliderInput( + "sm_conf_lev", i18n$t("Confidence level:"), + min = 0.85, max = 0.99, + value = state_init("sm_conf_lev", sm_args$conf_lev), step = 0.01 + ), + numericInput( + "sm_comp_value", i18n$t("Comparison value:"), + state_init("sm_comp_value", sm_args$comp_value) + ) + ), + conditionalPanel( + condition = "input.tabs_single_mean == 'Plot'", + selectizeInput( + inputId = "sm_plots", label = i18n$t("Select plots:"), + choices = sm_plots, + selected = state_multiple("sm_plots", sm_plots, "hist"), + multiple = TRUE, + options = list(placeholder = i18n$t("Select plots"), plugins = list("remove_button", "drag_drop")) + ) + ) + ), + help_and_report( + modal_title = i18n$t("Single mean"), + fun_name = "single_mean", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/single_mean.md")) + ) + ) +}) + +sm_plot <- reactive({ + list(plot_width = 650, plot_height = 400 * max(length(input$sm_plots), 1)) +}) + +sm_plot_width <- function() { + sm_plot() %>% + { + if (is.list(.)) .$plot_width else 650 + } +} + +sm_plot_height <- function() { + sm_plot() %>% + { + if (is.list(.)) .$plot_height else 400 + } +} + +## output is called from the main radiant ui.R +output$single_mean <- renderUI({ + register_print_output("summary_single_mean", ".summary_single_mean") + register_plot_output( + "plot_single_mean", ".plot_single_mean", + height_fun = "sm_plot_height" + ) + + ## two separate tabs + sm_output_panels <- tabsetPanel( + id = "tabs_single_mean", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_single_mean")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_single_mean"), + plotOutput("plot_single_mean", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Means"), + tool = i18n$t("Single mean"), + tool_ui = "ui_single_mean", + output_panels = sm_output_panels + ) +}) + +sm_available <- reactive({ + if (not_available(input$sm_var)) { + i18n$t("This analysis requires a variable of type numeric or interval. If none are\navailable please select another dataset.\n\n") %>% suggest_data("demand_uk") + } else if (is.na(input$sm_comp_value)) { + i18n$t("Please choose a comparison value") + } else { + "available" + } +}) + +.single_mean <- reactive({ + smi <- sm_inputs() + smi$envir <- r_data + do.call(single_mean, smi) +}) + +.summary_single_mean <- reactive({ + if (sm_available() != "available") { + return(sm_available()) + } + summary(.single_mean()) +}) + +.plot_single_mean <- reactive({ + if (sm_available() != "available") { + return(sm_available()) + } + validate(need(input$sm_plots, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(.single_mean(), plots = input$sm_plots, shiny = TRUE) + }) +}) + +single_mean_report <- function() { + if (is.empty(input$sm_var)) { + return(invisible()) + } + if (length(input$sm_plots) == 0) { + figs <- FALSE + outputs <- c("summary") + inp_out <- list("", "") + } else { + outputs <- c("summary", "plot") + inp_out <- list("", list(plots = input$sm_plots, custom = FALSE)) + figs <- TRUE + } + update_report( + inp_main = clean_args(sm_inputs(), sm_args), + fun_name = "single_mean", inp_out = inp_out, + outputs = outputs, figs = figs, + fig.width = sm_plot_width(), + fig.height = sm_plot_height() + ) +} + +download_handler( + id = "dlp_single_mean", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_single_mean"), + type = "png", + caption = i18n$t("Save single mean plot"), + plot = .plot_single_mean, + width = sm_plot_width, + height = sm_plot_height +) + +observeEvent(input$single_mean_report, { + r_info[["latest_screenshot"]] <- NULL + single_mean_report() +}) + +observeEvent(input$single_mean_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_single_mean_screenshot") +}) + +observeEvent(input$modal_single_mean_screenshot, { + single_mean_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/analysis/single_prop_ui.R b/radiant.basics/inst/app/tools/analysis/single_prop_ui.R new file mode 100644 index 0000000..df99c83 --- /dev/null +++ b/radiant.basics/inst/app/tools/analysis/single_prop_ui.R @@ -0,0 +1,228 @@ +############################### +# Single proportion - ui +############################### + +## alternative hypothesis options +sp_alt <- list("two.sided", "less", "greater") +names(sp_alt) <- c( + i18n$t("Two sided"), + i18n$t("Less than"), + i18n$t("Greater than") +) +sp_plots <- c("bar", "simulate") +names(sp_plots) <- c(i18n$t("Bar"), i18n$t("Simulate")) + +## list of function arguments +sp_args <- as.list(formals(single_prop)) + +## list of function inputs selected by user +sp_inputs <- reactive({ + ## loop needed because reactive values don't allow single bracket indexing + sp_args$data_filter <- if (input$show_filter) input$data_filter else "" + sp_args$dataset <- input$dataset + for (i in r_drop(names(sp_args))) { + sp_args[[i]] <- input[[paste0("sp_", i)]] + } + sp_args +}) + +output$ui_sp_var <- renderUI({ + vars <- c("None" = "", groupable_vars()) + selectInput( + inputId = "sp_var", label = i18n$t("Variable (select one):"), + choices = vars, + selected = state_single("sp_var", vars), + multiple = FALSE + ) +}) + +output$up_sp_lev <- renderUI({ + req(available(input$sp_var)) + levs <- .get_data()[[input$sp_var]] %>% + as.factor() %>% + levels() + + selectInput( + "sp_lev", i18n$t("Choose level:"), + choices = levs, + selected = state_single("sp_lev", levs), + multiple = FALSE + ) +}) + +output$ui_single_prop <- renderUI({ + req(input$dataset) + tagList( + wellPanel( + conditionalPanel( + condition = "input.tabs_single_prop == 'Summary'", + uiOutput("ui_sp_var"), + uiOutput("up_sp_lev"), + selectInput( + "sp_alternative", i18n$t("Alternative hypothesis:"), + choices = sp_alt, + selected = state_single("sp_alternative", sp_alt, sp_args$alternative), + multiple = FALSE + ), + sliderInput( + "sp_conf_lev", i18n$t("Confidence level:"), + min = 0.85, max = 0.99, step = 0.01, + value = state_init("sp_conf_lev", sp_args$conf_lev) + ), + numericInput( + "sp_comp_value", i18n$t("Comparison value:"), + value = state_init("sp_comp_value", sp_args$comp_value), + min = 0.01, max = 0.99, step = 0.01 + ), + # radioButtons("sp_type", label = "Test:", c("Binomial" = "binom", "Chi-square" = "chisq"), + radioButtons( + inputId = "sp_test", + label = i18n$t("Test type:"), + choices = { + opts <- c("binom", "z") + names(opts) <- c(i18n$t("Binomial exact"), i18n$t("Z-test")) + opts + }, + selected = state_init("sp_test", "binom"), inline = TRUE + ) + ), + conditionalPanel( + condition = "input.tabs_single_prop == 'Plot'", + selectizeInput( + "sp_plots", i18n$t("Select plots:"), + choices = sp_plots, + selected = state_multiple("sp_plots", sp_plots, "bar"), + multiple = TRUE, + options = list(placeholder = i18n$t("Select plots"), plugins = list("remove_button", "drag_drop")) + ) + ) + ), + help_and_report( + modal_title = i18n$t("Single proportion"), + fun_name = "single_prop", + help_file = inclMD(file.path(getOption("radiant.path.basics"), "app/tools/help/single_prop.md")) + ) + ) +}) + +sp_plot <- reactive({ + list(plot_width = 650, plot_height = 400 * max(length(input$sp_plots), 1)) +}) + +sp_plot_width <- function() { + sp_plot() %>% + (function(x) if (is.list(x)) x$plot_width else 650) +} + +sp_plot_height <- function() { + sp_plot() %>% + (function(x) if (is.list(x)) x$plot_height else 400) +} + +## output is called from the main radiant ui.R +output$single_prop <- renderUI({ + register_print_output("summary_single_prop", ".summary_single_prop") + register_plot_output( + "plot_single_prop", ".plot_single_prop", + height_fun = "sp_plot_height" + ) + + ## two separate tabs + sp_output_panels <- tabsetPanel( + id = "tabs_single_prop", + tabPanel(i18n$t("Summary"), value = "Summary", verbatimTextOutput("summary_single_prop")), + tabPanel( + i18n$t("Plot"), value = "Plot", + download_link("dlp_single_prop"), + plotOutput("plot_single_prop", height = "100%") + ) + ) + + stat_tab_panel( + menu = i18n$t("Basics > Proportions"), + tool = i18n$t("Single proportion"), + tool_ui = "ui_single_prop", + output_panels = sp_output_panels + ) +}) + +sp_available <- reactive({ + if (not_available(input$sp_var)) { + i18n$t("This analysis requires a categorical variable. In none are available\nplease select another dataset.\n\n") %>% suggest_data("consider") + } else if (input$sp_comp_value %>% (function(x) is.na(x) | x > 1 | x <= 0)) { + i18n$t("Please choose a comparison value between 0 and 1") + } else { + "available" + } +}) + +.single_prop <- reactive({ + spi <- sp_inputs() + spi$envir <- r_data + do.call(single_prop, spi) +}) + +.summary_single_prop <- reactive({ + if (sp_available() != "available") { + return(sp_available()) + } + summary(.single_prop()) +}) + +.plot_single_prop <- reactive({ + if (sp_available() != "available") { + return(sp_available()) + } + validate(need(input$sp_plots, i18n$t("Nothing to plot. Please select a plot type"))) + withProgress(message = i18n$t("Generating plots"), value = 1, { + plot(.single_prop(), plots = input$sp_plots, shiny = TRUE) + }) +}) + +single_prop_report <- function() { + if (is.empty(input$sp_var)) { + return(invisible()) + } + if (length(input$sp_plots) == 0) { + figs <- FALSE + outputs <- c("summary") + inp_out <- list("", "") + } else { + outputs <- c("summary", "plot") + inp_out <- list("", list(plots = input$sp_plots, custom = FALSE)) + figs <- TRUE + } + update_report( + inp_main = clean_args(sp_inputs(), sp_args), + fun_name = "single_prop", inp_out = inp_out, + outputs = outputs, figs = figs, + fig.width = sp_plot_width(), + fig.height = sp_plot_height() + ) +} + +download_handler( + id = "dlp_single_prop", + fun = download_handler_plot, + fn = function() paste0(input$dataset, "_single_prop"), + type = "png", + caption = i18n$t("Save single proportion plot"), + plot = .plot_single_prop, + width = sp_plot_width, + height = sp_plot_height +) + +observeEvent(input$single_prop_report, { + r_info[["latest_screenshot"]] <- NULL + single_prop_report() +}) + +observeEvent(input$single_prop_screenshot, { + r_info[["latest_screenshot"]] <- NULL + radiant_screenshot_modal("modal_single_prop_screenshot") +}) + +observeEvent(input$modal_single_prop_screenshot, { + single_prop_report() + removeModal() ## remove shiny modal after save +}) diff --git a/radiant.basics/inst/app/tools/help/clt.md b/radiant.basics/inst/app/tools/help/clt.md new file mode 100644 index 0000000..a446ed5 --- /dev/null +++ b/radiant.basics/inst/app/tools/help/clt.md @@ -0,0 +1,19 @@ +> 用随机抽样说明中心极限定理 + +### 什么是中心极限定理? + +“在概率论中,中心极限定理(CLT)指出,在特定条件下,大量独立随机变量(每个变量都有明确的期望值和方差)的算术平均值将近似服从正态分布,而与变量的潜在分布无关。也就是说,假设获取一个包含大量观测值的样本,每个观测值都是随机生成的,且不依赖于其他观测值的值,然后计算观测值的算术平均值。如果多次执行此过程,中心极限定理表明,计算得到的平均值将服从正态分布(通常称为‘钟形曲线’)。” + +来源:维基百科 + +## 抽样 + +要生成样本,请从 “分布(Distribution)” 下拉菜单中选择一种分布,并接受(或更改)默认值。然后点击 “抽样(Sample)” 按钮或按`CTRL-enter`(Mac 上为`CMD-enter`)运行模拟并显示模拟数据的图表。 + +### Khan 讲解中心极限定理 + + + +### R 函数 + +有关 Radiant 中用于概率计算的相关 R 函数概述,请参见*基础 > 概率* 。 \ No newline at end of file diff --git a/radiant.basics/inst/app/tools/help/compare_means.md b/radiant.basics/inst/app/tools/help/compare_means.md new file mode 100644 index 0000000..be4a420 --- /dev/null +++ b/radiant.basics/inst/app/tools/help/compare_means.md @@ -0,0 +1,120 @@ +> 比较数据中两个或多个变量或组的均值 + +均值比较 t 检验用于比较一个组中某个变量的均值与一个或多个其他组中同一变量的均值。总体中组间差异的原假设设为零。我们使用样本数据检验这一假设。 + +我们可以执行单侧检验(即`小于`或`大于`)或双侧检验(见 “备择假设(Alternative hypothesis)” 下拉菜单)。单侧检验用于评估现有数据是否提供证据表明组间样本均值差异小于(或大于)零。 + +### 示例:教授薪资 + +我们获取了美国某学院助理教授、副教授和教授的 9 个月学术薪资数据(2008-09 学年)。这些数据是学院行政部门为监测男女教师薪资差异而持续收集的一部分。数据包含 397 个观测值和以下 6 个变量: + +- `rank` = 因子,水平为 AsstProf(助理教授)、AssocProf(副教授)、Prof(教授) +- `discipline` = 因子,水平为 A(“理论型” 院系)或 B(“应用型” 院系) +- `yrs.since.phd` = 获得博士学位后的年数 +- `yrs.service` = 任职年数 +- `sex` = 因子,水平为 Female(女性)和 Male(男性) +- `salary` = 9 个月薪资(美元) + +这些数据来自 CAR 包,与以下书籍相关:Fox J. 和 Weisberg, S. (2011)《应用回归的 R 伴侣(第二版)》,Sage 出版社。 + +假设我们要检验职级较低的教授是否比职级较高的教授薪资更低。为检验这一假设,我们首先选择教授`rank`,并选择`salary`作为要在不同职级间比较的数值变量。在 “选择组合(Choose combinations)” 框中选择所有可用条目,对三个职级进行两两比较。注意,移除所有条目会自动选择所有组合。我们关注单侧假设(即`小于`)。 + ++ +输出的前两个区块显示检验的基本信息(如所选变量和置信水平)和汇总统计量(如每组的均值、标准差、误差边际等)。最后一个区块显示以下内容: + +* `Null hyp.`是原假设,`Alt. hyp.`是备择假设 +* `diff`是两组样本均值的差异(例如,80775.99 - 93876.44 = -13100.45)。如果原假设为真,我们预期这一差异较小(即接近零) +* `p.value`是在原假设为真时,找到与`diff`一样极端或更极端值的概率 + +如果勾选 “显示额外统计量(Show additional statistics)”,会添加以下输出: + +
+Pairwise mean comparisons (t-test) +Data : salary +Variables : rank, salary +Samples : independent +Confidence: 0.95 +Adjustment: None + + rank mean n n_missing sd se me + AsstProf 80,775.985 67 0 8,174.113 998.627 1,993.823 + AssocProf 93,876.438 64 0 13,831.700 1,728.962 3,455.056 + Prof 126,772.109 266 0 27,718.675 1,699.541 3,346.322 + + Null hyp. Alt. hyp. diff p.value se t.value df 0% 95% + AsstProf = AssocProf AsstProf < AssocProf -13100.45 < .001 1996.639 -6.561 101.286 -Inf -9785.958 *** + AsstProf = Prof AsstProf < Prof -45996.12 < .001 1971.217 -23.334 324.340 -Inf -42744.474 *** + AssocProf = Prof AssocProf < Prof -32895.67 < .001 2424.407 -13.569 199.325 -Inf -28889.256 *** + +Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ++ +* `se`是标准误(即`diff`抽样分布的标准差) +* `t.value`是与`diff`相关的 t 统计量,可与 t 分布比较(即`diff` / `se`) +* `df`是统计检验的自由度。注意,自由度使用 Welch 近似法计算 +* `0% 95%`显示样本均值差异的 95% 置信区间。这些数值提供了真实总体差异可能落入的范围 + +### 检验方法 + +我们可以使用三种方法评估原假设。我们选择显著性水平为 0.05。1 当然,每种方法会得出相同结论。 + +#### p 值 + +由于每个 p 值都**小于**显著性水平,我们拒绝每个评估的教授职级对的原假设。数据表明,副教授薪资高于助理教授,教授薪资高于助理教授和副教授。注意,“***” 用作显著性指标。 + +#### 置信区间 + +由于任何置信区间都**不**包含零,我们拒绝每个评估的职级组合的原假设。由于我们的备择假设是`小于`,置信区间实际上是总体薪资差异的 95% 置信上限(即 - 9785.958、-42744.474 和 - 28889.256)。 + +#### t 值 + +由于计算的 t 值(-6.561、-23.334 和 - 13.569)**小于**相应的临界 t 值,我们拒绝每个评估的职级组合的原假设。可通过 “基础(Basics)” 菜单中的概率计算器获取临界 t 值。以助理教授与副教授的检验为例,我们发现对于自由度为 101.286 的 t 分布(见`df`),临界 t 值为 1.66。由于备择假设是`小于`,我们选择 0.05 作为下侧概率界。 + ++ +除 “摘要(Summary)” 标签页中的数值输出外,我们还可以可视化研究`rank`与`salary`之间的关联(见 “绘图(Plot)” 标签页)。下方截图显示教授薪资的散点图和带有置信区间(黑色)与标准误(蓝色)条的条形图。与 “摘要” 标签页中的结果一致,不同职级的薪资存在明显差异。我们也可以选择将样本数据绘制成箱线图或密度曲线图。 + +
+ +### 多重比较调整 + +我们评估的比较越多,即使原假设为真,仅因随机因素而发现 “显著” 结果的可能性就越大。如果我们进行 100 次检验,并将**显著性水平**设为 0.05(或 5%),即使总体中不存在关联,我们也可能预期有 5 个 p 值小于或等于 0.05。 + +Bonferroni 调整确保 p 值根据所进行的检验数量适当缩放。这幅 XKCD 漫画清晰地说明了这类调整的必要性。 + +### 统计术语 + +这是**均值比较**检验,原假设为真实总体**均值差异**等于**0**。使用 0.05 的显著性水平,我们拒绝每个评估的职级对的原假设,并得出结论:真实总体**均值差异小于**0。 + +助理教授与副教授薪资差异检验的 p 值为 **< .001**。这是在原假设为真时,观察到与数据中样本**均值差异**一样极端或更极端的样本**均值差异**的概率。在本例中,它是当真实总体**均值差异**为**0**时,观察到样本**均值差异**小于(或等于)**-13100.45**的概率。 + +95% 置信界为 **-9785.958**。如果重复抽样并为每个样本计算 95% 置信界,真实总体均值将在 95% 的样本中低于该下界。 + +1**显著性水平**(通常用α表示)是你愿意接受的、在原假设实际为真时拒绝原假设的最高概率。常用的显著性水平为 0.05(或 5%)。 + +### 报告 > Rmd + +通过点击屏幕左下角的图标或按键盘上的`ALT-enter`,向*报告 > Rmd*添加代码以(重新)创建分析。 + +如果已创建图表,可使用`ggplot2`命令进行自定义(例如,`plot(result, plots = "scatter", custom = TRUE) + labs(title = "均值比较")`)。详情请参见*数据 > 可视化*。 + +### R 函数 + +有关 Radiant 中用于评估均值的相关 R 函数概述,请参见*基础 > 均值*。 + +`compare_means`工具中使用的来自`stats`包的核心函数是`t.test`。 + +### 视频教程 + +将以下完整命令复制粘贴到 RStudio 控制台(即左下角窗口),按回车即可获取 Radiant 教程系列中假设检验模块使用的所有材料: + +
usethis::use_course("https://www.dropbox.com/sh/0xvhyolgcvox685/AADSppNSIocrJS-BqZXhD1Kna?dl=1")+ +均值比较假设检验 + +- 本视频展示如何进行均值比较假设检验 +- 主题列表: + - 按组计算汇总统计量 + - 在 Radiant 中设置均值比较的假设检验 + - 使用 p 值和置信区间评估假设检验 diff --git a/radiant.basics/inst/app/tools/help/compare_props.md b/radiant.basics/inst/app/tools/help/compare_props.md new file mode 100644 index 0000000..d309afe --- /dev/null +++ b/radiant.basics/inst/app/tools/help/compare_props.md @@ -0,0 +1,119 @@ +> 比较数据中两个或多个组的比例 + +比例比较检验用于评估某些事件、行为、意图等的发生频率在不同组间是否存在差异。总体中组间比例差异的原假设设为零。我们使用样本数据检验这一假设。 + +我们可以执行单侧检验(即`小于`或`大于`)或双侧检验(见 “备择假设(Alternative hypothesis)” 下拉菜单)。单侧检验适用于评估样本数据是否表明,例如,某一无线运营商的掉话比例比其他运营商更高(或更低)。 + +### 示例 + +我们将使用泰坦尼克号乘客生存状态数据集的一个样本。泰坦尼克号乘客数据的主要来源是《泰坦尼克号百科全书》。原始来源之一是 Eaton & Haas(1994)的《泰坦尼克号:胜利与悲剧》(Patrick Stephens Ltd 出版),其中包含由多位研究者整理、经 Michael A. Findlay 编辑的乘客名单。我们关注数据中的两个变量: + +- `survived` = 因子,水平为`Yes`(是)和`No`(否) +- `pclass` = 乘客等级(1 等、2 等、3 等),作为社会经济地位(SES)的替代指标:1 等≈上层;2 等≈中层;3 等≈下层 + +假设我们要检验泰坦尼克号沉没事件中,不同乘客等级的生存比例是否存在差异。为检验这一假设,我们选择`pclass`作为分组变量,并计算`survived`中`yes`的比例(见 “选择水平(Choose level)”)(见 “变量(选择一个)(Variable (select one))”)。 + +在 “选择组合(Choose combinations)” 框中选择所有可用条目,对三个乘客等级进行两两比较。注意,移除所有条目会自动选择所有组合。除非我们对效应方向有明确假设,否则应使用双侧检验(即`two.sided`)。我们的第一个备择假设是 “1 等舱乘客的生存比例与 2 等舱乘客不同”。 + ++ +输出的前两个区块显示检验的基本信息(如所选变量和置信水平)和汇总统计量(如每组的比例、标准误、误差边际等)。最后一个区块显示以下内容: + +* `Null hyp.`是原假设,`Alt. hyp.`是备择假设 +* `diff`是两组样本比例的差异(例如,0.635 - 0.441 = 0.194)。如果原假设为真,我们预期这一差异较小(即接近零) +* `p.value`是在原假设为真时,找到与`diff`一样极端或更极端值的概率 + +如果勾选 “显示额外统计量(Show additional statistics)”,会添加以下输出: + +
+Pairwise proportion comparisons +Data : titanic +Variables : pclass, survived +Level : Yes in survived +Confidence: 0.95 +Adjustment: None + + pclass Yes No p n n_missing sd se me + 1st 179 103 0.635 282 0 8.086 0.029 0.056 + 2nd 115 146 0.441 261 0 8.021 0.031 0.060 + 3rd 131 369 0.262 500 0 9.832 0.020 0.039 + + Null hyp. Alt. hyp. diff p.value chisq.value df 2.5% 97.5% + 1st = 2nd 1st not equal to 2nd 0.194 < .001 20.576 1 0.112 0.277 *** + 1st = 3rd 1st not equal to 3rd 0.373 < .001 104.704 1 0.305 0.441 *** + 2nd = 3rd 2nd not equal to 3rd 0.179 < .001 25.008 1 0.107 0.250 *** + +Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ++ +* `chisq.value`是与`diff`相关的卡方统计量,可与卡方分布比较。关于该指标的计算方法,详见 “基础> 表格 > 交叉表” 的帮助文件。每组组合都会计算等效的 2×2 交叉表。 +* `df`是每个统计检验的自由度(1)。 +* `2.5% 97.5%`显示样本比例差异的 95% 置信区间。这些数值提供了真实总体差异可能落入的范围。 + +### 检验方法 + +我们可以使用三种方法评估原假设。我们选择显著性水平为 0.05。1 当然,每种方法会得出相同结论。 + +#### p 值 + +由于每个两两比较的 p 值都**小于**显著性水平,基于可用样本数据,我们可以拒绝比例相等的原假设。结果表明,1 等舱乘客比 2 等舱和 3 等舱乘客更可能在沉没事件中幸存;同样,2 等舱乘客比 3 等舱乘客更可能幸存。 + +#### 置信区间 + +由于任何置信区间都**不**包含零,我们拒绝每个评估的乘客等级组合的原假设。 + +#### 卡方值 + +由于计算的卡方值(20.576、104.704 和 25.008)**大于**相应的临界卡方值,我们拒绝每个评估的乘客等级组合的原假设。可通过 “基础(Basics)” 菜单中的概率计算器获取临界卡方值。以 1 等舱与 2 等舱乘客的检验为例,我们发现对于自由度为 1(见`df`)、置信水平为 0.95 的卡方分布,临界卡方值为 3.841。 + ++ +除 “摘要(Summary)” 标签页中的数值输出外,我们还可以可视化研究`pclass`与`survived`之间的关联(见 “绘图(Plot)” 标签页)。下方截图显示两个条形图。第一个图表包含样本中`survived`为`yes`的比例的置信区间(黑色)和标准误(蓝色)条。与 “摘要” 标签页中的结果一致,不同乘客等级的生存率存在明显差异。“并列(Dodge)” 图表按乘客等级并排显示`survived`中`yes`和`no`的比例:1 等舱乘客中`yes`的比例高于`no`,而 3 等舱乘客则相反。 + +
+ +### 技术说明 + +- Radiant 使用 R 的`prop.test`函数进行比例比较。当一个或多个期望频数较小时(例如≤5),该检验的 p 值通过模拟方法计算。出现这种情况时,建议使用 “基础> 表格 > 交叉表” 重新运行检验,并评估是否有单元格的期望频数低于 1。 +- 对于单侧检验(即`小于`或`大于`),临界值必须通过概率计算器中的正态分布获取,并对相应的 Z 统计量进行平方。 + +### 多重比较调整 + +我们评估的比较越多,即使原假设为真,仅因随机因素而发现 “显著” 结果的可能性就越大。如果我们进行 100 次检验,并将**显著性水平**设为 0.05(或 5%),即使总体中不存在关联,我们也可能预期有 5 个 p 值小于或等于 0.05。 + +邦费罗尼调整(Bonferroni adjustment)确保 p 值根据所进行的检验数量适当缩放。这幅 XKCD 漫画清晰地说明了这类调整的必要性。 + +### 统计术语 + +这是**比例比较**检验,原假设为真实总体**比例差异**等于**0**。使用 0.05 的显著性水平,我们拒绝每个评估的乘客等级对的原假设,并得出结论:真实总体**比例差异不等于 0**。 + +1 等舱与 2 等舱乘客生存比例差异检验的 p 值为 **< .001**。这是在原假设为真时,观察到与数据中样本**比例差异**一样极端或更极端的样本**比例差异**的概率。在本例中,它是当真实总体**比例差异**为**0**时,观察到样本**比例差异**小于 **-0.194**或大于**0.194** 的概率。 + +95% 置信区间为**0.112**至**0.277**。如果重复抽样并为每个样本计算 95% 置信区间,真实**总体比例差异**将在 95% 的样本中落入置信区间内。 + +1**显著性水平**(通常用α表示)是你愿意接受的、在原假设实际为真时拒绝原假设的最高概率。常用的显著性水平为 0.05(或 5%)。 + +### 报告 > Rmd + +通过点击屏幕左下角的图标或按键盘上的`ALT-enter`,向*报告 > Rmd*添加代码以(重新)创建分析。 + +如果已创建图表,可使用`ggplot2`命令进行自定义(例如,`plot(result, plots = "bar", custom = TRUE) + labs(title = "比例比较")`)。详情请参见*数据 > 可视化*。 + +### R 函数 + +有关 Radiant 中用于评估比例的相关 R 函数概述,请参见*基础 > 比例*。 + +`compare_props`工具中使用的来自`stats`包的核心函数是`prop.test`。 + +### 视频教程 + +将以下完整命令复制粘贴到 RStudio 控制台(即左下角窗口),按回车即可获取 Radiant 教程系列中假设检验模块使用的所有材料: + +
usethis::use_course("https://www.dropbox.com/sh/0xvhyolgcvox685/AADSppNSIocrJS-BqZXhD1Kna?dl=1")+ +比例比较假设检验 + +- 本视频展示如何进行比例比较假设检验 +- 主题列表: + - 在 Radiant 中设置比例比较的假设检验 + - 使用 p 值和置信区间评估假设检验 diff --git a/radiant.basics/inst/app/tools/help/correlation.md b/radiant.basics/inst/app/tools/help/correlation.md new file mode 100644 index 0000000..9520c3e --- /dev/null +++ b/radiant.basics/inst/app/tools/help/correlation.md @@ -0,0 +1,54 @@ +> 数据中变量的相关性如何? + +创建所选变量的相关矩阵。为每个变量对提供相关性和 p 值。要仅显示高于特定(绝对)水平的相关性,使用相关性截断框。 + +注意:相关性可基于`numeric`、`integer`、`date`和`factor`类型的变量计算。当纳入因子型变量时,应勾选 “调整因子型变量(Adjust for {factor} variables)” 框。进行调整后估计相关性时,因子型变量将被视为(有序)分类变量,其他所有变量将被视为连续变量。 + ++ +“绘图(Plot)” 标签页提供相关矩阵的可视化表示。注意,图表中的散点图默认最多显示 1000 个数据点。要生成使用所有观测值的散点图,在 “报告 > Rmd” 中使用`plot(result, n = -1)`。 + +“绘图” 标签页中显示的星号含义如下: + +- p 值在 0 到 0.001 之间:*** +- p 值在 0.001 到 0.01 之间:** +- p 值在 0.01 到 0.05 之间:* +- p 值在 0.05 到 0.1 之间:. + +
+ +图中使用的字体大小与两个变量间相关性的大小和显著性成正比。 + +### 方法) + +选择用于计算相关性的方法。最常用的方法是`Pearson`(皮尔逊)。详见维基百科。 + +### 相关性截断 + +要仅显示高于特定值的相关性,在 0 到 1 之间的数值输入框中选择非零值(例如 0.15)。 + +### 协方差矩阵 + +尽管我们通常使用相关矩阵,但也可通过勾选 “显示协方差矩阵(Show covariance matrix)” 框显示协方差矩阵。 + +## 存储为数据框 + +可通过(1)为新数据集提供名称和(2)点击 “存储(Store)” 按钮,将相关矩阵存储为数据框。新数据集将包含每个变量对的估计`correlation`(相关性)和`distance`(距离)度量,距离度量计算如下:`distance = 0.5 * (1 - correlation)`。当两个变量的相关性等于 - 1 时,该度量为 1;当两个变量的相关性等于 1 时,该度量为 0。关于此类数据集的示例,见下方 “数据> 查看” 标签页的截图。此结构的数据集可作为输入,通过 “多元分析 >(不)相似性分析” 创建基于(不)相似性的感知图。 + +
+ +### Khan 讲解相关性 + + + +### 报告 > Rmd + +通过点击屏幕左下角的图标或按键盘上的`ALT-enter`,向*报告 > Rmd*添加代码以(重新)创建分析。 + +默认情况下,相关性图抽样 1000 个数据点。要包含所有数据点,使用`plot(result, n = -1)`。例如,要为图表添加标题,使用`title(main = "相关性图\n\n")`。更多信息见R 图形文档。 + +### R 函数 + +有关 Radiant 中用于评估相关性的相关 R 函数概述,请参见*基础 > 表格*。 + +`correlation`工具中使用的来自`psych`包的核心函数是`corr.test`。 diff --git a/radiant.basics/inst/app/tools/help/cross_tabs.md b/radiant.basics/inst/app/tools/help/cross_tabs.md new file mode 100644 index 0000000..9019c53 --- /dev/null +++ b/radiant.basics/inst/app/tools/help/cross_tabs.md @@ -0,0 +1,66 @@ +> 交叉表分析用于评估分类变量之间是否存在关联。该工具也被称为卡方检验或列联表分析 + +### 示例 + +数据来自 580 名报纸读者的样本,这些读者表明了(1)他们最常阅读的报纸(《今日美国》或《华尔街日报》)和(2)他们的收入水平(低收入 vs. 高收入)。数据包含三个变量:受访者标识符(id)、受访者收入(高或低)以及受访者主要阅读的报纸(《今日美国》或《华尔街日报》)。 + +我们将研究收入水平与报纸选择之间是否存在关系。具体而言,我们检验以下原假设和备择假设: + +* H0:收入水平与报纸选择之间无关联 +* Ha:收入水平与报纸选择之间有关联 + +如果拒绝原假设,我们可以进一步研究哪些单元格对假设的关联有贡献。在 Radiant(基础 > 交叉表)中,选择收入作为第一个因子,报纸作为第二个因子。首先,比较观察频数和期望频数。期望频数基于原假设(即无关联)计算,公式为(行总计 × 列总计)/ 总总计。 + +
+ +(皮尔逊)卡方检验用于评估我们是否可以拒绝两个变量独立的原假设。它通过比较观察频数(即数据中实际看到的频数)与期望频数(即如果两个变量独立时预期看到的频数)来实现。如果期望频数表与观察频数表之间存在较大差异,卡方值将**较大**。每个单元格的卡方值计算公式为`(o - e)^2 / e`,其中`o`是单元格中的观察频数,`e`是原假设成立时该单元格的期望频数。点击 “卡方(Chi-squared)” 复选框可显示这些值。总卡方值通过对所有单元格求和获得,即它是 “卡方贡献(Contribution to chi-square)” 表中所示值的总和。 + +为了确定卡方值是否可被视为**较大**,我们首先计算自由度(df)。具体而言:自由度 =(行数 - 1)×(列数 - 1)。在 2×2 表格中,自由度 =(2-1)×(2-1)=1。“摘要(Summary)” 标签页的输出显示了卡方统计量的值、相关的自由度以及检验的 p 值。我们还能看到每个单元格对总卡方统计量的贡献。 + +记住要检查期望値:所有期望频数均大于 5,因此卡方统计量的 p 值不太可能存在偏差。与通常一样,当 p 值小于 0.05 时,我们拒绝原假设。由于我们的 p 值非常小(<0.001),我们可以拒绝原假设(即数据表明报纸阅读习惯与收入之间存在关联)。 + +我们可以使用与 187.783 的卡方值相关的 p 值来评估原假设。不过,我们也可以使用概率计算器计算临界卡方值。从下方输出中可以看到,如果选择 95% 的置信水平,该值为 3.841。由于计算得到的卡方值大于临界值(187.783 > 3.841),我们拒绝 “收入(Income)” 与 “报纸(Newspaper)” 独立的原假设。 + +
+ +我们也可以使用概率计算器确定与计算得到的卡方值相关的 p 值。与 “交叉表> 摘要” 标签页的输出一致,该`p.value`为`< .001`。 + +
+ +除 “摘要” 标签页中的数值输出外,我们还可以可视化评估假设(见 “绘图(Plot)” 标签页)。我们选择与之前相同的变量,但将绘制标准化偏差。该度量的计算公式为(o-e)/sqrt (e),即衡量表格中某个单元格的观察频数与期望频数差异的得分。当单元格的标准化偏差绝对值大于 1.96 时,该单元格与独立性模型(或无关联)存在显著偏差。 + +
+ +在图中,我们看到所有单元格都对收入与阅读习惯之间的关联有贡献,因为所有标准化偏差的绝对值都大于 1.96(即条形图延伸超出了图中的外部虚线)。 + +换句话说,与无关联的原假设成立时的预期相比,阅读《华尔街日报》的低收入受访者似乎更少,阅读《华尔街日报》的高收入受访者似乎更多;此外,阅读《今日美国》的低收入受访者更多,阅读《今日美国》的高收入受访者更少。 + +### 报告 > Rmd + +通过点击屏幕左下角的图标或按键盘上的`ALT-enter`,向*报告 > Rmd*添加代码以(重新)创建分析。 + +如果已创建图表,可使用`ggplot2`命令进行自定义(例如,`plot(result, check = "observed", custom = TRUE) + labs(y = "百分比")`)。详情请参见*数据 > 可视化*。 + +### 技术说明 + +当一个或多个期望値较小时(例如≤5),卡方检验的 p 值通过模拟方法计算。如果某些单元格的期望计数低于 1,可能需要**合并**行和 / 或列。 + +### R 函数 + +有关 Radiant 中用于评估分类变量间关联的相关 R 函数概述,请参见*基础 > 表格*。 + +`cross_tabs`工具中使用的来自`stats`包的核心函数是`chisq.test`。 + +### 视频教程 + +将以下完整命令复制粘贴到 RStudio 控制台(即左下角窗口),按回车即可获取 Radiant 教程系列中假设检验模块使用的所有材料: + +
usethis::use_course("https://www.dropbox.com/sh/0xvhyolgcvox685/AADSppNSIocrJS-BqZXhD1Kna?dl=1")+ +交叉表假设检验 + +- 本视频演示如何通过交叉表假设检验研究两个分类变量之间的关联 +- 主题列表: + - 在 Radiant 中设置交叉表的假设检验 + - 解释观察频数表、期望频数表和卡方贡献表的构建方式 + - 使用 p 值和临界值评估假设检验 diff --git a/radiant.basics/inst/app/tools/help/figures/compare_means_plot.png b/radiant.basics/inst/app/tools/help/figures/compare_means_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..78038008b60cd31b4eddaf6a4af9ab3d434b6563 GIT binary patch literal 121299 zcmbrlb6BQZ`#+p*+qTUKGu32Iw(ZF^S(7KbCfl}c+pfua@AiH+zx(&s+i@Ko>%Nvx zug^NK5ILET@Gv+qKtMq75}!mBfPlbKfq*~*p&$Tn3VTA!fq-Dz%|t}xBt%4rrjLh`{jygg6dKL9Q~3h0D)CSQIQC z7#~>PRw* 0X$ls3#0mJg=2xvk@wY&4(iQs o2T zjimmBviT+m;zL=f_JIM2j18q=x1#YAL5x6`iU&eelr379^@$P& 7e zoLujr?iY CCYF6_Rls(#-H}mv%=sMikOH%8gWQ0R8rE> z$&9=dOc(I3xmd4FxRg uRUApib)C7bb=p>?UQw @DNXF1SGM!_X#Y|}@;k|R$gD}M8uB48D}uB}wR z9!Dt<2OpV7qAU%HJ=vy>xX#JD2G?galMxob_jtj6oob+>RQOtswW#Zm+6M!dF^ ;PG0O7DTWJe(qwEeyT9Y2taAh^$!mq7j<#3BPP z5Z7{d0+fTX!SphQE)ennFnK0KjSm46$X*8o7ZBzKu!k@aln74< 2D8vjsL1Ys+rl?9_=pwsv=t__;aA|=ILi3s02Lx zj$%Y{ 4N4$Fdu~5 z>De>9QG3bZD(C@{96K*h|=%(S5g>u}KMi1qSNJN*vxnTtZ?=iAPd{Y>grg6)Or( zl6D{&PLhwzi8LGx-!t43)KkyG!qUVd!xCob%Hqai#L{n6HUu~5K|&q#`O{i1IT^GW zZeiezOw-4&QWUZ9g9JNZRX c_;) z)USCo)3e!@pAQOQMY~z?`O4W&G8UgxvQP6zrx_*%%3o6RKVRn>O$?bPnr`#p@Bs6m zq|u~Vr%7lvS=XG2uZpjJ{3^;Hs2!>uq8+r>&{pRa{EGAP_2mfy3^Y) |?p$5ELlHeIT`K#+|Dw-MI}R~dJeqYsBQd^+L|_wGjr#Vti41v&*MfI^Up z>4#p^+N|M8vDoV+OO_p}hG%B!2HG|PhQhT_bql7G`j nxr21x5wCoV4!6aK$1j zh$ZNyprxE&>ebsTnJe{{l@?a&$d?}%%j&IaJ*`a46D?gj&dT_E1$w-yJ%e6UAwNQ9 ziTHQ_$Z0dk-WKQw4n!eMt&F#uh2GxTtywz^pQbc{G^xCX-WSvw(VEhVYzSw_W&Et| zYPwlbTt+)wJLEJGv6pago3otR9LYP}y7Uqfln&XyUD3}fYcS3>o|9hQQquBGTUwj2 zC9QS-YyDHE$FnDlXOkzoXO8EvC*jlKqutBVOW(`si|SK7q%Mdbh%*En6MK@SF}?%e z1hcKxMjnxZoS~fCM67PCuAZ$16dnpz7444RPhl#-8gjUA%-zlb{4fPUCP8miP|Z{x z^1ayw$aBaRBs`?l(Zi%sqHR%Ep-TieL`zg(P*#yiF{{b)8TLd B}-LD>Sa1-vc;3di^aTi z2PKOY#-^qFOGQo-O}S3^$2W8P<%EOR O_=+fMZoz=75=JNQZc$G8av*;6SI#>pa)#7%S z{x!zk!d(}d78;y8vy v(Tvhgv C-)l84 z94@-O79G#B&Zm~AwT(5b*>`V>J0yClPM6=db2#xja;`hJEG})&OwTn}+-YXGuLo!d z)R(OoueI4x{*tlrc6?j)B?t9);dc3WZnWa=DY*Z- `EscY@WCu!>aS0F%wyh)#kc~MIQ9tP#@a3%NqWU-=sp;JQ zIMTf=-*K_Y4v ns@1JVb};=FH5_Yhht|(0BcF;E8np 1Il5j%w1Mc?@i<81xKn^o tQ`Pz2rx5qG4cJ@;r~?qHRa#Bs{hrMn~CM`J^xnozn*-I?>qS0j(#WC zZ*Kwg5`f`j{FC+qFrh}MHb6i^KoX+DO0K{st%$C1yGiG70Uh6j0;v>9klB2UMX}A2 zrZR-TWV5R~k4_o%6zi&H^BY@9yJOhWmRK0bM!bHprEL(5(BLew+EBU(zNDi(>!oPM zp+l+X_yU(efe{P=nFXp3LJZou?%B?IKEfM2`1XC|bKAI6Ytr5L_1$$#lG{G-ou{CI zKq43g3K8&sKUgFn20=dwR760?g#P=10EUriLH^_e_McB+Tt~y1Ij_SWoJ@uQe8xEB z*Hx#FA^Q?OVB)O4%m#fO#?^XLxe`CP?6yqBJ1e7zeCz5sJZ=|F4h{~m;xpzSy?>;p zh7ZP4lCrZqftmP8#{xlr|Km}E4iCZTKAb63G_|qmZ?IcElAq&gkbPfgPIAY?^?r0r zj9*Pnjr^#uKcN?hb~&cWczQ2+qNaXuI!#-6U9Mp0VU%Hp*CRE>=N~{p-ONi!wk?0{ z6Dby~iAwTuB42;{{&Jhy`9#eiY%slA@x28SC>Vog1JFy1O`FZs6g32eQJkYNeTtIh z(7*uJY{u;4TY*X-&Er=-0xHZeJP|S8APBL4P>3iW+7UT*CPIcj#ZgL9lBMla?=PqY z?so=Y34@qxO)f5)HLx5$QGypBr-sYV)ig~O8pXbH^?>>NU&+y00q9No7qNAB-bZ6I zjurS>xX=@{5S>AE3t$FO9kiO(kC`gJ5U{{7pFYd%4s00>H#}Zvx@WSezj_1s@J%Q& zL+ob$>kEj{3ko|4zd_D#8>9pqx%=$-W=P_@Nuukeji~B=IT88Fc=g6aU>xd6_J_P+ z!GkqH$?%wlO6TYuaf_o6`DT%I+>K`C4mSOL7C?C`O-QLz0 OXn@d!YvdAa#}OgeUNSNyFcp^zZ}f?F0kx z{-6DQ?L;Q=Gsc6eO~$vLAFf5@@sHniz!#aE$$EwB^A9Fqjub1=r_MfrsQzOkMB31B zUu<~*5 aof4}DpNI2vbIG=0 zzkHb9+5oIf9Nc$Y@C`35Ep0;8Gb3k%jV3?M|4;V(uGd%}K>SevGNLFc38Q4G089Cs z!hZq-G3X12Mi2r2{r5j?fbT0ZR*ht?&Pa*Q*yB8(!@J%IS)y#XYgmrKx{LkO79bIV z1&Z2+MrBnczO!;d5Cm=dsXwgo-?Zx>V>0cOpxsJjL8a6IRH6g`t GRU z(%qjH>}is0CvrNm4YPaHRBf%?XGnbo^?LSazkglA=UR1;(I)>x-2glaHUO!y4c#6z zn13g)ZZ6b4j(3zdS6(UQb|T`Mp9RQ=*z^bIQ|lG&)xb0xye=1;Mi2!4K{Qn`>$lB5 zsN%e7*Zp3=L79<#V**yg_5e4kgQJt9f>}EG8@9rgBDG)#4#XUT)VoOk@xajn)Tui( zCE_ x|;@ SNGl*K%tEImxl9K-bu7hK$W~5wzqo z=_+9R>PqT7m)>(zKN4^{aen!{HohT(!jRzweRkiMxN+g-ljvN#!H?W@VzzDj;*_Me z)z*dR$Yd0Ovvd)}-`<}RB(rV6-i$0;uAazUL3aL?3r@KwjI&rk=;K^%!$Vch|W!*ZShwDYB8bz1D`!Ja$)DO1sCB zp5r59q#& p9D3z>^~h3;-dcFo*`9B9$rK(Ya06YY@jX=aMAykk zT$8rqlm43mAeDnrarGXLRIa}ZKT{0Ym=4pIw{Bm=i(>w5yZ$cq1ffmUjm?h?z3Vk$ zOl OfH_62|uQSYV%wW zAY^3toIeXHZsLlncZ4OTyVB}b8ky~FU_Wo4FUj59!ESPG_l`TkoxefMXB>67`&zOd z%WSKX6xAA!_0?;|`UeQEJpp^w)Pv49^o%mOa*Vm~c(yilr1gyvdP8d5K6ODnj;+1* zBzU*tkG~j#N1nb=TDp(6Ffe>&v;=(}k?5oj$3px|&xn|z)V!ml!;$UKez)A1X+Eb- z#h08k6cUg9-cS4(f>$)JU$>9%m1mppyF0fp78QD7sAn^Ty_T3(jwm9~A^D_2`2-n$ zMLTXMvdF(2WaKp2;*>Fmee!&3F=sv^& du&SuU9%YD6Z z*L?VaYv~jY%=qZn-NVYE!NSdo;3IwP_ed#9NBrSv?>BWJ%j8IR?;7N{;{eRdUk2*| z$ULu6jNAS35e>!<(+gDl`4)El^Op;K1((mOWo7Y~aDS=CccB&3%mBp|Sz^fF{1>UP zE=}9bolft~TP5LmyAz5{DAjfP9o*V7EwB45ju{<6+sm-pQTo!`7 hc<<4_QJ7wPT8ai$`kwwC$;yeS}Nrd`tw(;?4>HA zRd@8zThAZ}atg)B#ds+gMTP)B{)_rLLx}{zpGE>u=wSoc31)u&6TElg^sx92qS%-b zr=xLluTwJ$2-w4&pFV*xvZr;qjO)$w5^g_=C2n7M CUnH-uApIV8ZkWVVSZ=@;t zVc>DYDtj3ZF!gHCjvJ>%U!Uuo@DkQu(KQ+JKXFvLcZ4OeIQ29w{ -A$1dY5N>LMc^p z?h|h?jSNdElR 2zT zuSK8O+WuGNWTEPIvsujDOo40lCg=&uJWqcr_75|-wkW3_wYsw6nVYFjGF&+M!xLQ7 z@Kno6udl-uUJ6E4iC z?vz4GZgW8CLA-%#v8I`V1vU!c z$Hh}S)JBpw5tm+C8b(_^R|tuC7w9w8o@Q#WgA$j?*BdSyG^1o{x3@D3mcUzDt9xyf z9U0W@!JL;i;MJH{?{_WyXipyy=C(ija%~K@e%a66%E$8* (nD{;ol9+CwLy18m}M=E2 #R?oaeF`< zDZR+nPE)jdHSdg!2`$d;iTEln`~_b80g)ohckbnY)D=9#eDxXfj7u!SwOVJyBXe{4 zCz;GuGlmAteIgSt;|lKsix)lirwNY^e`@ulEU!SV^Ect{>e-8x!w-QNr@u14S{Rha zyWY-cHyLx{M25O1#rOtr?h<=f(z0=s3S*y?iFEt+0SQ8~h@v^JS7DuDAEjgdxKk9c zgCF{Eiy2$#{=_jufHFsoP*^Q= M?&>$$6}=HW>d%jz|xH1iG~!P|cQ#dPzHgUD1Pteo5ERhdO<9pgZc z0yIIJVpN}WUqm(iWqvqb$lHFg_l=-j<( ulkhB)2H>Wn1E?J!*d3 b=cMj1up^bA9;BP-2OJiEG+kg<{QkcyqAq9=1M| z9uZ8|6O@Q=^5<>N3A-1`H#(8RWUNGHgOe!Uw=7)sJOVX=bl9(${Y z&!#kgaZ*10+0o|>)58|SC5}rTDX(hMEg2`M92lcx^>Gajm(M0X>z6vZX>L^55WMj4 zexb S&$rF3De; znJ71->mqF{NNvW4CQ;;H y9J-UQG{B6n_q1< zsz*1w)}J?QE@uyo$D}T5o7*Dihu%=zsxz{8zloxlko@i3@`($Cl#EN_;zX0M$Xp4p zP~2+T9BnK>FKLJJ D9S&)+M~6wS7ktIdgk* zS3urgS{`vFH9u~z3!~^YEv|eXiM}GYWL=Xy!_ABUK1zz_@LZOw83i9ZI@RitP5DtR z1#&9cclQggi#tYlb!0s0_@7JrHuaZ_l5F0JCFNvgan!FjY=l~_VhAbyWJu>=mWpDZ zVw4XU95ZK+#g5g9xY^NfC1QyovNp|*YVR-KS|AWJ%teA4X2}aA2xb$nd 5DxIhq*kj7#H!U3as^XroV%#>dxhkMp2DL4DbpAFX=S z?svtW?%BDx{Xh?SE708^*gL02H` 062DFtU;fGVj7f%ia=rA$iqe z0#%2iOL<0ETn6$=)!UTRs&ASy;O_V^6?Q3)>*Dwf#i3AFF$9e(&8rNHJgb4-l1OaH z!i?m~)Uc$hJ2x3bUqN2LS3{o5tv;qzCTC!#E0)?b>?CjU&q3%Y&|s{ogi9A%N2l}z z$Y^$xv@e@Em=mKp%8$^9&rZ{Z>#V}|+!)yDl6M%}v7^OHRpS;4-gNKv^quZ5yqqsr zdR3444(Pbk$QNm}m!!AY?k03l_{B pN&!7{B#EPMR|o4ac(%fV51Cc( zMeXDT+122 DjcWGd2hLS`;TT8SF_8HT-uG9jfwCZ%< zsAw>0iZCA)^eU!IivlrRNr|%GUi+1)Y-<8%D=^aZAPEBfQP+~?M3M2_UwK^3P8~gq zS{!(RBL4%Y#nkNG*E1VP0SwrmbN+-Igk)-5EctrE$+dx#je=fNxj&0%BV=&!*#9~n zh0DAve(ey9fHfM-HOyj*{-6T6%5#28oOdh~)te!~17#m&Wy*-sof0yJpb pd?N8$$54%Yy9z=3Nf69lkRDPe7pXG1H zS>96)QBK6QAQ<}~lGcM!#Mr;2! OBV<^qWG+?h0lcK@J4+j%{7EtFD*6qRB!7f6 z4VXT_u)dbOr{v OQ=!HJi9HtMk!#61wo&?S-RjiHhx#!jiN zLW|B14x3W Rx2RO|Cro_#g5R0rqLhPZ;B}PxGFwbJXx8Lxmg%7W zb?B}#Shh^acOT)fb%bd)3{jGO=j^ z=@UL)uMT``us(DppFO?KWDr0G0 Q40bn6zifBD7tMvq^`k7V5clH~IYQzkVB>UyOQKd+aREg* zmhhfJKlu8oE)!z<8)}gNxaRJM4@y5PWHpyRQ`4%iR)0u*Y;oCA`)Ryt5x21&;X764 zPEIh%i5bcs0YAzcT6sm?&Xkv^k%BwnsHq8`=!M9RXeks_fPHhWWH612?lmc@xq!1w zkMY@;hjp)THmW`Ss2)e2sCWr`a6Wizh2#w#xmf$b8Uo=i>(v@G&t=Hm>B;=^YL7(; zVhbO<31~plH&UR-{s@Vvj+=WxBuVt9D)$#jLoa{t55i%yw5JJt`QV~ev5MmtBCA>$ z!g1=fQbWT5wRy kM%8&6A;z!XLe}cN=3VCE5+^H z>Jy5;bQn+tN9c=;nhIBGD$Wr)rB@jk9n8(i9Wfb{i!B(Q#QDi^(T7X|;pJ^JLuHf^&)Dbi4pLt|qLg&ti^WIOguSGU>G!Cm#uj)e5G(FASTM6FY~r zr)HkD4te@RUIPM-vyy c>yuUVW|1IS`WqOrdz8ho#XQ@ z7kHFqXY$JDom?p-E#%Z^y;}0KPd|$`()kVYiPy{q15Kpl*vsMEs>!*Y9B}=BXRfX7 zHf76?CEZ@ib$Z*DFo!B6R9>zVIZ~X7-q!Tdp4B;0v7N6C8Ov#qjDjuFQD-%zv1PWL zR?`d`K+=Qh(pXR*wi>5&N0h;b$nV`I^)WrtTh+W}C5YA%d(4)|v-6Xt$gx^Or6mMr z+SLdQb+ylqkA?Pg%5PW+|3ORuJ>RL#>l+OqslD^gN?OE~?#2EGy8^|-o_R5$%F!{A z!Ks@nnk3DH_-+;NVNFUpCh)@m60YC?Mfc*~tKM^6lVD~&4CYss!fC_9YM+w{D%y1V z`KD$GV=5}A`Xdc P6YIKFzP|2W1etLTH_H*d{BSF=xk<_MD?c4w z&__ 6+&PbBYr(w#*s>95)zLiaT-UX+5g)`>G~G+(VHK0lYNc+~)Ah$ GSH zawo-yNf#E(RRYH>ht+nj2Et?z)^aE~L>0a67yU!D=+wsEg)3O~F75HKj8u)1{O~wy z*j`)7%2Y%sxJWTEDs>E7R#-GUIvP>Ci_}HujqJ8ulg>%i(LsgJF8J#DlQ$a_N> zzt~A#oLrx9ngMayLt~ZzenMWK70OPt0$p6ysGa(e6TL>zP!K3^hyT9Fi!%eQ$OCDB zR>Kk3aGi}`ljlwQtFB1kbAq=wbX|*r|4_j^v8+l5Zt#L3#A)4Vg6`EG*FJinUt3Tz z+*qMiU{77l?BSIXC!CZFnqVEcSksY?0z{~{ydRRh_X)-Plt;sebL5b@ABrX)=`i-s zUc#qAZ(aWLeI6`rz15xHt{nbHqCdcTGAdu@45_Nf(!4xyGc&VKl1HneyFDSOo?{kP z1`|&xtUqf@K?kRL5ujD95TQPSmd;bE$m57WNmSLiL``NxE`4C!_25ZYF ;yz5DkBpEPL-X@GPuSejJ zXQu2}Xg>}o#} a+LWF$GzSJ_OE;$|}PV^Rwvozj<>-1cHa zcqE JaCfc~#&Uw9E%Lw4r+?mdeZm4E z-BCzndgem kX1332$^VZDi2Q)S3!sSj{&lVQ=N7Q=0Jy9FcK_7>9s2h-$bbck0c{fh zhYfmPXU7IqIIgB$0>y9cGZ+#C5|JQ0$hs=(h3I=i!d%o!4rQ_Z=8M4By%f?vucCtC zK>(MhXH&>9GFsm+z}|-@Bk(mP1PbAlj0^??+^6eV8VdjIo`k6i>_CCD>Uojpzn4Mw zz6)s1JUpcTt`DaJ)RPWpl>RrFg}RV4H{xs+hk*aXe!OpT03~yS?f=j8zILR`9;vl! zhyHgzFs%d#E$fsF7L8Kq$ND SM8iM10G26^N9H=R zR_#G4t>*N@Lm3N~;@5L=wMZGpqWXj^M~C@WlXYc3r@Ng_<~V!=i&V(}W-F2oSm1bG z89>5WPVo;8G{!auXV=1SI59=hQ))n+G^FIeHzTKyZ5|?M5dS?c`m^k-M#*G}A@j+h zo0}0`@x+Tv+Wlw)e!`Ans1MDc9sZVE2CS+Fh?lS^LoI|xN>)=UOpa;XcdwpCUVa*@ zK_O&ol>2APxYgjG)_*GwRvK8KwC&Ic{Rg))=*6I-^L_mn0e z^8g@Dg}>$0$1@alk#nEeMA#Tpm3KWveCBK^qRBJD6uoeWnHJWq(am<;S&b%q;nY%W zyN%+W3@CQoL}ZQ+xv}IFvg#?>LjDUXjmEDXXEcU^iww1xs21FLgWVi2?xE&dn9;k2 zXl&ckYl5F5H)mgF2o=;v8T2(ct`c^L96}G>_eF9vZFqJ?jx0G>%m|d) OM }|lhk+|zLX%vTs4Qk&F=%Vo zLT?EnAU;Jk{8YA7NIcUboqt#DRZdI| (2_^0g=yE4dP>exz8b>!co%=~D^r_}5 z>uOEY(lYdCReiS@`Ok-tV8PP}mcZ F%Bw_ E^pcE27?HC<@IK|im7i0XEZvBbzCn)#|mz $j? Ao0gLm&ngd?I~}2^ zRP+RFX{d&2p-eP1;zg{}iCkHrcVfwvJq^n{7wEhWq$T=3D|>BbJs;wfENCK?2-d{@ zhV$fIy~8|#(1OMccFoK971l9|WhLn=eIqRTtG_ qIl9hxILv|lK= zp7t={L4}#;{_88ga~P5m(`3kV9*4TbIKtf!NrQ6XP?f=@plasTk39<(O-yNE$#K&m z?*=#vr4t^7h?VB&{R&fj8m}0e5vf;rY_;c7h3yXyb37$-!^J{%XWEavGOFfUx^xN# zvQ~n7eu`kc8lmvcA0vZ3ypB1Zbm+u;3EooK@I%mgQx}L-WEY1Fl5oZBqp+RLMy*g9 zw0r6|JU7KOQW^w8X|*oLIXY`)_l3`(8Kg~ZHwgbN-0vwTWJrpBvGWdG3+-L*_VDuE zT+v5;S9u6*C%?QWRhwf_=>w%BPq@XXn5?YGzDzyy?d0%b9W;}w I8y z3Od-#h4f6#Ee%fNZBIXpphmp7k(vRH !6r@%f&P<=V>$@(|;5ykte>;%3Gi4>cb3N7zSuEKw%brVMIt zlJ&o^4tn)&>-XL?oaBbNQaY&H2K6++z$hg6t^64z{TZSG3PPWuWlRwpsREkyfU#MX zPA yi%vr3R9KNNu-Kos2q;1Vz;E-X`fH9EDTbxR=$>)ef<&U~9vQKw8_1?Y=Y zR=U9K8dr(H%{ug|q^g}H b&nMTmRNq-5Hc%>P~7WabsJOqgyvQD&LfsLiA>r zZ@D-bpNNTpB{fG7Lc@}h?vzkz0QlC-nx5L~J!03}qf9MCd7-jA+ai%cTi-XB8V$wJ zPz`dXhDOOW&C8QD478LUV0cy|F}5fyl8_)A+Y*G)Qxse(UGQm|1kODu50W1N >8VB%W?wB)Ml@7lR!pl{SVi4$3QrY? z=$F5(7Ow<~VUh_o?QiJS9N8-1$zvz?hR^$kS4Uap6!MU9jM8=`e1tU=6XNxNq>XkT z@2ju|L;QLaRbses5&1c{JK&sO-XFdY-*inU>IjoFBrIL0r^q8m9*k1Tqg1zd ^WjeG=1#MMC$-5nZC=K$H2b zD||$cQ Qif F`< Hja{FsSSi 3>%xDa{ z{$w`*=C-4T{*I)`kE9wQhn>NNDD0_UJB2n*=V(R3yG2 #5dYZ3@w z+o>iRq2|7<|7%YMQ$7qzMW)1!jBuDxxeuPIRR`opkdeLabWv&rtBCVNf1On+adfHO z@fW%^8~`Xo-$6E~l9K0O-keuJI)3~kOw95Zq)vIy5IM2kB%QXcL2g){TR=JBFc2cN zaXl_5`%TXl?ysW(QwdmK_~F`?ks5DTAH-)OUJDwI=h?){TkjVYeIA_y1=-2@>P!tA zY6!4`nF;}(M5~U38kowd=-Go3)v7DG#|!fvFoV4X`3pyM1@A`ayc7|Ae+;|5P@rEz zdK0~LO46 7`V^$h!u<-17_N6a% zCqBv8X;{PtoRCWeeG zxQFn!I8{k-N#xGj%zE8M#evS<(!4RhR3zScnWRZxJe=5QprP3kR| zB& 9Gxbec>yI!oJoaUlhujbd6E z7h>fvTg&w&zj3OJ%V%~KZ3urxMxxErJ-EzKdNnY&UcmmZ2#gdYT0hpgb+-rG<1XWv z3`~|cKQC{?CjG6PK|glyNwK)($m_Z0EHLj!oh{5UHDVVunUSuRG<+!FtPql-dawLn zsOmc^k_qMEaRo=2n#$q@nSdoXOd;NL&E2-u@W=?*(yUUz67-co6~frWtr{ z1(i>KLz#Zd!2I1R=A-~%vq}CpJpTXiA^^AwjvD|diA2Vs|3AQ+0}22v=Qu9-@wZP( z{<5@xOd%rUBitQ?M@{>0T+TO({NiCh1Ckbx)%de*#&6#@AQiu72_KDA==^&iA}}6a z-U#V{{7Qp<5^CxY&G)Sm?Lo`ghCWd1L;fr15+?<&HRo}^rHdpKjEtp}{QzE2Q^Tg| zyl7OhXc>6VhSO*}@@~R}0cr3aBdG4bC(!?~WHP29sGmmDL4;nx-9d;Vy_Bnwc-;EI za@EG8ETRSSf#13Tp=&TH0Pox~ty)n2rzHALiU@%hBqU&j6AG>&B`VoA*=~aH5y(yq znf;pB_ykz!Rs@i0VOTx3f%m^x4eoorKc175(;d8+d*;{_fy;@oKbGdNZSbBV;;Tj> z1Y4k%j(;2RPlf}c%Ly)N=VtuV)6>9(Tp}e?Ng0{GZ_udX^q2^#04fPU!4+@3UmVY! znTAXDaWf(|N?k{{_rSZ);I*dys_k+p86DUe81SJ-z_7l+FA+WC$Bhuc6jTjBcYt8K z((brm`KN3M@u5thAC@&8(Rf^RLIx_wMhcgv{?e3`l;lGQ@dSu6Ag?J<-u9|<8coYm zVJ+gRD5CV_(!+AG79r`y 5^ZSQBsa64!6?3s3=PMJudE>*>pbk zR!pPh>ihRdB}xVQXuriefTUSHv{ay!fNuuc993}s=^SjQW cC?3a0oo@HpxJ(yd@c5E{C?yga?Doxwh+5j7b*9z5q1K O1xP dgeb{FDCT7tW4V+2G z6-tssOU3F9xKu 3F#;wDNp=VH+k&4?}Qpl aGGtzxn&aHm9WaR2d W zV*Dy_CPb>UIq0Tmq8ZC@<(g5CEZUVtF+K;1O&AHRigr&h)tvAPt%5}1AwVkFNGTOR z!C-SRBfpT!{ BT{no3m zbW+H6n-ZBCa6IPGP~~%l|5}C3xgH5ZYTAH!;{f|T!#I%#kE(@H2X7 f6bYDS6n?)&3E6(B!dBY_xSw8frc&j8DX&bdK4(V)5=UuS0_CoWjIhPil64Ww zcULx}Z`quBW8H}k{W$M@UpRn8M+*H_FJ=9xa7($e=F9h>z4a4>z5yD|J&x0%rkQTk ze_Qb1tt%56n7*X#6&w4vC~9g@@Zvd%kzAgc^Wu+(BD+5kVVMXVhy2SGw@{M?ZRe*! zaaV@th>G)oozB@ZpC@Zgmnc8Pnw=W#$Ogg)CN+vtjkC)*zV#aiNy@)!&t*BrTiTxt z42i#%lGuk3<;N)LET2rt3HIUA9Ctk`dsRw|FS1n2|3FYWz_KWse0JeBT~Nm#NRMc7 z%Xd=SK>HuemW_hNX^lrAlNt>8WQ^~~DLtGY1m}~gs^io=G%CeLR{(5k`$7uEttk@N zueCWffhKZFN`vS7%a(q~&R8*nC?9Mjt#i8+*UST2!5{;h^rqwJu?zu0=4c-YqQEl< zLL42=fM}sRGA`#{PjIR`_f~M3B3%@0kwH9}`T0j3485DZ3n-bF;!cGEs1L`^RzXkk zRtv9|MobKG^@RxNvWE#l^v#kRo_0P~2!<`!S4`9Tu6e>E*Joqmy{P7=%0Y=P${_=L zg#HGLFko918W6p|-#qx5zPr%L$*fC=q^%GiFJbw~LCP@013Z>!p={&{Mu)Fa8G_yf z0EZ)0v-$rq_Z41IZ{7QXD5$h_Hv-a)G)jlk-Q7sHpmcXP3=G{}(n!w`($Wr~)X??& zaPJ%U-uL?l{MMSah&5~G%sFT8{XBc`XOEL0jbW&~SI(Z$_{7>p8|5p3{?$yqQL>oS zABnbg0*PVY>z%1(&%Xp~A2y^Na(9qmXKdpdjTAf?$}|o+Fr@tmX!gF5v{=kCv!{If zY^=f~>tBQZf|In`l_y{Gb$ZV}c=Kz0gV15W__xLWs46*Y8_8P)R*vm06W=kS?`E{E z!K*VSiz1a$Sw`GyQ46cA@AM27BQ3k }zt*^X8>%t|u?(1` zmODxPBQlCU`^j3$sCk$&bJ0Q$r<|*|hG>i%+>DZXFYqFQPD46bkH`*)Iky20;uT}F zE<|Q )P;u$E0 H5 zl;bRFY;||=VXQQL`+$!^XP;w;RqAC0g|v15?jBeyA`6pJ Y|5&}j$x3t;zlnoPF8#y8*HZh`o<|hn{51DM zXuRQ@Apb6)h6EP1UTVdQXp(da=HyPKah4+aeU@m{VEgVK&YG7hH^^9hv;Caf(j!$u zKfEizRL2t5h&mGPYJPoXossofFj0c^zWt4CY+J9F+WASG?`ctRS(cH|j9f-sk)1DT zl?O__*7GtwbxXL{*Wi8c=K;u!fi1NMiF`_5seeDzP5<}N-A~PSwyw6^m7awiQF5`S zP~v5!@M?S#H7pD+9Z*D-6izZ^fQPLl$V?x(*ElL@+bS{ e=Wp`53pDC^bar=*F}l;KsVtrQTi?`jovkwMaO28@bfC-o5)s zv7JyEWuX)&{0{8gZFlD0KC6EvSh9K$R8>S|lT!jWNNYmpR@%{V(v)+|L6XiTMkKo5 z0wmk19JHng^9t(J>XF-7?7h1qi+c9iF|Pk2l9~L@!Jyk3VXfplt9=vz=m1J?Xt5G8 zf(131^XrxtIm9C=(E}&vCu3)4Zq$~12S*nfQfJK;h(tIdYJwszN65FYw}n zgWSF}Z8dZL$2co>sm3Y d8fA5A{J`Zap}{ziVL_b1lEX;adq|DFZF73)#k=e669!gTb=@$LWl3e<|90->Qw zUnku~^)B K0}pt1XvZ%3eD*Y!Y9RX*|LoQ z-(^cgf*T#~&(}%I$Pj+B*24d{x9fgIpKA;Sbni!Z*9RL*EzXk`(IYHvsB`+>% |42O}Oo+I^4pzF*|F zrUsyt)gO7Elpv@MQUP}k0r7LKBSZkeIF>&AQ2%kjX`Cp;H{k(!9^IFd26fX4n#>um zc=}IOYft`lZ-F@vqvU(P+}fr@e9tBiR2c>IoX5D?=Xc)zP7GHNpbtSO?ExlGpfyQ0 z@U |u`l2kij`+2pYmooThok# z2ck#-?B(B19x$xd8+Lnrz5zJFV7U!oOa qH-LjP5JS{;39BMyV(AQh)=gaN~_MC-SUEMETDnV)#(NfRYIGS wV99 zRW?h@D#axwpNgae+fcp!l{$gE%^9i>@y!2j8qlYQO+fMBE|3O||J-(=my^hRw_EDu zpp5kNNMCM+93H!LqwGqIk5VyY{RjT{-WzMzt$UTdi3}>JM$#js!afA#eiwOh2IPJ8 z3hAa?9^0dYH%Br4ee^j#frOmqqE%L(+!ctt*SZ59OUjE?92}3Z^1gljD`F~UfUZ8r zE<`^=z_oM=BV6&Rz}=ta83H=QtCoVJjLwUGRw9x3pEdT39*3M(sdQf 2R%$aHh)KH(5b1-DM0ZkmB!(X>7hu2w$TD`AW8x!}6V$KT|}LLI@4HQ)&wE zyr5wv(RbMc1Cs4 5|*o47g1J60S?oO*dXlPgn;_Q|^^_;V5?SnH_wIf!|+4uzCZu54rBfa)5 znQKP9KW4oToSo2)^7K0BIFB@lcQvE%?T$(23jSQF*^48B1dm8x!SM2jEx|ha-Xey* z?Z%;nlg*vKTG=?_Zxe{w*4_0^w(2k2Nxi|F*j)gL$isa7Y7&l> fHwl=! z6H`()n!0Y8@m_0wa$oU822w!0l{PnDvfj9_-=41y7NzK0Z(gpPcUlNOD*XX(TfIdG zTFQ9H1ZM)lxw(%3+vR 2p9R$|kWYEmTV-9gyRu>n^uKtYfZ62PeREHWoE >7iBU6!SfD5@blS_`QX7uz+{?1NQ@td>{wmW&^7CVSuhDVpP{P-T zv*>$@pkn*pDA4K$5?n5BKvNk(N`!uTL0W_!j{i2Qp%8TZP4fioQ7E1WLodeg-Ij>R zo(oZE2)u&k+?gg$IeY1il(7{arOC6)?I=}&n+xrJ?uuwDj%|&FX!_<(Yog7 e9J-mSrw0{yG-lC>$JazLMwi0tZ*`6qiq2L~4Z!IPSt^5CzT| z8F$Ucv-o$|=Z)OLk*;LAZ#aq%k{C<&2E5yIFw=xU&wdJ3X82X^?NoK9oRG!97+&mU zj%Qu=<~)GQTpX%fT+M#E6MXBFyll;f1Sm!Jv!9Qn{iJujGwtJg-U`Op@m*Jv=;-Ji z8Qh;51l#EN`E_Es%ZM-~g2NHNX>_r{EW)1ZX^c>zZTkM4q}&iwsd||F6JDjRv>RBA zk;7;VW6iPFE8BAEZvoP (C>Mw4Wf6#fdO#hK%UWbUyH-U-bP#R=Gr%^PiG z^3?J~V{d~U*&W>jd~N-~Gu;~Z$Mm@deWyZ-tJm<31cL$O@pxL3gin{f0e;zgzT@74 zJUa~9Rd8ob|M~(-SuTNslciiFdf!wihEPWyJSqTVtKfugy+np$3gzeLsLQe%25p)J zf)EqIZ--;Nt9@+;oe@h*x+eYj4J0a*vgDF3*B2Wx>?-SQV7bfz!;fvt!D?x(O)zfU zcyw}M+fWHc64*}E^XEhfG9zhaV^-~SXjGLonW~x)!LhNiuSq{E+kOl5_E-Y}GWdPb zCD%# o5rorU+8` z!D9R1hQ-Qgw)W11;NaYpoMGe=W{yrsRTpN46HEC{!LL1hB($ex#xE-}^0C%|NlXDA zw7L1A&*`l53ci+m%cW~P*0vd?q7y+t>%Ij(3^8ItxY!kcUJm{P>cL~twG5SP^~%h? zzv k=UN6Za(EwRTxfQH`c^ zk6@--+t@_$b5`hfUd>byRXPz>!M0Kicy^WZ }_ zP1VT;F%fP#DxIsG%bgW9A_>w-+{~m=fD78w*6LmMN$Eu~ R#Vf7Ri8amitnBOj^Z3gG +zr#`5d5-4rCY97$dW@Nshs1^NuDCt zfNR$yQ;kV{9Zt<}zAA7AhK^K%BC~$cBXkTh@SVOSszZMEb7y<1Kq^X2`_x&n;xo5j zUZ;cP^=ni-l5J98J`Ji5N(S^gG!Tl3sPL)u>6UWwMobZJF;Xf9dH`UH!6=8fc0UsG z_TdQX%d_yj*>u&-DFtY~6% azbu)Bjp8n=aCHhu-ptnk}R-F!*=bWS+XBwU&H 3}Pb4<5=R>$D63gi+75yFB~vC>NMs5dTSg+_Ks; zNGDtyQ3|M3k9f3_To+3|An^IiAdukVKX9o%x>F+8iGIWHQN(n3(uzi`9P~6Nv#4 zeRZk2C_PA3xmW=%rj%Phqc!Y7PR$;B`0Xb>?R%Ug|6{tsotB}Fs zvO7?suE59zGp1v_gCR0npBDDyW<&c`Ql%-dP$qjAu*~C3{s%_eqm(okMp!I)Y((*S z9v5PX4B0mk&niaZPovdGIBIJ=SFKUnkloGvSvJt7S|ju1m7Zfz7$D3BK*QT~h3KGv z?m#}~dnbwhB7g{uHkSs5*cULTu_to*C28Y>e-tFY>t$%SBCl7_6FLVb$w`uP4J5bn z@7`F7y3=6;Q@C(4v5oNf|7Tv<&!BEg+_ZAd>GYqy7$RIt-? 21XavwuYT(DX~D?#a-jHkQmQ#;0CirQ6;DRFT|&6Bq f!CX1G6@0--BC`l-{$QR-H@@ch-h!C= z9gt^`j803-*n_Ptk$tx1DwU$C(L8o;jbW#D+K5>Y;>uCeP<_Kpsd!Z4X z4f%mkAQ0HYRZLzn$hlf%2zk53I;>h7vj8Eg6A+b5 (*m{d+)d5sf8P8n8Byjy)x-}PEV%qGd*l7G6J_2D zX`B|393aPnEwv>=SmEttCwMXEOsbzv!ApMv%x0cV4C_q)#dPqD;br8Q;2 Sd7? 88)CQ%V|Ly7f00w@3LdD{jb9(N2 z@|t{B_tUh@%*<%QITO_j$|KCnTqsS^abZAm1&&SN_JV$5G3k{r&~8nYX0eqAUz-_9 zvx{kmY}EW&Ot9Q!@T+{zv< y!DZ>NK#Og{VU1s_fHaKVd0W1~fiV}>@?he)9h%O?#4_r(8Ay`U0Y O=#Q*^$+EBq_ z#50zU;vxo8Q3YAuY|j xLqo*R)#}u4>c#p>!Ah$~p7+*wQqJz4UwyP5BJc<6#XL zINeswApW(L?C|~(xf_ibw_J%c`LDxuU3fFxZBvf*xYEPjJsHP9 t%@kyy!Mf`0EX*CC*((cqy$TLc=`cG^AZQ zfppw+en=<&^C*KK 7nrDkNGbA2JhV|muYG(lA$4-0fUjCMHda*liWO+N-I|x zjurkm`O$QsVDvX<36wWg4Iw{%q@|@j|wtfG0OakirP@@s#60b_S z^it*X Au#9PzY>!W(qF=!xsXxgH!Q3A4hJf+qnGjCFK5xe2>LnvI=Miw@Qar0X5P)r zOi33vxu1o=2<<$xPvi4<&eVMcXOcL~4B{ppv(*JX^i2hp0VskWO!zL}pX;*E+DGY2 z#3eH8#)jKPwC$ QFPZ5tnT^m~{WYa+TXJT`tg* OeK%zMv8=Us6Rrm327-UW}BAAU6>T9ggjz@Xk zN9EJ|NAbs$OH9@7i(1v-r)zxTKCcS!mi>6B>nMd(eb_HU`YN$KC_;RrGWHWJ>ep9n zqc3*tQMg^0+BJHM|bQ_J?C )#; zg$B+z|3m5Yg9@ANoy0A>%@rI-thuxv>nhcx)<@En9p)p3Kj-us^SYyQ6RIp#ryGnR zA0)rOkdnB0|M|J=`5P~I$y&B$E62c0?sqNhPzl7|9c6mQ+Adb{58u2J?PnV1UxUF2 zNv%RU;emsVCSz3OjM;1_rUgDDPx(}6?knss?DZLxF#(AR_LzGsCJc4k2K)||ExHVh z7+R;l3_ex-kG!L$zH5C@(RR^VTdAcQHiPC^7O`Y!e2ZiGc+A3Snfl2^eCYeFb)sU1 zjqSw`lY_Zyv^tH{N{@3_(_%(LK0dxJ?X|~h&pe6TeeK`Z3JZEKdI|_x5Sf1CcGId$ zm+_W$&%RRE2r+xt=F^;iD)YKL5T?oGLuJd4zZ@M;Pii@tM;GRuMN~;!MD<4Q%WI?E z=J|JfAPTO(7dpH9=?%lBI~ocvxhR^nZyqK!2XiNA3&A{HCpg~~uj~>=&AFb6Y?)Lh zTKvut^!&)R{v%b!@a;pJR$-vjiZ5|&6C~nlbvp0vlu>p1iw>jOT{QteYh}n=g6D+- zPAWRKWSaUymbf-q(qCnKPQ*RW%2Pa)_9O
g{nD@2 z^9qW!ghtUm>$~{!ozXw+W8P@>5205!T1bp9_fzp*W}WQP_$hzD|2iBoP!K=_jffSY z-`w23t3&L@%oJyq{FN| 5K+(rP1zg6-$zaz7s8^7)0Cy4Rsfge3P4Ova4Mo<5? _u1sHL6L-%DZB+P;dY1WBDJ+y{Z0zyVd9AKB5?P&BJTuGNF(?x z$n>cKI?P0L(>YnS^=ThnwCXLc`I!+nis%qV(r($6O7yF=ZW)bKVw)Zd%_2tq9Tc-2 zBdOwi&|r-cE1s3NPAe!|u(A~Zl)Rb(X&tIZh4j3FoU4|;YQjK $BO0l=hdy3Kiqo2%B$TvVzx*=Tg0VzE_-jRvX=BB`~;Dg)QaH+enQ_+hOxfRfr zAxO0#8byx5$$12rL)p8(t4W%^_+srjibTcrUZ8P8u-ZZVsBi;!!QCf(!jt2gPII*V zU$sQb2fH=D$ma^iG3!b{csp`G-=Qm*F)ZPx?ajthL)?R)D*dMj{O2#aBmi =OL)_S1J z){zX4K0YdJ0x!f~JT5`cZ~^Zp@y)Hw_ji)hoy(sFZcj^mWS7R|Yo1|S{0!fVC3oR3 z6TPhT!V32}Bul)IAJNGF*-16u__gdPlq#MX8f6%IK)O}3au85ehdb8RWOON{t=M>< z)6s?iEn_?g>bf98p+oA?pom3H#grCBc@?KF8mf;6WAJLQ^!_1KL2{k{F+jNir$f+K zt=T7O;f2WaEfTAV05TD4_S5&fUvodHeZbtL7A25IF$fTq9ulR%^q6xPD+lUioWrB0 z=EtL(`p&=Ry{kHAJAR)UT+d&x-1}B}- )d__I!OMu5MPBV~1;IV8hoZX9f|1Gf69H8!~p8bcGVz zWRNUhrK{1DDmf*$N`*4=YlW-DuAg+f$5SuS?Ye8)TxMYNMZ85ZxBY_ jcW2}9@s>TIX1d5le*dE11`zN)3=s2n-O1gN85RPc1_cQ*;J-h @EI{ip9f%o@jHp8xfOX<%J>vAa^K#7wDs@+BWBk3&T`xoU1b^?dsRjx9WmS$0 zy`r_8&HXMXOm11Mv+8zz5Yk{!PTM+rzvUpTDHy|Zd`{>tEpXqJmA-O6H@6jdUO3@@ zaZ4LLb+zCP>bm_IW_EsSz$bD$aerQBb{)*>|9bgw>$J$$chh%yTJ5Z-6gE#10yFWv z{IP0~awjK|S>)C3)baB=N9o|+&qQ%$JYZBwkYZ#}`Y18_&i{_1<8&8;Zf!W{)`@QG zxK`|Hd7@Ax8!E(I#hcaX 5#%0pEds?j84yvIYl8|*00wc8-egHmP_f LkSs7fjDG`VwsAyLMfzu z;|89^>Y_xeo0CQSa|L+b{dw nIG@$LMK_-;UDg oczLuz`z&_dWTV8x2iELlBQm9OEUEw#Y__*At zux#Q5W#EJZVh&vrFkOkrC(w0;2@WdK03Twcp#4k&ox*~xlFj^MbUwp6vjWjQ>L0U1 zQ7bsXpheR?rY gwd5ygBTCEQJPp>o5_fBy_3@>ji4O5zGx>ux(U9sLRV&StO~Eef*VKKn3H?vSTk= z5FFasw YILZd)8ZO^5XzL? z72eNgb;DDZ8GUWzJK1^Dx}5QQsKonsOED7B9KBhbmlUb1Y8u+KPFClY@WRO{H%jr8 zaJWR7Kc=EY*be%2(F?zu4S1a3DeKm- @v0U7gF&=jm4w&o}sc@qSp>z2MD~A4R zTGba!k8J}zJ{}L$b9jdJ>LxH%DOAw(g)SU_Ah0t5nmP?8Q>)gns5Y5=#oPEeK>e*@ zZ(SEIH;J9V)(dNSGh(~XcvnyV8p{9=T=7|yI;@UXoAQ%-mA;;I)UIdX=(_6{9I<#* zE=twc?b5^aRRjAqV|CBKYF(wps1U22#;dU6fJVNXEi7T_qWL #aC>SP&zUp5KIm71s~Iaf(B|7LaN?*7VHvVA2Kv=rhC91 zTdJ?)jrP)%5YcjhD?nNKtECDbxYy=IjcJX;*hs{ns)-(7_Xu3v#*VV6Z%+WIyF-5i zcHKZ3diFSgQi=klm@ 7N?|tfTrA>xb5+O (Mo-b&i-cu=^!e<9L z$YdV5I8yP4$=danAFLM{ni_yf%UGt?3s5Byy&-%u@jy!(Z!Q|>@@x23O#`?P6%{0( zrW{J`(VMb^4Jl1?4187_CY8`SdzRyKJRF5W--mm^7Gc*KYalekQ=~!i0!jNx07a&@ z8I5?%f8ob}LTE4w!k1vM{SfBcsk#YaozNFw+u|q%ZJ)*uiS`LLt+&)}&wqXoR{80% zE5GDxO!+QWJ~{m-WLM^1-rf7=XmG?uqHkRIdS4}3K&ZL}<0=Rxo!dtJ=t_M5Yj4UE zZ)&)QFIXk)m7O)Rl|>sCy}mMN Kz^j-@@rBVelF?6VX^MM+3)NPtF Cr}KZTPlphJ20eeHanYVh|7~c@n}hZ` zkJC^rZs6-$I8IR+AUR)D*ZS*BhvVUs5*WCFMSirEiU|8LBxXZi>tXZ|ppQQR7AQ^L zm+D)SSgJ7v+KdaRuT>|