<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://raphaelsenn.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://raphaelsenn.github.io/" rel="alternate" type="text/html" /><updated>2026-07-07T12:56:07+00:00</updated><id>https://raphaelsenn.github.io/feed.xml</id><title type="html">Raphael Senn</title><subtitle>Personal website.</subtitle><author><name>Raphael Senn</name><email>raphaelsenn@gmx.de</email></author><entry><title type="html">Implementing Player Vectors</title><link href="https://raphaelsenn.github.io/posts/2026/07/implementing-player-vectors/" rel="alternate" type="text/html" title="Implementing Player Vectors" /><published>2026-07-07T00:00:00+00:00</published><updated>2026-07-07T00:00:00+00:00</updated><id>https://raphaelsenn.github.io/posts/2026/07/implementing-player-vectors</id><content type="html" xml:base="https://raphaelsenn.github.io/posts/2026/07/implementing-player-vectors/"><![CDATA[<p>Since this summer is the FIFA World Cup 2026, my first blog post is about football. The goal of this post is to discuss the Player Vectors framework, provide detailed algorithms that make it straightforward to implement - in any programming language of choice.
Further, we reduce the dimension of player vectors by using the non-linear dimensionality reduction algorithm UMAP.</p>

<h1 id="motivation">Motivation</h1>
<p>Player Vectors were introduced by Decroos and Davis <a href="#decroos2020player">[1]</a>. The motivation behind Player Vectors is to provide a flexible framework for representing a player’s playing style in a compact vector form - derived directly from raw event data.
Such compact representations can be used in player scouting (e.g. by searching for players with similar playing styles). Furthermore, they are designed to be interpretable by both human experts and machine learning systems. E.g. each component of a player vector represents the “strength” of the playing style in a “lookup table” of heatmaps; fixed-size vectors can be easly feed into machine learning systems - neural networks, decision trees, …</p>

<p>To represent playing style in a fixed-size vector, we first need to define what we mean by playing style. 
Here, we follow the definition of Decroos and Davis <a href="#decroos2020player">[1]</a>:</p>

<ul>
  <li><strong>“A player’s playing style can be characterized by his preferred area’s on the field to occupy and which actions he tends to perform in each of these locations.”</strong></li>
</ul>

<p>According to Decroos and Davis, successfully characterizing playing style from event data relies on two assumptions. 
First, most players differ in their playing style, and second, a player’s playing style remains constant over short periods of time.</p>

<h1 id="preliminaries">Preliminaries</h1>

<h3 id="player-vectors">Player Vectors</h3>

<hr />

<p>Feel free to first read the original paper <a href="#decroos2020player">[1]</a> <a href="https://tomdecroos.github.io/reports/ecml19_tomd.pdf">“Player Vectors: Characterizing Soccer Players’
Playing Style from Match Event Streams”</a> before continuing with this blog post.</p>

<h3 id="frobenius-norm">Frobenius norm</h3>

<hr />

<p>Let $\textbf{X} \in \mathbb{R}^{m \times n}$ be an arbitrary matrix, the Frobenius norm is defined as:</p>

\[\begin{equation}
    ||\textbf{X}||_{F} = \sqrt{\sum_{i = 1}^{m} \sum_{j=1}^{n} X_{i, j}^{2}}
\end{equation}\]

<h3 id="non-negative-matrix-factorization">Non-Negative Matrix Factorization</h3>

<hr />

<p>Let $\mathbf{X}$ be an arbitrary non-negative matrix,</p>

\[\mathbf{X} \in \mathbb{R}_{\geq 0}^{m \times n},\]

<p>and let $k \in \mathbb{N}$ with</p>

\[k &lt; \min\{n, m\}\]

<p>be the number of components.</p>

<p>Non-Negative Matrix Factorization (NMF or NNMF) <a href="#lee2001">[2]</a> finds two matrices</p>

\[\mathbf{W} \in \mathbb{R}_{\geq 0}^{m \times k}
\quad \text{and} \quad
\mathbf{H} \in \mathbb{R}_{\geq 0}^{k \times n}\]

