{"id":2574,"date":"2026-06-04T07:00:04","date_gmt":"2026-06-04T12:00:04","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=2574"},"modified":"2026-06-04T08:45:54","modified_gmt":"2026-06-04T13:45:54","slug":"nba-shot-location-heatmaps","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2026\/06\/nba-shot-location-heatmaps\/","title":{"rendered":"NBA shot location heatmaps"},"content":{"rendered":"<p>I&#8217;ll be honest, I really wanted to do a Miami Heat pun. Adebayo, Herro, and Powell are great but they just aren&#8217;t as recognizable as Luka Don\u010di\u0107 [I won&#8217;t use the fancy Cs again].<\/p>\n<p>The plan is to create a heatmap of Luka&#8217;s field goal attempts during the 2025-26 regular season. It will go beyond 2PA and 3PA stats and show exactly where on the court he took his shots. We know Luka is an excellent shooter but where does he prefer to shoot from?<\/p>\n<p>We&#8217;ll make something similar to a traditional shot chart (example below) but try to elevate it visually.<\/p>\n<figure id=\"attachment_2610\" aria-describedby=\"caption-attachment-2610\" style=\"width: 300px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/research-gate-image-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2610 size-medium\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/research-gate-image-1-300x237.png\" alt=\"\" width=\"300\" height=\"237\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/research-gate-image-1-300x237.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/research-gate-image-1-768x607.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/research-gate-image-1.png 850w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption id=\"caption-attachment-2610\" class=\"wp-caption-text\">A figure from a 2021 research paper about scoring probabilities at different areas of the court.<sup>[1]<\/sup><\/figcaption><\/figure>\n<hr \/>\n<h4>1. What&#8217;s a heatmap?<\/h4>\n<p>Just like a histogram visualizes a variable&#8217;s distribution along one dimension, a heatmap operates on two dimensions. A classic heatmap has square bins. The more points in a bin, the darker the color.<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/heatmap_demo.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2638 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/heatmap_demo.gif\" alt=\"\" width=\"600\" height=\"600\" \/><\/a><\/p>\n<p>That makes sense. We could divide the basketball court into a grid and count how many shots fall within each square.<\/p>\n<p>The main drawback is what happens at boundaries. For example, the two points near (20, 20) are right next to each other but they contribute to different bins. As far as the math is concerned they&#8217;re totally unrelated data points. A blocky, discontinuous approach like this can obscure what the data is saying.<\/p>\n<p>You might think, &#8220;Okay, so make the squares tiny.&#8221; That would definitely improve things and we&#8217;ll do that, but we can go a step further. Instead of simply grouping shots into bins and counting them, we can look at precise spots on the court and calculate a weighted total of every shot nearby. Because they&#8217;re weighted, shots closer to the spot will contribute more than shots further away.<\/p>\n<p>If you repeat that calculation many times all over the court you&#8217;ll end up smoothing out the data. This technique is called a Kernel Density Estimate (KDE). It will let us analyze the data in a smooth, continuous way and easily identify &#8220;hot spots&#8221; on the court.<\/p>\n<figure id=\"attachment_2603\" aria-describedby=\"caption-attachment-2603\" style=\"width: 900px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/kde_demo.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2603 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/kde_demo.png\" alt=\"\" width=\"900\" height=\"450\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/kde_demo.png 900w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/kde_demo-300x150.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/01\/kde_demo-768x384.png 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><figcaption id=\"caption-attachment-2603\" class=\"wp-caption-text\">A scatter plot and corresponding Kernel Density Estimate (KDE) heatmap.<\/figcaption><\/figure>\n<hr \/>\n<h4>2. Prepare the data.<\/h4>\n<p>More often than not, the best way to pull NBA stats is from <a href=\"https:\/\/www.nba.com\/stats\/leaders?SeasonType=Regular+Season&amp;Season=2024-25&amp;PerMode=Totals\" target=\"_blank\" rel=\"noopener\">nba.com<\/a>. The data on their web site is provided by an internal API that remains open to the public\u2014at least for now. <a href=\"https:\/\/pypi.org\/project\/nba_api\" target=\"_blank\" rel=\"noopener\">nba_api<\/a> is a Python client to interface with it.<\/p>\n<p>Once you&#8217;ve installed the package you can start pulling data. First you&#8217;ll need a player&#8217;s ID. You can find it in the player URL on nba.com or search for it this way.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from nba_api.stats.static import players\r\n\r\nplayer_list = players.find_players_by_full_name(\"Luka Don\u010di\u0107\")<\/pre>\n<p>We want shot chart data so we&#8217;ll use the <code>shotchartdetail<\/code> endpoint.<\/p>\n<p>Pass Luka&#8217;s <code>player_id<\/code> here. <code>team_id=0<\/code> ensures that we receive a player&#8217;s complete stats even if they&#8217;ve been traded. It&#8217;s not necessary for Luka but I usually include it anyway. The other arguments specify the stat we&#8217;re interested in (field goal attempts) and coverage (2025-26 regular season).<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from nba_api.stats.endpoints import shotchartdetail\r\n\r\nresponse = shotchartdetail.ShotChartDetail(player_id=1629029,\r\n                                           team_id=0,\r\n                                           season_nullable=\"2025-26\",\r\n                                           season_type_all_star=\"Regular Season\",\r\n                                           context_measure_simple=\"FGA\")<\/pre>\n<p>nba_api can package the data into a dictionary, a JSON object, or a pandas DataFrame. I prefer pandas.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\n\r\ndf = response.get_data_frames()[0]<\/pre>\n<p>The DataFrame includes 24 columns so I won&#8217;t try to print it here. Suffice it to say you could spend a <em>lot<\/em> of time working with stats from this one endpoint alone.<\/p>\n<p>We&#8217;re most interested in (x, y) locations of shots, which are found in the <em>LOC_X<\/em> and <em>LOC_Y<\/em> columns. Let&#8217;s narrow down the DataFrame and take a look.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">print(df[['GAME_ID', 'PLAYER_NAME', 'EVENT_TYPE', 'LOC_X', 'LOC_Y']].head())<\/pre>\n<p>The output:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">    GAME_ID  PLAYER_NAME   EVENT_TYPE  LOC_X_FT  LOC_Y_FT\r\n0  22500002  Luka Don\u010di\u0107    Made Shot      -6.0     17.75\r\n1  22500002  Luka Don\u010di\u0107  Missed Shot       1.8     31.15\r\n2  22500002  Luka Don\u010di\u0107    Made Shot      -8.8     30.05\r\n3  22500002  Luka Don\u010di\u0107  Missed Shot     -16.7     26.95\r\n4  22500002  Luka Don\u010di\u0107  Missed Shot       0.2      8.05<\/pre>\n<p>We don&#8217;t have to worry about made vs. missed shots because we&#8217;re interested in overall shot selection. At this point, if you wanted to, you could take the DataFrame and create something like the shot chart at the top of this post. But let&#8217;s keep pushing forward.<\/p>\n<p><em>LOC_X<\/em> and <em>LOC_Y<\/em> use an odd unit system. You might think those numbers are inches but they&#8217;re not. They&#8217;re actually tenths of a foot. So 157 means 15.7 feet. For some reason. Let&#8217;s go ahead and divide the values by 10 and create new columns named <em>LOC_X_FT<\/em> and <em>LOC_Y_FT<\/em>.<\/p>\n<p>The data assumes the hoop is at the top of the screen. That&#8217;s how shot charts are presented on nba.com so it makes sense. If we plot the data as-is with (0, 0) at the bottom, right-corner threes will be plotted as left-corner threes, and so on. We <em>could<\/em> transform the data now, i.e. subtract <em>LOC_Y<\/em> from 940 (a court is 94 feet long). But it will be easier to skip it and call <code>ax.invert_yaxis<\/code> later.<\/p>\n<p>Additionally, y=0 is located at the center of the hoop, which is 5&#8217;3&#8243; from the baseline, so we need to add 5.25. Our y=0 will be the baseline.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df['LOC_X_FT'] = df['LOC_X'] \/ 10\r\n\r\ndf['LOC_Y_FT'] = df['LOC_Y'] \/ 10 + 5.25<\/pre>\n<p>Now we have the necessary (x, y) shot data for a heatmap. The code seems tricky at first but it&#8217;s only a few lines. Let&#8217;s walk through what each line is doing.<\/p>\n<p><code>linspace<\/code> is simple. It returns a list of evenly spaced values between two limits. -25 to 25 corresponds to a basketball court&#8217;s 50-ft width. 0 to 47.5 is the length of a half court. All of Luka&#8217;s shot coordinates will fall within these bounds. A higher number of steps will be less &#8220;grainy,&#8221; but I prefer a little grain. 200 is a nice medium value.<\/p>\n<p>Use these lists to create a NumPy <code>meshgrid<\/code>. This method returns a pair of 2D arrays that together represent points within the grid layout. Think of them like a map overlaid on the basketball court.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import numpy as np\r\n\r\nx_grid = np.linspace(-25, 25, 200)\r\ny_grid = np.linspace(0, 47.5, 200)\r\nx_grid, y_grid = np.meshgrid(x_grid, y_grid)<\/pre>\n<p>Then create a KDE model. <code>gaussian_kde<\/code> accepts the real-world data. It returns a model that can be used to estimate values within its 2D area.<\/p>\n<p><code>np.vstack<\/code> is a bit of a curveball but don&#8217;t let it trip you up. It simply arranges the two pandas Series into a <em>vertically stacked<\/em> 2D array, which is how <code>guassian_kde<\/code> expects the data.<\/p>\n<p><code>bw_method<\/code> is an optional parameter that tweaks how the algorithm handles nearby points. A low <em>bandwidth<\/em>, like 0.04, will reveal more local maxima (hot spots) in the data. A higher bandwidth will tend to blur peaks together into a single large spot. Using a value this low is often a bad idea because it can convey false precision, but for a heatmap I think it&#8217;s justified. We aren&#8217;t trying to build a model to predict Luka&#8217;s future shots out of sample. We&#8217;re visualizing his shot selection in the 2025-26 season.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from scipy.stats import gaussian_kde\r\n\r\nkde_model = gaussian_kde(np.vstack([df['LOC_X_FT'], df['LOC_Y_FT']]), bw_method=0.04)<\/pre>\n<p>Now we have a KDE model capable of calculating values within the 2D area (the basketball court). We need to tell the model what points we&#8217;re interested in. That is, all the points contained in <code>x_grid<\/code> and <code>y_grid<\/code>.<\/p>\n<p><code>positions<\/code> is essentially an array of (x, y) ordered pairs. Once again, we vertically stack the data because that&#8217;s how the model expects it.<\/p>\n<p>Pass those coordinates into <code>kde_model<\/code> to do the calculations. It returns a 1D array so we <code>reshape<\/code> it to match our 2D arrays.<\/p>\n<p><code>z_density<\/code> is the final product. It&#8217;s the 2D array of &#8220;heat&#8221; values that we&#8217;ll plot in a moment.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">positions = np.vstack([x_grid.flatten(), y_grid.flatten()])\r\n\r\nz_density = np.reshape(kde_model(positions), x_grid.shape)<\/pre>\n<p>To recap, we&#8217;ve taken a scattershot of points (Luka&#8217;s shot data) and created a regularly spaced, fine mesh grid of <em>smoothed<\/em> points. <code>z_density<\/code> has a lot more elements than the data we pulled from the API.<\/p>\n<p>You might notice how similar this KDE code is to our <a href=\"https:\/\/wollen.org\/blog\/2025\/12\/matterhorn-3d\/\">Matterhorn&#8217;s grid interpolation<\/a> in a previous post. The scripts generate very different plots but both involve laying out a <code>meshgrid<\/code> and calculating a value at each point. Except in this case, values will be visualized with color (2D) rather than elevation (3D).<\/p>\n<hr \/>\n<h4>3. Plot the data.<\/h4>\n<p>I&#8217;ll use a custom Matplotlib style that will be linked at the bottom of this post.<\/p>\n<p>Create a Figure and an Axes instance (<code>ax<\/code>) for plotting.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\n\r\nplt.style.use(\"wollen_shooting.mplstyle\")\r\n\r\nfig, ax = plt.subplots()<\/pre>\n<p>We have a couple options for heatmap arrays in Matplotlib. In most cases, <code>pcolormesh<\/code> is the best choice. <code>imshow<\/code> can be slightly faster but it treats the grid like square pixels, so you have to think about aspect ratio. Let&#8217;s stick with the more flexible method.<\/p>\n<p>Pass in the 2D arrays we created before. <code>cmap<\/code> refers to Matplotlib&#8217;s <a href=\"https:\/\/matplotlib.org\/stable\/gallery\/color\/colormap_reference.html\" target=\"_blank\" rel=\"noopener\">colormap<\/a>. It&#8217;s the palette that will determine how hot and cold spots are presented. You can experiment with <code>cmap<\/code> and change the style dramatically. Or create your own color map.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.pcolormesh(x_grid, y_grid, z_density, cmap=\"turbo\")<\/pre>\n<p>Next, paint all the important markings on the court. It requires a bunch of lines, a few arcs, and a circle. You could certainly add more detail if you wanted.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from matplotlib.patches import Arc, Circle\r\n\r\n\r\ndef paint_court(axes):\r\n    for x_vals, y_vals in [([-25, 25], [47, 47]),\r\n                           ([-3, 3], [4, 4]),\r\n                           ([-8, -8], [0, 19]),\r\n                           ([8, 8], [0, 19]),\r\n                           ([-8, 8], [19, 19]),\r\n                           ([-22, -22], [0, 14]),\r\n                           ([22, 22], [0, 14])]:\r\n        axes.plot(x_vals, y_vals, color=\"white\", lw=2)\r\n    axes.add_artist(Circle((0, 5.25), 0.75, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 5.25), width=47.4, height=47.4, angle=0, theta1=21.8, theta2=158.2, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 5.25), width=8, height=8, angle=0, theta1=0, theta2=180, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 19), width=10, height=10, angle=0, theta1=0, theta2=180, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 47), width=12, height=12, angle=0, theta1=180, theta2=0, lw=2, ec=\"white\", fc=\"None\"))\r\n\r\n\r\npaint_court(ax)<\/pre>\n<p>Tick labels are mostly straightforward except I want to mirror y-ticks on the right-hand side of the plot. We can do this by calling <code>ax.twinx<\/code>. I think it helps to see court measurements in feet to judge how long certain shots are.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_ticks = range(-25, 30, 5)\r\nx_tick_labels = [f\"{abs(n)} ft\" if n != 0 else \"0\" for n in x_ticks]\r\n\r\ny_ticks = range(0, 50, 5)\r\ny_tick_labels = [f\"{abs(n)} ft\" if n != 0 else \"0\" for n in y_ticks]\r\n\r\nax.set(xticks=x_ticks,\r\n       xticklabels=x_tick_labels,\r\n       xlim=(-25, 25),\r\n       yticks=y_ticks,\r\n       yticklabels=y_tick_labels,\r\n       ylim=(0, 47.5))\r\n\r\nax1 = ax.twinx()\r\n\r\nax1.set(yticks=y_ticks,\r\n        yticklabels=y_tick_labels,\r\n        ylim=(0, 47.5))<\/pre>\n<p>As I mentioned earlier, data from nba.com assumes the hoop (0, 0) is at the top of the plot. That&#8217;s no problem. We can simply invert both y axes.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.invert_yaxis()\r\nax1.invert_yaxis()<\/pre>\n<p>Finally, save the figure. A high <code>dpi<\/code> makes a real difference in this case because of all the detail.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.savefig(\"luka_fga.png\", dpi=300)<\/pre>\n<hr \/>\n<h4>4. The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-scaled.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-3396 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-scaled.png\" alt=\"\" width=\"2560\" height=\"2304\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-scaled.png 2560w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-300x270.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-1024x922.png 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-768x691.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-1536x1382.png 1536w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/luka_fga-2048x1843.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/a><\/p>\n<p>Luka&#8217;s shot chart is more interesting than the Miami Heat squad anyway!<\/p>\n<p>Notice his preference for shooting threes from the left. It also stands out how much right-handed Luka prefers layups from that side of the basket. Still, the &#8220;blob&#8221; in the protected area is reasonably balanced. Some smaller players show a tendency to attack from the baseline rather than directly through the defense.<\/p>\n<hr style=\"width: 50%;\" \/>\n<p>These charts are great for side-by-side comparison. I won&#8217;t post the code but you can easily arrange several players into a subplot grid. Below are the top eight scorers of the 2025-26 season.<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-scaled.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-3398 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-scaled.png\" alt=\"\" width=\"2560\" height=\"2560\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-scaled.png 2560w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-1024x1024.png 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-150x150.png 150w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-768x768.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-1536x1536.png 1536w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-2048x2048.png 2048w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/06\/2025-26_shooting_3x3-800x800.png 800w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/a><\/p>\n<p>I&#8217;m sure these maps are something every NBA analytics team keeps an eye on.<\/p>\n<p>It&#8217;s clear that Luka takes a wider variety of shots than other top scorers. No surprise there. His versatility makes him one of the most exciting players to watch. Be aware that colors are normalized for each player. Red on one map doesn&#8217;t represent the same shot volume as red on another.<\/p>\n<p>This plot uses <code>cmap=\"hsv_r\"<\/code> just to mix things up. I think it looks sharp.<!-- HFCM by 99 Robots - Snippet # 15: endmark-python -->\n<span class=\"endmark-python\"><\/span>\n<!-- \/end HFCM by 99 Robots -->\n<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/nba_heatmaps.zip\"><strong>Download the Matplotlib style.<\/strong><\/a><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from nba_api.stats.endpoints import shotchartdetail\r\nimport pandas as pd\r\nfrom scipy.stats import gaussian_kde\r\nimport numpy as np\r\nimport matplotlib.pyplot as plt\r\nfrom matplotlib.patches import Arc, Circle\r\n\r\n\r\ndef paint_court(axes):\r\n    for x_vals, y_vals in [([-25, 25], [47, 47]),\r\n                           ([-3, 3], [4, 4]),\r\n                           ([-8, -8], [0, 19]),\r\n                           ([8, 8], [0, 19]),\r\n                           ([-8, 8], [19, 19]),\r\n                           ([-22, -22], [0, 14]),\r\n                           ([22, 22], [0, 14])]:\r\n        axes.plot(x_vals, y_vals, color=\"white\", lw=2)\r\n    axes.add_artist(Circle((0, 5.25), 0.75, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 5.25), width=47.4, height=47.4, angle=0, theta1=21.8, theta2=158.2, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 5.25), width=8, height=8, angle=0, theta1=0, theta2=180, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 19), width=10, height=10, angle=0, theta1=0, theta2=180, lw=2, ec=\"white\", fc=\"None\"))\r\n    axes.add_artist(Arc((0, 47), width=12, height=12, angle=0, theta1=180, theta2=0, lw=2, ec=\"white\", fc=\"None\"))\r\n\r\n\r\nresponse = shotchartdetail.ShotChartDetail(player_id=1629029,\r\n                                           team_id=0,\r\n                                           season_nullable=\"2025-26\",\r\n                                           season_type_all_star=\"Regular Season\",\r\n                                           context_measure_simple=\"FGA\")\r\n\r\ndf = response.get_data_frames()[0]\r\n\r\ndf['LOC_X_FT'] = df['LOC_X'] \/ 10\r\n\r\ndf['LOC_Y_FT'] = df['LOC_Y'] \/ 10 + 5.25\r\n\r\nx_grid = np.linspace(-25, 25, 200)\r\ny_grid = np.linspace(0, 47.5, 200)\r\nx_grid, y_grid = np.meshgrid(x_grid, y_grid)\r\n\r\nkde_model = gaussian_kde(np.vstack([df['LOC_X_FT'], df['LOC_Y_FT']]), bw_method=0.04)\r\n\r\npositions = np.vstack([x_grid.flatten(), y_grid.flatten()])\r\n\r\nz_density = np.reshape(kde_model(positions), x_grid.shape)\r\n\r\nplt.style.use(\"wollen_shooting.mplstyle\")\r\n\r\nfig, ax = plt.subplots()\r\n\r\nax.pcolormesh(x_grid, y_grid, z_density, cmap=\"turbo\")\r\n\r\npaint_court(ax)\r\n\r\nx_ticks = range(-25, 30, 5)\r\nx_tick_labels = [f\"{abs(n)} ft\" if n != 0 else \"0\" for n in x_ticks]\r\n\r\ny_ticks = range(0, 50, 5)\r\ny_tick_labels = [f\"{abs(n)} ft\" if n != 0 else \"0\" for n in y_ticks]\r\n\r\nax.set(xticks=x_ticks,\r\n       xticklabels=x_tick_labels,\r\n       xlim=(-25, 25),\r\n       yticks=y_ticks,\r\n       yticklabels=y_tick_labels,\r\n       ylim=(0, 47.5))\r\n\r\nax1 = ax.twinx()\r\n\r\nax1.set(yticks=y_ticks,\r\n        yticklabels=y_tick_labels,\r\n        ylim=(0, 47.5))\r\n\r\nax.invert_yaxis()\r\nax1.invert_yaxis()\r\n\r\nplt.savefig(\"luka_fga.png\", dpi=300)<\/pre>\n<hr style=\"width: 50%;\" \/>\n<p style=\"padding-left: 6%; padding-right: 12%; padding-bottom: 0px; margin-bottom: 2px;\"><strong>References<\/strong><\/p>\n<p style=\"padding-left: 9%; padding-right: 12%; padding-top: 0px; margin-top: 2px;\">1. Zuccolotto, P., Sandri, M., &amp; Manisera, M. (2021). Spatial Performance Indicators and Graphs in Basketball. Social Indicators Research, 156, 1\u201314. <a href=\"https:\/\/doi.org\/10.1007\/s11205-019-02237-2\" target=\"_blank\" rel=\"noopener\">https:\/\/doi.org\/10.1007\/s11205-019-02237-2<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ll be honest, I really wanted to do a Miami Heat pun. Adebayo, Herro, and Powell are great but they just aren&#8217;t as recognizable as Luka Don\u010di\u0107 [I won&#8217;t use the fancy Cs again]. The plan is to create a<\/p>\n","protected":false},"author":1,"featured_media":3400,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[59,469],"tags":[507,506,667,82,361,534,539,520,519,671,542,541,540,545,538,537,668,670,531,533,532,536,665,664,24,388,351,672,535,75,30,46,182,25,201,118,666,63,547,669,549,544],"class_list":["post-2574","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-sports","category-stats","tag-2d","tag-3d","tag-anthony-edwards","tag-array","tag-basketball","tag-bell-curve","tag-bw_method","tag-cmap","tag-colormap","tag-donovan-mitchell","tag-gauss","tag-gaussian","tag-gaussian_kde","tag-golden-state","tag-heat-map","tag-heatmap","tag-jaylen-brown","tag-kawhi-leonard","tag-kde","tag-kernel","tag-kernel-density-estimate","tag-line","tag-luka","tag-luka-doncic","tag-matplotlib","tag-meshgrid","tag-nba","tag-nikola-jokic","tag-normal","tag-numpy","tag-pandas","tag-plot","tag-probability","tag-python","tag-scatter","tag-scipy","tag-sga","tag-statistics","tag-stephen-curry","tag-tyrese-maxey","tag-vstack","tag-warriors"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2574","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/comments?post=2574"}],"version-history":[{"count":54,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2574\/revisions"}],"predecessor-version":[{"id":3413,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2574\/revisions\/3413"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/3400"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=2574"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=2574"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=2574"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}