Clustering spatial avec PostGIS?

97

Je cherche un algorithme de clustering spatial pour l'utiliser dans la base de données compatible PostGIS pour les entités ponctuelles. Je vais écrire la fonction plpgsql qui prend la distance entre les points du même cluster en entrée. A la sortie, la fonction retourne un tableau de clusters. La solution la plus évidente consiste à créer des zones tampons à une distance spécifiée autour de l'entité et à rechercher des entités dans cette mémoire tampon. Si de telles fonctionnalités existent, continuez à créer un tampon autour d'eux, etc. Si de telles fonctionnalités n'existent pas, cela signifie que la construction du cluster est terminée. Peut-être y at-il des solutions intelligentes?

drnextgis
la source
4
Il existe une grande variété de méthodes de classification en raison de la nature différente des données et des objectifs différents de la classification. Pour obtenir une vue d'ensemble de ce qui existe et pour savoir facilement ce que font les autres pour regrouper des matrices de distance, consultez le site CV @ SE . En fait, "choisir la méthode de regroupement" est presque une copie exacte de la vôtre et donne de bonnes réponses.
whuber
8
+1 à la question parce que trouver un exemple réel avec PostGIS SQL plutôt que des liens vers des algorithmes est une mission impossible pour autre chose qu'un clustering de grille de base, en particulier pour des groupements plus exotiques comme MCL
wildpeaks

Réponses:

112

Il existe au moins deux bonnes méthodes de clustering pour PostGIS: k- moyen (via kmeans-postgresqlextension) ou clustering de géométries situées à une distance seuil (PostGIS 2.2).


1) k signifie aveckmeans-postgresql

Installation: Vous devez avoir PostgreSQL 8.4 ou supérieur sur un système hôte POSIX (je ne saurais pas par où commencer pour MS Windows). Si vous avez installé ceci à partir de packages, assurez-vous de disposer également des packages de développement (par exemple, postgresql-develpour CentOS). Télécharger et extraire:

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/

Avant de compiler, vous devez définir la USE_PGXS variable d'environnement (mon précédent message m'avait demandé de supprimer cette partie de l' Makefileoption qui n'était pas la meilleure des options). Une de ces deux commandes devrait fonctionner pour votre shell Unix:

# bash
export USE_PGXS=1
# csh
setenv USE_PGXS 1

Maintenant, construisez et installez l'extension:

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

