작성일자 : 2024-05-07
Ver 0.1.1
강의에서 소개된 파이썬 주요 기능¶
- ffmpeg: https://anaconda.org/conda-forge/ffmpeg
- 터미널에서 conda install -c conda-forge ffmpeg 명령어 실행
- scipy.signal.savgol_filter: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.savgol_filter.html
- matplotlib.pyplot.quiver: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.quiver.html
- matplotlib.animation.FFMpegWriter: https://matplotlib.org/stable/api/_as_gen/matplotlib.animation.FFMpegWriter.html
- matplotlib.pyplot.clf: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.clf.html
- matplotlib.pyplot.close: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.close.html
선수별 속도 벡터 및 속력 산출¶
In [1]:
import os
new_dir = '/Users/limjongjun/Desktop/JayJay/Growth/Python/soccer-analytics'
os.chdir(new_dir)
import warnings
import numpy as np
import pandas as pd
import scipy.signal as signal
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from src.plot_utils import draw_pitch
warnings.simplefilter(action="ignore", category=pd.errors.PerformanceWarning)
(1) 결합 데이터 불러오기¶
In [2]:
match_id = 1
file = f'data_metrica/data/Sample_Game_{match_id}/Sample_Game_{match_id}_IntegratedData.csv'
traces = pd.read_csv(file, header=0, index_col=0)
traces
Out[2]:
period | time | H11_x | H11_y | H01_x | H01_y | H02_x | H02_y | H03_x | H03_y | ... | A24_speed | A26_vx | A26_vy | A26_speed | A27_vx | A27_vy | A27_speed | A28_vx | A28_vy | A28_speed | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
frame | |||||||||||||||||||||
1 | 1 | 0.04 | 0.08528 | 32.80184 | 33.95392 | 44.41896 | 35.04904 | 33.22684 | 32.16408 | 24.15972 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 1 | 0.08 | 0.09984 | 32.80184 | 33.95392 | 44.41896 | 35.04904 | 33.22684 | 32.16408 | 24.15972 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 | 1 | 0.12 | 0.11856 | 32.80184 | 33.95392 | 44.41896 | 35.04904 | 33.22684 | 32.16408 | 24.15972 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
4 | 1 | 0.16 | 0.12584 | 32.80184 | 33.92688 | 44.41556 | 35.03448 | 33.31184 | 32.18176 | 24.17672 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 | 1 | 0.20 | 0.13416 | 32.80184 | 33.90088 | 44.38292 | 35.01056 | 33.33224 | 32.18592 | 24.15904 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
145002 | 2 | 5800.08 | 9.76144 | 37.15860 | NaN | NaN | 68.47672 | 24.07336 | 71.52288 | 22.09116 | ... | 0.499132 | 0.354571 | 0.405385 | 0.538570 | 0.298571 | -0.494308 | 0.577482 | 0.0 | 0.0 | 0.0 |
145003 | 2 | 5800.12 | 9.82800 | 37.15860 | NaN | NaN | 68.50792 | 24.08356 | 71.52080 | 22.08028 | ... | 0.499132 | 0.453857 | 0.299462 | 0.543750 | 0.359857 | -0.606769 | 0.705455 | 0.0 | 0.0 | 0.0 |
145004 | 2 | 5800.16 | 9.88832 | 37.15860 | NaN | NaN | 68.54744 | 24.09308 | 71.53744 | 22.09932 | ... | 0.499132 | 0.553143 | 0.193538 | 0.586024 | 0.421143 | -0.719231 | 0.833459 | 0.0 | 0.0 | 0.0 |
145005 | 2 | 5800.20 | 9.92576 | 37.15792 | NaN | NaN | 68.58176 | 24.10464 | 71.58216 | 22.12176 | ... | 0.499132 | 0.652429 | 0.087615 | 0.658285 | 0.482429 | -0.831692 | 0.961483 | 0.0 | 0.0 | 0.0 |
145006 | 2 | 5800.24 | 9.92576 | 37.15792 | NaN | NaN | 68.64416 | 24.11008 | 71.63312 | 22.07824 | ... | 0.499132 | 0.751714 | -0.018308 | 0.751937 | 0.543714 | -0.944154 | 1.089519 | 0.0 | 0.0 | 0.0 |
145006 rows × 147 columns
(2) 특정 선수 속도/속력 산출¶
In [3]:
period = 1
player = 'H11' # 11번선수의 속도 벡터만 추출
player_cols = ['period', 'time', f'{player}_x', f'{player}_y']
player_traces = traces.loc[traces['period'] == period, player_cols]
player_traces
Out[3]:
period | time | H11_x | H11_y | |
---|---|---|---|---|
frame | ||||
1 | 1 | 0.04 | 0.08528 | 32.80184 |
2 | 1 | 0.08 | 0.09984 | 32.80184 |
3 | 1 | 0.12 | 0.11856 | 32.80184 |
4 | 1 | 0.16 | 0.12584 | 32.80184 |
5 | 1 | 0.20 | 0.13416 | 32.80184 |
... | ... | ... | ... | ... |
71264 | 1 | 2850.56 | 15.91304 | 45.93332 |
71265 | 1 | 2850.60 | 15.93800 | 45.93332 |
71266 | 1 | 2850.64 | 15.95360 | 45.93332 |
71267 | 1 | 2850.68 | 15.96920 | 45.93332 |
71268 | 1 | 2850.72 | 15.96920 | 45.93332 |
71268 rows × 4 columns
In [4]:
dt = player_traces['time'].diff()
vx = player_traces[f'{player}_x'].diff() / dt
vy = player_traces[f'{player}_y'].diff() / dt
raw_speeds = np.sqrt(vx ** 2 + vy ** 2) # 속력 구하기
# 12 m/s 보다 큰 값은 사람이 낼 수 없는 속력으로 이상치 처리
lim_speed = 12
vx[raw_speeds > lim_speed] = np.nan
vy[raw_speeds > lim_speed] = np.nan
plt.figure(figsize=(15, 5))
plt.rcParams.update({'font.size': 15})
plt.plot(player_traces['time'][10000:12000], raw_speeds[10000:12000])
plt.xlabel('Time [s]')
plt.ylabel('Speed [m/s]')
plt.show()
(3) 사비츠키-골레이 필터(Savitzky-Golay filter)를 활용한 속도/속력 스무딩(smoothing)¶
In [5]:
vx = signal.savgol_filter(vx, window_length=13, polyorder=1) # 주어진 점 좌우 13개 데이터를 참고
vy = signal.savgol_filter(vy, window_length=13, polyorder=1)
speeds = np.sqrt(vx ** 2 + vy ** 2)
plt.figure(figsize=(15, 5))
plt.rcParams.update({'font.size': 15})
plt.plot(player_traces['time'][10000:12000], speeds[10000:12000])
plt.xlabel('Time [s]')
plt.ylabel('Speed [m/s]')
plt.show()
(4) 선수별 속도/속력 산출 함수 구현¶
In [6]:
def calc_running_features(traces, lim_speed=12, smoothing=True, window_length=13, polyorder=1):
players = [c[:-2] for c in traces.columns if c[0] in ['H', 'A'] and c.endswith('_x')]
for period in traces['period'].unique():
period_traces = traces[traces['period'] == period]
idx = period_traces.index
dt = period_traces['time'].diff()
for player in players:
vx = period_traces[f'{player}_x'].diff() / dt
vy = period_traces[f'{player}_y'].diff() / dt
raw_speeds = np.sqrt(vx ** 2 + vy ** 2)
vx[raw_speeds > lim_speed] = np.nan
vy[raw_speeds > lim_speed] = np.nan
vx = vx.interpolate()
vy = vy.interpolate()
if smoothing:
vx = signal.savgol_filter(vx, window_length=13, polyorder=1)
vy = signal.savgol_filter(vy, window_length=13, polyorder=1)
traces.loc[idx, f'{player}_vx'] = vx
traces.loc[idx, f'{player}_vy'] = vy
traces.loc[idx, f'{player}_speed'] = np.sqrt(vx ** 2 + vy ** 2)
return traces
In [7]:
traces = calc_running_features(traces)
traces
Out[7]:
period | time | H11_x | H11_y | H01_x | H01_y | H02_x | H02_y | H03_x | H03_y | ... | A24_speed | A26_vx | A26_vy | A26_speed | A27_vx | A27_vy | A27_speed | A28_vx | A28_vy | A28_speed | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
frame | |||||||||||||||||||||
1 | 1 | 0.04 | 0.08528 | 32.80184 | 33.95392 | 44.41896 | 35.04904 | 33.22684 | 32.16408 | 24.15972 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 1 | 0.08 | 0.09984 | 32.80184 | 33.95392 | 44.41896 | 35.04904 | 33.22684 | 32.16408 | 24.15972 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 | 1 | 0.12 | 0.11856 | 32.80184 | 33.95392 | 44.41896 | 35.04904 | 33.22684 | 32.16408 | 24.15972 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
4 | 1 | 0.16 | 0.12584 | 32.80184 | 33.92688 | 44.41556 | 35.03448 | 33.31184 | 32.18176 | 24.17672 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 | 1 | 0.20 | 0.13416 | 32.80184 | 33.90088 | 44.38292 | 35.01056 | 33.33224 | 32.18592 | 24.15904 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
145002 | 2 | 5800.08 | 9.76144 | 37.15860 | NaN | NaN | 68.47672 | 24.07336 | 71.52288 | 22.09116 | ... | 0.499132 | 0.354571 | 0.405385 | 0.538570 | 0.298571 | -0.494308 | 0.577482 | 0.0 | 0.0 | 0.0 |
145003 | 2 | 5800.12 | 9.82800 | 37.15860 | NaN | NaN | 68.50792 | 24.08356 | 71.52080 | 22.08028 | ... | 0.499132 | 0.453857 | 0.299462 | 0.543750 | 0.359857 | -0.606769 | 0.705455 | 0.0 | 0.0 | 0.0 |
145004 | 2 | 5800.16 | 9.88832 | 37.15860 | NaN | NaN | 68.54744 | 24.09308 | 71.53744 | 22.09932 | ... | 0.499132 | 0.553143 | 0.193538 | 0.586024 | 0.421143 | -0.719231 | 0.833459 | 0.0 | 0.0 | 0.0 |
145005 | 2 | 5800.20 | 9.92576 | 37.15792 | NaN | NaN | 68.58176 | 24.10464 | 71.58216 | 22.12176 | ... | 0.499132 | 0.652429 | 0.087615 | 0.658285 | 0.482429 | -0.831692 | 0.961483 | 0.0 | 0.0 | 0.0 |
145006 | 2 | 5800.24 | 9.92576 | 37.15792 | NaN | NaN | 68.64416 | 24.11008 | 71.63312 | 22.07824 | ... | 0.499132 | 0.751714 | -0.018308 | 0.751937 | 0.543714 | -0.944154 | 1.089519 | 0.0 | 0.0 | 0.0 |
145006 rows × 147 columns
In [8]:
traces.to_csv('data_metrica/data/Sample_Game_1/Sample_Game_1_IntegratedData.csv')
(5) 여러 경기 위치 추적 데이터 가공 및 저장¶
In [9]:
matches = [d for d in os.listdir('data_metrica') if not d.startswith('.')]
matches.sort()
matches
Out[9]:
['README.md', 'data', 'documentation']
In [10]:
for match in matches:
home_file = f'data_metrica/data/{match}/{match}_RawTrackingData_Home_Team.csv'
away_file = f'data_metrica/data/{match}/{match}_RawTrackingData_Away_Team.csv'
event_file = f'data_metrica/data/{match}/{match}_RawEventsData.csv'
try:
home_traces = pd.read_csv(home_file, header=[0, 1, 2])
away_traces = pd.read_csv(away_file, header=[0, 1, 2])
events = pd.read_csv(event_file, header=0)
except FileNotFoundError:
continue
home_players = [c[2] for c in home_traces.columns[3:-2:2]]
home_trace_cols = [[f'H{int(p[6:]):02d}_x', f'H{int(p[6:]):02d}_y'] for p in home_players]
home_trace_cols = np.array(home_trace_cols).flatten().tolist()
home_traces.columns = ['period', 'frame', 'time'] + home_trace_cols + ['ball_x', 'ball_y']
home_traces = home_traces.set_index('frame').astype(float)
home_traces['period'] = home_traces['period'].astype(int)
away_players = [c[2] for c in away_traces.columns[3:-2:2]]
away_trace_cols = [[f'A{int(p[6:]):02d}_x', f'A{int(p[6:]):02d}_y'] for p in away_players]
away_trace_cols = np.array(away_trace_cols).flatten().tolist()
away_traces.columns = ['period', 'frame', 'time'] + away_trace_cols + ['ball_x', 'ball_y']
away_traces = away_traces.set_index('frame').astype(float)
away_traces['period'] = away_traces['period'].astype(int)
cols = home_traces.columns[:-2].tolist() + away_traces.columns[2:].tolist()
traces = pd.merge(home_traces, away_traces)[cols]
traces.index = home_traces.index.astype(int)
x_cols = [c for c in traces.columns if c.endswith('_x')]
y_cols = [c for c in traces.columns if c.endswith('_y')]
traces.loc[traces['period'] == 2, x_cols + y_cols] = 1 - traces.loc[traces['period'] == 2, x_cols + y_cols]
traces[x_cols] *= 104
traces[y_cols] *= 68
events.loc[events['Subtype'].isna(), 'Subtype'] = events.loc[events['Subtype'].isna(), 'Type']
for i, event in events.iterrows():
start_frame = event['Start Frame']
end_frame = event['End Frame']
traces.loc[start_frame:end_frame-1, 'event_player'] = event['From']
traces.loc[start_frame:end_frame-1, 'event_type'] = event['Type']
traces.loc[start_frame:end_frame-1, 'event_subtype'] = event['Subtype']
traces = calc_running_features(traces)
traces.to_csv(f'data_metrica/{match}/{match}_IntegratedData.csv')
print(f'Integrated data saved for {match}.')
경기 장면 애니메이션 시각화¶
(1) 속도 벡터 포함 특정 시점 이미지 시각화¶
In [11]:
frame = 1000
file = f'data_metrica/data/Sample_Game_{match_id}/Sample_Game_{match_id}_IntegratedData.csv'
traces = pd.read_csv(file, header=0, index_col=0)
data = traces.loc[frame]
fig, ax = draw_pitch(pitch='white', line='black')
for team, color in zip(['H', 'A'], ['r', 'b']):
x_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_x')]
y_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_y')]
ax.scatter(data[x_cols], data[y_cols], s=100, c=color, alpha=0.7)
for x, y in zip(x_cols, y_cols):
if not (np.isnan(data[x]) or np.isnan(data[y])):
ax.text(data[x] + 0.5, data[y] + 0.5, int(x[1:3]), fontsize=13, color=color)
vx_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_vx')]
vy_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_vy')]
# quiver : 화살표를 한번에 시각화
ax.quiver(
data[x_cols].astype(float), data[y_cols].astype(float),
data[vx_cols].astype(float), data[vy_cols].astype(float),
color=color, scale=8, scale_units='inches', width=0.002, alpha=0.7
)
ax.scatter(data['ball_x'], data['ball_y'], s=80, color='w', edgecolors='k')
time_text = f"{int(data['time'] // 60):02d}:{data['time'] % 60:05.2f}"
if not pd.isnull(data['event_subtype']):
event_text = f"{data['event_subtype']} by {data['event_player']}"
else:
event_text = ''
ax.text(51, 67, time_text, fontsize=15, ha='right', va='top')
ax.text(53, 67, event_text, fontsize=15, ha='left', va='top')
plt.show()
(2) 시점별 이미지 시각화 함수 구현¶
In [12]:
def plot_snapshot(
data, figax=None, team_colors=('r', 'b'),
annotate_players=True, annotate_events=True, show_velocities=True
):
if figax is None:
fig, ax = draw_pitch(pitch='white', line='black')
else:
fig, ax = figax
figobjs = []
for team, color in zip(['H', 'A'], team_colors):
x_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_x')]
y_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_y')]
obj = ax.scatter(data[x_cols], data[y_cols], s=100, c=color, alpha=0.7)
figobjs.append(obj)
if show_velocities:
vx_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_vx')]
vy_cols = [c for c in data.keys() if c.startswith(team) and c.endswith('_vy')]
obj = ax.quiver(
data[x_cols].astype(float), data[y_cols].astype(float),
data[vx_cols].astype(float), data[vy_cols].astype(float),
color=color, scale=8, scale_units='inches', width=0.002, alpha=0.7
)
figobjs.append(obj)
if annotate_players:
for x, y in zip(x_cols, y_cols):
if not (np.isnan(data[x]) or np.isnan(data[y])):
obj = ax.text(data[x] + 0.5, data[y] + 0.5, int(x[1:3]), fontsize=13, color=color)
figobjs.append(obj)
time_text = f"{int(data['time'] // 60):02d}:{data['time'] % 60:05.2f}"
if annotate_events:
if not pd.isnull(data['event_subtype']):
event_text = f"{data['event_subtype']} by {data['event_player']}"
else:
event_text = ''
figobjs.append(ax.text(51, 67, time_text, fontsize=15, ha='right', va='top'))
figobjs.append(ax.text(53, 67, event_text, fontsize=15, ha='left', va='top'))
else:
figobjs.append(ax.text(52, 67, time_text, fontsize=15, ha='center', va='top'))
obj = ax.scatter(data['ball_x'], data['ball_y'], s=80, color='w', edgecolors='k')
figobjs.append(obj)
ax.set_xlim(-10, 114)
ax.set_ylim(-7, 75)
return fig, ax, figobjs
In [13]:
frame = 1000
fig, ax, figobjs = plot_snapshot(traces.loc[frame])
plt.show()
figobjs
Out[13]:
[<matplotlib.collections.PathCollection at 0x16924a040>, <matplotlib.quiver.Quiver at 0x168687c70>, Text(5.34744, 34.50204, '11'), Text(21.41024, 56.25456, '1'), Text(20.68432, 47.09428, '2'), Text(20.99112, 39.804680000000005, '3'), Text(20.77688, 28.48608, '4'), Text(37.81416, 64.02968, '5'), Text(32.980239999999995, 52.19632, '6'), Text(28.1172, 41.54004, '7'), Text(34.924, 25.87148, '8'), Text(55.49832, 30.0392, '9'), Text(44.2112, 43.02924, '10'), <matplotlib.collections.PathCollection at 0x169231640>, <matplotlib.quiver.Quiver at 0x1691eae80>, Text(83.18520000000001, 34.36468, '25'), Text(55.11976000000001, 28.17328, '15'), Text(56.21592, 40.1678, '16'), Text(50.58536, 58.67876, '17'), Text(29.66368, 18.139200000000002, '18'), Text(34.2376, 37.83744, '19'), Text(42.56384, 54.5906, '20'), Text(17.01312, 57.3038, '21'), Text(37.09448, 66.29136, '22'), Text(22.0072, 42.10988, '23'), Text(24.42312, 45.04952, '24'), Text(51, 67, '00:40.00'), Text(53, 67, 'PASS by Player22'), <matplotlib.collections.PathCollection at 0x169251490>]
(3) 경기 장면 애니메이션 시각화 함수 구현¶
In [24]:
def save_clip(
clip_traces, fname='test', fps=25, figax=None, team_colors=('r', 'b'),
annotate_players=True, annotate_events=True, show_velocities=True
):
metadata = dict(title='Tracking Data', artist='Matplotlib', comment='Metrica tracking data clip')
# FFMpegWriter 패키지 필요
writer = animation.FFMpegWriter(fps=fps, metadata=metadata)
if not os.path.exists('match_clips'):
os.makedirs('match_clips')
file = f'/Users/limjongjun/Desktop/JayJay/Growth/Python/soccer-analytics/data_metrica/match_clips/{fname}.mp4'
if figax is None:
fig, ax = draw_pitch(pitch='white', line='black')
else:
fig, ax = figax
fig.set_tight_layout(True)
with writer.saving(fig, file, dpi=100):
for i in clip_traces.index:
frame_data = clip_traces.loc[i]
fig, ax, figobjs = plot_snapshot(
frame_data, (fig, ax), team_colors,
annotate_players, annotate_events, show_velocities
)
writer.grab_frame()
# 한 장면 장면 시각화 할 때마다 메모리에 객체들이 쌓이지 않도록 remove
for obj in figobjs:
obj.remove()
plt.clf()
plt.close(fig)
In [25]:
clip_traces = traces[:1000]
save_clip(clip_traces)
--------------------------------------------------------------------------- FileNotFoundError Traceback (most recent call last) Cell In[25], line 2 1 clip_traces = traces[:1000] ----> 2 save_clip(clip_traces) Cell In[24], line 20, in save_clip(clip_traces, fname, fps, figax, team_colors, annotate_players, annotate_events, show_velocities) 17 fig, ax = figax 18 fig.set_tight_layout(True) ---> 20 with writer.saving(fig, file, dpi=100): 21 for i in clip_traces.index: 22 frame_data = clip_traces.loc[i] File ~/opt/anaconda3/envs/class101/lib/python3.8/contextlib.py:113, in _GeneratorContextManager.__enter__(self) 111 del self.args, self.kwds, self.func 112 try: --> 113 return next(self.gen) 114 except StopIteration: 115 raise RuntimeError("generator didn't yield") from None File ~/opt/anaconda3/envs/class101/lib/python3.8/site-packages/matplotlib/animation.py:231, in AbstractMovieWriter.saving(self, fig, outfile, dpi, *args, **kwargs) 225 """ 226 Context manager to facilitate writing the movie file. 227 228 ``*args, **kw`` are any parameters that should be passed to `setup`. 229 """ 230 # This particular sequence is what contextlib.contextmanager wants --> 231 self.setup(fig, outfile, dpi, *args, **kwargs) 232 try: 233 yield self File ~/opt/anaconda3/envs/class101/lib/python3.8/site-packages/matplotlib/animation.py:320, in MovieWriter.setup(self, fig, outfile, dpi) 317 self._w, self._h = self._adjust_frame_size() 318 # Run here so that grab_frame() can write the data to a pipe. This 319 # eliminates the need for temp files. --> 320 self._run() File ~/opt/anaconda3/envs/class101/lib/python3.8/site-packages/matplotlib/animation.py:330, in MovieWriter._run(self) 327 _log.info('MovieWriter._run: running command: %s', 328 cbook._pformat_subprocess(command)) 329 PIPE = subprocess.PIPE --> 330 self._proc = subprocess.Popen( 331 command, stdin=PIPE, stdout=PIPE, stderr=PIPE, 332 creationflags=subprocess_creation_flags) File ~/opt/anaconda3/envs/class101/lib/python3.8/subprocess.py:858, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, encoding, errors, text) 854 if self.text_mode: 855 self.stderr = io.TextIOWrapper(self.stderr, 856 encoding=encoding, errors=errors) --> 858 self._execute_child(args, executable, preexec_fn, close_fds, 859 pass_fds, cwd, env, 860 startupinfo, creationflags, shell, 861 p2cread, p2cwrite, 862 c2pread, c2pwrite, 863 errread, errwrite, 864 restore_signals, start_new_session) 865 except: 866 # Cleanup if the child failed starting. 867 for f in filter(None, (self.stdin, self.stdout, self.stderr)): File ~/opt/anaconda3/envs/class101/lib/python3.8/subprocess.py:1704, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, start_new_session) 1702 if errno_num != 0: 1703 err_msg = os.strerror(errno_num) -> 1704 raise child_exception_type(errno_num, err_msg, err_filename) 1705 raise child_exception_type(err_msg) FileNotFoundError: [Errno 2] No such file or directory: 'ffmpeg'