Skip to contents

2019 Women's World Cup Predictions

Soccer Power Index (SPI) ratings and chances of advancing for every team
​
Team Rating
Chance of Finishing Group Stage In ...
Knockout Stage Chances
team
group
spi
Off.
Def.
1st Place
2nd Place
3rd Place
Make Round of 16
Make Qtr-Finals
Make Semifinals
Make Final
Win World Cup
USA flag
USA6 pts.
F
98.3
5.5
0.6
83%
17%
–
✓
78%
47%
35%
24%
France flag
France6 pts.
A
96.3
4.3
0.5
>99%
<1%
<1%
✓
78%
42%
30%
19%
Germany flag
Germany6 pts.
B
93.8
4.0
0.7
98%
2%
–
✓
89%
48%
28%
12%
Canada flag
Canada6 pts.
E
93.5
3.7
0.6
39%
61%
–
✓
59%
36%
20%
9%
England flag
England6 pts.
D
91.9
3.5
0.6
71%
29%
–
✓
69%
43%
16%
8%
Netherlands flag
Netherlands6 pts.
E
92.7
3.9
0.7
61%
39%
–
✓
59%
37%
19%
8%
Australia flag
Australia3 pts.
C
92.8
4.2
0.9
13%
54%
34%
>99%
54%
26%
10%
5%
Sweden flag
Sweden6 pts.
F
88.4
3.0
0.6
17%
83%
–
✓
47%
20%
10%
4%
Japan flag
Japan4 pts.
D
90.3
3.8
0.9
29%
63%
8%
✓
46%
27%
9%
4%
Brazil flag
Brazil3 pts.
C
89.5
3.6
0.9
26%
22%
52%
>99%
43%
17%
7%
3%
Spain flag
Spain3 pts.
B
86.5
3.1
0.8
1%
70%
29%
>99%
31%
11%
5%
2%
Norway flag
Norway3 pts.
A
83.7
3.0
0.9
<1%
94%
5%
>99%
44%
18%
4%
2%
China flag
China3 pts.
B
82.7
2.7
0.8
<1%
29%
70%
93%
34%
11%
3%
<1%
Italy flag
Italy6 pts.
C
76.1
3.0
1.3
61%
24%
15%
✓
37%
9%
2%
<1%
New Zealand flag
New Zealand0 pts.
E
77.6
2.8
1.2
–
–
48%
40%
10%
4%
<1%
<1%
Nigeria flag
Nigeria3 pts.
A
71.7
2.4
1.1
<1%
5%
91%
50%
13%
3%
<1%
<1%
Cameroon flag
Cameroon0 pts.
E
65.8
2.1
1.2
–
–
52%
23%
4%
1%
<1%
<1%
South Korea flag
South Korea0 pts.
A
76.4
2.7
1.1
–
<1%
4%
2%
<1%
<1%
<1%
<1%
Scotland flag
Scotland0 pts.
D
54.0
2.0
1.7
–
–
53%
52%
3%
<1%
<1%
<1%
Argentina flag
Argentina1 pts.
D
39.2
1.5
1.9
–
8%
40%
25%
<1%
<1%
<1%
<1%
Jamaica flag
Jamaica0 pts.
C
53.5
2.5
2.1
–
–
<1%
<1%
<1%
–
–
–
Thailand flag
Thailand0 pts.
F
40.7
2.0
2.4
–
–
34%
2%
<1%
<1%
–
–
South Africa flag
South Africa0 pts.
B
56.7
1.7
1.3
–
–
<1%
<1%
<1%
<1%
<1%
–
Chile flag
Chile0 pts.
F
46.5
1.8
1.8
–
–
66%
15%
<1%
<1%
<1%
–
Forecast from before 3rd group matches

Source: FiveThirtyEight

Raw data: wwc_forecasts.csv

Source Code

Full source: vignettes/womens-world-cup/womens-world-cup.Rmd

library(reactable)
library(htmltools)

forecasts <- read.csv("wwc_forecasts.csv", stringsAsFactors = FALSE)

rating_cols <- c("spi", "global_o", "global_d")
group_cols <- c("group_1", "group_2", "group_3")
knockout_cols <- c("make_round_of_16", "make_quarters", "make_semis", "make_final", "win_league")
forecasts <- forecasts[, c("team", "points", "group", rating_cols, group_cols, knockout_cols)]

rating_column <- function(maxWidth = 55, ...) {
  colDef(maxWidth = maxWidth, align = "center", class = "cell number", ...)
}

group_column <- function(class = NULL, ...) {
  colDef(cell = format_pct, maxWidth = 70, align = "center", class = paste("cell number", class), ...)
}

knockout_column <- function(maxWidth = 70, class = NULL, ...) {
  colDef(
    cell = format_pct,
    maxWidth = maxWidth,
    class = paste("cell number", class),
    style = function(value) {
      # Lighter color for <1%
      if (value < 0.01) {
        list(color = "#aaa")
      } else {
        list(color = "#111", background = knockout_pct_color(value))
      }
    },
    ...
  )
}

format_pct <- function(value) {
  if (value == 0) "  \u2013 "    # en dash for 0%
  else if (value == 1) "\u2713"  # checkmark for 100%
  else if (value < 0.01) " <1%"
  else if (value > 0.99) ">99%"
  else formatC(paste0(round(value * 100), "%"), width = 4)
}

make_color_pal <- function(colors, bias = 1) {
  get_color <- colorRamp(colors, bias = bias)
  function(x) rgb(get_color(x), maxColorValue = 255)
}

