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."
  )
}