library(shiny) library(tidyverse) library(DT) library(plotly) library(scales)
raw_data <- “Symbol,Company,Sector,PE_Current,ROE_2020,ROE_2021,ROE_2022,ROE_2023,ROE_2024,EPS_2020,EPS_2021,EPS_2022,EPS_2023,EPS_2024 AAPL,Apple Inc.,Technology,28.5,0.73,0.85,1.20,1.35,1.45,3.20,4.50,5.60,6.10,6.50 MSFT,Microsoft Corp.,Technology,35.2,0.35,0.38,0.40,0.42,0.44,5.10,6.20,8.05,9.60,11.10 AMZN,Amazon.com Inc.,Consumer Disc.,75.0,0.20,0.22,0.05,0.12,0.15,1.15,2.10,0.50,1.50,2.80 GOOGL,Alphabet Inc.,Technology,24.5,0.18,0.25,0.22,0.24,0.27,2.80,4.10,4.50,5.20,6.10 TSLA,Tesla Inc.,Consumer Disc.,68.5,0.05,0.15,0.25,0.20,0.21,0.25,0.80,1.90,2.40,3.10 JPM,JPMorgan Chase,Financials,11.5,0.12,0.15,0.14,0.16,0.17,8.50,10.10,11.50,12.80,14.50 JNJ,Johnson & Johnson,Health Care,15.8,0.22,0.24,0.23,0.25,0.26,6.50,7.10,7.50,8.10,8.50 PG,Procter & Gamble,Consumer Staples,24.5,0.28,0.29,0.30,0.31,0.32,4.80,5.10,5.40,5.90,6.40 XOM,Exxon Mobil,Energy,10.2,-0.05,0.08,0.25,0.20,0.18,-0.50,3.50,8.10,7.20,6.80 NVDA,NVIDIA Corp.,Technology,95.0,0.25,0.35,0.45,0.55,0.65,0.80,1.20,2.50,4.10,8.50 PFE,Pfizer Inc.,Health Care,12.5,0.15,0.22,0.35,0.18,0.15,2.10,3.50,5.80,2.90,2.40 KO,Coca-Cola Co.,Consumer Staples,21.5,0.35,0.38,0.40,0.42,0.45,1.80,2.05,2.20,2.50,2.80 HD,Home Depot,Consumer Disc.,22.5,0.45,0.50,0.55,0.52,0.50,10.20,11.50,13.20,14.10,15.20 BAC,Bank of America,Financials,10.8,0.08,0.11,0.12,0.11,0.10,2.10,2.80,3.20,3.10,3.30 VZ,Verizon Comm.,Communication,8.5,0.25,0.28,0.26,0.25,0.24,4.10,4.80,5.10,5.00,4.90 CSCO,Cisco Systems,Technology,13.5,0.28,0.29,0.30,0.29,0.28,2.80,3.00,3.20,3.50,3.60 INTC,Intel Corp.,Technology,85.0,0.22,0.25,0.15,0.01,-0.05,4.50,5.10,2.50,0.10,-0.50 DIS,Walt Disney,Communication,45.0,-0.02,0.02,0.05,0.08,0.12,-0.80,0.50,1.80,3.20,4.50 NFLX,Netflix Inc.,Communication,42.5,0.25,0.28,0.22,0.25,0.28,5.10,7.50,9.10,10.50,12.80 BA,Boeing Co.,Industrials,NA,-0.50,-0.40,-0.25,-0.10,-0.05,-15.00,-12.00,-8.00,-4.00,-2.50”
df <- read.csv(text = raw_data, stringsAsFactors = FALSE)
df_processed <- df %>% mutate( # Handle NA in PE PE_Current = as.numeric(PE_Current),
# 1. Calculate Average ROE (Stability Metric)
Avg_ROE = rowMeans(select(., starts_with("ROE")), na.rm = TRUE),
# 2. Calculate ROE Volatility (Standard Deviation)
SD_ROE = apply(select(., starts_with("ROE")), 1, sd, na.rm = TRUE),
# 3. Calculate EPS CAGR (Growth Metric) over 4 periods (2020 to 2024)
# Formula: (End_Value / Start_Value)^(1/n) - 1
# We handle cases where EPS is negative by simply marking them as NA or handling logic downstream
EPS_CAGR = case_when(
EPS_2020 > 0 & EPS_2024 > 0 ~ ((EPS_2024 / EPS_2020)^(1/4)) - 1,
TRUE ~ NA_real_ # Cannot calculate CAGR with negative starting/ending earnings easily
)
)
df_long <- df_processed %>% pivot_longer( cols = matches(“202[0-4]”), names_to = c(“Metric”, “Year”), names_sep = “_“, values_to =”Value” ) %>% mutate(Year = as.numeric(Year))
ui <- fluidPage( theme = bslib::bs_theme(bootswatch = “flatly”),
titlePanel(“Value Investing Screener: Finding the Compounders”),
sidebarLayout( sidebarPanel( h4(“Filter Criteria”), helpText(“Identify companies with high ROE and growing earnings.”),
sliderInput("min_roe", "Minimum Avg ROE (%):",
min = 0, max = 50, value = 15, step = 1),
sliderInput("min_cagr", "Min EPS Growth (CAGR %):",
min = -10, max = 50, value = 5, step = 1),
sliderInput("max_pe", "Maximum P/E Ratio:",
min = 0, max = 100, value = 50, step = 5),
checkboxGroupInput("sector_select", "Select Sectors:",
choices = unique(df$Sector),
selected = unique(df$Sector)),
hr(),
h5("Compounder Logic:"),
p("1. High ROE indicates efficiency."),
p("2. Positive EPS CAGR indicates growth."),
p("3. Stable ROE (low variance) indicates durability.")
),
mainPanel(
tabsetPanel(
# Tab 1: The Screener Table
tabPanel("Screener Results",
h3("Qualified Companies"),
DTOutput("screener_table")
),
# Tab 2: Visual Analysis
tabPanel("Visual Analysis",
h3("Earnings Trend (EPS)"),
plotlyOutput("eps_plot"),
br(),
h3("Efficiency Trend (ROE)"),
plotlyOutput("roe_plot")
)
)
)
) )
server <- function(input, output) {
# Reactive Data Filtering filtered_data <- reactive({ df_processed %>% filter( Avg_ROE >= (input\(min_roe / 100), (EPS_CAGR * 100) >= input\)min_cagr | is.na(EPS_CAGR), # Keep NA to show if user wants, or filter out PE_Current <= input\(max_pe | is.na(PE_Current), Sector %in% input\)sector_select ) })
# Render Data Table output$screener_table <- renderDT({ dat <- filtered_data() %>% select(Symbol, Company, Sector, PE_Current, Avg_ROE, EPS_CAGR) %>% mutate( Avg_ROE = percent(Avg_ROE, accuracy = 0.1), EPS_CAGR = percent(EPS_CAGR, accuracy = 0.1) )
datatable(dat, options = list(pageLength = 10))
})
# Render EPS Plot output$eps_plot <- renderPlotly({ req(nrow(filtered_data()) > 0)
# Filter long data based on the screened companies
plot_data <- df_long %>%
filter(Symbol %in% filtered_data()$Symbol) %>%
filter(Metric == "EPS")
p <- ggplot(plot_data, aes(x = Year, y = Value, color = Symbol, group = Symbol)) +
geom_line(size = 1) +
geom_point() +
theme_minimal() +
labs(title = "EPS Growth Trajectory (2020-2024)", y = "Earnings Per Share ($)")
ggplotly(p)
})
# Render ROE Plot output$roe_plot <- renderPlotly({ req(nrow(filtered_data()) > 0)
plot_data <- df_long %>%
filter(Symbol %in% filtered_data()$Symbol) %>%
filter(Metric == "ROE")
p <- ggplot(plot_data, aes(x = Year, y = Value, color = Symbol, group = Symbol)) +
geom_line(size = 1) +
geom_point() +
theme_minimal() +
scale_y_continuous(labels = percent) +
labs(title = "Return on Equity Stability (2020-2024)", y = "ROE")
ggplotly(p)
}) }
shinyApp(ui = ui, server = server)