IlmHamroh
Data Science va sun'iy intellekt/Maxsus mavzular9/12-dars45 daqiqa
Mundarija (23)

28.9-dars: Geografik ma'lumot

28-QISM — MAXSUS MAVZULAR · 9-dars


1. Kirish va motivatsiya

Oldingi ikki darsda agent qaror qabul qilib, dunyoni o'zgartirdi. Endi yana ma'lumotga qaytamiz — lekin o'ziga xos turdagi ma'lumotga: joylashuv. Taksi buyurtmasi, yetkazib berish manzili, do'kon, bankomat, telefon signali, ko'chmas mulk e'loni — bularning hammasida kenglik va uzunlik bor. Birinchi qarashda ular oddiy ikki son ustun: lat va lon. Aslida esa ular kursda ko'rgan boshqa ustunlarning birortasiga o'xshamaydi.

Birinchidan, masofa noto'g'ri hisoblanadi, agar koordinatalarni tekis deb olsangiz: Toshkent kengligida bir gradus uzunlik 83.5 km, bir gradus kenglik esa 111.2 km. Gradus farqlaridan Evklid masofasi sharq-g'arb yo'nalishini uchdan bir marta oshirib yuboradi. Ikkinchidan, qidiruv sekin: "shu nuqtadan 300 m ichidagi barcha buyurtmalar" savoli har safar million nuqtani ko'rib chiqishni talab qilmasligi uchun maxsus indeks kerak. Uchinchidan — va eng muhimi — yaqin nuqtalar bir-biriga o'xshaydi. Bu fazoviy avtokorrelyatsiya: bir mahalladagi kutish vaqti, uy narxi, kasallik darajasi qo'shni mahallanikiga yaqin. Bu model uchun foydali signal, lekin baholash uchun tuzoq: tasodifiy kross-validatsiyada test nuqtasining "qo'shnisi" o'quv to'plamida qoladi va model yangi hudud uchun qanchalik yomon ekanini yashiradi (17.8-darsdagi sizishning fazoviy ko'rinishi).

Real vaziyat. Taksi xizmati mijoz kutish vaqtini bashorat qilish modelini qurdi va tasodifiy 5-foldli CV da ajoyib natija oldi. Model Toshkentning yangi tumanlariga xizmat kengaygan paytda ishga tushirildi — va u yerda xato kutilganidan sezilarli katta chiqdi. Sabab: CV da har test safarining yonida, bir necha yuz metr narida o'quv safarlari bor edi, model esa "qo'shnilarni eslab" qolgan edi. 4-misolda aynan shu holatni o'lchaymiz: tasodifiy KFold yangi hudud xatosini muntazam kam ko'rsatadi, fazoviy blokli CV esa to'g'ri baholaydi.

Bu darsda geografik ma'lumot bilan ishlashning asosiy vositalarini noldan quramiz va har bir tuzoqni o'lchaymiz.

Bu darsda:

  • Koordinatalar: kenglik, uzunlik, WGS84
  • Masofa: gradusdagi Evklid xatosi, haversine formulasi, lokal proyeksiya
  • Yo'nalish (azimut)
  • Sintetik taksi safarlari Toshkent atrofida
  • Fazoviy indekslash: panjara, geohash (noldan), BallTree(metric="haversine")
  • Nuqta-poligon: ray casting noldan, fazoviy birlashtirish
  • Fazoviy belgilar va maqsadli kodlashdagi sizish
  • Fazoviy avtokorrelyatsiya (Moran I) va fazoviy kross-validatsiya
  • Issiq nuqtalar: DBSCAN haversine bilan
  • Tuzoqlar

ℹ Misollar numpy, pandas va sklearn bilan (Python 3.14). geopandas, shapely va folium o'rnatilmagan — ularning ishini kichik funksiyalar bilan noldan qilamiz, kutubxona kodi faqat nazariya bloklarida. Shaharlar koordinatalari taxminiy, tuman chegaralari va safarlar sintetik.


2. Nazariya — chuqur tushuntirish

2.1. Koordinatalar va WGS84

text
KENGLIK (latitude, lat):   ekvatordan shimol (+) / janub (-), -90..+90 gradus
UZUNLIK (longitude, lon):  Grinvichdan sharq (+) / g'arb (-), -180..+180 gradus
Toshkent (taxminan):       lat 41.31, lon 69.28

WGS84 - GPS ishlatadigan standart (EPSG:4326): Yer - ellipsoid,
  ekvator radiusi 6378.137 km, qutb radiusi 6356.752 km
  ML uchun odatda SHAR yaqinlashuvi yetarli: R = 6371.0 km
  (shahar va mamlakat miqyosida xato ~0.3% dan kam)

TARTIB TUZOG'I:  (lat, lon)  - odamlar va sklearn
                 (lon, lat)  - GeoJSON, shapely, ko'p GIS formatlar (x, y)
  ikkalasini aralashtirib yuborish - eng ko'p uchraydigan xato

ANIQLIK:  5 xona (0.00001 gradus) ~ 1.1 m;  3 xona ~ 110 m
  GPS xatosi shaharda 5-20 m, binolar orasida - ko'proq

2.2. Masofa: Evklid, haversine, lokal proyeksiya

text
GRADUSDAGI EVKLID (XATO):
  d = sqrt(dlat^2 + dlon^2) * 111.2 km
  muammo: 1 gradus uzunlik = 111.2 * cos(lat) km
  Toshkentda cos(41.3) = 0.75 -> sharq-g'arb masofasi 1/0.75 = 1.33 marta oshadi

HAVERSINE (katta doira masofasi, shar ustida):
  a = sin^2(dfi / 2) + cos(fi1) * cos(fi2) * sin^2(dlambda / 2)
  d = 2 * R * arcsin( sqrt(a) )
  fi - kenglik, lambda - uzunlik (RADIANDA!)
  aniq (shar uchun), har qanday masofada

EKVIREKTANGULYAR (lokal proyeksiya):
  x = R * dlambda * cos(fi0),   y = R * dfi,   d = sqrt(x^2 + y^2)
  fi0 - hududning o'rtacha kengligi
  shahar ichida xato ~0.01-0.2%; tekis koordinatalar -> Evklid,
  KMeans, KNN, chiziqli modellar to'g'ridan-to'g'ri ishlaydi

AZIMUT (yo'nalish, 0 = shimol, 90 = sharq):
  theta = atan2( sin(dlambda) cos(fi2),
                 cos(fi1) sin(fi2) - sin(fi1) cos(fi2) cos(dlambda) )

"Qush uchishi" masofa yo'l masofasi emas. Ko'chalar egri, daryolar va temir yo'llar aylanib o'tishni talab qiladi: shaharda yo'l masofasi odatda to'g'ri chiziqdan 1.2-1.5 marta uzun. Haqiqiy yo'l masofasi va vaqti uchun marshrutlash xizmatlari (OSRM, Google Directions) yoki yo'l grafi kerak — lekin haversine ko'pincha kuchli boshlang'ich belgi.

Haqiqiy loyihada proyeksiyalar bilan pyproj/geopandas ishlaydi: masalan, O'zbekiston uchun UTM zonasi 42N (EPSG:32642) metr birligidagi tekis koordinatalar beradi.

python
import geopandas as gpd

gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs="EPSG:4326")
gdf_m = gdf.to_crs("EPSG:32642")                    # UTM 42N, metrlarda
gdf_m["x"], gdf_m["y"] = gdf_m.geometry.x, gdf_m.geometry.y

2.3. Fazoviy indekslash

"300 m ichidagi hamma nuqtalar" yoki "eng yaqin 5 ta haydovchi" — har so'rovda barcha N nuqtaga masofa hisoblash O(N). Million nuqta va minglab so'rovda bu juda sekin.

text
PANJARA (grid):
  katak = (floor(lat / dlat), floor(lon / dlon)); lug'at: katak -> nuqtalar
  radius r so'rovi: katak o'lchami >= r bo'lsa, 3 x 3 qo'shni katak yetarli
  oddiy va tez; zichlik notekis bo'lsa - ba'zi kataklar to'lib ketadi

GEOHASH:
  uzunlik va kenglik oraliqlarini navbat bilan ikkiga bo'lish (bit interleaving)
  5 bit -> 1 belgi (base32): "tx3702"
  UMUMIY PREFIKS = yaqinlik (odatda): "tx37..." - bitta ~20 km katak
  uzunlik 5 ~ 4.9 x 3.7 km,  6 ~ 0.6 x 0.9 km,  7 ~ 150 x 110 m (Toshkentda)
  TUZOQ: chegaraning ikki tomonidagi yaqin nuqtalar - boshqa kod
         -> radius so'rovida qo'shni kataklar ham tekshiriladi
  qo'llanishi: ma'lumotlar bazasida indeks (matn prefiksi), kalit sifatida
               guruhlash, belgi (kategoriya)

BALLTREE (sklearn):
  nuqtalarni ichma-ich "sharlar" ga ajratadi; so'rovda uzoq sharlar
  butunlay tashlab yuboriladi
  metric="haversine" - kirish RADIANDA [lat, lon], natija RADIANDA
    radius = r_km / R,   masofa_km = natija * R
  kNN va radius so'rovlari; KDTree haversine ni qo'llamaydi

BOSHQALAR: H3 (Uber, olti burchakli kataklar), S2 (Google), R-tree (poligonlar)

2.4. Nuqta-poligon va fazoviy birlashtirish

"Bu buyurtma qaysi tumanda?" — nuqta-poligon (point-in-polygon) savoli. Ko'p nuqtalarni poligonlar bilan bog'lash — fazoviy birlashtirish (spatial join), pd.merge ning fazoviy ko'rinishi.

text
RAY CASTING:
  nuqtadan bir tomonga (masalan, sharqqa) nur chizamiz
  nur poligon chegarasini TOQ marta kessa - nuqta ichkarida, JUFT - tashqarida
  har qirra (y1,x1)-(y2,x2) uchun:
    kesadi = (y1 > y) != (y2 > y)             # qirra nur balandligini o'tadimi
    x_k = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
    agar kesadi va x < x_k: ichida = not ichida
  qavariq bo'lmagan poligonlarda ham ishlaydi; teshikli poligon -
  tashqi va ichki halqalar alohida

TUZOQLAR: nuqta aynan chegarada (qaysi tumanga?); poligonlar bir-birini
  yopib yoki bo'shliq qoldirib qo'ygan ("tashqarida" nuqtalar);
  koordinata tartibi (lat, lon) va (x, y)
python
import geopandas as gpd

tumanlar = gpd.read_file("tumanlar.geojson")                  # poligonlar
nuqtalar = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs="EPSG:4326")
birlashgan = gpd.sjoin(nuqtalar, tumanlar, how="left", predicate="within")

2.5. Fazoviy belgilar

text
MASOFA BELGILARI:   markazgacha, aeroportgacha, eng yaqin metro bekatigacha (km)
YO'NALISH:          markazdan azimut -> sin va cos (aylana belgi: 359 va 1
                    gradus yaqin, lekin sonlar sifatida uzoq; 17.5-darsdagi
                    soat kabi)
KATAK BELGILARI:    geohash / panjara katagi (kategoriya), katakdagi zichlik
AGREGATLAR:         radius r ichidagi buyurtmalar soni, o'rtacha narx
                    (BallTree bilan)
