Revision: 2016-11-17
URL: http://bbinsomnia.info/
Date: 2016-11-1

STARTERS vs. BENCH

Distribution of Minutes in ABA League Season 2015-16

Miha Peče

If you’re coming from NBA background, it’s probably normal that you perceive enormous gap between starter and bench player. Demarcation line is quite material, visible in earnings of particular players, and is also often primary reason behind the disappointments and frustrations, which from time to time show up in locker room or even reflect in the game.

In Europe situation is not so polarised (with inevitable exceptions). On the “rich” side of EU basketball, absence of cap limit probably facilitate ills of bench status, and everywhere else quality difference among players is probably smaller and starter status consequently less rigid or influential on distribution of available minutes to share between players.

This observations are just part of broad context from which I derived my curiosity for distribution of playing minutes amongst players. In this exploration my focus is on average playing time for starters and bench players. I will do calculations on 2015-16 season ABA League dataset. For this occasion I will leave out overtimes, as I hope, they will not distort results too much. Maybe I will do follow up latter, which will check this (somehow brave) prediction.

First let’s calculate starters' average minutes per game. Let’s do this in old way, purely with SQL.

In [2]:
import sqlite3, sys
import pandas as pd
import numpy as np
import texttable as tt

%matplotlib inline
import matplotlib.pyplot as plt
In [3]:
plt.style.use('ggplot')
conn = sqlite3.connect('../sql/aba_liga2015.sqlite')
In [4]:
sql_players_lg = "SELECT AVG(MP) FROM scr_game_players WHERE STARTER=1 AND ROUND='regular' AND MP<>0"
c = conn.execute(sql_players_lg)
lg_avg_st_min = c.fetchone()
lg_avg_st_min = lg_avg_st_min[0]
# Converting percentages into seconds and rounding on 2 decimals
lg_avg_st_min = (int(lg_avg_st_min) + (lg_avg_st_min-int(lg_avg_st_min)) * 0.6)
round(lg_avg_st_min, 2)
Out[4]:
24.4

In ABA League 2015-16 season starter got on average 24:40 min of playing time. If we compare this number with total game minutes for one player (40 min), that is 61,6% of available time.

Now let's check starter minutes distribution amongst all teams.

In [5]:
sql_starters_lg = "SELECT AVG(MP) AS AMP, TM FROM scr_game_players WHERE STARTER=1 \
                   AND ROUND='regular' AND MP<>0 GROUP BY TM ORDER BY AMP DESC"
c = conn.execute(sql_starters_lg)
lg_start_result = c.fetchall()

# Converting percentages into seconds
lg_avg_st_team_min = []
for amp, tm in lg_start_result:
    lg_avg_st_team_min.append([round((int(amp) + (amp-int(amp)) * 0.6), 2), tm])

Let's draw result in table first ...

In [6]:
tb_teams = tt.Texttable()
tb_teams.header(["Avg. min.", "Team"])
[tb_teams.add_row(x) for x in lg_avg_st_team_min]
print(tb_teams.draw())
+-----------+----------------+
| Avg. min. |      Team      |
+===========+================+
| 27.080    | Budućnost      |
+-----------+----------------+
| 26.570    | MZT Skopje     |
+-----------+----------------+
| 25.540    | Sutjeska       |
+-----------+----------------+
| 25.100    | Mega Leks      |
+-----------+----------------+
| 25.040    | Cibona         |
+-----------+----------------+
| 25.030    | Metalac        |
+-----------+----------------+
| 24.540    | Krka           |
+-----------+----------------+
| 24.350    | Igokea         |
+-----------+----------------+
| 24.350    | Zadar          |
+-----------+----------------+
| 24.100    | Tajfun         |
+-----------+----------------+
| 23.400    | Partizan       |
+-----------+----------------+
| 22.590    | Union Olimpija |
+-----------+----------------+
| 22.500    | Cedevita       |
+-----------+----------------+
| 22.250    | Crvena Zvezda  |
+-----------+----------------+

