Mätresultat i fotboll är i stort identiskt med mätresultat från många industriella processer eller resultat i vården eller i trafiken. Det centrala är händelser i tiden alltså ’mål per match’, ’antal reklamationer per månad’, ’antal olyckor per år’, etc. Men man kan också räkna ’antal per yta’ (älgar per hektar), eller ’antal per volym’ (’bakterier per milliter blod’), etc.

Den självklara statistiska modellen för ‘antalsdata’ är Poissonfördelningen. Lägsta värdet för ett dylika mätresultat är naturligtvis 0 men det finns i teorin inget högsta antal.

Poissonfördelningen har praktiskt nog bara en parameter, ett tal som är större än 0 och brukar kallas ‘intensiteten’ och betecknas med grekiska bokstaven ‘lambda’ (λ).

      Mer info: https://ovn.ing-stat.se [7], [20], [OE27], [OE28].

En analys av händelsedata är alltså en jakt på värdet λ. Ändrar sig värdet över tid (antal kunder per tidsenhet varierar antagligen systematiskt över en vecka)? Eller är värdet olika mellan maskiner eller mellan andra processer?, etc.

Inom fotbollsintresset, men även processer enligt ovan, finns det alltid en mängd tvärsäkra men ogrundade påstående om sambanden. I tider av stora fotbollsturneringar svämmar tidningarna över av påstående om det ena eller andra typ ‘den som lägger första straffen vinner matchen’ och med tillägget ‘…visar statistiken’. Skrapar man på ytan finns det oftast ingen grund för uppfattningen. Detta är kanske inte så viktigt i underhållsbranschen men kan ju vara katastofalt inom t.ex. vården.
Att lära att förstå och hantera händelsedata och Poissonfördelningen ger alltså stora fördelar.

Datamängden. Datamängden som analyseras nedan kommer från en hemsida. Den innehåller en mängd uppgifter såsom tidpunkter för gula och röda kort, tidpunkt för byten, m.m. Här används dock endast tidpunkt för mål för respektive lag och endast resultatet efter full tid. Resultat från straffläggning är alltså inte med.


   1. Antal mål per match - utfall och teoretisk fördelning

   2. Antal mål per 4-minuters intervall

   3. Tid till matchens första mål

   4. ‘Probability plot’ för tid fram till första mål

   5. Teoretisk fördelning för skillnad mellan antal mål

   6. Absolut skillnad i antal mål - utfall och teoretisk fördelning





 1. Antal mål per match - utfall och teoretisk fördelning

De två histogrammen visar summan av antal mål per match för cirka 100 matcher. Den blå fördelningen av resultat är Poissonfördelningen där intensiteten lambda (λ) skattats från data och den röda fördelningen är utfallet från VM-matcherna.
Det tycks inte vara någon dramatisk skillnad så en slutsats är att antal mål per match kan anses vara en slumpprocess som följer en Poissonfördelning:

library("tidyverse")                              # Load tidyverse packages
library("dplyr")  
library("ggplot2")


VM2026 <- read.csv("~/Desktop/Ing-Stat/VMfotboll 2026 rev D.csv", sep=";")
#   View(VM2026)

Målminut <- VM2026$Minut
Match    <- VM2026$Match
   
antalNoll <- sum(is.na(Målminut))

cellbredd  <- 4           # Minuter

cellnummer <- ceiling(Målminut/cellbredd)

Medelmaxminuter <- mean(Målminut[Målminut > 90], na.rm = TRUE)

listamax <- sort(Målminut[Målminut > 90])

antalMatcher <- max(Match, na.rm = TRUE)

antalMål <- length(Målminut) - antalNoll           # Varje rad är ett mål, utom '0-0'

totlambda <- antalMål/antalMatcher

målres <- VM2026 %>%                               # Beräknar antal mål per match.
          count(Match) %>%
          group_by(Match) %>%       
          mutate()

antalMål <- append(målres$n, rep(0, antalNoll))
mållista <- data.frame(antalMål)

res <- mållista %>%                               # Beräknar antal mål per match.
       count(antalMål) %>%
       group_by(antalMål) %>%       
       mutate()


