Olympic Mixed Doubles Curling: Power Play Strategy

Abstract

For this project, I wanted to focus on the 2026 CSAS Data Challenge, which focused on optimizing power play strategy in mixed doubles curling. Given data taken from the 2022 Winter Olympics, as well as the World Mixed Doubles Curling Championships from 2023-2025, I realized that many teams implement similar strategies, with mixed results. Power plays usage varied, but results showed that teams who use it later tend to win more.

Since Italy’s team won the Beijing Olympics in 2022 and the most recent World Championship in 2025, I focused on their data to look for optimal power play strategies. Italy tended to be more aggressive in their plays. The data showed that Italy is able to throw more technical shots than the average team, and using this advantage they use their power play for large momentum swing plays in the later ends, pressuring their opponents to take risks. Italy always used their power play in the last 3 ends of a match, with 1 exception in the 5th end where they scored 0 points. When looking into data from all teams across the 4 tournaments, the only power plays that scored 5+ points were always thrown in the last 3 ends. So the data shows that using the power play in the later half of the match generally leads to better results.

Introduction

Mixed Doubles Curling is played on a large sheet of ice sizing 150 ft x 15.6 ft. The goal is to have your stones be the closest to the center of the target ring, called the house. For every stone closer to the center than your opponent’s closest stone, you score a point. Each match is played with 8 ends (like innings), and teams throw 5 stones each per end for a total of 10 stones total.

Each Team starts with 1 stone already pre-placed on the ice sheet. A team has 1 in the center behind the innermost target circle, and the other team has 1 placed a few feet in front of the outermost target circle as a blocker. A power play can be used at the start of an end by the team who throws last that end, and those two pre-placed stones are shifted a few feet to the side to open up the playing field.

Every team who starts first tries to throw stones to the center, since no takeout shots can begin until the 4th stone. Then, they begin to throw guarding shots, tap ins to push their guards closer to the center, and takeout shots to knock their opponent’s stones away.

The goal of my project was to use R and Rstudio to help analyze the 4 datasets give by the CSAS 2026 data challenge to find the optimal power play strategy.

Data

The data sets were taken from the 2022 Winter Olympics, as well as the World Mixed Doubles Curling Championships from 2023, 2024, and 2025.

Using R, I was able to load the downloaded data sets into Rstudio to view the spreadsheets.

{r}
path <- getwd()
files <- list.files(path = path, pattern = "*.csv")

for(file in files) {
  perpos <- which(strsplit(file, "")[[1]]==".")
  assign(
    gsub(" ", "/", substr(file, 1, perpos-1)),
    read.csv(paste(path, file, sep = "/"))
  )
}

My initial goal was to find clear openers with high win rates, the Power Plays with the highest amount of success (measured via winning the End, or the match), and which Power Plays “scored” the most amount of points.

Methods

I wanted to find the winning power play strategy, so I ended to look at the teams that won the championships. In doing so, I realized that Italy has won 2 out of the 4 championships, Beijing 2022 Winter Olympics and the most recent 2025 World Mixed Doubles Curling competition. I decided to look at their data first, then compare it to the other teams.

Results

When comparing Italy’s data to the average from the other teams, I realized that the data looks quite similar, with Italy just having a slight higher amount of technical throws.

{r}
df<-rbind(transform(stones1.3, group = "Avg. for All Teams"), transform(italy1.2, group = "Italy"))

ggplot(data = df, aes(x = Task, y = n, fill = group))  +
  geom_col(position = "dodge") +
  geom_text(
    aes(label = n),
    vjust = -0.8, 
    position = position_dodge(1.1),
    size =3.5)+
  labs(
    title = "Different Types of Throws Used",
    x = "Type of Throw",
    y= "Throws Used"
  )
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.1     ✔ tibble    3.3.0
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.2.0     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Warning: `position_dodge()` requires non-overlapping x intervals.

The throws are categorized as

  • “0”: “Draw”

  • “1”: “Front”

  • “2”: “Guard”

  • “3”: “Raise / Tap-back”

  • “4”: “Wick / Soft Peeling”

  • “5”: “Freeze”

  • “6”: “Take-out”

  • “7”: “Hit and Roll”

  • “8”: “Clearing”

  • “9”: “Double Take-out”

  • “10”: “Promotion Take-out”

  • “11”: “through”

  • “13”: “no statistics”

As you can see, the averages of the other teams have a very similar trend to Italy’s data, meaning there seems to be a set way to “play the game” even if strategies vary. Utilizing this information, I delved deeper into the statistics of Italy’s team, looking at when they would use their power play.