MA'MURIY:           tuman (nuqta-poligon orqali)
MAQSADLI KODLASH:   katak bo'yicha maqsad o'rtachasi
  ⚠️ SIZISH: qatorning o'z maqsadi o'z belgisida -> kichik kataklarda
             (1-3 qator) belgi = maqsad; CV haddan tashqari yaxshi
  ✅ out-of-fold: har fold uchun faqat boshqa foldlardan hisoblash,
     silliqlash (umumiy o'rtachaga qarab tortish) - 17.8-dars

Daraxt modellari (15-qism) lat va lon dan fazoviy tuzilmani o'zlari qisman o'rganadi — lekin faqat o'qlarga parallel bo'linishlar bilan. Chiziqli modellar esa lat va lon dan "markazgacha masofa" ni umuman o'rgana olmaydi — ularga bu belgilarni biz berishimiz kerak (3-misol).

2.6. Fazoviy avtokorrelyatsiya va Moran I

text
TOBLERNING 1-QONUNI: "hamma narsa hamma narsaga bog'liq,
                      lekin yaqin narsalar uzoqdagilarga qaraganda ko'proq"

MORAN I (global fazoviy avtokorrelyatsiya):
  z_i = x_i - mean(x)
  w_ij - fazoviy vaznlar (masalan: j - i ning k eng yaqin qo'shnisidan biri -> 1)
  I = (N / S0) * sum_i sum_j w_ij z_i z_j / sum_i z_i^2,   S0 = sum w_ij
  I ~ -1/(N-1) ~ 0   tasodifiy joylashuv
  I > 0              o'xshash qiymatlar yaqin (klaster)
  I < 0              shaxmat taxtasi (qo'shnilar farqli)
  p-qiymat: qiymatlarni joylar orasida tasodifiy aralashtirib (permutatsiya)
            I ning "tasodifiy" taqsimoti bilan solishtirish (11-qism)

QOLDIQLARDA Moran I > 0 -> model fazoviy tuzilmani to'liq ushlamagan

2.7. Fazoviy kross-validatsiya

text
TASODIFIY KFold: test nuqtasining qo'shnilari (100-300 m) o'quv to'plamida
  -> model "qo'shnini eslab" bashorat qiladi
  -> xato: "ma'lum hududdagi yangi nuqta" uchun TO'G'RI,
           "yangi hudud" uchun OPTIMISTIK

FAZOVIY BLOKLI CV: hududni bloklarga bo'lamiz (masalan, 1.5 km),
  GroupKFold(groups = blok) -> test bloki butunlay o'quvdan tashqarida
  blok o'lchami >= avtokorrelyatsiya masofasi bo'lsin
  (juda katta blok - pessimistik va kam fold)

QAYSI BIRI? SAVOLGA BOG'LIQ (18-qismdagi "baholash dizayni" qoidasi):
  mavjud hududlarda yangi buyurtmalar    -> tasodifiy (vaqt bo'yicha!) CV
  yangi tuman / shahar / mamlakat        -> blokli CV
  vaqt ham bor bo'lsa                    -> vaqt + joy bo'yicha birga
python
from sklearn.model_selection import GroupKFold, cross_val_score

blok = (np.floor(x_km / 1.5) * 1000 + np.floor(y_km / 1.5)).astype(int)
ball = cross_val_score(model, X, y, cv=GroupKFold(5), groups=blok,
                       scoring="neg_root_mean_squared_error")

2.8. Issiq nuqtalar: DBSCAN haversine bilan

16.5-darsda DBSCAN ni ko'rgan edik: zichlik bo'yicha klasterlar, klasterlar sonini oldindan bermaymiz, shovqin nuqtalar alohida. Geografik ma'lumot uchun u juda qulay, chunki eps — metrdagi ma'noli radius:

text
DBSCAN(eps = 0.10 / R, min_samples = 50, metric="haversine",
       algorithm="ball_tree").fit(np.radians(P))
  eps: 100 m radius (radianda)
  min_samples: shu radiusda kamida 50 ta nuqta -> "yadro"
  natija: vokzal, bozor, aeroport kabi zich nuqtalar - klaster;
          tarqoq buyurtmalar - shovqin (-1)
KMeans dan farqi: shakl va son erkin, shovqinni majburan klasterga
                  qo'shmaydi; Evklid emas, haversine
HDBSCAN (sklearn 1.3+): turli zichlikdagi klasterlar uchun

Xaritada ko'rish uchun amalda folium (Leaflet) ishlatiladi:

python
import folium
from folium.plugins import HeatMap

xarita = folium.Map(location=[41.31, 69.28], zoom_start=12)
HeatMap(df[["lat", "lon"]].values.tolist(), radius=8).add_to(xarita)
xarita.save("issiq_nuqtalar.html")

2.9. Tuzoqlar

Asosiy tuzoqlar: gradus farqlaridan Evklid masofa; lat va lon tartibini aralashtirish; BallTree(haversine) ga gradus berish yoki natijani radiandan km ga o'tkazmaslik; "qush uchishi" ni yo'l masofasi deb olish; geohash qo'shnilarini tekshirmaslik; azimutni oddiy son sifatida berish (sin/cos o'rniga); kichik kataklarda maqsadli kodlashni o'z qatori bilan hisoblash; yangi hudud uchun model baholashda tasodifiy KFold; koordinatalarni chop etib, shaxsiy ma'lumotni (uy manzili) oshkor qilish; DBSCAN eps ni gradusda berish.


3. Tez ma'lumotnoma

python
import numpy as np
from sklearn.neighbors import BallTree
from sklearn.model_selection import GroupKFold
from sklearn.cluster import DBSCAN

R = 6371.0088                                             # km


def haversine(lat1, lon1, lat2, lon2):
    f1, f2 = np.radians(lat1), np.radians(lat2)
    a = (np.sin((f2 - f1) / 2) ** 2
         + np.cos(f1) * np.cos(f2) * np.sin(np.radians(lon2 - lon1) / 2) ** 2)
    return 2 * R * np.arcsin(np.sqrt(a))


# lokal tekis koordinatalar (km)
x = np.radians(lon - lon0) * np.cos(np.radians(lat0)) * R
y = np.radians(lat - lat0) * R

# BallTree: RADIAN kiritish, RADIAN natija
daraxt = BallTree(np.radians(P), metric="haversine")
d, idx = daraxt.query(np.radians(Q), k=5)                 # d * R -> km
ichida = daraxt.query_radius(np.radians(Q), r=0.3 / R)    # 300 m

# fazoviy blokli CV
blok = (np.floor(x / 1.5) * 1000 + np.floor(y / 1.5)).astype(int)
cv = GroupKFold(5)                                        # .split(X, y, groups=blok)

# issiq nuqtalar
belgi = DBSCAN(eps=0.1 / R, min_samples=50, metric="haversine",
               algorithm="ball_tree").fit_predict(np.radians(P))

Qaysi vaziyatda nima

Vazifa Vosita
Ikki nuqta orasidagi masofa haversine (istalgan masofa)
Shahar ichida tekis koordinata ekvirektangulyar yoki UTM (pyproj)
Eng yaqin qo'shnilar, radius BallTree(metric="haversine")
Ma'lumotlar bazasida joy bo'yicha qidiruv geohash / H3 prefiks indeksi, PostGIS
Nuqta qaysi hududda ray casting; geopandas.sjoin
Yo'nalish belgisi azimut -> sin, cos
Katak bo'yicha maqsad out-of-fold maqsadli kodlash
Yangi hudud uchun baholash GroupKFold fazoviy bloklar bo'yicha
Issiq nuqtalar DBSCAN (haversine, eps metrda)
Xarita folium, geopandas.plot

Geografik ma'lumot xulosasi

gradus - tekis emas: 1 gradus uzunlik = 111.2 * cos(lat) km
haversine - aniq; ekvirektangulyar - shaharda yetarli; yo'l != qush uchishi
indeks: panjara, geohash (+ qo'shnilar), BallTree (radian!)
belgilar: masofa, azimut (sin/cos), tuman, zichlik, TE (out-of-fold)
yaqin nuqtalar o'xshash (Moran I) -> yangi hudud uchun blokli CV

4. Batafsil misollar

Misollar numpy, pandas va sklearn bilan (Python 3.14). Har misol mustaqil ishlaydi. Safarlar sintetik, Toshkent atrofida; koordinatalar taxminiy.

Misol 1 — Masofa: Evklid xatosi, haversine va lokal proyeksiya

python
"""Masofa: gradusdagi Evklid xatosi, haversine noldan, lokal proyeksiya va yo'nalish."""

import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import haversine_distances

R_YER = 6371.0088                                  # Yerning o'rtacha radiusi, km

# Shaharlar markazlarining TAXMINIY koordinatalari (kenglik, uzunlik)
SHAHARLAR = {
    "Toshkent": (41.311, 69.279),
    "Samarqand": (39.655, 66.960),
    "Buxoro": (39.768, 64.421),
    "Namangan": (40.998, 71.672),
    "Nukus": (42.460, 59.603),
    "Chirchiq": (41.469, 69.582),
}


def haversine(lat1, lon1, lat2, lon2):
    """Katta doira masofasi (km). Argumentlar gradusda, numpy massiv bo'lishi mumkin."""
    f1, f2 = np.radians(lat1), np.radians(lat2)
    df, dl = f2 - f1, np.radians(lon2 - lon1)
    a = np.sin(df / 2) ** 2 + np.cos(f1) * np.cos(f2) * np.sin(dl / 2) ** 2
    return 2 * R_YER * np.arcsin(np.sqrt(np.clip(a, 0, 1)))


def evklid_gradus(lat1, lon1, lat2, lon2):
    """XATO usul: gradus farqlarini tekis koordinata deb olib, 111.2 ga ko'paytirish."""
    return np.hypot(lat2 - lat1, lon2 - lon1) * 111.195


def ekvirektangulyar(lat1, lon1, lat2, lon2, lat0=None):
    """Lokal proyeksiya: x = R * dlon * cos(lat0), y = R * dlat (km)."""
    if lat0 is None:
        lat0 = (lat1 + lat2) / 2
    x = np.radians(lon2 - lon1) * np.cos(np.radians(lat0)) * R_YER
    y = np.radians(lat2 - lat1) * R_YER
    return np.hypot(x, y)


def yonalish(lat1, lon1, lat2, lon2):
    """Boshlang'ich azimut (gradus, 0 = shimol, 90 = sharq)."""
    f1, f2 = np.radians(lat1), np.radians(lat2)
    dl = np.radians(lon2 - lon1)
    x = np.sin(dl) * np.cos(f2)
    y = np.cos(f1) * np.sin(f2) - np.sin(f1) * np.cos(f2) * np.cos(dl)
    return (np.degrees(np.arctan2(x, y)) + 360) % 360


def main() -> None:
    print("=== 1. Shaharlar orasidagi masofa (koordinatalar TAXMINIY) ===")
    t = SHAHARLAR["Toshkent"]
    print(f"  {'Toshkent ->':<12} {'haversine':>10} {'sklearn':>9} {'Evklid(gr)':>11} "
          f"{'xato %':>7} {'ekvirekt.':>10} {'xato %':>7} {'azimut':>7}")
    for nom, (la, lo) in SHAHARLAR.items():
        if nom == "Toshkent":
            continue
        h = haversine(t[0], t[1], la, lo)
        sk = haversine_distances(np.radians([t]), np.radians([(la, lo)]))[0, 0] * R_YER
        e = evklid_gradus(t[0], t[1], la, lo)
        q = ekvirektangulyar(t[0], t[1], la, lo)
        print(f"  {nom:<12} {h:>10.1f} {sk:>9.1f} {e:>11.1f} {100 * (e - h) / h:>+7.1f} "
              f"{q:>10.1f} {100 * (q - h) / h:>+7.2f} {yonalish(t[0], t[1], la, lo):>7.0f}")
    print("  (yo'l masofasi to'g'ri chiziqdan uzunroq - bu yerda 'qush uchishi')")

    print("\n=== 2. 1 gradus necha km? (kenglikka bog'liq) ===")
    for lat in [0, 30, 41.3, 60]:
        print(f"  kenglik {lat:>5}: 1 gradus kenglik = {haversine(lat, 69, lat + 1, 69):6.1f} km, "
              f"1 gradus uzunlik = {haversine(lat, 69, lat, 70):6.1f} km")

    print("\n=== 3. Sintetik taksi safarlari (Toshkent atrofi) ===")
    rng = np.random.default_rng(0)
    markazlar = np.array([[41.326, 69.228],     # Chorsu atrofi (sintetik)
                          [41.311, 69.279],     # markaz
                          [41.258, 69.281],     # aeroport tomoni
                          [41.365, 69.290],     # shimol (turar joy)
                          [41.285, 69.200],     # g'arb (turar joy)
                          [41.230, 69.220]])    # janub
    ulush = np.array([0.22, 0.28, 0.10, 0.15, 0.15, 0.10])
    n = 20_000
    k = rng.choice(len(markazlar), n, p=ulush)
    olish = markazlar[k] + rng.normal(0, 0.012, (n, 2))
    km = rng.lognormal(np.log(5), 0.6, n)                       # to'g'ri chiziq masofasi
    burchak = rng.uniform(0, 2 * np.pi, n)
    dlat = km * np.cos(burchak) / 111.195
    dlon = km * np.sin(burchak) / (111.195 * np.cos(np.radians(olish[:, 0])))
    tushish = olish + np.c_[dlat, dlon]
    soat = rng.choice(24, n, p=np.r_[np.full(6, 0.01), np.full(4, 0.07),
                                     np.full(6, 0.04), np.full(4, 0.07), np.full(4, 0.035)])
    tirband = np.isin(soat, [7, 8, 9, 17, 18, 19])
    h = haversine(olish[:, 0], olish[:, 1], tushish[:, 0], tushish[:, 1])
    yol = h * rng.uniform(1.2, 1.45, n)                          # ko'chalar egri
    narx = (6000 + 2200 * yol) * np.where(tirband, 1.3, 1.0) * rng.lognormal(0, 0.08, n)
    df = pd.DataFrame({"olish_lat": olish[:, 0], "olish_lon": olish[:, 1],
                       "tushish_lat": tushish[:, 0], "tushish_lon": tushish[:, 1],
                       "soat": soat, "narx": narx.round(-2)})
    print(f"  safarlar: {len(df)}, kenglik {df.olish_lat.min():.3f}-{df.olish_lat.max():.3f}, "
          f"uzunlik {df.olish_lon.min():.3f}-{df.olish_lon.max():.3f}")
    print(f"  haversine: mediana {np.median(h):.2f} km, 95-persentil {np.quantile(h, 0.95):.2f} km")
    print(f"  narx: mediana {df.narx.median():.0f} so'm, tirbandlikda "
          f"{df.narx[tirband].median() / df.narx[~tirband].median():.2f} barobar")

    print("\n=== 4. Shahar ichida qaysi masofa yetarli? ===")
    e = evklid_gradus(olish[:, 0], olish[:, 1], tushish[:, 0], tushish[:, 1])
    q = ekvirektangulyar(olish[:, 0], olish[:, 1], tushish[:, 0], tushish[:, 1], lat0=41.3)
    for nom, x in [("Evklid (gradus)", e), ("ekvirektangulyar", q)]:
        nisbiy = (x - h) / h
        print(f"  {nom:<17} o'rtacha xato {100 * nisbiy.mean():+6.2f}%, "
              f"maks |xato| {100 * np.abs(nisbiy).max():6.2f}%")
    for nom, x in [("haversine", h), ("Evklid (gradus)", e)]:
        r = np.corrcoef(x, df.narx)[0, 1]
        b = np.polyfit(x, df.narx, 1)[0]
        print(f"  narx ~ {nom:<16} korrelyatsiya {r:.4f}, 1 km uchun {b:7.0f} so'm")
    print("  ⭐ Evklid(gradus) sharq-g'arb masofasini ~1/cos(41.3) = "
          f"{1 / np.cos(np.radians(41.3)):.2f} marta oshiradi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Shaharlar orasidagi masofa (koordinatalar TAXMINIY) ===
  Toshkent ->   haversine   sklearn  Evklid(gr)  xato %  ekvirekt.  xato %  azimut
  Samarqand         269.0     269.0       316.9   +17.8      269.0   +0.01     228
  Buxoro            444.8     444.8       566.8   +27.4      444.9   +0.02     249
  Namangan          203.3     203.3       268.4   +32.0      203.3   +0.00      99
  Nukus             810.7     810.7      1083.5   +33.7      811.1   +0.06     282
  Chirchiq           30.8      30.8        38.0   +23.4       30.8   +0.00      55
  (yo'l masofasi to'g'ri chiziqdan uzunroq - bu yerda 'qush uchishi')

=== 2. 1 gradus necha km? (kenglikka bog'liq) ===
  kenglik     0: 1 gradus kenglik =  111.2 km, 1 gradus uzunlik =  111.2 km
  kenglik    30: 1 gradus kenglik =  111.2 km, 1 gradus uzunlik =   96.3 km
  kenglik  41.3: 1 gradus kenglik =  111.2 km, 1 gradus uzunlik =   83.5 km
  kenglik    60: 1 gradus kenglik =  111.2 km, 1 gradus uzunlik =   55.6 km

=== 3. Sintetik taksi safarlari (Toshkent atrofi) ===
  safarlar: 20000, kenglik 41.191-41.402, uzunlik 69.155-69.329
  haversine: mediana 4.98 km, 95-persentil 13.37 km
  narx: mediana 23000 so'm, tirbandlikda 1.30 barobar

=== 4. Shahar ichida qaysi masofa yetarli? ===
  Evklid (gradus)   o'rtacha xato +17.02%, maks |xato|  33.29%
  ekvirektangulyar  o'rtacha xato  +0.00%, maks |xato|   0.17%
  narx ~ haversine        korrelyatsiya 0.9395, 1 km uchun    3308 so'm
  narx ~ Evklid (gradus)  korrelyatsiya 0.9239, 1 km uchun    2731 so'm
  ⭐ Evklid(gradus) sharq-g'arb masofasini ~1/cos(41.3) = 1.33 marta oshiradi

Natija tahlili.

1-bo'lim — shaharlar orasidagi masofa (koordinatalar taxminiy, shuning uchun raqamlar ham taxminiy — masalan, Toshkent-Samarqand to'g'ri chiziqda ~269 km, yo'l bo'ylab esa ~300 km dan ortiq). Noldan yozilgan haversine sklearn.metrics.pairwise.haversine_distances bilan aynan mos. Gradus farqlaridan Evklid masofa har doim oshiradi: +17.8% (Samarqand, janubi-g'arb) dan +33.7% gacha (Nukus, deyarli g'arb). Xato yo'nalishga bog'liq: sharq-g'arb yo'nalishida (Namangan — azimut 99, Nukus — 282) eng katta, chunki aynan uzunlik gradusi "qisqaroq". Ekvirektangulyar proyeksiya esa hatto 800 km da ham 0.06% xato beradi.

2-bo'lim — sababi: bir gradus kenglik hamma joyda 111.2 km, bir gradus uzunlik esa ekvatorda 111.2, Toshkent kengligida 83.5, 60-kenglikda 55.6 km.

3-bo'lim — sintetik safarlar: 20 000 ta, olish nuqtalari oltita markaz atrofida (markaz, Chorsu tomoni, aeroport tomoni, turar joy massivlari). To'g'ri chiziq masofasi mediana 4.98 km, 95-persentil 13.37 km. Narx = boshlang'ich to'lov + yo'l km (to'g'ri chiziqdan 1.2-1.45 marta uzun) + tirbandlik koeffitsienti: mediana 23000 so'm, tirband soatlarda 1.30 barobar.

4-bo'lim — shahar ichida: gradusdagi Evklid o'rtacha +17.02%, eng ko'pi 33.29% xato beradi; ekvirektangulyar — 0.00% o'rtacha, 0.17% eng ko'pi. Narx bilan korrelyatsiya ham haversine uchun yuqoriroq (0.9395 va 0.9239) va "1 km narxi" to'g'ri chiqadi: haversine bo'yicha 3308 so'm (yo'l koeffitsienti bilan 2200 * ~1.33 * tirbandlik ga mos), Evklid bo'yicha esa noto'g'ri 2731 — chunki "kilometr" o'zi noto'g'ri o'lchangan. Ya'ni xato faqat masofada qolmaydi: undan hisoblangan har bir belgi va koeffitsient buziladi.

Misol 2 — Fazoviy indekslash: panjara, geohash va BallTree

python
"""Fazoviy indekslash: panjara, geohash noldan va BallTree(haversine)."""

import numpy as np
from sklearn.neighbors import BallTree

R_YER = 6371.0088
BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"


def haversine(lat1, lon1, lat2, lon2):
    f1, f2 = np.radians(lat1), np.radians(lat2)
    a = (np.sin((f2 - f1) / 2) ** 2
         + np.cos(f1) * np.cos(f2) * np.sin(np.radians(lon2 - lon1) / 2) ** 2)
    return 2 * R_YER * np.arcsin(np.sqrt(np.clip(a, 0, 1)))


def olish_nuqtalari(n, urug=0):
    """1-misoldagi sintetik taksi olish nuqtalari (Toshkent atrofi)."""
    rng = np.random.default_rng(urug)
    markazlar = np.array([[41.326, 69.228], [41.311, 69.279], [41.258, 69.281],
                          [41.365, 69.290], [41.285, 69.200], [41.230, 69.220]])
    k = rng.choice(6, n, p=[0.22, 0.28, 0.10, 0.15, 0.15, 0.10])
    return markazlar[k] + rng.normal(0, 0.012, (n, 2))


def geohash_kodla(lat, lon, uzunlik=7):
    """Kenglik va uzunlik oraliqlarini navbat bilan ikkiga bo'lish (bit
    interleaving): juft bit - uzunlik, toq bit - kenglik; 5 bit = 1 belgi."""
    lat_or, lon_or = [-90.0, 90.0], [-180.0, 180.0]
    kod, bit, qiymat, juft = [], 0, 0, True
    while len(kod) < uzunlik:
        oraliq, x = (lon_or, lon) if juft else (lat_or, lat)
        orta = (oraliq[0] + oraliq[1]) / 2
        if x >= orta:
            qiymat = qiymat * 2 + 1
            oraliq[0] = orta
        else:
            qiymat = qiymat * 2
            oraliq[1] = orta
        juft = not juft
        bit += 1
        if bit == 5:
            kod.append(BASE32[qiymat])
            bit, qiymat = 0, 0
    return "".join(kod)


def geohash_dekodla(kod):
    """Katak markazi va yarim o'lchamlari (gradus)."""
    lat_or, lon_or = [-90.0, 90.0], [-180.0, 180.0]
    juft = True
    for belgi in kod:
        q = BASE32.index(belgi)
        for i in range(4, -1, -1):
            oraliq = lon_or if juft else lat_or
            orta = (oraliq[0] + oraliq[1]) / 2
            if (q >> i) & 1:
                oraliq[0] = orta
            else:
                oraliq[1] = orta
            juft = not juft
    return ((lat_or[0] + lat_or[1]) / 2, (lon_or[0] + lon_or[1]) / 2,
            (lat_or[1] - lat_or[0]) / 2, (lon_or[1] - lon_or[0]) / 2)


def geohash_qoshnilar(kod):
    """8 ta qo'shni katak: markazni bir katakka siljitib qayta kodlash."""
    lat, lon, dlat, dlon = geohash_dekodla(kod)
    return sorted({geohash_kodla(lat + i * 2 * dlat, lon + j * 2 * dlon, len(kod))
                   for i in (-1, 0, 1) for j in (-1, 0, 1)} - {kod})


class Panjara:
    """Oddiy panjara indeksi: katak (i, j) -> nuqtalar ro'yxati."""

    def __init__(self, nuqtalar, katak_km, lat0=41.3):
        self.dlat = katak_km / 111.195
        self.dlon = katak_km / (111.195 * np.cos(np.radians(lat0)))
        self.nuqtalar = nuqtalar
        ij = self.katak(nuqtalar[:, 0], nuqtalar[:, 1])
        self.jadval = {}
        for n, (i, j) in enumerate(ij.tolist()):
            self.jadval.setdefault((i, j), []).append(n)

    def katak(self, lat, lon):
        return np.c_[np.floor(lat / self.dlat), np.floor(lon / self.dlon)].astype(int)

    def radius(self, lat, lon, r_km):
        """Katak o'lchami >= r bo'lsa, 3 x 3 qo'shni kataklar yetarli."""
        i, j = self.katak(np.array([lat]), np.array([lon]))[0]
        nomzod = [n for di in (-1, 0, 1) for dj in (-1, 0, 1)
                  for n in self.jadval.get((i + di, j + dj), [])]
        nomzod = np.array(nomzod, dtype=int)
        d = haversine(lat, lon, self.nuqtalar[nomzod, 0], self.nuqtalar[nomzod, 1])
        return np.sort(nomzod[d <= r_km]), len(nomzod)


def main() -> None:
    P = olish_nuqtalari(20_000)
    rng = np.random.default_rng(1)
    so_rov = P[rng.choice(len(P), 200, replace=False)] + rng.normal(0, 0.002, (200, 2))

    print("=== 1. Geohash noldan ===")
    lat, lon = 41.311, 69.279                         # Toshkent markazi (taxminiy)
    for u in [4, 5, 6, 7]:
        kod = geohash_kodla(lat, lon, u)
        la, lo, dla, dlo = geohash_dekodla(kod)
        print(f"  uzunlik {u}: {kod:<8} katak ~ {2 * dla * 111.195:6.2f} km x "
              f"{2 * dlo * 111.195 * np.cos(np.radians(lat)):6.2f} km, "
              f"markaz xatosi {1000 * haversine(lat, lon, la, lo):7.1f} m")
    kod6 = geohash_kodla(lat, lon, 6)
    print(f"  {kod6} ning qo'shnilari: {geohash_qoshnilar(kod6)}")
    la6, lo6, _, dlo6 = geohash_dekodla(kod6)
    a, b = (la6, lo6 - dlo6 - 0.00005), (la6, lo6 - dlo6 + 0.00005)  # chegara atrofi
    print(f"  ~{1000 * haversine(*a, *b):.0f} m oraliqdagi ikki nuqta: "
          f"{geohash_kodla(*a, 6)} va {geohash_kodla(*b, 6)}")
    print("  (chegaraning ikki tomonida - kodlar boshqa; shuning uchun radius"
          " so'rovida qo'shni kataklar ham tekshiriladi)")

    print("\n=== 2. Radius so'rovi (300 m), 200 so'rov, 20 000 nuqta ===")
    r_km = 0.3
    to_liq, jami_brute = [], 0
    for q in so_rov:
        d = haversine(q[0], q[1], P[:, 0], P[:, 1])
        to_liq.append(np.flatnonzero(d <= r_km))
        jami_brute += len(P)
    print(f"  to'liq ko'rib chiqish: {jami_brute} ta masofa hisoblandi")
    for katak in [0.3, 0.6, 1.2]:
        panjara = Panjara(P, katak)
        jami, mos = 0, True
        for q, t in zip(so_rov, to_liq):
            nat, nomzod = panjara.radius(q[0], q[1], r_km)
            jami += nomzod
            mos &= np.array_equal(nat, t)
        print(f"  panjara {katak:.1f} km: kataklar {len(panjara.jadval):>5}, "
              f"masofalar {jami:>8} ({jami / jami_brute:.4f}), natija bir xil: {mos}")
    daraxt = BallTree(np.radians(P), metric="haversine")
    daraxt.reset_n_calls()
    ind = daraxt.query_radius(np.radians(so_rov), r=r_km / R_YER)
    mos = all(np.array_equal(np.sort(i), t) for i, t in zip(ind, to_liq))
    print(f"  BallTree(haversine): masofalar {daraxt.get_n_calls():>8} "
          f"({daraxt.get_n_calls() / jami_brute:.4f}), natija bir xil: {mos}")
    soni = np.array([len(t) for t in to_liq])
    print(f"  300 m ichidagi nuqtalar: mediana {np.median(soni):.0f}, "
          f"min {soni.min()}, maks {soni.max()}")

    print("\n=== 3. Eng yaqin 5 qo'shni (BallTree) ===")
    daraxt.reset_n_calls()
    d, idx = daraxt.query(np.radians(so_rov), k=5)
    d_km = d * R_YER
    tekshir = np.sort(haversine(so_rov[0, 0], so_rov[0, 1], P[:, 0], P[:, 1]))[:5]
    print(f"  1-so'rov: BallTree {np.round(1000 * d_km[0], 1).tolist()} m")
    print(f"  1-so'rov: to'liq   {np.round(1000 * tekshir, 1).tolist()} m")
    print(f"  5-qo'shnigacha masofa: mediana {1000 * np.median(d_km[:, 4]):.0f} m, "
          f"90-persentil {1000 * np.quantile(d_km[:, 4], 0.9):.0f} m")
    print(f"  masofa hisoblari: {daraxt.get_n_calls()} (to'liq: {jami_brute})")
    print("  ⭐ BallTree radianda ishlaydi: kiritish np.radians, radius = km / R")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Geohash noldan ===
  uzunlik 4: tx37     katak ~  19.55 km x  29.36 km, markaz xatosi 16026.8 m
  uzunlik 5: tx370    katak ~   4.89 km x   3.67 km, markaz xatosi  2176.7 m
  uzunlik 6: tx3702   katak ~   0.61 km x   0.92 km, markaz xatosi   395.0 m
  uzunlik 7: tx3702r  katak ~   0.15 km x   0.11 km, markaz xatosi    39.4 m
  tx3702 ning qo'shnilari: ['tx36bp', 'tx36br', 'tx36bx', 'tx3700', 'tx3701', 'tx3703', 'tx3708', 'tx3709']
  ~8 m oraliqdagi ikki nuqta: tx3700 va tx3702
  (chegaraning ikki tomonida - kodlar boshqa; shuning uchun radius so'rovida qo'shni kataklar ham tekshiriladi)

=== 2. Radius so'rovi (300 m), 200 so'rov, 20 000 nuqta ===
  to'liq ko'rib chiqish: 4000000 ta masofa hisoblandi
  panjara 0.3 km: kataklar  2030, masofalar    36685 0.0092-bob, natija bir xil: True
  panjara 0.6 km: kataklar   626, masofalar   137241 0.0343-bob, natija bir xil: True
  panjara 1.2 km: kataklar   189, masofalar   420768 0.1052-bob, natija bir xil: True
  BallTree(haversine): masofalar   118218 0.0296-bob, natija bir xil: True
  300 m ichidagi nuqtalar: mediana 58, min 2, maks 186

=== 3. Eng yaqin 5 qo'shni (BallTree) ===
  1-so'rov: BallTree [58.4, 80.4, 83.5, 88.2, 99.7] m
  1-so'rov: to'liq   [58.4, 80.4, 83.5, 88.2, 99.7] m
  5-qo'shnigacha masofa: mediana 88 m, 90-persentil 176 m
  masofa hisoblari: 192901 (to'liq: 4000000)
  ⭐ BallTree radianda ishlaydi: kiritish np.radians, radius = km / R

Natija tahlili.

1-bo'lim — geohash noldan. Toshkent markazi tx3702r (7 belgi). Har belgi katakni 32 marta kichraytiradi: 4 belgi — 19.55 x 29.36 km, 6 belgi — 0.61 x 0.92 km, 7 belgi — 0.15 x 0.11 km (oxirgi ustun — nuqtadan uning katagi markazigacha masofa: 7 belgida 39.4 m). Kataklar kvadrat emas: toq va juft uzunlikda kenglik va uzunlikka turlicha bit tushadi. tx3702 ning 8 qo'shnisidan uchtasi (tx36bp, tx36br, tx36bx) boshqa prefiks bilan boshlanadi — ular 5-belgi darajasida boshqa katakda. Chegaraning ikki tomonidagi `8 m oraliqdagi ikki nuqta ham turli kod oldi (tx3700vatx3702`) — shuning uchun geohash bo'yicha qidiruvda qo'shni kataklar albatta qo'shiladi.

2-bo'lim — 200 ta radius so'rovi (300 m) 20 000 nuqtada. To'liq ko'rib chiqish — 4 000 000 masofa. Panjara 0.3 km kataklar bilan faqat 36685 ta masofa (0.92%) hisobladi — natija to'liq ko'rib chiqish bilan aynan bir xil. Katak kattalashgan sari nomzodlar ko'payadi: 1.2 km da 10.5%. BallTree 2.96% — panjaraning eng yaxshi sozlamasidan ko'proq, lekin u sozlashsiz ishlaydi va zichlik notekis bo'lsa ham barqaror. 300 m ichidagi nuqtalar soni juda farq qiladi: 2 dan 186 gacha — shahar zichligi notekis.

3-bo'lim — eng yaqin 5 qo'shni: BallTree va to'liq ko'rib chiqish aynan bir xil masofalarni berdi: [58.4, 80.4, 83.5, 88.2, 99.7] m. 5-qo'shnigacha masofa mediana 88 m — bu "mahalladagi talab" belgisi uchun tabiiy radius. kNN so'rovlari 192901 masofa hisobladi — to'liq ko'rib chiqishdan 20 marta kam.

Misol 3 — Nuqta-poligon, fazoviy belgilar va maqsadli kodlashdagi sizish

python
"""Nuqta-poligon (ray casting) noldan, fazoviy belgilar va maqsadli kodlashdagi sizish."""

import numpy as np
import pandas as pd
from matplotlib.path import Path
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, train_test_split

R_YER = 6371.0088
MARKAZ = (41.311, 69.279)

# SINTETIK tuman chegaralari (haqiqiy ma'muriy chegaralar EMAS): (lat, lon) uchlar
TUMANLAR = {
    "Markaz": [(41.290, 69.245), (41.335, 69.245), (41.335, 69.300), (41.312, 69.315),
               (41.290, 69.300)],
    "Shimol": [(41.335, 69.200), (41.420, 69.200), (41.420, 69.340), (41.335, 69.340),
               (41.335, 69.300), (41.335, 69.245)],
    "G'arb": [(41.230, 69.140), (41.335, 69.140), (41.335, 69.245), (41.290, 69.245),
              (41.290, 69.230), (41.230, 69.230)],
    "Janub": [(41.170, 69.230), (41.290, 69.230), (41.290, 69.300), (41.312, 69.315),
              (41.260, 69.340), (41.170, 69.340)],
}


def haversine(lat1, lon1, lat2, lon2):
    f1, f2 = np.radians(lat1), np.radians(lat2)
    a = (np.sin((f2 - f1) / 2) ** 2
         + np.cos(f1) * np.cos(f2) * np.sin(np.radians(lon2 - lon1) / 2) ** 2)
    return 2 * R_YER * np.arcsin(np.sqrt(np.clip(a, 0, 1)))


def ichidami(lat, lon, poligon):
    """Ray casting: nuqtadan sharqqa nur; chegarani toq marta kessa - ichkarida."""
    ichida = np.zeros(len(lat), dtype=bool)
    n = len(poligon)
    for i in range(n):
        (y1, x1), (y2, x2) = poligon[i], poligon[(i + 1) % n]
        kesadi = (y1 > lat) != (y2 > lat)                   # qirra nur balandligini kesadimi
        with np.errstate(divide="ignore", invalid="ignore"):
            x_kesish = x1 + (lat - y1) * (x2 - x1) / (y2 - y1)
        ichida ^= kesadi & (lon < x_kesish)
    return ichida


def safarlar(n, urug=0):
    """Sintetik safarlar: olish nuqtasi, soat va mijozning KUTISH vaqti (daqiqa)."""
    rng = np.random.default_rng(urug)
    markazlar = np.array([[41.326, 69.228], [41.311, 69.279], [41.258, 69.281],
                          [41.365, 69.290], [41.285, 69.200], [41.230, 69.220]])
    k = rng.choice(6, n, p=[0.22, 0.28, 0.10, 0.15, 0.15, 0.10])
    P = markazlar[k] + rng.normal(0, 0.012, (n, 2))
    soat = rng.integers(0, 24, n)
    d_markaz = haversine(P[:, 0], P[:, 1], *MARKAZ)
    # haydovchilar kam hududlar - silliq fazoviy maydon (sintetik)
    tanqis = np.array([[41.37, 69.31], [41.24, 69.19], [41.30, 69.24]])
    maydon = sum(3.0 * np.exp(-haversine(P[:, 0], P[:, 1], a, b) ** 2 / 2.0)
                 for a, b in tanqis)
    tirband = np.isin(soat, [7, 8, 9, 17, 18, 19])
    kutish = (3 + 0.35 * d_markaz + maydon + 2.5 * tirband
              + rng.gamma(2.0, 1.0, n))
    return pd.DataFrame({"lat": P[:, 0], "lon": P[:, 1], "soat": soat,
                         "kutish": kutish})


def maqsadli_kodlash(katak_train, y_train, katak_boshqa, silliq=20):
    """Katak bo'yicha o'rtacha (umumiy o'rtachaga qarab silliqlangan)."""
    umumiy = y_train.mean()
    g = pd.DataFrame({"k": katak_train, "y": y_train}).groupby("k")["y"].agg(["sum", "count"])
    qiymat = (g["sum"] + silliq * umumiy) / (g["count"] + silliq)
    return pd.Series(katak_boshqa).map(qiymat).fillna(umumiy).to_numpy()


def main() -> None:
    df = safarlar(20_000)

    print("=== 1. Nuqta-poligon: ray casting noldan ===")
    tuman = np.full(len(df), "tashqarida", dtype=object)
    for nom, pol in TUMANLAR.items():
        ich = ichidami(df.lat.to_numpy(), df.lon.to_numpy(), pol)
        mpl = Path([(lo, la) for la, lo in pol]).contains_points(df[["lon", "lat"]].to_numpy())
        tuman[ich] = nom
        print(f"  {nom:<7} nuqtalar {ich.sum():>6}, matplotlib bilan farq: "
              f"{int((ich != mpl).sum())}")
    df["tuman"] = tuman
    print("  fazoviy birlashtirish (spatial join) - tuman bo'yicha:")
    jad = df.groupby("tuman")["kutish"].agg(["count", "mean"]).sort_values("mean")
    for nom, q in jad.iterrows():
        print(f"    {nom:<11} safarlar {int(q['count']):>6}, o'rtacha kutish {q['mean']:5.2f} daq")

    print("\n=== 2. Fazoviy belgilar ===")
    df["d_markaz"] = haversine(df.lat, df.lon, *MARKAZ)
    f1, f2 = np.radians(MARKAZ[0]), np.radians(df.lat)
    dl = np.radians(df.lon - MARKAZ[1])
    burchak = np.arctan2(np.sin(dl) * np.cos(f2),
                         np.cos(f1) * np.sin(f2) - np.sin(f1) * np.cos(f2) * np.cos(dl))
    df["sin_b"], df["cos_b"] = np.sin(burchak), np.cos(burchak)   # burchak - aylana belgi
    dlat, dlon = 0.3 / 111.195, 0.3 / (111.195 * np.cos(np.radians(41.3)))
    df["katak"] = ((df.lat // dlat).astype(int).astype(str) + "_"
                   + (df.lon // dlon).astype(int).astype(str))
    df["zichlik"] = df.groupby("katak")["lat"].transform("size")
    print(f"  300 m kataklar: {df.katak.nunique()} ta, katakdagi safarlar mediana "
          f"{df.groupby('katak').size().median():.0f}, 1-3 safarli kataklar "
          f"{(df.groupby('katak').size() <= 3).mean():.2f} ulush")
    print(f"  korrelyatsiya bilan kutish: d_markaz {np.corrcoef(df.d_markaz, df.kutish)[0, 1]:.3f}, "
          f"zichlik {np.corrcoef(df.zichlik, df.kutish)[0, 1]:.3f}")

    print("\n=== 3. Maqsadli kodlash (katak bo'yicha): sizish ===")
    tr, te = train_test_split(df, test_size=0.3, random_state=0)
    ytr, yte = tr.kutish.to_numpy(), te.kutish.to_numpy()
    kf = KFold(5, shuffle=True, random_state=0)
    te_sizgan = maqsadli_kodlash(tr.katak, ytr, tr.katak, silliq=0)   # o'zi ham ichida!
    te_oof = np.zeros(len(tr))
    for a, b in kf.split(tr):
        te_oof[b] = maqsadli_kodlash(tr.katak.iloc[a], ytr[a], tr.katak.iloc[b])
    te_test = maqsadli_kodlash(tr.katak, ytr, te.katak)
    asos = ["soat"]
    toplamlar = {
        "bazaviy (o'rtacha)": None,
        "soat + lat/lon": asos + ["lat", "lon"],
        "+ d_markaz, burchak, tuman": asos + ["lat", "lon", "d_markaz", "sin_b", "cos_b", "tuman_k"],
        "+ TE sizgan (silliqsiz)": asos + ["lat", "lon", "te"],
        "+ TE out-of-fold": asos + ["lat", "lon", "te"],
    }
    tr = tr.assign(tuman_k=tr.tuman.map({n: i for i, n in enumerate(sorted(df.tuman.unique()))}))
    te = te.assign(tuman_k=te.tuman.map({n: i for i, n in enumerate(sorted(df.tuman.unique()))}))
    print(f"  {'belgilar':<28} {'CV RMSE':>8} {'test RMSE':>10} {'optimizm':>9}")
    natija = {}
    for nom, cols in toplamlar.items():
        if cols is None:
            cv = np.sqrt(np.mean((ytr - ytr.mean()) ** 2))
            ts = np.sqrt(np.mean((yte - ytr.mean()) ** 2))
            print(f"  {nom:<28} {cv:>8.3f} {ts:>10.3f} {ts - cv:>+9.3f}")
            continue
        Xtr = tr.assign(te=te_sizgan if "sizgan" in nom else te_oof)[cols].to_numpy()
        Xte = te.assign(te=te_test)[cols].to_numpy()
        xato = []
        for a, b in kf.split(Xtr):
            m = HistGradientBoostingRegressor(max_iter=150, random_state=0).fit(Xtr[a], ytr[a])
            xato.append(np.sqrt(np.mean((m.predict(Xtr[b]) - ytr[b]) ** 2)))
        m = HistGradientBoostingRegressor(max_iter=150, random_state=0).fit(Xtr, ytr)
        ts = np.sqrt(np.mean((m.predict(Xte) - yte) ** 2))
        natija[nom] = (np.mean(xato), ts)
        print(f"  {nom:<28} {np.mean(xato):>8.3f} {ts:>10.3f} {ts - np.mean(xato):>+9.3f}")
    print("  optimizm = test RMSE - CV RMSE (musbat - CV haddan tashqari yaxshi ko'rsatgan)")
    s, o = natija["+ TE sizgan (silliqsiz)"], natija["+ TE out-of-fold"]
    if s[0] < o[0] and s[1] > o[1]:
        print("  ⭐ sizgan TE: CV da eng yaxshi, testda yomonroq - maqsad o'zidan 'ko'rindi'")

    print("\n=== 4. Chiziqli model uchun fazoviy belgilar (test RMSE) ===")
    for d in (tr, te):
        d["tirband"] = d.soat.isin([7, 8, 9, 17, 18, 19]).astype(float)
    for nom, cols in [("tirband + lat/lon", ["tirband", "lat", "lon"]),
                      ("+ d_markaz", ["tirband", "lat", "lon", "d_markaz"]),
                      ("+ d_markaz, burchak", ["tirband", "lat", "lon", "d_markaz",
                                               "sin_b", "cos_b"])]:
        m = LinearRegression().fit(tr[cols], ytr)
        print(f"  {nom:<22} {np.sqrt(np.mean((m.predict(te[cols]) - yte) ** 2)):.3f}")
    print("  daraxt lat/lon dan o'zi 'masofa' ni o'rganadi; chiziqli model o'rgana olmaydi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Nuqta-poligon: ray casting noldan ===
  Markaz  nuqtalar   5511, matplotlib bilan farq: 0
  Shimol  nuqtalar   4054, matplotlib bilan farq: 0
  G'arb   nuqtalar   6972, matplotlib bilan farq: 0
  Janub   nuqtalar   2625, matplotlib bilan farq: 0
  fazoviy birlashtirish (spatial join) - tuman bo'yicha:
    Markaz      safarlar   5511, o'rtacha kutish  6.19 daq
    Janub       safarlar   2625, o'rtacha kutish  7.82 daq
    G'arb       safarlar   6972, o'rtacha kutish  8.07 daq
    Shimol      safarlar   4054, o'rtacha kutish  8.15 daq
    tashqarida  safarlar    838, o'rtacha kutish  9.60 daq

=== 2. Fazoviy belgilar ===
  300 m kataklar: 2030 ta, katakdagi safarlar mediana 6, 1-3 safarli kataklar 0.37 ulush
  korrelyatsiya bilan kutish: d_markaz 0.510, zichlik -0.283

=== 3. Maqsadli kodlash (katak bo'yicha): sizish ===
  belgilar                      CV RMSE  test RMSE  optimizm
  bazaviy (o'rtacha)              2.143      2.122    -0.021
  soat + lat/lon                  1.439      1.435    -0.004
  + d_markaz, burchak, tuman      1.439      1.433    -0.006
  + TE sizgan (silliqsiz)         1.373      1.564    +0.190
  + TE out-of-fold                1.439      1.438    -0.001
  optimizm = test RMSE - CV RMSE (musbat - CV haddan tashqari yaxshi ko'rsatgan)
  ⭐ sizgan TE: CV da eng yaxshi, testda yomonroq - maqsad o'zidan 'ko'rindi'

=== 4. Chiziqli model uchun fazoviy belgilar (test RMSE) ===
  tirband + lat/lon      1.753
  + d_markaz             1.474
  + d_markaz, burchak    1.470
  daraxt lat/lon dan o'zi 'masofa' ni o'rganadi; chiziqli model o'rgana olmaydi

Natija tahlili.

1-bo'lim — ray casting noldan to'rtta sintetik tuman poligonida (ikkitasi qavariq emas) matplotlib.path.Path.contains_points bilan nol farq berdi. Fazoviy birlashtirish natijasi: Markaz tumanida o'rtacha kutish eng kam (6.19 daqiqa), Shimol va G'arbda 8.07-8.15, poligonlardan tashqaridagi 838 safarda — 9.60. "Tashqarida" qatori muhim: haqiqiy chegaralarda ham bo'shliqlar va ustma-ust tushishlar bo'ladi — ularni alohida sanang, yo'qotib qo'ymang.

2-bo'lim — fazoviy belgilar. Markazgacha masofa kutish bilan kuchli bog'liq (0.510), zichlik — manfiy (-0.283: gavjum joyda haydovchi ko'p). 300 m kataklar 2030 ta, lekin katakdagi safarlar mediana 6 va kataklarning 37% ida 1-3 ta safar — maqsadli kodlash uchun xavfli sharoit.

3-bo'lim — HistGB bilan belgilar to'plamlari (5-foldli CV va alohida test):

  • Bazaviy (o'rtacha) 2.122 → soat + lat/lon 1.435. Daraxt koordinatalardan fazoviy tuzilmani o'zi o'rgandi.
  • d_markaz, burchak (sin/cos) va tuman qo'shilishi daraxtga deyarli hech narsa bermadi (1.433) — 14 000 qatorda lat/lon yetarli. Bu halol natija: muhandislik qilingan belgilar har doim ham yordam bermaydi.
  • Sizgan TE (katak o'rtachasi qatorning o'zi bilan, silliqlashsiz): CV da eng yaxshi (1.373), testda esa eng yomon modellardan (1.564) — optimizm +0.190. Kichik katakda qatorning o'z maqsadi uning belgisiga kirgan, model uni "ko'rgan".
  • Out-of-fold TE (silliqlash bilan): CV 1.439, test 1.438 — optimizm -0.001, halol baho. Foyda bermadi, lekin zarar ham yo'q.

4-bo'lim — chiziqli model uchun esa fazoviy belgilar hal qiluvchi: tirband + lat/lon bilan test RMSE 1.753, d_markaz qo'shilganda 1.474 — chunki "markazdan uzoqlashgan sari kutish oshadi" bog'liqligi lat va lon ga chiziqli emas. Burchak qo'shimcha deyarli bermadi (1.470).

Misol 4 — Fazoviy avtokorrelyatsiya, fazoviy CV va DBSCAN issiq nuqtalar

python
"""Fazoviy avtokorrelyatsiya (Moran I), fazoviy CV va DBSCAN(haversine) issiq nuqtalar."""

import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import GroupKFold, KFold
from sklearn.neighbors import BallTree, KNeighborsRegressor

R_YER = 6371.0088


def km_xy(lat, lon, lat0=41.3, lon0=69.25):
    """Lokal ekvirektangulyar proyeksiya (km) - shahar miqyosida yetarli."""
    x = np.radians(lon - lon0) * np.cos(np.radians(lat0)) * R_YER
    y = np.radians(lat - lat0) * R_YER
    return np.c_[x, y]


def malumot(n, urug=0):
    """Olish nuqtalari va kutish vaqti: silliq, lekin NOMA'LUM fazoviy maydon."""
    rng = np.random.default_rng(urug)
    markazlar = np.array([[41.326, 69.228], [41.311, 69.279], [41.258, 69.281],
                          [41.365, 69.290], [41.285, 69.200], [41.230, 69.220]])
    k = rng.choice(6, n, p=[0.22, 0.28, 0.10, 0.15, 0.15, 0.10])
    P = markazlar[k] + rng.normal(0, 0.012, (n, 2))
    # issiq nuqtalar (sintetik, taxminiy joylar): vokzal, bozor, aeroport
    issiq = np.array([[41.2925, 69.2870], [41.3265, 69.2355], [41.2600, 69.2810]])
    h = rng.random(n) < 0.12
    P[h] = issiq[rng.integers(0, 3, h.sum())] + rng.normal(0, 0.0006, (h.sum(), 2))
    xy = km_xy(P[:, 0], P[:, 1])
    maydon_rng = np.random.default_rng(123)                # maydon har doim bir xil
    c = maydon_rng.uniform(xy.min(0) - 1, xy.max(0) + 1, (80, 2))
    amp = maydon_rng.normal(0, 1.5, 80)
    maydon = sum(a * np.exp(-((xy - ci) ** 2).sum(1) / (2 * 0.8 ** 2))
                 for a, ci in zip(amp, c))
    soat = rng.integers(0, 24, n)
    tirband = np.isin(soat, [7, 8, 9, 17, 18, 19]).astype(float)
    y = 6 + maydon + 2 * tirband + rng.normal(0, 1.0, n)
    return P, xy, np.c_[xy, tirband], y


def moran_i(qiymat, xy, k=10, permutatsiya=99, urug=0):
    """Moran I: k eng yaqin qo'shni (teng vazn). ~0 - tasodifiy, >0 - o'xshashlar yaqin."""
    z = qiymat - qiymat.mean()
    _, idx = BallTree(xy).query(xy, k=k + 1)
    qoshni = idx[:, 1:]                                       # o'zini tashlaymiz

    def hisobla(zz):
        return len(zz) * np.sum(zz[:, None] * zz[qoshni]) / (qoshni.size * np.sum(zz ** 2))

    I = hisobla(z)
    rng = np.random.default_rng(urug)
    tasodif = np.array([hisobla(rng.permutation(z)) for _ in range(permutatsiya)])
    p = (1 + np.sum(tasodif >= I)) / (permutatsiya + 1)
    return I, tasodif.mean(), p


def rmse(a, b):
    return float(np.sqrt(np.mean((a - b) ** 2)))


def cv_rmse(model_fn, X, y, bolinish, groups=None):
    xato = []
    for a, b in bolinish.split(X, y, groups):
        m = model_fn().fit(X[a], y[a])
        xato.append(rmse(m.predict(X[b]), y[b]))
    return float(np.mean(xato))


def main() -> None:
    P, xy, X, y = malumot(12_000)

    print("=== 1. Fazoviy avtokorrelyatsiya: Moran I (k = 10, 3000 nuqta) ===")
    tanlov = np.random.default_rng(1).choice(len(y), 3000, replace=False)
    for nom, q in [("kutish vaqti", y[tanlov]),
                   ("aralashtirilgan kutish", np.random.default_rng(2).permutation(y[tanlov]))]:
        I, kutilgan, p = moran_i(q, xy[tanlov])
        print(f"  {nom:<24} I = {I:6.3f}  (tasodifiy ~ {kutilgan:+.3f}), p = {p:.2f}")

    print("\n=== 2. Tasodifiy KFold va fazoviy blokli CV ===")
    blok = 1.5                                                 # km
    guruh = (np.floor(xy[:, 0] / blok) * 1000 + np.floor(xy[:, 1] / blok)).astype(int)
    print(f"  {blok} km bloklar: {len(np.unique(guruh))} ta")
    # "haqiqiy" savol: xizmat YANGI hududlarga chiqadi - bloklarning 20% i butunlay yangi
    bloklar = np.unique(guruh)
    yangi = np.random.default_rng(3).choice(bloklar, len(bloklar) // 5, replace=False)
    test = np.isin(guruh, yangi)
    Xtr, ytr, gtr = X[~test], y[~test], guruh[~test]
    print(f"  o'quv: {len(ytr)} safar, yangi hudud (test): {int(test.sum())} safar")
    modellar = {
        "KNN (k=10)": lambda: KNeighborsRegressor(10),
        "HistGB": lambda: HistGradientBoostingRegressor(max_iter=150, random_state=0),
    }
    print(f"  {'model':<11} {'tasodifiy KFold':>16} {'blokli CV':>10} "
          f"{'yangi hudud':>12} {'bazaviy':>8}")
    natija = {}
    for nom, fn in modellar.items():
        r_kf = cv_rmse(fn, Xtr, ytr, KFold(5, shuffle=True, random_state=0))
        r_bl = cv_rmse(fn, Xtr, ytr, GroupKFold(5), gtr)
        r_te = rmse(fn().fit(Xtr, ytr).predict(X[test]), y[test])
        natija[nom] = (r_kf, r_bl, r_te)
        print(f"  {nom:<11} {r_kf:>16.3f} {r_bl:>10.3f} {r_te:>12.3f} "
              f"{rmse(np.full(test.sum(), ytr.mean()), y[test]):>8.3f}")
    print("  yangi hudud - o'quvda umuman bo'lmagan bloklardagi HAQIQIY xato")
    for nom, (r_kf, r_bl, r_te) in natija.items():
        print(f"  {nom:<11} optimizm: tasodifiy KFold {r_te - r_kf:+.3f}, "
              f"blokli {r_te - r_bl:+.3f}")

    print("  KNN, 8 xil 'yangi hudud' tanlovi - optimizm (yangi hudud - CV):")
    opt_kf, opt_bl = [], []
    for s in range(8):
        yangi = np.random.default_rng(100 + s).choice(bloklar, len(bloklar) // 5,
                                                      replace=False)
        tt = np.isin(guruh, yangi)
        fn = modellar["KNN (k=10)"]
        r_te = rmse(fn().fit(X[~tt], y[~tt]).predict(X[tt]), y[tt])
        opt_kf.append(r_te - cv_rmse(fn, X[~tt], y[~tt], KFold(5, shuffle=True, random_state=0)))
        opt_bl.append(r_te - cv_rmse(fn, X[~tt], y[~tt], GroupKFold(5), guruh[~tt]))
    for nom, o in [("tasodifiy KFold", opt_kf), ("blokli CV", opt_bl)]:
        o = np.array(o)
        print(f"    {nom:<16} {o.mean():+.3f}, SE {o.std(ddof=1) / np.sqrt(len(o)):.3f}")
    d = np.array(opt_kf) - np.array(opt_bl)
    se = d.std(ddof=1) / np.sqrt(len(d))
    print(f"    farq (juftlashgan): {d.mean():+.3f}, SE {se:.3f}, "
          f"sezilarli: {abs(d.mean()) > 2 * se}")

    print("\n=== 3. Qoldiqlarda avtokorrelyatsiya (HistGB, tasodifiy KFold) ===")
    q = np.zeros(len(ytr))
    for a, b in KFold(5, shuffle=True, random_state=0).split(Xtr):
        q[b] = ytr[b] - modellar["HistGB"]().fit(Xtr[a], ytr[a]).predict(Xtr[b])
    t2 = np.random.default_rng(4).choice(len(q), 3000, replace=False)
    I, kutilgan, p = moran_i(q[t2], Xtr[t2, :2])
    print(f"  qoldiqlar: I = {I:.3f} (tasodifiy ~ {kutilgan:+.3f}), p = {p:.2f}")

    print("\n=== 4. Issiq nuqtalar: DBSCAN(haversine), eps = 100 m, min_samples = 50 ===")
    db = DBSCAN(eps=0.10 / R_YER, min_samples=50, metric="haversine",
                algorithm="ball_tree").fit(np.radians(P))
    belgi = db.labels_
    klaster = sorted(set(belgi) - {-1}, key=lambda c: -np.sum(belgi == c))
    print(f"  klasterlar: {len(klaster)}, shovqin nuqtalar ulushi {np.mean(belgi == -1):.3f}")
    for i, c in enumerate(klaster[:6]):
        m = P[belgi == c].mean(0)
        print(f"    #{i}: {np.sum(belgi == c):>5} nuqta, markaz ({m[0]:.4f}, {m[1]:.4f})")
    if len(klaster) > 6:
        print(f"    (+{len(klaster) - 6} ta)")

    print("\n=== 5. ASCII zichlik xaritasi (shimol tepada) ===")
    qator, ustun = 16, 44
    lat_ch = np.linspace(P[:, 0].min(), P[:, 0].max(), qator + 1)
    lon_ch = np.linspace(P[:, 1].min(), P[:, 1].max(), ustun + 1)
    H, _, _ = np.histogram2d(P[:, 0], P[:, 1], bins=[lat_ch, lon_ch])
    belgilar = " -:=+*#%@"
    darajalar = np.quantile(H[H > 0], np.linspace(0, 1, len(belgilar))[1:-1])
    for r in range(qator - 1, -1, -1):
        satr = "".join(" " if H[r, c] == 0 else belgilar[1 + np.searchsorted(darajalar, H[r, c])]
                       if np.searchsorted(darajalar, H[r, c]) < len(belgilar) - 1 else "@"
                       for c in range(ustun))
        print("  |" + satr + "|")
    print(f"  kenglik {lat_ch[0]:.3f}-{lat_ch[-1]:.3f}, uzunlik {lon_ch[0]:.3f}-{lon_ch[-1]:.3f}")
    print("  ⭐ yaqin nuqtalar o'xshash -> tasodifiy CV sizadi; yangi hudud uchun blokli CV")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Fazoviy avtokorrelyatsiya: Moran I (k = 10, 3000 nuqta) ===
  kutish vaqti             I =  0.330  (tasodifiy ~ -0.001), p = 0.01
  aralashtirilgan kutish   I = -0.009  (tasodifiy ~ -0.000), p = 0.84

=== 2. Tasodifiy KFold va fazoviy blokli CV ===
  1.5 km bloklar: 116 ta
  o'quv: 9587 safar, yangi hudud (test): 2413 safar
  model        tasodifiy KFold  blokli CV  yangi hudud  bazaviy
  KNN (k=10)             1.056      1.149        1.094    1.625
  HistGB                 1.040      1.151        1.075    1.625
  yangi hudud - o'quvda umuman bo'lmagan bloklardagi HAQIQIY xato
  KNN (k=10)  optimizm: tasodifiy KFold +0.038, blokli -0.055
  HistGB      optimizm: tasodifiy KFold +0.035, blokli -0.076
  KNN, 8 xil 'yangi hudud' tanlovi - optimizm (yangi hudud - CV):
    tasodifiy KFold  +0.076, SE 0.022
    blokli CV        -0.023, SE 0.027
    farq (juftlashgan): +0.100, SE 0.007, sezilarli: True

=== 3. Qoldiqlarda avtokorrelyatsiya (HistGB, tasodifiy KFold) ===
  qoldiqlar: I = -0.019 (tasodifiy ~ +0.001), p = 1.00

=== 4. Issiq nuqtalar: DBSCAN(haversine), eps = 100 m, min_samples = 50 ===
  klasterlar: 3, shovqin nuqtalar ulushi 0.871
    #0:   533 nuqta, markaz (41.3265, 69.2355)
    #1:   520 nuqta, markaz (41.2600, 69.2810)
    #2:   491 nuqta, markaz (41.2925, 69.2869)

=== 5. ASCII zichlik xaritasi (shimol tepada) ===
  |                         ----: :=--::-  -   |
  |                       - -=:++####***:::-:  |
  |                     - -:++*#%@@@@@%%#++::- |
  |           :::+::=-::  -::=##%%@@@%%%**==:  |
  |       :-::++*#%#%#***===:+=+*####**++=:   -|
  |    -  -==*#%%@@@@@@%%**+**%%%%%#+*+==- :   |
  |     --::+*%%@@@@@@@@%*#%@@@@@@@@@%#*+:- -  |
  |--::=++*#######%%%##**+*#%@@@@@@@@%%++:--   |
  | =:++#%@@@@@%%#*+==: :==**#%%@@@@%#++--=    |
  |:-:**#@@@@@@%%#**=-  --==:********+=--      |
  |-:==++**####**++=:    =:++##%%@%%%*+::: -   |
  |  -  -::=====+++==+:=-:==*##%%@%%#*++=::    |
  |    :--::***%%#%%#**+- :=:++##****=+=::-    |
  |    -- -=+##%%%@@%#*=+-   -=:-::=-::        |
  |       =:++#*###**++=-      -     -         |
  |         : =:=+::-:---                      |
  kenglik 41.195-41.405, uzunlik 69.163-69.334
  ⭐ yaqin nuqtalar o'xshash -> tasodifiy CV sizadi; yangi hudud uchun blokli CV

Natija tahlili.

Kutish vaqti bu yerda noma'lum silliq fazoviy maydondan (80 ta tasodifiy "tepalik", ~0.8 km o'lchamda), tirbandlik va shovqindan iborat — xuddi haqiqiy hayotdagidek, uning shaklini oldindan bilmaymiz.

1-bo'lim — Moran I (10 ta eng yaqin qo'shni, 3000 nuqta): kutish vaqti uchun I = 0.330, 99 ta permutatsiyada birortasi ham bunga yetmadi (p = 0.01 — 99 permutatsiyada mumkin bo'lgan eng kichik qiymat). Xuddi shu qiymatlarni joylar orasida aralashtirsak — I = -0.009, p = 0.84: tuzilma yo'qoladi. Demak yaqin safarlarning kutish vaqti haqiqatan o'xshash.

2-bo'lim — asosiy savol: xizmat yangi hududlarga chiqsa, model qanchalik yaxshi? 1.5 km bloklarning 20% i butunlay yangi hudud sifatida ajratiladi (2413 safar), qolganida (9587) model o'qitiladi va ikki xil CV qilinadi. Bitta tanlovda: KNN uchun tasodifiy KFold 1.056, blokli CV 1.149, haqiqiy yangi hudud xatosi 1.094. Bu tanlovda blokli CV biroz pessimistik chiqdi (-0.055), tasodifiy esa optimistik (+0.038) — bitta tanlov shovqinli, shuning uchun 8 xil "yangi hudud" tanlovida takrorlaymiz: tasodifiy KFold optimizmi o'rtacha +0.076 (SE 0.022), blokli CV — -0.023 (SE 0.027, noldan farqi sezilarli emas). Juftlashgan farq +0.100, SE 0.007 — aniq sezilarli. Tasodifiy KFold yangi hudud xatosini muntazam kam ko'rsatadi, blokli CV esa uni siljishsiz baholaydi. Bazaviy (o'rtacha) 1.625 — ikkala model ham undan ancha yaxshi, ya'ni yangi hududda ham fazoviy tuzilmaning bir qismi (qo'shni bloklardan) o'tadi.

3-bo'lim — HistGB qoldiqlarida Moran I -0.019 (p = 1.00) — model fazoviy tuzilmani ushlagan, qoldiqlarda avtokorrelyatsiya qolmagan. E'tibor bering: bu ham tasodifiy KFold qoldiqlari; qoldiq tozaligi baholash dizaynining to'g'riligini isbotlamaydi — 2-bo'lim ko'rsatganidek, optimizm baribir bor.

4-bo'lim — DBSCAN (haversine, eps = 100 m, min_samples = 50) uchta issiq nuqtani topdi — sintetik vokzal, bozor va aeroport (491-533 nuqta), markazlari ular qo'yilgan joylarga to'rtinchi xona aniqligida mos. Qolgan 87.1% safar — shovqin: ular zich emas, tarqoq talab. KMeans bu nuqtalarni ham majburan klasterlarga bo'lib yuborgan bo'lardi (16.5-dars).

5-bo'lim — ASCII zichlik xaritasi (shimol tepada, fon — bo'sh joy, zichlik - dan @ gacha kvantillar bo'yicha). Bir nechta zich markaz va ular orasidagi siyrak hududlar ko'rinadi. Issiq nuqtalar (70 m) bu masshtabda (500 m katak) alohida ko'rinmaydi — DBSCAN esa ularni aniq ajratdi: xarita va klasterlash bir-birini to'ldiradi.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"lat va lon — oddiy sonlar, Evklid yetarli" Toshkentda sharq-g'arb masofasi 1.33 marta oshadi (1-misol: +17.8% dan +33.7% gacha)
"Haversine — yo'l masofasi" Qush uchishi; shaharda yo'l 1.2-1.5 marta uzun
"Tekis proyeksiya har doim xato" Shahar ichida ekvirektangulyar xatosi 0.17% dan kam
"Geohash prefiksi bir xil — nuqtalar yaqin, farqli — uzoq" Chegara atrofida 8 m oraliqdagi nuqtalar turli kod oladi
"BallTree ga gradus beraman" haversine — radianda; radius = km / R
"Muhandislik qilingan fazoviy belgilar har doim yordam beradi" Daraxtga deyarli bermadi; chiziqli modelga — ko'p (3-misol)
"Katak bo'yicha o'rtacha — xavfsiz belgi" O'z qatori bilan hisoblansa sizadi (optimizm +0.190)
"Tasodifiy KFold — standart, har doim to'g'ri" Yangi hudud uchun optimistik (+0.076, 8 tanlov)
"Qoldiqlarda Moran I ~ 0 — baholash to'g'ri" Qoldiq tozaligi CV dizaynini oqlamaydi

6. Keng tarqalgan xatolar va yechimlari

1. Gradusdagi Evklid

python
d = np.hypot(lat2 - lat1, lon2 - lon1) * 111                           # ⚠️
d = haversine(lat1, lon1, lat2, lon2)                                  # ✅

2. BallTree ga gradus

python
BallTree(P, metric="haversine").query_radius(Q, r=0.3)                 # ⚠️ gradus, km
BallTree(np.radians(P), metric="haversine").query_radius(np.radians(Q), r=0.3 / 6371)  # ✅

3. Koordinata tartibi

python
Point(lat, lon)                                                        # ⚠️ shapely (x, y)
Point(lon, lat)                                                        # ✅

4. Azimut oddiy son sifatida

python
df["burchak"] = azimut                                                 # ⚠️ 359 va 1 - "uzoq"
df["sin_b"], df["cos_b"] = np.sin(np.radians(azimut)), np.cos(np.radians(azimut))   # ✅

5. Sizgan maqsadli kodlash

python
df["te"] = df.groupby("katak")["y"].transform("mean")                  # ⚠️ o'zi ham ichida
# har fold uchun faqat o'quv qismidan, silliqlash bilan                  # ✅ out-of-fold

6. Yangi hudud uchun tasodifiy CV

python
cross_val_score(model, X, y, cv=KFold(5, shuffle=True))               # ⚠️
cross_val_score(model, X, y, cv=GroupKFold(5), groups=blok)            # ✅

7. DBSCAN eps gradusda

python
DBSCAN(eps=0.001).fit(P)                                               # ⚠️ gradus, Evklid
DBSCAN(eps=0.1 / 6371, metric="haversine").fit(np.radians(P))          # ✅ 100 m

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 16.5-dars (o'tilgan): DBSCAN — issiq nuqtalar haversine bilan
  • 17.5, 17.8-darslar (o'tilgan): Aylana belgilar (sin/cos) va sizish — azimut va maqsadli kodlash
  • 18-qism (o'tilgan): Baholash dizayni — fazoviy blokli CV GroupKFold bilan
  • 11-qism (o'tilgan): Permutatsiya testi — Moran I ning p-qiymati
  • 28.10-dars: Katta ma'lumot bilan ishlash — millionlab safarlar; geohash kaliti bo'yicha guruhlash
  • 28.11-dars: Sababiy xulosa — "yangi metro bekati atrofida talab oshdimi" kabi fazoviy savollar
  • Amalda: taksi va yetkazib berish, ko'chmas mulk narxi, do'kon joyini tanlash, bank filiallari, epidemiologiya, qishloq xo'jaligi

8. Eng yaxshi amaliyotlar

  1. Masofa uchun haversine yoki to'g'ri proyeksiya; gradusda Evklid hech qachon.

  2. Koordinata tartibini (lat, lon yoki lon, lat) har funksiyada tekshiring; CRS ni yozib qo'ying.

  3. Qidiruv uchun indeks: BallTree (radian), panjara yoki geohash + qo'shnilar.

  4. Fazoviy belgilar: masofalar, azimut (sin/cos), tuman, zichlik; maqsadli kodlash faqat out-of-fold.

  5. Moran I bilan avtokorrelyatsiyani o'lchang — maqsadda va qoldiqlarda.

  6. Baholash savolga mos: yangi hudud — blokli CV; blok o'lchami >= avtokorrelyatsiya masofasi.

  7. Issiq nuqtalar uchun DBSCAN, eps metrda o'ylab tanlang.

  8. Joylashuv — shaxsiy ma'lumot: aniq koordinatalarni saqlashda va chop etishda ehtiyot bo'ling (yaxlitlash, agregatsiya).


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # Toshkentda 1 gradus uzunlik necha km?
2.  # gradusdagi Evklid qaysi yo'nalishda eng ko'p xato qiladi?
3.  # ekvirektangulyar proyeksiya shahar ichida qancha xato beradi?
4.  # BallTree(haversine) qanday birlikda ishlaydi?
5.  # geohash uzunligi 6 - katak o'lchami?
6.  # nega geohash qidiruvida qo'shni kataklar kerak?
7.  # ray casting: nur chegarani 3 marta kesdi - nuqta qayerda?
8.  # azimutni qanday belgi qilish kerak?
9.  # sizgan TE da CV va test qanday bo'ladi?
10. # Moran I = 0.33 nimani bildiradi?
11. # yangi hudud uchun tasodifiy KFold?
12. # DBSCAN eps = 100 m ni qanday berasiz?
Javoblar
  1. 111.2 * cos(41.3) ~ 83.5 km
  2. Sharq-g'arb (uzunlik bo'ylab): +33.7% gacha
  3. 0.17% dan kam (1-misol)
  4. Radian: kirish np.radians, natija * R km
  5. Toshkentda ~`0.61 x 0.92` km
  6. Chegara atrofidagi yaqin nuqtalar turli kod oladi
  7. Ichkarida (toq)
  8. sin va cos
  9. CV eng yaxshi (1.373), test yomon (1.564)
  10. Yaqin safarlarning kutish vaqti o'xshash (kuchli musbat avtokorrelyatsiya)
  11. Optimistik (+0.076, SE 0.022)
  12. eps = 0.1 / 6371, metric="haversine", kirish radianda

Vazifa 2: Xatolarni tuzating

python
1.  d_km = np.hypot(df.lat2 - df.lat1, df.lon2 - df.lon1) * 111

2.  tree = BallTree(df[["lat", "lon"]].values, metric="haversine")
    ind = tree.query_radius(q, r=0.5)                      # 500 m

3.  df["yonalish"] = azimut(df.lat, df.lon, 41.31, 69.28)

4.  df["katak_orta"] = df.groupby("katak")["narx"].transform("mean")
    cross_val_score(model, df[belgilar], df.narx, cv=5)

5.  cross_val_score(model, X, y, cv=KFold(5, shuffle=True))   # yangi shaharga chiqamiz
Javoblar
python
1.  d_km = haversine(df.lat1, df.lon1, df.lat2, df.lon2)

2.  tree = BallTree(np.radians(df[["lat", "lon"]].values), metric="haversine")
    ind = tree.query_radius(np.radians(q), r=0.5 / 6371.0088)

3.  a = np.radians(azimut(df.lat, df.lon, 41.31, 69.28))
    df["sin_b"], df["cos_b"] = np.sin(a), np.cos(a)

4.  # har fold ichida: o'quv qismidan silliqlangan o'rtacha, test qismiga map
    # (yoki sklearn TargetEncoder - ichki cross-fitting bilan)

5.  cross_val_score(model, X, y, cv=GroupKFold(5), groups=blok)  # yoki shahar bo'yicha

Vazifa 3: Masofa

Modellang (1-misol asosida):

  1. Vincenty yoki ellipsoid formulasini qidirib, haversine bilan Toshkent-Nukus uchun solishtiring (farq ~0.3% atrofida bo'lishi kerak)
  2. Ekvirektangulyar xatosini masofa (1, 10, 100, 1000 km) va kenglik (0, 41, 70) bo'yicha jadval qiling
  3. Narx modelida yol / haversine nisbatini tuman bo'yicha baholang
  4. Azimut bo'yicha safarlar taqsimotini 8 yo'nalishga (Sh, ShSh, ...) bo'lib sanang

Vazifa 4: Indeks

Modellang (2-misol asosida):

  1. Geohash uzunligi 6 bilan radius so'rovini yozing (o'z katagi + 8 qo'shni) va natijani BallTree bilan solishtiring
  2. Nuqtalar sonini 200 000 ga oshiring — to'liq ko'rib chiqish, panjara va BallTree masofa hisoblari qanday o'sadi?
  3. "Har safar uchun 500 m ichidagi o'tgan safarlar soni" belgisini BallTree bilan hisoblang
  4. Panjara katagini 0.15 km qiling — 3 x 3 yetarlimi? Nega?

Vazifa 5: Poligon va belgilar

Modellang (3-misol asosida):

  1. Teshikli poligon (masalan, bog') uchun ray casting ni kengaytiring
  2. Nuqta aynan qirra ustida bo'lsa natija qanday? Qoida kiriting
  3. Eng yaqin "metro bekati" (5 ta sintetik nuqta) gacha masofa belgisini qo'shing — HistGB va chiziqli modelda ta'siri
  4. sklearn.preprocessing.TargetEncoder bilan out-of-fold TE ni takrorlang

Vazifa 6: Fazoviy CV va klasterlar

Modellang (4-misol asosida):

  1. Blok o'lchamini 0.5, 1.5, 3, 5 km qiling — blokli CV optimizmi qanday o'zgaradi?
  2. Maydonning o'lcham ko'lamini (0.8 km) 0.3 va 2 km qiling — Moran I va optimizm
  3. DBSCAN eps va min_samples ni o'zgartirib, issiq nuqtalar soni qanday o'zgarishini ko'ring; HDBSCAN bilan solishtiring
  4. Mahalliy Moran (LISA) ni yozing: har nuqta uchun z_i * mean(z_qo'shni) — qaysi joylar "issiq"?

Vazifa 7: O'ylash

Yetkazib berish kompaniyasi: "Toshkentda yetkazib berish vaqtini bashorat qilish modelimiz CV da RMSE 4.2 daqiqa berdi. Keyingi oy Samarqandda ishga tushiramiz — o'sha model bilan, xato ham shunday bo'ladi." Nima deysiz?

Javob

Qisqa javob: 4.2 daqiqa — Toshkentdagi ma'lum hududlar uchun baho; Samarqand — yangi hudud, u yerdagi xato bu raqamdan katta bo'lishi deyarli aniq.

1. Qanday CV qilingan? Agar tasodifiy KFold bo'lsa, har test buyurtmasining qo'shnilari o'quvda bo'lgan. 4-misolda bunday baho yangi hudud xatosini muntazam kam ko'rsatdi (+0.076, 8 tanlovda). Yangi shahar uchun esa farq ancha katta bo'ladi: boshqa ko'chalar tarmog'i, boshqa tirbandlik, boshqa zichlik.

2. Belgilar ko'chadimi? lat va lon Samarqandda umuman boshqa qiymatlar — daraxt model ular uchun eng chetdagi bargni ishlatadi (ekstrapolyatsiya yo'q). Ko'chadigan belgilar — nisbiy: masofa (haversine, km), shahar markazigacha masofa, zichlik, soat, tirbandlik. "Toshkent katagi TE" kabi belgilar Samarqandda ma'nosiz.

3. Qanday tekshirish.

python
# 1) belgilarni shaharga bog'liq bo'lmagan qilish: masofalar, zichlik, vaqt
# 2) fazoviy blokli CV (yoki bir nechta shahar bo'lsa - shahar bo'yicha GroupKFold)
# 3) Samarqandda birinchi haftalar: kichik pilot, haqiqiy xatoni o'lchash
# 4) monitoring va qayta o'qitish (27.11-27.13), mahalliy ma'lumot to'plangach

Rahbarga javob: "4.2 daqiqa — Toshkentning o'zi uchun. Samarqand uchun xato kattaroq bo'ladi; qanchaligini fazoviy CV bilan baholaymiz, belgilarni shaharga bog'liq bo'lmagan qilamiz va pilot davrida haqiqiy xatoni o'lchab, mahalliy ma'lumot bilan qayta o'qitamiz."

Nimani mustahkamlaydi: 2.2, 2.5, 2.7-bo'limlar.


Xulosa

Bu darsda geografik ma'lumot bilan ishlashni noldan qurdik.

Eng muhim uch fikr:

  1. Koordinata — tekis son emas. Gradus farqlaridan Evklid masofa Toshkentdan boshqa shaharlargacha +17.8% dan +33.7% gacha xato berdi va undan hisoblangan "1 km narxi" ni ham buzdi (2731 o'rniga 3308 so'm). Haversine noldan sklearn bilan aynan mos, shahar ichida esa ekvirektangulyar proyeksiya (0.17% dan kam xato) tekis koordinatalar beradi.

  2. Indeks va belgilar — tez va halol. Panjara 300 m radius so'rovlarini to'liq ko'rib chiqishning 0.92% masofa hisobi bilan, BallTree 2.96% bilan aynan bir xil natija bilan bajardi; geohash chegarasida 8 m oraliqdagi nuqtalar turli kod oldi. Ray casting matplotlib bilan nol farq berdi. Katak bo'yicha sizgan maqsadli kodlash CV da eng yaxshi (1.373), testda eng yomon (1.564) chiqdi; out-of-fold versiya halol (1.439 va 1.438). Chiziqli modelga d_markaz belgisi RMSE ni 1.753 dan 1.474 ga tushirdi, daraxtga esa deyarli ta'sir qilmadi.

  3. Yaqin nuqtalar o'xshash — baholash dizayni shunga mos bo'lsin. Kutish vaqtida Moran I 0.330 (p = 0.01). Yangi hudud uchun tasodifiy KFold xatoni muntazam kam ko'rsatdi (optimizm +0.076, SE 0.022), fazoviy blokli CV esa siljishsiz baholadi (-0.023, SE 0.027); farq +0.100 (SE 0.007). DBSCAN haversine bilan uchta issiq nuqtani topdi va tarqoq talabni (87.1%) shovqin sifatida qoldirdi.

Keyingi darsda Katta ma'lumot bilan ishlash: bitta kompyuter xotirasiga sig'maydigan ma'lumot — masalan, millionlab taksi safarlari — bilan qanday ishlash kerak.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
28.9-dars: Geografik ma'lumot — IlmHamroh