サッカーの試合データをPythonとAIで分析してみた
私はプログラミング経験は浅く、特にフロントエンドのプロジェクト経験が少しある程度です。現在、Pythonを学習中で、スポーツデータの分析をコードベースで行う方法について模索しています。
今回は、Morph[Github]を使ってサッカーの試合データをPythonとAIで分析してみました。
Morphは、PythonとマークダウンでAIアプリを構築できるフレームワークです。基本的なPythonとNode.jsの環境があれば、インタラクティブなAIアプリを少ないコードで構築することができます。
作ったもの
環境のセットアップ
morph new
コマンドで新しいプロジェクトを立ち上げます。この時パッケージマネージャーはpoetryを選択することを推奨しています。
morph new football-ai
このチュートリアルではOpenAI APIを使うので、 .env
にAPI KEYをセットしてください。
OPENAI_API_KEY=[Put your API KEY]
また、requirements.txt
にOpenAIのバージョンを記載してください。
openai==1.62.0
morph-data==0.1.10
データ可視化
今回作ったデータ可視化は以下の通りです
- ポジションごとの平均スタッツを試合ごとに可視化
- 「走行距離」「最大速度」「インパクト数」の平均値
- X軸は試合、Y軸はドロップダウンで選択
- 選手ごとの試合別スタッツを可視化
- 各選手の試合ごとのパフォーマンスを折れ線グラフで表示
- 選手の選択ドロップダウン
コード解説
コードの全体はGithubに公開しています。
使用するデータ
今回は、対戦相手や試合結果が入っている試合データと、それぞれの試合のトラッキングデータを使用します。 src/data
配下にデータを格納してください。
ここからは、コードの各部分のポイントを解説していきます。
データ分析
データのロード
データの読み込みには、Morphフレームワークの機能を使っています。MorphフレームワークではファイルシステムのCSVファイルをSQLで読み込むことができます。この部分はDuckDBを使って実現しています。
# CSVをSQLでロード
{{
config(
connection="DUCKDB"
)
}}
select * from read_csv_auto('data/data_mart.csv')
SQLのconfigで指定したaliasを用いて、Python関数でデータを受け取ります。
@morph.func
@morph.load_data("get_match_data")
@morph.load_data("get_master_match")
def chart_match(context: MorphGlobalContext):
# データをロード
match_df = context.data["get_match_data"]
master_df = context.data["get_master_match"]
各種指標の計算
各種指標の計算にはPandasを使っています。
# Dateカラムでjoin(内結合)
merged_df = match_df.merge(master_df, on="date", how="inner")
merged_df["date"] = pd.to_datetime(merged_df["date"], errors='coerce').dt.date
# X軸のラベルを "VS_Name (Date)" の形式にする
merged_df["Match_Label"] = merged_df["vs_name"] + " (" + merged_df["date"].astype(str) + ")"
units = {
"distance": "メートル",
"max_velocity": "km/h",
"impacts": "回"
}
Plotlyで可視化
チャートの作成には、Plotlyを用いています。
# 可視化
fig = px.bar(
avg_metrics_df,
x="Match_Label",
y=initial_metric, # 初期Y軸
color="position",
barmode="group",
title="Positionごとの平均指標 (VS_Name別)",
category_orders={"position": ["DF", "MF", "FW"]} # Positionの順番を指定
)
また、以下のようにすることで、インタラクティブな要素 (ドロップダウンやボタンなど) をチャートに追加することができます。
# スタッツ選択メニュー
metric_buttons = [
{
"label": metric,
"method": "update",
"args": [
{"y": [merged_df[merged_df["player_name"] == player][metric].tolist() for player in players]},
{"yaxis": {"title": f"{metric} ({units[metric]})"}}
]
} for metric in ["distance", "max_velocity", "impacts"]
]
# 選手選択メニュー(自由選択できるように)
player_buttons = [
{
"label": player,
"method": "update",
"args": [
{"visible": [True if p == player else "legendonly" for p in players]},
]
} for player in players
]
# すべての選手を表示するオプション
player_buttons.append(
{
"label": "All Players",
"method": "update",
"args": [{"visible": [True] * len(players)}]
}
)
# メニューを追加
fig.update_layout(
updatemenus=[
{"buttons": metric_buttons, "direction": "down", "showactive": True, "x": 0.17, "xanchor": "left", "y": 1.15, "yanchor": "top"},
{"buttons": player_buttons, "direction": "down", "showactive": True, "x": 0.02, "xanchor": "left", "y": 1.15, "yanchor": "top"},
]
)
AIチャット
続いて、AIチャット部分のコードを解説していきます。
システムプロンプト
正確なアウトプットを生成するために、データスキーマを明示的に渡しています。これにより、モデルがデータの構造を理解し、適切なSQLクエリを作成できるようになります。お手持ちのデータの内容に合わせてスキーマを修正してください。
Function callingでSQL生成し、実行
ユーザーが試合のデータ分析を求めた際、GPTがDuckDB用のSQLクエリを動的に生成します。このクエリを実行することで、試合結果や選手のパフォーマンスに関する統計情報を取得できます。
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
functions=[
{
"name": "generate_sql",
"description": "Generate SQL queries based on user prompt",
"parameters": {
"type": "object",
"properties": {
"sql_queries": {
"type": "array",
"description": "レポートを作成するために必要なSQLとその説明を出力",
"items": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL query to execute. Make sure to use 'from read_csv('data/data_mart.csv')' as the table source."
},
"description": {
"type": "string",
"description": "このクエリが計算する内容の説明を日本語で出力"
}
},
"required": ["query", "description"]
}
},
},
"required": ["sql_queries"]
}
}
]
)
# ChatGPTが生成したSQLを実行
response_json = json.loads(response.choices[0].message.function_call.arguments)
sql_queries = response_json['sql_queries']
for sql in sql_queries:
data = execute_sql(sql['query'], "DUCKDB")
data_md = data.to_markdown(index=False)
messages.append({
"role": "assistant",
"content": f"""集計したデータは以下の通りです。
{sql['description']}
})
SQLの実行結果を元にレポート生成
最後に、取得したデータを基に、GPTが試合の詳細な分析レポートを作成します。レポートには、試合の概要、注目すべき点、選手のパフォーマンス、試合の傾向などが含まれます。
# レポートを作成
messages.extend([
{
"role": "system",
"content": f"""優秀なアナリストの立場で試合データを分析し、レポートを作成してください。
以下の試合データを基に、詳細なレポートを作成してください。
データの概要を述べ、注目すべき点、選手のパフォーマンス、試合の傾向などを分析し、まとめてください。
"""
},{
"role": "user",
"content": prompt
}
])
yield stream_chat(f"""
# レポート
""")
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
stream=True,
)
for chunk in response:
yield stream_chat(chunk.choices[0].delta.content)
Morphフレームワークでは、 stream_chat
という関数を用いることでフロントエンドに対してストリーミングでレスポンスを送ることができます。
フロントエンドの構築
Morphでは、 src/pages
以下にmdxファイルを配置してフロントエンドを構築ができます。
今回は<Chat />、<Grid />、Tailwind CSSのユーティリティクラスを使用して、レイアウトも調整しています。
# アナリストダッシュボード ⚽️
<Grid cols="1">
<div>
<Embed loadData="chart_match" height={500} width={1250} />
</div>
</Grid>
<Grid cols="1">
<div>
<Embed loadData="chart_player" height={500} width={1250} />
</div>
</Grid>
<Grid cols="1">
<div className="p-4 bg-gray-50 rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
<Chat postData="chat" />
</div>
</Grid>
まとめ
以上のようにAIアプリ(AIダッシュボード)を簡単に作成することができました。スポーツ現場において、このAIアプリを導入することで、スポーツアナリストはデータ処理や可視化にかかる時間を大幅に削減し、より高度な分析や戦略立案に集中できるようになります。
作成したアプリはデプロイしてリンクで共有することができます。デプロイに関しては以下の記事を参照ください。
Discussion