Analysis of scores in a football match (using staircase)#

This example demonstrates how staircase can be used to mirror the functionality and analysis presented in the corresponding example with piso.

The Champions League quarter-final between Chelsea and Liverpool in 2009 is recognised as among the best games of all time. Liverpool scored twice in the first half in the 19th and 28th minute. Chelsea then opened their account in the second half with three unanswered goals in the 51st, 57th and 76th minute. Liverpool responded with two goals in the 81st and 83rd minute to put themselves ahead, however Chelsea drew with a goal in the 89th minute and advanced to the next stage on aggregate.

We start by importing pandas and staircase

In [1]: import pandas as pd

In [2]: import staircase as sc

For the analysis we will create a staircase.Stairs for each team, and wrap them up in a pandas.Series which is indexed by the club names. Using a Series in this way is by no means necessary but can be useful. We’ll create a function make_stairs which takes the minute marks of the goals and returns a staircase.Stairs. Each step function will be monotonically non-decreasing.

In [3]: def make_stairs(goal_time_mins):
   ...:     breaks = pd.to_timedelta(goal_time_mins, unit="min")
   ...:     return sc.Stairs(start=breaks).clip(pd.Timedelta(0), pd.Timedelta("90m"))
   ...: 

In [4]: scores = pd.Series(
   ...:     {
   ...:         "chelsea":make_stairs([51,57,76,89]),
   ...:         "liverpool":make_stairs([19,28,81,83]),
   ...:     }
   ...: )
   ...: 

In [5]: scores
Out[5]: 
chelsea      <staircase.Stairs, id=140108120977040>
liverpool    <staircase.Stairs, id=140108120970944>
dtype: object

To clarify we plot these step functions below.


../../_images/case_study_football_staircase.png

To enable analysis for separate halves of the game we’ll define a similar Series which defines the time intervals for each half with tuples of pandas.Timedeltas.

In [6]: halves = pd.Series(
   ...:     {
   ...:         "1st":(pd.Timedelta(0), pd.Timedelta("45m")),
   ...:         "2nd":(pd.Timedelta("45m"), pd.Timedelta("90m")),
   ...:     }
   ...: )
   ...: 

In [7]: halves
Out[7]: 
1st    (0 days 00:00:00, 0 days 00:45:00)
2nd    (0 days 00:45:00, 0 days 01:30:00)
dtype: object

We can now use our scores and halves Series to provide answers for miscellaneous questions. Note that comparing staircase.Stairs objects with relational operators produces boolean-valued step functions (Stairs objects). Finding the integral of these boolean step functions is equivalent to summing up lengths of intervals in the domain where the step function is equal to one.

How much game time did Chelsea lead for?

In [8]: (scores["chelsea"] > scores["liverpool"]).integral()
Out[8]: Timedelta('0 days 00:05:00')

How much game time did Liverpool lead for?

In [9]: (scores["chelsea"] < scores["liverpool"]).integral()
Out[9]: Timedelta('0 days 00:44:00')

How much game time were the teams tied for?

In [10]: (scores["chelsea"] == scores["liverpool"]).integral()
Out[10]: Timedelta('0 days 00:41:00')

How much game time in the first half were the teams tied for?

In [11]: (scores["chelsea"] == scores["liverpool"]).where(halves["1st"]).integral()
Out[11]: Timedelta('0 days 00:19:00')

For how long did Liverpool lead Chelsea by exactly one goal (split by half)?

In [12]: halves.apply(lambda x:
   ....:     (scores["liverpool"]==scores["chelsea"]+1).where(x).integral()
   ....: )
   ....: 
Out[12]: 
1st   0 days 00:09:00
2nd   0 days 00:12:00
dtype: timedelta64[ns]

What was the score at the 80 minute mark?

In [13]: sc.sample(scores, pd.Timedelta("80m"))
Out[13]: 
           0 days 01:20:00
chelsea                3.0
liverpool              2.0