การจัดกลุ่มเชิงพื้นที่ด้วย PostGIS


97

ฉันกำลังมองหาอัลกอริทึมการจัดกลุ่มเชิงพื้นที่เพื่อใช้งานภายในฐานข้อมูลที่เปิดใช้งาน PostGIS สำหรับคุณสมบัติจุด ฉันจะเขียนฟังก์ชั่น plpgsql ที่ใช้ระยะห่างระหว่างจุดภายในคลัสเตอร์เดียวกันกับอินพุต ที่ฟังก์ชั่นการส่งออกส่งกลับอาร์เรย์ของกลุ่ม ทางออกที่ชัดเจนที่สุดคือการสร้างโซนบัฟเฟอร์ที่ระบุระยะทางรอบ ๆ คุณสมบัติและค้นหาคุณสมบัติในบัฟเฟอร์นี้ หากคุณสมบัติดังกล่าวมีอยู่แล้วให้ดำเนินการต่อเพื่อสร้างบัฟเฟอร์รอบพวกเขา ฯลฯ หากคุณสมบัติดังกล่าวไม่มีอยู่นั่นหมายความว่าการสร้างคลัสเตอร์เสร็จสมบูรณ์ อาจจะมีวิธีแก้ปัญหาที่ฉลาดบ้างไหม?


4
มีวิธีการจัดกลุ่มที่หลากหลายเนื่องจากลักษณะของข้อมูลที่แตกต่างกันและวัตถุประสงค์ในการจัดกลุ่มที่แตกต่างกัน สำหรับภาพรวมของสิ่งที่มีอยู่และสำหรับบางอ่านง่ายเกี่ยวกับสิ่งที่คนอื่นกำลังทำเพื่อกลุ่มการฝึกอบรมระยะการค้นหา CV @ เว็บไซต์ของเเม่ ในความเป็นจริง"การเลือกวิธีการจัดกลุ่ม"เกือบจะซ้ำกับของคุณและมีคำตอบที่ดี
whuber

8
+1 กับคำถามเพราะการค้นหาตัวอย่าง PostGIS SQL จริงแทนที่จะเชื่อมโยงไปยังอัลกอริทึมเป็นภารกิจที่เป็นไปไม่ได้สำหรับสิ่งอื่นนอกเหนือจากการจัดกลุ่มกริดพื้นฐานโดยเฉพาะอย่างยิ่งสำหรับการจัดกลุ่มที่แปลกใหม่เช่นMCL
wildpeaks

คำตอบ:


112

มีวิธีการจัดกลุ่มที่ดีอย่างน้อยสองวิธีสำหรับ PostGIS: k -means (ผ่านkmeans-postgresqlส่วนขยาย) หรือรูปทรงเรขาคณิตการทำคลัสเตอร์ภายในระยะทางที่กำหนด (PostGIS 2.2)


1) k-หมายถึงด้วยkmeans-postgresql

การติดตั้ง:คุณต้องมี PostgreSQL 8.4 หรือสูงกว่าบนระบบโฮสต์ POSIX (ฉันไม่รู้ว่าจะเริ่มต้นที่ MS Windows ได้ที่ไหน) หากคุณมีการติดตั้งจากแพ็คเกจตรวจสอบให้แน่ใจว่าคุณมีแพ็คเกจการพัฒนา (เช่นpostgresql-develสำหรับ CentOS) ดาวน์โหลดและแตกไฟล์:

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/

ก่อนการสร้างคุณต้องตั้งค่าUSE_PGXS ตัวแปรสภาพแวดล้อม (โพสต์ก่อนหน้าของฉันได้รับคำสั่งให้ลบส่วนนี้ของMakefileซึ่งไม่ได้เป็นตัวเลือกที่ดีที่สุด) หนึ่งในสองคำสั่งเหล่านี้ควรใช้ได้กับ Unix shell

# bash
export USE_PGXS=1
# csh
setenv USE_PGXS 1

ตอนนี้สร้างและติดตั้งส่วนขยาย:

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

(หมายเหตุ: ฉันได้ลองกับ Ubuntu 10.10 แล้ว แต่ไม่มีโชคเพราะไม่มีเส้นทางpg_config --pgxsอยู่นี่อาจเป็นข้อผิดพลาดในการบรรจุภัณฑ์ของ Ubuntu)

การใช้งาน / ตัวอย่าง:คุณควรมีตารางจุดหนึ่ง (ฉันวาดคะแนนสุ่มหลอกใน QGIS) นี่คือตัวอย่างของสิ่งที่ฉันทำ:

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;

5ฉันให้ไว้ในอาร์กิวเมนต์ที่สองของkmeansฟังก์ชั่นหน้าต่างเป็นKจำนวนเต็มในการผลิตห้ากลุ่ม คุณสามารถเปลี่ยนเป็นจำนวนเต็มใดก็ได้ที่คุณต้องการ

ด้านล่างเป็นจุดสุ่มหลอก 31 จุดที่ฉันวาดและเซนทรอยด์ห้ารายการที่มีป้ายกำกับแสดงจำนวนในแต่ละคลัสเตอร์ สิ่งนี้ถูกสร้างขึ้นโดยใช้แบบสอบถาม SQL ด้านบน

