{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "##
Creating Player Radar Charts using Plotly
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Radar charts are a very popular way of visualising multiple features for a single observation(you may plot multiple observations on the same radar but that breaks eveything really quick). \n", "Follow along and I'll show you how to create your own in [Plotly](https://plot.ly/) and some other things you should keep in mind. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The method for creating said radars is pretty intuitive and there's surprisingly less code. Here's a an overview of the steps below:\n", "\n", "1. Imports \n", "\n", "2. Selecting the columns we need for the radars \n", "\n", "3. Choosing the templates for different positions \n", "\n", "4. Defining the `update_plot` function to allow us to interact with the radar using the widgets. " ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import plotly as py\n", "import plotly.graph_objs as go\n", "import ipywidgets as widgets\n", "import plotly.express as px\n", "import pandas as pd\n", "\n", "py.offline.init_notebook_mode(connected=True)\n", "\n", "cols = ['Player', 'Def duels per 90 rank',\n", " 'Def duels won rank',\n", " 'Interceptions per 90 rank', 'Non-penalty goals per 90 rank',\n", " 'Shots per 90 rank',\n", " 'Crosses rank', 'Succ Crosses rank', 'Succ Dribb rank', 'Progressive runs per 90 rank',\n", " 'Passes acc. %', 'Succ Pass rank',\n", " 'xA rank','Key passes per 90 rank',\n", " 'Through p90 rank', 'Comp DP rank',\n", " 'Comp DC rank'] ##These are the columns I decided I needed. You may choose more or less\n", "\n", "df = pd.read_excel(r\"C:\\\\Users\\ADMIN\\Desktop\\Abhishek\\James_Wingers.xlsx\", usecols = cols)" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
PlayerDef duels per 90 rankDef duels won rankInterceptions per 90 rankNon-penalty goals per 90 rankShots per 90 rankCrosses rankSucc Crosses rankSucc Dribb rankProgressive runs per 90 rankPasses acc. %Succ Pass rankxA rankKey passes per 90 rankThrough p90 rankComp DP rankComp DC rank
0S. Ferguson24.120.845.010.92.140.0094.516.48.771.6829.695.679.128.55.494.5
1S. Downing5.425.258.219.753.837.5091.275.865.978.9574.771.484.661.556.093.4
2J. Wallace60.482.426.335.152.733.2197.873.682.466.9361.550.559.368.171.497.8
3A. Armstrong6.524.19.853.879.126.7763.771.481.371.6326.363.754.942.873.663.7
4B. Celina1.06.56.549.489.030.7728.552.792.385.2195.684.664.894.594.528.5
\n", "
" ], "text/plain": [ " Player Def duels per 90 rank Def duels won rank \\\n", "0 S. Ferguson 24.1 20.8 \n", "1 S. Downing 5.4 25.2 \n", "2 J. Wallace 60.4 82.4 \n", "3 A. Armstrong 6.5 24.1 \n", "4 B. Celina 1.0 6.5 \n", "\n", " Interceptions per 90 rank Non-penalty goals per 90 rank \\\n", "0 45.0 10.9 \n", "1 58.2 19.7 \n", "2 26.3 35.1 \n", "3 9.8 53.8 \n", "4 6.5 49.4 \n", "\n", " Shots per 90 rank Crosses rank Succ Crosses rank Succ Dribb rank \\\n", "0 2.1 40.00 94.5 16.4 \n", "1 53.8 37.50 91.2 75.8 \n", "2 52.7 33.21 97.8 73.6 \n", "3 79.1 26.77 63.7 71.4 \n", "4 89.0 30.77 28.5 52.7 \n", "\n", " Progressive runs per 90 rank Passes acc. % Succ Pass rank xA rank \\\n", "0 8.7 71.68 29.6 95.6 \n", "1 65.9 78.95 74.7 71.4 \n", "2 82.4 66.93 61.5 50.5 \n", "3 81.3 71.63 26.3 63.7 \n", "4 92.3 85.21 95.6 84.6 \n", "\n", " Key passes per 90 rank Through p90 rank Comp DP rank Comp DC rank \n", "0 79.1 28.5 5.4 94.5 \n", "1 84.6 61.5 56.0 93.4 \n", "2 59.3 68.1 71.4 97.8 \n", "3 54.9 42.8 73.6 63.7 \n", "4 64.8 94.5 94.5 28.5 " ] }, "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I've included a part of the dataset [here](https://github.com/AbhishekSharma99/Sample-radar-dataset/blob/master/Sample.xlsx) (graciously provided to me by [James Socik](https://twitter.com/Blades_analytic)) so that you can follow along. The dataset is in [tidy format](https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html) which essentially means that *every observation is a row and every column is a feature*. For us, the observations are the players, and the features the stats for the particular player. \n", "The plotting will most definitely work for other not-so-clean formats as well - you'll have to make some minor tweaks to the data-shaping bit below." ] }, { "cell_type": "code", "execution_count": 60, "metadata": { "scrolled": false }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a9d7eba8ea884c459c85c5592c2d1457", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(Dropdown(description='Player List', options=('A. Adomah', 'A. Armstrong', 'A. Browne', '…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def update_plot(player, template):\n", " \n", " \"\"\"\n", " This function updates the plot everytime a widget is changed\n", " \"\"\"\n", " \n", " x = df.loc[df['Player'] == player]\n", " \n", " if template == \"Striker\":\n", " columns =['Non-penalty goals per 90 rank',\n", " 'Shots per 90 rank',\n", " 'Key passes per 90 rank',\n", " 'Passes acc. %',\n", " 'Succ Dribb rank',\n", " 'Progressive runs per 90 rank',\n", " 'Def duels per 90 rank']\n", " \n", " \n", " elif template == \"Attacking_mid\":\n", " columns= ['Non-penalty goals per 90 rank',\n", " 'Shots per 90 rank',\n", " 'Key passes per 90 rank',\n", " 'xA rank', \n", " 'Passes acc. %',\n", " 'Succ Dribb rank',\n", " 'Progressive runs per 90 rank',\n", " 'Def duels per 90 rank',\n", " 'Through p90 rank',\n", " 'Comp DP rank']\n", " \n", "\n", " elif template == \"Central_mid\":\n", " columns= ['Shots per 90 rank',\n", " 'Key passes per 90 rank',\n", " 'xA rank', \n", " 'Succ Dribb rank',\n", " 'Progressive runs per 90 rank',\n", " 'Through p90 rank',\n", " 'Passes acc. %', \n", " 'Comp DP rank',\n", " 'Def duels per 90 rank',\n", " 'Interceptions per 90 rank'] \n", " \n", "\n", " elif template == \"Full_back\":\n", " columns= ['Key passes per 90 rank',\n", " 'xA rank', \n", " 'Succ Dribb rank',\n", " 'Progressive runs per 90 rank', \n", " 'Crosses rank',\n", " 'Succ Crosses rank',\n", " 'Through p90 rank',\n", " 'Passes acc. %', \n", " 'Def duels per 90 rank',\n", " 'Interceptions per 90 rank',\n", " 'Comp DC rank']\n", " \n", " \n", " elif template == \"Central_defender\":\n", " columns= ['Progressive runs per 90 rank', \n", " 'Passes acc. %',\n", " 'Through p90 rank',\n", " 'Def duels per 90 rank',\n", " 'Interceptions per 90 rank']\n", " \n", " stats = x[columns] ## Some data-wrangling to get the data in the format we want\n", "\n", " stats = stats.T\n", " stats[\"theta\"] = stats.index\n", " stats.reset_index(drop=True, inplace=True)\n", " stats.columns = [\"r\",\"theta\"]\n", " \n", " ##Plotting\n", "\n", " fig = px.line_polar(stats, r='r', theta='theta', line_close=True)\n", " fig.update_traces(fill='toself', fillcolor = \"rgba(29,130,65,0.5)\")\n", " \n", " \n", " fig.update_layout(\n", " title=go.layout.Title(\n", " text=\"Player Radar - {}\".format(player)\n", " ),\n", " polar = dict(radialaxis = dict(visible=True, range=[0, 100])), ##ensuring range is always fixed\n", " annotations=[\n", " go.layout.Annotation(\n", " x=0.95,\n", " y=-0.1,\n", " showarrow=False,\n", " text=\"All ranks in league percentile\",\n", " xref=\"paper\",\n", " yref=\"paper\",\n", " font=dict(\n", " family=\"Courier New, monospace\",\n", " size=16,\n", " color=\"#ffffff\"\n", " ),\n", " bordercolor=\"#c7c7c7\",\n", " borderwidth=2,\n", " borderpad=4,\n", " bgcolor=\"rgba(45, 197, 247,0.9)\",\n", " opacity=0.8 \n", " )] \n", " )\n", "\n", " \n", " py.offline.iplot(fig)\n", " \n", "\n", "\n", "player = widgets.Dropdown(options=sorted(list(df[\"Player\"])), description='Player List')\n", "template = widgets.Dropdown(options= [\"Striker\", \"Attacking_mid\",\"Central_mid\", \"Full_back\", \"Central_defender\"], description = \"Template\")\n", "\n", "widgets.interactive(update_plot, player=player, template = template) ##linking the widgets above to the plotting function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I used Ashwin Raman's [blogpost](https://thefutebolist.wordpress.com/2018/10/19/a-guide-to-player-comparison-bar-graphs-and-how-i-make-them/) on bar plots to come up with the different templates. That's because I wasn't sure about it myself. Feel free to change them around. After that, I transposed the dataframe and renamed the columns to ease understanding. There might be a better way - a [numpy](https://numpy.org/) way, but I'm new to Plotly and wanted to progress according to the manual. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Caveats \n", "\n", "The post would be incomplete without including this [tweet](https://twitter.com/LukeBornn/status/864856335191388162?s=20) from Luke Bonn on why you should **not** use radar charts. It talks about the importance of the ordering of the variables and that's something you should be very careful with as well. Other than that, radars have also been criticised for the non-linear relation between area of the radar and values along the axes. Check out [Cleveland's Hierarchy](https://link.springer.com/chapter/10.1007/978-3-642-14600-8_46) - a research done which attempted to learn more about the perception of the different graphical representaions amongst the general population. \n", "\n", "Nonetheless, their popularity far outweighs their criticism and radar charts might be here to stay for a while. \n", "\n", "-----\n", "\n", "Any questions or concerns, feel free to text me on [Twitter](https://twitter.com/AbhishekS9_)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.7.2" } }, "nbformat": 4, "nbformat_minor": 2 }