cpflecht commited on
Commit
efc0048
·
verified ·
1 Parent(s): 2f443e2

Delete app.R

Browse files
Files changed (1) hide show
  1. app.R +0 -1036
app.R DELETED
@@ -1,1036 +0,0 @@
1
- # ============================================================
2
- # Oldham Athletic Player Scouting App — R Shiny Version
3
- #
4
- # Files needed in the same directory:
5
- # app.R
6
- # OA_sheet_for_app.csv
7
- # all_players_enriched_multiseason.csv (optional)
8
- #
9
- # Required R packages (install once):
10
- # install.packages(c("shiny","shinythemes","DT","dplyr","tidyr",
11
- # "readr","plotly","stringr","scales"))
12
- # ============================================================
13
-
14
- library(shiny)
15
- library(shinythemes)
16
- library(DT)
17
- library(dplyr)
18
- library(tidyr)
19
- library(readr)
20
- library(plotly)
21
- library(stringr)
22
- library(scales)
23
-
24
- # ============================================================
25
- # HELPERS
26
- # ============================================================
27
-
28
- clean_colnames <- function(df) {
29
- names(df) <- names(df) |>
30
- str_trim() |>
31
- str_to_lower() |>
32
- str_replace_all("[ \\-/]", "_") |>
33
- str_replace_all("\\.", "_")
34
- df
35
- }
36
-
37
- clean_player_key <- function(x) {
38
- x |>
39
- str_trim() |>
40
- str_to_lower() |>
41
- str_replace_all("\\.", "") |>
42
- str_replace_all(",", "") |>
43
- str_replace_all("-", " ") |>
44
- str_replace_all(" ", " ")
45
- }
46
-
47
- format_money <- function(x) {
48
- x <- suppressWarnings(as.numeric(x))
49
- ifelse(is.na(x), "Not listed",
50
- ifelse(x >= 1e6, paste0("\u20ac", round(x / 1e6, 1), "M"),
51
- ifelse(x >= 1e3, paste0("\u20ac", round(x / 1e3, 0), "K"),
52
- paste0("\u20ac", round(x, 0)))))
53
- }
54
-
55
- clean_value <- function(x) {
56
- if (is.null(x) || length(x) == 0 || (length(x) == 1 && is.na(x))) return("N/A")
57
- if (is.numeric(x)) return(round(x, 2))
58
- as.character(x)
59
- }
60
-
61
- pretty_label <- function(col) {
62
- custom <- c(
63
- player_name = "Player", team_name = "Club", competition_name = "Competition",
64
- season_name = "Season", primary_position = "Primary Position",
65
- secondary_position = "Secondary Position", country_id = "Country",
66
- player_height = "Height", player_weight = "Weight",
67
- player_season_minutes = "Minutes", market_value_eur = "Market Value",
68
- seasons_left_num = "Seasons Left", attainability = "Attainability",
69
- target_score = "Target Score", best_position_archetype_name = "Best Archetype",
70
- best_position_archetype_score = "Best Archetype Score",
71
- cb_score = "CB Score", fb_score = "FB Score", cmd_score = "CMD Score",
72
- cma_score = "CMA Score", wm_score = "WM Score", cf_score = "CF Score",
73
- st_score = "ST Score", gk_score = "GK Score",
74
- club_rank = "Club Rank", match_toughness = "Match Toughness",
75
- elo = "Club ELO", competition_rank = "Competition Rank", fit_score = "Fit Score"
76
- )
77
- if (col %in% names(custom)) return(unname(custom[col]))
78
- lbl <- col |>
79
- str_replace("player_season_", "") |>
80
- str_replace("attr_", "") |>
81
- str_replace("cat_", "") |>
82
- str_replace("score_", "") |>
83
- str_replace_all("_90$", " Per 90") |>
84
- str_replace_all("_", " ") |>
85
- str_to_title() |>
86
- str_replace("Np Xg", "NP xG") |>
87
- str_replace("Xa ", "xA ") |>
88
- str_replace("Xgchain", "xGChain") |>
89
- str_replace("Xgbuildup", "xGBuildup") |>
90
- str_replace("Obv", "OBV") |>
91
- str_replace("\\bGk\\b", "GK") |>
92
- str_replace("\\bCb\\b", "CB") |>
93
- str_replace("\\bFb\\b", "FB") |>
94
- str_replace("\\bCmd\\b", "CMD") |>
95
- str_replace("\\bCma\\b", "CMA") |>
96
- str_replace("\\bWm\\b", "WM") |>
97
- str_replace("\\bCf\\b", "CF") |>
98
- str_replace("\\bSt\\b", "ST")
99
- lbl
100
- }
101
-
102
- available_cols <- function(cols, df) {
103
- cols[cols %in% names(df)]
104
- }
105
-
106
- normalize_0_100 <- function(x) {
107
- x <- suppressWarnings(as.numeric(x))
108
- mn <- min(x, na.rm = TRUE)
109
- mx <- max(x, na.rm = TRUE)
110
- if (is.na(mn) || is.na(mx) || mn == mx) return(rep(0, length(x)))
111
- (x - mn) / (mx - mn) * 100
112
- }
113
-
114
- pretty_df <- function(data, df_source) {
115
- out <- data
116
- if ("market_value_eur" %in% names(out)) {
117
- out$market_value_eur <- format_money(out$market_value_eur)
118
- }
119
- num_cols <- names(out)[sapply(out, is.numeric)]
120
- out[num_cols] <- lapply(out[num_cols], function(x) round(x, 2))
121
- names(out) <- sapply(names(out), pretty_label)
122
- out
123
- }
124
-
125
- # ============================================================
126
- # COLUMN CONSTANTS
127
- # ============================================================
128
-
129
- PLAYER_COL <- "player_name"
130
- TEAM_COL <- "team_name"
131
- COMP_COL <- "competition_name"
132
- SEASON_COL <- "season_name"
133
- POSITION_COL <- "primary_position"
134
- SECONDARY_POSITION_COL<- "secondary_position"
135
- COUNTRY_COL <- "country_id"
136
- AGE_COL <- "age"
137
- HEIGHT_COL <- "player_height"
138
- WEIGHT_COL <- "player_weight"
139
- MINUTES_COL <- "player_season_minutes"
140
- MARKET_VALUE_COL <- "market_value_eur"
141
- CONTRACT_COL <- "seasons_left_num"
142
- ATTAINABILITY_COL <- "attainability"
143
- TARGET_SCORE_COL <- "target_score"
144
- ARCHETYPE_COL <- "best_position_archetype_name"
145
- ARCHETYPE_SCORE_COL <- "best_position_archetype_score"
146
- CLUB_RANK_COL <- "club_rank"
147
- MATCH_TOUGHNESS_COL <- "match_toughness"
148
- ELO_COL <- "elo"
149
- COMPETITION_RANK_COL <- "competition_rank"
150
-
151
- ATTR_COLS <- c(
152
- "attr_shot_stopping","attr_sweeping","attr_ball_claiming","attr_short_passing",
153
- "attr_long_passing","attr_pressing","attr_duels","attr_aerial",
154
- "attr_possession_retention","attr_blocking","attr_progression","attr_set_pieces",
155
- "attr_impact","attr_discipline","attr_dribbling","attr_chance_creation",
156
- "attr_finishing","attr_crossing","attr_box_presence","attr_holdup"
157
- )
158
-
159
- POSITION_SCORE_COLS <- c(
160
- "cb_score","fb_score","cmd_score","cma_score","wm_score","cf_score","st_score","gk_score"
161
- )
162
-
163
- ARCHETYPE_SCORE_COLS <- c(
164
- "score_defensive_cb","score_pressing_cb","score_ballplaying_cb",
165
- "score_defensive_fb","score_attacking_fb","score_possession_fb",
166
- "score_poacher","score_target_man","score_false_nine","score_complete_forward",
167
- "score_inside_forward","score_traditional_winger","score_playmaking_winger",
168
- "score_pressing_winger","score_complete_winger","score_defensive_midfielder",
169
- "score_deep_lying_playmaker","score_box_to_box_midfielder","score_advanced_playmaker",
170
- "score_wide_midfielder","score_attacking_runner","score_shot_stopper_gk",
171
- "score_sweeper_keeper_gk","score_ball_playing_gk"
172
- )
173
-
174
- KEY_METRICS <- c(
175
- "player_season_minutes","player_season_goals_90","player_season_assists_90",
176
- "player_season_np_xg_90","player_season_xa_90","player_season_key_passes_90",
177
- "player_season_passing_ratio","player_season_tackles_90","player_season_interceptions_90",
178
- "player_season_tackles_and_interceptions_90","player_season_aerial_wins_90",
179
- "player_season_aerial_ratio","player_season_dribbles_90","player_season_crosses_90",
180
- "player_season_long_balls_90","player_season_xgchain_90","player_season_xgbuildup_90",
181
- "player_season_obv_90"
182
- )
183
-
184
- SEARCH_TABLE_COLS <- c(
185
- PLAYER_COL, POSITION_COL, TEAM_COL, COMP_COL, AGE_COL, COUNTRY_COL,
186
- MINUTES_COL, MARKET_VALUE_COL, CONTRACT_COL, ARCHETYPE_COL, ARCHETYPE_SCORE_COL,
187
- TARGET_SCORE_COL, ATTAINABILITY_COL, POSITION_SCORE_COLS, ATTR_COLS
188
- )
189
-
190
- COMPARISON_COLS <- c(
191
- PLAYER_COL, POSITION_COL, TEAM_COL, COMP_COL, AGE_COL,
192
- MARKET_VALUE_COL, CONTRACT_COL, ARCHETYPE_COL, ARCHETYPE_SCORE_COL,
193
- TARGET_SCORE_COL, ATTAINABILITY_COL, POSITION_SCORE_COLS, ATTR_COLS
194
- )
195
-
196
- SHORTLIST_COLS <- c(
197
- PLAYER_COL, POSITION_COL, TEAM_COL, COMP_COL, AGE_COL,
198
- MINUTES_COL, MARKET_VALUE_COL, CONTRACT_COL, ARCHETYPE_COL, ARCHETYPE_SCORE_COL,
199
- TARGET_SCORE_COL, ATTAINABILITY_COL, KEY_METRICS, ATTR_COLS,
200
- POSITION_SCORE_COLS, ARCHETYPE_SCORE_COLS
201
- )
202
-
203
- RADAR_METRICS <- ATTR_COLS
204
- PERCENTILE_METRICS <- c(ATTR_COLS, TARGET_SCORE_COL, ATTAINABILITY_COL, ARCHETYPE_SCORE_COL)
205
- SIMILARITY_METRICS <- c(ATTR_COLS, TARGET_SCORE_COL, ATTAINABILITY_COL, ARCHETYPE_SCORE_COL)
206
- PERFORMANCE_TIME_METRICS <- POSITION_SCORE_COLS
207
-
208
- HISTORICAL_SEASONS <- c("2122" = "2021-22", "2223" = "2022-23",
209
- "2324" = "2023-24", "2425" = "2024-25")
210
- CURRENT_MAIN_SEASON_LABEL <- "2025-26"
211
-
212
- # ============================================================
213
- # LOAD DATA
214
- # ============================================================
215
-
216
- load_data <- function() {
217
- df <- read_csv("OA_sheet_for_app.csv", locale = locale(encoding = "latin1"),
218
- show_col_types = FALSE) |> clean_colnames()
219
-
220
- multi_df <- if (file.exists("all_players_enriched_multiseason.csv")) {
221
- read_csv("all_players_enriched_multiseason.csv",
222
- locale = locale(encoding = "latin1"), show_col_types = FALSE) |>
223
- clean_colnames()
224
- } else {
225
- data.frame()
226
- }
227
-
228
- if (PLAYER_COL %in% names(df)) {
229
- df$`_player_key` <- clean_player_key(df[[PLAYER_COL]])
230
- }
231
-
232
- multi_player_col <- NULL
233
- if (nrow(multi_df) > 0) {
234
- for (pc in c("player_name", "player", "name")) {
235
- if (pc %in% names(multi_df)) { multi_player_col <- pc; break }
236
- }
237
- if (!is.null(multi_player_col)) {
238
- multi_df$`_player_key` <- clean_player_key(multi_df[[multi_player_col]])
239
- } else {
240
- multi_df$`_player_key` <- ""
241
- }
242
-
243
- hist_suffixes <- c("_2122", "_2223", "_2324", "_2425")
244
- hist_cols <- names(multi_df)[sapply(names(multi_df),
245
- function(c) any(str_ends(c, hist_suffixes)))]
246
-
247
- if (length(hist_cols) > 0 && "_player_key" %in% names(multi_df)) {
248
- multi_keep <- multi_df |>
249
- select(all_of(c("_player_key", hist_cols))) |>
250
- distinct(`_player_key`, .keep_all = TRUE)
251
-
252
- overlap <- hist_cols[hist_cols %in% names(df)]
253
- if (length(overlap) > 0) df <- df |> select(-all_of(overlap))
254
-
255
- df <- left_join(df, multi_keep, by = "_player_key")
256
- }
257
- }
258
-
259
- # Coerce numeric columns
260
- num_cols_to_coerce <- available_cols(c(
261
- KEY_METRICS, ATTR_COLS, POSITION_SCORE_COLS, ARCHETYPE_SCORE_COLS,
262
- AGE_COL, HEIGHT_COL, WEIGHT_COL, MINUTES_COL, MARKET_VALUE_COL,
263
- ATTAINABILITY_COL, TARGET_SCORE_COL, ARCHETYPE_SCORE_COL,
264
- CLUB_RANK_COL, MATCH_TOUGHNESS_COL, ELO_COL, COMPETITION_RANK_COL
265
- ), df)
266
-
267
- hist_num_cols <- names(df)[sapply(names(df), function(c)
268
- any(str_ends(c, paste0("_", names(HISTORICAL_SEASONS)))))]
269
-
270
- for (col in unique(c(num_cols_to_coerce, hist_num_cols))) {
271
- df[[col]] <- suppressWarnings(as.numeric(df[[col]]))
272
- }
273
-
274
- list(df = df, multi_df = multi_df, multi_player_col = multi_player_col)
275
- }
276
-
277
- # ============================================================
278
- # PLAYER HELPERS
279
- # ============================================================
280
-
281
- get_player_row <- function(df, player) {
282
- if (is.null(player) || player == "" || !PLAYER_COL %in% names(df)) return(NULL)
283
- rows <- df[as.character(df[[PLAYER_COL]]) == as.character(player), ]
284
- if (nrow(rows) == 0) return(NULL)
285
- as.list(rows[1, ])
286
- }
287
-
288
- get_player_group <- function(df, row) {
289
- comp <- row[[COMP_COL]]
290
- pos <- row[[POSITION_COL]]
291
- group <- df
292
- if (COMP_COL %in% names(df) && POSITION_COL %in% names(df) &&
293
- !is.null(comp) && !is.na(comp) && !is.null(pos) && !is.na(pos)) {
294
- group <- df[df[[COMP_COL]] == comp & df[[POSITION_COL]] == pos, ]
295
- }
296
- if (nrow(group) == 0) group <- df
297
- group
298
- }
299
-
300
- top_attr_cols <- function(df, row = NULL, max_cols = 8) {
301
- cols <- available_cols(RADAR_METRICS, df)
302
- if (!is.null(row)) {
303
- cols <- cols[sapply(cols, function(c) {
304
- v <- row[[c]]
305
- !is.null(v) && !is.na(v)
306
- })]
307
- }
308
- head(cols, max_cols)
309
- }
310
-
311
- # ============================================================
312
- # PERFORMANCE OVER TIME HELPERS
313
- # ============================================================
314
-
315
- historical_candidate_columns <- function(base_metric, season_code) {
316
- short_metric <- str_replace(base_metric, "player_season_", "")
317
- candidates <- c(
318
- paste0(base_metric, "_", season_code),
319
- paste0(short_metric, "_", season_code)
320
- )
321
- if (str_ends(short_metric, "_90")) {
322
- no_90 <- str_replace(short_metric, "_90$", "")
323
- candidates <- c(candidates,
324
- paste0(no_90, "_90_", season_code),
325
- paste0(no_90, "_per_90_", season_code),
326
- paste0(no_90, "_p90_", season_code))
327
- }
328
- plurals <- c(crosses = "cross", goals = "goal", assists = "assist",
329
- dribbles = "dribble", tackles = "tackle", interceptions = "interception",
330
- aerial_wins = "aerial_win", key_passes = "key_pass", long_balls = "long_ball")
331
- for (pl in names(plurals)) {
332
- sg <- plurals[pl]
333
- if (str_detect(short_metric, pl)) {
334
- replaced <- str_replace(short_metric, pl, sg)
335
- candidates <- c(candidates, paste0(replaced, "_", season_code))
336
- if (str_ends(short_metric, "_90")) {
337
- candidates <- c(candidates,
338
- paste0(str_replace(replaced, "_90$", ""), "_per_90_", season_code))
339
- }
340
- }
341
- }
342
- if (base_metric %in% POSITION_SCORE_COLS) {
343
- pos_code <- str_replace(base_metric, "_score", "")
344
- candidates <- c(candidates,
345
- paste0(pos_code, "_score_", season_code),
346
- paste0(pos_code, "_", season_code))
347
- }
348
- unique(str_to_lower(candidates))
349
- }
350
-
351
- find_metric_value <- function(row, base_metric, season_code = NULL) {
352
- if (is.null(row)) return(NA_real_)
353
- if (is.null(season_code)) {
354
- v <- row[[base_metric]]
355
- return(if (is.null(v)) NA_real_ else suppressWarnings(as.numeric(v)))
356
- }
357
- for (col in historical_candidate_columns(base_metric, season_code)) {
358
- v <- row[[col]]
359
- if (!is.null(v) && !is.na(v)) return(suppressWarnings(as.numeric(v)))
360
- }
361
- NA_real_
362
- }
363
-
364
- get_multiseason_row <- function(multi_df, player) {
365
- if (is.null(multi_df) || nrow(multi_df) == 0) return(NULL)
366
- pk <- clean_player_key(player)
367
- if (!"_player_key" %in% names(multi_df)) return(NULL)
368
- matches <- multi_df[multi_df$`_player_key` == pk, ]
369
- if (nrow(matches) == 0) return(NULL)
370
- as.list(matches[1, ])
371
- }
372
-
373
- build_performance_metric_options <- function(df, multi_df) {
374
- options <- c()
375
- for (m in PERFORMANCE_TIME_METRICS) {
376
- current_exists <- m %in% names(df)
377
- hist_exists <- FALSE
378
- for (sc in names(HISTORICAL_SEASONS)) {
379
- for (cand in historical_candidate_columns(m, sc)) {
380
- if (cand %in% names(df) || (!is.null(multi_df) && nrow(multi_df) > 0 && cand %in% names(multi_df))) {
381
- hist_exists <- TRUE; break
382
- }
383
- }
384
- if (hist_exists) break
385
- }
386
- if (current_exists || hist_exists) options[pretty_label(m)] <- m
387
- }
388
- options
389
- }
390
-
391
- # ============================================================
392
- # UI
393
- # ============================================================
394
-
395
- ui <- fluidPage(
396
- theme = shinytheme("flatly"),
397
- tags$head(tags$style(HTML("
398
- .container-fluid { max-width: 98%; }
399
- table.dataTable { width: 100% !important; }
400
- .dataTables_wrapper { overflow-x: auto; }
401
- th, td { white-space: nowrap; }
402
- .section-header { border-bottom: 2px solid #2c3e50; margin-bottom: 15px; padding-bottom: 6px; }
403
- "))),
404
-
405
- titlePanel("Oldham Athletic Player Scouting"),
406
-
407
- tabsetPanel(id = "main_tabs",
408
-
409
- # ---- PLAYER SEARCH ----
410
- tabPanel("Player Search",
411
- br(),
412
- h4("Search and Filter Players", class = "section-header"),
413
- fluidRow(
414
- column(4, textInput("search_box", "Search Player Name", "")),
415
- column(4, selectizeInput("competition_filter", "Competition",
416
- choices = NULL, multiple = TRUE)),
417
- column(4, selectizeInput("team_filter", "Team",
418
- choices = NULL, multiple = TRUE))
419
- ),
420
- fluidRow(
421
- column(4, selectizeInput("position_filter", "Position",
422
- choices = NULL, multiple = TRUE)),
423
- column(4, selectizeInput("country_filter", "Country",
424
- choices = NULL, multiple = TRUE))
425
- ),
426
- fluidRow(
427
- column(4, uiOutput("age_slider_ui")),
428
- column(4, uiOutput("minutes_slider_ui"))
429
- ),
430
- br(),
431
- actionButton("search_btn", "Search Players", class = "btn-primary"),
432
- br(), br(),
433
- DTOutput("search_results"),
434
- textOutput("search_status")
435
- ),
436
-
437
- # ---- PLAYER PROFILE ----
438
- tabPanel("Player Profile",
439
- br(),
440
- h4("Full Player Profile", class = "section-header"),
441
- selectizeInput("selected_player", "Select Player", choices = NULL, width = "100%"),
442
- fluidRow(
443
- column(8, uiOutput("profile_output")),
444
- column(4,
445
- h5("Key Performance Summary"),
446
- DTOutput("key_summary")
447
- )
448
- ),
449
- br(),
450
- h4("Player Metrics", class = "section-header"),
451
- selectInput("metric_group", "Metric Group",
452
- choices = c("Attributes", "Position Scores", "Archetype Scores", "Key Season Stats"),
453
- selected = "Attributes"),
454
- DTOutput("metric_table"),
455
- br(),
456
- fluidRow(
457
- column(6, plotlyOutput("radar_plot", height = "500px")),
458
- column(6, plotlyOutput("percentile_plot", height = "500px"))
459
- ),
460
- br(),
461
- fluidRow(
462
- column(6, selectInput("profile_metric", "Performance Metric Over Time",
463
- choices = NULL)),
464
- column(6, actionButton("trend_btn", "Show Performance Chart", class = "btn-info"))
465
- ),
466
- plotlyOutput("trend_plot"),
467
- br(),
468
- textAreaInput("scout_notes", "Scout Notes", rows = 4,
469
- placeholder = "Enter notes to include in the scouting report."),
470
- fluidRow(
471
- column(3, downloadButton("report_btn", "Download Scouting Report (CSV)")),
472
- column(3, actionButton("shortlist_btn", "Add Player to Shortlist", class = "btn-success"))
473
- ),
474
- br(),
475
- DTOutput("shortlist_from_profile")
476
- ),
477
-
478
- # ---- PLAYER COMPARISON ----
479
- tabPanel("Player Comparison Tool",
480
- br(),
481
- h4("Compare Up To Three Players", class = "section-header"),
482
- fluidRow(
483
- column(4, selectizeInput("compare_1", "Player 1", choices = NULL)),
484
- column(4, selectizeInput("compare_2", "Player 2", choices = NULL)),
485
- column(4, selectizeInput("compare_3", "Player 3", choices = NULL))
486
- ),
487
- actionButton("compare_btn", "Compare Players", class = "btn-primary"),
488
- br(), br(),
489
- DTOutput("comparison_table"),
490
- br(),
491
- plotlyOutput("comparison_radar", height = "550px")
492
- ),
493
-
494
- # ---- FIT SCORE ----
495
- tabPanel("Fit Score Calculator",
496
- br(),
497
- h4("Fit Score Calculator", class = "section-header"),
498
- p("Select competitions and positions, then adjust trait weights to generate ranked recommendations."),
499
- fluidRow(
500
- column(6, selectizeInput("fit_competition_filter", "Competitions to Search",
501
- choices = NULL, multiple = TRUE)),
502
- column(6, selectizeInput("fit_position_filter", "Positions to Search",
503
- choices = NULL, multiple = TRUE))
504
- ),
505
- fluidRow(
506
- column(4, sliderInput("pressing_w", "Pressing", 0, 10, 5, step = 1)),
507
- column(4, sliderInput("duels_w", "Duels", 0, 10, 5, step = 1)),
508
- column(4, sliderInput("aerial_w", "Aerial", 0, 10, 4, step = 1))
509
- ),
510
- fluidRow(
511
- column(4, sliderInput("possession_w", "Possession Retention", 0, 10, 5, step = 1)),
512
- column(4, sliderInput("blocking_w", "Blocking", 0, 10, 4, step = 1)),
513
- column(4, sliderInput("progression_w", "Progression", 0, 10, 6, step = 1))
514
- ),
515
- fluidRow(
516
- column(4, sliderInput("impact_w", "Impact", 0, 10, 6, step = 1)),
517
- column(4, sliderInput("discipline_w", "Discipline", 0, 10, 3, step = 1)),
518
- column(4, sliderInput("dribbling_w", "Dribbling", 0, 10, 4, step = 1))
519
- ),
520
- fluidRow(
521
- column(4, sliderInput("chance_w", "Chance Creation", 0, 10, 5, step = 1)),
522
- column(4, sliderInput("finishing_w", "Finishing", 0, 10, 3, step = 1)),
523
- column(4, sliderInput("crossing_w", "Crossing", 0, 10, 3, step = 1))
524
- ),
525
- fluidRow(
526
- column(4, sliderInput("box_w", "Box Presence", 0, 10, 3, step = 1)),
527
- column(4, sliderInput("holdup_w", "Holdup", 0, 10, 3, step = 1)),
528
- column(4, sliderInput("target_w", "Target Score", 0, 10, 7, step = 1))
529
- ),
530
- fluidRow(
531
- column(4, sliderInput("attain_w", "Attainability", 0, 10, 6, step = 1))
532
- ),
533
- actionButton("fit_btn", "Generate Ranked Recommendations", class = "btn-primary"),
534
- br(), br(),
535
- DTOutput("fit_table")
536
- ),
537
-
538
- # ---- SIMILAR PLAYERS ----
539
- tabPanel("Similar Player Finder",
540
- br(),
541
- h4("Find Similar Players", class = "section-header"),
542
- selectizeInput("similar_player_select", "Select Player", choices = NULL, width = "60%"),
543
- actionButton("similar_btn", "Find Similar Players", class = "btn-primary"),
544
- br(), br(),
545
- DTOutput("similar_table")
546
- ),
547
-
548
- # ---- SHORTLIST ----
549
- tabPanel("Shortlist Manager",
550
- br(),
551
- h4("Shortlist Manager", class = "section-header"),
552
- fluidRow(
553
- column(4, selectizeInput("shortlist_player", "Add Player", choices = NULL)),
554
- column(2, br(), actionButton("add_shortlist_btn", "Add to Shortlist", class = "btn-success")),
555
- column(2, br(), actionButton("clear_shortlist_btn", "Clear Shortlist", class = "btn-danger")),
556
- column(2, br(), downloadButton("export_shortlist_btn", "Export CSV"))
557
- ),
558
- br(),
559
- DTOutput("shortlist_table")
560
- )
561
- )
562
- )
563
-
564
- # ============================================================
565
- # SERVER
566
- # ============================================================
567
-
568
- server <- function(input, output, session) {
569
-
570
- # --- Load data once ---
571
- app_data <- tryCatch(load_data(), error = function(e) {
572
- showNotification(paste("Error loading data:", e$message), type = "error", duration = NULL)
573
- list(df = data.frame(), multi_df = data.frame(), multi_player_col = NULL)
574
- })
575
-
576
- df <- app_data$df
577
- multi_df <- app_data$multi_df
578
-
579
- # Session shortlist
580
- shortlist <- reactiveVal(character(0))
581
-
582
- # --- Populate dropdowns on startup ---
583
- observe({
584
- req(nrow(df) > 0)
585
-
586
- comp_opts <- if (COMP_COL %in% names(df)) sort(unique(na.omit(as.character(df[[COMP_COL]])))) else character(0)
587
- team_opts <- if (TEAM_COL %in% names(df)) sort(unique(na.omit(as.character(df[[TEAM_COL]])))) else character(0)
588
- pos_opts <- if (POSITION_COL %in% names(df)) sort(unique(na.omit(as.character(df[[POSITION_COL]])))) else character(0)
589
- country_opts <- if (COUNTRY_COL %in% names(df)) sort(unique(na.omit(as.character(df[[COUNTRY_COL]])))) else character(0)
590
-
591
- # Build named player choices: "Name | Position | Team" -> player_name
592
- player_rows <- df[, available_cols(c(PLAYER_COL, POSITION_COL, TEAM_COL), df), drop = FALSE] |> distinct()
593
- player_choices <- setNames(
594
- as.character(player_rows[[PLAYER_COL]]),
595
- paste0(player_rows[[PLAYER_COL]], " | ",
596
- if (POSITION_COL %in% names(player_rows)) player_rows[[POSITION_COL]] else "",
597
- " | ",
598
- if (TEAM_COL %in% names(player_rows)) player_rows[[TEAM_COL]] else "")
599
- )
600
- player_choices <- player_choices[order(names(player_choices))]
601
-
602
- perf_opts <- build_performance_metric_options(df, multi_df)
603
-
604
- updateSelectizeInput(session, "competition_filter", choices = comp_opts, server = TRUE)
605
- updateSelectizeInput(session, "team_filter", choices = team_opts, server = TRUE)
606
- updateSelectizeInput(session, "position_filter", choices = pos_opts, server = TRUE)
607
- updateSelectizeInput(session, "country_filter", choices = country_opts, server = TRUE)
608
- updateSelectizeInput(session, "selected_player", choices = player_choices, server = TRUE)
609
- updateSelectizeInput(session, "compare_1", choices = c("" = "", player_choices), server = TRUE)
610
- updateSelectizeInput(session, "compare_2", choices = c("" = "", player_choices), server = TRUE)
611
- updateSelectizeInput(session, "compare_3", choices = c("" = "", player_choices), server = TRUE)
612
- updateSelectizeInput(session, "fit_competition_filter", choices = comp_opts, server = TRUE)
613
- updateSelectizeInput(session, "fit_position_filter", choices = pos_opts, server = TRUE)
614
- updateSelectizeInput(session, "similar_player_select", choices = player_choices, server = TRUE)
615
- updateSelectizeInput(session, "shortlist_player", choices = player_choices, server = TRUE)
616
- updateSelectInput(session, "profile_metric", choices = perf_opts)
617
- })
618
-
619
- # --- Dynamic sliders ---
620
- output$age_slider_ui <- renderUI({
621
- age_min <- if (AGE_COL %in% names(df)) floor(min(df[[AGE_COL]], na.rm = TRUE)) else 15
622
- age_max <- if (AGE_COL %in% names(df)) ceiling(max(df[[AGE_COL]], na.rm = TRUE)) else 45
623
- tagList(
624
- sliderInput("min_age_filter", "Minimum Age", age_min, age_max, age_min, step = 1),
625
- sliderInput("max_age_filter", "Maximum Age", age_min, age_max, age_max, step = 1)
626
- )
627
- })
628
-
629
- output$minutes_slider_ui <- renderUI({
630
- min_max <- if (MINUTES_COL %in% names(df)) ceiling(max(df[[MINUTES_COL]], na.rm = TRUE)) else 5000
631
- sliderInput("minutes_filter", "Minimum Minutes", 0, min_max, 0, step = 100)
632
- })
633
-
634
- # ---- SEARCH ----
635
- search_result_df <- eventReactive(input$search_btn, {
636
- data <- df
637
-
638
- if (!is.null(input$search_box) && nchar(input$search_box) > 0 && PLAYER_COL %in% names(data)) {
639
- data <- data[str_detect(as.character(data[[PLAYER_COL]]),
640
- regex(input$search_box, ignore_case = TRUE)), ]
641
- }
642
- if (length(input$competition_filter) > 0 && COMP_COL %in% names(data))
643
- data <- data[data[[COMP_COL]] %in% input$competition_filter, ]
644
- if (length(input$team_filter) > 0 && TEAM_COL %in% names(data))
645
- data <- data[data[[TEAM_COL]] %in% input$team_filter, ]
646
- if (length(input$position_filter) > 0 && POSITION_COL %in% names(data))
647
- data <- data[data[[POSITION_COL]] %in% input$position_filter, ]
648
- if (length(input$country_filter) > 0 && COUNTRY_COL %in% names(data))
649
- data <- data[data[[COUNTRY_COL]] %in% input$country_filter, ]
650
-
651
- min_age <- if (!is.null(input$min_age_filter)) input$min_age_filter else -Inf
652
- max_age <- if (!is.null(input$max_age_filter)) input$max_age_filter else Inf
653
- if (AGE_COL %in% names(data))
654
- data <- data[!is.na(data[[AGE_COL]]) &
655
- data[[AGE_COL]] >= min_age & data[[AGE_COL]] <= max_age, ]
656
-
657
- min_min <- if (!is.null(input$minutes_filter)) input$minutes_filter else 0
658
- if (MINUTES_COL %in% names(data))
659
- data <- data[!is.na(data[[MINUTES_COL]]) & data[[MINUTES_COL]] >= min_min, ]
660
-
661
- cols <- available_cols(SEARCH_TABLE_COLS, data)
662
- out <- data[, cols, drop = FALSE]
663
- if (nrow(out) == 0) return(data.frame(Message = "No players found."))
664
-
665
- sort_col <- if (TARGET_SCORE_COL %in% names(out)) TARGET_SCORE_COL else ATTAINABILITY_COL
666
- if (sort_col %in% names(out))
667
- out <- out[order(-out[[sort_col]], na.last = TRUE), ]
668
-
669
- pretty_df(out, df)
670
- })
671
-
672
- output$search_results <- renderDT({
673
- req(search_result_df())
674
- datatable(search_result_df(), selection = "single", rownames = FALSE,
675
- options = list(scrollX = TRUE, pageLength = 25))
676
- })
677
-
678
- output$search_status <- renderText({
679
- sel <- input$search_results_rows_selected
680
- if (!is.null(sel) && length(sel) > 0) {
681
- player <- search_result_df()[sel, "Player"]
682
- updateSelectizeInput(session, "selected_player", selected = player)
683
- paste("Loaded", player, "into the Player Profile tab.")
684
- } else {
685
- "Click a player row to load them into the Player Profile tab."
686
- }
687
- })
688
-
689
- # ---- PLAYER PROFILE ----
690
- current_player_row <- reactive({
691
- get_player_row(df, input$selected_player)
692
- })
693
-
694
- output$profile_output <- renderUI({
695
- row <- current_player_row()
696
- if (is.null(row)) return(p("Select a player to view their profile."))
697
- tagList(
698
- h2(row[[PLAYER_COL]]),
699
- h4(paste0(row[[TEAM_COL]], " | ", row[[COMP_COL]])),
700
- h4("Player Details"),
701
- tags$ul(
702
- tags$li(strong("Primary Position: "), clean_value(row[[POSITION_COL]])),
703
- tags$li(strong("Secondary Position: "), clean_value(row[[SECONDARY_POSITION_COL]])),
704
- tags$li(strong("Age: "), clean_value(row[[AGE_COL]])),
705
- tags$li(strong("Country: "), clean_value(row[[COUNTRY_COL]])),
706
- tags$li(strong("Height: "), paste0(clean_value(row[[HEIGHT_COL]]), " cm")),
707
- tags$li(strong("Weight: "), paste0(clean_value(row[[WEIGHT_COL]]), " kg")),
708
- tags$li(strong("Market Value: "), format_money(row[[MARKET_VALUE_COL]])),
709
- tags$li(strong("Contract: "), clean_value(row[[CONTRACT_COL]])),
710
- tags$li(strong("Minutes: "), clean_value(row[[MINUTES_COL]]))
711
- )
712
- )
713
- })
714
-
715
- output$key_summary <- renderDT({
716
- row <- current_player_row()
717
- if (is.null(row)) return(datatable(data.frame(Metric = "Select a player", Value = "")))
718
- out <- data.frame(
719
- Metric = c("Best Archetype","Best Archetype Score","Target Score",
720
- "Attainability","Club Rank","Match Toughness","Club ELO"),
721
- Value = c(clean_value(row[[ARCHETYPE_COL]]),
722
- clean_value(row[[ARCHETYPE_SCORE_COL]]),
723
- clean_value(row[[TARGET_SCORE_COL]]),
724
- clean_value(row[[ATTAINABILITY_COL]]),
725
- clean_value(row[[CLUB_RANK_COL]]),
726
- clean_value(row[[MATCH_TOUGHNESS_COL]]),
727
- clean_value(row[[ELO_COL]]))
728
- )
729
- datatable(out, rownames = FALSE, options = list(dom = "t", paging = FALSE))
730
- })
731
-
732
- output$metric_table <- renderDT({
733
- row <- current_player_row()
734
- if (is.null(row)) return(datatable(data.frame(Metric = "Select a player", Score = "")))
735
- cols <- switch(input$metric_group,
736
- "Attributes" = ATTR_COLS,
737
- "Position Scores" = POSITION_SCORE_COLS,
738
- "Archetype Scores" = ARCHETYPE_SCORE_COLS,
739
- "Key Season Stats" = KEY_METRICS,
740
- ATTR_COLS
741
- )
742
- cols <- available_cols(cols, df)
743
- rows_data <- lapply(cols, function(c) {
744
- v <- row[[c]]
745
- if (!is.null(v) && !is.na(v))
746
- data.frame(Metric = pretty_label(c),
747
- Score = round(as.numeric(v), 2))
748
- })
749
- out <- do.call(rbind, Filter(Negate(is.null), rows_data))
750
- if (is.null(out)) out <- data.frame(Metric = "No metrics available", Score = NA)
751
- out <- out[order(-out$Score, na.last = TRUE), ]
752
- datatable(out, rownames = FALSE, options = list(scrollX = TRUE, pageLength = 25))
753
- })
754
-
755
- output$radar_plot <- renderPlotly({
756
- row <- current_player_row()
757
- if (is.null(row)) return(plot_ly() |> layout(title = "Select a player"))
758
- metrics <- top_attr_cols(df, row, max_cols = 8)
759
- if (length(metrics) < 3) return(plot_ly() |> layout(title = "Not enough attributes"))
760
- group <- get_player_group(df, row)
761
- labels <- sapply(metrics, pretty_label)
762
- player_vals <- sapply(metrics, function(m) {
763
- v <- row[[m]]; if (is.null(v) || is.na(v)) 0 else as.numeric(v)
764
- })
765
- avg_vals <- sapply(metrics, function(m) {
766
- if (m %in% names(group)) mean(group[[m]], na.rm = TRUE) else 0
767
- })
768
- max_val <- max(100, max(player_vals, avg_vals, na.rm = TRUE) * 1.1, na.rm = TRUE)
769
- plot_ly(type = "scatterpolar", fill = "toself") |>
770
- add_trace(r = c(player_vals, player_vals[1]),
771
- theta = c(labels, labels[1]),
772
- name = as.character(input$selected_player)) |>
773
- add_trace(r = c(avg_vals, avg_vals[1]),
774
- theta = c(labels, labels[1]),
775
- name = "Position/Competition Avg") |>
776
- layout(title = paste(input$selected_player, "Attribute Radar"),
777
- polar = list(radialaxis = list(range = c(0, max_val))),
778
- legend = list(orientation = "h"))
779
- })
780
-
781
- output$percentile_plot <- renderPlotly({
782
- row <- current_player_row()
783
- if (is.null(row)) return(plot_ly() |> layout(title = "Select a player"))
784
- group <- get_player_group(df, row)
785
- rows_data <- lapply(available_cols(PERCENTILE_METRICS, df), function(m) {
786
- v <- suppressWarnings(as.numeric(row[[m]]))
787
- vals <- suppressWarnings(as.numeric(group[[m]]))
788
- vals <- vals[!is.na(vals)]
789
- if (!is.null(v) && !is.na(v) && length(vals) > 1) {
790
- pct <- mean(vals < v, na.rm = TRUE) * 100
791
- data.frame(Metric = pretty_label(m), Percentile = round(pct, 1))
792
- }
793
- })
794
- plot_df <- do.call(rbind, Filter(Negate(is.null), rows_data))
795
- if (is.null(plot_df) || nrow(plot_df) == 0)
796
- return(plot_ly() |> layout(title = "No percentile data"))
797
- plot_df <- plot_df[order(plot_df$Percentile), ]
798
- plot_ly(plot_df, x = ~Percentile, y = ~Metric, type = "bar", orientation = "h",
799
- text = ~paste0(Percentile, "%"), textposition = "outside") |>
800
- layout(title = paste(input$selected_player, "Percentiles vs Same Position and Competition"),
801
- xaxis = list(range = c(0, 110)), yaxis = list(title = ""),
802
- height = max(450, 32 * nrow(plot_df)))
803
- })
804
-
805
- observeEvent(input$trend_btn, {
806
- row <- current_player_row()
807
- multi_row <- get_multiseason_row(multi_df, input$selected_player)
808
- metric <- input$profile_metric
809
-
810
- output$trend_plot <- renderPlotly({
811
- if (is.null(row) || is.null(metric) || metric == "")
812
- return(plot_ly() |> layout(title = "Select a player and metric."))
813
- rows_data <- lapply(names(HISTORICAL_SEASONS), function(sc) {
814
- v <- NA_real_
815
- if (!is.null(multi_row)) v <- find_metric_value(multi_row, metric, sc)
816
- if (is.na(v)) v <- find_metric_value(row, metric, sc)
817
- if (!is.na(v)) data.frame(Season = HISTORICAL_SEASONS[sc], Score = v)
818
- })
819
- plot_df <- do.call(rbind, Filter(Negate(is.null), rows_data))
820
- curr_val <- find_metric_value(row, metric, NULL)
821
- if (!is.na(curr_val)) {
822
- plot_df <- plot_df[plot_df$Season != CURRENT_MAIN_SEASON_LABEL, ]
823
- plot_df <- rbind(plot_df, data.frame(Season = CURRENT_MAIN_SEASON_LABEL, Score = curr_val))
824
- }
825
- if (is.null(plot_df) || nrow(plot_df) == 0)
826
- return(plot_ly() |> layout(title = "No performance data found."))
827
- season_order <- c("2021-22","2022-23","2023-24","2024-25","2025-26")
828
- plot_df$Season <- factor(plot_df$Season, levels = season_order)
829
- plot_df <- plot_df[order(plot_df$Season), ]
830
- plot_ly(plot_df, x = ~Season, y = ~Score, type = "scatter", mode = "lines+markers+text",
831
- text = ~round(Score, 2), textposition = "top center") |>
832
- layout(title = paste0(input$selected_player, ": ", pretty_label(metric), " Over Time"))
833
- })
834
- })
835
-
836
- # --- PDF/CSV Report download ---
837
- output$report_btn <- downloadHandler(
838
- filename = function() {
839
- safe <- str_replace_all(input$selected_player, "[^A-Za-z0-9_]", "_")
840
- paste0(safe, "_scouting_report.csv")
841
- },
842
- content = function(file) {
843
- row <- current_player_row()
844
- if (is.null(row)) {
845
- write.csv(data.frame(Message = "No player selected"), file, row.names = FALSE)
846
- return()
847
- }
848
- all_cols <- available_cols(c(PLAYER_COL, TEAM_COL, COMP_COL, POSITION_COL,
849
- AGE_COL, COUNTRY_COL, HEIGHT_COL, WEIGHT_COL,
850
- MARKET_VALUE_COL, CONTRACT_COL, MINUTES_COL,
851
- ARCHETYPE_COL, ARCHETYPE_SCORE_COL, TARGET_SCORE_COL,
852
- ATTAINABILITY_COL, CLUB_RANK_COL, MATCH_TOUGHNESS_COL,
853
- ELO_COL, ATTR_COLS, KEY_METRICS, POSITION_SCORE_COLS,
854
- ARCHETYPE_SCORE_COLS), df)
855
- out <- df[as.character(df[[PLAYER_COL]]) == as.character(input$selected_player),
856
- all_cols, drop = FALSE]
857
- notes_row <- data.frame(Section = "Scout Notes", Notes = input$scout_notes)
858
- write.csv(out, file, row.names = FALSE)
859
- }
860
- )
861
-
862
- # --- Shortlist ---
863
- observeEvent(input$shortlist_btn, {
864
- p <- input$selected_player
865
- if (!is.null(p) && p != "" && !p %in% shortlist()) {
866
- shortlist(c(shortlist(), p))
867
- }
868
- })
869
-
870
- view_shortlist <- reactive({
871
- sl <- shortlist()
872
- if (length(sl) == 0) return(data.frame(Message = "No players added yet."))
873
- data <- df[as.character(df[[PLAYER_COL]]) %in% sl, ]
874
- cols <- available_cols(SHORTLIST_COLS, data)
875
- out <- data[, cols, drop = FALSE]
876
- if (nrow(out) == 0) return(data.frame(Message = "Shortlist is empty."))
877
- pretty_df(out, df)
878
- })
879
-
880
- output$shortlist_from_profile <- renderDT({
881
- datatable(view_shortlist(), rownames = FALSE,
882
- options = list(scrollX = TRUE, pageLength = 15))
883
- })
884
-
885
- # ---- COMPARISON ----
886
- comparison_df <- eventReactive(input$compare_btn, {
887
- players <- c(input$compare_1, input$compare_2, input$compare_3)
888
- players <- players[!is.null(players) & players != ""]
889
- if (length(players) == 0) return(data.frame(Message = "Select at least one player."))
890
- data <- df[as.character(df[[PLAYER_COL]]) %in% players, ]
891
- cols <- available_cols(COMPARISON_COLS, data)
892
- pretty_df(data[, cols, drop = FALSE], df)
893
- })
894
-
895
- output$comparison_table <- renderDT({
896
- datatable(comparison_df(), selection = "single", rownames = FALSE,
897
- options = list(scrollX = TRUE, pageLength = 25))
898
- })
899
-
900
- output$comparison_radar <- renderPlotly({
901
- players <- c(input$compare_1, input$compare_2, input$compare_3)
902
- players <- players[!is.null(players) & players != ""]
903
- if (length(players) == 0) return(plot_ly() |> layout(title = "Select players to compare."))
904
- first_row <- get_player_row(df, players[1])
905
- if (is.null(first_row)) return(plot_ly())
906
- metrics <- top_attr_cols(df, first_row, max_cols = 8)
907
- if (length(metrics) < 3) return(plot_ly() |> layout(title = "Not enough attributes."))
908
- labels <- sapply(metrics, pretty_label)
909
- fig <- plot_ly(type = "scatterpolar", fill = "toself")
910
- for (p in players) {
911
- row <- get_player_row(df, p)
912
- if (!is.null(row)) {
913
- vals <- sapply(metrics, function(m) {
914
- v <- row[[m]]; if (is.null(v) || is.na(v)) 0 else as.numeric(v)
915
- })
916
- fig <- fig |> add_trace(r = c(vals, vals[1]), theta = c(labels, labels[1]), name = p)
917
- }
918
- }
919
- fig |> layout(title = "Player Attribute Radar Comparison",
920
- polar = list(radialaxis = list(range = c(0, 110))),
921
- legend = list(orientation = "h"))
922
- })
923
-
924
- # ---- FIT SCORE ----
925
- fit_result_df <- eventReactive(input$fit_btn, {
926
- data <- df
927
- if (length(input$fit_competition_filter) > 0 && COMP_COL %in% names(data))
928
- data <- data[data[[COMP_COL]] %in% input$fit_competition_filter, ]
929
- if (length(input$fit_position_filter) > 0 && POSITION_COL %in% names(data))
930
- data <- data[data[[POSITION_COL]] %in% input$fit_position_filter, ]
931
- if (nrow(data) == 0) return(data.frame(Message = "No players found for selected filters."))
932
-
933
- weights <- c(
934
- attr_pressing = input$pressing_w, attr_duels = input$duels_w,
935
- attr_aerial = input$aerial_w, attr_possession_retention = input$possession_w,
936
- attr_blocking = input$blocking_w, attr_progression = input$progression_w,
937
- attr_impact = input$impact_w, attr_discipline = input$discipline_w,
938
- attr_dribbling = input$dribbling_w, attr_chance_creation = input$chance_w,
939
- attr_finishing = input$finishing_w, attr_crossing = input$crossing_w,
940
- attr_box_presence = input$box_w, attr_holdup = input$holdup_w
941
- )
942
- weights <- c(weights, setNames(c(input$target_w, input$attain_w),
943
- c(TARGET_SCORE_COL, ATTAINABILITY_COL)))
944
- total_weight <- sum(weights)
945
- if (total_weight == 0) return(data.frame(Message = "At least one weight must be > 0."))
946
-
947
- fit_vals <- rep(0, nrow(data))
948
- for (col in names(weights)) {
949
- w <- weights[col]
950
- if (col %in% names(data) && w > 0) {
951
- fit_vals <- fit_vals + normalize_0_100(data[[col]]) * w
952
- }
953
- }
954
- data$fit_score <- fit_vals / total_weight
955
- cols <- available_cols(c(PLAYER_COL, POSITION_COL, TEAM_COL, COMP_COL,
956
- AGE_COL, MINUTES_COL, MARKET_VALUE_COL, CONTRACT_COL,
957
- ARCHETYPE_COL, ARCHETYPE_SCORE_COL,
958
- TARGET_SCORE_COL, ATTAINABILITY_COL,
959
- names(weights)), data)
960
- cols <- c(cols, "fit_score")
961
- out <- data[order(-data$fit_score, na.last = TRUE), cols, drop = FALSE]
962
- pretty_df(head(out, 50), df)
963
- })
964
-
965
- output$fit_table <- renderDT({
966
- datatable(fit_result_df(), selection = "single", rownames = FALSE,
967
- options = list(scrollX = TRUE, pageLength = 25))
968
- })
969
-
970
- # ---- SIMILAR PLAYERS ----
971
- similar_result_df <- eventReactive(input$similar_btn, {
972
- row <- get_player_row(df, input$similar_player_select)
973
- if (is.null(row)) return(data.frame(Message = "Select a player."))
974
- metrics <- available_cols(SIMILARITY_METRICS, df)
975
- metrics <- metrics[sapply(metrics, function(m) {
976
- v <- row[[m]]; !is.null(v) && !is.na(v)
977
- })]
978
- metrics <- head(metrics, 24)
979
- if (length(metrics) == 0) return(data.frame(Message = "No similarity metrics available."))
980
- pos <- row[[POSITION_COL]]
981
- candidates <- df[as.character(df[[PLAYER_COL]]) != as.character(input$similar_player_select), ]
982
- if (POSITION_COL %in% names(df) && !is.null(pos) && !is.na(pos)) {
983
- sub <- candidates[candidates[[POSITION_COL]] == pos, ]
984
- if (nrow(sub) > 0) candidates <- sub
985
- }
986
- dist_vals <- rep(0, nrow(candidates))
987
- for (m in metrics) {
988
- all_vals <- suppressWarnings(as.numeric(df[[m]]))
989
- sd_val <- sd(all_vals, na.rm = TRUE)
990
- cand_vals <- suppressWarnings(as.numeric(candidates[[m]]))
991
- ref_val <- suppressWarnings(as.numeric(row[[m]]))
992
- if (!is.na(sd_val) && sd_val > 0) {
993
- dist_vals <- dist_vals + ((cand_vals - ref_val) / sd_val)^2
994
- }
995
- }
996
- candidates$similarity_score <- 100 / (1 + dist_vals)
997
- cols <- c(available_cols(c(PLAYER_COL, TEAM_COL, COMP_COL, POSITION_COL,
998
- AGE_COL, MARKET_VALUE_COL, ARCHETYPE_COL,
999
- ARCHETYPE_SCORE_COL, TARGET_SCORE_COL,
1000
- ATTAINABILITY_COL), candidates), "similarity_score")
1001
- out <- candidates[order(-candidates$similarity_score), cols, drop = FALSE]
1002
- pretty_df(head(out, 10), df)
1003
- })
1004
-
1005
- output$similar_table <- renderDT({
1006
- datatable(similar_result_df(), selection = "single", rownames = FALSE,
1007
- options = list(scrollX = TRUE, pageLength = 15))
1008
- })
1009
-
1010
- # ---- SHORTLIST MANAGER ----
1011
- observeEvent(input$add_shortlist_btn, {
1012
- p <- input$shortlist_player
1013
- if (!is.null(p) && p != "" && !p %in% shortlist()) {
1014
- shortlist(c(shortlist(), p))
1015
- }
1016
- })
1017
-
1018
- observeEvent(input$clear_shortlist_btn, {
1019
- shortlist(character(0))
1020
- })
1021
-
1022
- output$shortlist_table <- renderDT({
1023
- datatable(view_shortlist(), rownames = FALSE,
1024
- options = list(scrollX = TRUE, pageLength = 25))
1025
- })
1026
-
1027
- output$export_shortlist_btn <- downloadHandler(
1028
- filename = function() "shortlist_export.csv",
1029
- content = function(file) {
1030
- sl <- view_shortlist()
1031
- write.csv(sl, file, row.names = FALSE)
1032
- }
1033
- )
1034
- }
1035
-
1036
- shinyApp(ui, server)