PostGIS ile mekansal kümeleme?


97

Nokta özellikleri için PostGIS etkin veritabanı içinde kullanmak için uzamsal kümeleme algoritması arıyorum. Girişle aynı kümedeki noktalar arasındaki mesafeyi alan plpgsql işlevini yazacağım. Çıkış fonksiyonunda kümelerin dizisini döndürür. En belirgin çözüm, özellik çevresinde belirtilen arabellek bölgelerini oluşturmak ve bu arabellekte özellik aramaktır. Bu tür özellikler varsa, etraflarında bir tampon oluşturmaya devam edin. Bu tür özellikler mevcut değilse, küme oluşturma işlemi tamamlanmıştır. Belki bazı akıllı çözümler vardır?


4
Verilerin farklı olması ve kümelemenin farklı amaçları nedeniyle çok çeşitli kümeleme yöntemleri vardır. Dışarıdakilere genel bir bakış ve mesafe matrislerini kümelemek için başkalarının neler yaptığını okumak için , CV @ SE sitesinde arama yapın . Aslında, "kümeleme yönteminin seçilmesi" neredeyse sizinkinin aynısıdır ve iyi cevapları vardır.
whuber

8
Yerine algoritmalara bağlantıların gerçek PostGIS SQL örneğini bulmak çünkü soruya 1 özellikle gibi daha egzotik kümelenmelerinin için, temel ızgara kümeleme dışında herhangi bir amaçla misyon imkansız MCL
wildpeaks

Yanıtlar:


112

PostGIS için en az iki iyi kümeleme yöntemi vardır: k-değeri ( kmeans-postgresqluzatma yoluyla ) veya eşik uzaklıktaki kümeleme geometrileri (PostGIS 2.2)


1) k - ile demektirkmeans-postgresql

Kurulum: POSIX ana sisteminde PostgreSQL 8.4 veya daha üstü olması gerekir (MS Windows için nereden başlayacağımı bilemiyorum). Bunu paketlerden kurduysanız, geliştirme paketlerinin de olduğundan emin olun (örneğin, postgresql-develCentOS için). İndirin ve çıkarın:

wget http://api.pgxn.org/dist/kmeans/1.1.0/kmeans-1.1.0.zip
unzip kmeans-1.1.0.zip
cd kmeans-1.1.0/

İnşa etmeden önce, USE_PGXS ortam değişkenini ayarlamanız gerekir (önceki yazımın bu bölümün silinmesi talimatı vardı Makefile, bu seçeneklerin en iyisi değildi). Bu iki komuttan birinin Unix kabuğunuz için çalışması gerekir:

# bash
export USE_PGXS=1
# csh
setenv USE_PGXS 1

Şimdi uzantıyı derleyip kurun:

make
make install
psql -f /usr/share/pgsql/contrib/kmeans.sql -U postgres -D postgis

(Not: Bunu Ubuntu 10.10 ile de denedim, fakat yolu olmadığından şans pg_config --pgxsyok! Bu muhtemelen bir Ubuntu paketleme hatasıdır)

Kullanım / Örnek: Bir yerde bir puan tablosuna sahip olmalısınız (QGIS'de bir grup sahte rastgele nokta çizdim). İşte yaptığım şeye bir örnek:

SELECT kmeans, count(*), ST_Centroid(ST_Collect(geom)) AS geom
FROM (
  SELECT kmeans(ARRAY[ST_X(geom), ST_Y(geom)], 5) OVER (), geom
  FROM rand_point
) AS ksub
GROUP BY kmeans
ORDER BY kmeans;

5ikinci bağımsız değişken sağlanan bir kmeanspencere fonksiyonu olan K beş kümelerinin üretilmesinde tamsayıdır. Bunu, istediğiniz tamsayı olarak değiştirebilirsiniz.

Aşağıda çizdiğim 31 sözde rastgele nokta ve her kümedeki sayımı gösteren etiketli beş centroid. Bu, yukarıdaki SQL sorgusu kullanılarak oluşturuldu.

kmeans


Ayrıca, bu kümelerin nerede ST_MinimumBoundingCircle ile olduğunu göstermeye çalışabilirsiniz :

SELECT kmeans, ST_MinimumBoundingCircle(ST_Collect(geom)) AS circle
FROM (
  SELECT kmeans(ARRAY[ST_X(geom), ST_Y(geom)], 5) OVER (), geom
  FROM rand_point
) AS ksub
GROUP BY kmeans
ORDER BY kmeans;

Kmeans2


2) Bir eşik mesafesi içinde kümeleme ST_ClusterWithin

