{ "cells": [ { "cell_type": "markdown", "id": "45ef0eb0", "metadata": {}, "source": [ "# 📍 Recipe: Drawing a Pass Map\n", "\n", "This example demonstrates how to visualize a player's passes using `Flow` to process StatsBomb data and `mplsoccer` to draw the pitch.\n", "\n", "## 🧰 What You’ll Learn\n", "\n", "- How to filter for completed passes\n", "- How to extract pass start and end locations\n", "- How to use `.assign()` to extract nested data as an alternative to using `.select()` + `.rename()`\n", "- How to use `.split_array()` to extract the location array into multiple fields\n", "- How to optimize the flow for better performance\n", "- How to plot them using the `penaltyblog` `Pitch` class\n", "\n", "## Imports" ] }, { "cell_type": "code", "execution_count": 1, "id": "e6449b0d", "metadata": {}, "outputs": [], "source": [ "from IPython.display import HTML\n", "from penaltyblog.matchflow import Flow, where_equals, get_field\n", "from penaltyblog.viz import Pitch\n", "import plotly.io as pio" ] }, { "cell_type": "markdown", "id": "b6ae3204", "metadata": {}, "source": [ "## Load the Data" ] }, { "cell_type": "code", "execution_count": 2, "id": "76571641", "metadata": {}, "outputs": [], "source": [ "# Load events for a StatsBomb match\n", "match_id = 22912 # Champions League Final 2018/2019\n", "\n", "flow = Flow.statsbomb.events(match_id)" ] }, { "cell_type": "markdown", "id": "1ad09160", "metadata": {}, "source": [ "## Extract the Passes" ] }, { "cell_type": "code", "execution_count": 3, "id": "9b3cb682", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/martin/repos/penaltyblog/venv/lib/python3.13/site-packages/statsbombpy/api_client.py:21: NoAuthWarning: credentials were not supplied. open data access only\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "{'x': 60.0, 'y': 40.0, 'x2': 43.4, 'y2': 40.3}\n", "{'x': 39.6, 'y': 63.4, 'x2': 39.8, 'y2': 55.0}\n", "{'x': 86.5, 'y': 47.8, 'x2': 75.6, 'y2': 55.6}\n" ] } ], "source": [ "passes = (\n", " flow.filter(where_equals(\"player.name\", \"Harry Kane\"))\n", " .filter(where_equals(\"type.name\", \"Pass\"))\n", " .filter(lambda r: get_field(r, \"pass.outcome.name\") is None)\n", " .split_array(\"location\", [\"start_x\", \"start_y\"])\n", " .split_array(\"pass.end_location\", [\"end_x\", \"end_y\"])\n", " .select(\"start_x\", \"start_y\", \"end_x\", \"end_y\")\n", " .rename(start_x=\"x\", start_y=\"y\", end_x=\"x2\", end_y=\"y2\")\n", " .dropna()\n", ")\n", "\n", "for pass_ in passes.head(3):\n", " print(pass_)" ] }, { "cell_type": "markdown", "id": "2e227238", "metadata": {}, "source": [ "## Plotting the Passes" ] }, { "cell_type": "code", "execution_count": 4, "id": "7300585b", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/martin/repos/penaltyblog/venv/lib/python3.13/site-packages/statsbombpy/api_client.py:21: NoAuthWarning:\n", "\n", "credentials were not supplied. open data access only\n", "\n" ] }, { "data": { "text/html": [ "\n", "
\n", "\n", "