Clustering spaziale con PostGIS?


97

Sto cercando un algoritmo di clustering spaziale per usarlo all'interno del database PostGIS abilitato per le funzionalità puntuali. Scriverò la funzione plpgsql che prende la distanza tra i punti all'interno dello stesso cluster di input. Alla funzione di output restituisce una matrice di cluster. La soluzione più ovvia è costruire zone di buffer specificate a distanza intorno alla funzione e cercare funzionalità in questo buffer. Se tali funzionalità esistono, continua a creare un buffer attorno ad esse, ecc. Se tali funzionalità non esistono ciò significa che la creazione del cluster è completata. Forse ci sono alcune soluzioni intelligenti?


4
Esiste una grande varietà di metodi di clustering a causa della diversa natura dei dati e dei diversi scopi del clustering. Per una panoramica di ciò che è là fuori e per una facile lettura di ciò che gli altri stanno facendo per raggruppare le matrici di distanza, cerca nel sito CV @ SE . In effetti, "la scelta del metodo di clustering" è quasi un duplicato esatto del tuo e ha buone risposte.
whuber

8
+1 alla domanda perché trovare un vero esempio PostGIS SQL anziché i collegamenti agli algoritmi è una missione impossibile per qualsiasi cosa diversa dal clustering di base della griglia, specialmente per cluster più esotici come MCL
wildpeaks,

Risposte:


112

Esistono almeno due buoni metodi di clustering per PostGIS: k -means (via kmeans-postgresqlextension) o geometrie di clustering entro una distanza di soglia (PostGIS 2.2)


1) k- significa conkmeans-postgresql

Installazione: è necessario disporre di PostgreSQL 8.4 o versione successiva su un sistema host POSIX (non saprei da dove iniziare per MS Windows). Se hai installato questo dai pacchetti, assicurati di avere anche i pacchetti di sviluppo (ad esempio, postgresql-develper CentOS). Scarica ed estrai:

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/

Prima di creare, è necessario impostare la USE_PGXS variabile di ambiente (il mio post precedente mi ha indicato di eliminare questa parte del Makefile, che non era la migliore delle opzioni). Uno di questi due comandi dovrebbe funzionare per la tua shell Unix:

# bash
export USE_PGXS=1
# csh
setenv USE_PGXS 1

Ora compila e installa l'estensione:

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

(Nota: ho provato anche questo con Ubuntu 10.10, ma senza fortuna, poiché il percorso pg_config --pgxsnon esiste! Questo è probabilmente un bug di packaging di Ubuntu)

Utilizzo / Esempio: dovresti avere una tabella di punti da qualche parte (ho disegnato un sacco di punti pseudo casuali in QGIS). Ecco un esempio di quello che ho fatto:

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;

l' 5ho fornito nel secondo argomento della kmeansfunzione finestra è il numero intero K per produrre cinque cluster. Puoi cambiarlo con qualsiasi numero intero desideri.

Di seguito sono riportati i 31 punti pseudo casuali che ho disegnato ei cinque centroidi con l'etichetta che mostra il conteggio in ciascun cluster. Questo è stato creato usando la query SQL sopra.

Kmeans


Puoi anche tentare di illustrare dove si trovano questi cluster con 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) Clustering entro una distanza di soglia con ST_ClusterWithin

Questa funzione aggregata è inclusa in PostGIS 2.2 e restituisce una matrice di GeometryCollections in cui tutti i componenti si trovano a una distanza l'uno dall'altro.

Ecco un esempio di utilizzo, in cui una distanza di 100,0 è la soglia che risulta in 5 diversi cluster:

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

Il cluster centrale più grande ha un raggio di circolo chiuso di 65,3 unità o circa 130, che è più grande della soglia. Questo perché le singole distanze tra le geometrie dei membri sono inferiori alla soglia, quindi la lega insieme come un cluster più grande.