<p>that minimize the Frobenius norm</p>

\[\begin{equation}
\|\mathbf{X} - \mathbf{W}\mathbf{H}\|_{F}.
\end{equation}\]

<p>A useful property which will help us in later is that, each column of $\textbf{X}$ is a linear combination of the columns of $\textbf{W}$ weighted with the corresponding columns in $\textbf{H}$ <a href="#lee2001">[2]</a>. 
Formally, let $\mathbf{x_{i}}$ be the $i$-th column of $\textbf{X}$ and $\mathbf{h_{i}}$ be the $i$-th column of $\textbf{H}$, $\mathbf{x_{i}}$ can be computed as follows:</p>

\[\begin{equation}
    \mathbf{x_{i}} \approx \textbf{W}\mathbf{h_{i}}
  \end{equation}\]

<p>Therefore, $\textbf{X}$ can be approximated by the matrix multiplication of $\textbf{W}$ and $\textbf{H}$:</p>

\[\begin{equation}
    \textbf{X} \approx \textbf{W}\textbf{H}
\end{equation}\]

<p>It is important to notice, that NMF does not have a unique solution <a href="#nmf_unique">[3]</a>.</p>

<h2 id="algorithm-overview">Algorithm Overview</h2>
<p>When building player vectors we first need to select relevant event types (e.g. passes, shots, dribbles, …) and respective number of components (e.g. 4, 4, 5, …) for characterizing playing style <a href="#decroos2020player">[1]</a>.
The rest of the algorithm can be broken down to three major parts: <em>Constructing</em>, <em>Compressing</em>, and <em>Assembling</em> <a href="#decroos2020player">[1]</a>.</p>

<p>Flowchart 1 illustrates the three main steps when computing player vectors: Construction
(green), Compressing (red) and Assembling (blue).</p>

<figure id="flowchart_pvs" align="center">
  <img src="/images/posts/player_vectors/flowchart_pvs.png" alt="Flowchart 1" />
  <figcaption><em>Flowchart 1: Process of computing player vectors.</em></figcaption>
</figure>

<h3 id="construction">Construction</h3>

<hr />

<p>Let</p>

\[e \in \{\text{"shot"}, \text{"pass"}, \text{"dribble"}, \ldots \}\]

<p>denote a football event type. Given an event dataset</p>

\[\mathcal{D}_e = \{(p_i, x_i, y_i)\}_{i=1}^{N},\]

<p>containing $N$ events of type $e$, each 3-tuple consists of a player identification $p_i \in \mathbb{N}$ and the corresponding event location $(x_i, y_i) \in \mathbb{R}^2$. E.g. $p_i$ identifies the player who performed event $e$ at position $(x_i, y_i)$. $D_e$ may be all events of type $e$ in an entire season.</p>

<p>In the construction phase we compute player heatmaps (for an event of choice). Each player will have a unique heatmap summaraizing where he performs that event. The heatmaps can be computed as follows:</p>

<ul>
  <li>
    <p>Select an event of interest (e.g. $e = $ “pass”)</p>
  </li>
  <li>
    <p>Overlay a $m \times n$ grid over the soccer pitch</p>
  </li>
  <li>
    <p>Initalize a zero matrix $\textbf{X}^{(p)} \in \mathbb{N}_{0}$ for each player</p>
  </li>
  <li>
    <p>For each player, count the number of events on the pitch w.r.t. $(x, y)$ coordinates</p>
  </li>
  <li>
    <p>For each player, normalize the counts by dividing by the total number of playing time</p>
  </li>
  <li>
    <p>For each player, apply a Gaussian smoothing filter to the normalized counts</p>
  </li>
</ul>

<figure id="algorithm-1" align="center">
  <img src="/images/posts/player_vectors/algorithm_1.png" alt="Algorithm 1: Constructing Heatmaps" />
  <figcaption><em>Algorithm 1: Construct heatmaps.</em></figcaption>
</figure>

