Phân cụm không gian với PostGIS?


97

Tôi đang tìm kiếm thuật toán phân cụm không gian để sử dụng nó trong cơ sở dữ liệu hỗ trợ PostGIS cho các tính năng điểm. Tôi sẽ viết hàm plpgsql lấy khoảng cách giữa các điểm trong cùng một cụm làm đầu vào. Tại hàm đầu ra trả về mảng của cụm. Giải pháp rõ ràng nhất là xây dựng các vùng đệm được chỉ định khoảng cách xung quanh tính năng và tìm kiếm các tính năng vào bộ đệm này. Nếu các tính năng như vậy tồn tại thì tiếp tục xây dựng bộ đệm xung quanh chúng, v.v ... Nếu các tính năng đó không tồn tại có nghĩa là việc xây dựng cụm được hoàn thành. Có thể có một số giải pháp thông minh?


4
Có rất nhiều phương pháp phân cụm vì tính chất khác nhau của dữ liệu và mục đích phân cụm khác nhau. Để biết tổng quan về những gì ở ngoài kia và để dễ đọc về những gì người khác đang làm với ma trận khoảng cách cụm, hãy tìm kiếm trang CV @ SE . Trên thực tế, "chọn phương pháp phân cụm" gần như là một bản sao chính xác của bạn và có câu trả lời tốt.
whuber

8
+1 cho câu hỏi vì việc tìm một ví dụ SQL PostGIS thực tế thay vì liên kết đến các thuật toán là nhiệm vụ bất khả thi đối với bất kỳ điều gì ngoài việc phân cụm lưới cơ bản, đặc biệt là đối với các cụm kỳ lạ hơn như MCL
wildpeaks

Câu trả lời:


112

Có ít nhất hai phương pháp phân cụm tốt cho PostGIS: k -means (thông qua kmeans-postgresqltiện ích mở rộng) hoặc phân cụm hình học trong một khoảng cách ngưỡng (PostGIS 2.2)


1) k -means vớikmeans-postgresql

Cài đặt: Bạn cần phải có PostgreSQL 8.4 trở lên trên hệ thống máy chủ POSIX (Tôi không biết bắt đầu từ đâu cho MS Windows). Nếu bạn đã cài đặt gói này từ các gói, hãy đảm bảo bạn cũng có các gói phát triển (ví dụ: postgresql-develđối với CentOS). Tải xuống và giải né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/

Trước khi xây dựng, bạn cần đặt USE_PGXS biến môi trường (bài viết trước của tôi đã hướng dẫn xóa phần này Makefile, đây không phải là tùy chọn tốt nhất). Một trong hai lệnh này sẽ hoạt động cho shell Unix của bạn:

# bash
export USE_PGXS=1
# csh
setenv USE_PGXS 1

Bây giờ xây dựng và cài đặt tiện ích mở rộng:

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

(Lưu ý: Tôi cũng đã thử điều này với Ubuntu 10.10, nhưng không may mắn, vì đường dẫn trong pg_config --pgxskhông tồn tại! Đây có thể là một lỗi đóng gói Ubuntu)

Cách sử dụng / Ví dụ: Bạn nên có một bảng điểm ở đâu đó (Tôi đã vẽ một loạt các điểm ngẫu nhiên giả trong QGIS). Đây là một ví dụ với những gì tôi đã làm:

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;

đối số 5tôi cung cấp trong đối số thứ hai của kmeanshàm window là số nguyên K để tạo ra năm cụm. Bạn có thể thay đổi điều này thành bất kỳ số nguyên nào bạn muốn.

Dưới đây là 31 điểm ngẫu nhiên giả mà tôi đã vẽ và năm điểm với nhãn hiển thị số đếm trong mỗi cụm. Điều này đã được tạo bằng cách sử dụng truy vấn SQL ở trên.

Kmeans


Bạn cũng có thể cố gắng minh họa vị trí của các cụm này với 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) Phân cụm trong một khoảng cách ngưỡng với ST_ClusterWithin

Hàm tổng hợp này được bao gồm trong PostGIS 2.2 và trả về một mảng GeometryCollections trong đó tất cả các thành phần nằm trong khoảng cách của nhau.

Dưới đây là một ví dụ sử dụng, trong đó khoảng cách 100.0 là ngưỡng dẫn đến 5 cụm khác nhau:

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;

CụmWithin100

Cụm giữa lớn nhất có bán kính vòng tròn bao quanh là 65,3 đơn vị hoặc khoảng 130, lớn hơn ngưỡng. Điều này là do khoảng cách riêng giữa các hình học thành viên nhỏ hơn ngưỡng, vì vậy nó liên kết nó với nhau như một cụm lớn hơn.


2
Tuyệt vời, những sửa đổi này sẽ giúp cài đặt :-) Tuy nhiên tôi sợ rằng cuối cùng tôi thực sự không thể sử dụng tiện ích mở rộng đó bởi vì (nếu tôi hiểu chính xác), nó cần một số cụm ma thuật được mã hóa cứng, phù hợp với dữ liệu tĩnh bạn có thể tinh chỉnh nó trước nhưng sẽ không phù hợp với tôi để phân cụm các tập dữ liệu tùy ý (do nhiều bộ lọc), ví dụ: khoảng cách lớn trong cụm 10 điểm trên hình ảnh cuối cùng. Tuy nhiên, điều này cũng sẽ giúp người khác vì (afaik), đây là ví dụ SQL duy nhất hiện có (ngoại trừ một lớp lót trên trang chủ của tiện ích mở rộng) cho tiện ích mở rộng đó.
wildpeaks