# ------------------------- Nedan: skapa två stapeldiagram

xPoisson    <- c(0:8)
sannPoisson <- dpois(xPoisson, totlambda)

ggplot() + 
  geom_bar(aes(x = res$antalMål, y = res$n/sum(res$n)), stat = 'identity', width = 0.6, alpha = 0.40, fill = 'red') +
  scale_x_continuous(breaks = res$antalMål) + xlab('Antal mål i fotbollsmatcher (VM 2026)') + ylab('') + theme(legend.position="none")  +
  annotate("text", x = 6, y = Inf, label="Röd: resultatet från VM", vjust = 4.5, size = 5.5, colour = "red") +
  annotate("text", x = 6, y = Inf, label="Blå: Poissonfördelning", vjust = 6.5, size = 5.5, colour="blue") +
  geom_bar(aes(x = xPoisson, y = sannPoisson), stat='identity', colour='black', fill = 'blue', width=0.3, alpha=0.30)



 2. Antal mål per 4-minuters intervall

Tidpunkten för varje mål finns i datamängden och grafen är antal mål fördelade i 4-minutersintervaller. Detta blir då en ny Poissonprocess:

# -------------------------------------------- Nedan graf för antal mål i 4-min perioder

cellbredd  <- 4           # Mniuter
cellnummer <- ceiling(Målminut/cellbredd)
cell_lista <- data.frame(cellnummer)

listaTidslot <- cell_lista %>%                            # Beräknar antal mål per tidslot.
                count(cellnummer) %>%
                group_by(cellnummer) %>%       
                mutate()

listaTidslot <- na.omit(listaTidslot)

listaTidslot <- subset(listaTidslot, listaTidslot$cellnummer < 25)         # Tar bort celler över 25.

tidSlot <- listaTidslot$cellnummer                          
antMålperSlot <- listaTidslot$n

#lambda <- totlambda/96 * cellbredd

lamEst <- mean(antMålperSlot)

minAntMål <- min(antMålperSlot)
maxAntMål <- max(antMålperSlot)

tv <- round(ppois(minAntMål, lamEst), 3)
th <- round(ppois(maxAntMål, lamEst, lower.tail = FALSE), 3)

minLabel <- paste0("Sannolikhet under linjen: ", tv)
maxLabel <- paste0("Sannolikhet över linjen: ", th)

målPerTid <- ggplot() + ylim(0, NA) + geom_hline(yintercept=minAntMål) + geom_hline(yintercept=maxAntMål) + 
                        geom_label(x=5, y=minAntMål, aes(label = minLabel)) + ylab("Antal mål") + xlab("Tidsintervall om 4 minuter") +
                        geom_label(x=5, y=maxAntMål, aes(label = maxLabel)) + 
                        geom_point(aes(tidSlot, antMålperSlot), size=2, col="red") + geom_line(aes(tidSlot, antMålperSlot), linewidth=0.5, col="blue")
målPerTid



 3. Tid till matchens första mål

