Skip to content

Commit 7ea90b8

Browse files
author
Xing Han Lu
authored
Merge pull request #584 from plotly/update-soccer-analytics
Update soccer analytics app
2 parents 4f2d9c3 + 149e84d commit 7ea90b8

File tree

4 files changed

+112
-31
lines changed

4 files changed

+112
-31
lines changed

apps/dash-soccer-analytics/app.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
app.layout = dbc.Container(
8888
fluid=True,
8989
children=[
90-
html.Header([html.H3("Soccer Match Analysis")]),
90+
html.Header([html.H3("Match Analysis Tool")]),
9191
dbc.Card(
9292
dbc.Row([dbc.Col(c) for c in static_graph_controls], form=True), body=True
9393
),
@@ -349,7 +349,7 @@ def event_graph(event_file, team):
349349
fig_crosses = plotEvents("Crosses", event_file, team, "Home")
350350
fig_set_plays = plotEvents("Set Plays", event_file, team, "Home")
351351
fig_progressive_passes = plotEvents(
352-
"Progressive Passes Into Final 3rd", event_file, team, "Home"
352+
"Progressive Passes", event_file, team, "Home"
353353
)
354354
for x in [
355355
fig_shots,
@@ -433,6 +433,8 @@ def game_simulation_graph(n_clicks, speed, filename):
433433
plot_bgcolor="rgba(0, 0, 0, 0)",
434434
paper_bgcolor="rgba(0, 0, 0, 0)",
435435
)
436+
fig["layout"]["template"]["data"]["scatter"][0]["marker"]["line"]["color"] = "white"
437+
fig["layout"]["template"]["data"]["scatter"][0]["marker"]["opacity"] = 0.9
436438
return fig
437439

438440

apps/dash-soccer-analytics/event_plotter.py

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def find_assists(df):
5454

5555

5656
# Locate and build a dataframe of all set plays, ignoring kick-offs and throw-ins
57-
def find_set_plays(df):
57+
def find_set_plays(df, mode):
5858
sp_df = pd.DataFrame()
5959

6060
count = 0
@@ -100,9 +100,14 @@ def find_set_plays(df):
100100
),
101101
ignore_index=True,
102102
)
103+
if mode == "progressive":
104+
df = df.drop(index + 1)
103105
except Exception as e:
104106
print(e)
105107

108+
if mode == "progressive":
109+
sp_df = df
110+
106111
sp_df.loc[sp_df.To.isnull(), "Type"] = "Incomplete"
107112
return sp_df
108113

@@ -130,7 +135,7 @@ def left_justify_events(df, team_on_left):
130135
return df
131136

132137

