{"id":2520,"date":"2026-05-07T07:00:47","date_gmt":"2026-05-07T12:00:47","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=2520"},"modified":"2026-05-06T23:22:03","modified_gmt":"2026-05-07T04:22:03","slug":"celebrating-five-years-knock-on-wood","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2026\/05\/celebrating-five-years-knock-on-wood\/","title":{"rendered":"Celebrating five years (knock on wood)"},"content":{"rendered":"<p>I try to squeeze as much customization into my plots as I can, but the Matplotlib rabbit hole goes deep. Buried in the <a href=\"https:\/\/matplotlib.org\/stable\/plot_types\/index.html\" target=\"_blank\" rel=\"noopener\">documentation<\/a> you&#8217;ll find bizarre relics seemingly from a different world.<\/p>\n<p>Need a discrete colorbar with variable-width log-scale patches and custom end caps? Matplotlib has you covered. How about a map of the night sky using the Aitoff projection? Oh, you need the Lambert projection instead? That&#8217;s fine too.<\/p>\n<p>I don&#8217;t pretend to know half of what&#8217;s there. I&#8217;m familiar enough to find what I need in the documentation when I need it, which is the important thing. <strong>And I appreciate<\/strong> that smart people are so dedicated to supporting those features. You never know what tools you&#8217;ll need tomorrow.<\/p>\n<hr \/>\n<h4>1. Paths.<\/h4>\n<p>To celebrate five years of writing blog posts (mostly for Claude at this point) let&#8217;s dig into something niche but also fun.<\/p>\n<p>Matplotlib lets you create custom markers. When calling <code>scatter()<\/code> you can pass a list of tuples that define an arbitrary polygon. For example, here&#8217;s a goofy-looking trapezoid:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.scatter(x=range(-2, 3),\r\n           y=range(-2, 3),\r\n           marker=[(-1, -1), (-1, 1), (1, 0.4), (1, -0.4), (-1, -1)],\r\n           s=300,\r\n           edgecolor=\"black\",\r\n           linewidth=1.0)<\/pre>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/trapezoid.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2691\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/trapezoid.png\" alt=\"\" width=\"500\" height=\"500\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/trapezoid.png 500w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/trapezoid-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/trapezoid-150x150.png 150w\" sizes=\"auto, (max-width: 500px) 100vw, 500px\" \/><\/a><\/p>\n<p>Or for a more scalable approach, pass a <a href=\"https:\/\/matplotlib.org\/stable\/users\/explain\/artists\/paths.html\" target=\"_blank\" rel=\"noopener\">Path<\/a> object. Think of it like the <em>path<\/em> your finger would trace around the edge of a shape. It&#8217;s very similar to an SVG file.<\/p>\n<p>A Path&#8217;s basic structure is composed of <em>vertices<\/em> and <em>codes<\/em>. Vertices are points in (x, y) space and codes tell Matplotlib how exactly it should travel between points. You could build a simple triangle like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">verts = [(0, 0), (1, 1), (2, 0), (0, 0)]\r\ncodes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]<\/pre>\n<p>In this post I want to create a Path from plain text\u2014namely the <strong>W<\/strong> logo of this web site. That seems celebratory enough.<\/p>\n<p>Unsurprisingly if you&#8217;ve read the documentation, Matplotlib has a built-in API for this. There&#8217;s no need to tediously locate edges by hand or to jerry-rig your own Python solution. The process requires three imports and three lines of code.<\/p>\n<p>First, generate a Path using the aptly named <code>TextToPath<\/code> method. It returns vertices and codes as discussed above.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from matplotlib.textpath import TextToPath\r\nfrom matplotlib.font_manager import FontProperties\r\n\r\nverts, codes = TextToPath().get_text_path(FontProperties(family=\"Federant\", style=\"normal\"), \"W\")\r\n<\/pre>\n<p>Unfortunately it returns the Path of a bottom-left-aligned character, so markers will be drawn to the right and above where we want them. I don&#8217;t think there&#8217;s a good way to override this behavior on creation. If there is, please let me know!<\/p>\n<p>Fortunately it&#8217;s easy to translate the Path leftward and downward. Under the hood, <code>verts<\/code> is just a NumPy array. Add a second NumPy array containing x and y offsets to center the marker. Matplotlib <a href=\"https:\/\/matplotlib.org\/stable\/api\/transformations.html#matplotlib.transforms.Transform\" target=\"_blank\" rel=\"noopener\">Transforms<\/a> would work as well if you&#8217;d prefer a built-in method.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">verts += np.array([-50.0, -34.0])<\/pre>\n<p>Then pass the instructions to <code>Path<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from matplotlib.path import Path\r\n\r\nmarker_path = Path(verts, codes)<\/pre>\n<p>Now we can use little <strong>W<\/strong> markers instead of circles or the other default options. If you aren&#8217;t excited yet, you&#8217;re in the wrong place!<\/p>\n<hr \/>\n<h4>2. Prepare the data.<\/h4>\n<p>To use those markers we also need data. Since the traditional five-year anniversary gift is wood, let&#8217;s go with something exciting\u2014like lumber statistics.<\/p>\n<p>I scraped info about 584 wood species from <a href=\"https:\/\/www.wood-database.com\/wood-filter\/?fwp_paged=1\" target=\"_blank\" rel=\"noopener\">wood-database.com<\/a>. The <a href=\"https:\/\/www.kaggle.com\/datasets\/cviaxmiwnptr\/wood-species-statistics\" target=\"_blank\" rel=\"noopener\">full dataset<\/a> includes 17 columns but I took the liberty of trimming it down for this post. We&#8217;ll deal with two variables: elastic modulus and hardness.<\/p>\n<p>These are material properties that, like much of the Matplotlib library, a normal person will never have to worry about. The variables happen to have a relationship that lends itself well to a scatter plot, which will allow us to show off custom markers.<\/p>\n<p>Start by reading the dataset and dropping rows with missing data. It&#8217;s a tab-separated file so pass an argument to <code>delimiter<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\n\r\ndf = pd.read_csv(\"wood_database_trimmed.tsv\", delimiter=\"\\t\")\r\n\r\ndf = df.dropna(subset=['janka_hardness', 'modulus_of_elasticity'])<\/pre>\n<p>We can jump straight to a linear regression. The details don&#8217;t really matter but conceptually, we&#8217;re modeling hardness as the independent variable and elastic modulus as dependent.<\/p>\n<ul>\n<li>If a material is hard, it means it&#8217;s tough to leave a mark on it. You have to apply more force to make a dent.<\/li>\n<li>A higher elastic modulus means the wood is stiffer. It takes more force per unit area (pressure) to bend the wood out of its natural shape. <em>Elastic<\/em> implies the strain is small enough that the wood will snap back to its original shape. That&#8217;s in contrast to plastic deformation, which is permanent.<\/li>\n<\/ul>\n<p>The plot will show how correlated the two variables are. You would assume that a hard wood is stiffer, generally speaking, but how <em>strong<\/em> is the relationship?<\/p>\n<p>Use <code>scipy.stats.linregress<\/code> to find out.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from scipy.stats import linregress\r\n\r\nslope, intercept, r_value, p_value, std_err = linregress(df['janka_hardness'], df['modulus_of_elasticity'])<\/pre>\n<p>Take the regression line&#8217;s <code>slope<\/code> and <code>intercept<\/code> and generate a list of points. We&#8217;ll layer a line on top of the scatter plot in a moment.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_reg = [df['janka_hardness'].min(), df['janka_hardness'].max()]\r\ny_reg = [n * slope + intercept for n in x_reg]<\/pre>\n<hr \/>\n<h4>3. Plot the data.<\/h4>\n<p>I&#8217;ll use a custom wollen.org-themed Matplotlib style. It will be linked at the bottom of this post.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\n\r\nplt.style.use(\"wollen_org.mplstyle\")<\/pre>\n<p>Let&#8217;s go a step further and include histograms of the x and y variables. We can arrange them along the edges of the scatter plot, like this:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/subplot_mosaic_diagram.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2695\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/subplot_mosaic_diagram.png\" alt=\"\" width=\"500\" height=\"500\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/subplot_mosaic_diagram.png 500w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/subplot_mosaic_diagram-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/subplot_mosaic_diagram-150x150.png 150w\" sizes=\"auto, (max-width: 500px) 100vw, 500px\" \/><\/a><\/p>\n<p>The x variable&#8217;s distribution will be plotted on <em>hist_top<\/em> and the y variable on <em>hist_right<\/em>.<\/p>\n<p>You <em>could<\/em> use <code>plt.subplots(2, 2)<\/code> to achieve this layout, but <code>subplot_mosaic<\/code> makes things a little easier. It allows for naming the Axes when creating them.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">fig, axs = plt.subplot_mosaic([[\"hist_top\", \".\"],\r\n                               [\"main\", \"hist_right\"]],\r\n                              width_ratios=(5, 1),\r\n                              height_ratios=(1, 5),\r\n                              gridspec_kw={\"wspace\": 0.06,\r\n                                           \"hspace\": 0.06})<\/pre>\n<p>Now we can address the &#8220;main&#8221; Axes by name. Call <code>scatter<\/code> to plot the data.<\/p>\n<p>This is where we pass the custom <strong>W<\/strong> <code>marker_path<\/code> created earlier.<\/p>\n<p>Set <code>linewidth=0<\/code> to avoid turning the markers into ugly bold Ws. Matplotlib tries to draw a line around the Path&#8217;s perimeter, but in this case, the details are too small.<\/p>\n<p>Set <code>zorder=1<\/code> so we can layer the regression line on top.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">axs[\"main\"].scatter(df['janka_hardness'],\r\n                    df['modulus_of_elasticity'],\r\n                    color=\"#9885BF\",\r\n                    marker=marker_path,\r\n                    s=100,\r\n                    linewidth=0,\r\n                    zorder=1)<\/pre>\n<p>Plot the regression line on the &#8220;main&#8221; Axes as well. Pass a higher <code>zorder<\/code> value so the line appears on top of scatter markers.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">axs[\"main\"].plot(x_reg,\r\n                 y_reg,\r\n                 color=\"#1F2123\",\r\n                 zorder=2)<\/pre>\n<p>We can combine axes ticks and labels into a single <code>set<\/code> method. Elastic modulus values tend to be very large (in the millions of psi) so let&#8217;s save space by providing formatted strings to <code>yticklabels<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_ticks = range(0, 6000, 1000)\r\nx_lim = (-100, 5100)\r\ny_ticks = np.arange(0, 5e6, 5e5)\r\ny_tick_labels = [f\"{n \/ 1e6:.1f}M\" if n &gt; 0 else \"0\" for n in y_ticks]\r\ny_lim=(0, 4.6e6)\r\n\r\naxs[\"main\"].set(xticks=x_ticks,\r\n                yticks=y_ticks,\r\n                yticklabels=y_tick_labels,\r\n                xlim=x_lim,\r\n                ylim=y_lim,\r\n                xlabel=\"Janka Hardness (lbf)\",\r\n                ylabel=\"Elastic Modulus  (lbf\/in\u00b2)\")<\/pre>\n<p>That&#8217;s it for the scatter plot. We can move to histograms. The x variable, hardness, appears on the &#8220;hist_top&#8221; Axes.<\/p>\n<p>Pandas has a convenient <code>hist<\/code> method for turning DataFrames into histograms. You just need to define <code>bin<\/code> boundaries. This list of values should include the left edge of the leftmost bin and the right edge of the rightmost bin.<\/p>\n<p><code>rwidth<\/code> refers to <em>relative<\/em> width. A value of 0.7 means bars will be 70% as wide as the bin&#8217;s data range. There will be 30% white space between them.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df.hist(column=\"janka_hardness\",\r\n        bins=range(0, 4800, 100),\r\n        rwidth=0.7,\r\n        ax=axs[\"hist_top\"])<\/pre>\n<p>It&#8217;s a matter of style but I think it looks good to remove axis labels from the histograms. They already appear on the &#8220;main&#8221; Axes so no important information is lost.<\/p>\n<p>Pandas tries to force a title by default. Let&#8217;s remove it by passing an empty string.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">axs[\"hist_top\"].set(xticks=x_ticks,\r\n                    xticklabels=[],\r\n                    xlim=x_lim,\r\n                    yticks=[],\r\n                    title=\"\")<\/pre>\n<p>Then repeat the process for the y variable on &#8220;hist_right&#8221;.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df.hist(column=\"modulus_of_elasticity\",\r\n        bins=range(0, 4700000, 100000),\r\n        rwidth=0.7,\r\n        orientation=\"horizontal\",\r\n        ax=axs[\"hist_right\"])\r\n\r\naxs[\"hist_right\"].set(xticks=[],\r\n                      yticks=y_ticks,\r\n                      yticklabels=[],\r\n                      ylim=y_lim,\r\n                      title=\"\")<\/pre>\n<p>Use <code>text<\/code> to place a title in the upper-left corner of &#8220;main&#8221;. Cite the data source in the lower-right.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">axs[\"main\"].text(x=-50,\r\n                 y=4.55e6,\r\n                 s=f\"Lumber Species\\nHardness \u2022 Stiffness\\nR\u00b2={r_value**2:.2f}\",\r\n                 size=9,\r\n                 ha=\"left\",\r\n                 va=\"top\")\r\n\r\naxs[\"main\"].text(x=4980,\r\n                 y=0.01e6,\r\n                 s=\"Data: wood-database.com\",\r\n                 size=9,\r\n                 ha=\"right\",\r\n                 va=\"bottom\")<\/pre>\n<p>Finally, save the figure. It&#8217;s a good idea to increase <code>dpi<\/code> on account of the <strong>W<\/strong> markers&#8217; fine detail.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.savefig(\"lumber_hardness_stiffness.png\", dpi=200)<\/pre>\n<hr \/>\n<h4>4. The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2700 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1.png\" alt=\"\" width=\"2000\" height=\"2000\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1.png 2000w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1-1024x1024.png 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1-150x150.png 150w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1-768x768.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1-1536x1536.png 1536w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2026\/04\/lumber_hardness_elasticity-1-800x800.png 800w\" sizes=\"auto, (max-width: 2000px) 100vw, 2000px\" \/><\/a><\/p>\n<p>Lumber hardness and stiffness have a moderately strong correlation (R\u00b2=0.55) but there&#8217;s still plenty of variability between species. That makes sense for an organic material like wood.<\/p>\n<p>It&#8217;s worth noting that the scatter&#8217;s &#8220;funnel&#8221; shape is a sign of <a href=\"https:\/\/en.wikipedia.org\/wiki\/Homoscedasticity_and_heteroscedasticity\" target=\"_blank\" rel=\"noopener\">heteroscedasticity<\/a>. Variance isn&#8217;t constant along the x-range, which is an implicit assumption when calculating R\u00b2, so the correlation is likely overstated.<\/p>\n<p>Would I present this as a serious data visualization? Of course not. The Ws look silly and have no real connection to lumber. Not to mention the questionable regression analysis. This plot is more a demonstration of Paths as markers in Matplotlib. They get the job done and hopefully open the door to more sensible applications.<\/p>\n<p>With a little work you can adapt any SVG graphic to serve as a marker, like in this recent <a href=\"https:\/\/wollen.org\/blog\/2026\/04\/easter-the-first-sunday-after\/\">Easter date visualization<\/a>.<\/p>\n<p>It&#8217;s hard to believe I&#8217;ve been writing these posts for five years. I hope I&#8217;ve mixed in a few good ones. I don&#8217;t know if I&#8217;ll still be here five years from now but I&#8217;m glad to have made it this far. The process has forced me to push my limits and continue learning. Thanks for reading.<!-- HFCM by 99 Robots - Snippet # 16: endmark-w-round -->\n<span class=\"endmark-w-round\"><\/span>\n<!-- \/end HFCM by 99 Robots -->\n<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/wood_path_2026.zip\"><strong>Download the data.<\/strong><\/a><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport numpy as np\r\nfrom scipy.stats import linregress\r\nimport matplotlib.pyplot as plt\r\nfrom matplotlib.textpath import TextToPath\r\nfrom matplotlib.font_manager import FontProperties\r\nfrom matplotlib.path import Path\r\n\r\n\r\ndf = pd.read_csv(\"wood_database_trimmed.tsv\", delimiter=\"\\t\")\r\n\r\ndf = df.dropna(subset=['janka_hardness', 'modulus_of_elasticity'])\r\n\r\nslope, intercept, r_value, p_value, std_err = linregress(df['janka_hardness'], df['modulus_of_elasticity'])\r\n\r\nx_reg = [df['janka_hardness'].min(), df['janka_hardness'].max()]\r\ny_reg = [n * slope + intercept for n in x_reg]\r\n\r\nplt.style.use(\"wollen_org.mplstyle\")\r\n\r\nfig, axs = plt.subplot_mosaic([[\"hist_top\", \".\"],\r\n                               [\"main\", \"hist_right\"]],\r\n                              width_ratios=(5, 1),\r\n                              height_ratios=(1, 5),\r\n                              gridspec_kw={\"wspace\": 0.06,\r\n                                           \"hspace\": 0.06})\r\n\r\nverts, codes = TextToPath().get_text_path(FontProperties(family=\"Federant\", style=\"normal\"), \"W\")\r\nverts += np.array([-50.0, -34.0])\r\nmarker_path = Path(verts, codes)\r\n\r\naxs[\"main\"].scatter(df['janka_hardness'],\r\n                    df['modulus_of_elasticity'],\r\n                    color=\"#9885BF\",\r\n                    marker=marker_path,\r\n                    s=100,\r\n                    linewidth=0,\r\n                    zorder=1)\r\n\r\naxs[\"main\"].plot(x_reg,\r\n                 y_reg,\r\n                 color=\"#1F2123\",\r\n                 zorder=2)\r\n\r\nx_ticks = range(0, 6000, 1000)\r\nx_lim = (-100, 5100)\r\ny_ticks = np.arange(0, 5e6, 5e5)\r\ny_tick_labels = [f\"{n \/ 1e6:.1f}M\" if n &gt; 0 else \"0\" for n in y_ticks]\r\ny_lim=(0, 4.6e6)\r\n\r\naxs[\"main\"].set(xticks=x_ticks,\r\n                yticks=y_ticks,\r\n                yticklabels=y_tick_labels,\r\n                xlim=x_lim,\r\n                ylim=y_lim,\r\n                xlabel=\"Janka Hardness (lbf)\",\r\n                ylabel=\"Elastic Modulus  (lbf\/in\u00b2)\")\r\n\r\ndf.hist(column=\"janka_hardness\",\r\n        bins=range(0, 4800, 100),\r\n        rwidth=0.7,\r\n        ax=axs[\"hist_top\"])\r\n\r\naxs[\"hist_top\"].set(xticks=x_ticks,\r\n                    xticklabels=[],\r\n                    xlim=x_lim,\r\n                    yticks=[],\r\n                    title=\"\")\r\n\r\ndf.hist(column=\"modulus_of_elasticity\",\r\n        bins=range(0, 4700000, 100000),\r\n        rwidth=0.7,\r\n        orientation=\"horizontal\",\r\n        ax=axs[\"hist_right\"])\r\n\r\naxs[\"hist_right\"].set(xticks=[],\r\n                      yticks=y_ticks,\r\n                      yticklabels=[],\r\n                      ylim=y_lim,\r\n                      title=\"\")\r\n\r\naxs[\"main\"].text(x=-50,\r\n                 y=4.55e6,\r\n                 s=f\"Lumber Species\\nHardness \u2022 Stiffness\\nR\u00b2={r_value**2:.2f}\",\r\n                 size=9,\r\n                 ha=\"left\",\r\n                 va=\"top\")\r\n\r\naxs[\"main\"].text(x=4980,\r\n                 y=0.01e6,\r\n                 s=\"Data: wood-database.com\",\r\n                 size=9,\r\n                 ha=\"right\",\r\n                 va=\"bottom\")\r\n\r\nplt.savefig(\"lumber_hardness_stiffness.png\", dpi=200)<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I try to squeeze as much customization into my plots as I can, but the Matplotlib rabbit hole goes deep. Buried in the documentation you&#8217;ll find bizarre relics seemingly from a different world. Need a discrete colorbar with variable-width log-scale<\/p>\n","protected":false},"author":1,"featured_media":2745,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[279,469],"tags":[583,135,584,566,22,122,573,570,564,633,577,576,623,624,569,123,466,578,559,568,567,24,571,126,75,30,562,46,25,117,201,202,63,116,574,565,563,625,582,575,579,580,581,572],"class_list":["post-2520","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-nature","category-stats","tag-codes","tag-csv","tag-custom","tag-custom-marker","tag-data","tag-dataset","tag-elastic","tag-elasticity","tag-fontproperties","tag-funnel","tag-gridspec_kw","tag-height_ratios","tag-heteroscedasticity","tag-heteroskedasticity","tag-janka-hardness","tag-kaggle","tag-linear-regression","tag-lumber","tag-marker","tag-material-science","tag-materials","tag-matplotlib","tag-modulus","tag-mplstyle","tag-numpy","tag-pandas","tag-path","tag-plot","tag-python","tag-regression","tag-scatter","tag-scatter-plot","tag-statistics","tag-stats","tag-subplot_mosaic","tag-svg","tag-texttopath","tag-variance","tag-verts","tag-width_ratios","tag-wood","tag-wood-database","tag-wood-database-com","tag-youngs-modulus"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2520","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=2520"}],"version-history":[{"count":32,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2520\/revisions"}],"predecessor-version":[{"id":3390,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2520\/revisions\/3390"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/2745"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=2520"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=2520"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=2520"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}