Football Player Ratings
Interactive table of Panna football player ratings
Last updated: 19 February 2026
Interactive table of Panna ratings across 15 leagues. Click column headers to sort; use the search box to find players.
How to read: Offense = contribution to creating xG. Defense = contribution to preventing xG (negative = good defender). Panna = overall rating (offense - defense).
Show code
data_path <- here::here("data", "panna_ratings.csv")
if (file.exists(data_path)) {
ratings <- read.csv(data_path)
ratings <- ratings[order(ratings$panna_rank), ]
ratings <- ratings[seq_len(min(1000, nrow(ratings))), ]
ratings$panna_percentile <- round(ratings$panna_percentile, 1)
inthegame_table(
ratings,
defaultSorted = list(panna_rank = "asc"),
columns = list(
panna_rank = colDef(
name = "Rank",
width = 70,
align = "center"
),
player_name = colDef(
name = "Player",
minWidth = 180,
filterable = TRUE
),
panna = colDef(
name = "Panna",
format = colFormat(digits = 3),
style = function(value) {
color <- if (value > 0) "#2d8a4e" else "#c0392b"
list(fontWeight = "bold", color = color)
}
),
offense = colDef(
name = "Offense",
format = colFormat(digits = 3),
style = function(value) {
color <- if (value > 0) "#2d8a4e" else "#c0392b"
list(color = color)
}
),
defense = colDef(
name = "Defense",
format = colFormat(digits = 3),
style = function(value) {
# Negative defense = good (prevents goals)
color <- if (value < 0) "#2d8a4e" else "#c0392b"
list(color = color)
}
),
spm_overall = colDef(
name = "SPM",
format = colFormat(digits = 3)
),
total_minutes = colDef(
name = "Minutes",
format = colFormat(separators = TRUE, digits = 0)
),
panna_percentile = colDef(
name = "Percentile",
format = colFormat(digits = 1),
style = function(value) {
color <- if (value >= 90) "#2d8a4e"
else if (value >= 50) "#7f8c8d"
else "#c0392b"
list(color = color)
}
)
)
)
} else {
htmltools::p(
class = "text-muted",
"Football ratings data will be available once the automated data pipeline runs."
)
}