Italy almost always used their power play during the last 3 ends. The one time in the 5th end they used their power play, they scored 0 points.

Looking purely at the datasets and filtering by results (score), we will find that all power plays that scored 5+ points were scored during the last 3 ends for ALL teams across the 4 competitions.

I also calculated the difference in score average when Italy used their power play

Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.

and when they did not use their power play.

Italy seemed to score double the amount of average points per end during the times they used a power play. The total competition averages were 1.6 points for power play, and 0.92 for no power play ends.

Conclusions

It seems that on average, it is better to use the power play during the last 3 ends of the match. Using just this tactic alone, Italy’s team was able to win two competitions. Looking at the resulting score averages across all teams as well, the resulting score differential is quite large, up to 5-9 points score only during the last 3 ends of a match.

Bibliography

“CSAS 2026 Data Challenge.” CSAS 2026 : Data Challenge, CSAS, statds.org/events/csas2026/challenge.html. Accessed 14 Dec. 2025.

CSAS-Data-Challenge. “CSAs-Data-Challenge/2026: Data Challenge of Csas 2026.” GitHub, CSAS, github.com/CSAS-Data-Challenge/2026. Accessed 14 Dec. 2025.

R Core Team (2025). _R: A Language and Environment for Statistical Computing_. R Foundation for Statistical Computing, Vienna, Austria.   <https://www.R-project.org/>. 

Appendix

setwd("~/2026-main")

path <- getwd()
files <- list.files(path = path, pattern = "*.csv")

for(file in files) {
  perpos <- which(strsplit(file, "")[[1]]==".")
  assign(
    gsub(" ", "/", substr(file, 1, perpos-1)),
    read.csv(paste(path, file, sep = "/"))
  )
}

library(tidyverse)

italy <- Stones %>% filter(TeamID == 24)
italy1<- italy %>% 
  count(Task)

ggplot(data = italy1, aes(x = Task, y = n))  +
  geom_col()

stones1<- Stones %>% 
  count(Task)
ggplot(data = stones1, aes(x = Task, y = n))  +
  geom_col()

stones1.1 <- Stones %>%  filter(ShotID >= 16)
stones1.2<- stones1.1 %>% 
  count(Task)
ggplot(data = stones1.2, aes(x = Task, y = n))  +
  geom_col()

stones1.3 <- stones1.2 %>% mutate(n = n * 1/20)
ggplot(data = stones1.3, aes(x = Task, y = n))  +
  geom_col()

italy <- Stones %>% filter(TeamID == 24)
italy1.1<- italy %>% filter(ShotID>=16) 
italy1.2 <- italy1.1%>% 
  count(Task)
ggplot(data = italy1.2, aes(x = Task, y = n))  +
  geom_col()

ggplot(data = italy1.2, aes(x = Task, y = n))  +
  geom_col(fill = "darkgreen") +
 geom_text(
   aes(label = n),
   vjust = -0.8, 
   size =3.5)+
  labs(
    title = "Different Types of Throws Used",
  x = "Type of Throw",
  y= "Throws Used"
  )
  ##Now for everyone else 
ggplot(data = stones1.3, aes(x = Task, y = n))  +
  geom_col(fill = "darkgreen") +
  geom_text(
    aes(label = n),
    vjust = -0.8, 
    size =3.5)+
  labs(
    title = "Different Types of Throws Used",
    x = "Type of Throw",
    y= "Throws Used"
  )

##attempt to rbind them

df<-rbind(transform(stones1.3, group = "Avg. for All Teams"), transform(italy1.2, group = "Italy"))

ggplot(data = df, aes(x = Task, y = n, fill = group))  +
  geom_col(position = "dodge") +
  geom_text(
    aes(label = n),
    vjust = -0.8, 
    position = position_dodge(1.1),
    size =3.5)+
  labs(
    title = "Different Types of Throws Used",
    x = "Type of Throw",
    y= "Throws Used"
  )
## Test code 
resultend <- Ends %>%  filter(EndID > 0,PowerPlay!=0)
resultend2 <- resultend %>% 
  count(Result, EndID)