133-
# Once number of clusters is auto-calculated, graph the cluseters
138+
# Once number of clusters is auto-calculated, graph the clusters
134139
def create_cluster_graph(df, num_clusters):
135140
# creates a new trace for each set of data
136141
fig = make_subplots(
@@ -206,6 +211,67 @@ def drawAnnotations(df):
206211
return annotations_list
207212

208213

214+
def find_progressive_passes(df):
215+
# df = df.loc[(df['End_X'] - df['location_x']) > 1000] # limit passes to those greater than 10M forward
216+
df_own_half = df.loc[
217+
(df["End_X"] < 0.5) & (df["Start_X"] < 0.5)
218+
] # passes in own half
219+
df_diff_half = df.loc[
220+
(df["End_X"] > 0.5) & (df["Start_X"] < 0.5)
221+
] # passes in different half
222+
df_opp_half = df.loc[
223+
(df["End_X"] > 0.5) & (df["Start_X"] > 0.5)
224+
] # passes in opponent's half
225+
goal_x = float(1)
226+
goal_y = float(0.5)
227+
228+
# Passes in own half
229+
if len(df_own_half) > 0:
230+
# dist = math.hypot(x2 - x1, y2 - y1)
231+
df_own_half["orig_distance_to_goal"] = df_own_half.apply(
232+
lambda x: math.hypot(x["Start_X"] - goal_x, x["Start_Y"] - goal_y), axis=1
233+
)
234+
df_own_half["end_distance_to_goal"] = df_own_half.apply(
235+
lambda x: math.hypot(x["End_X"] - goal_x, x["End_Y"] - goal_y), axis=1
236+
)
237+
df_own_half["distance"] = (
238+
df_own_half["orig_distance_to_goal"] - df_own_half["end_distance_to_goal"]
239+
)
240+
df_own_half = df_own_half.loc[(df_own_half["distance"]) >= 0.30]
241+
242+
# Passes in both halves
243+
if len(df_diff_half) > 0:
244+
df_diff_half["orig_distance_to_goal"] = df_diff_half.apply(
245+
lambda x: math.hypot(x["Start_X"] - goal_x, x["Start_Y"] - goal_y), axis=1
246+
)
247+
df_diff_half["end_distance_to_goal"] = df_diff_half.apply(
248+
lambda x: math.hypot(x["End_X"] - goal_x, x["End_Y"] - goal_y), axis=1
249+
)
250+
251+
df_diff_half["distance"] = (
252+
df_diff_half["orig_distance_to_goal"] - df_diff_half["end_distance_to_goal"]
253+
)
254+
df_diff_half = df_diff_half.loc[(df_diff_half["distance"]) >= 0.15]
255+
256+
# Passes in opposition half
257+
if len(df_opp_half) > 0:
258+
df_opp_half["orig_distance_to_goal"] = df_opp_half.apply(
259+
lambda x: math.hypot(x["Start_X"] - goal_x, x["Start_Y"] - goal_y), axis=1
260+
)
261+
df_opp_half["end_distance_to_goal"] = df_opp_half.apply(
262+
lambda x: math.hypot(x["End_X"] - goal_x, x["End_Y"] - goal_y), axis=1
263+
)
264+
df_opp_half["distance"] = (
265+
df_opp_half["orig_distance_to_goal"] - df_opp_half["end_distance_to_goal"]
266+
)
267+
df_opp_half = df_opp_half.loc[(df_opp_half["distance"]) >= 0.12]
268+
269+
df_list = [df_own_half, df_diff_half, df_opp_half] # List of your dataframes
270+
df_combo = pd.concat(df_list)
271+
272+
return df_combo
273+
274+
209275
# Main function - graph all football events which occur in a match
210276
def plotEvents(eventType, filename, team, team_on_left):
211277
# Read in event csv data file
@@ -225,7 +291,7 @@ def plotEvents(eventType, filename, team, team_on_left):
225291

226292
# For events involving the graphing of movement of the ball from one location to another
227293
if (
228-
(eventType == "Progressive Passes Into Final 3rd")
294+
(eventType == "Progressive Passes")
229295
or (eventType == "Crosses")
230296
or (eventType == "Set Plays")
231297
or (eventType == "Assists to Shots")
@@ -235,14 +301,15 @@ def plotEvents(eventType, filename, team, team_on_left):
235301
if eventType == "Assists to Shots":
236302
df = find_assists(events_df)
237303
elif eventType == "Set Plays":
238-
df = find_set_plays(events_df)
239-
elif eventType == "Progressive Passes Into Final 3rd":
240-
df = events_df.loc[events_df["Type"] == "PASS"]
304+
df = find_set_plays(events_df, "normal")
305+
elif eventType == "Progressive Passes":
306+
df = find_set_plays(
307+
events_df, "progressive"
308+
) # take out set plays as they include corners and throw-ins
309+
df = df[(df["Start_Y"] > 0) & (df["Start_Y"] < 1)]
310+
df = df.loc[events_df["Type"] == "PASS"]
241311
df.reset_index(drop=True, inplace=True)
242-
df = df.loc[
243-
(df["End_X"] - df["Start_X"]) > 0.1
244-
] # limit passes to those greater than 10M forward
245-
df = df.loc[df["End_X"] > 0.7]
312+
df = find_progressive_passes(df)
246313
elif eventType == "Crosses":
247314
df = events_df.loc[events_df["Subtype"].str.contains("CROSS", na=False)]
248315
df.reset_index(drop=True, inplace=True)
@@ -279,7 +346,7 @@ def plotEvents(eventType, filename, team, team_on_left):
279346
"Crosses",
280347
"Set Plays",
281348
"Assists to Shots",
282-
"Progressive Passes Into Final 3rd",
349+
"Progressive Passes",
283350
]:
284351
colorfactor = df["Type"]
285352
fig = px.scatter(
@@ -299,7 +366,6 @@ def plotEvents(eventType, filename, team, team_on_left):
299366
"Start_X": False,
300367
"Start_Y": False,
301368
"size": False,
302-
"Type": False,
303369
"From": True,
304370
"To": True,
305371
},
@@ -377,6 +443,14 @@ def plotEvents(eventType, filename, team, team_on_left):
377443
# Metrica data starts 0, 0 at top left corner. Need to account for that or markers will be wrong
378444
fig.update_yaxes(autorange="reversed")
379445

446+
# Add corner flags to prevent zoom and pitch distortion
447+
fig.add_scatter(
448+
x=[0, 0, 1, 1],
449+
y=[0, 1, 0, 1],
450+
mode="markers",
451+
marker=dict(size=2, color="grey"),
452+
)
453+
380454
# Remove side color scale and hide zero and gridlines
381455
fig.update_layout(
382456
coloraxis_showscale=False,
@@ -423,7 +497,7 @@ def plotEvents(eventType, filename, team, team_on_left):
423497
fig.update_xaxes(title_text="")
424498
fig.update_yaxes(title_text="")
425499
image_file = "assets/Pitch.png"
426-
fig.update_yaxes(scaleanchor="x", scaleratio=0.65)
500+
fig.update_yaxes(scaleanchor="x", scaleratio=0.70)
427501

428502
from PIL import Image
429503

apps/dash-soccer-analytics/initial_figures.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
# Create initial placeholder figure for game simulator
66
def initial_figure_simulator():
7-
fig = px.scatter(x=[0, 0, 105, 105], y=[69, -2, 69, -2])
8-
fig.update_layout(xaxis=dict(range=[-4, 110]))
9-
fig.update_layout(yaxis=dict(range=[-4, 72]))
7+
# fig = px.scatter(x=[0, 0, 105, 105], y=[69, -2, 69, -2])
8+
fig = px.scatter(x=[0, 0, 1, 1], y=[0, 1, 0, 1])
9+
fig.update_layout(xaxis=dict(range=[0, 1]))
10+
fig.update_layout(yaxis=dict(range=[0, 1]))
1011
fig.update_traces(marker=dict(color="white", size=6))
1112

1213
# Remove side color scale and hide zero and gridlines
@@ -40,16 +41,16 @@ def initial_figure_simulator():
4041
xref="x",
4142
yref="y",
4243
x=0,
43-
y=69,
44-
sizex=105,
45-
sizey=71,
44+
y=1,
45+
sizex=1,
46+
sizey=1,
4647
sizing="stretch",
4748
opacity=0.7,
4849
layer="below",
4950
)
5051
)
5152