En större svensk dagstidning uppmärksammar varje år tiden till allsvenskans första mål (se https://ovn.ing-stat.se [OE4]). Histogrammet nedan visar samma sak för VMs alla matcher. Här är mätningen tiden fram till första händelsen dvs första målet i matchen. Tidsdata brukar modelleras med den s.k. Weibullfördelningen och som anges med den blå kurvan. (Se https://ovn.ing-stat.se [OE7-OE10], [OE16], [7], [53]):

# ------------------------------------------------------- Grafer 'första målet'

library(weibullness)
library(MASS)

matchOchMål <- VM2026[, c('Match', 'Minut')]
matchOchMål <- na.omit(matchOchMål)

Mål_1 <- matchOchMål %>%
         group_by(Match) %>%
         summarize(min(Minut))

första  <-  as.vector(Mål_1[['min(Minut)']])           # Tid för första målet som numersik kolumn (vector).

arg   <- fitdistr(första, "weibull")                   # Beräknar parameterskattningar.
shape <-arg$estimate[1]                                # 
scale <-arg$estimate[2]

forstamal <- data.frame(första)

medel <- mean(första)

hist <- ggplot(forstamal, aes(x = första)) + geom_histogram(binwidth=6, color='black', alpha = 0.5, 
                                             boundary = 0, fill = "yellow", position = 'identity', aes(y = after_stat(density)))
hist <- hist + geom_function(fun=dweibull, linewidth=1.5, color='blue', args=list(shape, scale))
hist <- hist + labs(x = "Minuter till första mål i VM-matcher 2026", y = " ")
hist <- hist + theme(axis.text.x = element_text(size = 15), axis.text.y = element_text(size = 15),  
                     axis.title.x = element_text(size = 15), axis.title.y = element_text(size = 15))
hist <- hist + geom_point(x = medel, y = 0, shape = 17, colour = "red", size = 5.5)
hist <- hist + annotate("text", x = -Inf, y = Inf, label="Tid till VM-matchers första mål", vjust = 2.8, 
                        hjust = -0.80, size = 5.5, colour = "blue" )
hist




 4. ‘Probability plot’ för tid fram till första mål

Diagrammet nedan är en avbildning av Weibullfördelningens s.k. fördelningsfunktion men för läsbarhetens skull har X- och Y-axlarna manipulerats så att den teoretiska funktionen blir en rät linje i diagrammet. Punkterna är datamängden och dessa överensstämmer bra med den teoretiska fördelningen. Observera att Y-skalan är annorlunda nederst till vänster vilket gör att punkterna tycks avvika mer än övriga:

(I mer seriösa sammanhang ger kunskap om tidsmodellen många möjligheter: man kan använda den för att prediktera eller för att se om påstådda förändringar ger kortare leveranstider, längre tid mellan reklamationer, kortare kötider, etc i stället för att gissa eller lyssna på den som talar högst.)

# -------------------------------------------------------------------------------------
# Weibull probability plot

n <- length(första)                                     # Antal data.
data <- första
data <- sort(data)

data <- data + runif(n, 0, 1)

i <- c(1:n)
y <- i/n                        # Simulerad F(x)

logY <- -log10(1-y)             # Log Y-axel.

ticks <- c(0.01, 0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95)
tickstext <- as.character(100*ticks)
ticks <- -log10(1-ticks)

para  <- fitdistr(data, "weibull")
form  <- paste("Shape: ", round(para$estimate[1], 2))
skala <- paste("Scale: ", round(para$estimate[2], 2))

min <- min(data)
pmin <- pweibull(min, para$estimate[1], para$estimate[2])

max <- max(data)
pmax <- pweibull(max, para$estimate[1], para$estimate[2])

VMlabel <- "Tid till VM-matchers första mål"

Weibgraf <- ggplot() + xlab("Tid till VM-matchers första mål (min)") +
  geom_point(aes(data, logY), size = 2, col="red") + scale_x_log10() + scale_y_log10(breaks=ticks, labels=tickstext) + ylab("Procent") +
  annotate("text", x = Inf, y = Inf, label="Weibull - a probability plot", hjust = 1.7, vjust = 1.8, fontface = "bold", color = "blue", size = 5) +
  annotate("text", x = Inf, y = Inf, label=form,  hjust = 3.95, color = "red", size = 4.5, vjust = 4.5) +
  annotate("text", x = Inf, y = Inf, label=skala, hjust = 3.85, color = "red", size = 4.5, vjust = 6.5) +
  annotate("text", x = 8, y = 0.5, label=VMlabel, color = "blue", size = 5.) +
  geom_segment(aes(x = min, y = -log10(1-pmin), xend = max, yend = -log10(1-pmax)), color = "grey")
Weibgraf




 5. Teoretisk fördelning för skillnad mellan antal mål

Summan av flera Poissonprocesser är också en Poissonprocess. T.ex. kundströmmar från olika håll, reklamationsärenden från olika kundcentrum, etc. Men för skillnaden mellan två Poissonprocesser finns ingen teoretisk motsvarighet.
För att hitta den teoretiska fördelningen för skillnaden i antal mål per match måste man därför utgå ifrån grunderna. Det räcker dock med kunskap från grundskolans högstadium – enkel matematik och grunder i sannolikhet – för att göra detta:

# ------------------------------------------------------------- Skillnad i mål

hemma <- rep.int(c(0:10), 11)
borta <- rep(0:10, each = 11)
diff  <- hemma-borta
absDiff <- abs(diff)

lam <- totlambda

pH <- round(dpois(hemma, lam), 4)
pB <- round(dpois(borta, lam), 4)

pDiff <- round(pH*pB, 4)

HB <- data.frame(hemma, borta, diff, absDiff, pH, pB, pDiff)

distr <- HB %>%                   
         group_by(diff) %>%
         summarize(sum(pDiff))


xvarde <- distr$diff
yvarde <- distr$'sum(pDiff)'

Label1 <- "Teoretisk sannolikhetsfördelning"
Label2 <- "Målskillnad mellan lagen"


diaDiff <- ggplot() + 
           scale_x_continuous(breaks = xvarde) + xlab('Målskillnad mellan lagen, fotbollsmatcher VM 2026') + ylab('Sannolikhet') + 
           theme(legend.position="none") + geom_text(x=-5, y=0.17, aes(label = Label1), color="blue", size=4.5) + 
           geom_text(x=-5, y=0.16, aes(label = Label2), color="red", size=4) +
           geom_bar(aes(x = xvarde, y = yvarde), stat='identity', colour='black', fill = 'blue', width=0.5, alpha=0.30)
diaDiff




 6. Absolut skillnad i antal mål - utfall och teoretisk fördelning

Under punkt 5 ovan finns den teoretiska fördelningen för skillnad i antal. Denna skillnad kan ju vara både negativ och positiv. Om man i stället beräknar absolut skillnad, dvs alla värden är då positiva, erhålles den teoretiska fördelningen nedan, i blått. Utfallet från VMs matcher visas i den röda fördelningen med god överensstämmelse:

# ------------------------------------------------------------- Absolut skillnad i mål

antMål <- HB %>%                   
          group_by(absDiff) %>%
          summarize(sum(pDiff))

xvarde <- antMål$absDiff
yvarde <- antMål$'sum(pDiff)'

absMål    <- data.frame(match = VM2026$Match, HB = VM2026$H.B, VM2026$Minut)
antalNoll <- sum(is.na(VM2026$Minut))

absMål <- na.omit(absMål)                     # Tar bort 'missing values' ur data.

a      <- ifelse(absMål$HB == 'H', 1, -1)
absMål <- data.frame(absMål, a)

Mål00 <- absMål %>%                           # Målskillnad (absolut)
  group_by(match) %>%
  summarize(diffAntMål = abs(sum(a)))

res00 <- Mål00 %>%                            # Antal matcher med 0, 1, 2,... mål.
  count(diffAntMål) %>%
  group_by(diffAntMål) %>%       
  mutate()

#res00$n[1] <- res00$n[1] + antalNoll          # Adderar antal oavgjorda.

Label3 <- "Absolut målskillnad mellan lagen"
Label4 <- "Resultat från VM-matcher"

ggplot() + 
  geom_bar(aes(x = res00$diffAntMål, y = res00$n/sum(res00$n)), stat = 'identity', width = 0.6, alpha = 0.40, fill = 'red') +
  scale_x_continuous(breaks = res00$diffAntMål) + xlab('Skillnad antal mål i fotboll') + ylab('Sannolikhet') + theme(legend.position="none")  +
  annotate("text", x = 6, y = Inf, label=Label3, vjust = 4.5, size = 4.5, colour = "blue") +
  annotate("text", x = 6, y = Inf, label=Label4, vjust = 6.9, size = 4.0, colour="red") +
  geom_bar(aes(x = xvarde, y = yvarde), stat='identity', colour='black', fill = 'blue', width=0.3, alpha=0.30)




(The analyses have been performed by the ‘R’-software and with the graphical interface ‘R’-studio. Both these are available on the net. See https://www.indstat.se and the button [Statistikprogram - R] for installation.)

(See https://ovn.ing-stat.se for many more simulations and animations.)