<p><a href="#algorithm-1">Algorithm 1</a> shows the process of computing player heatmaps of an event $e$. Each heatmap summarizes the spatial location where player $p$ performs event $e$. 
$G_{\sigma}$ denotes the 2-dimensional Gaussian filter with zero mean, which is a standard image processing algorithm. 
The operation $*$ denotes a 2-dimensional convolution which is also commonly used in image processing.</p>

<p>The Gaussian filter is defined by:</p>

\[\begin{equation}
    G_{\sigma}(x, y) = \frac{1}{\sqrt{2 \pi \sigma^2}}\exp(-\frac{x^2 + y^2}{2\sigma^2})
    \end{equation}\]

<p>A discrete 2-dimensional gaussian convolution is given by:</p>

\[\begin{equation}
    (\textbf{X} * G_{\sigma})_{i, j} = \sum_{m = -\infty}^{\infty} \sum_{n=-\infty}^{\infty} X_{m, n} G_{\sigma}(i - m, j - n)
    \end{equation}\]

<p>If we are interested in both the start and end location of an event, we can work with two datasets</p>

\[\mathcal{D}^{\text{start}}_{e}, \mathcal{D}^{\text{end}}_{e}\]

<p>containing the start and end positions. We then can compute the heatmaps in parallel using <a href="#algorithm-1">Algorithm 1</a>.</p>

<h3 id="compressing">Compressing</h3>

<hr />

<p>Assume we already computed player heatmaps</p>

\[\{\mathbf{X}^{(p_{1})}, ..., \mathbf{X}^{(p_{l})}\},\]

<p>where $l$ is the number of unique players in the dataset. In the next step, we compress the heatmaps using NMF. 
First, flatten each player heatmap $\textbf{X}^{(p_{i})} \in \mathbb{R}^{m \times n}$ into a column vector $\textbf{x}^{(p_{i})} \in \mathbb{R}^{mn}$. We then construct a matrix $\textbf{M} \in \mathbb{R}^{mn \times l}$ by stacking all flattend heatmaps:</p>

\[M = [\textbf{x}^{(p_{1})}, ..., \textbf{x}^{(p_{l})}] \in \mathbb{R}^{mn \times l},\]

<p>please note that the columns of matrix $\mathbf{M}$ are the reshaped heatmaps of each player.</p>

<p>Matrix $\mathbf{M}$ is compressed using NMF resulting in the factorization</p>

\[\mathbf{M} = \mathbf{W} \mathbf{H},\]

<p>where $\textbf{W} \in \mathbb{R}^{mn \times k}$ and $\textbf{H} \in \mathbb{R}^{k \times l}$.</p>

<p>As described in Preliminaries $k \in \mathbb{N}$ is a user defined parameter that controls the number of principal components, meaning $k$ directly controls the number of columns and rows of $\mathbf{W}$ and $\mathbf{H}$. 
The columns of $\mathbf{W}$ are the principal components which describe the spatial constructed heatmaps.
The rows of $\mathbf{H}$ are the compressed versions of the heatmaps.</p>

<p>We can interpret the $i$-th row $\mathbf{h_{i}}$ of $\mathbf{H}$ as the weights needed when multiplied with $\mathbf{W}$ to reconstruct the heatmap of $i$-th player:</p>

\[\textbf{x}^{(p_{i})} \approx \textbf{W}\mathbf{h_{i}}\]

<figure id="algorithm-2" align="center">
  <img src="/images/posts/player_vectors/algorithm_2.png" alt="Algorithm 1: Constructing Heatmaps" />
  <figcaption><em>Algorithm 2: Compress heatmaps.</em></figcaption>
</figure>

<p>Algorithm 2 shows the detailed procedure of compressing heatmaps using NMF. If we are interested in both the start and end locations of an event $e$, then for each player $p_i$ we first construct two heatmaps,</p>

\[\mathbf{X}^{(p_i)}_{\text{start}}, \mathbf{X}^{(p_i)}_{\text{end}} \in \mathbb{R}^{m \times n},\]

<p>as described in the Construction phase. We then flatten and concatenate as follows:</p>