52-
fig.update_yaxes(scaleanchor="x", scaleratio=1)
53+
fig.update_yaxes(scaleanchor="x", scaleratio=0.70)
5354

5455
fig.update_layout(autosize=True)
5556

@@ -64,11 +65,11 @@ def initial_figure_simulator():
6465

6566
# Create initial placeholder figure for event plot
6667
def initial_figure_events():
67-
fig = px.scatter(x=[0], y=[0])
68-
68+
# fig = px.scatter(x=[0], y=[0])
69+
fig = px.scatter(x=[0, 0, 1, 1], y=[0, 1, 0, 1])
6970
fig.update_traces(marker=dict(color="white", size=6))
70-
fig.update_layout(yaxis=dict(range=[-3650, 3650]))
71-
fig.update_layout(xaxis=dict(range=[-5300, 5300]))
71+
fig.update_layout(yaxis=dict(range=[0, 1]))
72+
fig.update_layout(xaxis=dict(range=[0, 1]))
7273
fig.update_layout(margin=dict(l=10, r=100, b=10, t=45))
7374

7475
fig.update_layout(modebar=dict(bgcolor="rgba(0, 0, 0, 0)"))
@@ -93,7 +94,7 @@ def initial_figure_events():
9394
fig.update_xaxes(title_text="")
9495
fig.update_yaxes(title_text="")
9596
fig.update_xaxes(fixedrange=True)
96-
fig.update_yaxes(scaleanchor="x", scaleratio=1)
97+
fig.update_yaxes(scaleanchor="x", scaleratio=0.70)
9798
fig.update_layout(margin=dict(l=10, r=30, b=30, t=30), autosize=True)
9899
image_file = "assets/Pitch.png"
99100

@@ -105,10 +106,10 @@ def initial_figure_events():
105106
source=img,
106107
xref="x",
107108
yref="y",
108-
x=-5150,
109-
y=3685,
110-
sizex=10450,
111-
sizey=7340,
109+
x=0,
110+
y=1,
111+
sizex=1,
112+
sizey=1,
112113
sizing="stretch",
113114
opacity=0.8,
114115
layer="below",

apps/dash-soccer-analytics/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
--extra-index-url=https://dash-playground.plotly.host/Docs/packages
12
dash==1.19.0
23
gunicorn==20.0.4
34
plotly~=4.14.3
@@ -9,3 +10,6 @@ dash-daq==0.5.0
910
dash-bootstrap-components==0.10.7
1011
dash-html-components==1.1.2
1112
dash-core-components==1.15.0
13+
14+
15+

0 commit comments

Comments
 (0)