2
Fantastico, queste modifiche aiuteranno per l'installazione :-) Tuttavia temo di non poter davvero usare l'estensione alla fine perché (se ho capito bene), ha bisogno di un numero magico di cluster hardcoded, che va bene con i dati statici prima puoi perfezionarlo in anticipo, ma non mi farebbe comodo per raggruppare set di dati arbitrari (a causa di vari filtri), ad esempio il grande spazio nel cluster a 10 punti sull'ultima immagine. Tuttavia, questo aiuterà anche altre persone perché (afaik), questo è l'unico esempio SQL esistente (ad eccezione di quelli della home page dell'estensione) per quell'estensione.
Wildpeaks

(ah mi hai risposto nello stesso momento in cui ho cancellato il commento precedente per riformularlo, scusa)
wildpeaks

7
Per il raggruppamento dei kmean è necessario specificare in anticipo il numero di cluster; Sono curioso di sapere se esistono algoritmi alternativi in ​​cui il numero di cluster non è richiesto.
Djj

1
La versione 1.1.0 è ora disponibile: api.pgxn.org/dist/kmeans/1.1.0/kmeans-1.1.0.zip
djq,

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

27

Ho scritto una funzione che calcola i cluster di funzionalità in base alla distanza tra loro e costruisce lo scafo convesso su queste funzionalità:

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;

Esempio di utilizzo di questa funzione:

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

'poi' - nome del layer, 'wkb_geometry' - nome della colonna della geometria, 'ogc_fid' - chiave primaria della tabella, 14000 - distanza del cluster.

Il risultato dell'utilizzo di questa funzione:

inserisci qui la descrizione dell'immagine


Grande! Potresti aggiungere un esempio di come utilizzare anche la tua funzione? Grazie!
underdark

1
Ho modificato un po 'di codice sorgente e ho aggiunto un esempio dell'uso della funzione.
drnextgis,

Ho appena provato a usarlo su Postgres 9.1 e la riga "FOR innr IN EXECUTE 'SELECT' || gid || ' Gid AS, "|| geom ||" AS geom FROM tmp WHERE dmn IS NULL 'LOOP "genera il seguente errore. Qualche idea ? ERRORE: funzione con valori impostati chiamata nel contesto che non può accettare un set
bitbox

Non sono sicuro di come utilizzare questo codice in PG (PostGIS n00b) nella mia tabella. dove potrei iniziare a capire questa sintassi? Ho una tabella con i dorsali e Lons che voglio a raggrupparsi
MGA

Prima di tutto devi costruire una geometrycolonna nella tua tabella, non conservare Lonlat separatamente e creare una colonna con valori univoci (ID).
drnextgis,

10

Finora, il più promettente che ho trovato è questa estensione per il clustering di K-significa come una funzione finestra: http://pgxn.org/dist/kmeans/

Tuttavia, non sono stato ancora in grado di installarlo correttamente.


Altrimenti, per il clustering di base della griglia, è possibile utilizzare 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

Integrazione della risposta @MikeT ...

Per MS Windows:

Requisiti:

Cosa farai:

  • Modifica il codice sorgente per esportare la funzione kmeans in una DLL.
  • Compilare il codice sorgente con il cl.execompilatore per generare una DLL con kmeansfunzione.
  • Inserisci la DLL generata nella cartella PostgreSQL \ lib.
  • Quindi puoi "creare" (link) l'UDF in PostgreSQL tramite il comando SQL.

passi:

  1. Scarica e installa / estrai i requisiti.
  2. Apri kmeans.cin qualsiasi editor:

    1. Dopo le #includerighe definire la macro DLLEXPORT con:

      #if defined(_WIN32)
          #define DLLEXPORT __declspec(dllexport)
      #else
         #define DLLEXPORT
      #endif
      
    2. Metti DLLEXPORTprima di ognuna di queste righe:

      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. Aprire la riga di comando di Visual C ++.

  4. Nella riga di comando:

    1. Vai a estratto kmeans-postgresql.
    2. Imposta il tuo POSTGRESPATH, il mio ad esempio è: SET POSTGRESPATH=C:\Program Files\PostgreSQL\9.5
    3. Correre

      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. Copia il kmeans.dllin%POSTGRESPATH%\lib

  6. Ora esegui il comando SQL nel tuo database per "CREARE" la funzione.

    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

Ecco un modo per visualizzare in QGIS il risultato della query PostGIS fornita in 2) in questa risposta

Poiché QGIS non gestisce né raccolte di geometrie né tipi di dati diversi nella stessa colonna di geometria, ho creato due livelli, uno per i cluster e uno per i punti raggruppati.

Innanzitutto per i cluster, hai solo bisogno di poligoni, altri risultati sono punti solitari:

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'

Quindi, per i punti raggruppati, è necessario trasformare i raccordi geometrici in multipunto:

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

Alcuni punti sono alle stesse coordinate, quindi l'etichetta potrebbe creare confusione.

Clustering in QGIS


2

Puoi usare la soluzione Kmeans più facilmente con il metodo ST_ClusterKMeans disponibile in postgis da 2.3 Esempio:

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;

Il riquadro di delimitazione delle funzioni viene utilizzato come geometria del cluster nell'esempio sopra. La prima immagine mostra le geometrie originali e la seconda è il risultato della selezione sopra.

Geometrie originali Cluster di funzionalità


1

Soluzione di clustering dal basso Ottieni un singolo cluster da una nuvola di punti con diametro massimo in postgis che non richiede query dinamiche.

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

e un tipo con ID cluster

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

Successivamente la funzione algoritmo

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

Uso:

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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.