Bu toplama işlevi PostGIS 2.2'ye dahil edilmiştir ve tüm bileşenlerin birbirinden uzakta olduğu bir GeometryCollections dizisi döndürür.

İşte 100.0'lık bir mesafenin 5 farklı kümeye neden olan eşik olduğu örnek bir kullanım:

SELECT row_number() over () AS id,
  ST_NumGeometries(gc),
  gc AS geom_collection,
  ST_Centroid(gc) AS centroid,
  ST_MinimumBoundingCircle(gc) AS circle,
  sqrt(ST_Area(ST_MinimumBoundingCircle(gc)) / pi()) AS radius
FROM (
  SELECT unnest(ST_ClusterWithin(geom, 100)) gc
  FROM rand_point
) f;

ClusterWithin100

En büyük orta küme, eşikten daha büyük olan 65.3 ünite veya yaklaşık 130'luk bir kapalı çember yarıçapına sahiptir. Bunun nedeni, üye geometrileri arasındaki bireysel mesafelerin eşikten daha az olmasıdır, bu nedenle onu bir büyük küme olarak birbirine bağlar.


2
Harika, bu değişiklikler kurulum için yardımcı olacak :-) Ancak sonunda bu uzantıyı kullanamayacağımdan korkuyorum çünkü (doğru anladıysam), sabit veri kodlu bir sihirli kümeye ihtiyaç duyuyor, çünkü statik verilerle iyi çalışıyor çünkü önceden ince ayar yapabilirsiniz ancak kümeler için keyfi (çeşitli filtreler nedeniyle) veri kümeleri için uygun olmaz (örneğin, son görüntüdeki 10-nokta kümesindeki büyük boşluk). Ancak bu, diğer insanlara da yardımcı olacaktır (afaik), bu uzantı için mevcut tek SQL örneğidir (uzantının ana sayfasında bulunanlar hariç).
wildpeaks

(ah sen bunu yeniden formüle önceki yorum sildi aynı anda cevap, üzgünüm)
wildpeaks

7
Kümelenme kümeleri için küme sayısını önceden belirtmeniz gerekir; Küme sayısının gerekli olmadığı alternatif algoritmalar olup olmadığını merak ediyorum.
djq

1
Sürüm 1.1.0 artık kullanıma hazır: api.pgxn.org/dist/kmeans/1.1.0/kmeans-1.1.0.zip
djq

1
@ maxd no. Verilen A = ²r², sonra r = √ (A / π).
Mike T

27

Aralarındaki mesafeye bağlı olarak özellik kümelerini hesaplayan ve bu özelliklerin üzerinde dışbükey gövde oluşturan bir fonksiyon yazdım:

CREATE OR REPLACE FUNCTION get_domains_n(lname varchar, geom varchar, gid varchar, radius numeric)
    RETURNS SETOF record AS
$$
DECLARE
    lid_new    integer;
    dmn_number integer := 1;
    outr       record;
    innr       record;
    r          record;