off_rating_color <- make_color_pal(c("#ff2700", "#f8fcf8", "#44ab43"), bias = 1.3)
def_rating_color <- make_color_pal(c("#ff2700", "#f8fcf8", "#44ab43"), bias = 0.6)
knockout_pct_color <- make_color_pal(c("#ffffff", "#f2fbd2", "#c9ecb4", "#93d3ab", "#35b0ab"), bias = 2)

tbl <- reactable(
  forecasts,
  pagination = FALSE,
  defaultSorted = "win_league",
  defaultSortOrder = "desc",
  defaultColGroup = colGroup(headerVAlign = "bottom"),
  columnGroups = list(
    colGroup(name = "Team Rating", columns = rating_cols),
    colGroup(name = "Chance of Finishing Group Stage In ...", columns = group_cols),
    colGroup(name = "Knockout Stage Chances", columns = knockout_cols)
  ),
  defaultColDef = colDef(
    vAlign = "center",
    headerVAlign = "bottom",
    class = "cell",
    headerClass = "header"
  ),
  columns = list(
    team = colDef(
      defaultSortOrder = "asc",
      minWidth = 200,
      headerStyle = list(fontWeight = 700),
      cell = function(value, index) {
        div(
          class = "team",
          img(class = "team-flag", alt = paste(value, "flag"), src = sprintf("images/%s.png", value)),
          div(
            span(class = "team-name", value),
            span(class = "team-record", sprintf("%s pts.", forecasts[index, "points"]))
          )
        )
      }
    ),
    points = colDef(show = FALSE),
    group = colDef(defaultSortOrder = "asc", align = "center", maxWidth = 75,
                   class = "cell group", headerStyle = list(fontWeight = 700)),
    spi = rating_column(format = colFormat(digits = 1)),
    global_o = rating_column(
      name = "Off.",
      cell = function(value) {
        scaled <- (value - min(forecasts$global_o)) / (max(forecasts$global_o) - min(forecasts$global_o))
        color <- off_rating_color(scaled)
        value <- format(round(value, 1), nsmall = 1)
        div(class = "spi-rating", style = list(background = color), value)
      }
    ),
    global_d = rating_column(
      name = "Def.", 
      defaultSortOrder = "asc",
      cell = function(value) {
        scaled <- 1 - (value - min(forecasts$global_d)) / (max(forecasts$global_d) - min(forecasts$global_d))
        color <- def_rating_color(scaled)
        value <- format(round(value, 1), nsmall = 1)
        div(class = "spi-rating", style = list(background = color), value)
      }
    ),
    group_1 = group_column(name = "1st Place", class = "border-left"),
    group_2 = group_column(name = "2nd Place"),
    group_3 = group_column(name = "3rd Place"),
    make_round_of_16 = knockout_column(name = "Make Round of 16", class = "border-left"),
    make_quarters = knockout_column(name = "Make Qtr-Finals"),
    make_semis = knockout_column(name = "Make Semifinals", maxWidth = 90),
    make_final = knockout_column(name = "Make Final"),
    win_league = knockout_column(name = "Win World Cup")
  ),
  # Emphasize borders between groups when sorting by group
  rowClass = JS("
    function(rowInfo, state) {
      const firstSorted = state.sorted[0]
      if (firstSorted && firstSorted.id === 'group') {
        const nextRow = state.pageRows[rowInfo.viewIndex + 1]
        if (nextRow && rowInfo.values['group'] !== nextRow.group) {
          return 'group-last'
        }
      }
    }"
  ),
  showSortIcon = FALSE,
  borderless = TRUE,
  class = "standings-table"
)

div(class = "standings",
  div(class = "title",
    h2("2019 Women's World Cup Predictions"),
    "Soccer Power Index (SPI) ratings and chances of advancing for every team"
  ),
  tbl,
  "Forecast from before 3rd group matches"
)
htmltools::tags$link(
  href = "https://fonts.googleapis.com/css?family=Karla:400,700|Fira+Mono&display=fallback",
  rel = "stylesheet"
)
.standings {
  font-family: Karla, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 0.875rem;
}

.title {
  margin-top: 2rem;
  margin-bottom: 1.125rem;
  font-size: 1rem;
}

.title h2 {
  font-size: 1.25rem;
  font-weight: 600;
}

.standings-table {
  margin-bottom: 1.25rem;
}

.header {
  border-bottom-color: #555;
  font-size: 0.8125rem;
  font-weight: 400;
  text-transform: uppercase;
}

/* Highlight headers when sorting */
.header:hover,
.header:focus,
.header[aria-sort="ascending"],
.header[aria-sort="descending"] {
  background-color: #eee;
}

.border-left {
  border-left: 2px solid #555;
}

/* Use box-shadow to create row borders that appear behind vertical borders */
.cell {
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
}

.group-last .cell {
  box-shadow: inset 0 -2px 0 #555;
}

.team {
  display: flex;
  align-items: center;
}

.team-flag {
  height: 1.3rem;
  border: 1px solid #f0f0f0;
}

.team-name {
  margin-left: 0.5rem;
  font-size: 1.125rem;
  font-weight: 700;
}

.team-record {
  margin-left: 0.35rem;
  color: hsl(0, 0%, 45%);
  font-size: 0.8125rem;
}

.group {
  font-size: 1.1875rem;
}

.number {
  font-family: "Fira Mono", Consolas, Monaco, monospace;
  font-size: 1rem;
  white-space: pre;
}

.spi-rating {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: auto;
  width: 1.875rem;
  height: 1.875rem;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 50%;
  color: #000;
  font-size: 0.8125rem;
  letter-spacing: -1px;
}