Skip to content

Commit 27a4ce7

Browse files
committed
Add focal variety user guide notebook (#1040)
1 parent c0ad897 commit 27a4ce7

File tree

1 file changed

+204
-0
lines changed

1 file changed

+204
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Focal Variety\n",
8+
"\n",
9+
"Focal variety counts the number of distinct values in a sliding\n",
10+
"neighbourhood window. It is most useful for categorical rasters\n",
11+
"(land-cover, soil type, geology codes) where you want to map\n",
12+
"boundary complexity or patch fragmentation.\n",
13+
"\n",
14+
"This notebook shows how to compute focal variety with\n",
15+
"`xrspatial.focal.focal_stats` across different kernel shapes."
16+
]
17+
},
18+
{
19+
"cell_type": "code",
20+
"execution_count": null,
21+
"metadata": {},
22+
"outputs": [],
23+
"source": [
24+
"import numpy as np\n",
25+
"import xarray as xr\n",
26+
"import matplotlib.pyplot as plt\n",
27+
"\n",
28+
"from xrspatial.convolution import circle_kernel, custom_kernel\n",
29+
"from xrspatial.focal import focal_stats"
30+
]
31+
},
32+
{
33+
"cell_type": "markdown",
34+
"metadata": {},
35+
"source": [
36+
"## Create a synthetic land-cover raster\n",
37+
"\n",
38+
"We build a 60x60 grid with four land-cover classes arranged in\n",
39+
"quadrants, plus a few scattered patches to make things interesting."
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": null,
45+
"metadata": {},
46+
"outputs": [],
47+
"source": [
48+
"rng = np.random.default_rng(42)\n",
49+
"rows, cols = 60, 60\n",
50+
"\n",
51+
"# Four quadrants: classes 1-4\n",
52+
"lc = np.ones((rows, cols), dtype=np.float64)\n",
53+
"lc[:rows//2, cols//2:] = 2\n",
54+
"lc[rows//2:, :cols//2] = 3\n",
55+
"lc[rows//2:, cols//2:] = 4\n",
56+
"\n",
57+
"# Scatter some class-5 patches\n",
58+
"for _ in range(30):\n",
59+
" r, c = rng.integers(0, rows), rng.integers(0, cols)\n",
60+
" lc[r:r+3, c:c+3] = 5\n",
61+
"\n",
62+
"land_cover = xr.DataArray(lc, dims=['y', 'x'], name='land_cover')\n",
63+
"\n",
64+
"fig, ax = plt.subplots(figsize=(5, 5))\n",
65+
"land_cover.plot(ax=ax, cmap='Set2', add_colorbar=True)\n",
66+
"ax.set_title('Synthetic land-cover raster')\n",
67+
"ax.set_aspect('equal')\n",
68+
"plt.tight_layout()\n",
69+
"plt.show()"
70+
]
71+
},
72+
{
73+
"cell_type": "markdown",
74+
"metadata": {},
75+
"source": [
76+
"## Compute focal variety with a 3x3 box kernel\n",
77+
"\n",
78+
"A 3x3 box kernel counts how many distinct classes appear in the\n",
79+
"immediate 8-connected neighbourhood of each pixel."
80+
]
81+
},
82+
{
83+
"cell_type": "code",
84+
"execution_count": null,
85+
"metadata": {},
86+
"outputs": [],
87+
"source": [
88+
"kernel_box = np.ones((3, 3))\n",
89+
"result_box = focal_stats(land_cover, kernel_box, stats_funcs=['variety'])\n",
90+
"variety_box = result_box.sel(stats='variety')\n",
91+
"\n",
92+
"fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n",
93+
"land_cover.plot(ax=axes[0], cmap='Set2', add_colorbar=True)\n",
94+
"axes[0].set_title('Land cover')\n",
95+
"axes[0].set_aspect('equal')\n",
96+
"\n",
97+
"variety_box.plot(ax=axes[1], cmap='YlOrRd', add_colorbar=True)\n",
98+
"axes[1].set_title('Focal variety (3x3 box)')\n",
99+
"axes[1].set_aspect('equal')\n",
100+
"plt.tight_layout()\n",
101+
"plt.show()"
102+
]
103+
},
104+
{
105+
"cell_type": "markdown",
106+
"metadata": {},
107+
"source": [
108+
"Pixels deep inside a uniform quadrant show variety = 1. Pixels on\n",
109+
"boundaries between classes show variety = 2, 3, or 4 depending on\n",
110+
"how many classes meet at that point. The scattered class-5 patches\n",
111+
"create small pockets of higher variety."
112+
]
113+
},
114+
{
115+
"cell_type": "markdown",
116+
"metadata": {},
117+
"source": [
118+
"## Larger kernel: 5x5 circle\n",
119+
"\n",
120+
"Increasing the kernel radius captures more of the surrounding\n",
121+
"landscape, so variety values near boundaries will be higher."
122+
]
123+
},
124+
{
125+
"cell_type": "code",
126+
"execution_count": null,
127+
"metadata": {},
128+
"outputs": [],
129+
"source": [
130+
"kernel_circle = circle_kernel(2, 2, 2)\n",
131+
"result_circle = focal_stats(land_cover, kernel_circle, stats_funcs=['variety'])\n",
132+
"variety_circle = result_circle.sel(stats='variety')\n",
133+
"\n",
134+
"fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n",
135+
"variety_box.plot(ax=axes[0], cmap='YlOrRd', add_colorbar=True)\n",
136+
"axes[0].set_title('Variety (3x3 box)')\n",
137+
"axes[0].set_aspect('equal')\n",
138+
"\n",
139+
"variety_circle.plot(ax=axes[1], cmap='YlOrRd', add_colorbar=True)\n",
140+
"axes[1].set_title('Variety (5x5 circle)')\n",
141+
"axes[1].set_aspect('equal')\n",
142+
"plt.tight_layout()\n",
143+
"plt.show()"
144+
]
145+
},
146+
{
147+
"cell_type": "markdown",
148+
"metadata": {},
149+
"source": [
150+
"## Combining variety with other focal stats\n",
151+
"\n",
152+
"You can request variety alongside other statistics in one call.\n",
153+
"Here we grab both range and variety to compare continuous and\n",
154+
"categorical measures of local heterogeneity."
155+
]
156+
},
157+
{
158+
"cell_type": "code",
159+
"execution_count": null,
160+
"metadata": {},
161+
"outputs": [],
162+
"source": [
163+
"result_combo = focal_stats(land_cover, kernel_box,\n",
164+
" stats_funcs=['range', 'variety'])\n",
165+
"\n",
166+
"fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n",
167+
"result_combo.sel(stats='range').plot(ax=axes[0], cmap='viridis',\n",
168+
" add_colorbar=True)\n",
169+
"axes[0].set_title('Focal range')\n",
170+
"axes[0].set_aspect('equal')\n",
171+
"\n",
172+
"result_combo.sel(stats='variety').plot(ax=axes[1], cmap='YlOrRd',\n",
173+
" add_colorbar=True)\n",
174+
"axes[1].set_title('Focal variety')\n",
175+
"axes[1].set_aspect('equal')\n",
176+
"plt.tight_layout()\n",
177+
"plt.show()"
178+
]
179+
},
180+
{
181+
"cell_type": "markdown",
182+
"metadata": {},
183+
"source": [
184+
"Range measures the numeric spread (max minus min) while variety\n",
185+
"counts distinct classes. For categorical data, variety is usually\n",
186+
"the more meaningful measure since the numeric distance between\n",
187+
"class codes is arbitrary."
188+
]
189+
}
190+
],
191+
"metadata": {
192+
"kernelspec": {
193+
"display_name": "Python 3",
194+
"language": "python",
195+
"name": "python3"
196+
},
197+
"language_info": {
198+
"name": "python",
199+
"version": "3.10.0"
200+
}
201+
},
202+
"nbformat": 4,
203+
"nbformat_minor": 4
204+
}

0 commit comments

Comments
 (0)