Kmeans


คุณสามารถพยายามแสดงให้เห็นว่ากลุ่มเหล่านี้อยู่ที่ไหนกับST_MinimumBoundingCircle :

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) การจัดกลุ่มภายในระยะทางขีด จำกัด ด้วย ST_ClusterWithin

ฟังก์ชันการรวมนี้รวมอยู่ใน PostGIS 2.2 และส่งกลับอาร์เรย์ของ GeometryCollections ที่ส่วนประกอบทั้งหมดอยู่ภายในระยะทางซึ่งกันและกัน

นี่คือตัวอย่างการใช้งานโดยที่ระยะทาง 100.0 เป็นเกณฑ์ที่ให้ผลลัพธ์ใน 5 คลัสเตอร์ที่แตกต่างกัน:

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

กลุ่มกลางที่ใหญ่ที่สุดมีรัศมีวงกลมล้อมรอบเป็น 65.3 หน่วยหรือประมาณ 130 ซึ่งมีขนาดใหญ่กว่าเกณฑ์ นี่เป็นเพราะระยะทางระหว่างแต่ละรูปทรงเรขาคณิตของสมาชิกนั้นน้อยกว่าขีด จำกัด ดังนั้นมันจึงเชื่อมโยงเข้าด้วยกันเป็นกระจุกดาวที่ใหญ่กว่า


2
เยี่ยมมากการแก้ไขเหล่านี้จะช่วยในการติดตั้ง :-) อย่างไรก็ตามฉันกลัวว่าฉันไม่สามารถใช้ส่วนขยายนั้นได้ในท้ายที่สุดเพราะ (ถ้าฉันเข้าใจถูกต้อง) มันต้องใช้จำนวนกลุ่มของ hardcoded ซึ่งใช้ได้ดีกับข้อมูลคงที่ คุณสามารถปรับแต่งได้ล่วงหน้า แต่จะไม่เหมาะกับฉันสำหรับการจัดกลุ่มข้อมูลตามอำเภอใจ (เนื่องจากตัวกรองต่างๆ) เช่นช่องว่างขนาดใหญ่ในคลัสเตอร์ 10 คะแนนในภาพสุดท้าย อย่างไรก็ตามสิ่งนี้จะช่วยคนอื่นเช่นกันเพราะ (afaik) นี่เป็นเพียงตัวอย่าง SQL ที่มีอยู่เท่านั้น (ยกเว้นหนึ่ง liners บนหน้าแรกของส่วนขยาย) สำหรับส่วนขยายนั้น
wildpeaks

(อาคุณตอบในเวลาเดียวกันฉันลบความคิดเห็นก่อนหน้านี้เพื่อปรับโครงสร้างมันขอโทษ)
wildpeaks

7
สำหรับ kmeans การจัดกลุ่มคุณต้องระบุจำนวนของคลัสเตอร์ล่วงหน้า ฉันสงสัยว่ามีอัลกอริธึมทางเลือกซึ่งไม่จำเป็นต้องใช้จำนวนกลุ่ม
djq

1
เวอร์ชั่น 1.1.0 มีให้บริการแล้ว: api.pgxn.org/dist/kmeans/1.1.0/kmeans-1.1.0.zip
djq

1
@maxd ไม่ รับ A = πr²จากนั้น r = √ (A / π)
Mike T

27

ฉันได้เขียนฟังก์ชันที่คำนวณกลุ่มของคุณสมบัติตามระยะทางระหว่างพวกเขาและสร้างฮัลล์นูนเหนือคุณลักษณะนี้:

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;

ตัวอย่างการใช้ฟังก์ชั่นนี้:

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

'poi' - ชื่อของเลเยอร์ 'wkb_geometry' - ชื่อของคอลัมน์รูปทรงเรขาคณิต 'ogc_fid' - คีย์หลักของตาราง 14000 - ระยะห่างของคลัสเตอร์

ผลลัพธ์ของการใช้ฟังก์ชั่นนี้:

ป้อนคำอธิบายรูปภาพที่นี่


ที่ดี! คุณสามารถเพิ่มตัวอย่างของวิธีการใช้ฟังก์ชั่นของคุณด้วยได้หรือไม่? ขอบคุณ!
underdark

1
ฉันได้แก้ไขซอร์สโค้ดเล็กน้อยและได้เพิ่มตัวอย่างการใช้ฟังก์ชั่น
drnextgis

เพิ่งลองใช้สิ่งนี้กับ postgres 9.1 และบรรทัด "FOR innr IN EXECUTE 'SELECT' || gid || ' ในฐานะที่เป็น gid, '|| geom ||' ตาม geom จาก tmp dmn อยู่ที่ไหน NULL 'LOOP "ให้ข้อผิดพลาดต่อไปนี้ ความคิดใด ๆ ข้อผิดพลาด: ฟังก์ชั่น set
values ​​ถูก