BEGIN

    DROP TABLE IF EXISTS tmp;
    EXECUTE 'CREATE TEMPORARY TABLE tmp AS SELECT '||gid||', '||geom||' FROM '||lname;
    ALTER TABLE tmp ADD COLUMN dmn integer;
    ALTER TABLE tmp ADD COLUMN chk boolean DEFAULT FALSE;
    EXECUTE 'UPDATE tmp SET dmn = '||dmn_number||', chk = FALSE WHERE '||gid||' = (SELECT MIN('||gid||') FROM tmp)';

    LOOP
        LOOP
            FOR outr IN EXECUTE 'SELECT '||gid||' AS gid, '||geom||' AS geom FROM tmp WHERE dmn = '||dmn_number||' AND NOT chk' LOOP
                FOR innr IN EXECUTE 'SELECT '||gid||' AS gid, '||geom||' AS geom FROM tmp WHERE dmn IS NULL' LOOP
                    IF ST_DWithin(ST_Transform(ST_SetSRID(outr.geom, 4326), 3785), ST_Transform(ST_SetSRID(innr.geom, 4326), 3785), radius) THEN
                    --IF ST_DWithin(outr.geom, innr.geom, radius) THEN
                        EXECUTE 'UPDATE tmp SET dmn = '||dmn_number||', chk = FALSE WHERE '||gid||' = '||innr.gid;
                    END IF;
                END LOOP;
                EXECUTE 'UPDATE tmp SET chk = TRUE WHERE '||gid||' = '||outr.gid;
            END LOOP;
            SELECT INTO r dmn FROM tmp WHERE dmn = dmn_number AND NOT chk LIMIT 1;
            EXIT WHEN NOT FOUND;
       END LOOP;
       SELECT INTO r dmn FROM tmp WHERE dmn IS NULL LIMIT 1;
       IF FOUND THEN
           dmn_number := dmn_number + 1;
           EXECUTE 'UPDATE tmp SET dmn = '||dmn_number||', chk = FALSE WHERE '||gid||' = (SELECT MIN('||gid||') FROM tmp WHERE dmn IS NULL LIMIT 1)';
       ELSE
           EXIT;
       END IF;
    END LOOP;

    RETURN QUERY EXECUTE 'SELECT ST_ConvexHull(ST_Collect('||geom||')) FROM tmp GROUP by dmn';

    RETURN;
END
$$
LANGUAGE plpgsql;

Bu işlevi kullanma örneği:

SELECT * FROM get_domains_n('poi', 'wkb_geometry', 'ogc_fid', 14000) AS g(gm geometry)

'poi' - katmanın adı, 'wkb_geometry' - geometri sütununun adı, 'ogc_fid' - tablonun ana anahtarı, 14000 - küme mesafesi.

Bu işlevi kullanmanın sonucu:

görüntü tanımını buraya girin


Harika! İşlevinizi nasıl kullanacağınıza dair bir örnek ekleyebilir misiniz? Teşekkürler!
underdark

1
Biraz kaynak kodunu değiştirdim ve fonksiyon kullanma örneği ekledim.
drnextgis

Sadece 9.1 postgresinde ve "SELECT IN 'INÇ İÇİN' SEÇ '|| gid ||' satırını kullanmayı denedim. Gid, '|| geom ||' Dmpt NEREDE NULL 'LOOP' NEREDE gelen geom aşağıdaki hatayı verir. Herhangi bir fikir ? HATA: Bir kümeyi kabul edemeyen bağlam olarak adlandırılan küme değerli işlev
bitbox

Bu kodu tablomda PG (PostGIS n00b) olarak nasıl kullanacağımdan emin değilim. bu sözdizimini anlamaya nereden başlayabilirim? Kümelenmek istediğim kulplu ve kulplu bir masam var
mga

Öncelikle, geometrytablonuzda sütun oluşturmak zorundasınız, ayrı ayrı lonlat saklamak ve benzersiz değerler (ID) içeren sütunlar yapmak zorunda değilsiniz.
drnextgis

10

Şimdiye kadar bulduğum en umut verici şey K-araçlarının bir pencere işlevi olarak kümelenmesi için bu uzantısı: http://pgxn.org/dist/kmeans/

Ancak henüz başarıyla yükleyemedim.


Aksi halde, temel ızgara kümelemesi için SnapToGrid'i kullanabilirsiniz .

SELECT
    array_agg(id) AS ids,
    COUNT( position ) AS count,
    ST_AsText( ST_Centroid(ST_Collect( position )) ) AS center,
FROM mytable
GROUP BY
    ST_SnapToGrid( ST_SetSRID(position, 4326), 22.25, 11.125)
ORDER BY
    count DESC
;

2

@MikeT cevabı tamamlanıyor ...

MS Windows için:

