21#include <unordered_map>
27using namespace Qt::StringLiterals;
31constexpr uint KMEANS_MAX_ITERATIONS = 1000;
33QString QgsKMeansClusteringAlgorithm::name()
const
35 return u
"kmeansclustering"_s;
38QString QgsKMeansClusteringAlgorithm::displayName()
const
40 return QObject::tr(
"K-means clustering" );
43QStringList QgsKMeansClusteringAlgorithm::tags()
const
45 return QObject::tr(
"clustering,clusters,kmeans,points" ).split(
',' );
48QString QgsKMeansClusteringAlgorithm::group()
const
50 return QObject::tr(
"Vector analysis" );
53QString QgsKMeansClusteringAlgorithm::groupId()
const
55 return u
"vectoranalysis"_s;
58void QgsKMeansClusteringAlgorithm::initAlgorithm(
const QVariantMap & )
63 QStringList initializationMethods;
64 initializationMethods << QObject::tr(
"Farthest points" ) << QObject::tr(
"K-means++" );
65 addParameter(
new QgsProcessingParameterEnum( u
"METHOD"_s, QObject::tr(
"Method" ), initializationMethods,
false, 0,
false ) );
67 auto fieldNameParam = std::make_unique<QgsProcessingParameterString>( u
"FIELD_NAME"_s, QObject::tr(
"Cluster field name" ), u
"CLUSTER_ID"_s );
69 addParameter( fieldNameParam.release() );
70 auto sizeFieldNameParam = std::make_unique<QgsProcessingParameterString>( u
"SIZE_FIELD_NAME"_s, QObject::tr(
"Cluster size field name" ), u
"CLUSTER_SIZE"_s );
72 addParameter( sizeFieldNameParam.release() );
77QString QgsKMeansClusteringAlgorithm::shortHelpString()
const
80 "This algorithm calculates the 2D distance based k-means cluster number for each input feature.\n\n"
81 "If input geometries are lines or polygons, the clustering is based on the centroid of the feature."
85QList<QgsAcademicReference> QgsKMeansClusteringAlgorithm::academicReferences()
const
88 createPresentation( { u
"Arthur, D."_s, u
"Vassilvitskii, S."_s }, 2007, u
"K-Means++: The Advantages of Careful Seeding"_s, u
"Proc. of the Annu. ACM-SIAM Symp. on Discrete Algorithms"_s, QString(), u
"8"_s );
91 { u
"Bhattacharya, A."_s, u
"Eube, J."_s, u
"Röglin, H."_s, u
"Schmidt, M."_s },
93 u
"Noisy, Greedy and Not So Greedy k-means++"_s,
94 u
"28th Annual European Symposium on Algorithms (ESA 2020)"_s,
95 u
"Leibniz International Proceedings in Informatics (LIPIcs)"_s,
99 return { ref1, ref2 };
102QString QgsKMeansClusteringAlgorithm::shortDescription()
const
104 return QObject::tr(
"Calculates the 2D distance based k-means cluster number for each input feature." );
107QgsKMeansClusteringAlgorithm *QgsKMeansClusteringAlgorithm::createInstance()
const
109 return new QgsKMeansClusteringAlgorithm();
114 QGS_MARK_ALGORITHM_SOURCE
116 std::unique_ptr<QgsProcessingFeatureSource> source( parameterAsSource( parameters, u
"INPUT"_s, context ) );
120 int k = parameterAsInt( parameters, u
"CLUSTERS"_s, context );
121 int initializationMethod = parameterAsInt( parameters, u
"METHOD"_s, context );
123 QgsFields outputFields = source->fields();
125 const QString clusterFieldName = parameterAsString( parameters, u
"FIELD_NAME"_s, context );
126 newFields.
append(
QgsField( clusterFieldName, QMetaType::Type::Int ) );
127 const QString clusterSizeFieldName = parameterAsString( parameters, u
"SIZE_FIELD_NAME"_s, context );
128 newFields.
append(
QgsField( clusterSizeFieldName, QMetaType::Type::Int ) );
132 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u
"OUTPUT"_s, context, dest, outputFields, source->wkbType(), source->sourceCrs() ) );
137 feedback->
pushInfo( QObject::tr(
"Collecting input points" ) );
138 const double step = source->featureCount() > 0 ? 50.0 /
static_cast< double >( source->featureCount() ) : 1;
141 int featureWithGeometryCount = 0;
144 std::vector<Feature> clusterFeatures;
146 QHash<QgsFeatureId, std::size_t> idToObj;
158 featureWithGeometryCount++;
174 idToObj[feat.
id()] = clusterFeatures.size();
175 clusterFeatures.emplace_back( Feature( point ) );
180 feedback->
reportError( QObject::tr(
"Number of geometries is less than the number of clusters requested, not all clusters will get data" ) );
186 feedback->
pushInfo( QObject::tr(
"Calculating clusters" ) );
189 std::vector<QgsPointXY> centers( k );
190 switch ( initializationMethod )
193 initClustersFarthestPoints( clusterFeatures, centers, k, feedback );
196 initClustersPlusPlus( clusterFeatures, centers, k, feedback );
201 calculateKMeans( clusterFeatures, centers, k, feedback );
205 std::unordered_map<int, int> clusterSize;
206 for (
auto it = idToObj.constBegin(); it != idToObj.constEnd(); ++it )
208 clusterSize[clusterFeatures[it.value()].cluster]++;
211 features = source->getFeatures();
223 const auto obj = idToObj.find( feat.
id() );
226 attr << QVariant() << QVariant();
230 attr << 0 << featureWithGeometryCount;
234 const int cluster = clusterFeatures[*obj].cluster;
235 attr << cluster << clusterSize[cluster];
248 outputs.insert( u
"OUTPUT"_s, dest );
254void QgsKMeansClusteringAlgorithm::initClustersFarthestPoints( std::vector<Feature> &points, std::vector<QgsPointXY> ¢ers,
const int k,
QgsProcessingFeedback *feedback )
256 const std::size_t n = points.size();
262 for (
int i = 0; i < k; i++ )
263 centers[i] = points[0].point;
267 std::size_t duplicateCount = 1;
271 double distanceP1 = 0;
272 double distanceP2 = 0;
273 double maxDistance = -1;
274 for ( std::size_t i = 1; i < n; i++ )
276 distanceP1 = points[i].point.sqrDist( points[p1].point );
277 distanceP2 = points[i].point.sqrDist( points[p2].point );
280 if ( ( distanceP1 > maxDistance ) || ( distanceP2 > maxDistance ) )
282 maxDistance = std::max( distanceP1, distanceP2 );
283 if ( distanceP1 > distanceP2 )
294 if ( feedback && duplicateCount > 1 )
296 feedback->
pushWarning( QObject::tr(
"There are at least %n duplicate input(s), the number of output clusters may be less than was requested",
nullptr,
static_cast< int >( duplicateCount ) ) );
303 centers[0] = points[p1].point;
304 centers[1] = points[p2].point;
309 std::vector<double> distances( n );
312 for ( std::size_t j = 0; j < n; j++ )
314 distances[j] = points[j].point.sqrDist( centers[0] );
320 for (
int i = 2; i < k; i++ )
322 std::size_t candidateCenter = 0;
323 double maxDistance = std::numeric_limits<double>::lowest();
326 for ( std::size_t j = 0; j < n; j++ )
329 if ( distances[j] < 0 )
333 distances[j] = std::min( points[j].point.sqrDist( centers[i - 1] ), distances[j] );
336 if ( distances[j] > maxDistance )
339 maxDistance = distances[j];
344 Q_ASSERT( maxDistance >= 0 );
347 distances[candidateCenter] = -1;
349 centers[i] = points[candidateCenter].point;
354void QgsKMeansClusteringAlgorithm::initClustersPlusPlus( std::vector<Feature> &points, std::vector<QgsPointXY> ¢ers,
const int k,
QgsProcessingFeedback *feedback )
356 const std::size_t n = points.size();
362 for (
int i = 0; i < k; i++ )
363 centers[i] = points[0].point;
368 std::random_device rd;
369 std::mt19937 gen( rd() );
370 std::uniform_int_distribution<size_t> distrib( 0, n - 1 );
372 std::size_t p1 = distrib( gen );
373 centers[0] = points[p1].point;
376 std::vector<double> distances( n );
377 double totalError = 0;
378 std::size_t duplicateCount = 1;
379 for (
size_t i = 0; i < n; i++ )
381 double distance = points[i].point.sqrDist( centers[0] );
382 distances[i] = distance;
383 totalError += distance;
389 if ( feedback && duplicateCount > 1 )
391 feedback->
pushWarning( QObject::tr(
"There are at least %n duplicate input(s), the number of output clusters may be less than was requested",
nullptr,
static_cast< int >( duplicateCount ) ) );
399 unsigned int numCandidateCenters = 2 +
static_cast< int >( std::floor( std::log( k ) ) );
400 std::vector<double> randomNumbers( numCandidateCenters );
401 std::vector<size_t> candidateCenters( numCandidateCenters );
403 std::uniform_real_distribution<double> dis( 0.0, 1.0 );
404 for (
int i = 1; i < k; i++ )
407 for (
unsigned int j = 0; j < numCandidateCenters; j++ )
409 randomNumbers[j] = dis( gen ) * totalError;
413 std::vector<double> cumSum = distances;
414 for (
size_t j = 1; j < n; j++ )
416 cumSum[j] += cumSum[j - 1];
420 for (
unsigned int j = 0; j < numCandidateCenters; j++ )
425 while ( low <= high )
427 size_t mid = low + ( high - low ) / 2;
428 if ( cumSum[mid] < randomNumbers[j] )
446 candidateCenters[j] = low;
449 std::vector<std::vector<double>> distancesCandidateCenters( numCandidateCenters, std::vector<double>( n ) );
453 double currentError = 0;
454 double lowestError = std::numeric_limits<double>::max();
455 unsigned int bestCandidateIndex = 0;
456 for (
unsigned int j = 0; j < numCandidateCenters; j++ )
458 for (
size_t z = 0; z < n; z++ )
461 double distance = points[candidateCenters[j]].point.sqrDist( points[z].point );
463 if ( distance > distances[z] )
465 distance = distances[z];
467 distancesCandidateCenters[j][z] = distance;
468 currentError += distance;
470 if ( lowestError > currentError )
472 lowestError = currentError;
473 bestCandidateIndex = j;
478 for (
size_t j = 0; j < n; j++ )
480 distances[j] = distancesCandidateCenters[bestCandidateIndex][j];
483 centers[i] = points[candidateCenters[bestCandidateIndex]].point;
485 totalError = lowestError;
491void QgsKMeansClusteringAlgorithm::calculateKMeans( std::vector<QgsKMeansClusteringAlgorithm::Feature> &objs, std::vector<QgsPointXY> ¢ers,
int k,
QgsProcessingFeedback *feedback )
493 int converged =
false;
494 bool changed =
false;
497 std::vector<uint> weights( k );
500 for ( i = 0; i < KMEANS_MAX_ITERATIONS && !converged; i++ )
505 findNearest( objs, centers, k, changed );
506 updateMeans( objs, centers, weights, k );
507 converged = !changed;
510 if ( !converged && feedback )
511 feedback->
reportError( QObject::tr(
"Clustering did not converge after %n iteration(s)",
nullptr,
static_cast<int>( i ) ) );
513 feedback->
pushInfo( QObject::tr(
"Clustering converged after %n iteration(s)",
nullptr,
static_cast<int>( i ) ) );
518void QgsKMeansClusteringAlgorithm::findNearest( std::vector<QgsKMeansClusteringAlgorithm::Feature> &points,
const std::vector<QgsPointXY> ¢ers,
const int k,
bool &changed )
521 const std::size_t n = points.size();
522 for ( std::size_t i = 0; i < n; i++ )
524 Feature &point = points[i];
527 double currentDistance = point.point.sqrDist( centers[0] );
528 int currentCluster = 0;
531 for (
int cluster = 1; cluster < k; cluster++ )
533 const double distance = point.point.sqrDist( centers[cluster] );
534 if ( distance < currentDistance )
536 currentDistance = distance;
537 currentCluster = cluster;
542 if ( point.cluster != currentCluster )
545 point.cluster = currentCluster;
552void QgsKMeansClusteringAlgorithm::updateMeans(
const std::vector<Feature> &points, std::vector<QgsPointXY> ¢ers, std::vector<uint> &weights,
const int k )
554 const uint n = points.size();
555 std::fill( weights.begin(), weights.end(), 0 );
556 for (
int i = 0; i < k; i++ )
558 centers[i].setX( 0.0 );
559 centers[i].setY( 0.0 );
561 for ( uint i = 0; i < n; i++ )
563 const int cluster = points[i].cluster;
564 centers[cluster] +=
QgsVector( points[i].point.x(), points[i].point.y() );
565 weights[cluster] += 1;
567 for (
int i = 0; i < k; i++ )
569 centers[i] /= weights[i];
@ VectorAnyGeometry
Any vector layer with geometry.
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Encapsulates an academic reference and formats it according to style guidelines.
static QgsAcademicReference createPresentation(const QStringList &authors, int year, const QString &title, const QString &meeting, const QString &publisher=QString(), const QString &pages=QString())
Creates a conference paper or presentation reference.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
bool hasGeometry() const
Returns true if the feature has an associated geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
void setProgress(double progress)
Sets the current progress for the feedback object.
Encapsulate a field in an attribute table or data source.
Container of fields for a vector layer.
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
A geometry is the spatial representation of a feature.
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
QgsGeometry centroid() const
Returns the center of mass of a geometry.
Qgis::WkbType wkbType() const
Returns type of the geometry as a WKB type (point / linestring / polygon etc.).
Contains information about the context in which a processing algorithm is executed.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
void featureAddedToSink(const QString &output)
Reports that a feature was added to the the sink associated with the specified algorithm output.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
virtual void pushWarning(const QString &warning)
Pushes a warning informational message from the algorithm.
void featureSinkFinalized(const QString &output)
Reports that a feature sink has been finalized.
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
An enum based parameter for processing algorithms, allowing for selection from predefined values.
A feature sink output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
A numeric parameter for processing algorithms.
static QgsFields combineFields(const QgsFields &fieldsA, const QgsFields &fieldsB, const QString &fieldsBPrefix=QString())
Combines two field lists, avoiding duplicate field names (in a case-insensitive manner).
Represent a 2-dimensional vector.
static Qgis::WkbType flatType(Qgis::WkbType type)
Returns the flat type for a WKB type.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
T qgsgeometry_cast(QgsAbstractGeometry *geom)