\[\textbf{x}^{(p^{(i)})} = \text{concat}(\text{flatten}(\mathbf{X}^{(p^{(i)})}_{start}), \text{flatten}(\mathbf{X}^{(p^{(i)})}_{end})) \in \mathbb{R}^{2mn}\]

<p>Therefore $\mathbf{M} \in \mathbb{R}^{2mn \times l}, \mathbf{W} \in \mathbb{R}^{2mn \times k}, \text{} \mathbf{H} \in \mathbb{R}^{k \times l}$.</p>

<h3 id="assemble">Assemble</h3>

<hr />

<p>If we are building player vectors for just one event type $e$ with $k$ components, we would not need to assemble, because our player vector of the i-th player would just be the i-th column of matrix $\mathbf{H}$</p>

\[\mathbf{v}^{p_i}_{e} = \mathbf{h_{i}} = \mathbf{H_{:,i}}\]

<p>However, if we are using more than one event, we simply concatentate all matching player columns of matrices</p>

\[\mathbf{H}_{e_1}, \mathbf{H}_{e_2}, \mathbf{H}_{e_3}, ...\]

<p>together. E.g., let</p>

\[\mathbf{H}_{\mathrm{shot}} \in \mathbb{R}^{4 \times l}
\quad \text{and} \quad
\mathbf{H}_{\mathrm{cross}} \in \mathbb{R}^{8 \times l}\]

<p>be compressed heatmaps for the events shot and cross with components 4 and 8. The 4 + 8 = 12 dimensional player vector of the i-th player would be</p>

\[v^{(p_{i})}_{\text{shot, cross}} = \text{concat} (\textbf{h}^\text{shot}_{i}, \textbf{h}^{cross}_{i}) \in \mathbb{R}_{\ge 0}^{12}\]

<figure id="algorithm-3" align="center">
  <img src="/images/posts/player_vectors/algorithm_3.png" alt="Algorithm 1: Constructing Heatmaps" />
  <figcaption><em>Algorithm 3: Assemble player vectors.</em></figcaption>
</figure>

<h2 id="empirical-analysis">Empirical Analysis</h2>
<p>In this section, we fit Player Vectors to the event data from the 2017/2018 seasonal Wyscout kaggle dataset <a href="#wyscout">[7]</a>, <a href="#pappalardo2019soccer">[8]</a> and interprete the results.</p>

<p>The source code of my Player Vectors implementation can be found here:</p>

<ul>
  <li><a href="https://github.com/raphaelsenn/playervectors">https://github.com/raphaelsenn/playervectors</a></li>
</ul>

<p>The Wyscout kaggle dataset is availbale here:</p>

<ul>
  <li><a href="https://www.kaggle.com/datasets/aleespinosa/soccer-match-event-dataset">Wyscout 2017/2018 Dataset Kaggle</a></li>
</ul>

<p>My Player Vector package can be installed using pip:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>playervectors
</code></pre></div></div>
<hr />

<p>To characterize the playing style of football players we followed
Decroos and Davis <a href="#decroos2020player">[1]</a> by choosing the events shot, cross, dribble and pass. We constructed
24-dimensional player vectors for the four selected events shot (4 components), cross (4 components), 
dribble (8 components) and pass (8 components) on the event data.
The shot, cross, and dribble components only describe event start locations, whereas the pass components describe both start and end location.
To discretize the soccer pitch, we used a grid of shape $(50 \times 50)$ which was laid over the pitch as described in Algorithm 1. 
The smoothing parameter $\sigma = 4.0$ was found optimal when applying the Gaussian filter.
In our implementation we referred to the Non-Negative Matrix Factorization implementation of scikit-learn library <a href="#sklearn">[4]</a> and the Gaussian smoothing filter implementation of SciPy <a href="#scipy">[6]</a>.</p>

<p>The code snippets below illustrates, how one can compute player vectors from raw event data. But before we compute player vectors, we first need to preprocess the data:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">ast</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>


