You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/09/04 16:40:49 UTC
(sedona) branch master updated: [DOCS] Add blog post: zonal statistics with RS_ZonalStatsAll (#3318)
This is an automated email from the ASF dual-hosted git repository.
jiayuasu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new 241ae748d2a [DOCS] Add blog post: zonal statistics with RS_ZonalStatsAll (#3318)
241ae748d2a is described below
commit 241ae748d2aef5ee4fccca292e831de4eaa23862
Author: Jia Yu <ji...@apache.org>
AuthorDate: Fri Sep 4 09:40:42 2026 -0700
[DOCS] Add blog post: zonal statistics with RS_ZonalStatsAll (#3318)
---
docs/blog/posts/zonal-statistics-cover.png | Bin 0 -> 2012209 bytes
docs/blog/posts/zonal-statistics-relief.svg | 231 ++++++++++++++++++++++++++++
docs/blog/posts/zonal-statistics-slope.svg | 93 +++++++++++
docs/blog/posts/zonal-statistics.md | 215 ++++++++++++++++++++++++++
4 files changed, 539 insertions(+)
diff --git a/docs/blog/posts/zonal-statistics-cover.png b/docs/blog/posts/zonal-statistics-cover.png
new file mode 100644
index 00000000000..46efc7381d1
Binary files /dev/null and b/docs/blog/posts/zonal-statistics-cover.png differ
diff --git a/docs/blog/posts/zonal-statistics-relief.svg b/docs/blog/posts/zonal-statistics-relief.svg
new file mode 100644
index 00000000000..8088afc50f7
--- /dev/null
+++ b/docs/blog/posts/zonal-statistics-relief.svg
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="960" height="930" viewBox="0 0 960 930" role="img" aria-labelledby="relief-title relief-desc">
+ <title id="relief-title">Elevation range for every Swiss canton</title>
+ <desc id="relief-desc">Each line shows the lowest and highest elevation in one Swiss canton. The red dot shows the mean. The difference is 4,166 metres in Valais and 197 metres in Genève.</desc>
+ <style>
+ text { font-family: Inter, "Helvetica Neue", Arial, sans-serif; fill: #1c1c1c; }
+ .kicker { fill: #ca463a; font-size: 12px; font-weight: 700; letter-spacing: 1.8px; }
+ .title { font-size: 29px; font-weight: 700; letter-spacing: -0.45px; }
+ .subtitle { fill: #59616d; font-size: 15px; }
+ .grid, .tick line { stroke: #e5e9ee; stroke-width: 1; shape-rendering: crispEdges; }
+ .tick text { fill: #59616d; font-size: 13px; }
+ .axis-label { fill: #59616d; font-size: 15px; font-weight: 500; }
+ .row-label { fill: #59616d; font-size: 14px; font-weight: 500; }
+ .row-label.emphasis { fill: #1c1c1c; font-weight: 700; }
+ .range { stroke: #aab3bd; stroke-width: 3; stroke-linecap: butt; }
+ .endpoint { stroke: #1c1c1c; stroke-width: 1.75; stroke-linecap: butt; }
+ .mean { fill: #ca463a; stroke: #ffffff; stroke-width: 1.5; }
+ .leader { fill: none; stroke: #59616d; stroke-width: 1.25; stroke-linecap: butt; }
+ .callout-name { fill: #ca463a; font-size: 11px; font-weight: 800; letter-spacing: 1.2px; }
+ .callout-value { fill: #1c1c1c; font-size: 15px; font-weight: 700; }
+ </style>
+ <rect width="960" height="930" fill="#ffffff"/>
+ <text class="kicker" x="44" y="26">COPERNICUS 90 M ELEVATION</text>
+ <text class="title" x="44" y="58">Elevation range for every Swiss canton</text>
+ <text class="subtitle" x="44" y="84">Lowest, mean, and highest elevation from RS_ZonalStatsAll</text>
+ <text class="subtitle" x="920" y="84" text-anchor="end">line = lowest to highest · <tspan fill="#ca463a" font-weight="700">red dot = mean</tspan></text>
+ <g aria-hidden="true">
+ <g class="tick"><line x1="220" y1="118" x2="220" y2="850"/><text x="220" y="876" text-anchor="middle">0</text></g>
+ <g class="tick"><line x1="360" y1="118" x2="360" y2="850"/><text x="360" y="876" text-anchor="middle">1,000</text></g>
+ <g class="tick"><line x1="500" y1="118" x2="500" y2="850"/><text x="500" y="876" text-anchor="middle">2,000</text></g>
+ <g class="tick"><line x1="640" y1="118" x2="640" y2="850"/><text x="640" y="876" text-anchor="middle">3,000</text></g>
+ <g class="tick"><line x1="780" y1="118" x2="780" y2="850"/><text x="780" y="876" text-anchor="middle">4,000</text></g>
+ <g class="tick"><line x1="920" y1="118" x2="920" y2="850"/><text x="920" y="876" text-anchor="middle">5,000</text></g>
+ <line class="grid" x1="220" y1="850" x2="920" y2="850"/>
+ </g>
+ <g>
+ <g class="canton" data-name="Valais" data-min="368" data-mean="2138" data-max="4534">
+ <text class="row-label emphasis" x="204" y="155" text-anchor="end">Valais</text>
+ <line class="range" x1="271.52" y1="150" x2="854.76" y2="150"/>
+ <line class="endpoint" x1="271.52" y1="145" x2="271.52" y2="155"/>
+ <line class="endpoint" x1="854.76" y1="145" x2="854.76" y2="155"/>
+ <circle class="mean" cx="519.32" cy="150" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Graubünden" data-min="256" data-mean="2023" data-max="3961">
+ <text class="row-label" x="204" y="182" text-anchor="end">Graubünden</text>
+ <line class="range" x1="255.84" y1="177" x2="774.54" y2="177"/>
+ <line class="endpoint" x1="255.84" y1="172" x2="255.84" y2="182"/>
+ <line class="endpoint" x1="774.54" y1="172" x2="774.54" y2="182"/>
+ <circle class="mean" cx="503.22" cy="177" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Uri" data-min="433" data-mean="1901" data-max="3579">
+ <text class="row-label" x="204" y="209" text-anchor="end">Uri</text>
+ <line class="range" x1="280.62" y1="204" x2="721.06" y2="204"/>
+ <line class="endpoint" x1="280.62" y1="199" x2="280.62" y2="209"/>
+ <line class="endpoint" x1="721.06" y1="199" x2="721.06" y2="209"/>
+ <circle class="mean" cx="486.14" cy="204" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Glarus" data-min="411" data-mean="1584" data-max="3597">
+ <text class="row-label" x="204" y="236" text-anchor="end">Glarus</text>
+ <line class="range" x1="277.54" y1="231" x2="723.58" y2="231"/>
+ <line class="endpoint" x1="277.54" y1="226" x2="277.54" y2="236"/>
+ <line class="endpoint" x1="723.58" y1="226" x2="723.58" y2="236"/>
+ <circle class="mean" cx="441.76" cy="231" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Ticino" data-min="193" data-mean="1400" data-max="3360">
+ <text class="row-label" x="204" y="263" text-anchor="end">Ticino</text>
+ <line class="range" x1="247.02" y1="258" x2="690.4" y2="258"/>
+ <line class="endpoint" x1="247.02" y1="253" x2="247.02" y2="263"/>
+ <line class="endpoint" x1="690.4" y1="253" x2="690.4" y2="263"/>
+ <circle class="mean" cx="416" cy="258" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Obwalden" data-min="433" data-mean="1342" data-max="3172">
+ <text class="row-label" x="204" y="290" text-anchor="end">Obwalden</text>
+ <line class="range" x1="280.62" y1="285" x2="664.08" y2="285"/>
+ <line class="endpoint" x1="280.62" y1="280" x2="280.62" y2="290"/>
+ <line class="endpoint" x1="664.08" y1="280" x2="664.08" y2="290"/>
+ <circle class="mean" cx="407.88" cy="285" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Bern" data-min="402" data-mean="1202" data-max="4082">
+ <text class="row-label" x="204" y="317" text-anchor="end">Bern</text>
+ <line class="range" x1="276.28" y1="312" x2="791.48" y2="312"/>
+ <line class="endpoint" x1="276.28" y1="307" x2="276.28" y2="317"/>
+ <line class="endpoint" x1="791.48" y1="307" x2="791.48" y2="317"/>
+ <circle class="mean" cx="388.28" cy="312" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Appenzell Innerrhoden" data-min="571" data-mean="1133" data-max="2431">
+ <text class="row-label" x="204" y="344" text-anchor="end">Appenzell Innerrhoden</text>
+ <line class="range" x1="299.94" y1="339" x2="560.34" y2="339"/>
+ <line class="endpoint" x1="299.94" y1="334" x2="299.94" y2="344"/>
+ <line class="endpoint" x1="560.34" y1="334" x2="560.34" y2="344"/>
+ <circle class="mean" cx="378.62" cy="339" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Schwyz" data-min="405" data-mean="1100" data-max="2752">
+ <text class="row-label" x="204" y="371" text-anchor="end">Schwyz</text>
+ <line class="range" x1="276.7" y1="366" x2="605.28" y2="366"/>
+ <line class="endpoint" x1="276.7" y1="361" x2="276.7" y2="371"/>
+ <line class="endpoint" x1="605.28" y1="361" x2="605.28" y2="371"/>
+ <circle class="mean" cx="374" cy="366" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Nidwalden" data-min="433" data-mean="1076" data-max="2836">
+ <text class="row-label" x="204" y="398" text-anchor="end">Nidwalden</text>
+ <line class="range" x1="280.62" y1="393" x2="617.04" y2="393"/>
+ <line class="endpoint" x1="280.62" y1="388" x2="280.62" y2="398"/>
+ <line class="endpoint" x1="617.04" y1="388" x2="617.04" y2="398"/>
+ <circle class="mean" cx="370.64" cy="393" r="4.5"/>
+ </g>
+ <g class="canton" data-name="St. Gallen" data-min="394" data-mean="986" data-max="3177">
+ <text class="row-label" x="204" y="425" text-anchor="end">St. Gallen</text>
+ <line class="range" x1="275.16" y1="420" x2="664.78" y2="420"/>
+ <line class="endpoint" x1="275.16" y1="415" x2="275.16" y2="425"/>
+ <line class="endpoint" x1="664.78" y1="415" x2="664.78" y2="425"/>
+ <circle class="mean" cx="358.04" cy="420" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Appenzell Ausserrhoden" data-min="436" data-mean="935" data-max="2395">
+ <text class="row-label" x="204" y="452" text-anchor="end">Appenzell Ausserrhoden</text>
+ <line class="range" x1="281.04" y1="447" x2="555.3" y2="447"/>
+ <line class="endpoint" x1="281.04" y1="442" x2="281.04" y2="452"/>
+ <line class="endpoint" x1="555.3" y1="442" x2="555.3" y2="452"/>
+ <circle class="mean" cx="350.9" cy="447" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Neuchâtel" data-min="425" data-mean="918" data-max="1534">
+ <text class="row-label" x="204" y="479" text-anchor="end">Neuchâtel</text>
+ <line class="range" x1="279.5" y1="474" x2="434.76" y2="474"/>
+ <line class="endpoint" x1="279.5" y1="469" x2="279.5" y2="479"/>
+ <line class="endpoint" x1="434.76" y1="469" x2="434.76" y2="479"/>
+ <circle class="mean" cx="348.52" cy="474" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Fribourg" data-min="429" data-mean="859" data-max="2331">
+ <text class="row-label" x="204" y="506" text-anchor="end">Fribourg</text>
+ <line class="range" x1="280.06" y1="501" x2="546.34" y2="501"/>
+ <line class="endpoint" x1="280.06" y1="496" x2="280.06" y2="506"/>
+ <line class="endpoint" x1="546.34" y1="496" x2="546.34" y2="506"/>
+ <circle class="mean" cx="340.26" cy="501" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Vaud" data-min="368" data-mean="845" data-max="3139">
+ <text class="row-label" x="204" y="533" text-anchor="end">Vaud</text>
+ <line class="range" x1="271.52" y1="528" x2="659.46" y2="528"/>
+ <line class="endpoint" x1="271.52" y1="523" x2="271.52" y2="533"/>
+ <line class="endpoint" x1="659.46" y1="523" x2="659.46" y2="533"/>
+ <circle class="mean" cx="338.3" cy="528" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Luzern" data-min="403" data-mean="774" data-max="2300">
+ <text class="row-label" x="204" y="560" text-anchor="end">Luzern</text>
+ <line class="range" x1="276.42" y1="555" x2="542" y2="555"/>
+ <line class="endpoint" x1="276.42" y1="550" x2="276.42" y2="560"/>
+ <line class="endpoint" x1="542" y1="550" x2="542" y2="560"/>
+ <circle class="mean" cx="328.36" cy="555" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Jura" data-min="365" data-mean="697" data-max="1289">
+ <text class="row-label" x="204" y="587" text-anchor="end">Jura</text>
+ <line class="range" x1="271.1" y1="582" x2="400.46" y2="582"/>
+ <line class="endpoint" x1="271.1" y1="577" x2="271.1" y2="587"/>
+ <line class="endpoint" x1="400.46" y1="577" x2="400.46" y2="587"/>
+ <circle class="mean" cx="317.58" cy="582" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Zug" data-min="387" data-mean="669" data-max="1559">
+ <text class="row-label" x="204" y="614" text-anchor="end">Zug</text>
+ <line class="range" x1="274.18" y1="609" x2="438.26" y2="609"/>
+ <line class="endpoint" x1="274.18" y1="604" x2="274.18" y2="614"/>
+ <line class="endpoint" x1="438.26" y1="604" x2="438.26" y2="614"/>
+ <circle class="mean" cx="313.66" cy="609" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Solothurn" data-min="286" data-mean="623" data-max="1430">
+ <text class="row-label" x="204" y="641" text-anchor="end">Solothurn</text>
+ <line class="range" x1="260.04" y1="636" x2="420.2" y2="636"/>
+ <line class="endpoint" x1="260.04" y1="631" x2="260.04" y2="641"/>
+ <line class="endpoint" x1="420.2" y1="631" x2="420.2" y2="641"/>
+ <circle class="mean" cx="307.22" cy="636" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Schaffhausen" data-min="343" data-mean="546" data-max="931">
+ <text class="row-label" x="204" y="668" text-anchor="end">Schaffhausen</text>
+ <line class="range" x1="268.02" y1="663" x2="350.34" y2="663"/>
+ <line class="endpoint" x1="268.02" y1="658" x2="268.02" y2="668"/>
+ <line class="endpoint" x1="350.34" y1="658" x2="350.34" y2="668"/>
+ <circle class="mean" cx="296.44" cy="663" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Zürich" data-min="332" data-mean="539" data-max="1275">
+ <text class="row-label" x="204" y="695" text-anchor="end">Zürich</text>
+ <line class="range" x1="266.48" y1="690" x2="398.5" y2="690"/>
+ <line class="endpoint" x1="266.48" y1="685" x2="266.48" y2="695"/>
+ <line class="endpoint" x1="398.5" y1="685" x2="398.5" y2="695"/>
+ <circle class="mean" cx="295.46" cy="690" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Basel-Landschaft" data-min="249" data-mean="523" data-max="1166">
+ <text class="row-label" x="204" y="722" text-anchor="end">Basel-Landschaft</text>
+ <line class="range" x1="254.86" y1="717" x2="383.24" y2="717"/>
+ <line class="endpoint" x1="254.86" y1="712" x2="254.86" y2="722"/>
+ <line class="endpoint" x1="383.24" y1="712" x2="383.24" y2="722"/>
+ <circle class="mean" cx="293.22" cy="717" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Thurgau" data-min="368" data-mean="499" data-max="992">
+ <text class="row-label" x="204" y="749" text-anchor="end">Thurgau</text>
+ <line class="range" x1="271.52" y1="744" x2="358.88" y2="744"/>
+ <line class="endpoint" x1="271.52" y1="739" x2="271.52" y2="749"/>
+ <line class="endpoint" x1="358.88" y1="739" x2="358.88" y2="749"/>
+ <circle class="mean" cx="289.86" cy="744" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Aargau" data-min="260" data-mean="480" data-max="914">
+ <text class="row-label" x="204" y="776" text-anchor="end">Aargau</text>
+ <line class="range" x1="256.4" y1="771" x2="347.96" y2="771"/>
+ <line class="endpoint" x1="256.4" y1="766" x2="256.4" y2="776"/>
+ <line class="endpoint" x1="347.96" y1="766" x2="347.96" y2="776"/>
+ <circle class="mean" cx="287.2" cy="771" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Genève" data-min="332" data-mean="420" data-max="529">
+ <text class="row-label emphasis" x="204" y="803" text-anchor="end">Genève</text>
+ <line class="range" x1="266.48" y1="798" x2="294.06" y2="798"/>
+ <line class="endpoint" x1="266.48" y1="793" x2="266.48" y2="803"/>
+ <line class="endpoint" x1="294.06" y1="793" x2="294.06" y2="803"/>
+ <circle class="mean" cx="278.8" cy="798" r="4.5"/>
+ </g>
+ <g class="canton" data-name="Basel-Stadt" data-min="245" data-mean="299" data-max="520">
+ <text class="row-label" x="204" y="830" text-anchor="end">Basel-Stadt</text>
+ <line class="range" x1="254.3" y1="825" x2="292.8" y2="825"/>
+ <line class="endpoint" x1="254.3" y1="820" x2="254.3" y2="830"/>
+ <line class="endpoint" x1="292.8" y1="820" x2="292.8" y2="830"/>
+ <circle class="mean" cx="261.86" cy="825" r="4.5"/>
+ </g>
+ </g>
+ <g aria-label="Valais has 4,166 metres of relief">
+ <path class="leader" d="M 854.76 143 L 864.76 120 L 900 120"/>
+ <text class="callout-name" x="900" y="105" text-anchor="end">VALAIS</text>
+ <text class="callout-value" x="900" y="121" text-anchor="end">4,166 m difference</text>
+ </g>
+ <g aria-label="Genève has 197 metres of relief">
+ <path class="leader" d="M 299.06 798 L 338 798"/>
+ <text class="callout-name" x="348" y="793">GENÈVE</text>
+ <text class="callout-value" x="348" y="810">197 m difference</text>
+ </g>
+ <text class="axis-label" x="570" y="908" text-anchor="middle">Elevation in metres</text>
+</svg>
diff --git a/docs/blog/posts/zonal-statistics-slope.svg b/docs/blog/posts/zonal-statistics-slope.svg
new file mode 100644
index 00000000000..37293537b44
--- /dev/null
+++ b/docs/blog/posts/zonal-statistics-slope.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="960" height="650" viewBox="0 0 960 650" role="img" aria-labelledby="slope-title slope-desc">
+ <title id="slope-title">The highest canton is not the steepest</title>
+ <desc id="slope-desc">Each dot represents one Swiss canton. The horizontal axis shows mean elevation. The vertical axis shows mean slope. Uri has the highest mean slope at 28.3 degrees. Valais has the highest mean elevation at 2,138 metres.</desc>
+ <style>
+ text { font-family: Inter, "Helvetica Neue", Arial, sans-serif; fill: #1c1c1c; }
+ .kicker { fill: #ca463a; font-size: 12px; font-weight: 700; letter-spacing: 1.8px; }
+ .title { font-size: 29px; font-weight: 700; letter-spacing: -0.45px; }
+ .subtitle { fill: #59616d; font-size: 15px; }
+ .grid, .tick line { stroke: #e5e9ee; stroke-width: 1; shape-rendering: crispEdges; }
+ .tick text, .axis-label { fill: #59616d; font-size: 13px; }
+ .axis-label { font-size: 15px; font-weight: 500; }
+ .point { fill: #91a5bd; stroke: #ffffff; stroke-width: 1.5; }
+ .point.uri { fill: #ca463a; }
+ .point.valais { fill: #406b9d; }
+ .guide-uri { stroke: #ca463a; }
+ .guide-valais { stroke: #406b9d; }
+ .guide-uri, .guide-valais { fill: none; stroke-width: 1.25; stroke-dasharray: 4 5; opacity: 0.28; stroke-linecap: butt; }
+ .label-leader { stroke: #87919d; stroke-width: 1; stroke-linecap: butt; }
+ .support-label { fill: #59616d; font-size: 13px; font-weight: 500; }
+ .callout-name { font-size: 11px; font-weight: 800; letter-spacing: 1.2px; }
+ .callout-value { fill: #1c1c1c; font-size: 16px; font-weight: 700; }
+ .leader { fill: none; stroke-width: 1.3; stroke-linecap: butt; }
+ </style>
+ <rect width="960" height="650" fill="#ffffff"/>
+ <text class="kicker" x="60" y="26">MEAN ELEVATION AND SLOPE</text>
+ <text class="title" x="60" y="58">The highest canton is not the steepest</text>
+ <text class="subtitle" x="60" y="84">The same 62 tile and canton pairs show elevation and slope</text>
+ <g aria-hidden="true">
+ <g class="tick"><line x1="262.96" y1="116" x2="262.96" y2="566"/><text x="262.96" y="590" text-anchor="middle">500</text></g>
+ <g class="tick"><line x1="439.91" y1="116" x2="439.91" y2="566"/><text x="439.91" y="590" text-anchor="middle">1,000</text></g>
+ <g class="tick"><line x1="616.87" y1="116" x2="616.87" y2="566"/><text x="616.87" y="590" text-anchor="middle">1,500</text></g>
+ <g class="tick"><line x1="793.83" y1="116" x2="793.83" y2="566"/><text x="793.83" y="590" text-anchor="middle">2,000</text></g>
+ <g class="tick"><line x1="86" y1="566" x2="900" y2="566"/><text x="70" y="570" text-anchor="end">0</text></g>
+ <g class="tick"><line x1="86" y1="495.69" x2="900" y2="495.69"/><text x="70" y="499.69" text-anchor="end">5</text></g>
+ <g class="tick"><line x1="86" y1="425.38" x2="900" y2="425.38"/><text x="70" y="429.38" text-anchor="end">10</text></g>
+ <g class="tick"><line x1="86" y1="355.06" x2="900" y2="355.06"/><text x="70" y="359.06" text-anchor="end">15</text></g>
+ <g class="tick"><line x1="86" y1="284.75" x2="900" y2="284.75"/><text x="70" y="288.75" text-anchor="end">20</text></g>
+ <g class="tick"><line x1="86" y1="214.44" x2="900" y2="214.44"/><text x="70" y="218.44" text-anchor="end">25</text></g>
+ <g class="tick"><line x1="86" y1="144.13" x2="900" y2="144.13"/><text x="70" y="148.13" text-anchor="end">30</text></g>
+ </g>
+ <g aria-hidden="true">
+ <path class="guide-uri" d="M 86 168.03 H 758.79 V 566"/>
+ <path class="guide-valais" d="M 86 198.97 H 842.67 V 566"/>
+ </g>
+ <g>
+ <g class="canton-point" data-name="Basel-Stadt" data-elevation="299" data-slope="3.6"><circle class="point" cx="191.82" cy="515.37" r="5.2"/></g>
+ <g class="canton-point" data-name="Genève" data-elevation="420" data-slope="2.6"><circle class="point" cx="234.64" cy="529.44" r="5.2"/></g>
+ <g class="canton-point" data-name="Aargau" data-elevation="480" data-slope="7.8"><circle class="point" cx="255.88" cy="456.31" r="5.2"/></g>
+ <g class="canton-point" data-name="Thurgau" data-elevation="499" data-slope="4.4"><circle class="point" cx="262.6" cy="504.12" r="5.2"/></g>
+ <g class="canton-point" data-name="Basel-Landschaft" data-elevation="523" data-slope="11.4"><circle class="point" cx="271.1" cy="405.69" r="5.2"/></g>
+ <g class="canton-point" data-name="Zürich" data-elevation="539" data-slope="6.9"><circle class="point" cx="276.76" cy="468.97" r="5.2"/></g>
+ <g class="canton-point" data-name="Schaffhausen" data-elevation="546" data-slope="8.8"><circle class="point" cx="279.24" cy="442.25" r="5.2"/></g>
+ <g class="canton-point" data-name="Solothurn" data-elevation="623" data-slope="11"><circle class="point" cx="306.49" cy="411.31" r="5.2"/></g>
+ <g class="canton-point" data-name="Zug" data-elevation="669" data-slope="8.2"><circle class="point" cx="322.77" cy="450.69" r="5.2"/></g>
+ <g class="canton-point" data-name="Jura" data-elevation="697" data-slope="11.2"><circle class="point" cx="332.68" cy="408.5" r="5.2"/></g>
+ <g class="canton-point" data-name="Luzern" data-elevation="774" data-slope="10.9"><circle class="point" cx="359.93" cy="412.72" r="5.2"/></g>
+ <g class="canton-point" data-name="Vaud" data-elevation="845" data-slope="9.5"><circle class="point" cx="385.06" cy="432.41" r="5.2"/></g>
+ <g class="canton-point" data-name="Fribourg" data-elevation="859" data-slope="10.3"><circle class="point" cx="390.01" cy="421.16" r="5.2"/></g>
+ <g class="canton-point" data-name="Neuchâtel" data-elevation="918" data-slope="9.5"><circle class="point" cx="410.89" cy="432.41" r="5.2"/></g>
+ <g class="canton-point" data-name="Appenzell Ausserrhoden" data-elevation="935" data-slope="12.8"><circle class="point" cx="416.91" cy="386" r="5.2"/></g>
+ <g class="canton-point" data-name="St. Gallen" data-elevation="986" data-slope="15.5"><circle class="point" cx="434.96" cy="348.03" r="5.2"/></g>
+ <g class="canton-point" data-name="Nidwalden" data-elevation="1076" data-slope="21"><circle class="point" cx="466.81" cy="270.69" r="5.2"/></g>
+ <g class="canton-point" data-name="Schwyz" data-elevation="1100" data-slope="17.2"><circle class="point" cx="475.3" cy="324.12" r="5.2"/></g>
+ <g class="canton-point" data-name="Appenzell Innerrhoden" data-elevation="1133" data-slope="17.7"><circle class="point" cx="486.98" cy="317.09" r="5.2"/></g>
+ <g class="canton-point" data-name="Bern" data-elevation="1202" data-slope="16.9"><circle class="point" cx="511.4" cy="328.34" r="5.2"/></g>
+ <g class="canton-point" data-name="Obwalden" data-elevation="1342" data-slope="21.4"><circle class="point" cx="560.95" cy="265.06" r="5.2"/></g>
+ <g class="canton-point" data-name="Ticino" data-elevation="1400" data-slope="27.1"><circle class="point" cx="581.48" cy="184.91" r="5.2"/></g>
+ <g class="canton-point" data-name="Glarus" data-elevation="1584" data-slope="27.6"><circle class="point" cx="646.6" cy="177.87" r="5.2"/></g>
+ <g class="canton-point" data-name="Graubünden" data-elevation="2023" data-slope="25.3"><circle class="point" cx="801.97" cy="210.22" r="5.2"/></g>
+ <g class="canton-point" data-name="Uri" data-elevation="1901" data-slope="28.3"><circle class="point uri" cx="758.79" cy="168.03" r="7"/></g>
+ <g class="canton-point" data-name="Valais" data-elevation="2138" data-slope="26.1"><circle class="point valais" cx="842.67" cy="198.97" r="7"/></g>
+ </g>
+ <g>
+ <g aria-label="Basel-Stadt"><line class="label-leader" x1="191.82" y1="515.37" x2="91" y2="489"/><text class="support-label" x="98" y="493" text-anchor="start">Basel-Stadt</text></g>
+ <g aria-label="Genève"><line class="label-leader" x1="234.64" y1="529.44" x2="233" y2="542"/><text class="support-label" x="240" y="546" text-anchor="start">Genève</text></g>
+ <g aria-label="Ticino"><line class="label-leader" x1="581.48" y1="184.91" x2="523" y2="174"/><text class="support-label" x="516" y="178" text-anchor="end">Ticino</text></g>
+ <g aria-label="Glarus"><line class="label-leader" x1="646.6" y1="177.87" x2="632" y2="141"/><text class="support-label" x="625" y="145" text-anchor="end">Glarus</text></g>
+ <g aria-label="Graubünden"><line class="label-leader" x1="801.97" y1="210.22" x2="813" y2="231"/><text class="support-label" x="806" y="235" text-anchor="end">Graubünden</text></g>
+ </g>
+ <g aria-label="Uri has the highest mean slope at 28.3 degrees">
+ <path class="leader guide-uri" d="M 753.79 163.03 L 712 126" opacity="1"/>
+ <text class="callout-name" fill="#ca463a" x="702" y="111" text-anchor="end">URI</text>
+ <text class="callout-value" x="702" y="129" text-anchor="end">highest mean slope: 28.3°</text>
+ </g>
+ <g aria-label="Valais has the highest mean elevation at 2,138 metres">
+ <path class="leader guide-valais" d="M 847.67 203.97 L 890 226" opacity="1"/>
+ <text class="callout-name" fill="#406b9d" x="890" y="244" text-anchor="end">VALAIS</text>
+ <text class="callout-value" x="890" y="262" text-anchor="end">highest mean elevation: 2,138 m</text>
+ </g>
+ <text class="axis-label" x="493" y="626" text-anchor="middle">Mean elevation in metres</text>
+ <text class="axis-label" x="22" y="341" text-anchor="middle" transform="rotate(-90 22 341)">Mean slope in degrees</text>
+</svg>
diff --git a/docs/blog/posts/zonal-statistics.md b/docs/blog/posts/zonal-statistics.md
new file mode 100644
index 00000000000..c63fbe4cc34
--- /dev/null
+++ b/docs/blog/posts/zonal-statistics.md
@@ -0,0 +1,215 @@
+---
+date:
+ created: 2026-09-04
+links:
+ - RS_ZonalStatsAll reference: https://sedona.apache.org/latest/api/sql/Raster-Band-Accessors/RS_ZonalStatsAll/
+ - Raster tutorial: https://sedona.apache.org/latest/tutorial/raster/
+ - Copernicus DEM on AWS: https://registry.opendata.aws/copernicus-dem/
+authors:
+ - jia
+title: "GROUP BY, But for Pixels"
+---
+
+# GROUP BY, But for Pixels
+
+Many raster analyses need one result for each polygon. Examples include mean elevation by canton, forest cover by district, and flood depth by parcel. A raster stores values in a grid of pixels. A polygon defines an area, also called a zone.
+
+Zonal statistics summarize the raster pixels inside each zone. A spatial join matches data by location. Apache Sedona performs both steps in one query. This example reads 7,006,027 elevation pixels from Amazon S3 and summarizes all 26 Swiss cantons in 56 seconds.
+
+
+
+<!-- more -->
+
+## One call, nine numbers
+
+`RS_ZonalStatsAll(raster, zone)` calculates nine statistics for the raster pixels inside a geometry. It returns a struct with these named fields: `count`, `sum`, `mean`, `median`, `mode`, `stddev`, `variance`, `min`, and `max`. A struct is one value that contains several named fields. The mean is the arithmetic average. Use `RS_ZonalStats(raster, zone, 'mean')` when one statistic is enough.
+
+By default, Sedona includes a pixel when its center is inside the zone. This rule works well for large zones. For a zone that is only a few pixels wide, pass `allTouched = true` to include every pixel that the geometry touches.
+
+`RS_ZonalStatsAll` excludes NODATA pixels by default. NODATA marks a pixel that has no valid measurement. If a zone contains NODATA pixels, its mean uses the remaining valid pixels.
+
+Sedona also checks the coordinate reference system, or CRS. A CRS defines how coordinates map to locations on Earth. If the zone and raster use different systems, Sedona converts the zone to the raster's CRS before it calculates the statistics.
+
+## Switzerland in one query
+
+The elevation data comes from the [Copernicus 90 m DEM](https://registry.opendata.aws/copernicus-dem/). A digital elevation model, or DEM, stores ground height in a raster. This public dataset stores one GeoTIFF raster file for each 1° tile. Eighteen files cover Switzerland.
+
+The tiled `raster` reader from [our post about the 2 GB limit](https://sedona.apache.org/latest/blog/2026/07/10/open-huge-geotiffs-without-the-2-gb-wall/) reads the files from S3. The option `retile = false` keeps each file in one row:
+
+```python
+dem = (
+ sedona.read.format("raster")
+ .option("retile", "false")
+ .load(
+ "s3a://copernicus-dem-90m/"
+ "Copernicus_DSM_COG_30_N4{5,6,7}_00_E0{05,06,07,08,09,10}_00_DEM/*_DEM.tif"
+ )
+)
+dem.createOrReplaceTempView("dem")
+```
+
+Each row contains 1,200 × 1,200 pixels. The data uses EPSG:4326, a geographic CRS, with a pixel size of 3 arc-seconds. The 26 Swiss cantons are the zones. Sedona reads their polygons from the Overture divisions dataset. The files use GeoParquet, a format for geographic data.
+
+??? example "Session setup and canton data"
+
+ ```python
+ from sedona.spark import SedonaContext
+
+ config = (
+ SedonaContext.builder()
+ .master("local[4]")
+ .config("spark.driver.memory", "10g")
+ .config(
+ "spark.jars.packages",
+ "org.apache.sedona:sedona-spark-shaded-3.5_2.12:1.9.1,"
+ "org.datasyslab:geotools-wrapper:1.9.1-33.5,"
+ "org.apache.hadoop:hadoop-aws:3.3.4",
+ )
+ .config(
+ "spark.hadoop.fs.s3a.aws.credentials.provider",
+ "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider",
+ )
+ .config(
+ "spark.hadoop.fs.s3a.bucket.copernicus-dem-90m.endpoint.region", "eu-central-1"
+ )
+ .getOrCreate()
+ )
+ sedona = SedonaContext.create(config)
+
+ cantons = (
+ sedona.read.format("geoparquet")
+ .load(
+ "s3a://overturemaps-us-west-2/release/2026-08-19.0/"
+ "theme=divisions/type=division_area/"
+ )
+ .where("bbox.xmin BETWEEN 5.5 AND 10.7 AND bbox.ymin BETWEEN 45.7 AND 47.9")
+ .where("country = 'CH' AND subtype = 'region'")
+ .selectExpr("names.primary AS name", "geometry")
+ )
+ cantons.createOrReplaceTempView("cantons")
+ ```
+
+This SQL query joins the raster tiles to the cantons and calculates the results:
+
+```sql
+WITH parts AS (
+ SELECT c.name, RS_ZonalStatsAll(d.rast, c.geometry) AS s
+ FROM dem d JOIN cantons c ON RS_Intersects(d.rast, c.geometry)
+)
+SELECT name,
+ ROUND(SUM(s.sum) / SUM(s.count)) AS mean_m,
+ ROUND(MIN(s.min)) AS min_m,
+ ROUND(MAX(s.max)) AS max_m
+FROM parts
+WHERE s.count > 0
+GROUP BY name
+ORDER BY mean_m DESC
+```
+
+The query returns 26 rows and summarizes 7,006,027 pixels. The first run has no cached data and takes 56 seconds. Cantons in the Alps appear at the top of the table:
+
+```
++----------------------------+---------+------+-----+------+
+|name |px |mean_m|min_m|max_m |
++----------------------------+---------+------+-----+------+
+|Valais/Wallis |876887.0 |2138.0|368.0|4534.0|
+|Graubünden/Grischun/Grigioni|1202449.0|2023.0|256.0|3961.0|
+|Uri |182555.0 |1901.0|433.0|3579.0|
+|Glarus |116676.0 |1584.0|411.0|3597.0|
+|Ticino |472768.0 |1400.0|193.0|3360.0|
+|Obwalden |83282.0 |1342.0|433.0|3172.0|
+|Bern/Berne |1008137.0|1202.0|402.0|4082.0|
+|Appenzell Innerrhoden |29553.0 |1133.0|571.0|2431.0|
++----------------------------+---------+------+-----+------+
+```
+
+
+
+The difference between the lowest and highest point is 4,166 m in Valais. The difference is 197 m in Genève. Both results come from the same query.
+
+## Why one canton can produce several rows
+
+`RS_Intersects(d.rast, c.geometry)` joins each raster tile to every canton that overlaps it. This is a raster-vector spatial join: one side contains raster tiles, and the other contains polygon shapes. The 26 cantons and 18 tiles produce **62 matching pairs**. A canton that crosses a tile boundary has one result for each tile.
+
+Genève crosses the 6°E tile boundary, so it produces two rows:
+
+```
++------+--------+--------+----------------------------------------------------------------------------------------------------------------------------------------------+
+|name |tile_lon|tile_lat|s |
++------+--------+--------+----------------------------------------------------------------------------------------------------------------------------------------------+
+|Genève|5.0 |47.0 |{2761.0, 1133564.8060302734, 410.5631314850678, 411.58612060546875, 345.5, 41.7174495306343, 1740.3455953410198, 332.0, 515.2003784179688} |
+|Genève|6.0 |47.0 |{44656.0, 1.8780723717163086E7, 420.56439710594384, 423.7024383544922, 368.0, 34.66281959027446, 1201.5110619479146, 346.5, 529.1962280273438}|
++------+--------+--------+----------------------------------------------------------------------------------------------------------------------------------------------+
+```
+
+Each tile and canton pair is a separate unit of work. Spark can process the 62 pairs in parallel. The same SQL pattern also works with more tiles and zones. Spark sends the pairs to worker processes across the cluster. The driver manages the query, but it does not receive the raster tiles.
+
+## How to combine results from several tiles
+
+When a canton overlaps several tiles, the final `GROUP BY` must combine the partial results. Each statistic needs the correct formula:
+
+- **Add `count` and `sum`.** Addition works for any number of pairs.
+- **Combine `min` and `max`** by taking the lowest minimum and the highest maximum.
+- **Calculate `mean` as a ratio:** `SUM(s.sum) / SUM(s.count)`. Do not average the mean from each pair. That method gives the same weight to 2,761 pixels and 44,656 pixels, which changes Genève's result by 5 m.
+- **Do not combine `median`, `mode`, `stddev`, or `variance` from the partial values.** For example, the median of several tile medians is not the median of all pixels. These statistics need all pixels for the zone in one raster row. `retile = false` is enough when the zone fits inside one source raster. Otherwise, merge the tiles with `RS_Union_Aggr` before the zonal statistics call.
+
+One more case can produce a wrong number without an error. `RS_Intersects` compares the tile boundary with the zone. A tile can overlap a zone even when no pixel center is inside it. One of the 62 pairs has this result, and `RS_ZonalStatsAll` returns nine zeros:
+
+```
++----------------------------+---------------------------------------------+
+|name |s |
++----------------------------+---------------------------------------------+
+|Graubünden/Grischun/Grigioni|{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}|
++----------------------------+---------------------------------------------+
+```
+
+One of those zeros is the `min` value. If that row enters `MIN(s.min)`, the result for Graubünden becomes 0 m:
+
+```
++----------------------------+---------+------------+
+|name |naive_min|filtered_min|
++----------------------------+---------+------------+
+|Graubünden/Grischun/Grigioni|0.0 |255.5 |
++----------------------------+---------+------------+
+```
+
+The filter `WHERE s.count > 0` removes empty pairs. The correct minimum for Graubünden is 255.5 m. Apply this filter before the final `GROUP BY`.
+
+## Use the same pairs for another question
+
+Zonal statistics can process a raster that a query creates. This example starts with the elevation DEM. A Python user-defined function, or UDF, uses NumPy to calculate the slope of each pixel. The UDF returns the slope values as a new raster column:
+
+```python
+@udf(returnType=RasterType())
+def slope_deg(raster):
+ z = raster.as_numpy_masked()[0].astype(np.float64)
+ t = raster.affine_trans
+ lat = t.ip_y + t.scale_y * raster.height / 2.0
+ m_per_deg_x = 111320.0 * math.cos(math.radians(lat))
+ dzdy, dzdx = np.gradient(z, abs(t.scale_y) * 111320.0, abs(t.scale_x) * m_per_deg_x)
+ slope = np.degrees(np.arctan(np.hypot(dzdx, dzdy)))
+ out = np.where(np.isnan(slope), -9999.0, slope).astype(np.float32)
+ return raster.with_bands(out, nodata=-9999.0)
+```
+
+Wrap the DEM column in `slope_deg(...)`, then run the same join and aggregate. The canton ranking changes. Uri has a mean slope of 28.3° and a maximum slope of 72.5°. Valais has a mean slope of 26.1°, but its mean elevation is 237 m higher than Uri's.
+
+Elevation and slope answer different questions, but both use the same 62 pairs:
+
+
+
+## Use the pattern with other data
+
+The same pattern works with other raster and polygon datasets. For example, replace the DEM with [ESA WorldCover](https://registry.opendata.aws/esa-worldcover/). A UDF can mark pixels that contain one type of land cover, such as forest or built-up land. The sum of the marked pixels gives the pixel count for each zone.
+
+The polygons can also represent parcels instead of cantons. The spatial join does not change. Use the [`geotiff.metadata` reader](https://sedona.apache.org/latest/blog/2026/08/07/index-a-million-rasters-without-reading-a-pixel/) to find the raster tiles that overlap the polygons before opening those tiles.
+
+Raster resolution sets the size of each pixel. A 30 m DEM uses nine times as many pixels as a 90 m DEM to cover the same area. With the 30 m Copernicus DEM, the highest value in Valais is 4,539 m instead of 4,534 m. Both values estimate terrain height from a surface model stored on a grid. The SQL query stays the same.
+
+## The main idea
+
+Raster grids and polygon shapes use different data models. Many pipelines join them by exporting data, running a Python script, and saving intermediate files. `RS_ZonalStatsAll` keeps the join and the statistics in one SQL query. It can read raster data from object storage such as S3.
+
+Use the correct formula for each statistic, and remove pairs with no valid pixels. The same SQL pattern can then process a small set of zones or a large one.
+
+*See the [`RS_ZonalStatsAll` reference](https://sedona.apache.org/latest/api/sql/Raster-Band-Accessors/RS_ZonalStatsAll/) for all parameters. See the [raster tutorial](https://sedona.apache.org/latest/tutorial/raster/) for reading and writing examples. This example contains modified Copernicus DEM data (2026).*