Spaces:
Sleeping
Sleeping
start
Browse files- Pitch3D.py +369 -0
- app.py +266 -0
- config_location_player.py +77 -0
- functions.py +689 -0
- requirements.txt +7 -0
- stats_manager.py +154 -0
Pitch3D.py
ADDED
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# import numpy as np
|
3 |
+
# import streamlit as st
|
4 |
+
# import plotly.express as px
|
5 |
+
# import plotly.graph_objects as go
|
6 |
+
# import pandas as pd
|
7 |
+
|
8 |
+
# # Soccer field dimensions (in meters)
|
9 |
+
# WIDTH = 80 # Width of the field
|
10 |
+
# LENGTH = 120 # Length of the field
|
11 |
+
# GOAL_HEIGHT = 2.44 # Standard goal height
|
12 |
+
# PENALTY_AREA_WIDTH = 40.3
|
13 |
+
# PENALTY_AREA_DEPTH = 16.5
|
14 |
+
# GOAL_AREA_WIDTH = 18.32
|
15 |
+
# GOAL_AREA_DEPTH = 5.5
|
16 |
+
# GOAL_WIDTH = 7.32 # Standard width of a soccer goal
|
17 |
+
|
18 |
+
# def create_field_df():
|
19 |
+
# """Create dataframes for different parts of the soccer field."""
|
20 |
+
# field_perimeter_bounds = [[0, 0, 0], [WIDTH, 0, 0], [WIDTH, LENGTH, 0], [0, LENGTH, 0], [0, 0, 0]]
|
21 |
+
# field_df = pd.DataFrame(field_perimeter_bounds, columns=['x', 'y', 'z'])
|
22 |
+
# field_df['line_group'] = 'field_perimeter'
|
23 |
+
# field_df['color'] = 'field'
|
24 |
+
|
25 |
+
# half_field_bounds = [[0, LENGTH / 2, 0], [WIDTH, LENGTH / 2, 0]]
|
26 |
+
# half_df = pd.DataFrame(half_field_bounds, columns=['x', 'y', 'z'])
|
27 |
+
# half_df['line_group'] = 'half_field'
|
28 |
+
# half_df['color'] = 'field'
|
29 |
+
|
30 |
+
# left_penalty_df = create_rectangle_df((WIDTH - PENALTY_AREA_WIDTH) / 2, 0, PENALTY_AREA_WIDTH, PENALTY_AREA_DEPTH, 'left_penalty_area')
|
31 |
+
# right_penalty_df = create_rectangle_df((WIDTH - PENALTY_AREA_WIDTH) / 2, LENGTH - PENALTY_AREA_DEPTH, PENALTY_AREA_WIDTH, PENALTY_AREA_DEPTH, 'right_penalty_area')
|
32 |
+
# left_goal_df = create_rectangle_df((WIDTH - GOAL_AREA_WIDTH) / 2, 0, GOAL_AREA_WIDTH, GOAL_AREA_DEPTH, 'left_goal_area')
|
33 |
+
# right_goal_df = create_rectangle_df((WIDTH - GOAL_AREA_WIDTH) / 2, LENGTH - GOAL_AREA_DEPTH, GOAL_AREA_WIDTH, GOAL_AREA_DEPTH, 'right_goal_area')
|
34 |
+
|
35 |
+
# return pd.concat([field_df, half_df, left_penalty_df, right_penalty_df, left_goal_df, right_goal_df])
|
36 |
+
|
37 |
+
# def create_rectangle_df(start_x, start_y, width, height, line_group):
|
38 |
+
# """Create a dataframe representing a rectangle on the field."""
|
39 |
+
# rectangle_bounds = [
|
40 |
+
# [start_x, start_y, 0],
|
41 |
+
# [start_x + width, start_y, 0],
|
42 |
+
# [start_x + width, start_y + height, 0],
|
43 |
+
# [start_x, start_y + height, 0],
|
44 |
+
# [start_x, start_y, 0]
|
45 |
+
# ]
|
46 |
+
# df = pd.DataFrame(rectangle_bounds, columns=['x', 'y', 'z'])
|
47 |
+
# df['line_group'] = line_group
|
48 |
+
# df['color'] = 'field'
|
49 |
+
# return df
|
50 |
+
|
51 |
+
# def create_center_circle():
|
52 |
+
# """Create a 3D line trace for the center circle."""
|
53 |
+
# theta = np.linspace(0, 2 * np.pi, 100)
|
54 |
+
# x = [(WIDTH / 2) + (9.15 * np.cos(t)) for t in theta]
|
55 |
+
# y = [(LENGTH / 2) + (9.15 * np.sin(t)) for t in theta]
|
56 |
+
# z = [0] * 100
|
57 |
+
# return go.Scatter3d(x=x, y=y, z=z, mode='lines', line=dict(color='white', width=2))
|
58 |
+
|
59 |
+
# def create_goalposts():
|
60 |
+
# """Create goalpost lines for both ends of the field."""
|
61 |
+
# goalposts = []
|
62 |
+
|
63 |
+
# goalposts.extend([
|
64 |
+
# go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) - (GOAL_WIDTH / 2)], y=[LENGTH, LENGTH], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
65 |
+
# go.Scatter3d(x=[(WIDTH / 2) + (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[LENGTH, LENGTH], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
66 |
+
# go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[LENGTH, LENGTH], z=[GOAL_HEIGHT, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4))
|
67 |
+
# ])
|
68 |
+
|
69 |
+
# goalposts.extend([
|
70 |
+
# go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) - (GOAL_WIDTH / 2)], y=[0, 0], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
71 |
+
# go.Scatter3d(x=[(WIDTH / 2) + (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[0, 0], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
72 |
+
# go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[0, 0], z=[GOAL_HEIGHT, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4))
|
73 |
+
# ])
|
74 |
+
|
75 |
+
# return goalposts
|
76 |
+
|
77 |
+
# def generate_trajectory(start_point, end_point, peak_height=10, num_coords=100, trajectory_type='parabolic'):
|
78 |
+
# """Generate a trajectory (parabolic or linear) between start and end points."""
|
79 |
+
# shot_start_x, shot_start_y, start_z = start_point
|
80 |
+
# hoop_x, hoop_y, end_z = end_point
|
81 |
+
|
82 |
+
# if trajectory_type == 'parabolic':
|
83 |
+
# distance_x = hoop_x - shot_start_x
|
84 |
+
# a = -4 * peak_height / (distance_x ** 2)
|
85 |
+
|
86 |
+
# shot_path_coords = []
|
87 |
+
# for index, x in enumerate(np.linspace(shot_start_x, hoop_x, num_coords + 1)):
|
88 |
+
# z = a * (x - (shot_start_x + hoop_x) / 2) ** 2 + peak_height
|
89 |
+
# y = shot_start_y + (hoop_y - shot_start_y) * (index / num_coords)
|
90 |
+
# shot_path_coords.append([x, y, z])
|
91 |
+
|
92 |
+
# shot_path_coords[0][2] = start_z # Ensure start z is as specified
|
93 |
+
# shot_path_coords[-1][2] = end_z # Ensure end z is as specified
|
94 |
+
|
95 |
+
# elif trajectory_type == 'linear':
|
96 |
+
# shot_path_coords = []
|
97 |
+
# for index, x in enumerate(np.linspace(shot_start_x, hoop_x, num_coords + 1)):
|
98 |
+
# y = shot_start_y + (hoop_y - shot_start_y) * (index / num_coords)
|
99 |
+
# z = start_z + (end_z - start_z) * (index / num_coords)
|
100 |
+
# shot_path_coords.append([x, y, z])
|
101 |
+
|
102 |
+
# return pd.DataFrame(shot_path_coords, columns=['x', 'y', 'z'])
|
103 |
+
|
104 |
+
# def plot_trajectories(fig, start_points, end_points, trajectory_type='parabolic', peak_height=10, num_coords=100):
|
105 |
+
# """Plot multiple trajectories on the field."""
|
106 |
+
# for start_point, end_point in zip(start_points, end_points):
|
107 |
+
# trajectory_df = generate_trajectory(start_point, end_point, peak_height, num_coords, trajectory_type)
|
108 |
+
# fig.add_trace(go.Scatter3d(
|
109 |
+
# y=trajectory_df['x'],
|
110 |
+
# x=trajectory_df['y'],
|
111 |
+
# z=trajectory_df['z'],
|
112 |
+
# mode='lines',
|
113 |
+
# line=dict(color='red', width=4)
|
114 |
+
# ))
|
115 |
+
# fig.add_trace(go.Scatter3d(
|
116 |
+
# y=[trajectory_df['x'].iloc[0], trajectory_df['x'].iloc[-1]],
|
117 |
+
# x=[trajectory_df['y'].iloc[0], trajectory_df['y'].iloc[-1]],
|
118 |
+
# z=[trajectory_df['z'].iloc[0], trajectory_df['z'].iloc[-1]],
|
119 |
+
# mode='markers',
|
120 |
+
# marker=dict(size=3, color='red')
|
121 |
+
# ))
|
122 |
+
|
123 |
+
# def create_soccer_field_plot():
|
124 |
+
# """Create a 3D soccer field plot with trajectories."""
|
125 |
+
# field_df = create_field_df()
|
126 |
+
|
127 |
+
# fig = px.line_3d(
|
128 |
+
# data_frame=field_df, x='x', y='y', z='z', line_group='line_group', color='color',
|
129 |
+
# color_discrete_map={'field': '#FFFFFF'}
|
130 |
+
# )
|
131 |
+
|
132 |
+
# fig.add_trace(go.Mesh3d(
|
133 |
+
# x=[0, WIDTH, WIDTH, 0],
|
134 |
+
# y=[0, 0, LENGTH, LENGTH],
|
135 |
+
# z=[0, 0, 0, 0],
|
136 |
+
# color='rgb(0, 128, 0)',
|
137 |
+
# opacity=0.5
|
138 |
+
# ))
|
139 |
+
|
140 |
+
# fig.add_trace(create_center_circle())
|
141 |
+
# for goalpost in create_goalposts():
|
142 |
+
# fig.add_trace(goalpost)
|
143 |
+
|
144 |
+
# max_dimension = max(WIDTH, LENGTH, GOAL_HEIGHT)
|
145 |
+
# fig.update_layout(
|
146 |
+
# scene=dict(
|
147 |
+
# aspectmode="manual",
|
148 |
+
# aspectratio=dict(x=1, y=1, z=0.125),
|
149 |
+
# xaxis=dict(
|
150 |
+
# range=[-10, max_dimension + 10],
|
151 |
+
# visible=False
|
152 |
+
# ),
|
153 |
+
# yaxis=dict(
|
154 |
+
# range=[-10, max_dimension + 10],
|
155 |
+
# visible=False
|
156 |
+
# ),
|
157 |
+
# zaxis=dict(
|
158 |
+
# range=[0, 15],
|
159 |
+
# visible=False
|
160 |
+
# ),
|
161 |
+
# camera=dict(
|
162 |
+
# eye=dict(x=0.34, y=0, z=0.45)
|
163 |
+
# ),
|
164 |
+
# ),
|
165 |
+
# paper_bgcolor='rgba(0,0,0,0)',
|
166 |
+
# plot_bgcolor='rgba(0,0,0,0)',
|
167 |
+
# showlegend=False,
|
168 |
+
# )
|
169 |
+
|
170 |
+
# return fig
|
171 |
+
|
172 |
+
# def main_3D_pitch(start_points, end_points, trajectory_type='linear'):
|
173 |
+
# st.title("3D Soccer Field Trajectory Visualization")
|
174 |
+
|
175 |
+
# fig = create_soccer_field_plot()
|
176 |
+
|
177 |
+
# plot_trajectories(fig, start_points, end_points, trajectory_type=trajectory_type, peak_height=5, num_coords=100)
|
178 |
+
|
179 |
+
# st.plotly_chart(fig)
|
180 |
+
import numpy as np
|
181 |
+
import streamlit as st
|
182 |
+
import plotly.express as px
|
183 |
+
import plotly.graph_objects as go
|
184 |
+
import pandas as pd
|
185 |
+
|
186 |
+
# Soccer field dimensions (in meters)
|
187 |
+
WIDTH = 80 # Width of the field
|
188 |
+
LENGTH = 120 # Length of the field
|
189 |
+
GOAL_HEIGHT = 2.44 # Standard goal height
|
190 |
+
PENALTY_AREA_WIDTH = 40.3
|
191 |
+
PENALTY_AREA_DEPTH = 16.5
|
192 |
+
GOAL_AREA_WIDTH = 18.32
|
193 |
+
GOAL_AREA_DEPTH = 5.5
|
194 |
+
GOAL_WIDTH = 7.32 # Standard width of a soccer goal
|
195 |
+
|
196 |
+
def create_field_df():
|
197 |
+
"""Create dataframes for different parts of the soccer field."""
|
198 |
+
field_perimeter_bounds = [[0, 0, 0], [WIDTH, 0, 0], [WIDTH, LENGTH, 0], [0, LENGTH, 0], [0, 0, 0]]
|
199 |
+
field_df = pd.DataFrame(field_perimeter_bounds, columns=['x', 'y', 'z'])
|
200 |
+
field_df['line_group'] = 'field_perimeter'
|
201 |
+
field_df['color'] = 'field'
|
202 |
+
|
203 |
+
half_field_bounds = [[0, LENGTH / 2, 0], [WIDTH, LENGTH / 2, 0]]
|
204 |
+
half_df = pd.DataFrame(half_field_bounds, columns=['x', 'y', 'z'])
|
205 |
+
half_df['line_group'] = 'half_field'
|
206 |
+
half_df['color'] = 'field'
|
207 |
+
|
208 |
+
left_penalty_df = create_rectangle_df((WIDTH - PENALTY_AREA_WIDTH) / 2, 0, PENALTY_AREA_WIDTH, PENALTY_AREA_DEPTH, 'left_penalty_area')
|
209 |
+
right_penalty_df = create_rectangle_df((WIDTH - PENALTY_AREA_WIDTH) / 2, LENGTH - PENALTY_AREA_DEPTH, PENALTY_AREA_WIDTH, PENALTY_AREA_DEPTH, 'right_penalty_area')
|
210 |
+
left_goal_df = create_rectangle_df((WIDTH - GOAL_AREA_WIDTH) / 2, 0, GOAL_AREA_WIDTH, GOAL_AREA_DEPTH, 'left_goal_area')
|
211 |
+
right_goal_df = create_rectangle_df((WIDTH - GOAL_AREA_WIDTH) / 2, LENGTH - GOAL_AREA_DEPTH, GOAL_AREA_WIDTH, GOAL_AREA_DEPTH, 'right_goal_area')
|
212 |
+
|
213 |
+
return pd.concat([field_df, half_df, left_penalty_df, right_penalty_df, left_goal_df, right_goal_df])
|
214 |
+
|
215 |
+
def create_rectangle_df(start_x, start_y, width, height, line_group):
|
216 |
+
"""Create a dataframe representing a rectangle on the field."""
|
217 |
+
rectangle_bounds = [
|
218 |
+
[start_x, start_y, 0],
|
219 |
+
[start_x + width, start_y, 0],
|
220 |
+
[start_x + width, start_y + height, 0],
|
221 |
+
[start_x, start_y + height, 0],
|
222 |
+
[start_x, start_y, 0]
|
223 |
+
]
|
224 |
+
df = pd.DataFrame(rectangle_bounds, columns=['x', 'y', 'z'])
|
225 |
+
df['line_group'] = line_group
|
226 |
+
df['color'] = 'field'
|
227 |
+
return df
|
228 |
+
|
229 |
+
def create_center_circle():
|
230 |
+
"""Create a 3D line trace for the center circle."""
|
231 |
+
theta = np.linspace(0, 2 * np.pi, 100)
|
232 |
+
x = [(WIDTH / 2) + (9.15 * np.cos(t)) for t in theta]
|
233 |
+
y = [(LENGTH / 2) + (9.15 * np.sin(t)) for t in theta]
|
234 |
+
z = [0] * 100
|
235 |
+
return go.Scatter3d(x=x, y=y, z=z, mode='lines', line=dict(color='white', width=2))
|
236 |
+
|
237 |
+
def create_goalposts():
|
238 |
+
"""Create goalpost lines for both ends of the field."""
|
239 |
+
goalposts = []
|
240 |
+
|
241 |
+
goalposts.extend([
|
242 |
+
go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) - (GOAL_WIDTH / 2)], y=[LENGTH, LENGTH], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
243 |
+
go.Scatter3d(x=[(WIDTH / 2) + (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[LENGTH, LENGTH], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
244 |
+
go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[LENGTH, LENGTH], z=[GOAL_HEIGHT, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4))
|
245 |
+
])
|
246 |
+
|
247 |
+
goalposts.extend([
|
248 |
+
go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) - (GOAL_WIDTH / 2)], y=[0, 0], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
249 |
+
go.Scatter3d(x=[(WIDTH / 2) + (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[0, 0], z=[0, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4)),
|
250 |
+
go.Scatter3d(x=[(WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2)], y=[0, 0], z=[GOAL_HEIGHT, GOAL_HEIGHT], mode='lines', line=dict(color='black', width=4))
|
251 |
+
])
|
252 |
+
|
253 |
+
return goalposts
|
254 |
+
|
255 |
+
def create_goal_net(start_x, end_x, start_y, end_y, height, width):
|
256 |
+
"""Create a 3D mesh for the goal net."""
|
257 |
+
x = [start_x, end_x, end_x, start_x, start_x]
|
258 |
+
y = [start_y, start_y, end_y, end_y, start_y]
|
259 |
+
z = [height, height, height, height, height]
|
260 |
+
return go.Mesh3d(x=x, y=y, z=z, opacity=0.1, color='blue')
|
261 |
+
|
262 |
+
def generate_trajectory(start_point, end_point, peak_height=10, num_coords=100, trajectory_type='parabolic'):
|
263 |
+
"""Generate a trajectory (parabolic or linear) between start and end points."""
|
264 |
+
shot_start_x, shot_start_y, start_z = start_point
|
265 |
+
hoop_x, hoop_y, end_z = end_point
|
266 |
+
|
267 |
+
if trajectory_type == 'parabolic':
|
268 |
+
distance_x = hoop_x - shot_start_x
|
269 |
+
a = -4 * peak_height / (distance_x ** 2)
|
270 |
+
|
271 |
+
shot_path_coords = []
|
272 |
+
for index, x in enumerate(np.linspace(shot_start_x, hoop_x, num_coords + 1)):
|
273 |
+
z = a * (x - (shot_start_x + hoop_x) / 2) ** 2 + peak_height
|
274 |
+
y = shot_start_y + (hoop_y - shot_start_y) * (index / num_coords)
|
275 |
+
shot_path_coords.append([x, y, z])
|
276 |
+
|
277 |
+
shot_path_coords[0][2] = start_z # Ensure start z is as specified
|
278 |
+
shot_path_coords[-1][2] = end_z # Ensure end z is as specified
|
279 |
+
|
280 |
+
elif trajectory_type == 'linear':
|
281 |
+
shot_path_coords = []
|
282 |
+
for index, x in enumerate(np.linspace(shot_start_x, hoop_x, num_coords + 1)):
|
283 |
+
y = shot_start_y + (hoop_y - shot_start_y) * (index / num_coords)
|
284 |
+
z = start_z + (end_z - start_z) * (index / num_coords)
|
285 |
+
shot_path_coords.append([x, y, z])
|
286 |
+
|
287 |
+
return pd.DataFrame(shot_path_coords, columns=['x', 'y', 'z'])
|
288 |
+
|
289 |
+
def plot_trajectories(fig, start_points, end_points, trajectory_type='parabolic', peak_height=10, num_coords=100):
|
290 |
+
"""Plot multiple trajectories on the field."""
|
291 |
+
for start_point, end_point in zip(start_points, end_points):
|
292 |
+
trajectory_df = generate_trajectory(start_point, end_point, peak_height, num_coords, trajectory_type)
|
293 |
+
fig.add_trace(go.Scatter3d(
|
294 |
+
y=trajectory_df['x'],
|
295 |
+
x=trajectory_df['y'],
|
296 |
+
z=trajectory_df['z'],
|
297 |
+
mode='lines',
|
298 |
+
line=dict(color='red', width=4)
|
299 |
+
))
|
300 |
+
fig.add_trace(go.Scatter3d(
|
301 |
+
y=[trajectory_df['x'].iloc[0], trajectory_df['x'].iloc[-1]],
|
302 |
+
x=[trajectory_df['y'].iloc[0], trajectory_df['y'].iloc[-1]],
|
303 |
+
z=[trajectory_df['z'].iloc[0], trajectory_df['z'].iloc[-1]],
|
304 |
+
mode='markers',
|
305 |
+
marker=dict(size=3, color='red')
|
306 |
+
))
|
307 |
+
|
308 |
+
def create_soccer_field_plot():
|
309 |
+
"""Create a 3D soccer field plot with trajectories."""
|
310 |
+
field_df = create_field_df()
|
311 |
+
|
312 |
+
fig = px.line_3d(
|
313 |
+
data_frame=field_df, x='x', y='y', z='z', line_group='line_group', color='color',
|
314 |
+
color_discrete_map={'field': '#FFFFFF'}
|
315 |
+
)
|
316 |
+
|
317 |
+
fig.add_trace(go.Mesh3d(
|
318 |
+
x=[0, WIDTH, WIDTH, 0],
|
319 |
+
y=[0, 0, LENGTH, LENGTH],
|
320 |
+
z=[0, 0, 0, 0],
|
321 |
+
color='rgb(0, 128, 0)',
|
322 |
+
opacity=0.5
|
323 |
+
))
|
324 |
+
|
325 |
+
fig.add_trace(create_center_circle())
|
326 |
+
for goalpost in create_goalposts():
|
327 |
+
fig.add_trace(goalpost)
|
328 |
+
|
329 |
+
# # Add goal nets
|
330 |
+
# fig.add_trace(create_goal_net((WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2), 0, -GOAL_HEIGHT, GOAL_HEIGHT, GOAL_WIDTH))
|
331 |
+
# fig.add_trace(create_goal_net((WIDTH / 2) - (GOAL_WIDTH / 2), (WIDTH / 2) + (GOAL_WIDTH / 2), LENGTH, LENGTH + GOAL_HEIGHT, GOAL_HEIGHT, GOAL_WIDTH))
|
332 |
+
|
333 |
+
max_dimension = max(WIDTH, LENGTH, GOAL_HEIGHT)
|
334 |
+
fig.update_layout(
|
335 |
+
scene=dict(
|
336 |
+
aspectmode="manual",
|
337 |
+
aspectratio=dict(x=1, y=1, z=0.125),
|
338 |
+
xaxis=dict(
|
339 |
+
range=[-10, max_dimension + 10],
|
340 |
+
visible=False
|
341 |
+
),
|
342 |
+
yaxis=dict(
|
343 |
+
range=[-10, max_dimension + 10],
|
344 |
+
visible=False
|
345 |
+
),
|
346 |
+
zaxis=dict(
|
347 |
+
range=[0, 15],
|
348 |
+
visible=False
|
349 |
+
),
|
350 |
+
camera=dict(
|
351 |
+
eye=dict(x=0.34, y=0, z=0.45)
|
352 |
+
),
|
353 |
+
),
|
354 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
355 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
356 |
+
showlegend=False,
|
357 |
+
)
|
358 |
+
|
359 |
+
return fig
|
360 |
+
|
361 |
+
def main_3D_pitch(start_points, end_points, trajectory_type='linear'):
|
362 |
+
st.title("3D Soccer Field Trajectory Visualization")
|
363 |
+
|
364 |
+
fig = create_soccer_field_plot()
|
365 |
+
|
366 |
+
plot_trajectories(fig, start_points, end_points, trajectory_type=trajectory_type, peak_height=5, num_coords=100)
|
367 |
+
|
368 |
+
st.plotly_chart(fig)
|
369 |
+
|
app.py
ADDED
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
from statsbombpy import sb
|
4 |
+
import config_location_player as clp
|
5 |
+
import functions
|
6 |
+
import Pitch3D
|
7 |
+
import json
|
8 |
+
from stats_manager import StatsManager
|
9 |
+
|
10 |
+
st.set_page_config(layout="wide")
|
11 |
+
|
12 |
+
st.markdown(
|
13 |
+
"""
|
14 |
+
<style>
|
15 |
+
.centered-image {
|
16 |
+
display: block;
|
17 |
+
margin-left: auto;
|
18 |
+
margin-right: auto;
|
19 |
+
width: 20%; /* Adjust this percentage to resize the image */
|
20 |
+
}
|
21 |
+
.ban-image {
|
22 |
+
display: block;
|
23 |
+
margin-left: 0;
|
24 |
+
margin-right: 0;
|
25 |
+
width: 100%; /* Adjust this percentage to resize the image */
|
26 |
+
height: 50%; /* Adjust this percentage to resize the image */
|
27 |
+
}
|
28 |
+
.banner-image {
|
29 |
+
background-image: url('https://statsbomb.com/wp-content/uploads/2023/03/IconLockup_MediaPack-min.png');
|
30 |
+
background-size: cover;
|
31 |
+
background-position: center;
|
32 |
+
height: 300px; /* Adjust the height to your preference */
|
33 |
+
width: 100%;
|
34 |
+
margin: 0 auto;
|
35 |
+
margin-bottom: 100px;
|
36 |
+
}
|
37 |
+
</style>
|
38 |
+
""",
|
39 |
+
unsafe_allow_html=True
|
40 |
+
)
|
41 |
+
|
42 |
+
st.markdown('<div class="banner-image"></div>', unsafe_allow_html=True)
|
43 |
+
|
44 |
+
|
45 |
+
# st.markdown(
|
46 |
+
# '<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRsmWldT7_OE2kDhbehYcuTNHzItGFbeH5igw&s" class="centered-image"> <br> <br> <br>',
|
47 |
+
# unsafe_allow_html=True
|
48 |
+
# )
|
49 |
+
|
50 |
+
|
51 |
+
|
52 |
+
#PARTIE 1 : MATCH CHOOSEN
|
53 |
+
col1, col2 = st.columns([1, 3])
|
54 |
+
with col1:
|
55 |
+
|
56 |
+
competition = sb.competitions()
|
57 |
+
|
58 |
+
#Gender Choice
|
59 |
+
# on = st.toggle("Men" if st.session_state.get('competition_gender', 'women') == 'women' else "Women")
|
60 |
+
# competition_gender = 'men' if on else 'women'
|
61 |
+
# st.session_state.competition_gender = competition_gender
|
62 |
+
competition_gender = "male"
|
63 |
+
competition_gender_df = competition[competition['competition_gender']==competition_gender]
|
64 |
+
|
65 |
+
|
66 |
+
|
67 |
+
#Competition Choice
|
68 |
+
competitions = competition_gender_df['competition_name'].unique()
|
69 |
+
selected_competition = st.selectbox(
|
70 |
+
"Choose your competition",
|
71 |
+
competitions,
|
72 |
+
)
|
73 |
+
selected_competition_df = competition_gender_df[competition_gender_df['competition_name']==selected_competition]
|
74 |
+
|
75 |
+
|
76 |
+
|
77 |
+
|
78 |
+
|
79 |
+
#Season Choice
|
80 |
+
seasons = selected_competition_df['season_name'].unique()
|
81 |
+
selected_season = st.selectbox(
|
82 |
+
"Choose your season",
|
83 |
+
seasons,
|
84 |
+
)
|
85 |
+
|
86 |
+
competition_id = selected_competition_df[selected_competition_df['season_name']==selected_season]['competition_id'].iloc[0]
|
87 |
+
season_id = selected_competition_df[selected_competition_df['season_name']==selected_season]['season_id'].iloc[0]
|
88 |
+
matches = sb.matches(competition_id=competition_id, season_id=season_id)
|
89 |
+
|
90 |
+
|
91 |
+
#Season Choice
|
92 |
+
# teams = selected_competition_df[['home_team','away_team']].unique()
|
93 |
+
teams = pd.concat([matches['away_team'], matches['home_team']]).unique()
|
94 |
+
selected_team = st.selectbox(
|
95 |
+
"Choose your team",
|
96 |
+
teams,
|
97 |
+
)
|
98 |
+
one_team_matches = matches[(matches['home_team'] == selected_team) | (matches['away_team'] == selected_team)]
|
99 |
+
# matches = one_team_matches['match_id']
|
100 |
+
matches = one_team_matches['match_date']
|
101 |
+
|
102 |
+
selected_match = st.selectbox(
|
103 |
+
"Choose your match",
|
104 |
+
matches,
|
105 |
+
)
|
106 |
+
selected_one_team_matches = one_team_matches[one_team_matches['match_date']==selected_match]
|
107 |
+
match_id = selected_one_team_matches['match_id'].iloc[0]
|
108 |
+
|
109 |
+
home_team = selected_one_team_matches['home_team'].iloc[0]
|
110 |
+
home_score = selected_one_team_matches['home_score'].iloc[0]
|
111 |
+
home_color = "#0B8494"
|
112 |
+
|
113 |
+
away_team = selected_one_team_matches['away_team'].iloc[0]
|
114 |
+
away_score = selected_one_team_matches['away_score'].iloc[0]
|
115 |
+
away_color = "#F05A7E"
|
116 |
+
|
117 |
+
home_lineups = sb.lineups(match_id=match_id)[home_team]
|
118 |
+
away_lineups = sb.lineups(match_id=match_id)[away_team]
|
119 |
+
|
120 |
+
events = sb.events(match_id=match_id)
|
121 |
+
home_tactic_formation = events['tactics'].iloc[0]['formation']
|
122 |
+
away_tactic_formation = events['tactics'].iloc[1]['formation']
|
123 |
+
|
124 |
+
|
125 |
+
|
126 |
+
|
127 |
+
|
128 |
+
|
129 |
+
|
130 |
+
#PARTIE 2 : DASHBOARD
|
131 |
+
with col2:
|
132 |
+
|
133 |
+
# st.write(events.filter(regex='^bad_behaviour_card|^team$'))
|
134 |
+
|
135 |
+
# Créer un gestionnaire de statistiques
|
136 |
+
stats_manager = StatsManager(events)
|
137 |
+
|
138 |
+
# Appel des fonctions via le gestionnaire
|
139 |
+
home_possession, away_possession = stats_manager.get_possession()
|
140 |
+
home_xg, away_xg = stats_manager.get_total_xg()
|
141 |
+
home_shots, away_shots = stats_manager.get_total_shots()
|
142 |
+
home_off_target, away_off_target = stats_manager.get_total_shots_off_target()
|
143 |
+
home_on_target, away_on_target = stats_manager.get_total_shots_on_target()
|
144 |
+
home_passes, away_passes = stats_manager.get_total_passes()
|
145 |
+
home_successful_passes, away_successful_passes = stats_manager.get_successful_passes()
|
146 |
+
home_corners, away_corners = stats_manager.get_total_corners()
|
147 |
+
home_fouls, away_fouls = stats_manager.get_total_fouls()
|
148 |
+
home_yellow_cards, away_yellow_cards = stats_manager.get_total_yellow_cards()
|
149 |
+
home_red_cards, away_red_cards = stats_manager.get_total_red_cards()
|
150 |
+
|
151 |
+
# Créer la liste des scores, en excluant ceux qui ne peuvent pas être calculés
|
152 |
+
categories_scores = [
|
153 |
+
{"catégorie": "Possession de balle (%)", "Home Team": home_possession, "Away Team": away_possession},
|
154 |
+
{"catégorie": "xG (Buts attendus)", "Home Team": home_xg, "Away Team": away_xg},
|
155 |
+
{"catégorie": "Tirs", "Home Team": home_shots, "Away Team": away_shots},
|
156 |
+
{"catégorie": "Tirs cadrés", "Home Team": home_on_target, "Away Team": away_on_target},
|
157 |
+
{"catégorie": "Tirs non cadrés", "Home Team": home_off_target, "Away Team": away_off_target},
|
158 |
+
{"catégorie": "Passes", "Home Team": home_passes, "Away Team": away_passes},
|
159 |
+
{"catégorie": "Passes réussies", "Home Team": home_successful_passes, "Away Team": away_successful_passes},
|
160 |
+
{"catégorie": "Corners", "Home Team": home_corners, "Away Team": away_corners},
|
161 |
+
{"catégorie": "Fautes", "Home Team": home_fouls, "Away Team": away_fouls},
|
162 |
+
{"catégorie": "Cartons Jaunes", "Home Team": home_yellow_cards, "Away Team": away_yellow_cards},
|
163 |
+
{"catégorie": "Cartons Rouges", "Home Team": home_red_cards, "Away Team": away_red_cards},
|
164 |
+
]
|
165 |
+
|
166 |
+
# Filtrer les catégories valides
|
167 |
+
categories_scores = [entry for entry in categories_scores if entry is not None]
|
168 |
+
|
169 |
+
# Extraire les catégories et scores
|
170 |
+
categories = [entry['catégorie'] for entry in categories_scores]
|
171 |
+
home_scores = [entry['Home Team'] for entry in categories_scores]
|
172 |
+
away_scores = [entry['Away Team'] for entry in categories_scores]
|
173 |
+
|
174 |
+
# Afficher le graphique
|
175 |
+
functions.display_normalized_scores(home_scores, away_scores, categories, home_color=home_color, away_color=away_color)
|
176 |
+
|
177 |
+
|
178 |
+
|
179 |
+
# st.image('img/logo_stade.png', width=200)
|
180 |
+
pitch_color='#d2d2d2',
|
181 |
+
st.markdown(
|
182 |
+
f"""
|
183 |
+
<div style='text-align: center;'>
|
184 |
+
<span style='color: {home_color}; margin-right: 30px;'>{home_team} {home_score}</span>
|
185 |
+
<span style='margin-right: 30px;'>-</span>
|
186 |
+
<span style='color: {away_color};'>{away_score} {away_team}</span>
|
187 |
+
</div>
|
188 |
+
<div style='text-align: center;'>
|
189 |
+
<span style='color: {pitch_color}; margin-right: 30px;'>{home_tactic_formation}</span>
|
190 |
+
<span style='margin-right: 30px;'> </span>
|
191 |
+
<span style='color: {pitch_color};'>{away_tactic_formation}</span>
|
192 |
+
</div>
|
193 |
+
""",
|
194 |
+
unsafe_allow_html=True
|
195 |
+
)
|
196 |
+
|
197 |
+
|
198 |
+
col21, col22 = st.columns([1,1])
|
199 |
+
|
200 |
+
position_id_to_coordinates_home = clp.initial_player_position_allpitch_home
|
201 |
+
position_id_to_coordinates_away = clp.initial_player_position_allpitch_away
|
202 |
+
# functions.display_player_names_and_positions_twoTeam(home_lineups, away_lineups, position_id_to_coordinates_home, position_id_to_coordinates_away)
|
203 |
+
|
204 |
+
with open('data/club.json', encoding='utf-8') as f:
|
205 |
+
images_data = json.load(f)
|
206 |
+
|
207 |
+
# Integrate club logo
|
208 |
+
home_team_image = functions.get_best_match_image(home_team, images_data)
|
209 |
+
away_team_image = functions.get_best_match_image(away_team, images_data)
|
210 |
+
|
211 |
+
|
212 |
+
with col21:
|
213 |
+
|
214 |
+
# if home_team_image:
|
215 |
+
# st.image(home_team_image)
|
216 |
+
|
217 |
+
functions.display_player_names_and_positions_oneTeam(home_lineups, position_id_to_coordinates_home, home_color)
|
218 |
+
|
219 |
+
st.markdown(
|
220 |
+
f"""
|
221 |
+
<div style='text-align: center;'>
|
222 |
+
<span style='color: {home_color}; margin-right: 30px;'>{selected_one_team_matches['home_managers'].iloc[0]}</span>
|
223 |
+
</div>
|
224 |
+
""",
|
225 |
+
unsafe_allow_html=True
|
226 |
+
)
|
227 |
+
|
228 |
+
with col22:
|
229 |
+
|
230 |
+
# if away_team_image:
|
231 |
+
# st.image(away_team_image)
|
232 |
+
|
233 |
+
functions.display_player_names_and_positions_oneTeam(away_lineups, position_id_to_coordinates_away,away_color)
|
234 |
+
|
235 |
+
st.markdown(
|
236 |
+
f"""
|
237 |
+
<div style='text-align: center;'>
|
238 |
+
<span style='color: {away_color}; margin-right: 30px;'>{selected_one_team_matches['away_managers'].iloc[0]}</span>
|
239 |
+
</div>
|
240 |
+
""",
|
241 |
+
unsafe_allow_html=True
|
242 |
+
)
|
243 |
+
|
244 |
+
|
245 |
+
|
246 |
+
|
247 |
+
|
248 |
+
|
249 |
+
|
250 |
+
def ensure_3d_coordinates(coord):
|
251 |
+
if len(coord) == 2:
|
252 |
+
return coord + [0]
|
253 |
+
return coord
|
254 |
+
|
255 |
+
shot_events = events.filter(regex='^(shot|location|team)').dropna(how='all')
|
256 |
+
shot_events_location = shot_events[shot_events['shot_end_location'].notna()]
|
257 |
+
|
258 |
+
shot_events_location['location'] = shot_events_location['location'].apply(ensure_3d_coordinates)
|
259 |
+
shot_events_location['shot_end_location'] = shot_events_location['shot_end_location'].apply(ensure_3d_coordinates)
|
260 |
+
|
261 |
+
start_points = shot_events_location['location'].tolist()
|
262 |
+
end_points = shot_events_location['shot_end_location'].tolist()
|
263 |
+
|
264 |
+
st.write(shot_events_location)
|
265 |
+
|
266 |
+
Pitch3D.main_3D_pitch(start_points,end_points)
|
config_location_player.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
pitch_width = 120
|
3 |
+
pitch_height = 80
|
4 |
+
nb_line = 7
|
5 |
+
space_line = (pitch_width/2)/7
|
6 |
+
x_line1 = 5
|
7 |
+
|
8 |
+
x_lines = [x_line1 + i * space_line for i in range(7)]
|
9 |
+
initial_player_position_middlepitch_home = {
|
10 |
+
1: (x_lines[0], 40),
|
11 |
+
2: (x_lines[1], 70),
|
12 |
+
3: (x_lines[1], 55),
|
13 |
+
4: (x_lines[1], 40),
|
14 |
+
5: (x_lines[1], 25),
|
15 |
+
6: (x_lines[1], 10),
|
16 |
+
7: (x_lines[2], 70),
|
17 |
+
8: (x_lines[2], 10),
|
18 |
+
9: (x_lines[2], 55),
|
19 |
+
10: (x_lines[2], 40),
|
20 |
+
11: (x_lines[2], 25),
|
21 |
+
12: (x_lines[3], 70),
|
22 |
+
13: (x_lines[3], 55),
|
23 |
+
14: (x_lines[3], 40),
|
24 |
+
15: (x_lines[3], 25),
|
25 |
+
16: (x_lines[3], 10),
|
26 |
+
17: (x_lines[4], 70),
|
27 |
+
18: (x_lines[4], 55),
|
28 |
+
19: (x_lines[4], 40),
|
29 |
+
20: (x_lines[4], 25),
|
30 |
+
21: (x_lines[4], 10),
|
31 |
+
22: (x_lines[6], 55),
|
32 |
+
23: (x_lines[6], 40),
|
33 |
+
24: (x_lines[6], 25),
|
34 |
+
25: (x_lines[5], 40)
|
35 |
+
}
|
36 |
+
|
37 |
+
initial_player_position_middlepitch_away = {
|
38 |
+
position_id: (pitch_width - x, y)
|
39 |
+
for position_id, (x, y) in initial_player_position_middlepitch_home.items()
|
40 |
+
}
|
41 |
+
|
42 |
+
|
43 |
+
|
44 |
+
space_line = (pitch_width*0.8)/7
|
45 |
+
x_lines = [x_line1 + i * space_line for i in range(7)]
|
46 |
+
initial_player_position_allpitch_home = {
|
47 |
+
1: (x_lines[0], 40),
|
48 |
+
2: (x_lines[1], 70),
|
49 |
+
3: (x_lines[1], 55),
|
50 |
+
4: (x_lines[1], 40),
|
51 |
+
5: (x_lines[1], 25),
|
52 |
+
6: (x_lines[1], 10),
|
53 |
+
7: (x_lines[2], 70),
|
54 |
+
8: (x_lines[2], 10),
|
55 |
+
9: (x_lines[2], 55),
|
56 |
+
10: (x_lines[2], 40),
|
57 |
+
11: (x_lines[2], 25),
|
58 |
+
12: (x_lines[3], 70),
|
59 |
+
13: (x_lines[3], 55),
|
60 |
+
14: (x_lines[3], 40),
|
61 |
+
15: (x_lines[3], 25),
|
62 |
+
16: (x_lines[3], 10),
|
63 |
+
17: (x_lines[4], 70),
|
64 |
+
18: (x_lines[4], 55),
|
65 |
+
19: (x_lines[4], 40),
|
66 |
+
20: (x_lines[4], 25),
|
67 |
+
21: (x_lines[4], 10),
|
68 |
+
22: (x_lines[6], 55),
|
69 |
+
23: (x_lines[6], 40),
|
70 |
+
24: (x_lines[6], 25),
|
71 |
+
25: (x_lines[5], 40)
|
72 |
+
}
|
73 |
+
|
74 |
+
initial_player_position_allpitch_away = {
|
75 |
+
position_id: (pitch_width - x, y)
|
76 |
+
for position_id, (x, y) in initial_player_position_allpitch_home.items()
|
77 |
+
}
|
functions.py
ADDED
@@ -0,0 +1,689 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from mplsoccer import Pitch
|
2 |
+
import streamlit as st
|
3 |
+
import requests
|
4 |
+
from bs4 import BeautifulSoup
|
5 |
+
from fuzzywuzzy import fuzz, process
|
6 |
+
import plotly.express as px
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import streamlit as st
|
10 |
+
|
11 |
+
def display_player_names_and_positions_middlePitch(home_lineups, away_lineups, position_id_to_coordinates_home, position_id_to_coordinates_away):
|
12 |
+
# Dictionaries to store coordinates and player names for both home and away teams
|
13 |
+
coordinates_dict_home = {}
|
14 |
+
player_names_dict_home = {}
|
15 |
+
coordinates_dict_away = {}
|
16 |
+
player_names_dict_away = {}
|
17 |
+
|
18 |
+
# Process home team lineup
|
19 |
+
for index, row in home_lineups.iterrows():
|
20 |
+
player_name = row['player_name']
|
21 |
+
position_info = row['positions'][0] if row['positions'] else {}
|
22 |
+
position_id = position_info.get('position_id', 'Unknown')
|
23 |
+
|
24 |
+
if position_id in position_id_to_coordinates_home:
|
25 |
+
coordinates_dict_home[position_id] = position_id_to_coordinates_home[position_id]
|
26 |
+
player_names_dict_home[position_id] = player_name
|
27 |
+
|
28 |
+
# Process away team lineup
|
29 |
+
for index, row in away_lineups.iterrows():
|
30 |
+
player_name = row['player_name']
|
31 |
+
position_info = row['positions'][0] if row['positions'] else {}
|
32 |
+
position_id = position_info.get('position_id', 'Unknown')
|
33 |
+
|
34 |
+
if position_id in position_id_to_coordinates_away:
|
35 |
+
coordinates_dict_away[position_id] = position_id_to_coordinates_away[position_id]
|
36 |
+
player_names_dict_away[position_id] = player_name
|
37 |
+
|
38 |
+
# Plotting the pitch
|
39 |
+
pitch = Pitch(
|
40 |
+
# pitch_color='#1f77b4',
|
41 |
+
pitch_color='#d2d2d2',
|
42 |
+
line_color='white',
|
43 |
+
stripe=False,
|
44 |
+
pitch_type='statsbomb'
|
45 |
+
)
|
46 |
+
|
47 |
+
fig, ax = pitch.draw()
|
48 |
+
|
49 |
+
# Plotting home team positions
|
50 |
+
if coordinates_dict_home:
|
51 |
+
x_coords_home, y_coords_home = zip(*coordinates_dict_home.values())
|
52 |
+
ax.scatter(x_coords_home, y_coords_home, color='red', s=300, edgecolors='white', zorder=3)
|
53 |
+
|
54 |
+
for position_id, (x, y) in coordinates_dict_home.items():
|
55 |
+
name = player_names_dict_home.get(position_id, "Unknown")
|
56 |
+
ax.text(x, y - 4, f'{name.split()[-1]}', color='black', ha='center', va='center', fontsize=12, zorder=6)
|
57 |
+
|
58 |
+
# Plotting away team positions
|
59 |
+
if coordinates_dict_away:
|
60 |
+
x_coords_away, y_coords_away = zip(*coordinates_dict_away.values())
|
61 |
+
ax.scatter(x_coords_away, y_coords_away, color='blue', s=300, edgecolors='white', zorder=3)
|
62 |
+
|
63 |
+
for position_id, (x, y) in coordinates_dict_away.items():
|
64 |
+
name = player_names_dict_away.get(position_id, "Unknown")
|
65 |
+
ax.text(x, y - 4, f'{name.split()[-1]}', color='black', ha='center', va='center', fontsize=12, zorder=6)
|
66 |
+
|
67 |
+
# Display the plot in Streamlit
|
68 |
+
st.pyplot(fig)
|
69 |
+
|
70 |
+
|
71 |
+
|
72 |
+
def display_player_names_and_positions_twoTeam(home_lineups, away_lineups, position_id_to_coordinates_home, position_id_to_coordinates_away):
|
73 |
+
# Dictionaries to store coordinates and player names for both home and away teams
|
74 |
+
coordinates_dict_home = {}
|
75 |
+
player_names_dict_home = {}
|
76 |
+
coordinates_dict_away = {}
|
77 |
+
player_names_dict_away = {}
|
78 |
+
|
79 |
+
# Process home team lineup
|
80 |
+
for index, row in home_lineups.iterrows():
|
81 |
+
player_name = row['player_name']
|
82 |
+
position_info = row['positions'][0] if row['positions'] else {}
|
83 |
+
position_id = position_info.get('position_id', 'Unknown')
|
84 |
+
|
85 |
+
if position_id in position_id_to_coordinates_home:
|
86 |
+
coordinates_dict_home[position_id] = position_id_to_coordinates_home[position_id]
|
87 |
+
player_names_dict_home[position_id] = player_name
|
88 |
+
|
89 |
+
# Process away team lineup
|
90 |
+
for index, row in away_lineups.iterrows():
|
91 |
+
player_name = row['player_name']
|
92 |
+
position_info = row['positions'][0] if row['positions'] else {}
|
93 |
+
position_id = position_info.get('position_id', 'Unknown')
|
94 |
+
|
95 |
+
if position_id in position_id_to_coordinates_away:
|
96 |
+
coordinates_dict_away[position_id] = position_id_to_coordinates_away[position_id]
|
97 |
+
player_names_dict_away[position_id] = player_name
|
98 |
+
|
99 |
+
# Plotting the pitch
|
100 |
+
pitch = Pitch(
|
101 |
+
# pitch_color='#1f77b4',
|
102 |
+
pitch_color='#d2d2d2',
|
103 |
+
line_color='white',
|
104 |
+
stripe=False,
|
105 |
+
pitch_type='statsbomb'
|
106 |
+
)
|
107 |
+
|
108 |
+
fig, ax = pitch.draw()
|
109 |
+
|
110 |
+
# Plotting home team positions
|
111 |
+
if coordinates_dict_home:
|
112 |
+
x_coords_home, y_coords_home = zip(*coordinates_dict_home.values())
|
113 |
+
ax.scatter(x_coords_home, y_coords_home, color='red', s=300, edgecolors='white', zorder=3)
|
114 |
+
|
115 |
+
for position_id, (x, y) in coordinates_dict_home.items():
|
116 |
+
name = player_names_dict_home.get(position_id, "Unknown")
|
117 |
+
ax.text(x, y - 4, f'{name.split()[-1]}', color='black', ha='center', va='center', fontsize=12, zorder=6)
|
118 |
+
|
119 |
+
# Plotting away team positions
|
120 |
+
if coordinates_dict_away:
|
121 |
+
x_coords_away, y_coords_away = zip(*coordinates_dict_away.values())
|
122 |
+
ax.scatter(x_coords_away, y_coords_away, color='blue', s=300, edgecolors='white', zorder=3)
|
123 |
+
|
124 |
+
for position_id, (x, y) in coordinates_dict_away.items():
|
125 |
+
name = player_names_dict_away.get(position_id, "Unknown")
|
126 |
+
ax.text(x, y - 4, f'{name.split()[-1]}', color='black', ha='center', va='center', fontsize=12, zorder=6)
|
127 |
+
|
128 |
+
# Display the plot in Streamlit
|
129 |
+
st.pyplot(fig)
|
130 |
+
|
131 |
+
|
132 |
+
|
133 |
+
|
134 |
+
|
135 |
+
|
136 |
+
|
137 |
+
def display_player_names_and_positions_oneTeam(home_lineups, position_id_to_coordinates_home,color):
|
138 |
+
# Dictionaries to store coordinates and player names for the home team
|
139 |
+
coordinates_dict_home = {}
|
140 |
+
player_names_dict_home = {}
|
141 |
+
|
142 |
+
# Process home team lineup
|
143 |
+
for index, row in home_lineups.iterrows():
|
144 |
+
player_name = row['player_name']
|
145 |
+
position_info = row['positions'][0] if row['positions'] else {}
|
146 |
+
position_id = position_info.get('position_id', 'Unknown')
|
147 |
+
|
148 |
+
if position_id in position_id_to_coordinates_home:
|
149 |
+
coordinates_dict_home[position_id] = position_id_to_coordinates_home[position_id]
|
150 |
+
player_names_dict_home[position_id] = player_name
|
151 |
+
|
152 |
+
# Plotting the pitch
|
153 |
+
pitch = Pitch(
|
154 |
+
pitch_color='#d2d2d2',
|
155 |
+
line_color='white',
|
156 |
+
stripe=False,
|
157 |
+
pitch_type='statsbomb'
|
158 |
+
)
|
159 |
+
|
160 |
+
fig, ax = pitch.draw()
|
161 |
+
|
162 |
+
# Plotting home team positions
|
163 |
+
if coordinates_dict_home:
|
164 |
+
x_coords_home, y_coords_home = zip(*coordinates_dict_home.values())
|
165 |
+
ax.scatter(x_coords_home, y_coords_home, color=color, s=300, edgecolors='white', zorder=3)
|
166 |
+
|
167 |
+
for position_id, (x, y) in coordinates_dict_home.items():
|
168 |
+
name = player_names_dict_home.get(position_id, "Unknown")
|
169 |
+
ax.text(x, y - 4, f'{name.split()[-1]}', color='black', ha='center', va='center', fontsize=12, zorder=6)
|
170 |
+
|
171 |
+
# Display the plot in Streamlit
|
172 |
+
st.pyplot(fig)
|
173 |
+
|
174 |
+
|
175 |
+
|
176 |
+
|
177 |
+
# Function to find the best image match based on the team name
|
178 |
+
def get_best_match_image(team_name, images_data):
|
179 |
+
image_names = [image['image_name'] for image in images_data]
|
180 |
+
best_match, match_score = process.extractOne(team_name, image_names, scorer=fuzz.token_sort_ratio)
|
181 |
+
if match_score > 70: # Adjust the threshold as needed
|
182 |
+
for image in images_data:
|
183 |
+
if image['image_name'] == best_match:
|
184 |
+
return image['image_link']
|
185 |
+
return None
|
186 |
+
|
187 |
+
|
188 |
+
|
189 |
+
|
190 |
+
|
191 |
+
|
192 |
+
|
193 |
+
|
194 |
+
|
195 |
+
|
196 |
+
|
197 |
+
|
198 |
+
|
199 |
+
class StadiumImageFetcher:
|
200 |
+
def __init__(self):
|
201 |
+
self.headers = {
|
202 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
203 |
+
}
|
204 |
+
|
205 |
+
def get_stadium_image_url(self, stadium_name):
|
206 |
+
# Formulate the Google Image search URL
|
207 |
+
query = stadium_name.replace(' ', '+')
|
208 |
+
url = f"https://www.google.com/search?hl=en&tbm=isch&q={query}"
|
209 |
+
|
210 |
+
# Send the request
|
211 |
+
response = requests.get(url, headers=self.headers)
|
212 |
+
|
213 |
+
# Parse the HTML content using BeautifulSoup
|
214 |
+
soup = BeautifulSoup(response.text, 'lxml')
|
215 |
+
|
216 |
+
# Find the first image result
|
217 |
+
images = soup.find_all('img')
|
218 |
+
|
219 |
+
if images and len(images) > 1: # The first image might be the Google logo, so skip it
|
220 |
+
return images[1]['src']
|
221 |
+
else:
|
222 |
+
return None
|
223 |
+
|
224 |
+
def display_stadium(self):
|
225 |
+
st.title("Stadium Image Display")
|
226 |
+
|
227 |
+
# Input the stadium name
|
228 |
+
stadium_name = st.text_input("Enter the stadium name:")
|
229 |
+
|
230 |
+
if stadium_name:
|
231 |
+
# Get the image URL
|
232 |
+
image_url = self.get_stadium_image_url(stadium_name)
|
233 |
+
|
234 |
+
if image_url:
|
235 |
+
st.image(image_url, caption=f'{stadium_name} Stadium')
|
236 |
+
else:
|
237 |
+
st.write("Image not found.")
|
238 |
+
|
239 |
+
def main_stadium():
|
240 |
+
stadium = StadiumImageFetcher()
|
241 |
+
stadium.display_stadium()
|
242 |
+
|
243 |
+
|
244 |
+
|
245 |
+
|
246 |
+
def box_plot_pass(events,team):
|
247 |
+
|
248 |
+
event_filterTeam = events[events['team']==team]
|
249 |
+
event_pass = event_filterTeam.filter(regex='^(pass)', axis=1).dropna(how='all')
|
250 |
+
event_pass_length = event_pass['pass_length']
|
251 |
+
|
252 |
+
fig = px.box(event_pass_length, y=event_pass_length, labels={'y':'Pass Length'}, title=f'{team} Distribution of Pass Length')
|
253 |
+
fig.update_layout(width=300, height=600) # 200 pixels de largeur, 400 pixels de hauteur
|
254 |
+
|
255 |
+
# Afficher le graphique dans Streamlit
|
256 |
+
st.plotly_chart(fig)
|
257 |
+
|
258 |
+
|
259 |
+
|
260 |
+
|
261 |
+
|
262 |
+
|
263 |
+
|
264 |
+
|
265 |
+
|
266 |
+
|
267 |
+
|
268 |
+
|
269 |
+
# def get_possession(events):
|
270 |
+
# """
|
271 |
+
# Extracts and normalizes the possession counts for the two teams from the events DataFrame.
|
272 |
+
|
273 |
+
# Parameters:
|
274 |
+
# - events (pd.DataFrame): DataFrame containing an events column 'possession_team'.
|
275 |
+
|
276 |
+
# Returns:
|
277 |
+
# - home_team_possession (float): Normalized possession percentage for the home team, rounded to one decimal place.
|
278 |
+
# - away_team_possession (float): Normalized possession percentage for the away team, rounded to one decimal place.
|
279 |
+
# """
|
280 |
+
# # Get possession counts
|
281 |
+
# possession_counts = events['possession_team'].value_counts()
|
282 |
+
|
283 |
+
# # Get the top two possession teams
|
284 |
+
# home_team_possession, away_team_possession = possession_counts.iloc[0], possession_counts.iloc[1]
|
285 |
+
|
286 |
+
# # Calculate total possession
|
287 |
+
# total_possession = home_team_possession + away_team_possession
|
288 |
+
|
289 |
+
# # Normalize possession and round to one decimal place
|
290 |
+
# home_team_possession = round((home_team_possession / total_possession) * 100, 1)
|
291 |
+
# away_team_possession = round((away_team_possession / total_possession) * 100, 1)
|
292 |
+
|
293 |
+
# return home_team_possession, away_team_possession
|
294 |
+
|
295 |
+
|
296 |
+
|
297 |
+
# def get_total_xg(events):
|
298 |
+
# """
|
299 |
+
# Calcule la somme totale des xG (expected goals) par équipe et retourne les valeurs arrondies à 2 décimales.
|
300 |
+
|
301 |
+
# Parameters:
|
302 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'shot_statsbomb_xg' et 'team'.
|
303 |
+
|
304 |
+
# Returns:
|
305 |
+
# - home_xg (float): Somme totale des xG pour l'équipe à domicile, arrondie à 2 décimales.
|
306 |
+
# - away_xg (float): Somme totale des xG pour l'équipe à l'extérieur, arrondie à 2 décimales.
|
307 |
+
# """
|
308 |
+
# # Filtrer les colonnes et supprimer les lignes où 'shot_statsbomb_xg' est NaN
|
309 |
+
# data = events.filter(regex='^shot_statsbomb_xg|^team$').dropna(subset=['shot_statsbomb_xg'])
|
310 |
+
|
311 |
+
# # Grouper par 'team' et calculer la somme des xG
|
312 |
+
# data = data.groupby('team')['shot_statsbomb_xg'].sum()
|
313 |
+
|
314 |
+
# # Arrondir les résultats à 2 décimales
|
315 |
+
# home_xg, away_xg = round(data.iloc[0], 2), round(data.iloc[1], 2)
|
316 |
+
|
317 |
+
# return home_xg, away_xg
|
318 |
+
|
319 |
+
# def get_total_shots(events):
|
320 |
+
# """
|
321 |
+
# Calcule le nombre total de tirs (shots) par équipe.
|
322 |
+
|
323 |
+
# Parameters:
|
324 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'shot_statsbomb_xg' et 'team'.
|
325 |
+
|
326 |
+
# Returns:
|
327 |
+
# - home_shots (int): Nombre total de tirs pour l'équipe à domicile.
|
328 |
+
# - away_shots (int): Nombre total de tirs pour l'équipe à l'extérieur.
|
329 |
+
# """
|
330 |
+
# # Filtrer les colonnes et supprimer les lignes où 'shot_statsbomb_xg' est NaN
|
331 |
+
# data = events.filter(regex='^shot_statsbomb_xg|^team$').dropna(subset=['shot_statsbomb_xg'])
|
332 |
+
|
333 |
+
# # Grouper par 'team' et compter le nombre de tirs
|
334 |
+
# shot_counts = data.groupby('team').count()['shot_statsbomb_xg']
|
335 |
+
|
336 |
+
# # Retourner les comptes pour les deux équipes
|
337 |
+
# home_shots, away_shots = shot_counts.iloc[0], shot_counts.iloc[1]
|
338 |
+
|
339 |
+
# return home_shots, away_shots
|
340 |
+
|
341 |
+
|
342 |
+
# def get_total_shots_off_target(events):
|
343 |
+
# """
|
344 |
+
# Calcule le nombre total de tirs non cadrés (off target) par équipe.
|
345 |
+
|
346 |
+
# Parameters:
|
347 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'shot_outcome' et 'team'.
|
348 |
+
|
349 |
+
# Returns:
|
350 |
+
# - home_off_target (int): Nombre total de tirs non cadrés pour l'équipe à domicile.
|
351 |
+
# - away_off_target (int): Nombre total de tirs non cadrés pour l'équipe à l'extérieur.
|
352 |
+
# """
|
353 |
+
# # Define outcomes that indicate a shot was off target
|
354 |
+
# off_target_outcomes = ['Off T', 'Blocked', 'Missed']
|
355 |
+
|
356 |
+
# # Filter the events DataFrame for shots off target
|
357 |
+
# data = events[events['shot_outcome'].isin(off_target_outcomes)]
|
358 |
+
|
359 |
+
# # Group by 'team' and count the number of off-target shots
|
360 |
+
# off_target_counts = data.groupby('team').size()
|
361 |
+
|
362 |
+
# # Return the counts for the two teams
|
363 |
+
# home_off_target, away_off_target = off_target_counts.iloc[0], off_target_counts.iloc[1]
|
364 |
+
|
365 |
+
# return home_off_target, away_off_target
|
366 |
+
|
367 |
+
|
368 |
+
# def get_total_shots_on_target(events):
|
369 |
+
# """
|
370 |
+
# Calcule le nombre total de tirs cadrés (on target) par équipe.
|
371 |
+
|
372 |
+
# Parameters:
|
373 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'shot_outcome' et 'team'.
|
374 |
+
|
375 |
+
# Returns:
|
376 |
+
# - home_on_target (int): Nombre total de tirs cadrés pour l'équipe à domicile.
|
377 |
+
# - away_on_target (int): Nombre total de tirs cadrés pour l'équipe à l'extérieur.
|
378 |
+
# """
|
379 |
+
# # Define outcomes that indicate a shot was on target
|
380 |
+
# on_target_outcomes = ['Goal', 'Saved', 'Saved To Post', 'Shot Saved Off Target']
|
381 |
+
|
382 |
+
# # Filter the events DataFrame for shots on target
|
383 |
+
# data = events[events['shot_outcome'].isin(on_target_outcomes)]
|
384 |
+
|
385 |
+
# # Group by 'team' and count the number of on-target shots
|
386 |
+
# on_target_counts = data.groupby('team').size()
|
387 |
+
|
388 |
+
# # Return the counts for the two teams
|
389 |
+
# home_on_target, away_on_target = on_target_counts.iloc[0], on_target_counts.iloc[1]
|
390 |
+
|
391 |
+
# return home_on_target, away_on_target
|
392 |
+
|
393 |
+
|
394 |
+
|
395 |
+
# def get_total_passes(events):
|
396 |
+
# """
|
397 |
+
# Calcule le nombre total de passes par équipe.
|
398 |
+
|
399 |
+
# Parameters:
|
400 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'pass_outcome' et 'team'.
|
401 |
+
|
402 |
+
# Returns:
|
403 |
+
# - home_passes (int): Nombre total de passes pour l'équipe à domicile.
|
404 |
+
# - away_passes (int): Nombre total de passes pour l'équipe à l'extérieur.
|
405 |
+
# """
|
406 |
+
# # Filtrer les colonnes 'pass_outcome' et 'team', puis grouper par équipe et compter le nombre de passes
|
407 |
+
# pass_counts = events.filter(regex='^pass_end_location|^team$').groupby('team').count()['pass_end_location']
|
408 |
+
|
409 |
+
# # Retourner les comptes pour les deux équipes
|
410 |
+
# home_passes, away_passes = pass_counts.iloc[0], pass_counts.iloc[1]
|
411 |
+
|
412 |
+
# return home_passes, away_passes
|
413 |
+
|
414 |
+
# def get_successful_passes(events):
|
415 |
+
# """
|
416 |
+
# Calculates the total number of successful passes for home and away teams.
|
417 |
+
|
418 |
+
# Parameters:
|
419 |
+
# - events (pd.DataFrame): DataFrame containing the columns for all passes and pass outcomes.
|
420 |
+
|
421 |
+
# Returns:
|
422 |
+
# - home_successful_passes (int): Successful passes for the home team.
|
423 |
+
# - away_successful_passes (int): Successful passes for the away team.
|
424 |
+
# """
|
425 |
+
# # Get the total passes using the get_total_passes function
|
426 |
+
# home_passes, away_passes = get_total_passes(events)
|
427 |
+
|
428 |
+
# # Identify unsuccessful passes based on 'type' or another column indicating unsuccessful outcomes
|
429 |
+
# unsuccessful_passes = events.filter(regex='^pass_outcome|^team$').groupby('team').count().reset_index()['pass_outcome']
|
430 |
+
|
431 |
+
# # Calculate successful passes by subtracting unsuccessful passes from total passes
|
432 |
+
# home_unsuccessful_passes = unsuccessful_passes.iloc[0]
|
433 |
+
# away_unsuccessful_passes = unsuccessful_passes.iloc[1]
|
434 |
+
|
435 |
+
# home_successful_passes = home_passes - home_unsuccessful_passes
|
436 |
+
# away_successful_passes = away_passes - away_unsuccessful_passes
|
437 |
+
|
438 |
+
# return home_successful_passes, away_successful_passes
|
439 |
+
|
440 |
+
# def get_total_corners(events):
|
441 |
+
# """
|
442 |
+
# Calcule le nombre total de corners par équipe.
|
443 |
+
|
444 |
+
# Parameters:
|
445 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'pass_type' et 'team'.
|
446 |
+
|
447 |
+
# Returns:
|
448 |
+
# - home_corners (int): Nombre total de corners pour l'équipe à domicile.
|
449 |
+
# - away_corners (int): Nombre total de corners pour l'équipe à l'extérieur.
|
450 |
+
# """
|
451 |
+
# # Filtrer les colonnes 'pass_type' et 'team', puis grouper par équipe et compter le nombre de corners
|
452 |
+
# corner_counts = events.filter(regex='^pass_type|^team$').query('pass_type == "Corner"').groupby('team').count()['pass_type']
|
453 |
+
|
454 |
+
# # Extract the team names from the events DataFrame
|
455 |
+
# teams = events['team'].unique()
|
456 |
+
|
457 |
+
# # Handle cases where a team might not have any corners
|
458 |
+
# home_corners = corner_counts.get(teams[0], 0)
|
459 |
+
# away_corners = corner_counts.get(teams[1], 0)
|
460 |
+
|
461 |
+
# return home_corners, away_corners
|
462 |
+
|
463 |
+
|
464 |
+
|
465 |
+
# def get_total_fouls(events):
|
466 |
+
# """
|
467 |
+
# Calcule le nombre total de fautes par équipe.
|
468 |
+
|
469 |
+
# Parameters:
|
470 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'team' et 'type'.
|
471 |
+
|
472 |
+
# Returns:
|
473 |
+
# - home_fouls (int): Nombre total de fautes pour l'équipe à domicile.
|
474 |
+
# - away_fouls (int): Nombre total de fautes pour l'équipe à l'extérieur.
|
475 |
+
# """
|
476 |
+
# fouls = events[events['type'] == 'Foul Committed']
|
477 |
+
# foul_counts = fouls.groupby('team').size()
|
478 |
+
# home_fouls, away_fouls = foul_counts.iloc[0], foul_counts.iloc[1]
|
479 |
+
|
480 |
+
# return home_fouls, away_fouls
|
481 |
+
|
482 |
+
|
483 |
+
|
484 |
+
# def get_total_yellow_cards(events):
|
485 |
+
# """
|
486 |
+
# Calcule le nombre total de cartons jaunes par équipe.
|
487 |
+
|
488 |
+
# Parameters:
|
489 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'bad_behaviour_card' et 'team'.
|
490 |
+
|
491 |
+
# Returns:
|
492 |
+
# - home_yellow_cards (int): Nombre total de cartons jaunes pour l'équipe à domicile.
|
493 |
+
# - away_yellow_cards (int): Nombre total de cartons jaunes pour l'équipe à l'extérieur.
|
494 |
+
# """
|
495 |
+
# # Filtrer les colonnes 'bad_behaviour_card' et 'team', puis grouper par équipe et compter le nombre de cartons jaunes
|
496 |
+
# yellow_card_counts = events.filter(regex='^bad_behaviour_card|^team$').query('bad_behaviour_card == "Yellow Card"').groupby('team').count()['bad_behaviour_card']
|
497 |
+
|
498 |
+
# # Extraire les noms des équipes de l'événement DataFrame
|
499 |
+
# teams = events['team'].unique()
|
500 |
+
|
501 |
+
# # Gérer les cas où une équipe pourrait ne pas avoir de cartons jaunes
|
502 |
+
# home_yellow_cards = yellow_card_counts.get(teams[0], 0)
|
503 |
+
# away_yellow_cards = yellow_card_counts.get(teams[1], 0)
|
504 |
+
|
505 |
+
# return home_yellow_cards, away_yellow_cards
|
506 |
+
|
507 |
+
# def get_total_red_cards(events):
|
508 |
+
# """
|
509 |
+
# Calcule le nombre total de cartons rouges par équipe.
|
510 |
+
|
511 |
+
# Parameters:
|
512 |
+
# - events (pd.DataFrame): DataFrame contenant les colonnes 'bad_behaviour_card' et 'team'.
|
513 |
+
|
514 |
+
# Returns:
|
515 |
+
# - home_red_cards (int): Nombre total de cartons rouges pour l'équipe à domicile.
|
516 |
+
# - away_red_cards (int): Nombre total de cartons rouges pour l'équipe à l'extérieur.
|
517 |
+
# """
|
518 |
+
# # Filtrer les colonnes 'bad_behaviour_card' et 'team', puis grouper par équipe et compter le nombre de cartons rouges
|
519 |
+
# red_card_counts = events.filter(regex='^bad_behaviour_card|^team$').query('bad_behaviour_card == "Red Card"').groupby('team').count()['bad_behaviour_card']
|
520 |
+
|
521 |
+
# # Extraire les noms des équipes de l'événement DataFrame
|
522 |
+
# teams = events['team'].unique()
|
523 |
+
|
524 |
+
# # Gérer les cas où une équipe pourrait ne pas avoir de cartons rouges
|
525 |
+
# home_red_cards = red_card_counts.get(teams[0], 0)
|
526 |
+
# away_red_cards = red_card_counts.get(teams[1], 0)
|
527 |
+
|
528 |
+
# return home_red_cards, away_red_cards
|
529 |
+
|
530 |
+
|
531 |
+
|
532 |
+
|
533 |
+
# def display_normalized_scores(home_scores, away_scores, categories, home_color='blue', away_color='green', background_color='lightgray', bar_height=0.8, spacing_factor=2.5):
|
534 |
+
# """
|
535 |
+
# Displays a horizontal bar chart with normalized scores for home and away teams.
|
536 |
+
|
537 |
+
# Parameters:
|
538 |
+
# - home_scores: List of scores for the home team.
|
539 |
+
# - away_scores: List of scores for the away team.
|
540 |
+
# - categories: List of category names for each score.
|
541 |
+
# - home_color: Color of the bars representing the home team.
|
542 |
+
# - away_color: Color of the bars representing the away team.
|
543 |
+
# - background_color: Color of the background rectangles.
|
544 |
+
# - bar_height: Height of the bars and background rectangles.
|
545 |
+
# - spacing_factor: Factor to adjust the spacing between bars.
|
546 |
+
# """
|
547 |
+
|
548 |
+
# # Internal container size variables
|
549 |
+
# container_width = '50%' # Adjust width as needed
|
550 |
+
# container_height = 'auto' # Adjust height as needed
|
551 |
+
|
552 |
+
# # Normalizing the scores
|
553 |
+
# home_normalized = []
|
554 |
+
# away_normalized = []
|
555 |
+
# for home, away in zip(home_scores, away_scores):
|
556 |
+
# total = home + away
|
557 |
+
# if total != 0:
|
558 |
+
# home_normalized.append((home / total) * 100)
|
559 |
+
# away_normalized.append((away / total) * 100)
|
560 |
+
# else:
|
561 |
+
# home_normalized.append(0)
|
562 |
+
# away_normalized.append(0)
|
563 |
+
|
564 |
+
# # Augmenting the spacing between the bars by multiplying y_pos by a factor
|
565 |
+
# y_pos = np.arange(len(categories)) * spacing_factor
|
566 |
+
|
567 |
+
# # Plot
|
568 |
+
# fig, ax = plt.subplots(figsize=(10, 8))
|
569 |
+
|
570 |
+
# # Adding light gray backgrounds for each category with the same height as the bars
|
571 |
+
# for i in range(len(categories)):
|
572 |
+
# ax.add_patch(plt.Rectangle((-100, y_pos[i] - bar_height / 2), 200, bar_height, color=background_color, alpha=0.3, linewidth=0))
|
573 |
+
|
574 |
+
# # Plotting normalized scores for both teams
|
575 |
+
# ax.barh(y_pos, home_normalized, height=bar_height, color=home_color, align='center', label='Home Team')
|
576 |
+
# ax.barh(y_pos, [-score for score in away_normalized], height=bar_height, color=away_color, align='center', label='Away Team')
|
577 |
+
|
578 |
+
# # Positioning the category names above the bars to avoid overlap
|
579 |
+
# for i in range(len(categories)):
|
580 |
+
# ax.text(0, y_pos[i] + bar_height / 2 + 0.1, categories[i], ha='center', va='bottom', fontsize=10)
|
581 |
+
|
582 |
+
# # Adding non-normalized values to the end of the bars
|
583 |
+
# for i in range(len(categories)):
|
584 |
+
# ax.text(home_normalized[i] / 2, y_pos[i], f'{home_scores[i]}', va='center', color='white', fontweight='bold')
|
585 |
+
# ax.text(-away_normalized[i] / 2, y_pos[i], f'{away_scores[i]}', va='center', color='white', fontweight='bold')
|
586 |
+
|
587 |
+
# # Adjusting the axis limits
|
588 |
+
# ax.set_xlim(-100, 100)
|
589 |
+
# ax.set_ylim(-1, max(y_pos) + spacing_factor)
|
590 |
+
|
591 |
+
# # Hiding the spines
|
592 |
+
# for spine in ax.spines.values():
|
593 |
+
# spine.set_visible(False)
|
594 |
+
|
595 |
+
# # Removing y-ticks and x-ticks
|
596 |
+
# ax.set_yticks([])
|
597 |
+
# ax.set_xticks([])
|
598 |
+
|
599 |
+
# # Custom HTML/CSS to control the size of the container
|
600 |
+
# st.markdown(
|
601 |
+
# f"""
|
602 |
+
# <style>
|
603 |
+
# .resizable-graph-container {{
|
604 |
+
# width: {container_width}; /* Adjust the width as needed */
|
605 |
+
# height: {container_height}; /* Adjust the height as needed */
|
606 |
+
# padding: 10px;
|
607 |
+
# overflow: auto; /* Handle overflow if the graph is larger than the container */
|
608 |
+
# }}
|
609 |
+
# </style>
|
610 |
+
# <div class="resizable-graph-container">
|
611 |
+
# """,
|
612 |
+
# unsafe_allow_html=True
|
613 |
+
# )
|
614 |
+
|
615 |
+
# # Displaying the plot in Streamlit
|
616 |
+
# st.pyplot(fig)
|
617 |
+
|
618 |
+
# # Closing the custom container
|
619 |
+
# st.markdown('</div>', unsafe_allow_html=True)
|
620 |
+
|
621 |
+
|
622 |
+
def display_normalized_scores(home_scores, away_scores, categories, home_color='blue', away_color='green', background_color='lightgray', bar_height=0.8, spacing_factor=2.5):
|
623 |
+
"""
|
624 |
+
Displays a horizontal bar chart with normalized scores for home and away teams.
|
625 |
+
|
626 |
+
Parameters:
|
627 |
+
- home_scores: List of scores for the home team.
|
628 |
+
- away_scores: List of scores for the away team.
|
629 |
+
- categories: List of category names for each score.
|
630 |
+
- home_color: Color of the bars representing the home team.
|
631 |
+
- away_color: Color of the bars representing the away team.
|
632 |
+
- background_color: Color of the background rectangles.
|
633 |
+
- bar_height: Height of the bars and background rectangles.
|
634 |
+
- spacing_factor: Factor to adjust the spacing between bars.
|
635 |
+
"""
|
636 |
+
|
637 |
+
# Normalizing the scores
|
638 |
+
home_normalized = []
|
639 |
+
away_normalized = []
|
640 |
+
for home, away in zip(home_scores, away_scores):
|
641 |
+
# Remplacer les None par 0 pour éviter les erreurs
|
642 |
+
home = 0 if home is None else home
|
643 |
+
away = 0 if away is None else away
|
644 |
+
|
645 |
+
total = home + away
|
646 |
+
if total != 0:
|
647 |
+
home_normalized.append((home / total) * 100)
|
648 |
+
away_normalized.append((away / total) * 100)
|
649 |
+
else:
|
650 |
+
home_normalized.append(0)
|
651 |
+
away_normalized.append(0)
|
652 |
+
|
653 |
+
# Augmenting the spacing between the bars by multiplying y_pos by a factor
|
654 |
+
y_pos = np.arange(len(categories)) * spacing_factor
|
655 |
+
|
656 |
+
# Plot
|
657 |
+
fig, ax = plt.subplots(figsize=(10, 8))
|
658 |
+
|
659 |
+
# Adding light gray backgrounds for each category with the same height as the bars
|
660 |
+
for i in range(len(categories)):
|
661 |
+
ax.add_patch(plt.Rectangle((-100, y_pos[i] - bar_height / 2), 200, bar_height, color=background_color, alpha=0.3, linewidth=0))
|
662 |
+
|
663 |
+
# Plotting normalized scores for both teams
|
664 |
+
ax.barh(y_pos, home_normalized, height=bar_height, color=home_color, align='center', label='Home Team')
|
665 |
+
ax.barh(y_pos, [-score for score in away_normalized], height=bar_height, color=away_color, align='center', label='Away Team')
|
666 |
+
|
667 |
+
# Positioning the category names above the bars to avoid overlap
|
668 |
+
for i in range(len(categories)):
|
669 |
+
ax.text(0, y_pos[i] + bar_height / 2 + 0.1, categories[i], ha='center', va='bottom', fontsize=10)
|
670 |
+
|
671 |
+
# Adding non-normalized values to the end of the bars
|
672 |
+
for i in range(len(categories)):
|
673 |
+
ax.text(home_normalized[i] / 2, y_pos[i], f'{home_scores[i]}', va='center', color='white', fontweight='bold')
|
674 |
+
ax.text(-away_normalized[i] / 2, y_pos[i], f'{away_scores[i]}', va='center', color='white', fontweight='bold')
|
675 |
+
|
676 |
+
# Adjusting the axis limits
|
677 |
+
ax.set_xlim(-100, 100)
|
678 |
+
ax.set_ylim(-1, max(y_pos) + spacing_factor)
|
679 |
+
|
680 |
+
# Hiding the spines
|
681 |
+
for spine in ax.spines.values():
|
682 |
+
spine.set_visible(False)
|
683 |
+
|
684 |
+
# Removing y-ticks and x-ticks
|
685 |
+
ax.set_yticks([])
|
686 |
+
ax.set_xticks([])
|
687 |
+
|
688 |
+
# Displaying the plot in Streamlit
|
689 |
+
st.pyplot(fig)
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
pandas
|
3 |
+
plotly
|
4 |
+
requests
|
5 |
+
statsbombpy
|
6 |
+
mplsoccer
|
7 |
+
matplotlib
|
stats_manager.py
ADDED
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
|
3 |
+
def check_columns(df, required_columns):
|
4 |
+
"""
|
5 |
+
Vérifie si les colonnes requises sont présentes dans le DataFrame.
|
6 |
+
|
7 |
+
Parameters:
|
8 |
+
- df (pd.DataFrame): Le DataFrame à vérifier.
|
9 |
+
- required_columns (list): Liste des noms de colonnes requis.
|
10 |
+
|
11 |
+
Returns:
|
12 |
+
- bool: True si toutes les colonnes sont présentes, sinon False.
|
13 |
+
"""
|
14 |
+
return all(column in df.columns for column in required_columns)
|
15 |
+
|
16 |
+
class StatsManager:
|
17 |
+
def __init__(self, events):
|
18 |
+
self.events = events
|
19 |
+
|
20 |
+
def calculate_stat(self, required_columns, func):
|
21 |
+
"""
|
22 |
+
Vérifie si les colonnes nécessaires sont présentes, puis applique la fonction de calcul si elles sont présentes.
|
23 |
+
|
24 |
+
Parameters:
|
25 |
+
- required_columns (list): Liste des colonnes requises.
|
26 |
+
- func (callable): Fonction à appliquer si les colonnes sont présentes.
|
27 |
+
|
28 |
+
Returns:
|
29 |
+
- tuple: Les résultats de la fonction si les colonnes sont présentes, sinon (None, None).
|
30 |
+
"""
|
31 |
+
if check_columns(self.events, required_columns):
|
32 |
+
return func(self.events)
|
33 |
+
else:
|
34 |
+
return None, None
|
35 |
+
|
36 |
+
def get_possession(self):
|
37 |
+
return self.calculate_stat(['possession_team'], self._calculate_possession)
|
38 |
+
|
39 |
+
def _calculate_possession(self, events):
|
40 |
+
possession_counts = events['possession_team'].value_counts()
|
41 |
+
if len(possession_counts) < 2:
|
42 |
+
return None, None
|
43 |
+
total_possession = possession_counts.iloc[0] + possession_counts.iloc[1]
|
44 |
+
home_possession = round((possession_counts.iloc[0] / total_possession) * 100, 1)
|
45 |
+
away_possession = round((possession_counts.iloc[1] / total_possession) * 100, 1)
|
46 |
+
return home_possession, away_possession
|
47 |
+
|
48 |
+
def get_total_xg(self):
|
49 |
+
return self.calculate_stat(['shot_statsbomb_xg', 'team'], self._calculate_total_xg)
|
50 |
+
|
51 |
+
def _calculate_total_xg(self, events):
|
52 |
+
data = events[['shot_statsbomb_xg', 'team']].dropna(subset=['shot_statsbomb_xg'])
|
53 |
+
if data.empty:
|
54 |
+
return None, None
|
55 |
+
data = data.groupby('team')['shot_statsbomb_xg'].sum()
|
56 |
+
if len(data) < 2:
|
57 |
+
return None, None
|
58 |
+
return round(data.iloc[0], 2), round(data.iloc[1], 2)
|
59 |
+
|
60 |
+
def get_total_shots(self):
|
61 |
+
return self.calculate_stat(['shot_statsbomb_xg', 'team'], self._calculate_total_shots)
|
62 |
+
|
63 |
+
def _calculate_total_shots(self, events):
|
64 |
+
data = events[['shot_statsbomb_xg', 'team']].dropna(subset=['shot_statsbomb_xg'])
|
65 |
+
if data.empty:
|
66 |
+
return None, None
|
67 |
+
shot_counts = data.groupby('team').count()['shot_statsbomb_xg']
|
68 |
+
if len(shot_counts) < 2:
|
69 |
+
return None, None
|
70 |
+
return shot_counts.iloc[0], shot_counts.iloc[1]
|
71 |
+
|
72 |
+
def get_total_shots_off_target(self):
|
73 |
+
return self.calculate_stat(['shot_outcome', 'team'], self._calculate_total_shots_off_target)
|
74 |
+
|
75 |
+
def _calculate_total_shots_off_target(self, events):
|
76 |
+
off_target_outcomes = ['Off T', 'Blocked', 'Missed']
|
77 |
+
data = events[events['shot_outcome'].isin(off_target_outcomes)]
|
78 |
+
if data.empty:
|
79 |
+
return None, None
|
80 |
+
off_target_counts = data.groupby('team').size()
|
81 |
+
if len(off_target_counts) < 2:
|
82 |
+
return None, None
|
83 |
+
return off_target_counts.iloc[0], off_target_counts.iloc[1]
|
84 |
+
|
85 |
+
def get_total_shots_on_target(self):
|
86 |
+
return self.calculate_stat(['shot_outcome', 'team'], self._calculate_total_shots_on_target)
|
87 |
+
|
88 |
+
def _calculate_total_shots_on_target(self, events):
|
89 |
+
on_target_outcomes = ['Goal', 'Saved', 'Saved To Post', 'Shot Saved Off Target']
|
90 |
+
data = events[events['shot_outcome'].isin(on_target_outcomes)]
|
91 |
+
if data.empty:
|
92 |
+
return None, None
|
93 |
+
on_target_counts = data.groupby('team').size()
|
94 |
+
if len(on_target_counts) < 2:
|
95 |
+
return None, None
|
96 |
+
return on_target_counts.iloc[0], on_target_counts.iloc[1]
|
97 |
+
|
98 |
+
def get_total_passes(self):
|
99 |
+
return self.calculate_stat(['pass_end_location', 'team'], self._calculate_total_passes)
|
100 |
+
|
101 |
+
def _calculate_total_passes(self, events):
|
102 |
+
pass_counts = events.filter(regex='^pass_end_location|^team$').groupby('team').count()['pass_end_location']
|
103 |
+
if len(pass_counts) < 2:
|
104 |
+
return None, None
|
105 |
+
return pass_counts.iloc[0], pass_counts.iloc[1]
|
106 |
+
|
107 |
+
def get_successful_passes(self):
|
108 |
+
return self.calculate_stat(['pass_outcome', 'team'], self._calculate_successful_passes)
|
109 |
+
|
110 |
+
def _calculate_successful_passes(self, events):
|
111 |
+
home_passes, away_passes = self.get_total_passes()
|
112 |
+
unsuccessful_passes = events.filter(regex='^pass_outcome|^team$').groupby('team').count().reset_index()['pass_outcome']
|
113 |
+
if len(unsuccessful_passes) < 2:
|
114 |
+
return None, None
|
115 |
+
home_unsuccessful_passes = unsuccessful_passes.iloc[0]
|
116 |
+
away_unsuccessful_passes = unsuccessful_passes.iloc[1]
|
117 |
+
return home_passes - home_unsuccessful_passes, away_passes - away_unsuccessful_passes
|
118 |
+
|
119 |
+
def get_total_corners(self):
|
120 |
+
return self.calculate_stat(['pass_type', 'team'], self._calculate_total_corners)
|
121 |
+
|
122 |
+
def _calculate_total_corners(self, events):
|
123 |
+
corner_counts = events.filter(regex='^pass_type|^team$').query('pass_type == "Corner"').groupby('team').count()['pass_type']
|
124 |
+
if len(corner_counts) < 2:
|
125 |
+
return None, None
|
126 |
+
return corner_counts.iloc[0], corner_counts.iloc[1]
|
127 |
+
|
128 |
+
def get_total_fouls(self):
|
129 |
+
return self.calculate_stat(['type', 'team'], self._calculate_total_fouls)
|
130 |
+
|
131 |
+
def _calculate_total_fouls(self, events):
|
132 |
+
fouls = events[events['type'] == 'Foul Committed']
|
133 |
+
foul_counts = fouls.groupby('team').size()
|
134 |
+
if len(foul_counts) < 2:
|
135 |
+
return None, None
|
136 |
+
return foul_counts.iloc[0], foul_counts.iloc[1]
|
137 |
+
|
138 |
+
def get_total_yellow_cards(self):
|
139 |
+
return self.calculate_stat(['bad_behaviour_card', 'team'], self._calculate_total_yellow_cards)
|
140 |
+
|
141 |
+
def _calculate_total_yellow_cards(self, events):
|
142 |
+
yellow_card_counts = events.filter(regex='^bad_behaviour_card|^team$').query('bad_behaviour_card == "Yellow Card"').groupby('team').count()['bad_behaviour_card']
|
143 |
+
if len(yellow_card_counts) < 2:
|
144 |
+
return None, None
|
145 |
+
return yellow_card_counts.iloc[0], yellow_card_counts.iloc[1]
|
146 |
+
|
147 |
+
def get_total_red_cards(self):
|
148 |
+
return self.calculate_stat(['bad_behaviour_card', 'team'], self._calculate_total_red_cards)
|
149 |
+
|
150 |
+
def _calculate_total_red_cards(self, events):
|
151 |
+
red_card_counts = events.filter(regex='^bad_behaviour_card|^team$').query('bad_behaviour_card == "Red Card"').groupby('team').count()['bad_behaviour_card']
|
152 |
+
if len(red_card_counts) < 2:
|
153 |
+
return None, None
|
154 |
+
return red_card_counts.iloc[0], red_card_counts.iloc[1]
|