サッカーの試合データをPythonとAIで分析してみた

2025/03/07に公開

私はプログラミング経験は浅く、特にフロントエンドのプロジェクト経験が少しある程度です。現在、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に公開しています。

https://github.com/daidogenki/morph-football-ai/

使用するデータ

https://github.com/daidogenki/morph-football-ai/blob/main/data/sports_analyst_article - master_match.csv

https://github.com/daidogenki/morph-football-ai/blob/main/data/sports_analyst_article - match_data.csv

今回は、対戦相手や試合結果が入っている試合データと、それぞれの試合のトラッキングデータを使用します。 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チャット部分のコードを解説していきます。

https://github.com/daidogenki/morph-football-ai/blob/main/src/python/chat.py

システムプロンプト

正確なアウトプットを生成するために、データスキーマを明示的に渡しています。これにより、モデルがデータの構造を理解し、適切な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アプリを導入することで、スポーツアナリストはデータ処理や可視化にかかる時間を大幅に削減し、より高度な分析や戦略立案に集中できるようになります。

作成したアプリはデプロイしてリンクで共有することができます。デプロイに関しては以下の記事を参照ください。

https://zenn.dev/morph_tech_blog/articles/efa64d2b0c652d#デプロイしてアプリを共有する

Morphテックブログ

Discussion