{ "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", "
\n", "
\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pitch = Pitch(\n", " provider=\"statsbomb\",\n", " orientation=\"horizontal\",\n", " view=\"full\",\n", " theme=\"night\",\n", " show_axis=False,\n", " show_legend=False,\n", " width=400,\n", " height=400,\n", " title=\"Harry Kane – Completed Passes\",\n", ")\n", "\n", "pitch.plot_arrows(passes)\n", "# NOTE: normally we'd just call `pitch.show()` here, but since\n", "# we're exporting to HTML docs, we need to use `HTML` to export\n", "# the plot\n", "# pitch.show()\n", "HTML(pio.to_html(pitch.fig, include_plotlyjs=\"cdn\"))" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.1" } }, "nbformat": 4, "nbformat_minor": 5 }