... and also in bar chart.

In [7]:
t1 = [(round(x[0], 2)) for x in lg_avg_st_team_min]
t2 = [x[1] for x in lg_avg_st_team_min]
pos = np.arange(len(t2))
bar_width = 0.85

plt.bar(pos, t1, bar_width, color="blue", align="center")
plt.xticks(pos, t2, rotation=90)
plt.show()

Let's also check bench minutes ...

In [8]:
sql_bench_lg = "SELECT AVG(MP) AS AMP, TM FROM scr_game_players WHERE STARTER=0 \
                AND ROUND='regular' AND MP<>0 GROUP BY TM ORDER BY AMP DESC"
c = conn.execute(sql_bench_lg)
lg_bench_result = c.fetchall()

# Converting percentages into seconds
lg_avg_bnc_team_min = []
for amp, tm in lg_bench_result:
    lg_avg_bnc_team_min.append([round((int(amp) + (amp-int(amp)) * 0.6), 2), tm])
In [9]:
tb_teams_bnc = tt.Texttable()
tb_teams_bnc.header(["Avg. min.", "Team"])
[tb_teams_bnc.add_row(x) for x in lg_avg_bnc_team_min]
print(tb_teams_bnc.draw())
+-----------+----------------+
| Avg. min. |      Team      |
+===========+================+
| 16.210    | Mega Leks      |
+-----------+----------------+
| 16.170    | Union Olimpija |
+-----------+----------------+
| 16.070    | Cedevita       |
+-----------+----------------+
| 15.390    | Cibona         |
+-----------+----------------+
| 15.370    | Tajfun         |
+-----------+----------------+
| 15.370    | Partizan       |
+-----------+----------------+
| 15.110    | Zadar          |
+-----------+----------------+
| 15.020    | Sutjeska       |
+-----------+----------------+
| 14.570    | Krka           |
+-----------+----------------+
| 14.550    | Igokea         |
+-----------+----------------+
| 14.380    | Crvena Zvezda  |
+-----------+----------------+
| 13.590    | Metalac        |
+-----------+----------------+
| 13.280    | Budućnost      |
+-----------+----------------+
| 13.030    | MZT Skopje     |
+-----------+----------------+
In [13]:
#Averagge playing time for bench player
avg_lg_bench_min = np.mean([x[0] for x in lg_bench_result])
# Converting percentages into seconds and rounding on 2 decimals
avg_lg_bench_min = (int(avg_lg_bench_min) + (avg_lg_bench_min-int(avg_lg_bench_min)) * 0.6)
round(avg_lg_bench_min, 2)
Out[13]:
15.039999999999999

Bench players got, on average, 15:04 min of playing time per game. Of course, average is dependent on how many players divided the reminder of minutes starters left — it's probably more indicative of how many players is coach using per game.

To complement average team bench minutes we can check total "bench" appearances per team.

In [14]:
sql_bench_tot = "SELECT COUNT(*) AS COUNT, TM FROM scr_game_players WHERE STARTER=0 AND ROUND='regular' \
                AND MP<>0 GROUP BY TM ORDER by COUNT DESC"
c = conn.execute(sql_bench_tot)
lg_tot_bnc_team_min = c.fetchall()
In [15]:
tb_teams_tot = tt.Texttable()
tb_teams_tot.header(["# of bench players app.", "Team"])
[tb_teams_tot.add_row(x) for x in lg_tot_bnc_team_min]
print(tb_teams_tot.draw())
+-------------------------+----------------+
| # of bench players app. |      Team      |
+=========================+================+
| 158                     | Crvena Zvezda  |
+-------------------------+----------------+
| 144                     | Partizan       |
+-------------------------+----------------+
| 142                     | Union Olimpija |
+-------------------------+----------------+
| 140                     | Cedevita       |
+-------------------------+----------------+
| 139                     | Metalac        |
+-------------------------+----------------+
| 138                     | Krka           |
+-------------------------+----------------+
| 136                     | Igokea         |
+-------------------------+----------------+
| 135                     | Tajfun         |
+-------------------------+----------------+
| 132                     | Zadar          |
+-------------------------+----------------+
| 130                     | MZT Skopje     |
+-------------------------+----------------+
| 128                     | Budućnost      |
+-------------------------+----------------+
| 127                     | Sutjeska       |
+-------------------------+----------------+
| 124                     | Cibona         |
+-------------------------+----------------+
| 121                     | Mega Leks      |
+-------------------------+----------------+