Gereksinimler:

Ne yapacaksın:

  • Kmeans işlevini bir DLL dosyasına vermek için kaynak kodunu değiştirin.
  • İşlevli cl.exebir DLL dosyası oluşturmak için kaynak kodunu derleyici ile derleyin kmeans.
  • Oluşturulan DLL dosyasını PostgreSQL \ lib klasörüne yerleştirin.
  • Ardından UDF'yi PostgreSQL'e SQL komutu aracılığıyla "oluşturabilir" (link).

Adımlar:

  1. İndirme ve yükleme / alma gereksinimleri.
  2. kmeans.cHerhangi bir düzenleyicide açın :

    1. #includeSatırlar DLLEXPORT makrosunu tanımladıktan sonra :

      #if defined(_WIN32)
          #define DLLEXPORT __declspec(dllexport)
      #else
         #define DLLEXPORT
      #endif
      
    2. DLLEXPORTBu satırların her birinin önüne koyun :

      PG_FUNCTION_INFO_V1(kmeans_with_init);
      PG_FUNCTION_INFO_V1(kmeans);
      
      extern Datum kmeans_with_init(PG_FUNCTION_ARGS);
      extern Datum kmeans(PG_FUNCTION_ARGS);
      
  3. Visual C ++ Komut Satırı'nı açın.

  4. Komut satırında:

    1. Çıkartılana git kmeans-postgresql.
    2. POSTGRESPATH cihazınızı ayarlayın, örneğin benim: SET POSTGRESPATH=C:\Program Files\PostgreSQL\9.5
    3. Çalıştırmak

      cl.exe /I"%POSTGRESPATH%\include" /I"%POSTGRESPATH%\include\server" /I"%POSTGRESPATH%\include\server\port\win32" /I"%POSTGRESPATH%\include\server\port\win32_msvc" /I"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include" /LD kmeans.c "%POSTGRESPATH%\lib\postgres.lib"
  5. Kopya kmeans.dlliçin%POSTGRESPATH%\lib

  6. Şimdi fonksiyonunu "CREATE" olarak veritabanında SQL komutunu çalıştır.

    CREATE FUNCTION kmeans(float[], int) RETURNS int
    AS '$libdir/kmeans'
    LANGUAGE c VOLATILE STRICT WINDOW;
    
    CREATE FUNCTION kmeans(float[], int, float[]) RETURNS int
    AS '$libdir/kmeans', 'kmeans_with_init'
    LANGUAGE C IMMUTABLE STRICT WINDOW;
    

2

QGIS'de bu cevaplayıcıda 2) 'de verilen PostGIS sorgusunun sonucunu göstermenin bir yolu:

QGIS, ne geometri toplamalarını ne de farklı veri tiplerini aynı geometri sütununda işlemediğinden, iki küme oluşturdum, biri kümeler için, diğeri kümelenmiş noktalar için.

İlk önce, kümeler için yalnızca çokgenlere ihtiyacınız vardır, diğer sonuçlar yalnız puanlardır:

SELECT id,countfeature,circle FROM (SELECT row_number() over () AS id,
  ST_NumGeometries(gc) as countfeature,
  ST_MinimumBoundingCircle(gc) AS circle
FROM (
  SELECT unnest(ST_ClusterWithin(the_geom, 100)) gc
  FROM rand_point
) f) a WHERE ST_GeometryType(circle) = 'ST_Polygon'

Daha sonra kümelenmiş noktalar için, geometri toplamalarını çok noktadan dönüştürmeniz gerekir:

SELECT row_number() over () AS id,
  ST_NumGeometries(gc) as countfeature,
  ST_CollectionExtract(gc,1) AS multipoint
FROM (
  SELECT unnest(ST_ClusterWithin(the_geom, 100)) gc
  FROM rand_point
) f

Bazı noktalar aynı koordinatlarda olduğundan etiket kafa karıştırıcı olabilir.

QGIS'de Kümeleme


2

Kmeans çözümünü , postgis'te 2.3 bulunan ST_ClusterKMeans yöntemi ile daha kolay kullanabilirsiniz. Örnek:

SELECT kmean, count(*), ST_SetSRID(ST_Extent(geom), 4326) as bbox 
FROM
(
    SELECT ST_ClusterKMeans(geom, 20) OVER() AS kmean, ST_Centroid(geom) as geom
    FROM sls_product 
) tsub
GROUP BY kmean;

Sınırlayıcı özellikler kutusu yukarıdaki örnekte küme geometrisi olarak kullanılır. İlk resim orijinal geometrileri gösterir ve ikincisi yukarıdaki seçimin sonucudur.

Orijinal geometriler Özellik kümeleri


1

Aşağıdan kümeleme çözümü Postgis'te dinamik sorgulama içermeyen maksimum çaplı nokta bulutundan tek bir küme alın .

CREATE TYPE pt AS (
    gid character varying(32),
    the_geom geometry(Point))

ve küme kimliğine sahip bir tür

CREATE TYPE clustered_pt AS (
    gid character varying(32),
    the_geom geometry(Point)
    cluster_id int)

Sonraki algoritma fonksiyonu

CREATE OR REPLACE FUNCTION buc(points pt[], radius integer)
RETURNS SETOF clustered_pt AS
$BODY$

DECLARE
    srid int;
    joined_clusters int[];

BEGIN

--If there's only 1 point, don't bother with the loop.
IF array_length(points,1)<2 THEN
    RETURN QUERY SELECT gid, the_geom, 1 FROM unnest(points);
    RETURN;
END IF;

CREATE TEMPORARY TABLE IF NOT EXISTS points2 (LIKE pt) ON COMMIT DROP;

BEGIN
    ALTER TABLE points2 ADD COLUMN cluster_id serial;
EXCEPTION
    WHEN duplicate_column THEN --do nothing. Exception comes up when using this function multiple times
END;

TRUNCATE points2;
    --inserting points in
INSERT INTO points2(gid, the_geom)
    (SELECT (unnest(points)).* ); 

--Store the srid to reconvert points after, assumes all points have the same SRID
srid := ST_SRID(the_geom) FROM points2 LIMIT 1;

UPDATE points2 --transforming points to a UTM coordinate system so distances will be calculated in meters.
SET the_geom =  ST_TRANSFORM(the_geom,26986);

--Adding spatial index
CREATE INDEX points_index
ON points2
USING gist
(the_geom);

ANALYZE points2;

LOOP
    --If the smallest maximum distance between two clusters is greater than 2x the desired cluster radius, then there are no more clusters to be formed
    IF (SELECT ST_MaxDistance(ST_Collect(a.the_geom),ST_Collect(b.the_geom))  FROM points2 a, points2 b
        WHERE a.cluster_id <> b.cluster_id
        GROUP BY a.cluster_id, b.cluster_id 
        ORDER BY ST_MaxDistance(ST_Collect(a.the_geom),ST_Collect(b.the_geom)) LIMIT 1)
        > 2 * radius
    THEN
        EXIT;
    END IF;

    joined_clusters := ARRAY[a.cluster_id,b.cluster_id]
        FROM points2 a, points2 b
        WHERE a.cluster_id <> b.cluster_id
        GROUP BY a.cluster_id, b.cluster_id
        ORDER BY ST_MaxDistance(ST_Collect(a.the_geom),ST_Collect(b.the_geom)) 
        LIMIT 1;

    UPDATE points2
    SET cluster_id = joined_clusters[1]
    WHERE cluster_id = joined_clusters[2];

    --If there's only 1 cluster left, exit loop
    IF (SELECT COUNT(DISTINCT cluster_id) FROM points2) < 2 THEN
        EXIT;

    END IF;

END LOOP;

RETURN QUERY SELECT gid, ST_TRANSFORM(the_geom, srid)::geometry(point), cluster_id FROM points2;
END;
$BODY$
LANGUAGE plpgsql

Kullanımı:

WITH subq AS(
    SELECT ARRAY_AGG((gid, the_geom)::pt) AS points
    FROM data
    GROUP BY collection_id)
SELECT (clusters).* FROM 
    (SELECT buc(points, radius) AS clusters FROM subq
) y;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.