<span class="c1"># Load the Wyscout event data
# Dataset is available here:
# * https://www.kaggle.com/datasets/aleespinosa/soccer-match-event-dataset
</span><span class="n">df_events</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'./data/actions.csv'</span><span class="p">)</span>       <span class="c1"># Event data
</span><span class="n">df_players</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'./data/players.csv'</span><span class="p">)</span>      <span class="c1"># Player person information
</span><span class="n">df_playerank</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'./data/playerank.csv'</span><span class="p">)</span>  <span class="c1"># Player match information
</span>
<span class="c1"># player_id -&gt; total minutes
</span><span class="n">pid_to_minutes</span> <span class="o">=</span> <span class="p">(</span>
    <span class="n">df_minutes</span>
    <span class="p">.</span><span class="n">groupby</span><span class="p">(</span><span class="s">"playerID"</span><span class="p">)[</span><span class="s">"minutes"</span><span class="p">]</span>
    <span class="p">.</span><span class="nb">sum</span><span class="p">()</span>
    <span class="p">.</span><span class="n">to_dict</span><span class="p">()</span>
<span class="p">)</span>

<span class="c1"># player_id -&gt; full name
</span><span class="n">pid_to_name</span> <span class="o">=</span> <span class="p">(</span>
    <span class="n">df_players</span>
    <span class="p">.</span><span class="n">assign</span><span class="p">(</span><span class="n">full_name</span><span class="o">=</span><span class="n">df_players</span><span class="p">[</span><span class="s">"firstName"</span><span class="p">].</span><span class="n">fillna</span><span class="p">(</span><span class="s">""</span><span class="p">)</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">+</span> <span class="n">df_players</span><span class="p">[</span><span class="s">"lastName"</span><span class="p">].</span><span class="n">fillna</span><span class="p">(</span><span class="s">""</span><span class="p">))</span>
    <span class="p">.</span><span class="n">set_index</span><span class="p">(</span><span class="s">"wyId"</span><span class="p">)[</span><span class="s">"full_name"</span><span class="p">]</span>
    <span class="p">.</span><span class="nb">str</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span>
    <span class="p">.</span><span class="n">to_dict</span><span class="p">()</span>
<span class="p">)</span>

<span class="c1"># player_id -&gt; position, e.g. "GK", "DF", "MF", "FW"
</span><span class="n">roles</span> <span class="o">=</span> <span class="n">df_players</span><span class="p">[</span><span class="s">"role"</span><span class="p">].</span><span class="nb">map</span><span class="p">(</span><span class="n">ast</span><span class="p">.</span><span class="n">literal_eval</span><span class="p">)</span>

<span class="n">pid_to_position</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
    <span class="nb">zip</span><span class="p">(</span><span class="n">df_players</span><span class="p">[</span><span class="s">"wyId"</span><span class="p">],</span> <span class="n">roles</span><span class="p">.</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">r</span><span class="p">:</span> <span class="n">r</span><span class="p">[</span><span class="s">"code2"</span><span class="p">]))</span>
<span class="p">)</span>

<span class="c1"># Normalize playing direction of event data
# NOTE: In this dataset all events are normalized from right-to-left, we normalize from left-to-right
</span><span class="n">df_events</span><span class="p">[</span><span class="s">'start_x'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">105</span> <span class="o">-</span> <span class="n">df_events</span><span class="p">[</span><span class="s">'start_x'</span><span class="p">]</span>
<span class="n">df_events</span><span class="p">[</span><span class="s">'start_y'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">68</span> <span class="o">-</span> <span class="n">df_events</span><span class="p">[</span><span class="s">'start_y'</span><span class="p">]</span>
<span class="n">df_events</span><span class="p">[</span><span class="s">'end_x'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">105</span> <span class="o">-</span> <span class="n">df_events</span><span class="p">[</span><span class="s">'end_x'</span><span class="p">]</span>
<span class="n">df_events</span><span class="p">[</span><span class="s">'end_y'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">68</span> <span class="o">-</span> <span class="n">df_events</span><span class="p">[</span><span class="s">'end_y'</span><span class="p">]</span>
</code></pre></div></div>

<p>After preprocessing, we can compute player vectors as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="kn">from</span> <span class="nn">playervectors</span> <span class="kn">import</span> <span class="n">PlayerVectors</span>
<span class="kn">from</span> <span class="nn">playervectors.visualize</span> <span class="kn">import</span> <span class="n">plot_principal_components</span>

<span class="n">pvs</span> <span class="o">=</span> <span class="n">PlayerVectors</span><span class="p">(</span>
    <span class="n">grid</span><span class="o">=</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">50</span><span class="p">),</span>
    <span class="n">sigma</span><span class="o">=</span><span class="mf">4.0</span><span class="p">,</span>
    <span class="n">actions</span><span class="o">=</span><span class="p">[</span><span class="s">'shot'</span><span class="p">,</span> <span class="s">'cross'</span><span class="p">,</span> <span class="s">'dribble'</span><span class="p">,</span> <span class="s">'pass'</span><span class="p">],</span>
    <span class="n">components</span><span class="o">=</span><span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
    <span class="n">with_end_coordinates</span><span class="o">=</span><span class="p">[</span><span class="bp">False</span><span class="p">,</span> <span class="bp">False</span><span class="p">,</span> <span class="bp">False</span><span class="p">,</span> <span class="bp">True</span><span class="p">]</span>
<span class="p">)</span>

<span class="n">pvs</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span>
    <span class="n">df_events</span><span class="o">=</span><span class="n">df_events</span><span class="p">,</span>
    <span class="n">minutes_played</span><span class="o">=</span><span class="n">minutes_played</span><span class="p">,</span>
    <span class="n">player_names</span><span class="o">=</span><span class="n">playersID_to_name</span><span class="p">,</span>
    <span class="n">verbose</span><span class="o">=</span><span class="bp">True</span>
<span class="p">)</span>

<span class="c1"># Visualize principal components
</span><span class="n">plot_principal_components</span><span class="p">(</span><span class="n">pvs</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<figure id="18-principle-components" align="center">
  <img src="/images/posts/player_vectors/principal_components.png" alt="Algorithm 1: Constructing Heatmaps" />
  <figcaption><em>Figure 1: The heatmaps of each component.</em></figcaption>
</figure>

<p>Figure 1 illustrates all 24 components of the player vector,
constructed from the events</p>

\[\{\text{shot}, \text{cross}, \text{dribble}, \text{pass}\}\]

<p>with respective principal components</p>

\[\{4, 4, 8, 8\}\]

<p>over an entire season of multiple leagues.</p>

<p>Given a arbitrary player $p_i$, the resulting player vector</p>

\[v^{(p_i)} \in \mathbb{R}_{\ge 0}^{24}\]

<p>is therefore 24-dimensional, containing non-negative values.
Each element $v^{(p_i)}_{j}$ represents the playing style strength in the $j$-th heatmap shown in Figure 1. The values in the vector therefore indicate how strongly the player expresses each global playing style heatmap shown in Figure 1.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">playervectors.visualize</span> <span class="kn">import</span> <span class="n">plot_weight_distribution</span>


<span class="n">plot_weight_distribution</span><span class="p">(</span><span class="n">pvs</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<figure id="principle-components-distribution" align="center">
  <img src="/images/posts/player_vectors/principal_components_distribution.png" alt="Figure 2" />
  <figcaption><em>Figure 2: The weight distribution of our 24-component player vectors.</em></figcaption>
</figure>

<p>Our approach yielded more than 3000 distinct player vectors computed from more than 2.4M events.
Figure 2 shows the weight distribution across all player vectors. Since passes are the most common event, their associated weights dominate the distribution.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">playervectors.visualize</span> <span class="kn">import</span> <span class="n">plot_player_weights</span>

<span class="n">selected_players</span> <span class="o">=</span> <span class="p">[</span>
    <span class="mi">14712</span><span class="p">,</span>       <span class="c1"># Manuel Neuer,         Positon: Goalkeeper
</span>    <span class="mi">3359</span><span class="p">,</span>        <span class="c1"># Lionel Messi,         Positon: Center forward
</span>    <span class="mi">14817</span><span class="p">,</span>       <span class="c1"># Robert Lewandowski,   Positon: Center forward
</span>    <span class="mi">3322</span><span class="p">,</span>        <span class="c1"># Christiano Ronaldo,   Positon: Center forward
</span>    <span class="mi">3306</span><span class="p">,</span>        <span class="c1"># Sergio Ramos,         Positon: Center back
</span>    <span class="mi">38021</span><span class="p">,</span>       <span class="c1"># Kevin De Bruyne       Positon: Central midfielder
</span><span class="p">]</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">nrows</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">ncols</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">8</span><span class="p">))</span>
<span class="n">ax</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">flatten</span><span class="p">()</span>

<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">player_id</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">selected_players</span><span class="p">):</span>
    <span class="n">plot_player_weights</span><span class="p">(</span><span class="n">pvs</span><span class="p">,</span> <span class="n">player_id</span><span class="o">=</span><span class="n">player_id</span><span class="p">,</span> <span class="n">ax</span><span class="o">=</span><span class="n">ax</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>

<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<figure id="principle-components-distribution" align="center">
  <img src="/images/posts/player_vectors/player_weights.png" alt="Figure 3: Constructing Heatmaps" />
  <figcaption><em>Figure 3: Player vectors for Manuel Neuer (Goalkeeper), Lionel Messi (Striker), Robert Lewandowski (Striker), Cristiano Ronaldo (Striker), Sergio Ramos (Defender), and Kevin De Bruyne (Midfielder).</em></figcaption>
</figure>

<p>Figure 3 shows the player vectors for Manuel Neuer (Goalkeeper), Lionel Messi (Striker), Robert Lewandowski (Striker), Cristiano Ronaldo (Striker), Sergio Ramos (Defender), and Kevin De Bruyne (Midfielder).</p>

<p>For Manuel Neuer we see high values in the components 9, 17, 18, and 19. Looking these value up in the heatmap table gives patterns that match his role as a goalkeeper quite well. Lionel Messi has high values in component 1, which corresponds to close range shots, and component 23, which also fits his attacking role.</p>

<h3 id="embed-player-vectors-using-umap">Embed Player Vectors using UMAP</h3>

<p>The objective in this subsection consists of reducing the dimensionality of our $24$-dimensional player vectors using Uniform Manifold Approximation and Projection for Dimension Reduction (UMAP) <a href="#umap">[5]</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">seaborn</span> <span class="k">as</span> <span class="n">sns</span>
<span class="kn">import</span> <span class="nn">umap</span>

<span class="c1"># Shape [n_players, 24]
</span><span class="n">X_pvs</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">([</span><span class="n">val</span> <span class="k">for</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">pvs</span><span class="p">.</span><span class="n">player_vectors_</span><span class="p">.</span><span class="n">values</span><span class="p">()])</span>

<span class="c1"># Playing position
</span><span class="n">y_labels</span> <span class="o">=</span> <span class="p">[</span><span class="n">pid_to_position</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">pvs</span><span class="p">.</span><span class="n">player_vectors_</span><span class="p">.</span><span class="n">keys</span><span class="p">()]</span>

<span class="n">reducer</span> <span class="o">=</span> <span class="n">umap</span><span class="p">.</span><span class="n">UMAP</span><span class="p">(</span><span class="n">n_neighbors</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span> <span class="n">metric</span><span class="o">=</span><span class="s">"manhattan"</span><span class="p">)</span>

<span class="c1"># Shape [n_players, 2]
</span><span class="n">embedding</span> <span class="o">=</span> <span class="n">reducer</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>

<span class="n">data</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s">"z0"</span><span class="p">:</span> <span class="n">embedding</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">],</span> <span class="s">"z1"</span><span class="p">:</span> <span class="n">embedding</span><span class="p">[:,</span> <span class="mi">1</span><span class="p">],</span> <span class="s">"Position"</span><span class="p">:</span> <span class="n">y_labels</span><span class="p">})</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">()</span>
<span class="n">sns</span><span class="p">.</span><span class="n">scatterplot</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">x</span><span class="o">=</span><span class="s">"z0"</span><span class="p">,</span> <span class="n">y</span><span class="o">=</span><span class="s">"z1"</span><span class="p">,</span> <span class="n">hue</span><span class="o">=</span><span class="s">"Position"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<figure id="umap-embedding" align="center">
  <img src="/images/posts/player_vectors/pv_umap_embedding.png" alt="Figure 3: Constructing Heatmaps" />
  <figcaption><em>Figure 4: 2-dimensional UMAP embedding of our player vectors with n_neighbors=15 and min_dist = 0.01.</em></figcaption>
</figure>

<p>Figure 4 shows the resulting UMAP-embeddings of our player vectors.
Each player is labeled with their according to thier playing position on the field.</p>

<p>We observe that, even without relying on clustering, that player vectors are meaningfully embedded (except for some outliers). Goalkeepers and defenders form clearly clusters, which we attribute to their distinctive playing styles. E.g. most goalkeepers
and defenders exhibit low values in the shot and cross components, making them easily
distinguishable.
Midfielders experience the highest variance - their embeddings are spread the widest. This may be because some midfielders operate in
attacking positions, while others play deeper and resemble defenders.</p>

<h2 id="conclusion">Conclusion</h2>
<p>We have successfully implemented the Player Vectors framework and computed player vectors on a per-season level using the complete Wyscout dataset of the 2017/2018 season. 
Furthermore, we embedded player vectors using the dimensionality reduction algorithm UMAP and derived distinctive clusters when labeling players with thier actual playing position.</p>

<h2 id="references">References</h2>

<p><a id="decroos2020player"></a>
[1] T. Decroos and J. Davis, “Player Vectors: Characterizing Soccer Players’ Playing Style from Match Event Streams,” in <i>Machine Learning and Knowledge Discovery in Databases</i>, ECML PKDD 2019, Lecture Notes in Computer Science, vol. 11908, Springer, Cham, 2020, pp. 569–584.</p>

<p><a id="lee2001"></a>
[2] D. D. Lee and H. S. Seung, “Algorithms for Non-negative Matrix Factorization,” <em>Advances in Neural Information Processing Systems</em>, 13, 2001.</p>

<p><a id="nmf_unique"></a>
[3] Donoho, David L. and Stodden, Victoria, “When does non-negative matrix factorization give a correct decomposition into parts?”, <em>Advances in Neural Information Processing Systems.</em></p>

<p><a id="sklearn"></a>
[4] F. Pedregosa et al., “Scikit-learn: Machine Learning in Python,”
<em>Journal of Machine Learning Research</em>, vol. 12, pp. 2825–2830, 2011.</p>

<p><a id="umap"></a>
[5] L. McInnes, J. Healy, and J. Melville, “UMAP: Uniform Manifold Approximation
and Projection for Dimension Reduction,” arXiv:1802.03426, 2018.</p>

<p><a id="scipy"></a>
[6] P. Virtanen et al., “SciPy 1.0: Fundamental Algorithms for Scientific Computing
in Python,” <em>Nature Methods</em>, vol. 17, pp. 261–272, 2020.</p>

<p><a id="wyscout"></a>
[7] Wyscout soccer match event dataset, Kaggle.</p>

<p><a id="pappalardo2019soccer"></a>
[8] L. Pappalardo and E. Massucco, “Soccer match event dataset,” figshare, Collection, 2019. doi: 10.6084/m9.figshare.c.4415000.v5.</p>]]></content><author><name>Raphael Senn</name><email>raphaelsenn@gmx.de</email></author><category term="data-science" /><summary type="html"><![CDATA[Since this summer is the FIFA World Cup 2026, my first blog post is about football. The goal of this post is to discuss the Player Vectors framework, provide detailed algorithms that make it straightforward to implement - in any programming language of choice. Further, we reduce the dimension of player vectors by using the non-linear dimensionality reduction algorithm UMAP.]]></summary></entry></feed>