... or maybe more usable for analysis, how many players per game is coach using.

In [16]:
sql_play_per_game = "SELECT COUNT(*) AS COUNT, TM FROM scr_game_players WHERE ROUND='regular' AND MP<>0 \
                     GROUP BY NumG, TM"
sql_num_play_game = pd.read_sql(sql_play_per_game, conn)
df_num_play_game = sql_num_play_game.groupby(sql_num_play_game['TM']).mean().sort_values('COUNT', ascending=False)
df_num_play_game['COUNT'] = df_num_play_game['COUNT'].apply(lambda x: int(x) + (x-int(x)) * 0.6).round(2)
df_num_play_game
Out[16]:
COUNT
TM
Crvena Zvezda 11.05
Partizan 10.30
Union Olimpija 10.28
Cedevita 10.23
Metalac 10.21
Krka 10.18
Igokea 10.14
Tajfun 10.12
Zadar 10.05
MZT Skopje 10.00
Budućnost 9.55
Sutjeska 9.53
Cibona 9.46
Mega Leks 9.39

Now, let's calculate standard deviation amongst starters of different teams to see the amount of variation in minutes distribution. Population is small, but hopefully this could serve as kind of auxiliary metric.

I will use Pandas for calculation.

In [17]:
sql_pd = "SELECT NAME, MP, TM, STARTER, POSITION FROM scr_game_players WHERE ROUND='regular' "
df_main = pd.read_sql(sql_pd, conn)

teams = ('Union Olimpija', 'Cibona', 'Zadar', 'Budućnost', 'Cedevita', 'Crvena Zvezda', 'Igokea', 'Krka',
          'Mega Leks', 'Sutjeska', 'MZT Skopje', 'Partizan', 'Tajfun', 'Metalac')
df_teams = pd.DataFrame()
for team in teams:
    df_tmp = df_main[(df_main["TM"] == team) & (df_main["STARTER"]==1) &(df_main["MP"]!=0)]
    ser = df_tmp["MP"].describe().round(2)
    df_teams[team] = ser
# Axis change and sort by standard deviation
df_teams.transpose().sort_values("std")
Out[17]:
count mean std min 25% 50% 75% max
Crvena Zvezda 130.0 22.42 6.57 5.0 18.25 22.0 27.00 36.0
Budućnost 130.0 27.13 6.98 11.0 22.00 27.0 32.75 43.0
Cedevita 130.0 22.83 6.98 4.0 18.00 23.0 27.00 37.0
MZT Skopje 130.0 26.95 7.06 5.0 23.00 28.5 32.00 39.0
Metalac 130.0 25.05 7.18 6.0 20.25 26.0 30.00 39.0
Krka 130.0 24.89 7.21 1.0 21.25 26.0 29.00 39.0
Zadar 130.0 24.58 7.24 7.0 19.00 25.5 30.00 40.0
Tajfun 130.0 24.17 7.36 6.0 19.00 25.0 29.75 41.0
Mega Leks 130.0 25.17 7.69 1.0 20.25 26.0 31.75 41.0
Igokea 130.0 24.58 7.96 4.0 21.00 26.0 29.75 40.0
Sutjeska 130.0 25.89 7.99 3.0 22.00 27.0 31.00 41.0
Cibona 130.0 25.07 8.05 3.0 21.00 26.0 31.00 38.0
Union Olimpija 130.0 22.98 8.44 2.0 18.00 24.0 29.00 52.0
Partizan 129.0 23.66 8.63 5.0 19.00 25.0 29.00 55.0