ggplot(data = resultend2, aes (x= EndID + Result, y=n))+
  geom_col(fill="cyan3", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  labs(
    title = "Power Play End vs Resulting Score",
    x = "The End a Power Play is Used per Match",
    y= "The Resulting Score"
  )

## graph not used, the data frames are used. 
ends1 <- Ends %>% filter(EndID >0, PowerPlay!= "0" )
ends2 <- ends1 %>%  filter (TeamID ==24)
ggplot(data = ends2, aes (x= EndID, y=Result))+
  geom_col(fill="cyan3", position = "dodge")+
  geom_text(
    aes(label = Result),
    vjust = -.8,
    position = position_dodge(1.1),
    size = 3.5)+
  labs(
    title = "When Italy Uses their Power Play, and the resulting score that End",
    x = "End/Round the Power Play is Used",
    y= "Resulting Score that End/Round"
  )
## grapoh not used. the dataframes are used. 
endsavg <- Ends %>% filter(EndID >0, PowerPlay!= "0" )
endsavg1 <- ends1 %>%  filter (TeamID !=24)
ggplot(data = endsavg1, aes (x= EndID, y=Result))+
 geom_col(fill="cyan3", position = "dodge")+
 geom_text(
   aes(label = Result),
   vjust = -.8,
   position = position_dodge(1.1),
 size = 3.5)+
  labs(
    title = "When Italy Uses their Power Play, and the resulting score that End",
    x = "End/Round the Power Play is Used",
    y= "Resulting Score that End/Round"
  )


##Count 
ggplot(data = endsavg2, aes (x= EndID, y=n))+
  geom_col(fill="cyan3", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  labs(
    title = "Which End Italy Uses their Power Play",
    x = "The End a Power Play is Used per Match",
    y= "The Count a Power Play is used during that End "
  )

##succesessful graph 
ends2.1 <- ends2%>% 
  count(EndID)
ggplot(data = ends2.1, aes (x= EndID, y=n))+
  geom_col(fill="cyan3", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  labs(
    title = "Which End Italy Uses their Power Play",
    x = "The End a Power Play is Used per Match",
    y= "The Count a Power Play is used during that End "
  )

ppresult<- ends2 %>% 
  count (Result)
##PPR2<- ppresult %>% summarise(mean_col = mean(Result, na.rm = TRUE)) FAILED
ggplot(data = ppresult, aes (x= Result, y=n))+
  geom_col(fill="cyan3", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  geom_vline(xintercept = 2.07, color = "blue", linetype = "dashed", size = 1) +
  annotate("text", x = 2.07, y= 6, label = "Mean Score = 2.07", angle = 90,vjust = -0.5, size = 5)+
  labs(
    title = "Resulting Score when Italy Uses their Power Play",
    x = "Resulting Score",
    y= "Count of Resulting Score"
  )

ItalyScore <- Ends %>% filter(EndID >0, TeamID==24, is.na(PowerPlay))
ItalyScore1 <- ItalyScore %>% 
  count(Result)
##ItalyScore2<- ItalyScore1 %>% summarise(mean_col = mean(Result, na.rm = TRUE)) FAILED
ggplot(data = ItalyScore1, aes (x= Result, y=n))+
  geom_col(fill="cyan3", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  geom_vline(xintercept = 1.11, color = "blue", linetype = "dashed", size = 1) +
  annotate("text", x = 1.11, y= 40, label = "Mean Score = 1.11", angle = 90, vjust = -0.5, size = 5)+
  labs(
    title = "Resulting Score when Italy does not use their Power Play",
    x = "Resulting Score",
    y= "Count of Resulting Score"
  )

##geom_smooth(method = "lm", se = FALSE, color = "blue", aes(group = 1))

AvgT <- Ends %>% filter(EndID >0, TeamID!=24, is.na(PowerPlay))
avgt<- AvgT %>% 
  count(Result)
ggplot(data = avgt, aes (x= Result, y=n))+
  geom_col(fill="firebrick2", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  geom_vline(xintercept = .92, color = "blue", linetype = "dashed", size = 1) +
  annotate("text", x = .92, y= 1500, label = "Mean Score = .92", angle = 90, vjust = -0.5, size = 5)+
  labs(
    title = "Resulting Score when Avg does not use their Power Play",
    x = "Resulting Score",
    y= "Count of Resulting Score"
  )

AvgTP <- Ends %>% filter(EndID >0, TeamID!=24, PowerPlay>= 0)
avgtp<- AvgTP %>% 
  count(Result)
ggplot(data = avgtp, aes (x= Result, y=n))+
  geom_col(fill="firebrick2", position = "dodge")+
  geom_text(
    aes(label = n),
    vjust = -.8,
    size = 3.5)+
  geom_vline(xintercept = 1.62, color = "blue", linetype = "dashed", size = 1) +
  annotate("text", x = 1.62, y= 150, label = "Mean Score = 1.62", angle = 90, vjust = -0.5, size = 5)+
  labs(
    title = "Resulting Score when Avg does not use their Power Play",
    x = "Resulting Score",
    y= "Count of Resulting Score"
  )
citation()