|
| 1 | +.. _user_guide.football_staircase_example: |
| 2 | + |
| 3 | + |
| 4 | +Analysis of scores in a football match (using staircase) |
| 5 | +=========================================================== |
| 6 | + |
| 7 | +.. ipython:: python |
| 8 | + :suppress: |
| 9 | +
|
| 10 | + import matplotlib.pyplot as plt |
| 11 | + import matplotlib.ticker as ticker |
| 12 | + plt.style.use('seaborn') |
| 13 | +
|
| 14 | +This example demonstrates how :mod:`staircase` can be used to mirror the functionality |
| 15 | +and analysis presented in the :ref:`corresponding example with piso <user_guide.football_example>`. |
| 16 | + |
| 17 | + The Champions League quarter-final between Chelsea and Liverpool |
| 18 | + in 2009 is recognised as among the best games of all time. |
| 19 | + Liverpool scored twice in the first half in the 19th and 28th minute. |
| 20 | + Chelsea then opened their account in the second half with three |
| 21 | + unanswered goals in the 51st, 57th and 76th minute. Liverpool |
| 22 | + responded with two goals in the 81st and 83rd minute to put themselves |
| 23 | + ahead, however Chelsea drew with a goal in the 89th minute and advanced |
| 24 | + to the next stage on aggregate. |
| 25 | + |
| 26 | + |
| 27 | +We start by importing :mod:`pandas` and :mod:`staircase` |
| 28 | + |
| 29 | +.. ipython:: python |
| 30 | +
|
| 31 | + import pandas as pd |
| 32 | + import staircase as sc |
| 33 | +
|
| 34 | +
|
| 35 | +For the analysis we will create a :class:`staircase.Stairs` for each team, and wrap them up in a :class:`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 :class:`staircase.Stairs`. Each step function will be monotonically non-decreasing. |
| 36 | + |
| 37 | +.. ipython:: python |
| 38 | +
|
| 39 | + def make_stairs(goal_time_mins): |
| 40 | + breaks = pd.to_timedelta(goal_time_mins, unit="min") |
| 41 | + return sc.Stairs(start=breaks).clip(pd.Timedelta(0), pd.Timedelta("90m")) |
| 42 | +
|
| 43 | + scores = pd.Series( |
| 44 | + { |
| 45 | + "chelsea":make_stairs([51,57,76,89]), |
| 46 | + "liverpool":make_stairs([19,28,81,83]), |
| 47 | + } |
| 48 | + ) |
| 49 | + scores |
| 50 | +
|
| 51 | +
|
| 52 | +To clarify we plot these step functions below. |
| 53 | + |
| 54 | +.. ipython:: python |
| 55 | + :suppress: |
| 56 | +
|
| 57 | + fig, axes = plt.subplots(ncols=2, figsize=(8,3), sharey=True) |
| 58 | + vals = scores["chelsea"].step_values |
| 59 | + vals.index = vals.index/pd.Timedelta("1min") |
| 60 | + sc.Stairs.from_values(0, vals).plot(axes[0]) |
| 61 | + axes[0].set_title("Chelsea") |
| 62 | + axes[0].set_xlabel("time (mins)") |
| 63 | + axes[0].set_ylabel("score") |
| 64 | + axes[0].yaxis.set_major_locator(ticker.MultipleLocator()) |
| 65 | + axes[0].set_xlim(0,90) |
| 66 | + vals = scores["liverpool"].step_values |
| 67 | + vals.index = vals.index/pd.Timedelta("1min") |
| 68 | + sc.Stairs.from_values(0, vals).plot(axes[1]) |
| 69 | + axes[1].set_title("Liverpool") |
| 70 | + axes[1].set_xlabel("time (mins)") |
| 71 | + axes[1].set_ylabel("score") |
| 72 | + @savefig case_study_football_staircase.png |
| 73 | + plt.tight_layout(); |
| 74 | +
|
| 75 | +
|
| 76 | +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 :class:`pandas.Timedeltas`. |
| 77 | + |
| 78 | +.. ipython:: python |
| 79 | +
|
| 80 | + halves = pd.Series( |
| 81 | + { |
| 82 | + "1st":(pd.Timedelta(0), pd.Timedelta("45m")), |
| 83 | + "2nd":(pd.Timedelta("45m"), pd.Timedelta("90m")), |
| 84 | + } |
| 85 | + ) |
| 86 | + halves |
| 87 | +
|
| 88 | +
|
| 89 | +We can now use our *scores* and *halves* Series to provide answers for miscellaneous questions. Note that comparing :class:`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. |
| 90 | + |
| 91 | +**How much game time did Chelsea lead for?** |
| 92 | + |
| 93 | +.. ipython:: python |
| 94 | +
|
| 95 | + (scores["chelsea"] > scores["liverpool"]).integral() |
| 96 | +
|
| 97 | +
|
| 98 | +**How much game time did Liverpool lead for?** |
| 99 | + |
| 100 | +.. ipython:: python |
| 101 | +
|
| 102 | + (scores["chelsea"] < scores["liverpool"]).integral() |
| 103 | +
|
| 104 | +**How much game time were the teams tied for?** |
| 105 | + |
| 106 | +.. ipython:: python |
| 107 | +
|
| 108 | + (scores["chelsea"] == scores["liverpool"]).integral() |
| 109 | +
|
| 110 | +**How much game time in the first half were the teams tied for?** |
| 111 | + |
| 112 | +.. ipython:: python |
| 113 | +
|
| 114 | + (scores["chelsea"] == scores["liverpool"]).where(halves["1st"]).integral() |
| 115 | +
|
| 116 | +**For how long did Liverpool lead Chelsea by exactly one goal (split by half)?** |
| 117 | + |
| 118 | +.. ipython:: python |
| 119 | +
|
| 120 | + halves.apply(lambda x: |
| 121 | + (scores["liverpool"]==scores["chelsea"]+1).where(x).integral() |
| 122 | + ) |
| 123 | +
|
| 124 | +**What was the score at the 80 minute mark?** |
| 125 | + |
| 126 | +.. ipython:: python |
| 127 | +
|
| 128 | + sc.sample(scores, pd.Timedelta("80m")) |
0 commit comments