ฉันไม่แน่ใจว่าจะใช้รหัสนี้อย่างไรใน PG (PostGIS n00b) ในตารางของฉัน ฉันจะเริ่มเข้าใจไวยากรณ์นี้ได้ที่ไหน ผมมีตารางที่มี lats และ Lons ว่าผมอยากจะคลัสเตอร์
MGA

ก่อนอื่นคุณต้องสร้างgeometryคอลัมน์ในตารางของคุณไม่ต้องแยกเก็บลอนและแยกคอลัมน์ด้วยค่าที่ไม่ซ้ำ (IDs)
drnextgis

10

จนถึงตอนนี้สิ่งที่ฉันคาดหวังมากที่สุดคือส่วนขยายสำหรับการจัดกลุ่ม K-mean เป็นฟังก์ชันหน้าต่าง: http://pgxn.org/dist/kmeans/

อย่างไรก็ตามฉันยังไม่สามารถติดตั้งได้สำเร็จ


มิฉะนั้นสำหรับการจัดกลุ่มตารางพื้นฐานคุณสามารถใช้SnapToGrid

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 ...

สำหรับ MS Windows:

ที่ต้องการ:

คุณจะทำอะไร:

  • ปรับแต่งซอร์สโค้ดเพื่อส่งออกฟังก์ชั่น kmeans ไปยัง DLL
  • คอมไพล์ซอร์สโค้ดด้วยcl.exeคอมไพเลอร์เพื่อสร้าง DLL พร้อมkmeansฟังก์ชัน
  • วาง DLL ที่สร้างขึ้นลงในโฟลเดอร์ PostgreSQL \ lib
  • จากนั้นคุณสามารถ "สร้าง" (ลิงก์) UDF ลงใน PostgreSQL ผ่านคำสั่ง SQL

ขั้นตอน:

  1. ดาวน์โหลดและติดตั้ง / แยกข้อกำหนด
  2. เปิดkmeans.cในเครื่องมือแก้ไขใด ๆ :

    1. หลังจาก#includeบรรทัดกำหนดมาโคร DLLEXPORT ด้วย:

      #if defined(_WIN32)
          #define DLLEXPORT __declspec(dllexport)
      #else
         #define DLLEXPORT
      #endif
      
    2. ใส่DLLEXPORTก่อนแต่ละบรรทัดเหล่านี้:

      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. เปิดบรรทัดคำสั่ง c ++ Visual

  4. ในบรรทัดคำสั่ง:

    1. kmeans-postgresqlไปที่สกัด
    2. ตั้งค่า POSTGRESPATH ของคุณตัวอย่างเช่น: SET POSTGRESPATH=C:\Program Files\PostgreSQL\9.5
    3. วิ่ง

      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. คัดลอกkmeans.dllไปยัง%POSTGRESPATH%\lib

  6. ตอนนี้รันคำสั่ง SQL ในฐานข้อมูลของคุณเพื่อ "สร้าง" ฟังก์ชัน

    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 ในแบบสอบถาม PostGIS ที่ให้ไว้ใน 2) ในเครื่องมือนี้

ในขณะที่ QGIS ไม่ได้จัดการรูปทรงเรขาคณิตหรือประเภทข้อมูลที่แตกต่างกันในคอลัมน์รูปทรงเรขาคณิตเดียวกันฉันได้สร้างสองชั้นหนึ่งชั้นสำหรับกลุ่มและอีกหนึ่งสำหรับจุดคลัสเตอร์

ขั้นแรกสำหรับกลุ่มคุณต้องการรูปหลายเหลี่ยมเท่านั้นผลลัพธ์อื่น ๆ คือคะแนนโดดเดี่ยว:

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'

จากนั้นสำหรับจุดที่เป็นกลุ่มคุณจะต้องเปลี่ยนรูปทรงเรขาคณิตของคอลเลคชั่นแบบหลายจุด:

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

บางจุดอยู่ในพิกัดเดียวกันดังนั้นฉลากอาจทำให้เกิดความสับสน

การทำคลัสเตอร์ใน QGIS


2

คุณสามารถใช้โซลูชัน Kmeans ได้ง่ายขึ้นด้วยวิธีST_ClusterKMeansที่มีให้ใน postgis จาก 2.3 ตัวอย่าง:

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;

กล่องขอบเขตของคุณสมบัติใช้เป็นรูปทรงเรขาคณิตของคลัสเตอร์ในตัวอย่างข้างต้น ภาพแรกแสดงรูปทรงเรขาคณิตดั้งเดิมและภาพที่สองเป็นผลลัพธ์ของการเลือกด้านบน

รูปทรงดั้งเดิม กลุ่มคุณสมบัติ


1

โซลูชันการจัดกลุ่มด้านล่างจากรับคลัสเตอร์เดียวจากกลุ่มเมฆที่มีเส้นผ่านศูนย์กลางสูงสุดใน postgisซึ่งไม่เกี่ยวข้องกับการสืบค้นแบบไดนามิก

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

และประเภทที่มี ID คลัสเตอร์

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

ถัดไปฟังก์ชั่นอัลกอริทึม

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

การใช้งาน:

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;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.