(ah bạn đã trả lời cùng lúc tôi đã xóa bình luận trước đó để cải tổ nó, xin lỗi)
wildpeaks

7
Đối với cụm kmeans bạn cần xác định trước số lượng cụm; Tôi tò mò nếu có các thuật toán thay thế trong đó số lượng cụm không được yêu cầu mặc dù.
djq

1
Phiên bản 1.1.0 hiện có sẵn: api.pgxn.org/dist/kmeans/1.1.0/kmeans-1.1.0.zip
djq

1
@maxd không. Cho A = πr², thì r = √ (A / π).
Mike T

27

Tôi đã viết hàm tính toán các cụm tính năng dựa trên khoảng cách giữa chúng và xây dựng thân tàu lồi qua các tính năng này:

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;

Ví dụ về việc sử dụng chức năng này:

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

'Poi' - tên của lớp, 'wkb_geometry' - tên của cột hình học, 'ogc_fid' - khóa chính của bảng, khoảng cách 14000 - cụm.

Kết quả của việc sử dụng chức năng này:

nhập mô tả hình ảnh ở đây


Tuyệt quá! Bạn có thể thêm một ví dụ về cách sử dụng chức năng của bạn không? Cảm ơn!
underdark

1
Tôi đã sửa đổi một chút mã nguồn và đã thêm ví dụ về việc sử dụng hàm.
drnextgis

Chỉ cần thử sử dụng tính năng này trên postgres 9.1 và dòng "FOR innr IN EXECUTE 'SELECT' || gid || ' NHƯ gid, '| | geom | |' NHƯ geom TỪ tmp WHERE dmn LÀ NULL 'LOOP "mang lại lỗi sau. Có ý kiến ​​gì không? LRI: hàm có giá trị được đặt trong ngữ cảnh không thể chấp nhận một bộ
bitbox

Tôi không chắc chắn về cách sử dụng mã này trong PG (PostGIS n00b) trong bảng của mình. Tôi có thể bắt đầu hiểu cú pháp này ở đâu? Tôi có một bảng có lats và lons mà tôi muốn gom lại
mga

Trước hết, bạn phải xây dựng geometrycột trong bảng của mình, không được lưu trữ riêng lẻ và tạo cột với các giá trị (ID) duy nhất.
drnextgis

10

Cho đến nay, hứa hẹn nhất mà tôi tìm thấy là tiện ích mở rộng này cho K-nghĩa là phân cụm dưới dạng hàm cửa sổ: http://pgxn.org/dist/kmeans/

Tuy nhiên tôi chưa thể cài đặt thành công.


Mặt khác, để phân cụm lưới cơ bản, bạn có thể sử dụng 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

Bổ sung câu trả lời @MikeT ...

Đối với MS Windows:

Yêu cầu:

Bạn sẽ làm gì:

  • Tinh chỉnh mã nguồn để xuất hàm kmeans sang DLL.
  • Biên dịch mã nguồn với cl.exetrình biên dịch để tạo một DLL có kmeanschức năng.
  • Đặt DLL được tạo vào thư mục PostgreSQL \ lib.
  • Sau đó, bạn có thể "tạo" (liên kết) UDF vào PostgreSQL thông qua lệnh SQL.

Các bước:

  1. Tải về & cài đặt / giải nén yêu cầu.
  2. Mở kmeans.ctrong bất kỳ trình soạn thảo:

    1. Sau khi #includecác dòng xác định macro DLLEXPORT với:

      #if defined(_WIN32)
          #define DLLEXPORT __declspec(dllexport)
      #else
         #define DLLEXPORT
      #endif
      
    2. Đặt DLLEXPORTtrước mỗi dòng này:

      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. Mở dòng lệnh Visual C ++.

  4. Trong dòng lệnh:

    1. Đi đến trích xuất kmeans-postgresql.
    2. Đặt POSTGRESPATH của bạn, ví dụ của tôi là: SET POSTGRESPATH=C:\Program Files\PostgreSQL\9.5
    3. Chạy

      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. Sao chép kmeans.dllvào%POSTGRESPATH%\lib

  6. Bây giờ hãy chạy lệnh SQL trong cơ sở dữ liệu của bạn để "TẠO" hàm.

    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

Đây là một cách để hiển thị trong QGIS kết quả của truy vấn PostGIS được đưa ra trong 2) trong anwser này

Vì QGIS không xử lý các bộ lọc hình học cũng như các kiểu dữ liệu khác nhau trong cùng một cột hình học, tôi đã tạo hai lớp, một cho các cụm và một cho các điểm được nhóm.

Đầu tiên cho các cụm, bạn chỉ cần đa giác, kết quả khác là các điểm cô đơn:

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'

Sau đó, đối với các điểm được nhóm, bạn cần chuyển đổi các hình học trong đa điểm:

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

Một số điểm có cùng tọa độ để nhãn có thể gây nhầm lẫn.

Phân cụm trong QGIS


2

Bạn có thể sử dụng giải pháp Kmeans dễ dàng hơn với phương pháp ST_ClusterKMeans có sẵn trong postgis từ 2.3 Ví dụ:

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;

Hộp giới hạn của các tính năng được sử dụng làm hình dạng cụm trong ví dụ trên. Hình ảnh đầu tiên cho thấy hình học ban đầu và hình ảnh thứ hai là kết quả của việc chọn ở trên.

Hình học gốc Cụm tính năng


1

Giải pháp phân cụm từ dưới lên từ Lấy một cụm từ đám mây điểm có đường kính tối đa trong postgis không liên quan đến truy vấn động.

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

và một loại có id cụm

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

Tiếp theo hàm thuật toán

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

Sử dụng:

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;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.