🗂️ სივრცითი ინდექსები: k-d tree, R-tree, Geohash¶
Quadtree სივრცითი ინდექსის ერთადერთი ვარიანტი არ არის. თითოეული სტრუქტურა სხვადასხვა ამოცანას უკეთ ერგება, ამიტომ GIS-ში რამდენიმე მათგანს ხვდებით.
| სტრუქტურა | ინახავს | ძირითადი ძალა | სად გვხვდება |
|---|---|---|---|
| Quadtree | წერტილებს, უჯრებს | მარტივია, ტაილებს უკავშირდება | ვებ-რუკები, რასტრები |
| k-d tree | წერტილებს | უახლოესი მეზობელი | scikit-learn, KDTree |
| R-tree | მართკუთხედებს (ხაზებს, პოლიგონებს) | ობიექტების bbox-ებზე ძებნა | PostGIS (GiST), QGIS, GeoPackage |
| Geohash | წერტილებს (სტრიქონი) | სტრიქონული პრეფიქსი, ბაზა-მეგობრული | Redis, Elasticsearch, MongoDB |
🎮 სცადეთ თქვენც¶
k-d tree: დაამატეთ წერტილები და გადაატარეთ მაუსი: წითელი ხაზი უახლოეს წერტილზე მიდის, ქვემოთ კი ჩანს, რამდენი კვანძი შეამოწმა ხემ. Geohash: გადართეთ რეჟიმი, გადაატარეთ მაუსი საქართველოზე და შეცვალეთ კოდის სიგრძე.
🔹 k-d tree¶
იდეა: ყოველ დონეზე ერთი კოორდინატის მედიანით ვყოფთ წერტილებს: ერთხელ x-ით, შემდეგ y-ით, შემდეგ ისევ x-ით და ა.შ. ხე დაბალანსებულია და წერტილის ჩასმას არ საჭიროებს, ამიტომ მხოლოდ სტატიკური მონაცემებისთვის კარგია.
უახლოესი მეზობელი: ჯერ იმ მხარეს ჩავდივართ, სადაც სამიზნე წერტილია. შემდეგ ვამოწმებთ: ამ წერტილიდან გამყოფ წრფემდე მანძილი მიღებულ საუკეთესო მანძილზე მცირეა თუ არა. თუ მცირეა, მეორე ტოტიც უნდა შევამოწმოთ. თუ არა, მთელ ტოტს სრულად ვტოვებთ.
🔹 R-tree¶
იდეა: Quadtree და k-d tree წერტილებს ინახავენ. ხაზი და პოლიგონი კი წერტილი არ არის, ამიტომ R-tree ობიექტებს მათი bbox-ით ინახავს. ახლო ობიექტები ერთ ჯგუფში ხვდება, ჯგუფს თავისი გარსშემოსაზღვრავი მართკუთხედი აქვს და ჯგუფები ხის მომდევნო დონეზე კვლავ ჯგუფდება. ძებნისას ის ტოტი, რომლის მართკუთხედი საძიებელს არ კვეთს, გამოიტოვება.
ქვემოთ STR (Sort-Tile-Recursive) გზით აგებული ხეა: მონაცემებს x-ით ვახარისხებთ, ვჭრით ვერტიკალურ ზოლებად, ზოლს y-ით ვახარისხებთ და თითო 8 ობიექტს ვაჯგუფებთ.
R-tree გრძელვადიანი ინდექსია
ეს რეალიზაცია მთლიანად მონაცემთა აგების ბოლოს იშენება. რეალური სისტემები (PostGIS, QGIS) R-tree-ს დინამიურად ავსებენ და ამისთვის უფრო რთულ ალგორითმებს იყენებენ, თუმცა დაძებნის პრინციპი იგივეა.
🔹 Geohash¶
იდეა: დედამიწა იყოფა ბადედ, რომლის ყოველი უჯრა სტრიქონით აღინიშნება. კოდის თითო სიმბოლო უჯრას 32 ნაწილად ყოფს. ორი წერტილი, რომელთა გეოჰეშებს საერთო პრეფიქსი აქვს, ახლოს არიან. ამიტომ ახლო წერტილების ძებნა ჩვეულებრივ ტექსტურ ინდექსზე LIKE 'szrv%' ძიებად იქცევა.
| სიგრძე | უჯრის ზომა (დაახლოებით) |
|---|---|
| 3 | 156 × 156 კმ |
| 4 | 39 × 19.5 კმ |
| 5 | 4.9 × 4.9 კმ |
| 6 | 1.2 × 0.6 კმ |
| 8 | 38 × 19 მ |
საზღვრის პრობლემა
ორი წერტილი შეიძლება ფიზიკურად ერთმანეთის გვერდით იყოს, მაგრამ უჯრის საზღვრის სხვადასხვა მხარეს და სრულიად განსხვავებული გეოჰეში ჰქონდეს. ამიტომ ახლო წერტილებისთვის ყოველთვის საჭიროა 8 მეზობელი უჯრის შემოწმება (gh_neighbors).
🐍 რეალიზაცია სუფთა Python-ით¶
| spatial_indexes.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
გამოყენება¶
import random
random.seed(3)
pts = [(random.random() * 100, random.random() * 100) for _ in range(3000)]
# k-d tree
tree = build_kdtree(pts)
print(kd_nearest(tree, (50, 50)))
print(len(kd_range(tree, (20, 30, 45, 50))))
# R-tree: 2000 პატარა მართკუთხედი
boxes = []
for i in range(2000):
x, y = random.random() * 100, random.random() * 100
boxes.append((x, y, x + random.random() * 2, y + random.random() * 2, i))
root = str_pack(boxes, capacity=8)
print(len(r_query(root, (10, 10, 18, 18))))
# Geohash
print(gh_encode(57.64911, 10.40744, 11)) # u4pruydqqvj
print(gh_encode(41.6938, 44.8015, 6)) # თბილისი: szrv5q
print(gh_bounds('szrv5q'))
print(gh_neighbors('szrv5q'))
სირთულე
| სტრუქტურა | აგება | ძებნა (საშუალოდ) |
|---|---|---|
| k-d tree | O(n log n) |
O(log n) უახლოესი მეზობელი |
| R-tree (STR) | O(n log n) |
O(log n + k) |
| Geohash | O(1) თითო წერტილზე |
ინდექსის ძიება პრეფიქსზე |
🧰 იგივე ბიბლიოთეკებით¶
QgsSpatialIndex R-tree-ს იყენებს:
📝 დავალებები¶
დავალება 1
შეადარეთ k-d tree-ის უახლოესი მეზობლის ძებნა სრულ გადარჩევას: რამდენ კვანძს ამოწმებს ხე 100 შემთხვევით მოთხოვნაზე 3000 წერტილში? დაამატეთ მრიცხველი kd_nearest-ში.
ამოხსნა
def kd_nearest_counted(node, target, best=None, counter=None):
if node is None:
return best
counter[0] += 1
d = (node.point[0] - target[0]) ** 2 + (node.point[1] - target[1]) ** 2
if best is None or d < best[1]:
best = (node.point, d)
diff = target[node.axis] - node.point[node.axis]
near, far = (node.left, node.right) if diff < 0 else (node.right, node.left)
best = kd_nearest_counted(near, target, best, counter)
if diff * diff < best[1]:
best = kd_nearest_counted(far, target, best, counter)
return best
total = 0
for _ in range(100):
q = (random.random() * 100, random.random() * 100)
c = [0]
kd_nearest_counted(tree, q, None, c)
total += c[0]
print('საშუალოდ', total / 100, 'კვანძი 3000-დან')
დავალება 2
გეოჰეშით იპოვეთ თბილისის ცენტრიდან 1 კმ-ში მყოფი წერტილები: ჯერ 5-სიმბოლოიანი უჯრებით (ცენტრალური უჯრა და 8 მეზობელი) კანდიდატები აარჩიეთ, შემდეგ ზუსტი მანძილით გაფილტრეთ (ჰავერსინი).
ამოხსნა
import math, random
def haversine(lat1, lon1, lat2, lon2):
r = 6371000
p1, p2 = math.radians(lat1), math.radians(lat2)
a = (math.sin((p2 - p1) / 2) ** 2 +
math.cos(p1) * math.cos(p2) * math.sin(math.radians(lon2 - lon1) / 2) ** 2)
return 2 * r * math.asin(math.sqrt(a))
random.seed(5)
center = (41.6938, 44.8015)
points = [(center[0] + random.uniform(-0.3, 0.3),
center[1] + random.uniform(-0.3, 0.3)) for _ in range(20000)]
# 1) გეოჰეშით კანდიდატები
index = {}
for p in points:
index.setdefault(gh_encode(*p, 5), []).append(p)
cells = [gh_encode(*center, 5)]
cells += gh_neighbors(cells[0])
candidates = [p for c in cells for p in index.get(c, [])]
# 2) ზუსტი ფილტრი
result = [p for p in candidates if haversine(*center, *p) <= 1000]
brute = [p for p in points if haversine(*center, *p) <= 1000]
print(len(candidates), len(result), len(brute), len(result) == len(brute))
True უნდა იყოს: მეზობელი უჯრების გარეშე ზოგი წერტილი გამოგვრჩებოდა.
უჯრის ზომა რადიუსზე დიდი უნდა იყოს
6-სიმბოლოიანი უჯრა დაახლოებით 1.2 × 0.6 კმ-ია. 3×3 უჯრა ცენტრიდან მხოლოდ ~0.9 კმ-ს აფარებს ჩრდილოეთ-სამხრეთით, ამიტომ 1 კმ რადიუსისთვის 6 სიმბოლო გამოგვრჩენდა ნაწილს (შეამოწმეთ: ამ მონაცემებზე 6 სიმბოლოთი 16 წერტილი მოიძებნება 17-ის ნაცვლად). ზოგადი წესი: ავირჩიოთ ისეთი სიგრძე, რომ უჯრის ყველაზე მოკლე გვერდი ≥ რადიუსი.
დავალება 3
გაზომეთ დრო: 20 000 პატარა მართკუთხედზე (a) სრული გადარჩევა, (ბ) str_pack + r_query. რამდენჯერ სწრაფია ძებნა და რამდენი დრო მიდის ხის აგებაზე?
ამოხსნა
import time
boxes = []
for i in range(20000):
x, y = random.random() * 1000, random.random() * 1000
boxes.append((x, y, x + random.random() * 3, y + random.random() * 3, i))
t = time.perf_counter()
root = str_pack(boxes, 8)
build_time = time.perf_counter() - t
queries = [(x, y, x + 20, y + 20) for x, y in
((random.random() * 980, random.random() * 980) for _ in range(500))]
def hit(b, q):
return not (b[0] > q[2] or b[2] < q[0] or b[1] > q[3] or b[3] < q[1])
t = time.perf_counter()
for q in queries:
[b for b in boxes if hit(b, q)]
brute = time.perf_counter() - t
t = time.perf_counter()
for q in queries:
r_query(root, q)
tree_t = time.perf_counter() - t
print(f'აგება {build_time:.3f}წმ, სრული {brute:.3f}წმ, R-tree {tree_t:.3f}წმ')