(Remarque: j'ai aussi essayé cela avec Ubuntu 10.10, mais pas de chance, car le chemin d'accès pg_config --pgxsn'existe pas! C'est probablement un bogue d'emballage Ubuntu)

Utilisation / Exemple: Vous devriez avoir un tableau de points quelque part (j'ai dessiné un tas de points pseudo aléatoires dans QGIS). Voici un exemple avec ce que j'ai fait:

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;

le 5I fourni dans le deuxième argument de la kmeansfonction window est le K entier pour produire cinq clusters. Vous pouvez changer ceci en tout entier que vous voulez.

Ci-dessous se trouvent les 31 points pseudo aléatoires que j'ai dessinés et les cinq centroïdes avec l'étiquette indiquant le nombre dans chaque groupe. Cela a été créé en utilisant la requête SQL ci-dessus.

Kmeans


Vous pouvez également essayer d’illustrer où se trouvent ces clusters avec 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 dans une distance seuil avec ST_ClusterWithin

Cette fonction d'agrégat est incluse dans PostGIS 2.2 et renvoie un tableau de GeometryCollections où tous les composants sont distants l'un de l'autre.

Voici un exemple d'utilisation où une distance de 100,0 est le seuil qui résulte en 5 grappes différentes:

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

Le groupe central le plus grand a un cercle entourant le cercle de 65,3 unités, soit environ 130 unités, ce qui est supérieur au seuil. Cela s'explique par le fait que les distances individuelles entre les géométries des membres sont inférieures au seuil. Elles sont donc reliées comme un cluster plus grand.

Mike T
la source
2
Génial, ces modifications aideront à l’installation :-) Cependant, je crains de ne pas pouvoir utiliser cette extension à la fin car (si j’ai bien compris), il faut un nombre magique de clusters codés en dur, ce qui convient parfaitement avec des données statiques vous pouvez le régler à l'avance, mais ne me convient pas pour la mise en cluster de jeux de données arbitraires (dus à divers filtres), par exemple le grand espace dans le cluster à 10 points sur la dernière image. Cependant, cela aidera également d'autres personnes car (autant que je sache), il s'agit du seul exemple SQL existant (à l'exception des doublures sur la page d'accueil de l'extension) de cette extension.
paroles sauvages
(ah vous avez répondu en même temps, j'ai supprimé le commentaire précédent pour le reformuler, désolé)
wildpeaks
7
Pour le groupage kméen, vous devez spécifier le nombre de clusters à l'avance. Je suis curieux de savoir s'il existe d'autres algorithmes pour lesquels le nombre de clusters n'est pas requis.
djq
1
La version 1.1.0 est maintenant disponible: api.pgxn.org/dist/kmeans/1.1.0/kmeans-1.1.0.zip
djq
1
@ maxd no. Soit A = πr², alors r = √ (A / π).
Mike T
27

J'ai écrit une fonction qui calcule des groupes de caractéristiques en fonction de la distance qui les sépare et construit une coque convexe sur ces caractéristiques:

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;

Exemple d'utilisation de cette fonction:

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

'poi' - nom de la couche, 'wkb_geometry' - nom de la colonne de géométrie, 'ogc_fid' - clé primaire de la table, 14000 - distance du cluster.

Le résultat de l'utilisation de cette fonction:

entrez la description de l'image ici

drnextgis
la source
Génial! Pourriez-vous ajouter un exemple d'utilisation de votre fonction? Merci!
underdark
1
J'ai modifié un peu de code source et ajouté un exemple d'utilisation de la fonction.
drnextgis
Je viens d’essayer d’utiliser ceci sur postgres 9.1 et la ligne "FOR innr IN EXECUTE 'SELECT' || gid || ' AS gid, '|| geom ||' AS geom FROM tmp Where dmn IS NULL 'LOOP "génère l'erreur suivante. Des idées ? ERREUR: fonction définie par une valeur appelée dans un contexte qui ne peut accepter un ensemble
bitbox
Je ne sais pas comment utiliser ce code dans PG (PostGIS n00b) dans mon tableau. Où pourrais-je commencer à comprendre cette syntaxe? J'ai une table avec des lats et des lons que je veux regrouper
mga
Tout d'abord, vous devez créer une geometrycolonne dans votre table, ne pas stocker lonlat séparément ni créer une colonne avec des valeurs uniques (ID).
drnextgis
10

Jusqu'ici, le plus prometteur que j'ai trouvé est cette extension pour le clustering K-means en tant que fonction de fenêtre: http://pgxn.org/dist/kmeans/

Cependant, je n'ai pas encore réussi à l'installer.


Sinon, vous pouvez utiliser SnapToGrid pour la mise en grappe de base .

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
;
énigmes sauvages
la source
2

Complétant la réponse @MikeT ...

Pour MS Windows:

Exigences:

Que vas tu faire:

  • Tweak le code source pour exporter la fonction kmeans à une DLL.
  • Compilez le code source avec le cl.execompilateur pour générer une DLL avec une kmeansfonction.
  • Placez la DLL générée dans le dossier PostgreSQL \ lib.
  • Ensuite, vous pouvez "créer" (lier) l'UDF dans PostgreSQL via la commande SQL.

Pas:

  1. Télécharger et installer / extraire les exigences.
  2. Ouvrez le kmeans.cdans n'importe quel éditeur:

    1. Après les #includelignes, définissez la macro DLLEXPORT avec:

      #if defined(_WIN32)
          #define DLLEXPORT __declspec(dllexport)
      #else
         #define DLLEXPORT
      #endif
    2. Mettez DLLEXPORTavant chacune de ces lignes:

      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. Ouvrez la ligne de commande Visual C ++.

  4. En ligne de commande:

    1. Aller à la extrait kmeans-postgresql.
    2. Définissez votre POSTGRESPATH, le mien par exemple est: SET POSTGRESPATH=C:\Program Files\PostgreSQL\9.5
    3. Courir

      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. Copier le kmeans.dllà%POSTGRESPATH%\lib

  6. Exécutez maintenant la commande SQL dans votre base de données pour "CREER" la fonction.

    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;
Caiohamamura
la source
2

Voici un moyen d’afficher dans QGIS le résultat de la requête PostGIS donnée en 2) dans cette réponse

Comme QGIS ne gère ni les collectes de géométrie ni les types de données différents dans la même colonne de géométrie, j'ai créé deux couches, une pour les clusters et une pour les points en cluster.

Tout d'abord pour les clusters, vous n'avez besoin que de polygones, les autres résultats sont des points isolés:

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'

Ensuite, pour les points en cluster, vous devez transformer les collectes de géométries en points multiples:

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

Certains points sont aux mêmes coordonnées, de sorte que l'étiquette pourrait être source de confusion.

Regroupement dans QGIS

Nicolas Boisteault
la source
2

Vous pouvez utiliser la solution Kmeans plus facilement avec la méthode ST_ClusterKMeans disponible dans postgis à partir de 2.3 Exemple:

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;

Le cadre de sélection des entités est utilisé comme géométrie de cluster dans l'exemple ci-dessus. La première image montre les géométries d'origine et la seconde est le résultat de la sélection ci-dessus.

Géométries d'origine Clusters de fonctionnalités

Volda
la source
1

Solution de clustering ascendante à partir de Obtenir un cluster unique à partir d'un nuage de points de diamètre maximum dans Postgis, sans aucune requête dynamique.

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

et un type avec identifiant de cluster

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

Suivant la fonction de l'algorithme

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

Usage:

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;
raphael
la source