Spaces:
Running
Running
library(shiny) | |
library(shinydashboard) | |
library(shinydashboardPlus) | |
library(shinyWidgets) | |
library(shinycssloaders) | |
library(DT) | |
library(plotly) | |
library(scico) | |
library(ggthemes) | |
library(data.table) | |
library(dtplyr) | |
library(parallel) | |
library(Rnumerai) | |
# ============================================================================== | |
# Tournament Information | |
# ============================================================================== | |
# Download latest leaderboard from Numerai and get a list of all models | |
d_lb <- get_leaderboard() | |
ls_model <- sort(d_lb$username) | |
# Round info | |
d_comp <- get_competitions() | |
# ============================================================================== | |
# Helper Functions | |
# ============================================================================== | |
# Download raw data | |
download_raw_data <- function(model_name) { | |
# Download data from Numerai | |
d_raw <- round_model_performances(model_name) | |
# Remove rows without CORR | |
d_raw <- d_raw[!is.na(d_raw$corr), ] | |
# Add the model name | |
d_raw$model <- model_name | |
# Return | |
return(d_raw) | |
} | |
# Reformat | |
reformat_data <- function(d_raw) { | |
# Keep some columns only | |
col_keep <- c("model", "corr", "corrPercentile", "corrWMetamodel", | |
"fncV3", "fncV3Percentile", "payout", "roundPayoutFactor", | |
"roundNumber", "roundResolved", "selectedStakeValue", | |
"tc", "tcPercentile") | |
d_munged <- d_raw[, col_keep, with = FALSE] | |
# Reformat percentile | |
d_munged[, corrPercentile := round(corrPercentile * 100, 6)] | |
d_munged[, fncV3Percentile := round(fncV3Percentile * 100, 6)] | |
d_munged[, tcPercentile := round(tcPercentile * 100, 6)] | |
# Reorder columns | |
setcolorder(d_munged, c("model", "roundNumber", "roundResolved", | |
"selectedStakeValue", | |
"corr", "corrPercentile", | |
"fncV3", "fncV3Percentile", | |
"tc", "tcPercentile", | |
"corrWMetamodel", | |
"roundPayoutFactor", "payout")) | |
# Rename columns | |
colnames(d_munged) <- c("model", "round", "resolved", | |
"stake", | |
"corr", "corr_pct", | |
"fncv3", "fncv3_pct", | |
"tc", "tc_pct", | |
"corr_meta", | |
"pay_ftr", "payout") | |
# Return | |
return(d_munged) | |
} | |
# ============================================================================== | |
# UI | |
# ============================================================================== | |
ui <- shinydashboardPlus::dashboardPage( | |
title = "Shiny Numerati", | |
skin = "black-light", | |
options = list(sidebarExpandOnHover = TRUE), | |
header = shinydashboardPlus::dashboardHeader( | |
title = "✨ Shiny Numerati", | |
userOutput("user") | |
), | |
# ============================================================================ | |
# Sidebar | |
# ============================================================================ | |
sidebar = shinydashboardPlus::dashboardSidebar( | |
id = "sidebar", | |
sidebarMenu( | |
menuItem(text = "Start Here", tabName = "start", icon = icon("play")), | |
menuItem(text = "Payout Summary", tabName = "payout", icon = icon("credit-card")), | |
menuItem(text = "Model Performance", tabName = "performance", icon = icon("line-chart")), | |
menuItem(text = "About", tabName = "about", icon = icon("question-circle")) | |
), | |
minified = TRUE, | |
collapsed = FALSE | |
), | |
# ============================================================================ | |
# Main Body | |
# ============================================================================ | |
body = dashboardBody( | |
tabItems( | |
# ======================================================================== | |
# Start Here | |
# ======================================================================== | |
tabItem(tabName = "start", | |
fluidPage( | |
# ============================================================== | |
# Special script to keep session alive | |
# ============================================================== | |
tags$head( | |
HTML( | |
" | |
<script> | |
var socket_timeout_interval | |
var n = 0 | |
$(document).on('shiny:connected', function(event) { | |
socket_timeout_interval = setInterval(function(){ | |
Shiny.onInputChange('count', n++) | |
}, 5000) | |
}); | |
$(document).on('shiny:disconnected', function(event) { | |
clearInterval(socket_timeout_interval) | |
}); | |
</script> | |
" | |
) | |
), | |
# ============================================================== | |
# First Page | |
# ============================================================== | |
markdown("# **Shiny Numerati**"), | |
markdown("### Community Dashboard for the Numerai Classic Tournament"), | |
br(), | |
fluidRow( | |
column(6, | |
markdown("## **Step 1 - Select Your Models**"), | |
markdown("### First, click this ⬇"), | |
pickerInput(inputId = "model", | |
label = " ", | |
choices = ls_model, | |
multiple = TRUE, | |
width = "100%", | |
options = list( | |
`title` = "---------->>> HERE <<<----------", | |
`header` = "Notes: 1) Use the search box below to find and select your models. 2) Use 'Select All' for quick selection.", | |
size = 20, | |
`actions-box` = TRUE, | |
`live-search` = TRUE, | |
`live-search-placeholder` = "For example, try lgbm_v4 or integration_test", | |
`virtual-scroll` = TRUE, | |
`multiple-separator` = ", ", | |
`selected-text-format`= "count > 3", | |
`count-selected-text` = "{0} models selected (out of {1})", | |
`deselect-all-text` = "Deselect All", | |
`select-all-text` = "Select All" | |
) | |
) | |
), | |
column(6, | |
markdown("## **Step 2 - Download Data**"), | |
markdown("### Next, click this ⬇ (it may take a while)"), | |
br(), | |
actionBttn(inputId = "button_download", | |
label = "Download Data from Numerai", | |
color = "primary", | |
icon = icon("cloud-download"), | |
style = "gradient", | |
block = TRUE | |
) | |
) | |
), | |
br(), | |
h3(strong(textOutput(outputId = "text_download"))), | |
verbatimTextOutput(outputId = "print_download"), | |
br(), | |
h3(strong(textOutput(outputId = "text_preview"))), | |
shinycssloaders::withSpinner(DTOutput("dt_model")), | |
br(), | |
h3(strong(textOutput(outputId = "text_next"))) | |
) | |
), | |
# ======================================================================== | |
# Payout Summary | |
# ======================================================================== | |
tabItem(tabName = "payout", | |
fluidPage( | |
markdown("# **Payout Summary**"), | |
markdown("### Remember to refresh the charts after making changes to model selection or settings below"), | |
br(), | |
fluidRow( | |
column(6, | |
markdown("## **Step 1 - Define the Range**"), | |
sliderInput(inputId = "range_round", | |
label = "Numerai Classic Tournament Rounds", | |
width = "100%", | |
min = min(d_comp$number), | |
max = max(d_comp$number), | |
# note: daily rounds from round 339 | |
value = c(394, max(d_comp$number)) | |
) | |
), | |
column(6, | |
markdown("## **Step 2 - Visualise**"), | |
br(), | |
actionBttn(inputId = "button_filter", | |
label = "Create / Refresh Charts", | |
color = "primary", | |
icon = icon("refresh"), | |
style = "gradient", | |
block = TRUE) | |
) | |
), | |
br(), | |
tabsetPanel(type = "tabs", | |
tabPanel("All Models", | |
br(), | |
h3(strong(textOutput(outputId = "text_payout"))), | |
fluidRow( | |
# class = "text-center", | |
valueBoxOutput("payout_confirmed", width = 3), | |
valueBoxOutput("payout_pending", width = 3), | |
valueBoxOutput("payout_total", width = 3), | |
valueBoxOutput("payout_average", width = 3) | |
), | |
br(), | |
shinycssloaders::withSpinner(plotlyOutput("plot_payout_stacked")), | |
br(), | |
br(), | |
br(), | |
DTOutput("dt_payout_summary") | |
), | |
tabPanel("Individual Models", | |
br(), | |
shinycssloaders::withSpinner(plotlyOutput("plot_payout_individual"))) | |
) | |
) | |
), | |
# ======================================================================== | |
# Model Performance | |
# ======================================================================== | |
tabItem(tabName = "performance", | |
fluidPage( | |
markdown("![image](https://media.giphy.com/media/cftSzNoCTfSyAWctcl/giphy.gif)") | |
) | |
), | |
# ======================================================================== | |
# About | |
# ======================================================================== | |
tabItem(tabName = "about", | |
markdown("## **About this App**"), | |
markdown('#### Yet another Numerai community dashboard by <b><a href="https://linktr.ee/jofaichow" target="_blank">Jo-fai Chow</a></b>.'), | |
br(), | |
markdown("## **Acknowledgements**"), | |
markdown("- #### This hobby project was inspired by Rajiv's <b><a href='https://huggingface.co./spaces/rajistics/shiny-kmeans' target='_blank'>shiny-kmeans</a></b> on 🤗 Spaces."), | |
markdown('- #### The <b><a href="https://linktr.ee/jofaichow" target="_blank">Rnumerai</a></b> package from Omni Analytics Group.'), | |
br(), | |
markdown("## **Changelog**"), | |
markdown( | |
" | |
- #### **0.1.0** — First prototype with an interactive table output | |
- #### **0.1.1** — Added a functional `Payout Summary` page | |
- #### **0.1.2** — `Payout Summary` layout updates | |
"), | |
br(), | |
markdown("## **Session Info**"), | |
verbatimTextOutput(outputId = "session_info"), | |
br(), | |
textOutput("keepAlive") # trick to keep session alive | |
) | |
# ======================================================================== | |
) # end of tabItems | |
), | |
footer = shinydashboardPlus::dashboardFooter( | |
left = "Powered by ❤️, ☕, Shiny, and 🤗 Spaces", | |
right = paste0("Version 0.1.2")) | |
) | |
# ============================================================================== | |
# Server | |
# ============================================================================== | |
server <- function(input, output) { | |
# About Joe | |
output$user <- renderUser({ | |
dashboardUser( | |
name = "JC", | |
image = "https://numerai-public-images.s3.amazonaws.com/profile_images/aijoe_v5_compressed-iJWEo1WeHkpH.jpg", | |
subtitle = "@matlabulous", | |
footer = p('"THE NMR LIFE CHOSE ME."', class = 'text-center') | |
) | |
}) | |
# ============================================================================ | |
# Reactive: Data | |
# ============================================================================ | |
react_ls_model <- eventReactive(input$button_download, {sort(input$model)}) | |
output$print_download <- renderPrint({react_ls_model()}) | |
output$text_download <- renderText({ | |
if (length(react_ls_model()) >= 1) "Your Selection:" else " " | |
}) | |
output$text_preview <- renderText({ | |
if (length(react_ls_model()) >= 1) "Data Preview:" else " " | |
}) | |
output$text_next <- renderText({ | |
if (length(react_ls_model()) >= 1) "⬅ [NEW] Payout Summary 📈📊🔥" else " " | |
}) | |
react_d_model <- eventReactive( | |
input$button_download, | |
{ | |
# Parallelised download | |
d_raw <- rbindlist(mclapply(X = input$model, | |
FUN = download_raw_data, | |
mc.cores = detectCores())) | |
# Data munging | |
d_munged <- reformat_data(d_raw) | |
# Return final result | |
d_munged | |
} | |
) | |
# ============================================================================ | |
# Reactive: DataTable | |
# ============================================================================ | |
output$dt_model <- DT::renderDT({ | |
DT::datatable( | |
# Data | |
react_d_model(), | |
# Other Options | |
rownames = FALSE, | |
extensions = "Buttons", | |
options = | |
list( | |
dom = 'Bflrtip', # https://datatables.net/reference/option/dom | |
buttons = list('csv', 'excel', 'copy', 'print'), # https://rstudio.github.io/DT/003-tabletools-buttons.html | |
order = list(list(0, 'asc'), list(1, 'asc')), | |
pageLength = 5, | |
lengthMenu = c(5, 10, 20, 100, 500, 1000, 50000), | |
columnDefs = list(list(className = 'dt-center', targets = "_all"))) | |
) |> | |
# Reformat individual columns | |
formatRound(columns = c("corr", "tc", "fncv3", "corr_meta", "pay_ftr"), digits = 4) |> | |
formatRound(columns = c("corr_pct", "tc_pct", "fncv3_pct"), digits = 1) |> | |
formatRound(columns = c("stake", "payout"), digits = 2) |> | |
formatStyle(columns = c("model"), | |
fontWeight = "bold") |> | |
formatStyle(columns = c("stake"), | |
fontWeight = "bold", | |
color = styleInterval(cuts = -1e-15, values = c("#D24141", "#2196F3"))) |> | |
formatStyle(columns = c("corr", "fncv3"), | |
color = styleInterval(cuts = -1e-15, values = c("#D24141", "black"))) |> | |
formatStyle(columns = c("tc"), | |
color = styleInterval(cuts = -1e-15, values = c("#D24141", "#A278DC"))) |> | |
formatStyle(columns = c("corr_pct", "tc_pct", "fncv3_pct"), | |
color = styleInterval(cuts = c(1, 5, 15, 85, 95, 99), | |
values = c("#692020", "#9A2F2F", "#D24141", | |
"#D1D1D1", # light grey | |
"#00A800", "#007000", "#003700"))) |> | |
formatStyle(columns = c("payout"), | |
fontWeight = "bold", | |
color = styleInterval(cuts = c(-1e-15, 1e-15), | |
values = c("#D24141", "#D1D1D1", "#00A800"))) | |
}) | |
# ============================================================================ | |
# Reactive: filtering data for all charts | |
# ============================================================================ | |
react_d_filter <- eventReactive( | |
input$button_filter, | |
{ | |
# Model data | |
d_filter <- react_d_model() | |
# Filtering | |
d_filter <- d_filter[pay_ftr > 0, ] # ignoring the new daily rounds for now | |
d_filter <- d_filter[round >= input$range_round[1], ] | |
d_filter <- d_filter[round <= input$range_round[2], ] | |
# Return | |
d_filter | |
}) | |
react_d_payout_summary <- eventReactive( | |
input$button_filter, | |
{ | |
# Summarise payout | |
d_smry <- | |
react_d_filter() |> lazy_dt() |> | |
group_by(round, resolved) |> | |
summarise(stake = sum(stake, na.rm = T), | |
payout = sum(payout, na.rm = T)) |> | |
as.data.table() | |
d_smry$rate_of_return <- (d_smry$payout / d_smry$stake) * 100 | |
# Rename | |
colnames(d_smry) <- c("round", "resolved", "total_stake", | |
"total_payout", "rate_of_return") | |
# Return | |
d_smry | |
}) | |
# ============================================================================ | |
# Reactive: Payout Value Boxes | |
# ============================================================================ | |
output$text_payout <- renderText({ | |
if (nrow(react_d_filter()) >= 1) "Payouts in NMR" else " " | |
}) | |
output$payout_confirmed <- renderValueBox({ | |
valueBox(value = round(sum(react_d_filter()[resolved == TRUE, ]$payout, na.rm = T), 2), | |
subtitle = "Confirmed", | |
icon = icon("check"), | |
color = "green") | |
}) | |
output$payout_pending <- renderValueBox({ | |
valueBox(value = round(sum(react_d_filter()[resolved == FALSE, ]$payout, na.rm = T), 2), | |
subtitle = "Pending", | |
icon = icon("clock"), | |
color = "yellow") | |
}) | |
output$payout_total <- renderValueBox({ | |
valueBox(value = round(sum(react_d_filter()$payout, na.rm = T), 2), | |
subtitle = "Confirmed + Pending", | |
icon = icon("plus"), | |
color = "aqua") | |
}) | |
output$payout_average <- renderValueBox({ | |
valueBox(value = round((sum(react_d_filter()$payout, na.rm = T) / length(unique(react_d_filter()$round))), 2), | |
subtitle = "Round Average", | |
icon = icon("credit-card"), | |
color = "light-blue") | |
}) | |
# ============================================================================ | |
# Reactive: Payout Charts | |
# ============================================================================ | |
# Stacked Bar Chart | |
output$plot_payout_stacked <- renderPlotly({ | |
# ggplot | |
p <- ggplot(react_d_filter(), | |
aes(x = round, y = payout, fill = payout, | |
text = paste("Model:", model, | |
"\nRound:", round, | |
"\nResolved:", resolved, | |
"\nPayout:", round(payout,2), "NMR"))) + | |
geom_bar(position = "stack", stat = "identity") + | |
theme( | |
panel.border = element_rect(fill = 'transparent', | |
color = "grey", linewidth = 0.25), | |
panel.background = element_rect(fill = 'transparent'), | |
plot.background = element_rect(fill = 'transparent', color = NA), | |
panel.grid.major = element_blank(), | |
panel.grid.minor = element_blank(), | |
strip.background = element_rect(fill = 'transparent'), | |
strip.text = element_text(), | |
strip.clip = "on", | |
legend.background = element_rect(fill = 'transparent'), | |
legend.box.background = element_rect(fill = 'transparent') | |
) + | |
geom_hline(aes(yintercept = 0), linewidth = 0.25, color = "grey") + | |
scale_fill_scico(palette = "vikO", direction = -1, midpoint = 0) + | |
xlab("Tournament Round") + | |
ylab("Payout (NMR)") | |
# Generate plotly | |
ggplotly(p, tooltip = "text") | |
}) | |
# Individual | |
output$plot_payout_individual <- renderPlotly({ | |
# Get the number of unique models | |
n_model <- length(unique(react_d_filter()$model)) | |
# Base plot | |
p <- ggplot(react_d_filter(), aes(x = round, y = payout, fill = payout, | |
text = paste("Round:", round, | |
"\nResolved:", resolved, | |
"\nPayout:", round(payout,2), "NMR"))) + | |
geom_bar(stat = "identity") + | |
theme( | |
panel.border = element_rect(fill = 'transparent', | |
color = "grey", linewidth = 0.25), | |
panel.background = element_rect(fill = 'transparent'), | |
plot.background = element_rect(fill = 'transparent', color = NA), | |
panel.grid.major = element_blank(), | |
panel.grid.minor = element_blank(), | |
strip.background = element_rect(fill = 'transparent'), | |
strip.text = element_text(), | |
strip.clip = "on", | |
legend.background = element_rect(fill = 'transparent'), | |
legend.box.background = element_rect(fill = 'transparent') | |
) + | |
geom_hline(aes(yintercept = 0), linewidth = 0.25, color = "grey") + | |
scale_fill_scico(palette = "vikO", direction = -1, midpoint = 0) + | |
xlab("Tournament Round") + | |
ylab("Confirmed / Pending Payout (NMR)") | |
# Facet setting | |
if (n_model %% 5 == 0) { | |
p <- p + facet_wrap(. ~ model, ncol = 5) | |
} else { | |
p <- p + facet_wrap(. ~ model) | |
} | |
# Dynamic height adjustment | |
height <- 600 # default | |
if (n_model > 10) height = 800 | |
if (n_model > 15) height = 1000 | |
if (n_model > 20) height = 1200 | |
if (n_model > 25) height = 1400 | |
if (n_model > 30) height = 1600 | |
if (n_model > 35) height = 1800 | |
if (n_model > 40) height = 2000 | |
if (n_model > 45) height = 2200 | |
if (n_model > 50) height = 2400 | |
# Generate plotly | |
ggplotly(p, height = height, tooltip = "text") | |
}) | |
# ============================================================================ | |
# Reactive: Payout Summary Table | |
# ============================================================================ | |
output$dt_payout_summary <- DT::renderDT({ | |
# Generate a new DT | |
DT::datatable( | |
# Data | |
react_d_payout_summary(), | |
# Other Options | |
rownames = FALSE, | |
extensions = "Buttons", | |
options = | |
list( | |
dom = 'Bflrtip', # https://datatables.net/reference/option/dom | |
buttons = list('csv', 'excel', 'copy', 'print'), # https://rstudio.github.io/DT/003-tabletools-buttons.html | |
order = list(list(0, 'asc'), list(1, 'asc')), | |
pageLength = 100, | |
lengthMenu = c(5, 10, 20, 100, 500, 1000), | |
columnDefs = list(list(className = 'dt-center', targets = "_all"))) | |
) |> | |
# Reformat individual columns | |
formatRound(columns = c("total_stake", "total_payout", "rate_of_return"), digits = 2) |> | |
formatStyle(columns = c("round"), fontWeight = "bold") |> | |
formatStyle(columns = c("total_stake"), | |
fontWeight = "bold", | |
color = styleInterval(cuts = -1e-15, values = c("#D24141", "#2196F3"))) |> | |
formatStyle(columns = c("total_payout"), | |
fontWeight = "bold", | |
color = styleInterval(cuts = c(-1e-15, 1e-15), | |
values = c("#D24141", "#D1D1D1", "#00A800"))) |> | |
formatStyle(columns = c("rate_of_return"), | |
fontWeight = "bold", | |
color = styleInterval(cuts = c(-1e-15, 1e-15), | |
values = c("#D24141", "#D1D1D1", "#00A800"))) | |
}) | |
# ============================================================================ | |
# Session Info | |
# ============================================================================ | |
output$session_info <- renderPrint({ | |
sessionInfo() | |
}) | |
# ============================================================================ | |
# Trick to keep session alive | |
# https://tickets.dominodatalab.com/hc/en-us/articles/360015932932-Increasing-the-timeout-for-Shiny-Server | |
# ============================================================================ | |
output$keepAlive <- renderText({ | |
req(input$count) | |
# paste("keep alive ", input$count) | |
" " | |
}) | |
} | |
# ============================================================================== | |
# App | |
# ============================================================================== | |
shinyApp(ui, server) | |