How was distribution of minutes in playoff?

In [18]:
sql_playoff = "SELECT NAME, MP, TM, STARTER, POSITION FROM scr_game_players WHERE ROUND='playoff' "
df_main = pd.read_sql(sql_playoff, conn)

tm_playoff = ('Budućnost', 'Cedevita', 'Crvena Zvezda', 'Mega Leks')
df_playoff = pd.DataFrame()
for team in tm_playoff:
    df_tmp = df_main[(df_main["TM"] == team) & (df_main["STARTER"]==1) &(df_main["MP"]!=0)]
    ser = df_tmp["MP"].describe().round(2)
    df_playoff[team] = ser
# Axis change and sort by standard deviation
df_playoff = df_playoff.transpose().sort_values("mean")[["mean", "50%"]]
df_playoff['mean'] = df_playoff['mean'].apply(lambda x: int(x) + (x-int(x)) * 0.6).round(2)
df_playoff['50%'] = df_playoff['50%'].apply(lambda x: int(x) + (x-int(x)) * 0.6).round(2)
df_playoff
Out[18]:
mean 50%
Crvena Zvezda 23.02 22.0
Budućnost 26.18 24.3
Cedevita 27.12 26.3
Mega Leks 30.58 33.0

Insights

Let's combine acquired numbers with some personal game impressions and observations.

My first inquiry of minutes distribution last season was motivated with Union Olimpija case, where I had difficulties to follow logic in lineups and player changes during game. Data certainly show more even distribution between starters and bench with comparatively big variability from game to game. Of course, I can't claim that 8.44 standard deviation and 23 minutes per game for starters are numbers, which per se reveal this fact. Assessing more comparatively, Olimpija had deep bench with 142 appearances of bench players, and it also had 2nd highest average of bench playing minutes.

Was this dispersion of available minutes harmful for Olimpija's overall standing? I would more agree than disagree with this hypothesis, but I can accept some objective reasons that probably convinced coach Potočnik for this kind of strategy. For instance, he got only two exceptional starters, followed by rather average or in-experienced players. Maybe even more important, he followed also broader team strategy to develop young talents.

You can also make some conclusions for cluster of first three leading teams, which usually have big ambitions in euro-level competitions (Euroleague, Eurocup). Crvena Zvezda and Cedevita had the most balanced distribution of minutes, you could presume that they were resting starters or - with different words - their bench players were good enough for quality level other teams had. Playoff numbers confirm this view, as Cedevita raised average starter minutes considerably and Budućnost stayed on their "max" level (more latter). Mega Leks numbers are skewed a little because of injuries in playoff, and consequently also numbers of Crvena Zvezda, who easily walked through playoff with some blow-out wins. They obviously didn't need a lot of help from starters.

Budućnost is special case. It seems they are more focused on regional ABA League, not so much on Europe level competition, and for sure they don't develop young guys. Consequently, they dominate in regular rounds and disappoint in playoffs. They were also not successful in Eurocup, where they were eliminated in the first round.

Numbers above also detect some teams, which have few or none above-average or average players in roster, and "deep" bench of rarely used players. I remember, for instance, when Zadar played in Ljubljana, their point guard, Cosey Glenn, played the whole game. (They are still using the same strategy in this season, as they played a player for 40 min again already four times.) If combining average starter minutes with standard deviation, I would include in this class also MZT and Metalac, but probably more teams could be positioned here.

Just one remark for the end. To really get more holistic overview of roster minutes distribution, you would have to check also some others metrics. For instance, you would have to get distributions inside particular teams, which I will try next time.

Revisions

  • 2017-11-17. Added conversion from percentages into seconds and calculation for average number of players per team used in one game.